chore: 升级 DevHarness 工作流 (#108)

This commit is contained in:
QiuSW
2026-08-27 17:16:26 +08:00
parent e14b65b586
commit 266bf236b8
12 changed files with 937 additions and 382 deletions
+166 -70
View File
@@ -3,21 +3,25 @@ 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"))
from check_harness import ( # noqa: E402
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_go_admin_ui_rules,
check_goadmin_baseline,
check_repository_readme,
check_task_template,
core_mapping_errors,
missing_sections,
@@ -45,6 +49,12 @@ class CoreDocumentTests(unittest.TestCase):
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(
@@ -54,6 +64,102 @@ class CoreDocumentTests(unittest.TestCase):
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.md"
self.assertEqual(
CORE_PAGE_PATHS.get("Product-Requirements"),
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_ticket_and_design_evidence_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("## 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)
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))
@@ -99,6 +205,42 @@ class TaskTemplateTests(unittest.TestCase):
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)
@@ -117,23 +259,6 @@ class TaskTemplateTests(unittest.TestCase):
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:
@@ -176,73 +301,44 @@ class TaskTemplateTests(unittest.TestCase):
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:
class YoVisionBaselineTests(unittest.TestCase):
def test_go_admin_ui_rules_are_preserved(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:
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 / "goadmin-baseline.json").write_text(
'{"authority":"tag","runtimes":{},'
'"sources":{"go-admin":{}},"rules":{}}',
(root / "README.md").write_text(
"# 项目\n创建 Gitea 远端仓库\n",
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,
)
check_repository_readme(errors, root)
self.assertTrue(any("README.md 缺少" in error for error in 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_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:
+156 -1
View File
@@ -12,7 +12,14 @@ from unittest.mock import Mock, patch
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "dev_scripts"))
from new_task_archive import build_archive, safe_title # noqa: E402
from harness import ( # noqa: E402
build_archive,
existing_task_mirrors,
export_task_archives,
main as harness_main,
safe_title,
task_target,
)
from wiki_docs import ( # noqa: E402
Config,
Mapping,
@@ -158,6 +165,154 @@ class ArchiveTests(unittest.TestCase):
self.assertIn("Task-12-login", result)
self.assertNotIn("YYYY-MM-DD", result)
@patch("harness.WikiClient")
def test_create_archive_does_not_change_core_mapping(self, client_class) -> None:
with tempfile.TemporaryDirectory() as directory:
config_path = Path(directory) / "wiki-docs.json"
original = json.dumps(
{
"schema_version": 1,
"gitea_url": "http://gitea.example",
"owner": "o",
"repository": "r",
"mappings": [
{"page": "Home", "path": "docs/README.md"}
],
}
)
config_path.write_text(original, encoding="utf-8")
client = client_class.return_value
client.list_pages.return_value = []
client.get_page.return_value = WikiPage(
title="Task-Archive-Template",
sub_url="Task-Archive-Template.-",
text="# <工单号> <标题>\nYYYY-MM-DD\n<链接>\n<页面名>\n",
revision="a" * 40,
html_url="http://gitea.example/wiki/template",
)
client.create_page.return_value = WikiPage(
title="Task-14-按需导出",
sub_url="Task-14.-",
text="# 14 按需导出\n",
revision="b" * 40,
html_url="http://gitea.example/wiki/task-14",
)
with patch.object(
sys,
"argv",
[
"harness.py",
"archive",
"14",
"按需导出",
"--config",
str(config_path),
],
):
result = harness_main()
self.assertEqual(config_path.read_text(encoding="utf-8"), original)
self.assertEqual(result, 0)
client.create_page.assert_called_once()
def test_task_target_uses_stable_safe_name(self) -> None:
with tempfile.TemporaryDirectory() as directory:
target = task_target("Task-14-修复:导出", Path(directory))
self.assertEqual(target.name, "14-修复-导出.md")
def test_existing_mirror_keeps_historical_custom_filename(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
path = root / "docs" / "task" / "2-初级维护者文档体系.md"
path.parent.mkdir(parents=True)
page = WikiPage(
title="Task-2-Junior-Maintainer-Docs",
sub_url="Task-2-Junior-Maintainer-Docs.-",
text="# 2 文档\n",
revision="c" * 40,
html_url="http://gitea.example/wiki/task-2",
)
path.write_text(render_mirror(page), encoding="utf-8")
mirrors = existing_task_mirrors(root)
self.assertEqual(
mirrors["Task-2-Junior-Maintainer-Docs"].name,
"2-初级维护者文档体系.md",
)
@patch("harness.dirty_paths", return_value=[])
def test_incremental_export_skips_same_revision(self, _dirty) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
path = root / "docs" / "task" / "14-按需导出.md"
path.parent.mkdir(parents=True)
page = WikiPage(
title="Task-14-按需导出",
sub_url="Task-14.-",
text="# 14 按需导出\n",
revision="d" * 40,
html_url="http://gitea.example/wiki/task-14",
)
path.write_text(render_mirror(page), encoding="utf-8")
client = Mock()
client.list_pages.return_value = [
{
"title": page.title,
"sub_url": page.sub_url,
"last_commit": {"sha": page.revision},
}
]
messages = export_task_archives(client, root=root)
self.assertTrue(messages[0].startswith("跳过:"))
client.get_page_from_metadata.assert_not_called()
@patch(
"harness.dirty_paths",
return_value=[" M docs/task/14-按需导出.md"],
)
def test_export_stops_before_wiki_read_when_task_mirror_is_dirty(
self, _dirty
) -> None:
client = Mock()
with self.assertRaisesRegex(WikiDocsError, "未提交改动"):
export_task_archives(client)
client.list_pages.assert_not_called()
@patch("harness.dirty_paths", return_value=[])
def test_full_export_reads_all_and_never_deletes_extra_file(self, _dirty) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
task_dir = root / "docs" / "task"
task_dir.mkdir(parents=True)
extra = task_dir / "99-历史快照.md"
extra_page = WikiPage(
title="Task-99-历史快照",
sub_url="Task-99.-",
text="# 99 历史快照\n",
revision="e" * 40,
html_url="http://gitea.example/wiki/task-99",
)
extra.write_text(render_mirror(extra_page), encoding="utf-8")
page = WikiPage(
title="Task-14-按需导出",
sub_url="Task-14.-",
text="# 14 按需导出\n",
revision="f" * 40,
html_url="http://gitea.example/wiki/task-14",
)
client = Mock()
metadata = {
"title": page.title,
"sub_url": page.sub_url,
"last_commit": {"sha": page.revision},
}
client.list_pages.return_value = [metadata]
client.get_page_from_metadata.return_value = page
messages = export_task_archives(client, export_all=True, root=root)
exported = root / "docs" / "task" / "14-按需导出.md"
self.assertTrue(exported.is_file())
self.assertTrue(extra.is_file())
self.assertTrue(messages[0].startswith("已导出:"))
client.get_page_from_metadata.assert_called_once_with(metadata, page.title)
if __name__ == "__main__":
unittest.main()