muse-agent-example/tests/skills/assemble-context/test_assemble_writer_context.py
zizi c9f69d9d6d 治理: Skill 测试治理第一阶段——harness 控制平面 + 实现测试迁出运行时目录
范围(不含 design-story-foundation、docs/、humanization/README.md 等进行中改动):

1. 新增 harness/ 控制平面
   - skill_harness.py 静态审计:32 个运行时 Skill 的 frontmatter/manifest/文档污染,当前 0 问题
   - run_selected.py 选择性执行器:manifest 与磁盘一一对账、依赖阻断、
     空跑与 skip-only 失败关闭、AST 测试形状门
   - manifests/skills.json:32 个 Skill 的合同责任方与协作领域登记
   - manifests/test-inventory.json:81 个测试资产登记
   - specs/skill-testing.md 与 README.md:测试分层、证据边界与 harness 职责

2. 实现测试从 .claude/skills/*/scripts/ 迁至 tests/skills/<skill>/
   - 71 个测试文件迁移并修复项目根与临时目录运行导入
   - 数据库触发器测试宽泛异常收窄为 psycopg.errors.RaiseException
   - 抽取离线大测试拆出真实 PG smoke(默认阻断,不计入离线通过)
   - 抽取 presence 去重边界拆出独立测试:493 + 78 = 571 项检查不变

3. 运行时文档清理
   - 13 个 SKILL.md 移除自测/离线验证段落、测试命令与测试文件事实源表述,
     只保留运行时合同;业务运行合同、额度、授权与离线模式均保留

4. SoT 同步
   - AGENTS.md:新增 Skill 领域索引(7 个合同责任方分组,覆盖 32 个运行时 Skill)
   - 领域 07:测试入口改由 harness/manifests/ 登记,SKILL.md 不承载测试命令
   - humanization 覆盖矩阵:活动测试路径同步迁移

验证证据: harness 自测 15 项 + runner 自测 13 项通过;静态审计 32 Skill / 0 问题;
73 个非数据库测试通过;8 个集成条目中 6 个 PostgreSQL 项被依赖门明确阻断;
py_compile 与 git diff --check 通过。未连接 PostgreSQL、网络、真实模型或额度。

已知边界: 真正 skill_behavior_eval 仍为 0,尚未验证任何 Skill 自然语言行为;
evaluate-frozen-replay 的 raw 存储边界冲突留待单独治理。
2026-08-19 01:50:20 +08:00

602 lines
25 KiB
Python
Raw 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.

#!/usr/bin/env python3
"""事实证据与原文证据双线组装测试。"""
from __future__ import annotations
import copy
import hashlib
import pathlib
import sys
import unittest
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
SCRIPT_DIR = PROJECT_ROOT / ".claude" / "skills" / "assemble-context" / "scripts"
sys.path.insert(0, str(SCRIPT_DIR))
from assemble_writer_context import ( # noqa: E402
AssemblyError,
assemble_context,
build_context_allowlist_diff_receipt,
)
from retrieve_writer_sources import retrieve_writer_sources # noqa: E402
from writer_contract import retrieval_identity, validate_writer_context # noqa: E402
def digest(text: str) -> str:
"""生成测试证据的规范 SHA-256。"""
return "sha256:" + hashlib.sha256(text.encode("utf-8")).hexdigest()
def prose(chapter: int, block_id: int, text: str, *, recent: bool = False, purpose: str = "card_source") -> dict:
"""构造块级可引用原文。"""
return {
"evidenceId": f"prose:{chapter}:{block_id}",
"chapter": chapter,
"sourceRef": {
"sourceId": f"chapter:{chapter}:block:{block_id}",
"sourceVersion": f"chapter-{chapter}-block-{block_id}-v1",
"chapter": chapter,
"blockId": block_id,
"startCodePoint": 0,
"endCodePoint": len(text),
},
"contentSha256": digest(text),
"purpose": purpose,
"text": text,
"isRecentBaseline": recent,
}
def fact(fact_id: str, text: str, source_type: str = "formal_setting", risk: str = "medium") -> dict:
"""构造独立权威事实证据。"""
return {
"evidenceId": fact_id,
"fact": text,
"sourceType": source_type,
"sourceRef": {
"sourceId": f"{source_type}:{fact_id}",
"sourceVersion": f"{source_type}-v1",
},
"contentSha256": digest(text),
"riskLevel": risk,
}
def plan(run_id: str = "run-a") -> dict:
"""构造合法固定检索计划。"""
value = {
"planVersion": "writer-retrieval-plan-v1",
"runId": run_id,
"asOf": 6,
"queries": [],
"cardIndexVersion": "cards-v1",
"proseIndexVersion": "prose-v1",
"filters": {
"workId": 8,
"asOfChapter": 6,
"sourceStatus": "active",
"authorizationRequired": True,
},
"tieBreak": "score DESC, sourceVersion ASC, sourceId ASC, sourceOffset ASC",
"tokenBudget": {"maxContextChars": 50000},
}
value["planId"] = retrieval_identity(value)
return value
def fine_outline() -> dict:
"""构造含五类覆盖目标的细纲输入。"""
return {
"sourceRef": {"sourceId": "fine-outline:7", "sourceVersion": "outline-v3", "chapter": 7},
"hardConstraints": ["甲携剑抵达城门"],
"adjustableBeats": ["可调整过渡方式"],
"declaredNewFacts": [],
"entities": [{"id": "character:甲", "type": "character", "name": "甲"}],
"relations": [{"id": "relation:甲乙", "type": "character_relation", "name": "甲乙关系"}],
"items": [{"id": "item:剑", "type": "item", "name": "剑"}],
"locations": [{"id": "location:城门", "type": "location", "name": "城门"}],
"powerSystems": [{"id": "power:灵力", "type": "power_system", "name": "灵力"}],
}
def recent_chapters(last: int = 6) -> list[dict]:
"""构造第一章到冻结章的完整章节原文。"""
return [prose(chapter, 100 + chapter, f"第{chapter}章完整原文。") for chapter in range(1, last + 1)]
def base_kwargs() -> dict:
"""构造 assemble_context 的公共合法输入。"""
return {
"run_id": "run-a",
"attempt": 1,
"mode": "production",
"purpose": "production",
"quality_policy_version": "writer-production-v1",
"work_id": 8,
"target_chapter": 7,
"as_of": 6,
"source_version": "raw-file-v1:sha256:" + "a" * 64,
"authorization_snapshot": {
"snapshotId": "auth-1",
"allowedPurpose": "generation",
"verifiedAt": "2026-07-20T00:00:00Z",
},
"source_status": "active",
"retrieval_plan": plan(),
"retrieval_result": {
"cards": [],
"factEvidence": [],
"proseEvidence": [],
"indexHints": [],
"unverifiedIndexHints": [],
"manifest": {
"manifestVersion": "writer-retrieval-manifest-v1",
"manifestId": "sha256:" + "b" * 64,
"planId": plan()["planId"],
"sources": [],
"omittedSources": [],
},
},
"fine_outline": fine_outline(),
"narrative_state": {
"time": "当日",
"location": "城门",
"characterPositions": {"甲": "城外"},
"immediateSituation": "即将交战",
},
"recent_chapters": recent_chapters(),
"output_contract": {
"targetChars": 4000,
"minChars": 3600,
"maxChars": 4400,
"frontmatterRequired": False,
},
"token_budget": {"maxContextChars": 50000},
"generated_at": "2026-07-20T00:00:00Z",
}
class AssembleWriterContextTest(unittest.TestCase):
def test_previous_four_full_chapters_are_baseline(self):
result = assemble_context(**base_kwargs())
baseline = [item for item in result["context"]["proseEvidence"] if item["isRecentBaseline"]]
self.assertEqual([item["chapter"] for item in baseline], [3, 4, 5, 6])
self.assertTrue(all(item["purpose"] == "recent_full_chapter" for item in baseline))
validate_writer_context(result["context"])
def test_when_fewer_than_four_exist_start_at_chapter_one_without_duplicates(self):
kwargs = base_kwargs()
kwargs.update(
{
"target_chapter": 3,
"as_of": 2,
"recent_chapters": recent_chapters(2),
"retrieval_plan": {**plan(), "asOf": 2, "filters": {**plan()["filters"], "asOfChapter": 2}},
}
)
kwargs["retrieval_plan"]["planId"] = retrieval_identity({key: value for key, value in kwargs["retrieval_plan"].items() if key != "planId"})
kwargs["retrieval_result"]["manifest"]["planId"] = kwargs["retrieval_plan"]["planId"]
result = assemble_context(**kwargs)
chapters = [item["chapter"] for item in result["context"]["proseEvidence"]]
self.assertEqual(chapters, [1, 2])
def test_missing_chapter_in_required_continuous_baseline_fails_closed(self):
kwargs = base_kwargs()
kwargs["recent_chapters"] = [item for item in recent_chapters() if item["chapter"] != 4]
with self.assertRaises(AssemblyError):
assemble_context(**kwargs)
def test_diagnostic_arms_use_frozen_index_hints_without_bypassing_contract(self):
"""A/B/C 均由同一组装入口产生,B 只含提示,A/C 保留近章基线。"""
hint = {
"cardId": "card:甲",
"name": "甲",
"type": "character",
"content": "甲在冻结点仍位于城外",
"sourceId": "card-source:甲",
"sourceVersion": "cards-v1",
"asOf": 6,
}
contexts = {}
for arm, strategy in (
("A", "historical_prose_only"),
("B", "card_index_only"),
("C", "card_index_plus_prose"),
):
kwargs = base_kwargs()
kwargs.update(
{
"mode": "diagnostic_only",
"purpose": "evaluation",
"quality_policy_version": "writer-eval-v1",
"evidence_strategy": strategy,
}
)
kwargs["retrieval_result"]["indexHints"] = [copy.deepcopy(hint)]
contexts[arm] = assemble_context(**kwargs)["context"]
self.assertEqual(contexts["A"]["indexHints"], [])
self.assertEqual(contexts["A"]["factEvidence"], [])
self.assertEqual([item["chapter"] for item in contexts["A"]["proseEvidence"]], [3, 4, 5, 6])
self.assertEqual(contexts["B"]["proseEvidence"], [])
self.assertEqual(contexts["B"]["factEvidence"], [])
self.assertEqual(contexts["B"]["indexHints"], [hint])
self.assertEqual([item["chapter"] for item in contexts["C"]["proseEvidence"]], [3, 4, 5, 6])
self.assertEqual(contexts["C"]["indexHints"], [hint])
self.assertTrue(all(not context["acceptanceEligible"] for context in contexts.values()))
def test_evaluation_ac_writer_projection_hides_orchestration_and_emits_diff_receipt(self):
"""A/C 写手投影只能因等额原文及其来源和派生哈希不同。"""
hint = {
"cardId": "card:甲",
"name": "甲",
"type": "character",
"content": "甲的卡摘要不得进入 A/C 写手输入",
"sourceId": "card-source:甲",
"sourceVersion": "cards-v1",
"asOf": 6,
}
common = base_kwargs()
common["mode"] = "diagnostic_only"
common["purpose"] = "evaluation"
common["quality_policy_version"] = "writer-eval-v1"
common["token_budget"] = {"maxContextChars": 50000, "proseCharBudget": 6}
common["retrieval_result"]["factEvidence"] = [
fact("fact:shared", "甲必须守住城门", risk="high")
]
common["retrieval_result"]["indexHints"] = [hint]
common["retrieval_result"]["proseEvidence"] = [
{**prose(1, 201, "甲守住城门。"), "retrievalArm": "A"},
{**prose(2, 202, "甲战在城外。"), "retrievalArm": "C"},
]
contexts = {}
creative_inputs = {}
for arm, strategy in (
("A", "generic_prose_retrieval"),
("C", "card_indexed_prose_retrieval"),
):
kwargs = copy.deepcopy(common)
kwargs["evidence_strategy"] = strategy
assembled = assemble_context(**kwargs)
contexts[arm] = assembled["context"]
creative_inputs[arm] = assembled["writerCreativeInput"]
for creative_input in creative_inputs.values():
self.assertEqual(
set(creative_input),
{
"fineOutline", "narrativeState", "factConstraints",
"proseExcerpts", "patternReferences", "lengthContract",
"styleConstraints",
},
)
self.assertNotIn("retrievalPlan", creative_input)
self.assertNotIn("evidenceStrategy", creative_input)
self.assertNotIn("甲的卡摘要不得进入 A/C 写手输入", str(creative_input))
supplemental = [
item for item in creative_input["proseExcerpts"] if not item["isRecentBaseline"]
]
self.assertEqual(sum(len(item["text"]) for item in supplemental), 6)
receipt = build_context_allowlist_diff_receipt(
sample_id="sample-7",
context_a=contexts["A"],
context_c=contexts["C"],
prose_char_budget=6,
)
self.assertTrue(receipt["ok"], receipt)
self.assertEqual(receipt["schemaVersion"], "writer-creative-input-allowlist-diff-v2")
self.assertEqual(receipt["proseCharCount"], {"A": 6, "C": 6})
self.assertTrue(receipt["receiptSha256"].startswith("sha256:"))
def test_ac_allowlist_diff_rejects_non_prose_drift(self):
"""细纲等非原文字段漂移时,diff receipt 必须失败关闭。"""
kwargs = base_kwargs()
kwargs.update(
{
"mode": "diagnostic_only",
"purpose": "evaluation",
"quality_policy_version": "writer-eval-v1",
"evidence_strategy": "generic_prose_retrieval",
}
)
context_a = assemble_context(**kwargs)["context"]
context_c_kwargs = copy.deepcopy(kwargs)
context_c_kwargs["fine_outline"]["hardConstraints"] = ["被篡改的约束"]
context_c = assemble_context(**context_c_kwargs)["context"]
with self.assertRaisesRegex(AssemblyError, "allowlist"):
build_context_allowlist_diff_receipt(
sample_id="sample-7",
context_a=context_a,
context_c=context_c,
prose_char_budget=0,
)
def test_ac_exact_prose_budget_fails_when_either_arm_is_short(self):
"""A/C 任一臂历史原文不足预注册字符数时都不得缩小预算。"""
kwargs = base_kwargs()
kwargs.update(
{
"mode": "diagnostic_only",
"purpose": "evaluation",
"quality_policy_version": "writer-eval-v1",
"evidence_strategy": "generic_prose_retrieval",
"token_budget": {"maxContextChars": 50000, "proseCharBudget": 20},
}
)
kwargs["retrieval_result"]["proseEvidence"] = [
{**prose(1, 201, "只有六字。"), "retrievalArm": "A"}
]
with self.assertRaisesRegex(AssemblyError, "proseCharBudget"):
assemble_context(**kwargs)
def test_index_hint_strict_fields_and_freeze_are_enforced(self):
"""组装器不接受检索器夹带字段或越过 asOf 的提示。"""
base_hint = {
"cardId": "card:甲", "name": "甲", "type": "character",
"content": "冻结状态", "sourceId": "card-source:甲",
"sourceVersion": "cards-v1", "asOf": 6,
}
for mutation in ("unknown", "future"):
kwargs = base_kwargs()
kwargs.update(
{
"mode": "diagnostic_only",
"purpose": "diagnostic",
"quality_policy_version": "writer-eval-v1",
"evidence_strategy": "card_index_only",
}
)
hint = copy.deepcopy(base_hint)
if mutation == "unknown":
hint["score"] = 1.0
else:
hint["asOf"] = 7
kwargs["retrieval_result"]["indexHints"] = [hint]
with self.subTest(mutation=mutation), self.assertRaises(AssemblyError):
assemble_context(**kwargs)
def test_retrieve_to_assemble_keeps_all_cards_as_indexes_and_only_c_reads_card_sources(self):
"""真实串联证明卡索引与原文回读独立,A/B/C 只改变约定变量。"""
with_ref = {
"cardId": "card:甲",
"type": "character",
"name": "甲",
"score": 0.9,
"sourceVersion": "cards-v1",
"sourceId": "canonical-entity:甲",
"sourceOffset": 0,
"sourceRefs": [{
"sourceId": "chapter:2:block:202",
"sourceVersion": "chapter-2-block-202-v1",
"chapter": 2,
"blockId": 202,
"startCodePoint": 0,
"endCodePoint": 6,
}],
"milestones": [
{"chapter": 2, "fact": "甲在城门受伤"},
{"chapter": 8, "fact": "未来状态"},
],
"sourceKind": "canonical_entity",
"productionRetrievalEligible": True,
}
without_ref = {
**copy.deepcopy(with_ref),
"cardId": "card:乙",
"name": "乙",
"sourceId": "canonical-entity:乙",
"sourceRefs": [],
"milestones": [{"chapter": 4, "fact": "乙仍留守城内"}],
}
class CardRepository:
"""返回同时含有、缺少原文指针的冻结卡。"""
def search(self, retrieval_plan):
del retrieval_plan
return [copy.deepcopy(with_ref), copy.deepcopy(without_ref)]
class ProseRepository:
"""按卡指针返回补充历史原文。"""
def read_source_refs(self, *, work_id, as_of, source_refs):
self.call = (work_id, as_of, copy.deepcopy(source_refs))
return [{
"sourceRef": copy.deepcopy(source_refs[0]),
"chapter": 2,
"text": "甲在城门受伤。",
"purpose": "card_source",
}]
kwargs = base_kwargs()
retrieval_result = retrieve_writer_sources(
plan=kwargs["retrieval_plan"],
card_repository=CardRepository(),
prose_repository=ProseRepository(),
)
contexts = {}
for arm, strategy in (
("A", "historical_prose_only"),
("B", "card_index_only"),
("C", "card_index_plus_prose"),
):
arm_kwargs = copy.deepcopy(kwargs)
arm_kwargs.update({
"mode": "diagnostic_only",
"purpose": "evaluation",
"quality_policy_version": "writer-eval-v1",
"evidence_strategy": strategy,
"retrieval_result": copy.deepcopy(retrieval_result),
})
contexts[arm] = assemble_context(**arm_kwargs)["context"]
expected_cards = {"card:甲", "card:乙"}
self.assertEqual(contexts["A"]["indexHints"], [])
self.assertEqual([item["chapter"] for item in contexts["A"]["proseEvidence"]], [3, 4, 5, 6])
self.assertEqual({item["cardId"] for item in contexts["B"]["indexHints"]}, expected_cards)
self.assertEqual(contexts["B"]["proseEvidence"], [])
self.assertEqual({item["cardId"] for item in contexts["C"]["indexHints"]}, expected_cards)
card_manifest_sources = [
item
for item in contexts["C"]["retrievalManifest"]["sources"]
if item.get("sourceType") == "diagnostic_card_index"
]
self.assertEqual(
{item["sourceId"] for item in card_manifest_sources},
{"canonical-entity:甲", "canonical-entity:乙"},
)
self.assertEqual(
[item["chapter"] for item in contexts["C"]["proseEvidence"]],
[3, 4, 5, 6, 2],
)
self.assertIn(
{"sourceId": "canonical-entity:乙", "reason": "missing_source_refs"},
contexts["C"]["omittedSources"],
)
self.assertTrue(all(not context["factEvidence"] for context in contexts.values()))
self.assertTrue(all(not context["acceptanceEligible"] for context in contexts.values()))
def test_card_prose_is_deduplicated_and_stably_sorted_after_baseline(self):
kwargs = base_kwargs()
duplicate = prose(6, 106, "第6章完整原文。")
supplemental_b = prose(2, 202, "补充乙。")
supplemental_a = prose(1, 201, "补充甲。")
kwargs["retrieval_result"]["proseEvidence"] = [supplemental_b, duplicate, supplemental_a]
result = assemble_context(**kwargs)
evidence = result["context"]["proseEvidence"]
self.assertEqual(sum(item["sourceRef"]["sourceId"] == duplicate["sourceRef"]["sourceId"] for item in evidence), 1)
supplemental = [item for item in evidence if not item["isRecentBaseline"]]
self.assertEqual([item["sourceRef"]["sourceId"] for item in supplemental], ["chapter:1:block:201", "chapter:2:block:202"])
def test_fact_and_prose_evidence_are_separate_and_five_types_report_coverage(self):
kwargs = base_kwargs()
kwargs["retrieval_result"]["factEvidence"] = [
fact("fact:character", "甲保持警惕", risk="high"),
fact("fact:relation", "甲乙关系稳定"),
fact("fact:item", "剑仍在甲手中"),
fact("fact:location", "城门已经关闭"),
fact("fact:power", "灵力消耗受限"),
]
result = assemble_context(**kwargs)
context = result["context"]
self.assertTrue(all("fact" in item and "text" not in item for item in context["factEvidence"]))
self.assertTrue(all("text" in item and "fact" not in item for item in context["proseEvidence"]))
self.assertEqual(
{item["elementType"] for item in context["evidenceCoverage"]},
{"character", "character_relation", "item", "location", "power_system"},
)
self.assertTrue(all(item["status"] in {"supported", "style_gap"} for item in context["evidenceCoverage"]))
def test_same_snapshot_and_inputs_have_same_manifest_and_context_hash(self):
first = assemble_context(**base_kwargs())
second_kwargs = base_kwargs()
second_kwargs["run_id"] = "run-b"
second_kwargs["retrieval_plan"]["runId"] = "run-b"
second = assemble_context(**second_kwargs)
self.assertEqual(first["context"]["retrievalManifest"]["manifestId"], second["context"]["retrievalManifest"]["manifestId"])
self.assertEqual(first["context"]["contextSnapshot"]["contextSha256"], second["context"]["contextSnapshot"]["contextSha256"])
self.assertEqual(first["manifestMarkdown"], second["manifestMarkdown"])
def test_budget_keeps_hard_outline_recent_prose_and_high_risk_fact_with_trace(self):
kwargs = base_kwargs()
kwargs["retrieval_result"]["factEvidence"] = [
fact("fact:high", "甲的高风险身份事实" + "甲" * 300, risk="high"),
fact("fact:low", "低风险背景" + "乙" * 300, risk="low"),
]
kwargs["retrieval_result"]["proseEvidence"] = [prose(1, 900, "很早的补充原文" + "旧" * 500)]
kwargs["recent_chapters"] = [
prose(chapter, 100 + chapter, f"第{chapter}章" + "近" * 350)
for chapter in range(1, 7)
]
kwargs["token_budget"] = {"maxContextChars": 7500}
result = assemble_context(**kwargs)
context = result["context"]
self.assertIn("甲携剑抵达城门", context["fineOutline"]["hardConstraints"])
self.assertIn("fact:high", {item["evidenceId"] for item in context["factEvidence"]})
baseline = [item["chapter"] for item in context["proseEvidence"] if item["isRecentBaseline"]]
self.assertEqual(baseline, [3, 4, 5, 6])
self.assertLessEqual(context["tokenBudget"]["usedContextChars"], 7500)
self.assertTrue(context["omittedSources"])
self.assertIn("token_budget", {item["reason"] for item in context["omittedSources"]})
def test_budget_that_cannot_hold_four_full_chapters_fails_closed(self):
kwargs = base_kwargs()
kwargs["recent_chapters"] = [
prose(chapter, 100 + chapter, f"第{chapter}章" + "近" * 500)
for chapter in range(1, 7)
]
kwargs["token_budget"] = {"maxContextChars": 3000}
with self.assertRaisesRegex(AssemblyError, "连续前四章全文基线"):
assemble_context(**kwargs)
class StyleInjectionTest(unittest.TestCase):
"""style 真注入:规划期选定的文风约束进冻结上下文并投影给 writer。"""
def test_no_style_means_context_unchanged_and_projection_empty(self):
result = assemble_context(**base_kwargs())
# 未提供文风时整个键省略,上下文逐字节不变(不破坏既有冻结哈希)。
self.assertNotIn("styleConstraints", result["context"])
self.assertEqual(result["writerCreativeInput"]["styleConstraints"], [])
def test_style_constraints_freeze_and_project_to_writer(self):
kwargs = base_kwargs()
kwargs["style_constraints"] = ["禁用 仿佛/似乎/不禁/眼眸", "句式偏短,节奏紧"]
result = assemble_context(**kwargs)
self.assertEqual(
result["context"]["styleConstraints"],
["禁用 仿佛/似乎/不禁/眼眸", "句式偏短,节奏紧"],
)
self.assertEqual(
result["writerCreativeInput"]["styleConstraints"],
["禁用 仿佛/似乎/不禁/眼眸", "句式偏短,节奏紧"],
)
def test_blank_style_rules_are_dropped(self):
kwargs = base_kwargs()
kwargs["style_constraints"] = [" ", "", "冷峻克制"]
result = assemble_context(**kwargs)
self.assertEqual(result["context"]["styleConstraints"], ["冷峻克制"])
def test_humanization_contract_is_frozen_and_projected_only_as_writer_guidance(self):
kwargs = base_kwargs()
kwargs["humanization_contract"] = {
"schema_version": "ai-flavor-prevention-v2",
"work_ref": "work:8",
"built_from": {
"rule_library_version": "v-test",
"voice_ledger_sha256": "a" * 64,
},
"negative_constraints": [],
"positive_samples": [],
"protected_spans": [],
"blacklist": [],
"writer_constraints": ["避免空洞元话语;遇到对白先判断人物功能。"],
}
result = assemble_context(**kwargs)
self.assertTrue(any("避免空洞元话语" in item for item in result["context"]["styleConstraints"]))
self.assertEqual(result["context"]["humanizationProvenance"]["constraintCount"], 1)
self.assertTrue(any("避免空洞元话语" in item for item in result["writerCreativeInput"]["styleConstraints"]))
self.assertNotIn("humanizationProvenance", result["writerCreativeInput"])
validate_writer_context(result["context"])
if __name__ == "__main__":
unittest.main()