"""灵感③ 人物塑造续审契约测试:CharacterizationReview schema + characterization_spec。 advisory 单维——仅建议、不阻断验收(D2)。 契约测试——构造符合 schema 的 mock 响应,校验字段默认值降级、tier、只读(writes==())、 输出 schema 派生。人物塑造是 advisory:**不加入 REVIEW_RESERVED_NAMES**。不联网、无 DB。 """ from __future__ import annotations import pytest from pydantic import ValidationError from ww_agents import ( REVIEW_RESERVED_NAMES, AgentSpec, CharacterizationIssue, CharacterizationReview, characterization_spec, ) # ---- CharacterizationReview schema ---- def test_characterization_parses_mock_response() -> None: # Arrange:模拟网关 instructor 校验后的结构化产出(每条带逐字引用 + 置信度) mock = { "issues": [ { "character": "沈砚", "aspect": "动机一致性", "where": "第4段决斗前退缩", "quote": "他忽然转身就走,什么也没说。", "diagnosis": "与人物卡既定「护妹至死」动机断裂,无过渡", "suggestion": "让退缩扣回护妹动机(如察觉陷阱护妹先撤)", "confidence": 0.82, } ] } # Act review = CharacterizationReview.model_validate(mock) # Assert assert len(review.issues) == 1 issue = review.issues[0] assert issue.character == "沈砚" assert issue.aspect == "动机一致性" assert issue.quote == "他忽然转身就走,什么也没说。" assert issue.confidence == pytest.approx(0.82) def test_characterization_defaults_to_empty_issues() -> None: # 无人物卡 / 无问题 → 空 issues(默认值降级,仿 StyleDriftReview 韧性) review = CharacterizationReview() assert review.issues == [] def test_characterization_issue_all_fields_have_defaults() -> None: # 畸形/缺字段守韧性:全字段给默认值,空 dict 也能解析 issue = CharacterizationIssue.model_validate({}) assert issue.character == "" assert issue.aspect == "" assert issue.where == "" assert issue.quote == "" assert issue.diagnosis == "" assert issue.suggestion == "" assert issue.confidence == 0.0 def test_characterization_issue_partial_fields_ok() -> None: # 缺 confidence/quote 时仍解析(降级默认),不抛 issue = CharacterizationIssue.model_validate({"character": "阿黎", "diagnosis": "扁平"}) assert issue.character == "阿黎" assert issue.diagnosis == "扁平" assert issue.confidence == 0.0 # ---- characterization_spec 声明(analyst 档,advisory 单维)---- def test_characterization_spec_is_analyst_tier() -> None: # 不变量 #2:只声明档位,不写 model assert characterization_spec.tier == "analyst" assert characterization_spec.name == "characterization" def test_characterization_spec_is_read_only() -> None: # 不变量 #3:审查只读,唯一落库=collect 留痕 assert characterization_spec.writes == () def test_characterization_spec_reads_characters() -> None: # 以人物卡 motive/appearance 为客观锚点 assert characterization_spec.reads == ("characters",) def test_characterization_spec_output_schema() -> None: assert characterization_spec.output_schema is CharacterizationReview def test_characterization_spec_prompt_ignores_length_and_diction() -> None: # 对抗 LLM-judge 冗长/辞藻偏见:prompt 必须显式声明忽略文本长度与辞藻华丽度 prompt = characterization_spec.system_prompt assert "忽略文本长度与辞藻华丽度" in prompt def test_characterization_not_in_review_reserved_names() -> None: # advisory:不进受信保留名(不阻断验收、无 conflicts 通道) assert "characterization" not in REVIEW_RESERVED_NAMES def test_characterization_spec_is_agent_spec_and_immutable() -> None: assert isinstance(characterization_spec, AgentSpec) with pytest.raises(ValidationError): characterization_spec.tier = "writer"