feat(maf): add secure roundtable workflow runtime

This commit is contained in:
董宏凯 committed 2026-08-01 17:51:53 +08:00
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"