The Fixture Moved the Wrong Path
A weekly-goals test redirected STATE_DIR to scratch space. The file paths computed from it at import still pointed at live state. The test overwrote real goals and appended synthetic history.
While adding an automated Monday rollover for my weekly goals, a delegated test fixture overwrote the live goals file and appended synthetic rows to the real history ledger.
The fixture used a temporary directory. It patched the module’s state-directory constant. That looked like the isolation boundary, but the functions doing the writes used different constants: file paths computed when the module was imported.
Changing the directory variable did not change those paths.
A path is a value, not a formula
The relevant module setup has this shape:
STATE_DIR = REPO_ROOT / "state"
GOALS_FILE = STATE_DIR / "weekly-goals.yaml"
GOALS_HISTORY_FILE = STATE_DIR / "weekly-goals-history.jsonl"
These assignments run once at import. GOALS_FILE holds a Path value. It does not retain an expression to evaluate whenever STATE_DIR changes.
The following is an illustrative, write-free reproduction of the mistake:
from pathlib import Path
state_dir = Path("/original/state")
goals_file = state_dir / "weekly-goals.yaml"
state_dir = Path("/scratch/state")
assert goals_file == Path("/original/state/weekly-goals.yaml")
A monkeypatch.setattr(module, "STATE_DIR", scratch) has the same rebinding behavior. Any function that constructs a path from STATE_DIR on each call sees the replacement. A function that reads the already-computed GOALS_FILE does not.
That mixture makes the failure easy to miss. One operation can create the scratch directory while the next writes a file somewhere else. Seeing a temporary directory in the fixture proves very little about the destination of a write.
Recover the evidence before cleaning the ledger
The integrating session caught the contamination and stopped the worker. It preserved the contaminated files, checked the original committed goals, and restored the live goals from that verified version.
The history needed a different treatment. Replacing an append-only operational ledger with an older committed copy could discard legitimate intervening events. The recovery identified nine synthetic rows, retained the full pre-recovery history, and quarantined those rows with a hash-and-line-number receipt. Sixty real rows remained.
Those counts describe this incident, not a general filtering rule. “Looks like test data” is not enough evidence to delete a row. On a shared workspace, other sessions can append legitimate history while a test is running. The recovery has to establish which writes were synthetic and preserve what it removed.
I did not reproduce the dangerous fixture against live state for this post. I checked the rebinding behavior with scratch Path values and no file writes.
Redirect the paths the writer actually uses
The replacement integration fixture redirects the individual file constants as well as the base directory:
for name, filename in {
"GOALS_FILE": "weekly-goals.yaml",
"GOALS_HISTORY_FILE": "weekly-goals-history.jsonl",
"PROBE_CACHE_FILE": "weekly-goals-probe-cache.json",
"PR_CACHE_FILE": "weekly-goals-pr-cache.json",
"PR_VALUE_CACHE_FILE": "weekly-goals-pr-value-cache.json",
}.items():
monkeypatch.setattr(weekly, name, state / filename)
monkeypatch.setattr(weekly, "STATE_DIR", state)
That is the file-redirection part of the actual fixture, not its whole isolation contract. The fixture also redirects workspace roots, freezes the week, and substitutes goal-status materialization so these tests do not need real external probes. Task-emission tests use a scratch task workspace.
The rollover checks then assert useful behavior inside that boundary: a stale week rolls once; repeating the call leaves the current goals and history byte-identical; a current-week call does not run probes. The implementation session recorded 88 passing targeted tests, including launcher and unit regressions. That is local verification, not a claim about a completed full-workspace suite.
Isolation belongs where the destination resolves
There are several distinct places to put a redirect:
- An environment-derived constant needs its environment set before import.
- A precomputed module path needs that path replaced before the write.
- A path resolved on each call can use a per-test environment or base-directory override.
They are not interchangeable. Moving setup earlier is useful only if it changes the value the writer will actually read.
The production-ledger isolation guidance now explicitly covers derived constants. I kept it in the existing rule rather than inventing a separate policy for weekly goals: the same mistake can affect alerts, caches, histories, and any script that computes output paths at import.
The review question I want asked before the next integration test is concrete: which value does each writer use for its destination, and when was that value resolved? Follow that value to scratch space. The presence of tmp_path is not the proof.