# -*- coding: utf-8 -*- """Seam test for scripts/catalog_gates.py --fast. Run: python3 scripts/test_catalog_gates.py WHY THIS EXISTS. An entry point is a seam by definition: a runner that LISTS a gate but never executes it is inert and fully green. So the assertion is on the member gate's OWN distinctive stdout — never on the runner's summary line — plus the exit code. The second and third tests pin --fast's CONTENT, not just its exit code: the two runtime gates must NOT run (a push that pulls images and starts containers gets bypassed within a week, and the bypass becomes the habit), and the skip must be ANNOUNCED — a silently narrowed run reads as "covered everything" when it did not. """ import os import subprocess import sys import unittest ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ENTRY = os.path.join(ROOT, "scripts", "catalog_gates.py") class CatalogGatesFastTest(unittest.TestCase): @classmethod def setUpClass(cls): p = subprocess.run([sys.executable, ENTRY, "--fast"], cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) cls.rc = p.returncode cls.out = p.stdout.decode("utf-8", "replace") def test_exit_code_is_zero(self): self.assertEqual(self.rc, 0, self.out) def test_static_gate_actually_ran(self): self.assertIn("image-pin gate", self.out, "check-image-pins is listed but its own output never appeared — an inert " "runner prints the summary without calling anything:\n%s" % self.out) def test_runtime_gates_did_not_run(self): for fingerprint in ("resolvability gate", "volume-persistence gate", "canary"): self.assertNotIn(fingerprint, self.out, "a runtime gate ran under --fast (%r) — --fast must touch no network " "and no container runtime:\n%s" % (fingerprint, self.out)) self.assertNotIn("image-resolvable OK", self.out) self.assertNotIn("volume-persistence OK", self.out) def test_skip_is_announced(self): self.assertIn("--fast SKIPPED", self.out) self.assertIn("image-resolvable", self.out) self.assertIn("volume-persistence", self.out) def test_default_run_still_selects_all_three(self): """--fast must not change the no-flag behaviour. Asserted on the GATES table rather than by running it — the default run deploys every template and takes minutes per app.""" import importlib.util spec = importlib.util.spec_from_file_location("catalog_gates_under_test", ENTRY) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) # STALE UNTIL 2026-09-23: this read 5 gates and 3 fast ones while the table had grown to 7 # (copy-i18n, probe-matches-compose) — the test was red and nobody ran it. Now 9 with the test # record's two halves; the slow pair stays out of --fast. STALE AGAIN until 2026-09-30: 10 since # probe-measured joined; the test had been red on main (found by the more-night-apps session). # 11 since 2026-10-01: onboarding (NEW-APP-CHECKLIST.md), fast and history-free, so it runs in CI too. # 12 since family-gate joined — STALE AGAIN until 2026-10-05 (found by the burn-down, round 2). # 13 since 2026-10-05: mem-limit-sum (R-758), fast and history-free, so it runs in CI too. # 14 since the same night: data-key (R-127 leg a). self.assertEqual(len(mod.GATES), 14) self.assertIn("data-key", [g[0] for g in mod.GATES if g[3] and not g[4]]) self.assertIn("mem-limit-sum", [g[0] for g in mod.GATES if g[3] and not g[4]]) self.assertIn("onboarding", [g[0] for g in mod.GATES if g[3] and not g[4]]) self.assertEqual([g[0] for g in mod.GATES if not g[3]], ["image-resolvable", "volume-persistence"]) self.assertIn("test-record", [g[0] for g in mod.GATES if g[3] and not g[4]]) # runs in CI too # the gates that need git history are the ones the CI half cannot run (R-452's shallow gap) self.assertEqual([g[0] for g in mod.GATES if g[4]], ["engine-major", "catalog-since", "test-record-move"]) def test_catalog_since_ran_under_fast(self): """R-452's gate is fast (git reads only) and must be IN --fast, like engine-major. (Until 2026-10-05 this method sat after `if __name__ == "__main__":`, outside any class, so it never ran.)""" self.assertIn("catalog-since gate", self.out) def test_engine_major_ran_under_fast(self): """The 2026-09-13 gate is fast (git reads only) and must be IN --fast, or the hook that exists to enforce its rule never runs it.""" self.assertIn("engine-major gate", self.out) def test_shallow_clone_skips_engine_major_out_loud(self): """On a --depth 1 clone (what CI has) the runner must SKIP engine-major and SAY so — never convict every push, never pass silently. Asserted on a real shallow clone of this repo.""" import shutil, tempfile tmp = tempfile.mkdtemp(prefix="catalog-shallow-") try: subprocess.run(["git", "clone", "-q", "--depth", "1", "file://" + ROOT, tmp], check=True, capture_output=True) # test the WORKING-TREE runner and gate, not whatever HEAD happens to hold for fn in ("catalog_gates.py", "check-engine-major.py", "check-image-pins.py", "check-catalog-since.py"): shutil.copy(os.path.join(ROOT, "scripts", fn), os.path.join(tmp, "scripts", fn)) p = subprocess.run([sys.executable, os.path.join(tmp, "scripts", "catalog_gates.py"), "--fast"], cwd=tmp, capture_output=True, text=True) out = p.stdout + p.stderr self.assertIn("SHALLOW CLONE", out) self.assertIn("engine-major", out) self.assertNotIn("engine-major gate OK", out) self.assertEqual(p.returncode, 0, out) finally: shutil.rmtree(tmp, ignore_errors=True) class SummaryTellsRefusedFromUndecided(unittest.TestCase): """R-605: a gate whose HARNESS refused (exit 3 — its canary failed, nothing was judged) and a gate that ran and could not decide (exit 2) used to print the same word. Decoys each way, on the runner's summary.""" @classmethod def setUpClass(cls): import importlib.util spec = importlib.util.spec_from_file_location("catalog_gates_summary", ENTRY) cls.mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(cls.mod) def _run(self, results): import contextlib import io buf = io.StringIO() with contextlib.redirect_stdout(buf): rc = self.mod.summarise(results) return rc, buf.getvalue() def test_a_refused_harness_does_not_read_as_undetermined(self): rc, out = self._run([("image-pins", 0), ("volume-persistence", 3)]) self.assertEqual(rc, 2, "a refused harness is never a pass") self.assertIn("DID-NOT-RUN", out) self.assertIn("DID NOT RUN", out) self.assertIn("volume-persistence", out.split("DID NOT RUN", 1)[1]) self.assertNotIn("UNDETERMINED", out) self.assertNotIn("INCONCLUSIVE", out) def test_an_undetermined_run_does_not_read_as_refused(self): rc, out = self._run([("image-pins", 0), ("image-resolvable", 2)]) self.assertEqual(rc, 2) self.assertIn("INCONCLUSIVE", out) self.assertIn("UNDETERMINED", out) self.assertNotIn("DID NOT RUN", out) self.assertNotIn("DID-NOT-RUN", out) def test_a_conviction_still_outranks_both(self): rc, out = self._run([("image-pins", 1), ("image-resolvable", 2), ("volume-persistence", 3)]) self.assertEqual(rc, 1) self.assertIn("CONVICTED: image-pins", out) if __name__ == "__main__": unittest.main(verbosity=2)