diff --git a/scripts/check-pins.sh b/scripts/check-pins.sh index 219a017e..cab88a2b 100755 --- a/scripts/check-pins.sh +++ b/scripts/check-pins.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # 组合指针一致:容器仓在 Dev 钉住的提交上,deps/ 里的每个 gitlink -# 必须等于 Dev 顶层对应指针。development-guidelines 只告警。 +# 必须等于 Dev 顶层对应指针。development-guidelines / development-manager +# 是治理规范与工具,版本由各项目固定采纳自行决定,不是运行时依赖,只告警。 set -euo pipefail root=$(cd "$(dirname "$0")/.." && pwd) cd "$root" @@ -48,10 +49,10 @@ for container in "${containers[@]}"; do while read -r mode _typ sha name; do [ "$mode" = "160000" ] || continue base=${name#deps/} - if [ "$base" = "development-guidelines" ]; then - dev=$(pin_of deps/development-guidelines) + if [ "$base" = "development-guidelines" ] || [ "$base" = "development-manager" ]; then + dev=$(pin_of "deps/$base") if [ "$sha" != "$dev" ]; then - echo "warn: $container deps/development-guidelines is ${sha:0:12}, Dev is ${dev:0:12}" >&2 + echo "warn: $container deps/$base is ${sha:0:12}, Dev is ${dev:0:12}" >&2 fi continue fi diff --git a/tests/test_check_pins.py b/tests/test_check_pins.py new file mode 100644 index 00000000..009281ae --- /dev/null +++ b/tests/test_check_pins.py @@ -0,0 +1,117 @@ +"""check-pins.sh:治理 gitlink 只告警,运行时依赖仍严格校验。""" +import os +import shutil +import subprocess +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "check-pins.sh" +CONTAINERS = ("modules/backend", "deps/agent-native", "deps/agent-squad", "deps/agent-intent") + + +class CheckPinsTests(unittest.TestCase): + def setUp(self): + self.tmp = Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.tmp, ignore_errors=True) + cfg = self.tmp / "gitconfig" + cfg.write_text("") + self.env = dict( + os.environ, + GIT_CONFIG_GLOBAL=str(cfg), + GIT_CONFIG_NOSYSTEM="1", + GIT_AUTHOR_NAME="t", + GIT_AUTHOR_EMAIL="t@example.invalid", + GIT_COMMITTER_NAME="t", + GIT_COMMITTER_EMAIL="t@example.invalid", + ) + self.dev = self.tmp / "dev" + (self.dev / "scripts").mkdir(parents=True) + shutil.copy(SCRIPT, self.dev / "scripts" / "check-pins.sh") + self.git(self.dev, "init", "-q") + + def git(self, cwd, *args): + return subprocess.run( + ["git", *args], cwd=cwd, env=self.env, check=True, capture_output=True, text=True + ).stdout.strip() + + def repo(self, rel, *contents): + """在 Dev 内建嵌套仓,按内容逐次提交,返回各提交 SHA。""" + path = self.dev / rel + path.mkdir(parents=True) + self.git(path, "init", "-q") + shas = [] + for text in contents: + (path / "f").write_text(text) + self.git(path, "add", "f") + self.git(path, "commit", "-qm", text) + shas.append(self.git(path, "rev-parse", "HEAD")) + return shas + + def link(self, repo, rel, sha): + self.git(repo, "update-index", "--add", "--cacheinfo", f"160000,{sha},{rel}") + + def build(self, backend_deps, dev_pins): + """backend_deps/dev_pins:{deps 名: sha};其余容器无 deps。""" + for rel, sha in dev_pins.items(): + self.link(self.dev, rel, sha) + for container in CONTAINERS: + path = self.dev / container + if not path.exists(): + self.repo(container, container) + if container == "modules/backend": + for name, sha in backend_deps.items(): + self.link(path, f"deps/{name}", sha) + self.git(path, "commit", "-qm", "deps") + self.link(self.dev, container, self.git(path, "rev-parse", "HEAD")) + + def run_pins(self): + return subprocess.run( + ["bash", "scripts/check-pins.sh"], cwd=self.dev, env=self.env, capture_output=True, text=True + ) + + def test_governance_mismatch_warns_and_passes(self): + g_old, g_new = self.repo("deps/development-guidelines", "g1", "g2") + m_old, m_new = self.repo("deps/development-manager", "m1", "m2") + self.build( + {"development-guidelines": g_old, "development-manager": m_old}, + {"deps/development-guidelines": g_new, "deps/development-manager": m_new}, + ) + res = self.run_pins() + self.assertEqual(res.returncode, 0, res.stderr) + self.assertIn("warn: modules/backend deps/development-guidelines", res.stderr) + self.assertIn("warn: modules/backend deps/development-manager", res.stderr) + self.assertIn("pins ok", res.stdout) + + def test_runtime_mismatch_fails(self): + old, new = self.repo("deps/error-code-system", "e1", "e2") + self.build({"error-code-system": old}, {"deps/error-code-system": new}) + res = self.run_pins() + self.assertEqual(res.returncode, 1) + self.assertIn("pin: modules/backend deps/error-code-system is", res.stderr) + + def test_runtime_match_passes(self): + (sha,) = self.repo("deps/error-code-system", "e1") + self.build({"error-code-system": sha}, {"deps/error-code-system": sha}) + res = self.run_pins() + self.assertEqual(res.returncode, 0, res.stderr) + self.assertIn("ok modules/backend error-code-system", res.stdout) + + def test_runtime_missing_object_fails(self): + (sha,) = self.repo("deps/error-code-system", "e1") + self.build({"error-code-system": "1" * 40}, {"deps/error-code-system": sha}) + res = self.run_pins() + self.assertEqual(res.returncode, 1) + self.assertIn("is missing locally", res.stderr) + + def test_runtime_without_dev_pin_fails(self): + (sha,) = self.repo("deps/logging-system", "l1") + self.build({"logging-system": sha}, {}) + res = self.run_pins() + self.assertEqual(res.returncode, 1) + self.assertIn("has no Dev pin", res.stderr) + + +if __name__ == "__main__": + unittest.main()