#!/usr/bin/env python3 """最终交付格式校验器的回归检查;不模拟或约束创作对话。""" import sys import tempfile import unittest from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[3] SCRIPT_DIR = PROJECT_ROOT / "muse/lifecycle/flow/skills/book/完善故事基础设定/scripts" sys.path.insert(0, str(SCRIPT_DIR)) from validate_candidates import validate_candidates # noqa: E402 DRAFT = """# 雨伞|作品探索 ## 根设定 作者希望保留雨天的兄妹关系。 ## 一、作品定位与核心命题 待定:先从人物行动中寻找中心。 ## 二、故事世界与社会系统 未探索:尚未讨论社会制度。 ## 三、角色与关系 哥哥每天擦一把从不用的伞,妹妹故意把它拿走。 ## 四、故事机制与关键实体 未探索:尚未决定是否有超自然力量。 ## 五、阶段与状态变化 未探索:只有当前这一幕。 ## 六、情节架构与全书大纲 未探索:兄妹争执后的事情还没想好。 ## 七、主题与读者体验弧线 暂想保留亲近中带着疑问的感觉。 ## 八、信息、悬念与伏笔管理 待定:哥哥为何留着伞。 ## 九、叙事执行与一致性 先从动作里表现关系。 ## 待决与取舍 保留争执这一幕,暂缓决定雨伞的来历。 """ class CandidateValidatorTest(unittest.TestCase): def setUp(self) -> None: self.temp_dir = tempfile.TemporaryDirectory() self.addCleanup(self.temp_dir.cleanup) self.root = Path(self.temp_dir.name) def codes(self, *contents: str) -> set[str]: paths = [] for index, content in enumerate(contents): path = self.root / f"draft-{index}.md" path.write_text(content, encoding="utf-8") paths.append(path) _, problems = validate_candidates(paths) return {problem.code for problem in problems} def test_one_partial_draft_without_ids_or_length_limits_passes(self) -> None: self.assertEqual(set(), self.codes(DRAFT)) def test_drafts_can_expand_different_subsets(self) -> None: expanded = DRAFT.replace( "待定:先从人物行动中寻找中心。", "### 1.1 作品形态与定位\n\n### 1.1.1 题材、载体与目标读者\n\n暂定现实题材。", ) self.assertEqual(set(), self.codes(DRAFT, expanded)) def test_unknown_missing_or_reordered_headings_fail(self) -> None: for content in ( DRAFT.replace("## 三、角色与关系", "## 三、人物"), DRAFT.replace("## 三、角色与关系\n", ""), DRAFT.replace("待定:先从人物行动中寻找中心。", "### 1.2 故事承诺与一句话梗概\n\n设想。\n\n### 1.1 作品形态与定位\n\n设想。"), ): with self.subTest(content=content): self.assertIn("TREE_MISMATCH", self.codes(content)) def test_empty_chapter_and_bare_placeholder_fail_but_explained_unknown_passes(self) -> None: self.assertIn("SECTION_EMPTY", self.codes(DRAFT.replace("未探索:尚未讨论社会制度。", ""))) self.assertIn("CANDIDATE_PLACEHOLDER", self.codes(DRAFT.replace("待定:哥哥为何留着伞。", "TODO"))) self.assertEqual(set(), self.codes(DRAFT)) def test_optional_ids_are_unique_but_have_no_frozen_bands(self) -> None: numbered = DRAFT.replace("哥哥每天", "- **S090|哥哥:** 哥哥每天").replace( "先从动作里表现关系。", "- **S003|表达:** 先从动作里表现关系。" ) self.assertEqual(set(), self.codes(numbered)) self.assertIn("SETTING_ID_DUPLICATE", self.codes(numbered.replace("S003", "S090"))) def test_missing_title_or_deep_heading_fails(self) -> None: self.assertIn("CANDIDATE_STRUCTURE", self.codes(DRAFT.replace("# 雨伞|作品探索\n", ""))) self.assertIn("CANDIDATE_STRUCTURE", self.codes(DRAFT + "\n#### 额外层级\n说明。")) def test_missing_file_is_reported(self) -> None: _, problems = validate_candidates([self.root / "missing.md"]) self.assertEqual(["CANDIDATE_FILE_UNREADABLE"], [problem.code for problem in problems]) if __name__ == "__main__": unittest.main()