feat(maf): add secure roundtable workflow runtime
This commit is contained in:
1 parent
e690475e6e
commit
bd4131b4e2
52 files changed
+10315
-96
No files matched your search
@@ -1,16 +1,16 @@
|
||||
"""Tests for the MAF roundtable persistence + service layer (阶段一骨架).
|
||||
"""Tests for the MAF roundtable persistence + service layer (?????).
|
||||
|
||||
Covers the Phase 1 exit criteria:
|
||||
- Session CRUD with participants + capability snapshot.
|
||||
- Run lifecycle (create / get / list / cancel).
|
||||
- Monotonic per-run event seq under concurrent appends (MAF-01-06).
|
||||
- Event 断线补拉 (list_events after_seq=N).
|
||||
- Event ???? (list_events after_seq=N).
|
||||
- Action idempotency-key uniqueness.
|
||||
- Approval decision transition + "already decided" guard.
|
||||
- User isolation (MAF-01-07) — non-owner gets None / 404, not 403.
|
||||
- User isolation (MAF-01-07) ??non-owner gets None / 404, not 403.
|
||||
- Admin diagnostic access bypass.
|
||||
- Feature flag (MAF-01-04) — writes raise MafDisabledError when off; reads open.
|
||||
- Run gating — running session rejects config edits.
|
||||
- Feature flag (MAF-01-04) ??writes raise MafDisabledError when off; reads open.
|
||||
- Run gating ??running session rejects config edits.
|
||||
- capability_snapshot frozen at creation.
|
||||
- MAF tables have no FK to legacy roundtable_* tables.
|
||||
"""
|
||||
@@ -21,12 +21,12 @@ import asyncio
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from maf_test_helpers import make_maf_service
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||
|
||||
from app.maf_roundtable.service import (
|
||||
MafConflictError,
|
||||
MafNotFoundError,
|
||||
MafRoundtableService,
|
||||
)
|
||||
from deerflow.persistence.base import Base
|
||||
from deerflow.persistence.maf_roundtable import make_maf_roundtable_store
|
||||
@@ -45,7 +45,7 @@ async def store(tmp_path):
|
||||
|
||||
|
||||
def _svc(store, *, enabled=True, admin_diagnostic_access=True):
|
||||
return MafRoundtableService(store, enabled=enabled, admin_diagnostic_access=admin_diagnostic_access)
|
||||
return make_maf_service(store, enabled=enabled, admin_diagnostic_access=admin_diagnostic_access)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -54,23 +54,26 @@ async def test_create_and_get_session_round_trips(store):
|
||||
created = await svc.create_session(
|
||||
user_id="u1",
|
||||
body={
|
||||
"title": "舆情会商",
|
||||
"task_text": "分析暑期文旅舆情",
|
||||
"title": "????",
|
||||
"task_text": "????????",
|
||||
"mode": "structured",
|
||||
"model_name": "glm-5",
|
||||
"max_rounds": 5,
|
||||
"write_policy": "confirm_high_risk",
|
||||
"capability_snapshot": {"agents": ["a1"]},
|
||||
"participants": [
|
||||
{"agent_id": "mgr", "display_name": "主持人", "role": "manager", "sort_order": 0},
|
||||
{"agent_id": "seat-a", "display_name": "席A", "role": "seat", "sort_order": 1},
|
||||
{"agent_id": "mgr", "display_name": "host", "role": "manager", "sort_order": 0},
|
||||
{"agent_id": "seat-a", "display_name": "?A", "role": "seat", "sort_order": 1},
|
||||
],
|
||||
},
|
||||
)
|
||||
assert created["user_id"] == "u1"
|
||||
assert created["status"] == "draft"
|
||||
assert created["mode"] == "structured"
|
||||
assert created["capability_snapshot"] == {"agents": ["a1"]}
|
||||
snap = created["capability_snapshot"]
|
||||
assert snap is not None
|
||||
assert snap.get("version") == 1
|
||||
assert isinstance(snap.get("agents"), list)
|
||||
assert len(snap["agents"]) == 2
|
||||
assert len(created["participants"]) == 2
|
||||
assert created["participants"][0]["agent_id"] == "mgr"
|
||||
|
||||
@@ -102,9 +105,9 @@ async def test_update_session_partial_and_participants_replace(store):
|
||||
updated = await svc.update_session(
|
||||
session_id=s["id"],
|
||||
user_id="u1",
|
||||
fields={"title": "新标题", "participants": [{"agent_id": "a2", "role": "seat"}]},
|
||||
fields={"title": "new-title", "participants": [{"agent_id": "a2", "role": "seat"}]},
|
||||
)
|
||||
assert updated["title"] == "新标题"
|
||||
assert updated["title"] == "new-title"
|
||||
assert updated["task_text"] == "orig"
|
||||
assert {p["agent_id"] for p in updated["participants"]} == {"a2"}
|
||||
|
||||
@@ -112,11 +115,12 @@ async def test_update_session_partial_and_participants_replace(store):
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_session_freezes_capability_snapshot(store):
|
||||
svc = _svc(store)
|
||||
s = await svc.create_session(user_id="u1", body={"task_text": "t", "capability_snapshot": {"v": 1}})
|
||||
s = await svc.create_session(user_id="u1", body={"task_text": "t"})
|
||||
original = s["capability_snapshot"]
|
||||
updated = await svc.update_session(
|
||||
session_id=s["id"], user_id="u1", fields={"capability_snapshot": {"v": 999}}
|
||||
)
|
||||
assert updated["capability_snapshot"] == {"v": 1}
|
||||
assert updated["capability_snapshot"] == original
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -130,7 +134,7 @@ async def test_delete_session_owner_scoped(store):
|
||||
await svc.get_session(session_id=s["id"], user_id="u1")
|
||||
|
||||
|
||||
# ── run lifecycle ──────────────────────────────────────────────────────────
|
||||
# ?? run lifecycle ??????????????????????????????????????????????????????????
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -175,7 +179,7 @@ async def test_cancel_run_marks_session_cancelled(store):
|
||||
assert refreshed["status"] == "cancelled"
|
||||
|
||||
|
||||
# ── events: monotonic seq + concurrent appends (MAF-01-06) ────────────────
|
||||
# ?? events: monotonic seq + concurrent appends (MAF-01-06) ????????????????
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -193,8 +197,8 @@ async def test_append_event_monotonic_seq(store):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_appends_produce_dense_seq_set(store):
|
||||
"""Many parallel appends must produce a dense 1..N seq set — no gaps, no
|
||||
collisions — exercising the MAX+1 retry on the (run_id, seq) unique
|
||||
"""Many parallel appends must produce a dense 1..N seq set ??no gaps, no
|
||||
collisions ??exercising the MAX+1 retry on the (run_id, seq) unique
|
||||
constraint (MAF-01-06)."""
|
||||
svc = _svc(store)
|
||||
s = await svc.create_session(user_id="u1", body={"task_text": "t"})
|
||||
@@ -218,7 +222,7 @@ async def test_concurrent_appends_produce_dense_seq_set(store):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_events_after_seq_pull(store):
|
||||
"""断线补拉: events with seq > after_seq in ascending order."""
|
||||
"""????: events with seq > after_seq in ascending order."""
|
||||
svc = _svc(store)
|
||||
s = await svc.create_session(user_id="u1", body={"task_text": "t"})
|
||||
run = await svc.create_run(session_id=s["id"], user_id="u1")
|
||||
@@ -229,7 +233,7 @@ async def test_list_events_after_seq_pull(store):
|
||||
assert latest == 5
|
||||
|
||||
|
||||
# ── actions: idempotency key ──────────────────────────────────────────────
|
||||
# ?? actions: idempotency key ??????????????????????????????????????????????
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -264,7 +268,7 @@ async def test_action_idempotency_key_unique(store):
|
||||
assert found["tool_name"] == "update_task"
|
||||
|
||||
|
||||
# ── approvals: decision transition ────────────────────────────────────────
|
||||
# ?? approvals: decision transition ????????????????????????????????????????
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -273,7 +277,7 @@ async def test_approval_decision_and_already_decided_guard(store):
|
||||
s = await svc.create_session(user_id="u1", body={"task_text": "t"})
|
||||
run = await svc.create_run(session_id=s["id"], user_id="u1")
|
||||
appr = await store.approvals.create_approval(
|
||||
{"session_id": s["id"], "run_id": run["id"], "summary": "高风险写入", "status": "pending"}
|
||||
{"session_id": s["id"], "run_id": run["id"], "summary": "approve write", "status": "pending"}
|
||||
)
|
||||
decided = await svc.decide_approval(approval_id=appr["id"], decision="approved", user_id="u1")
|
||||
assert decided["status"] == "approved"
|
||||
|
||||
Reference in new issue
Block a user