262 lines
11 KiB
Python
262 lines
11 KiB
Python
from __future__ import annotations
|
||
|
||
import sys
|
||
import tempfile
|
||
import unittest
|
||
from pathlib import Path
|
||
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
sys.path.insert(0, str(ROOT / "dev_scripts"))
|
||
|
||
from check_harness import ( # noqa: E402
|
||
CORE_DOCUMENT_REQUIREMENTS,
|
||
CORE_PAGE_PATHS,
|
||
REQUIRED_FILES,
|
||
check_claude_code_entry,
|
||
check_core_documents,
|
||
check_agent_efficiency_rules,
|
||
check_go_admin_ui_rules,
|
||
check_goadmin_baseline,
|
||
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_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_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_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_coordination_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", encoding="utf-8")
|
||
errors: list[str] = []
|
||
check_task_template(errors, root)
|
||
self.assertIn(
|
||
"单元任务模板缺少:- 任务类型:单项目 / 协同",
|
||
errors,
|
||
)
|
||
self.assertIn("单元任务模板缺少:- 主 agent:", errors)
|
||
self.assertIn("单元任务模板缺少:## 协同接口", errors)
|
||
self.assertIn("单元任务模板缺少:- 生产者:", errors)
|
||
self.assertIn("单元任务模板缺少:- 消费者:", errors)
|
||
self.assertIn("单元任务模板缺少:- write_paths:", 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_claude_code_entry_imports_shared_rules(self) -> None:
|
||
errors: list[str] = []
|
||
check_claude_code_entry(errors)
|
||
self.assertEqual(errors, [])
|
||
|
||
def test_go_admin_ui_rules_are_present(self) -> None:
|
||
errors: list[str] = []
|
||
check_go_admin_ui_rules(errors)
|
||
self.assertEqual(errors, [])
|
||
|
||
def test_go_admin_ui_rules_require_each_scope(self) -> None:
|
||
with tempfile.TemporaryDirectory() as directory:
|
||
root = Path(directory)
|
||
(root / "Sense").mkdir()
|
||
(root / "Bell").mkdir()
|
||
(root / "AGENTS.md").write_text("", encoding="utf-8")
|
||
(root / "Sense" / "AGENTS.md").write_text("", encoding="utf-8")
|
||
(root / "Bell" / "AGENTS.md").write_text("", encoding="utf-8")
|
||
errors: list[str] = []
|
||
check_go_admin_ui_rules(errors, root)
|
||
self.assertTrue(any(error.startswith("AGENTS.md ") for error in errors))
|
||
self.assertTrue(any(error.startswith("Sense/AGENTS.md ") for error in errors))
|
||
self.assertTrue(any(error.startswith("Bell/AGENTS.md ") for error in errors))
|
||
|
||
def test_go_admin_ui_rules_report_missing_scope_file(self) -> None:
|
||
with tempfile.TemporaryDirectory() as directory:
|
||
errors: list[str] = []
|
||
check_go_admin_ui_rules(errors, Path(directory))
|
||
self.assertIn("缺少 go-admin 规则文件:AGENTS.md", errors)
|
||
self.assertIn("缺少 go-admin 规则文件:Sense/AGENTS.md", errors)
|
||
self.assertIn("缺少 go-admin 规则文件:Bell/AGENTS.md", errors)
|
||
|
||
def test_goadmin_baseline_is_frozen(self) -> None:
|
||
errors: list[str] = []
|
||
check_goadmin_baseline(errors)
|
||
self.assertEqual(errors, [])
|
||
|
||
def test_goadmin_baseline_rejects_unpinned_source(self) -> None:
|
||
with tempfile.TemporaryDirectory() as directory:
|
||
root = Path(directory)
|
||
(root / "goadmin-baseline.json").write_text(
|
||
'{"authority":"tag","runtimes":{},'
|
||
'"sources":{"go-admin":{}},"rules":{}}',
|
||
encoding="utf-8",
|
||
)
|
||
errors: list[str] = []
|
||
check_goadmin_baseline(errors, root)
|
||
self.assertTrue(any("go-admin.commit" in error for error in errors))
|
||
self.assertIn(
|
||
"GoAdmin 技术基线必须以上游 URL 和 commit 为权威标识",
|
||
errors,
|
||
)
|
||
|
||
def test_goadmin_baseline_reports_missing_file(self) -> None:
|
||
with tempfile.TemporaryDirectory() as directory:
|
||
errors: list[str] = []
|
||
check_goadmin_baseline(errors, Path(directory))
|
||
self.assertEqual(
|
||
errors,
|
||
["缺少 GoAdmin 技术基线:goadmin-baseline.json"],
|
||
)
|
||
|
||
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()
|