- 续审 Agent 声明(AgentSpec) + 结构化输出契约(ContinuityReview/Conflict 五类) - LangGraph 并行审子图(可扩四审) + collect 落 chapter_reviews 留痕 + review SSE(section/conflict) - 验收-side Repository:章节 accepted 版本晋升 + digest append-only + 审稿留痕/裁决 - API:review(SSE) + reviews 历史 + accept(单原子事务:晋升 version + 终稿 digest + 裁决留痕) - 冲突 gate:未决裁决拦截(CONFLICT_UNRESOLVED);digest 从终稿提炼(不变量#4) - 前端:审稿报告页 + 冲突就地标注 + 裁决(采纳/忽略/手改) + 未决禁验收 + 「本次将更新」清单 - M2 E2E:真实 DB + 多档位 mock 网关零 token 走通 写→审→裁决→验收→摘要入库 - 多 agent 协同台账(PROGRESS.md) + 共享记忆(memory/contracts·decisions·gotchas)
96 lines
3.1 KiB
Python
96 lines
3.1 KiB
Python
"""T2.1 续审契约测试:ContinuityReview schema + continuity_spec 声明(C6)。
|
||
|
||
契约测试——构造符合 schema 的 mock 响应,校验 type 枚举约束、conflicts 列表、
|
||
四审只读(writes==[])。不联网、无 DB。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import pytest
|
||
from pydantic import ValidationError
|
||
from ww_agents import AgentSpec, Conflict, ContinuityReview, continuity_spec
|
||
|
||
|
||
def test_continuity_review_parses_mock_response() -> None:
|
||
# Arrange:模拟网关 instructor 校验后的结构化产出
|
||
mock = {
|
||
"conflicts": [
|
||
{
|
||
"type": "性格漂移",
|
||
"where": "第 3 段,主角忽然示弱",
|
||
"refs": ["第 1 章人物卡:性格刚烈"],
|
||
"suggestion": "改为强硬回应,或补内心转折铺垫",
|
||
},
|
||
{
|
||
"type": "时间线倒错",
|
||
"where": "开篇提到三日后",
|
||
"refs": ["第 5 章 digest:事件发生在次日"],
|
||
"suggestion": "统一为次日",
|
||
},
|
||
]
|
||
}
|
||
|
||
# Act
|
||
review = ContinuityReview.model_validate(mock)
|
||
|
||
# Assert
|
||
assert len(review.conflicts) == 2
|
||
assert review.conflicts[0].type == "性格漂移"
|
||
assert review.conflicts[1].refs == ["第 5 章 digest:事件发生在次日"]
|
||
|
||
|
||
def test_continuity_review_allows_empty_conflicts() -> None:
|
||
review = ContinuityReview.model_validate({"conflicts": []})
|
||
assert review.conflicts == []
|
||
|
||
|
||
def test_continuity_review_defaults_to_empty_conflicts() -> None:
|
||
review = ContinuityReview()
|
||
assert review.conflicts == []
|
||
|
||
|
||
def test_conflict_type_rejects_unknown_value() -> None:
|
||
with pytest.raises(ValidationError):
|
||
Conflict.model_validate({"type": "排版错误", "where": "x", "refs": [], "suggestion": "y"})
|
||
|
||
|
||
def test_conflict_refs_default_empty() -> None:
|
||
conflict = Conflict.model_validate(
|
||
{"type": "设定违例", "where": "战斗场景", "suggestion": "删去违例能力"}
|
||
)
|
||
assert conflict.refs == []
|
||
|
||
|
||
def test_continuity_spec_is_analyst_tier() -> None:
|
||
assert continuity_spec.tier == "analyst"
|
||
assert continuity_spec.name == "continuity"
|
||
|
||
|
||
def test_continuity_spec_is_read_only() -> None:
|
||
# 不变量 #3:四审只读
|
||
assert continuity_spec.writes == []
|
||
|
||
|
||
def test_continuity_spec_declares_expected_reads() -> None:
|
||
assert continuity_spec.reads == ["chapter_digests", "characters", "world_entities"]
|
||
|
||
|
||
def test_continuity_spec_output_schema_is_continuity_review() -> None:
|
||
assert continuity_spec.output_schema is ContinuityReview
|
||
|
||
|
||
def test_continuity_spec_has_nonempty_system_prompt() -> None:
|
||
assert continuity_spec.system_prompt.strip()
|
||
|
||
|
||
def test_agent_spec_is_immutable() -> None:
|
||
# frozen:加载后不得改动
|
||
with pytest.raises(ValidationError):
|
||
continuity_spec.tier = "writer"
|
||
|
||
|
||
def test_agent_spec_writer_allows_none_output_schema() -> None:
|
||
writer = AgentSpec(name="writer", tier="writer", system_prompt="写")
|
||
assert writer.output_schema is None
|
||
assert writer.scope == "builtin"
|