写→审链新增第五审「人物塑造」(灵感③/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 作客观锚点)。
116 lines
4.1 KiB
Python
116 lines
4.1 KiB
Python
"""灵感③ 人物塑造续审契约测试: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"
|