✨ feat: WP-03 三界归真作用域与消息版本谱系(ADR-020 落地)
- 迁移 5:conversation.realm(默认真界,检查约束);message.realm 快照、canonized_at、parent_message_id 自引用与 variant_kind(origin/revision/candidate)
- 界域端点:GET conversation(realm/当前分支)与 POST realm(入境/出境显式切换,留审计——ADR-020 切换必须可见)
- 归真端点 POST /v1/messages/{id}/canonizations:scope 必须与消息界域一致(不一致 422);幂等(重复归真 200 原状态);境界归真产出 world.canonized Outbox 事件(世界知识候选入口,WP-04 消费);幻境归真仅标记试演记录
- 编辑版本 POST /v1/messages/{id}/revisions:新消息 variant=revision 引用原消息(markdown 修订同样经灵台引擎规范化),原消息永不修改
- 测试 44 项全绿(+8):界域快照、归真正/反/幂等、幻境试演、revision 原消息不变、跨域 401/403/404 fail-closed
This commit is contained in:
1 parent
37c53862c4
commit
603a683c14
6 files changed
+432
-6
No files matched your search
@@ -326,3 +326,136 @@ class TestPaginationAndBranches:
|
||||
f"/v1/conversations/{cid}/current-branch", json={"branch_id": bid}
|
||||
)
|
||||
assert resp.status_code in (401, 403) # 未带新会话 Cookie:401;带错 Cookie:拒绝
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("client")
|
||||
class TestRealmsAndCanonization:
|
||||
async def _make_message(self, client, headers, cid, key, text="hi") -> dict:
|
||||
resp = await client.post(
|
||||
f"/v1/conversations/{cid}/messages",
|
||||
json={"content": [{"type": "text", "text": text}], "start_run": False},
|
||||
headers={**headers, "Idempotency-Key": key},
|
||||
)
|
||||
assert resp.status_code == 201, resp.text
|
||||
return resp.json()["message"]
|
||||
|
||||
async def test_realm_switch_snapshots_into_new_messages(self, client: AsyncClient) -> None:
|
||||
headers = await login(client, "realm1@example.com")
|
||||
cid = await _create_conversation(client, headers)
|
||||
detail = await client.get(f"/v1/conversations/{cid}", headers=headers)
|
||||
assert detail.json()["realm"] == "reality" # 默认真界
|
||||
sw = await client.post(
|
||||
f"/v1/conversations/{cid}/realm", json={"realm": "simulation"}, headers=headers
|
||||
)
|
||||
assert sw.status_code == 200 and sw.json()["realm"] == "simulation"
|
||||
await self._make_message(client, headers, cid, "idem-sim1")
|
||||
await client.get(f"/v1/conversations/{cid}/messages", headers=headers)
|
||||
# (realm 字段在消息响应中暂未透出,以归真行为间接验证)
|
||||
# 回到真界
|
||||
await client.post(
|
||||
f"/v1/conversations/{cid}/realm", json={"realm": "reality"}, headers=headers
|
||||
)
|
||||
|
||||
async def test_canonize_reality_message(self, client: AsyncClient) -> None:
|
||||
headers = await login(client, "can1@example.com")
|
||||
cid = await _create_conversation(client, headers)
|
||||
msg = await self._make_message(client, headers, cid, "idem-can1")
|
||||
resp = await client.post(
|
||||
f"/v1/messages/{msg['id']}/canonizations",
|
||||
json={"scope": "reality"},
|
||||
headers=headers,
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
assert resp.json()["canonical_scope"] == "reality"
|
||||
assert resp.json()["canonized_at"] is not None
|
||||
|
||||
async def test_canonize_idempotent_replay(self, client: AsyncClient) -> None:
|
||||
headers = await login(client, "can2@example.com")
|
||||
cid = await _create_conversation(client, headers)
|
||||
msg = await self._make_message(client, headers, cid, "idem-can2")
|
||||
first = await client.post(
|
||||
f"/v1/messages/{msg['id']}/canonizations", json={"scope": "reality"}, headers=headers
|
||||
)
|
||||
second = await client.post(
|
||||
f"/v1/messages/{msg['id']}/canonizations", json={"scope": "reality"}, headers=headers
|
||||
)
|
||||
assert first.status_code == 201 and second.status_code == 200
|
||||
assert second.json()["canonized_at"] == first.json()["canonized_at"]
|
||||
|
||||
async def test_canonize_scope_mismatch_rejected(self, client: AsyncClient) -> None:
|
||||
"""真界消息不能归真到境界/幻境作用域(ADR-020 作用域一致)。"""
|
||||
headers = await login(client, "can3@example.com")
|
||||
cid = await _create_conversation(client, headers)
|
||||
msg = await self._make_message(client, headers, cid, "idem-can3")
|
||||
resp = await client.post(
|
||||
f"/v1/messages/{msg['id']}/canonizations", json={"scope": "world"}, headers=headers
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
|
||||
async def test_simulation_message_canonizes_as_rehearsal(self, client: AsyncClient) -> None:
|
||||
"""幻境消息归真仅标记试演记录(ADR-020)。"""
|
||||
headers = await login(client, "can4@example.com")
|
||||
cid = await _create_conversation(client, headers)
|
||||
await client.post(
|
||||
f"/v1/conversations/{cid}/realm", json={"realm": "simulation"}, headers=headers
|
||||
)
|
||||
msg = await self._make_message(client, headers, cid, "idem-can4")
|
||||
resp = await client.post(
|
||||
f"/v1/messages/{msg['id']}/canonizations", json={"scope": "simulation"}, headers=headers
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
|
||||
async def test_canonize_cross_domain_404(self, client: AsyncClient) -> None:
|
||||
alice = await login(client, "can5@example.com")
|
||||
cid = await _create_conversation(client, alice)
|
||||
msg = await self._make_message(client, alice, cid, "idem-can5")
|
||||
await login(client, "can5-evil@example.com")
|
||||
resp = await client.post(
|
||||
f"/v1/messages/{msg['id']}/canonizations", json={"scope": "reality"}
|
||||
)
|
||||
assert resp.status_code in (401, 403, 404) # 未携带新会话 Cookie/CSRF:拒绝
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("client")
|
||||
class TestRevisions:
|
||||
async def test_revision_keeps_original_intact(self, client: AsyncClient) -> None:
|
||||
|
||||
headers = await login(client, "rev1@example.com")
|
||||
cid = await _create_conversation(client, headers)
|
||||
original = None
|
||||
resp = await client.post(
|
||||
f"/v1/conversations/{cid}/messages",
|
||||
json={"content": [{"type": "text", "text": "原始内容"}], "start_run": False},
|
||||
headers={**headers, "Idempotency-Key": "idem-rev1"},
|
||||
)
|
||||
original = resp.json()["message"]
|
||||
rev = await client.post(
|
||||
f"/v1/messages/{original['id']}/revisions",
|
||||
json={"content": [{"type": "markdown", "text": "修订后\n```python\nprint(2)\n```"}]},
|
||||
headers=headers,
|
||||
)
|
||||
assert rev.status_code == 201, rev.text
|
||||
body = rev.json()
|
||||
assert body["parent_message_id"] == original["id"]
|
||||
assert body["variant_kind"] == "revision"
|
||||
# markdown 修订经引擎规范化为两块
|
||||
assert [b["type"] for b in body["content"]] == ["markdown", "code"]
|
||||
# 原消息内容不变:列表中两条,原消息文本仍是"原始内容"
|
||||
listing = await client.get(f"/v1/conversations/{cid}/messages", headers=headers)
|
||||
by_id = {m["id"]: m for m in listing.json()["data"]}
|
||||
assert by_id[original["id"]]["content"][0]["text"] == "原始内容"
|
||||
|
||||
async def test_revision_cross_domain_404(self, client: AsyncClient) -> None:
|
||||
alice = await login(client, "rev2@example.com")
|
||||
cid = await _create_conversation(client, alice)
|
||||
resp = await client.post(
|
||||
f"/v1/conversations/{cid}/messages",
|
||||
json={"content": [{"type": "text", "text": "x"}], "start_run": False},
|
||||
headers={**alice, "Idempotency-Key": "idem-rev2"},
|
||||
)
|
||||
mid = resp.json()["message"]["id"]
|
||||
await login(client, "rev2-evil@example.com")
|
||||
rev = await client.post(
|
||||
f"/v1/messages/{mid}/revisions", json={"content": [{"type": "text", "text": "篡改"}]}
|
||||
)
|
||||
assert rev.status_code in (401, 403, 404)
|
||||
Reference in new issue
Block a user