"""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()