Twelve tests in test_database_integration.py carry
@pytest.mark.integration and need a real Postgres. The marker was
registered in pytest.ini and used correctly on the file — but `make test`
never deselected it and no target ever selected it. So those tests
neither ran nor passed: they errored on every invocation, and `make test`
exited 2 permanently, which teaches a reader that the exit code means
nothing. That is worse than either running them or not having them.
`make test` is now hermetic per D-26 — 164 passed, 19 deselected, exit 0,
verified inside an unprivileged network namespace as well as outside.
WHY THE AUDIT MISSED THIS, which is the part worth keeping. T-55 measured
each suite with and without a network and treated identical results as
proof of no live dependency. These twelve fail identically both ways,
because postgres-shared is a Docker-internal name that a host process
cannot resolve in either condition. A namespace proves a test does not
reach the network; it cannot distinguish that from a test whose
dependency is unreachable regardless. The positive control was run
against a host that WAS reachable, so it never covered this case.
The new target refuses an empty selection: pytest exit 5 fails with its
own message and a collection error gets a different one, so "nothing to
run" can never be read as "everything passed". Mutation-checked — anchor
asserted unique, marker renamed, target failed at exit 2 with the
intended message, Makefile restored byte-identical.
It reports honestly when Postgres is absent rather than skipping: 6
passed, 1 skipped, 12 errors from this host, which is the true state.
Co-Authored-By: Claude <noreply@anthropic.com>
`make setup` created the venv and ran pip install, then exited 0 whether or
not the result was usable — exactly D-24's failure shape, and the one this
repo hit hardest: on 2026-08-09 there was no venv at all here while CLAUDE.md
documented .venv/bin/python -m pytest as the way to run tests, and nothing in
setup would have caught that state before a person did.
Add a `pytest tests/ --collect-only -q` step at the end. It imports every
test module and everything each one pulls in from src/, without running a
single test, and fails the target on a broken interpreter or a broken
dependency graph alike.
Verified directly:
- absent interpreter -> exit 127, target halts
- apscheduler (declared, imported by src/main.py) uninstalled -> collect-only
exits 4 with ModuleNotFoundError, target halts
- make setup afterwards reinstalls it and collect-only exits 0, 189 tests
collected, same count as before the mutation
- idempotent: a warm rerun on an already-correct venv changes nothing and
collects the same 189 tests, just faster (7s vs 30s cold)
T-47 (workspace).
Co-Authored-By: Claude <noreply@anthropic.com>
The hook carried ~50 lines of gitleaks logic and a comment explaining it was
self-contained because "this repo has no Makefile". It has one now, so the
reason is gone and the arrangement is backwards: a hook is a trigger, and
logic belongs where it can be read, run by hand, and changed under review.
.githooks/pre-push is now a byte-identical shim onto `make pre-push` in every
repo in the workspace. The scan itself moves to ci/secrets.sh unchanged, and
`make secrets` runs it on its own.
The call surface is identical everywhere; what it runs is not, and should not
be — each repo gates what it actually has. That is the point of standardising
the name rather than the contents: nobody has to read a repo to find out how
to check it.
secrets runs first, deliberately. It is the only failure here that cannot be
undone by fixing it afterwards — a failed lint costs another commit, a pushed
credential is cached and indexed whether or not it is later deleted.
Some of these gates fail today, on lint debt that predates them, and they are
left wired anyway. The board was measured once and written down in T-56
instead of being worked around here. Narrowing each gate to whatever already
passes would produce a gate that reports success for doing nothing, which is
the failure this workspace keeps rediscovering.
Co-Authored-By: Claude <noreply@anthropic.com>
Environment guards now exit 69 rather than 1, so a caller can tell a suite
that could not start from one that ran and failed. The first toj test sweep
reported "3 repositories failed" and none of the three had executed a test —
two could not find go, one had no venv. That points the reader at the tests
when the fault is in the environment.
Only the environment guards change. A gitleaks finding, a failed test run and
a vulncheck hit still exit 1, because those did run and did fail.
Co-Authored-By: Claude <noreply@anthropic.com>
Every repo gets one at the root: help, plus test and lint where those exist.
The point is that a target name means the same thing in every repo, so an
agent or a person can act without reading the repo first.
Paths resolve here rather than in callers (D-10). python3 on this host is 3.8
and cannot parse these sources, and a bare pytest or ruff resolves only in a
login shell — so both are named explicitly through the venv, and a missing
venv fails with the command to fix it rather than a bare no-such-file.
Co-Authored-By: Claude <noreply@anthropic.com>