Files
writer-work-flow/packages/agents/tests/test_characterization_spec.py
Yaojia Wang 72b3c81146 feat(review): 人物塑造 advisory 审(第五审,仅建议不阻断验收)
写→审链新增第五审「人物塑造」(灵感③/D2):advisory 单维——仅建议、不阻断
验收(不进 conflicts、不碰 assert_conflicts_resolved、不入 REVIEW_RESERVED_NAMES);
只做人物塑造、不做氛围。

- SPEC characterization_spec(analyst,reads=("characters",),writes=())+
  prompts/characterization.md(只读、显式忽略文本长度与辞藻华丽度、引用逐字片段+置信度)
- schema CharacterizationReview{issues[...]}(全字段默认值守解析韧性)+ 注册 catalog
- 计数三处 22→23 + regen prompt_hashes.json(仅加一条)
- DB chapter_reviews 加 characterization JSONB nullable 列(迁移 f6a7b8c9d0e1)
- 接线:graph REVIEW_SPECS 追加 · chain ReviewRecordRepo · collect(extract/record) ·
  review_repo(Protocol/Sql/_to_view/ReviewView) · sse(event/factory/section) · normalize 自动纳入
- 契约 ReviewHistoryItem 加 characterization(gen:api 已重生成)
- 前端 sse/history/useReviewStream + CharacterizationPanel(只展示无裁决 UI、置信度降序+低置信折叠)+ ReviewReport 挂面板
- 测试 test_characterization_does_not_block_accept + 计数/golden + collect/normalize + 前端 reducer/normalize/parse

守不变量 #2/#3/#9;依赖 ⑧(motive/appearance 作客观锚点)。
2026-07-06 17:08:38 +02:00

116 lines
4.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""灵感③ 人物塑造续审契约测试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"