5b1972b7f9
gates / gates (push) Successful in 2s
- fixtures (upgrade_fixtures_box.py): calibre-web (Upload form -> OPDS readback + the served EPUB), wger (web login -> weight entry API), crafty-controller (API v2 roles), uptime-kuma (socket.io polling: setup, login, addStatusPage -> public /api/status-page/<slug>); gitea posts its own first-run installer form (R-624's fixable case) and keeps the admin CLI for an installed one. calibre-web and wger run the template's own after_install on the bench, which has none. - R-735: the bench's `password:N:special` now has the controller's shape (randomWithSpecial); test seen failing first (length 32, no special), then 45/45. - upgrade_boxport / the memory watch: a backend traefik reaches over https (loadbalancer.server.scheme) is reached over https on the bench too (crafty-controller). - upgrade-test: files-before/after-detail.json and `files_changed_detail` NAME the files behind a files_may_change mark (R-734's method, now in the harness); test ChangedFiles. - test_catalog_gates: the gate count was stale (9, the runner has 10) and red on main; now 10. Evidence: felhom.eu/documentation/audits/more-night-apps-2026-09-30/ Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
105 lines
5.3 KiB
Python
105 lines
5.3 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).
|
|
self.assertEqual(len(mod.GATES), 10)
|
|
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)
|