87 lines
3.8 KiB
Python
87 lines
3.8 KiB
Python
#!/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 / "muse" / "lifecycle" / "context" / "skills" / "准备任务上下文" / "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()
|