将角色与 Skill 从 .claude 迁入 .agent,移除 Claude CLI 运行时并接入固定 Opus 角色 profile、完整 schema、预算 deadline、raw 与回执证据链。 同步拆分 Skill 职责、复利 lesson、Gate 回放、Dashboard 人审入口、数据库登记和机械门禁;候选设计正文不包含在本提交中。
125 lines
4.6 KiB
Python
125 lines
4.6 KiB
Python
#!/usr/bin/env python3
|
||
"""前期设计候选校验器的离线测试:只覆盖机械门禁自身的正确性。"""
|
||
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
import tempfile
|
||
import unittest
|
||
|
||
|
||
PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
||
SCRIPT_DIR = PROJECT_ROOT / ".agent" / "skills" / "design-story-foundation" / "scripts"
|
||
sys.path.insert(0, str(SCRIPT_DIR))
|
||
|
||
from validate_candidates import ValidationConfig, validate_candidates, validate_root # noqa: E402
|
||
|
||
TREE = json.loads((SCRIPT_DIR / "candidate_tree.json").read_text(encoding="utf-8"))
|
||
BLOCK = "> 回答:本节回答什么。\n> 应写:本节写什么。\n> 不写:本节不写什么。\n> 关联:其他节。\n"
|
||
ITEM = "- **S{sid:03d}|条目:** 这是带条件、代价和剧情用途的完整设定内容。"
|
||
|
||
|
||
def chapter_bands() -> list[tuple[int, int]]:
|
||
"""按每章实际条目数切分号段,保证全局连续编号都落在本章号段内。"""
|
||
bands = []
|
||
cursor = 1
|
||
for chapter in TREE["chapters"]:
|
||
count = sum(len(section["units"]) for section in chapter["sections"])
|
||
bands.append((cursor, cursor + count - 1))
|
||
cursor += count
|
||
return bands
|
||
|
||
|
||
BANDS = chapter_bands()
|
||
|
||
|
||
def make_candidate() -> str:
|
||
parts = ["# 候选\n\n## 目录\n\n- 章节列表\n"]
|
||
counter = 1
|
||
for index, chapter in enumerate(TREE["chapters"]):
|
||
start, end = BANDS[index]
|
||
parts.append(f"## {chapter['heading']}\n\n<!-- S{start:03d}-S{end:03d} -->\n")
|
||
for section in chapter["sections"]:
|
||
parts.append(f"### {section['heading']}\n\n{BLOCK}")
|
||
for unit in section["units"]:
|
||
parts.append(f"### {unit}\n\n{BLOCK}")
|
||
parts.append(ITEM.format(sid=counter))
|
||
counter += 1
|
||
return "\n".join(parts)
|
||
|
||
|
||
class CandidateValidatorTest(unittest.TestCase):
|
||
def setUp(self) -> None:
|
||
self.temp_dir = tempfile.TemporaryDirectory()
|
||
self.root = Path(self.temp_dir.name)
|
||
self.config = ValidationConfig(min_candidates=2, min_item_chars=10)
|
||
|
||
def tearDown(self) -> None:
|
||
self.temp_dir.cleanup()
|
||
|
||
def write(self, name: str, content: str) -> Path:
|
||
path = self.root / name
|
||
path.write_text(content, encoding="utf-8")
|
||
return path
|
||
|
||
def codes(self, *contents: str) -> set[str]:
|
||
paths = [self.write(f"c{index}.md", content) for index, content in enumerate(contents)]
|
||
_, problems = validate_candidates(paths, self.config)
|
||
return {problem.code for problem in problems}
|
||
|
||
def test_two_same_shape_candidates_pass(self) -> None:
|
||
self.assertEqual(set(), self.codes(make_candidate(), make_candidate()))
|
||
|
||
def test_missing_unit_heading_fails(self) -> None:
|
||
broken = make_candidate().replace("### 1.1.3 同类作品中的差异\n", "")
|
||
self.assertIn("TREE_MISMATCH", self.codes(make_candidate(), broken))
|
||
|
||
def test_deep_heading_fails(self) -> None:
|
||
broken = make_candidate().replace(
|
||
"### 1.1.1 题材、载体与目标读者\n", "### 1.1.1 题材、载体与目标读者\n\n#### 细分\n"
|
||
)
|
||
self.assertIn("CANDIDATE_STRUCTURE", self.codes(make_candidate(), broken))
|
||
|
||
def test_duplicate_and_out_of_range_ids_fail(self) -> None:
|
||
broken = make_candidate().replace("S002|条目:", "S001|条目:").replace("S003|条目:", "S002|条目:")
|
||
codes = self.codes(make_candidate(), broken)
|
||
self.assertIn("SETTING_ID_DUPLICATE", codes)
|
||
self.assertIn("SETTING_ORDER", codes)
|
||
|
||
def test_placeholder_fails(self) -> None:
|
||
broken = make_candidate().replace("完整设定内容", "TODO")
|
||
self.assertIn("CANDIDATE_PLACEHOLDER", self.codes(make_candidate(), broken))
|
||
|
||
def test_root_contract_accepts_clean_items(self) -> None:
|
||
root_doc = self.write(
|
||
"root.md",
|
||
"""# 前期设计
|
||
|
||
## 一、根设定
|
||
|
||
- **作品前提:**主角刚结束高考,随后进入拥有星际机甲的陌生时代并寻找立足之地。
|
||
- **披露要求:**前三章只呈现当前危机和能力征兆,终局答案留到后续阶段逐步揭开。
|
||
|
||
## 二、定盘
|
||
""",
|
||
)
|
||
self.assertEqual([], validate_root(root_doc, self.config))
|
||
|
||
def test_root_rejects_process_language(self) -> None:
|
||
root_doc = self.write(
|
||
"root.md",
|
||
"""# 前期设计
|
||
|
||
## 一、根设定
|
||
|
||
- **代理要求:**Claude 子代理分别生成候选文档,再由主会话比较和选择最终方向。
|
||
|
||
## 二、定盘
|
||
""",
|
||
)
|
||
self.assertIn("ROOT_PROCESS_LEAK", {problem.code for problem in validate_root(root_doc, self.config)})
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|