chore(workflow): adopt lightweight governance (#221)
This commit is contained in:
+240
-2
@@ -5,8 +5,11 @@ import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from argparse import Namespace
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
from urllib.error import HTTPError
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -21,6 +24,7 @@ from harness import ( # noqa: E402
|
||||
existing_task_mirrors,
|
||||
export_task_archives,
|
||||
main as harness_main,
|
||||
run_sync,
|
||||
safe_title,
|
||||
task_target,
|
||||
)
|
||||
@@ -126,8 +130,215 @@ class MirrorTests(unittest.TestCase):
|
||||
sync_all(config, client)
|
||||
client.get_page.assert_not_called()
|
||||
|
||||
@patch("wiki_docs.dirty_mirror_paths", return_value=[])
|
||||
def test_incremental_sync_lists_once_and_skips_unchanged_body(self, _dirty) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
target = root / "docs" / "README.md"
|
||||
target.parent.mkdir(parents=True)
|
||||
target.write_text(render_mirror(self.page), encoding="utf-8")
|
||||
config = Config(
|
||||
path=root / "wiki-docs.json",
|
||||
gitea_url="http://gitea.example",
|
||||
owner="o",
|
||||
repository="r",
|
||||
mappings=(Mapping("Home", "docs/README.md"),),
|
||||
)
|
||||
client = Mock()
|
||||
client.list_pages.return_value = [
|
||||
{
|
||||
"title": "Home",
|
||||
"sub_url": "Home",
|
||||
"last_commit": {"sha": self.page.revision},
|
||||
}
|
||||
]
|
||||
with patch("wiki_docs.ROOT", root):
|
||||
messages = sync_all(config, client)
|
||||
self.assertTrue(messages[0].startswith("无变化:"))
|
||||
client.list_pages.assert_called_once_with()
|
||||
client.get_page_from_metadata.assert_not_called()
|
||||
|
||||
@patch("wiki_docs.dirty_mirror_paths", return_value=[])
|
||||
def test_incremental_sync_fetches_changed_page(self, _dirty) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
target = root / "docs" / "README.md"
|
||||
target.parent.mkdir(parents=True)
|
||||
target.write_text(render_mirror(self.page), encoding="utf-8")
|
||||
changed = WikiPage(
|
||||
title="Home",
|
||||
sub_url="Home",
|
||||
text="# 新首页\n",
|
||||
revision="b" * 40,
|
||||
html_url=self.page.html_url,
|
||||
)
|
||||
metadata = {
|
||||
"title": "Home",
|
||||
"sub_url": "Home",
|
||||
"last_commit": {"sha": changed.revision},
|
||||
}
|
||||
config = Config(
|
||||
path=root / "wiki-docs.json",
|
||||
gitea_url="http://gitea.example",
|
||||
owner="o",
|
||||
repository="r",
|
||||
mappings=(Mapping("Home", "docs/README.md"),),
|
||||
)
|
||||
client = Mock()
|
||||
client.list_pages.return_value = [metadata]
|
||||
client.get_page_from_metadata.return_value = changed
|
||||
with patch("wiki_docs.ROOT", root):
|
||||
messages = sync_all(config, client)
|
||||
_metadata, body = parse_mirror(target.read_text(encoding="utf-8"))
|
||||
self.assertTrue(messages[0].startswith("已更新:"))
|
||||
self.assertEqual(body, changed.text)
|
||||
client.get_page_from_metadata.assert_called_once_with(metadata, "Home")
|
||||
|
||||
@patch("wiki_docs.dirty_mirror_paths", return_value=[])
|
||||
def test_sync_does_not_write_partial_mirrors_when_later_read_fails(
|
||||
self, _dirty
|
||||
) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
first_target = root / "docs" / "first.md"
|
||||
second_target = root / "docs" / "second.md"
|
||||
first_target.parent.mkdir(parents=True)
|
||||
first_original = render_mirror(self.page)
|
||||
first_target.write_text(first_original, encoding="utf-8")
|
||||
second_target.write_text(first_original, encoding="utf-8")
|
||||
config = Config(
|
||||
path=root / "wiki-docs.json",
|
||||
gitea_url="http://gitea.example",
|
||||
owner="o",
|
||||
repository="r",
|
||||
mappings=(
|
||||
Mapping("First", "docs/first.md"),
|
||||
Mapping("Second", "docs/second.md"),
|
||||
),
|
||||
)
|
||||
pages = [
|
||||
{
|
||||
"title": name,
|
||||
"sub_url": name,
|
||||
"last_commit": {"sha": revision},
|
||||
}
|
||||
for name, revision in (("First", "b" * 40), ("Second", "c" * 40))
|
||||
]
|
||||
changed = WikiPage(
|
||||
title="First",
|
||||
sub_url="First",
|
||||
text="# Changed\n",
|
||||
revision="b" * 40,
|
||||
html_url="http://gitea.example/o/r/wiki/First",
|
||||
)
|
||||
client = Mock()
|
||||
client.list_pages.return_value = pages
|
||||
client.get_page_from_metadata.side_effect = [
|
||||
changed,
|
||||
WikiDocsError("rate limited"),
|
||||
]
|
||||
with patch("wiki_docs.ROOT", root):
|
||||
with self.assertRaises(WikiDocsError):
|
||||
sync_all(config, client)
|
||||
self.assertEqual(first_target.read_text(encoding="utf-8"), first_original)
|
||||
|
||||
@patch("wiki_docs.dirty_mirror_paths", return_value=[])
|
||||
def test_sync_fetches_when_remote_revision_is_missing(self, _dirty) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
target = root / "docs" / "README.md"
|
||||
target.parent.mkdir(parents=True)
|
||||
target.write_text(render_mirror(self.page), encoding="utf-8")
|
||||
metadata = {"title": "Home", "sub_url": "Home"}
|
||||
config = Config(
|
||||
path=root / "wiki-docs.json",
|
||||
gitea_url="http://gitea.example",
|
||||
owner="o",
|
||||
repository="r",
|
||||
mappings=(Mapping("Home", "docs/README.md"),),
|
||||
)
|
||||
client = Mock()
|
||||
client.list_pages.return_value = [metadata]
|
||||
client.get_page_from_metadata.return_value = self.page
|
||||
with patch("wiki_docs.ROOT", root):
|
||||
sync_all(config, client)
|
||||
client.get_page_from_metadata.assert_called_once_with(metadata, "Home")
|
||||
|
||||
def test_deep_check_fetches_unchanged_page_body(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
target = root / "docs" / "README.md"
|
||||
target.parent.mkdir(parents=True)
|
||||
target.write_text(render_mirror(self.page), encoding="utf-8")
|
||||
metadata = {
|
||||
"title": "Home",
|
||||
"sub_url": "Home",
|
||||
"last_commit": {"sha": self.page.revision},
|
||||
}
|
||||
config = Config(
|
||||
path=root / "wiki-docs.json",
|
||||
gitea_url="http://gitea.example",
|
||||
owner="o",
|
||||
repository="r",
|
||||
mappings=(Mapping("Home", "docs/README.md"),),
|
||||
)
|
||||
client = Mock()
|
||||
client.list_pages.return_value = [metadata]
|
||||
client.get_page_from_metadata.return_value = self.page
|
||||
with patch("wiki_docs.ROOT", root):
|
||||
sync_all(config, client, check=True, deep_check=True)
|
||||
client.list_pages.assert_called_once_with()
|
||||
client.get_page_from_metadata.assert_called_once_with(metadata, "Home")
|
||||
|
||||
@patch("harness.run_check", return_value=0)
|
||||
@patch("harness.sync_all", return_value=["已更新:docs/README.md"])
|
||||
@patch("harness.WikiClient")
|
||||
@patch("harness.load_config")
|
||||
def test_verify_reuses_one_deep_sync(
|
||||
self, load, client_class, sync, check
|
||||
) -> None:
|
||||
config = Mock()
|
||||
load.return_value = config
|
||||
result = run_sync(
|
||||
Namespace(verify=True, check=False, deep_check=False, config="config.json")
|
||||
)
|
||||
self.assertEqual(result, 0)
|
||||
sync.assert_called_once_with(
|
||||
config, client_class.return_value, check=False, deep_check=True
|
||||
)
|
||||
check.assert_called_once()
|
||||
|
||||
|
||||
class WikiClientTests(unittest.TestCase):
|
||||
@patch("wiki_docs.time.sleep")
|
||||
@patch("wiki_docs.urlopen")
|
||||
def test_get_retries_a_rate_limited_request(self, urlopen, sleep) -> None:
|
||||
config = Config(
|
||||
path=Path("wiki-docs.json"),
|
||||
gitea_url="http://gitea.example",
|
||||
owner="o",
|
||||
repository="r",
|
||||
mappings=(),
|
||||
)
|
||||
limited = HTTPError(
|
||||
"http://gitea.example/api/v1/repos/o/r/wiki/pages",
|
||||
429,
|
||||
"Too Many Requests",
|
||||
None,
|
||||
BytesIO(b"limited"),
|
||||
)
|
||||
success = MagicMock()
|
||||
success.__enter__.return_value.read.return_value = b"[]"
|
||||
urlopen.side_effect = [limited, success]
|
||||
|
||||
result = WikiClient(config, token="token")._request(
|
||||
"GET", "/repos/o/r/wiki/pages"
|
||||
)
|
||||
|
||||
self.assertEqual(result, [])
|
||||
self.assertEqual(urlopen.call_count, 2)
|
||||
sleep.assert_called_once_with(1.0)
|
||||
|
||||
def test_encoded_unicode_sub_url_is_not_double_encoded(self) -> None:
|
||||
config = Config(
|
||||
path=Path("wiki-docs.json"),
|
||||
@@ -323,7 +534,7 @@ class GovernancePolicyTests(unittest.TestCase):
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
agents = (ROOT / "AGENTS.md").read_text(encoding="utf-8")
|
||||
self.assertIn("Gitea 工单是单次任务", agents)
|
||||
self.assertIn("需要工单的任务以 Gitea 工单作为单次", agents)
|
||||
self.assertIn("标准流程不创建 Wiki 任务归档", agents)
|
||||
self.assertIn("Wiki 同步由长期事实变化触发,不由任务完成触发", agents)
|
||||
|
||||
@@ -340,6 +551,33 @@ class GovernancePolicyTests(unittest.TestCase):
|
||||
self.assertIn("### 线上原型审核与按需导出", workflow)
|
||||
self.assertIn("## Windows PowerShell 与 UTF-8", local_development)
|
||||
|
||||
def test_goauto_uses_lightweight_governance_and_explicit_authorization(self) -> None:
|
||||
agents = (ROOT / "AGENTS.md").read_text(encoding="utf-8")
|
||||
profile = (ROOT / "docs" / "00-project-profile.md").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
workflow = (ROOT / "docs" / "01-workflow.md").read_text(encoding="utf-8")
|
||||
for text in (agents, profile, workflow):
|
||||
self.assertIn("轻量治理", text)
|
||||
for text in (agents, workflow):
|
||||
self.assertIn("明确授权后的执行", text)
|
||||
self.assertIn("不能单独构成人工授权", text)
|
||||
self.assertIn("不得仅因操作不可逆而重复询问或拒绝", text)
|
||||
self.assertIn("平台自身强制的审批", text)
|
||||
|
||||
def test_language_and_windows_command_policy_is_documented(self) -> None:
|
||||
agents = (ROOT / "AGENTS.md").read_text(encoding="utf-8")
|
||||
workflow = (ROOT / "docs" / "01-workflow.md").read_text(encoding="utf-8")
|
||||
local_development = (
|
||||
ROOT / "docs" / "04-local-development-and-verification.md"
|
||||
).read_text(encoding="utf-8")
|
||||
for text in (agents, workflow):
|
||||
self.assertIn("默认使用中文", text)
|
||||
self.assertIn("日志和错误原文保持原样", text)
|
||||
self.assertIn("不得默认套用 Bash", agents)
|
||||
self.assertIn("### PowerShell 语法与外部命令", local_development)
|
||||
self.assertIn("rg -n -g '*.md'", local_development)
|
||||
|
||||
def test_deployment_template_is_required_and_mapped(self) -> None:
|
||||
path = "docs/templates/deployment.md"
|
||||
self.assertIn(path, harness.REQUIRED_FILES)
|
||||
|
||||
Reference in New Issue
Block a user