#!/usr/bin/env python3 """细纲统一合同回归:锁住「技能产出形」与「装配消费形」合一后的装配行为。 历史 bug:planning 技能曾只产 chapterGoal/keyEvents/entities/foreshadowing/... 这套 表达形字段,缺 writer 装配硬依赖的 sourceRef/hardConstraints/adjustableBeats/ declaredNewFacts,导致 _outline_contract 直接 AssemblyError——planner 产出装配不了。 统一合同(meta/schemas/fine_outline.yaml)把必填集定为两者并集后,按统一形状产出 必须能装配;缺任一 writer 必填字段必须失败关闭。 """ from __future__ import annotations import pathlib import sys import unittest PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] SCRIPT_DIR = PROJECT_ROOT / ".agent" / "skills" / "assemble-context" / "scripts" sys.path.insert(0, str(SCRIPT_DIR)) from assemble_writer_context import AssemblyError, _outline_contract # noqa: E402 def unified_outline() -> dict: """按 meta/schemas/fine_outline.yaml 必填集构造的统一形状细纲。""" return { "targetChapter": 3, "sourceRef": {"sourceId": "outline:volume_1", "sourceVersion": "v1"}, "chapterGoal": "茧出击后首次实战,吞食更强异种核心完成升级", "hardConstraints": ["衔接第2章结尾:茧自行撕开舱门出击"], "keyEvents": [ {"id": "event-1", "order": 1, "event": "搏杀中被压制", "participants": ["林深"], "trigger": "锁定更强异种", "resultDirection": "暴露升级前不足"}, ], "adjustableBeats": ["搏杀战术细节可调整"], "declaredNewFacts": [ {"factId": "fact-ch3-1", "text": "茧吞食更强核心即升级", "sourceRef": {"sourceId": "outline:volume_1", "sourceVersion": "v1"}}, ], "mustAppearEntities": ["林深"], "chapterEndHook": "深渊更深处回应了这次进食", # 推荐字段也在,但不影响装配子集 "foreshadowing": [{"action": "推进", "subject": "脑内声音", "evidence": "unknown"}], "unknowns": [], "assumptions": [], } class FineOutlineUnificationTest(unittest.TestCase): def test_unified_shape_assembles_and_extracts_writer_subset(self): result = _outline_contract(unified_outline()) # 装配只抽 writer 需要的四字段,表达形字段不倾倒给写手。 self.assertEqual( set(result), {"sourceRef", "hardConstraints", "adjustableBeats", "declaredNewFacts"}, ) self.assertEqual(result["hardConstraints"], ["衔接第2章结尾:茧自行撕开舱门出击"]) def test_legacy_skill_only_shape_fails_closed(self): # 旧技能形:只有表达形字段,缺 writer 四必填——必须装配失败(复现原 bug)。 legacy = { "targetChapter": 3, "chapterGoal": "本章戏剧任务", "keyEvents": [{"id": "e1", "order": 1, "event": "事件"}], "entities": [{"name": "林深", "type": "character", "role": "主角"}], "foreshadowing": [{"action": "埋", "subject": "伏笔", "evidence": "unknown"}], "stateChanges": ["状态变化"], "chapterEndHook": "钩子", "unknowns": [], "assumptions": [], } with self.assertRaises(AssemblyError): _outline_contract(legacy) def test_missing_any_writer_required_field_fails_closed(self): for missing in ("sourceRef", "hardConstraints", "adjustableBeats", "declaredNewFacts"): outline = unified_outline() del outline[missing] with self.assertRaises(AssemblyError, msg=f"缺 {missing} 应装配失败"): _outline_contract(outline) if __name__ == "__main__": unittest.main()