chore: bootstrap YoVision DevHarness
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
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_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_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_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()
|
||||
@@ -0,0 +1,163 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
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 wiki_docs import ( # noqa: E402
|
||||
Config,
|
||||
Mapping,
|
||||
WikiClient,
|
||||
WikiDocsError,
|
||||
WikiPage,
|
||||
dirty_mirror_paths,
|
||||
load_config,
|
||||
parse_mirror,
|
||||
render_mirror,
|
||||
sync_all,
|
||||
validate_mappings,
|
||||
)
|
||||
|
||||
|
||||
class MappingTests(unittest.TestCase):
|
||||
def test_rejects_path_outside_docs(self) -> None:
|
||||
with self.assertRaisesRegex(WikiDocsError, "docs/"):
|
||||
validate_mappings([{"page": "Home", "path": "README.md"}])
|
||||
|
||||
def test_rejects_duplicate_page(self) -> None:
|
||||
with self.assertRaisesRegex(WikiDocsError, "重复映射"):
|
||||
validate_mappings(
|
||||
[
|
||||
{"page": "Home", "path": "docs/README.md"},
|
||||
{"page": "Home", "path": "docs/other.md"},
|
||||
]
|
||||
)
|
||||
|
||||
def test_normalizes_api_suffix_from_environment(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "wiki-docs.json"
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"gitea_url": "http://configured.example",
|
||||
"owner": "owner",
|
||||
"repository": "repo",
|
||||
"mappings": [
|
||||
{"page": "Home", "path": "docs/README.md"}
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with patch.dict(
|
||||
os.environ, {"GITEA_URL": "http://gitea.example/api/v1"}, clear=False
|
||||
):
|
||||
config = load_config(path)
|
||||
self.assertEqual(config.gitea_url, "http://gitea.example")
|
||||
|
||||
|
||||
class MirrorTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.page = WikiPage(
|
||||
title="Home",
|
||||
sub_url="Home",
|
||||
text="# 首页\n",
|
||||
revision="a" * 40,
|
||||
html_url="http://gitea.example/o/r/wiki/Home",
|
||||
)
|
||||
|
||||
def test_render_includes_traceable_metadata(self) -> None:
|
||||
rendered = render_mirror(self.page)
|
||||
metadata, body = parse_mirror(rendered)
|
||||
self.assertEqual(metadata["wiki_page"], "Home")
|
||||
self.assertEqual(metadata["wiki_revision"], "a" * 40)
|
||||
self.assertTrue(metadata["synchronized_at"].endswith("Z"))
|
||||
self.assertEqual(body, "# 首页\n")
|
||||
|
||||
def test_unchanged_revision_preserves_sync_time(self) -> None:
|
||||
first = render_mirror(self.page)
|
||||
second = render_mirror(self.page, first)
|
||||
self.assertEqual(first, second)
|
||||
|
||||
@patch("wiki_docs.subprocess.run")
|
||||
def test_dirty_mirror_paths_are_reported(self, run) -> None:
|
||||
run.return_value.stdout = " M docs/README.md\n"
|
||||
config = Config(
|
||||
path=Path("wiki-docs.json"),
|
||||
gitea_url="http://gitea.example",
|
||||
owner="o",
|
||||
repository="r",
|
||||
mappings=(Mapping("Home", "docs/README.md"),),
|
||||
)
|
||||
self.assertEqual(dirty_mirror_paths(config), [" M docs/README.md"])
|
||||
|
||||
@patch("wiki_docs.dirty_mirror_paths", return_value=[" M docs/README.md"])
|
||||
def test_sync_stops_before_reading_wiki_when_mirror_is_dirty(self, _dirty) -> None:
|
||||
config = Config(
|
||||
path=Path("wiki-docs.json"),
|
||||
gitea_url="http://gitea.example",
|
||||
owner="o",
|
||||
repository="r",
|
||||
mappings=(Mapping("Home", "docs/README.md"),),
|
||||
)
|
||||
client = Mock()
|
||||
with self.assertRaisesRegex(WikiDocsError, "未提交改动"):
|
||||
sync_all(config, client)
|
||||
client.get_page.assert_not_called()
|
||||
|
||||
|
||||
class WikiClientTests(unittest.TestCase):
|
||||
def test_encoded_unicode_sub_url_is_not_double_encoded(self) -> None:
|
||||
config = Config(
|
||||
path=Path("wiki-docs.json"),
|
||||
gitea_url="http://gitea.example",
|
||||
owner="o",
|
||||
repository="r",
|
||||
mappings=(Mapping("中文", "docs/chinese.md"),),
|
||||
)
|
||||
client = WikiClient(config, token="")
|
||||
client.list_pages = Mock(
|
||||
return_value=[{"title": "中文", "sub_url": "%E4%B8%AD%E6%96%87.-"}]
|
||||
)
|
||||
encoded = __import__("base64").b64encode("# 中文\n".encode()).decode()
|
||||
with patch.object(
|
||||
client,
|
||||
"_request",
|
||||
return_value={
|
||||
"title": "中文",
|
||||
"content_base64": encoded,
|
||||
"last_commit": {"sha": "b" * 40},
|
||||
},
|
||||
) as request:
|
||||
page = client.get_page("中文")
|
||||
api_path = request.call_args.args[1]
|
||||
self.assertIn("%E4%B8%AD%E6%96%87.-", api_path)
|
||||
self.assertNotIn("%25E4", api_path)
|
||||
self.assertTrue(page.html_url.endswith("/%E4%B8%AD%E6%96%87.-"))
|
||||
|
||||
|
||||
class ArchiveTests(unittest.TestCase):
|
||||
def test_safe_title_handles_windows_characters(self) -> None:
|
||||
self.assertEqual(safe_title(' 修复:"登录" / 超时 '), "修复-登录-超时")
|
||||
|
||||
def test_build_archive_replaces_known_fields(self) -> None:
|
||||
template = "# <工单号> <标题>\nYYYY-MM-DD\n<链接>\n<页面名>\n"
|
||||
result = build_archive(template, "12", "修复登录", "Task-12-login", "http://i/12")
|
||||
self.assertIn("# 12 修复登录", result)
|
||||
self.assertIn("http://i/12", result)
|
||||
self.assertIn("Task-12-login", result)
|
||||
self.assertNotIn("YYYY-MM-DD", result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user