RingFacts · 9 August 2026
Three tiers, split by what each one needs rather than by what it is called. That single distinction is the whole design: this repository commits to itself, unattended, every six hours, so the tests that gate a commit have to run with no network, no API keys, and no database.
Everything else follows from where the gate falls.
Before this, verification was four scripts at the repository root, each written the day something broke. None ran together, most needed live credentials, and nothing failed when they did. That was survivable while every commit was typed by hand.
It stopped being survivable once the check-in runs began committing code at 4am.
self-improvement.md §4 tells those runs to verify before claiming —
but a principle nothing enforces is a principle that erodes. So the tests that
gate a commit must be runnable by an agent with nothing mounted.
The gate covers the two tiers that need nothing. Tier 3 skips itself when
TEST_DATABASE_URL is unset, so it never blocks a session that has no
secrets. The fourth row is not a test at all — see below.
What each one is responsible for, and how it avoids the network.
deps parameter whose every default is the real implementation, so production behaviour is unchanged. Tests swap in a scripted matcher, a scripted embedder, and an in-memory store. Articles are handed in directly, so no feed is ever fetched.bigint and Postgres does. A fake that returned numbers would hide the exact bug that nearly shipped last week.ON CONFLICT returns null instead of throwing, or whether the schema still has the columns the code writes. Those need real Postgres.NO_CLAIM three times and NEW once. Any assertion built on that flakes, and a suite you learn to ignore is worse than no suite at all. So the matcher is stubbed in every tier above.Four real defects, none of which were found by reasoning about the code.
found by — a dry run against live feeds
The new dependency object was written with translateToEnglish as
JavaScript shorthand, so its key was translateToEnglish while the call
site read deps.translate. Translation failed on every foreign-language
article — and because that path fails open, the original headline posted and
nothing looked wrong.
All thirty pipeline tests passed, because each one supplies its own dependencies and none exercised the defaults. The suite now checks its own wiring at the source level, and that check has been verified to fail when the bug is put back.
found by — mutating the thresholds on purpose
Changing the duplicate threshold broke three tests; changing the tier rule broke three more. Changing how far back the hunt looks broke nothing — every test pinned that value explicitly, so the filter itself was never exercised. Two tests added.
found by — tier 3, on its first run
The schema declares a 768-wide vector column; the embedder asked the API for 768 as a private constant. Changing either alone would have failed every insert at runtime in the cloud, with no local signal. The constant is now shared, and the test asserts the columns match it.
found by — reading the files being committed
Two tracked files still named an actual tracked subject — the thing a commit last week set out to remove. Both now use a synthetic name, as the fixtures do.
The first command is the one that matters.
# every commit runs this automatically; 0.43s, offline npm test # once per clone, to enable the gate git config core.hooksPath .githooks # the SQL tier, against a branch — never main TEST_DATABASE_URL=$(neonctl connection-string test \ --project-id calm-mouse-60802247) npm run test:sql # still required before deploying — stubs cannot see real wiring DRY_RUN=1 node hunter.js
The tests do not replace the dry run. They stub the network on purpose, which means they cannot see whether the real parts are plugged into each other. Stubs verify logic; only a real run verifies wiring — which is exactly how the translation bug was caught after the whole suite went green.
Each omission is a decision, not an oversight.
No CI. The commit hook is the gate. Deployment runs from
setup.sh, not a pipeline, so a hosted workflow would test code that is
already committed and add a second place for the rules to live.
No coverage threshold. A percentage target rewards testing whatever is cheapest to test — here that would be the message formatters, not the name filter that can silently lose an entire outlet.
No mocking library. The seams are ordinary function parameters with real defaults. Nothing is patched at runtime.
The audit scripts stay as they are. They measure production data to tune thresholds. That is instrumentation, not testing, and merging the two would break the measurement discipline the thresholds depend on.