Why: PR #682 merged a stale branch that reset five component pins to pre-batch values, silently dropping the batch-1 composition on main. Evidence: materialized byte-exact from the reviewed transport (gatemat-run.json); regression/forward/same/skip/missing-object cases (verify-pingate.json); make test-governance and test-pins re-run below. Impact: new check script wired into check-pins and task-pr-check; PIN_REGRESSION_SKIP=1 documents deliberate downgrades. Refs: feature/audit-followups-pin-gate-m5
135 lines
5.6 KiB
Python
135 lines
5.6 KiB
Python
"""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"
|
||
REGRESSION = ROOT / "scripts" / "check_pin_regression.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")
|
||
shutil.copy(REGRESSION, self.dev / "scripts" / "check_pin_regression.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)
|
||
|
||
def test_pin_regression_against_origin_main_fails(self):
|
||
old, new = self.repo("deps/error-code-system", "e1", "e2")
|
||
self.build({"error-code-system": old}, {"deps/error-code-system": old})
|
||
self.git(self.dev, "commit", "-qm", "pr")
|
||
stale = self.git(self.dev, "rev-parse", "HEAD")
|
||
self.link(self.dev, "deps/error-code-system", new)
|
||
self.git(self.dev, "commit", "-qm", "main")
|
||
self.git(self.dev, "update-ref", "refs/remotes/origin/main", "HEAD")
|
||
self.git(self.dev, "reset", "-q", stale)
|
||
res = self.run_pins()
|
||
self.assertEqual(res.returncode, 1, res.stderr)
|
||
self.assertIn("ok modules/backend error-code-system", res.stdout)
|
||
self.assertIn("pin: deps/error-code-system moves BACKWARD", res.stderr)
|
||
self.assertNotIn("pins ok", res.stdout)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|