- Conflict schema 增 original/replacement(可缺):可局部修复时审稿提议精确补丁 - 续审 prompt 指示填逐字 original + 改后 replacement,不可定位则留空 - orchestrator SSE conflict 事件 + apps/api ReviewConflictView 透传两字段 - 重生成 TS 客户端 schema.d.ts;memory/contracts.md 记录契约变更(向后兼容) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
121 lines
4.0 KiB
Python
121 lines
4.0 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_conflict_patch_fields_default_none() -> None:
|
||
# 一键采纳补丁字段(original/replacement)可缺:不可定位的冲突两者留空。
|
||
conflict = Conflict.model_validate(
|
||
{"type": "设定违例", "where": "战斗场景", "suggestion": "删去违例能力"}
|
||
)
|
||
assert conflict.original is None
|
||
assert conflict.replacement is None
|
||
|
||
|
||
def test_conflict_carries_patch_pair_when_localizable() -> None:
|
||
# 可局部修复:original 逐字原文最小句段 + replacement 改后文本,供前端一键 find-replace。
|
||
conflict = Conflict.model_validate(
|
||
{
|
||
"type": "设定违例",
|
||
"where": "第1段",
|
||
"refs": [],
|
||
"suggestion": "迈巴赫改为奔驰",
|
||
"original": "迈巴赫扬长而去的尾灯",
|
||
"replacement": "奔驰扬长而去的尾灯",
|
||
}
|
||
)
|
||
assert conflict.original == "迈巴赫扬长而去的尾灯"
|
||
assert conflict.replacement == "奔驰扬长而去的尾灯"
|
||
|
||
|
||
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"
|