Files
weichen ba1d3b2b81 Add the pin-regression gate to pins checks and the PR gate
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
2026-10-22 06:16:13 +00:00

135 lines
5.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()