from __future__ import annotations import sys import tempfile import unittest import unittest.mock from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "dev_scripts")) import harness # noqa: E402 from harness import ( # noqa: E402 CORE_DOCUMENT_REQUIREMENTS, CORE_PAGE_PATHS, REQUIRED_FILES, check_claude_code_entry, check_required_files, check_core_documents, check_agent_efficiency_rules, check_repository_readme, check_task_template, core_mapping_errors, missing_sections, ) from wiki_docs import load_config # noqa: E402 class CoreDocumentTests(unittest.TestCase): def test_current_core_documents_have_required_sections(self) -> None: errors: list[str] = [] check_core_documents(errors) self.assertEqual(errors, []) def test_missing_sections_reports_each_heading(self) -> None: missing = missing_sections("# 页面\n## 已有\n", ("## 已有", "## 缺少")) self.assertEqual(missing, ["## 缺少"]) def test_every_core_document_is_required(self) -> None: for path in CORE_DOCUMENT_REQUIREMENTS: self.assertIn(path, REQUIRED_FILES) def test_every_core_page_has_exact_mapping(self) -> None: config = load_config() mappings = {mapping.page: mapping.path for mapping in config.mappings} for page, path in CORE_PAGE_PATHS.items(): self.assertEqual(mappings.get(page), path) def test_task_archives_are_not_core_mappings(self) -> None: config = load_config() self.assertFalse( any(mapping.path.startswith("docs/task/") for mapping in config.mappings) ) def test_existing_project_adoption_guide_is_core_document(self) -> None: path = "docs/08-existing-project-adoption.md" self.assertEqual( CORE_PAGE_PATHS.get("Existing-Project-Adoption-Guide"), path, ) self.assertIn(path, CORE_DOCUMENT_REQUIREMENTS) self.assertIn("## 可复制 Agent 指令", CORE_DOCUMENT_REQUIREMENTS[path]) def test_product_requirements_overview_is_core_document(self) -> None: path = "docs/09-product-requirements-overview.md" self.assertEqual( CORE_PAGE_PATHS.get("Product-Requirements-Overview"), path, ) required = CORE_DOCUMENT_REQUIREMENTS[path] self.assertIn("## 当前需求索引", required) self.assertIn("## 原型与设计资产", required) self.assertIn("### 原型门禁", required) self.assertIn("### 线上原型与按需 HTML 快照", required) self.assertIn("### 原型确认记录", required) self.assertIn("## 更新时机", required) def test_workflow_requires_governance_modes_and_minimum_gates(self) -> None: required = CORE_DOCUMENT_REQUIREMENTS["docs/01-workflow.md"] self.assertIn("## 按治理模式选择最小门禁", required) self.assertIn("### 不可裁剪底线", required) self.assertIn("### 先判断是否需要工单", required) self.assertIn("### 再判断设计证据", required) self.assertIn("### 线上原型审核与按需导出", required) def test_workflow_requires_online_wiki_initialization_gate(self) -> None: workflow = CORE_DOCUMENT_REQUIREMENTS["docs/01-workflow.md"] setup = CORE_DOCUMENT_REQUIREMENTS[ "docs/07-new-project-documentation-setup.md" ] self.assertIn("## 新项目 Wiki 初始化门禁", workflow) self.assertIn("#### 在线创建与回读门禁", setup) def test_workflow_requires_issue_only_task_record(self) -> None: workflow = CORE_DOCUMENT_REQUIREMENTS["docs/01-workflow.md"] setup = CORE_DOCUMENT_REQUIREMENTS[ "docs/07-new-project-documentation-setup.md" ] self.assertIn("## 稳定文档与可选历史快照", workflow) self.assertIn("#### 工程基线裁剪", setup) def test_workflow_requires_gitea_mcp_and_minimal_issue_reading(self) -> None: workflow = CORE_DOCUMENT_REQUIREMENTS["docs/01-workflow.md"] self.assertIn("## Gitea 交互与工单最小读取", workflow) errors: list[str] = [] check_agent_efficiency_rules(errors) self.assertEqual(errors, []) def test_existing_project_adoption_requires_upgrade_process(self) -> None: required = CORE_DOCUMENT_REQUIREMENTS[ "docs/08-existing-project-adoption.md" ] self.assertIn("## 后续升级", required) self.assertIn("### 升级步骤", required) self.assertIn("### 可复制升级指令", required) def test_project_profile_requires_dev_harness_baseline(self) -> None: required = CORE_DOCUMENT_REQUIREMENTS["docs/00-project-profile.md"] self.assertIn("## 项目治理模式", required) self.assertIn("## DevHarness 来源与基线", required) def test_new_project_setup_requires_baseline_selection(self) -> None: required = CORE_DOCUMENT_REQUIREMENTS[ "docs/07-new-project-documentation-setup.md" ] self.assertIn("### 2. 选择建设基线", required) self.assertIn("#### 判断案例", required) self.assertIn("### 3. 识别子项目与交付单元", required) self.assertIn("#### 选择治理模式", required) self.assertIn("#### 需求总览启用条件", required) def test_local_development_requires_powershell_utf8_boundaries(self) -> None: required = CORE_DOCUMENT_REQUIREMENTS[ "docs/04-local-development-and-verification.md" ] self.assertIn("## Windows PowerShell 与 UTF-8", required) errors: list[str] = [] check_agent_efficiency_rules(errors) self.assertEqual(errors, []) def test_deployment_template_is_required_and_mapped(self) -> None: path = "docs/templates/deployment.md" self.assertIn(path, REQUIRED_FILES) config = load_config() mappings = {mapping.page: mapping.path for mapping in config.mappings} self.assertEqual(mappings.get("Deployment-Template"), path) # 部署页按项目需要创建,不强制每个项目产出实例页。 self.assertNotIn(path, CORE_DOCUMENT_REQUIREMENTS) def test_missing_deployment_template_is_reported(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) with unittest.mock.patch.object(harness, "ROOT", root): errors: list[str] = [] check_required_files(errors) self.assertTrue( any("docs/templates/deployment.md" in error for error in errors) ) def test_missing_or_wrong_core_mapping_is_reported(self) -> None: errors = core_mapping_errors({"Home": "docs/wrong.md"}) self.assertTrue(any("Home -> docs/README.md" in error for error in errors)) self.assertTrue( any("Architecture-and-Code-Map" in error for error in errors) ) class TaskTemplateTests(unittest.TestCase): def test_task_template_requires_document_impact(self) -> None: errors: list[str] = [] check_task_template(errors) self.assertEqual(errors, []) def test_task_template_requires_delivery_document_impact(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) template = root / ".gitea" / "issue_template" / "task.md" template.parent.mkdir(parents=True) template.write_text( "## 依赖与并行\n" "- 前置工单:无 / #编号\n" "- 是否允许与前置工单并行:是 / 否\n" "- 原因:\n" "## 原始需求\n" "- 来源:用户对话 / Gitea / 其他\n" "- 提出时间:\n" "- 关键原话或脱敏摘要:\n" "## 需求变化记录\n" "| 日期 | 变化内容 | 原因 | 用户确认 |\n" "## 文档影响\n" "- [ ] 不影响长期文档,原因:\n" "- [ ] 更新架构与代码地图\n" "- [ ] 更新业务规则与术语\n" "- [ ] 更新常见修改或故障排查\n", encoding="utf-8", ) errors: list[str] = [] check_task_template(errors, root) self.assertIn("单元任务模板缺少:## 交付文档影响", errors) self.assertIn( "单元任务模板缺少:- [ ] 无交付文档影响,原因:", errors, ) def test_task_template_requires_design_and_prototype_gate(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) template = root / ".gitea" / "issue_template" / "task.md" template.parent.mkdir(parents=True) template.write_text("## 基本信息\n", encoding="utf-8") errors: list[str] = [] check_task_template(errors, root) self.assertIn("单元任务模板缺少:## 设计与原型门禁", errors) self.assertIn( "单元任务模板缺少:- 可编辑设计源、线上原型链接和访问检查:", errors, ) self.assertIn( "单元任务模板缺少:- 本地 HTML 导出:未要求 / 用户明确要求 / 项目规则要求", errors, ) self.assertIn( "单元任务模板缺少:- 本地 HTML 路径、版本和资源检查(仅显式导出时填写):", errors, ) def test_task_template_requires_optional_snapshot_policy(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) template = root / ".gitea" / "issue_template" / "task.md" template.parent.mkdir(parents=True) template.write_text("## 基本信息\n", encoding="utf-8") errors: list[str] = [] check_task_template(errors, root) self.assertIn("单元任务模板缺少:## 任务记录与可选快照", errors) self.assertIn( "单元任务模板缺少:- [ ] 默认不创建任务快照", errors, ) def test_task_template_requires_subproject_impact(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) template = root / ".gitea" / "issue_template" / "task.md" template.parent.mkdir(parents=True) template.write_text("## 基本信息\n", encoding="utf-8") errors: list[str] = [] check_task_template(errors, root) self.assertIn("单元任务模板缺少:## 子项目影响", errors) self.assertIn( "单元任务模板缺少:- 是否跨子项目:是 / 否", errors, ) self.assertIn( "单元任务模板缺少:- 是否修改共享接口或契约:是 / 否;唯一事实来源:", errors, ) def test_task_template_requires_dependency_fields(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) template = root / ".gitea" / "issue_template" / "task.md" template.parent.mkdir(parents=True) template.write_text( "## 文档影响\n" "- [ ] 不影响长期文档,原因:\n" "- [ ] 更新架构与代码地图\n" "- [ ] 更新业务规则与术语\n" "- [ ] 更新常见修改或故障排查\n", encoding="utf-8", ) errors: list[str] = [] check_task_template(errors, root) self.assertIn("单元任务模板缺少:## 依赖与并行", errors) self.assertIn("单元任务模板缺少:- 前置工单:无 / #编号", errors) def test_task_template_requires_requirement_traceability(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) template = root / ".gitea" / "issue_template" / "task.md" template.parent.mkdir(parents=True) template.write_text( "## 依赖与并行\n" "- 前置工单:无 / #编号\n" "- 是否允许与前置工单并行:是 / 否\n" "- 原因:\n" "## 文档影响\n" "- [ ] 不影响长期文档,原因:\n" "- [ ] 更新架构与代码地图\n" "- [ ] 更新业务规则与术语\n" "- [ ] 更新常见修改或故障排查\n", encoding="utf-8", ) errors: list[str] = [] check_task_template(errors, root) self.assertIn("单元任务模板缺少:## 原始需求", errors) self.assertIn("单元任务模板缺少:## 需求变化记录", errors) class AgentRuleTests(unittest.TestCase): def test_required_agent_rules_are_present(self) -> None: errors: list[str] = [] check_agent_efficiency_rules(errors) self.assertEqual(errors, []) def test_repository_readme_requires_online_wiki_gate(self) -> None: errors: list[str] = [] check_repository_readme(errors) self.assertEqual(errors, []) def test_repository_readme_rejects_missing_gate(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / "README.md").write_text( "# 项目\n创建 Gitea 远端仓库\n", encoding="utf-8", ) errors: list[str] = [] check_repository_readme(errors, root) self.assertTrue(any("README.md 缺少" in error for error in errors)) def test_claude_code_entry_imports_shared_rules(self) -> None: errors: list[str] = [] check_claude_code_entry(errors) self.assertEqual(errors, []) def test_claude_code_entry_requires_exact_import_line(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / "CLAUDE.md").write_text( "共同规则事实来源\n只记录 Claude Code 特有\n" "共同规则只修改 `AGENTS.md`\n", encoding="utf-8", ) errors: list[str] = [] check_claude_code_entry(errors, root) self.assertIn("CLAUDE.md 缺少独立的 @AGENTS.md 导入", errors) if __name__ == "__main__": unittest.main()