"""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"