The assertion read assert 0 == 2. Three tests in tests/dvlaw/test_post.py, failing simultaneously. The branch was fix/thesis-spawn-circuit-breaker. No code touching post_today had changed since the last green run. The only difference was a production config flag: auto_mode switched back on in dvlaw.db as part of the 2026-09-17 time-gate fix.
That was the clue I missed for longer than I should have.
What post_today does
post_today is the entry point for the article pipeline Telegram poster. Each morning the launchd cron calls it, reads today’s article digest, and (when auto_mode is on) posts each candidate to Telegram without waiting for an explicit trigger. When auto_mode is off, it stages the candidates and waits.
The flag lives in dvlaw.db. post_today opens its own SQLite connection at the start of every run to read it. Not a parameter. Not a fixture. A direct connection to the database at the path resolved from the environment, which in the test runner on my machine pointed at the real operator database.
How the tests worked before
Each of the three tests asserted that post_today respected auto_mode: set up a scenario, call the function, check message counts. The problem: they never gave post_today a hermetic database. The fixtures covered the triage data (candidates, digest date, the usual setup) but not the config connection post_today opens independently.
As long as the real dvlaw.db had auto_mode = 0, the tests behaved correctly. That had been the case since auto_mode was first introduced. The tests passed every run. Nobody noticed the coupling.
On 2026-09-17, the time-gate fix landed. Validating the new sourcing behaviour under production-equivalent config required auto_mode on. The moment dvlaw.db had auto_mode = 1, three tests that expected zero outbound messages started seeing two. The assertion numbers came from the real database state, not the fixture.
The failure shape
assert 0 == 2 is not a random failure. It is a test reading side-channel state that lives outside the fixture and inside the real system. The 0 was the test’s expected message count under its locally-configured scenario. The 2 was the actual output, driven by the real auto_mode flag and whatever candidates were genuinely in the live database at run time.
The tests were passing for the right reason on the wrong foundations. They passed because production config happened to match what they assumed. That’s not a test. That’s a coincidence.
Why it surfaces at a config change
A test that reads from the real database will fail at exactly the moment production state diverges from the test’s hidden assumption. Not when a code bug lands, not when a dependency changes. Only when a config change creates the mismatch.
This failure mode is awkward because the assertion diff (0 != 2) gives no indication that the source of 2 is outside the code under test. The natural first assumption is that post_today has a regression. That assumption costs time before you trace back to the isolation gap.
In this case, the 2026-09-17 devlog entry made the causality clear: auto_mode was the only thing that had changed, and the failures appeared the same day. Without that anchor the debug cycle would have been longer.
The fix
Making the three tests hermetic meant giving post_today a temp database for its config connection, one where auto_mode is set to exactly what the test requires.
The pattern is the same as the rest of the article pipeline test suite: a temp_db fixture in conftest.py that initialises a fresh SQLite in a tmp_path, runs the migrations, and exposes a connection. The three non-hermetic tests needed the same treatment, extended to cover the config table that post_today reads independently.
Once the temp db had the correct auto_mode value for each test’s scenario, post_today read from the fixture and the assertions held regardless of what the real dvlaw.db on disk contained.
The shape of the original mistake
The tests were written when auto_mode was off in production and nobody was thinking about the independent connection post_today opens. The fixture covered the input side (triage candidates, digest data) but not the configuration side. The config read looked like an internal implementation detail, not a test surface.
It stopped looking like an internal detail the moment it started producing test-observable outputs from outside the fixture.
The tell is the independent connection. Any function that opens its own database connection, rather than accepting one as a parameter, can read state the test fixture doesn’t control. That’s not a design problem in isolation. post_today is an operator-level entry point and opening its own connection is correct in production. In tests, it means the config table is also a test surface, not just a detail.
What to check for
When a test suite has a conftest.py temp_db fixture, the natural assumption is that all database reads go through it. That assumption holds only when every connection in the code under test is passed in rather than opened internally. The places to audit:
- Functions that open connections by resolving an env-var path at call time
- Module-level connection initialisation that runs at import
- Config reads factored away from the main data reads into a separate
open()call
Any of these can produce a test that reads from the real system and fails only when real state diverges from the assumption.
Out of scope: changing post_today’s production signature to accept an injected connection. It is an operator entry point. The test fix was the right intervention.
The devlog chain
The 2026-09-17 entry covers the time-gate fix and the auto_mode change that triggered the test failures as a side-effect. The 2026-09-18 entry covers the test hermetics as a separate, causally-linked follow-up. Two entries, two commits, one visible chain in the log.
If both had been bundled into one commit, the test isolation problem would have been an incidental correction inside a larger change, invisible as a distinct failure pattern. Separate commits make the causal structure legible.
The fix for the three tests is a small conftest extension. The time between the original tests being written and the isolation gap surfacing is a normal trajectory for a solo project. The lesson: recognise the pattern at write time. Independent connections in operator entry points are a test isolation surface. Fixture coverage should extend to them.


