Files
app-catalog-felhom.eu/scripts/test_catalog_gates.py
T
admin dc0ab8b2a8
gates / gates (push) Successful in 2s
New-app checklist: reviewed, a gate (onboarding), the wger pilot record, the existing apps' gap page
NEW-APP-CHECKLIST.md: the reviewer's draft reviewed - 60 rows in 10 groups, each with how/why and a since date;
7 rows added, 16 sharpened, 9 wrong claims fixed. onboarding/_TEMPLATE.md (one line per id), onboarding/wger.md
(the pilot, exempt app, 11 open rows each a register row), onboarding/EXISTING-APPS-GAPS.md (read only, from
scripts/onboarding_gaps.py). Gate onboarding (scripts/check-onboarding.py) in --fast: a template directory not
among the 53 published before 2026-10-01 needs a complete record; decoys in test_gate_decoys.py (16 cases, 5 gate
mutants seen red). CLAUDE.md, REUSE.md 5, README point to it. No template changed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-01 15:20:52 +02:00

107 lines
5.5 KiB
Python

# -*- 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.
self.assertEqual(len(mod.GATES), 11)
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_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)
if __name__ == "__main__":
unittest.main(verbosity=2)
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."""
self.assertIn("catalog-since gate", self.out)