test_the_suite_does_not_write_to_the_PRODUCTION_log uses global mutable state as its oracle — any concurrent session reddens it
#822
Closed
opened 2026-08-22 23:51:22 +02:00 by timothy
·
2 comments
No Branch/Tag Specified
main
renovate/meziantou.analyzer-3.x
release/v26.15.0-notes
fix/830-add-items-error-surface
renovate/lucene.net
renovate/cliwrap-3.x
issue-806-guard-populations
renovate/dotnet-monorepo
scratch/767b-poisoned
scratch/767b-control
release/v26.14.0-notes
release/v26.14.0
renovate/sqlitepclraw.bundle_e_sqlite3-3.x
docs/510-skill-logo-bug-policy
fix/510-watermark-resolution-policy
fix/629-verdict-classifier-falseopens
fix/609-decisions-edit-token-scope
issue-135-clear-to-none
release/v26.12.0-notes
fix/409b-lastscan-api-parity
fix/401-updatechannel-mirror-422
fix/327-playlist-rename-validation
fix/410-scancancel-log-level
fix/409-447-librariesscreen-neverscanned
fix/338-zap-exit-code
fix/367-plex-budget-message
fix/310-debom-legacy-cs
ci/604-lane-rebalance
feat/388-design-mirror
feat/247-test-ownership
feat/247-primary-action
feat/357-player-owned-playback
feat/357-jellyfin-plugin-poc
fix/289-mcp-hardening
issue58-mcp
feat/244-channels-extract
ci/auto-bump-prod-compose
feat/multi-rerun-collections-api
feat/collections-api
feat/quick-wins
feat/185-docs-part2
feat/140-collections-screen
feat/146-channel-edit
feat/147-classic-ui-link
issue22-renovate-dashboard
feat/91-cutover
feat/63-composite-create
feat/65-library-browse
feat/85-epg
feat/86-schedule-editor
feat/109-dashboard-data
feat/99-session-tracking
fix/dockerfile-node-tag
feat/59-spa-foundation
docs/59-ui-redesign-brief
feat/102-json-guide
feat/111-schedule-durations
feat/104-artwork-upload
feat/103-media-sources-api
feat/playouts-read-api
feat/108-health-api
feat/105-picker-list-endpoints
issue-97-channel-state-api
issue42-jellyfin-musicvideos
issue46-rest-api-error-contract
dependabot/nuget/ErsatzTV.FFmpeg.Tests/multi-d307a2e06f
qsv-improvements
hdr-vulkan-cuda-test
v26.15.0
v26.14.0
v26.13.0
v26.12.0
v26.11.0
v26.10.0
v26.9.0
v26.8.0
v26.7.0
blazor-final
v26.6.0
v26.5.0
v26.4.0
v26.3.1
v26.3.0
v26.2.0
v26.1.1
v26.1.0
v25.9.0
v25.8.0
v25.7.1
v25.7.0
v25.6.0
v25.5.0
v25.4.0
v25.3.1
v25.3.0
v25.2.0
v25.1.0
v0.8.8-beta
v0.8.7-beta
v0.8.6-beta
v0.8.5-beta
v0.8.4-beta
v0.8.3-beta
v0.8.2-beta
v0.8.1-beta
v0.8.0-beta
v0.7.9-beta
v0.7.8-beta
v0.7.7-beta
v0.7.6-beta
v0.7.5-beta
v0.7.4-beta
v0.7.3-beta
v0.7.2-beta
v0.7.1-beta
v0.7.0-beta
v0.6.9-beta
v0.6.8-beta
v0.6.7-beta
v0.6.6-beta
v0.6.5-beta
v0.6.4-beta
v0.6.3-beta
v0.6.2-beta
v0.6.1-beta
v0.6.0-beta
v0.5.8-beta
v0.5.7-beta
v0.5.6-beta
v0.5.5-beta
v0.5.4-beta
v0.5.3-beta
v0.5.2-beta
v0.5.1-beta
v0.5.0-beta
v0.4.5-alpha
v0.4.4-alpha
v0.4.3-alpha
v0.4.2-alpha
v0.4.1-alpha
v0.4.0-alpha
v0.3.8-alpha
v0.3.7-alpha
develop
v0.3.6-alpha
v0.3.5-alpha
v0.3.4-alpha
v0.3.3-alpha
v0.3.2-alpha
v0.3.1-alpha
v0.3.0-alpha
v0.2.5-alpha
v0.2.4-alpha
v0.2.3-alpha
v0.2.2-alpha
v0.2.1-alpha
v0.2.0-alpha
v0.1.5-alpha
v0.1.4-alpha
v0.1.3-alpha
v0.1.2-alpha
v0.1.1-alpha
v0.1.0-alpha
v0.0.62-alpha
v0.0.61-alpha
v0.0.60-alpha
v0.0.59-alpha
v0.0.58-alpha
v0.0.57-alpha
v0.0.56-alpha
v0.0.55-alpha
v0.0.54-alpha
v0.0.53-alpha
v0.0.52-alpha
v0.0.51-alpha
v0.0.50-alpha
v0.0.49-prealpha
v0.0.48-prealpha
v0.0.47-prealpha
v0.0.46-prealpha
v0.0.45-prealpha
v0.0.44-prealpha
v0.0.43-prealpha
v0.0.42-prealpha
v0.0.41-prealpha
v0.0.40-prealpha
v0.0.39-prealpha
v0.0.38-prealpha
v0.0.37-prealpha
v0.0.36-prealpha
v0.0.35-prealpha
v0.0.34-prealpha
v0.0.33-prealpha
v0.0.32-prealpha
v0.0.31-prealpha
v0.0.30-prealpha
v0.0.29-prealpha
v0.0.28-prealpha
v0.0.27-prealpha
v0.0.26-prealpha
v0.0.25-prealpha
v0.0.24-prealpha
v0.0.23-prealpha
v0.0.22-prealpha
v0.0.21-prealpha
v0.0.20-prealpha
v0.0.19-prealpha
v0.0.18-prealpha
v0.0.17-prealpha
v0.0.16-prealpha
v0.0.15-prealpha
v0.0.14-prealpha
v0.0.13-prealpha
v0.0.12-prealpha
v0.0.11-prealpha
v0.0.10-prealpha
v0.0.9-prealpha
v0.0.8-prealpha
v0.0.7-prealpha
v0.0.6-prealpha
v0.0.5-prealpha
v0.0.4-prealpha
v0.0.3-prealpha
v0.0.2-prealpha
v0.0.1-prealpha
Labels
Clear labels
ad-hoc
api
bug
ci-cd
content
dependencies
enhancement
frontend
in-progress
jellyfin
parked
priority: high
priority: low
priority: medium
review
security
One-off / ad-hoc work not tracked by a dedicated issue
REST API / HTTP endpoints
Something isn't working
Build, test, deploy pipeline
Channel content / schedules / playlists
Dependency updates (Renovate)
New feature or improvement
ChicoryTV React SPA frontend
Claimed by an active session — do not pick up
Jellyfin tuner / IPTV integration
Excluded from automatic queue pickup; work only when explicitly selected
Adversarial review finding
Security / vulnerability fix
No labels
priority: medium
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: timothy/ersatztv#822
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found while running the suite during #807. Not a defect in #807's change; filed separately.
The race
scripts/tests/test_hook_fire_log.py::test_the_suite_does_not_write_to_the_PRODUCTION_logsnapshotsst_mtime_nsof~/.cache/ersatztv/hook-fire/*.jsonlbefore and after running a hook, and asserts the two are equal.That directory is the shared production hook-fire log that every live Claude Code session on this machine writes to. So any other session firing a hook inside that window flips an mtime and the test fails, reporting
a test run modified the production hook-fire log— an accusation about the suite, when the writer was an unrelated process.Observed: one red in a full-suite run, green in isolation immediately afterwards; independently corroborated during review, which found a file in that directory written 13 seconds earlier by another session.
This repo now routinely runs several sessions and worktree-isolated review agents at once, so this is a live and guaranteed race rather than a theoretical one.
Why it matters beyond the flake
The failure message is confidently wrong about the cause, which is the expensive part — it points the next reader at the test suite rather than at concurrency, and the honest diagnosis costs a re-run plus a directory listing to reach. A flake that misattributes is worse than one that just fails.
It also weakens a real guard: the invariant (the suite must not write to the production log) is worth keeping, and a test that cries wolf gets waved through, which is how
ci.decisions-lifecycle-flakealready has to be documented as "do nothing".Options
XDG_CACHE_HOME, making the production path unreachable from the suite by construction — dedup by construction rather than by assertion, perdocs/defect-shapes-773.md§4 detector C. This is probably the right one: it makes the property structural instead of observed.Done-when
~/.cache/ersatztv/hook-fire/Claiming #809 + #822 together as a single-mechanism bundle: both are the same guard
(
scripts/tests/test_hook_fire_log.py::test_the_suite_does_not_write_to_the_PRODUCTION_log) and thesame conftest isolation seam. #822 is the oracle reading global mutable state; #809 is the isolation
being per-file rather than per-suite. Both bodies independently land on the same structural answer —
make the production path unreachable from the suite by construction, with one place that builds the
hook subprocess env — so fixing them separately would mean building the mechanism twice.
Claude Code session, worktree off
origin/main.Closing record
Outcome: Fixed in http://192.168.1.95:3000/timothy/ersatztv/pulls/874, together with #809 — the two are one mechanism.
test_the_suite_does_not_write_to_the_PRODUCTION_logno longer reads~/.cache/ersatztv/hook-fire/at all. It now asserts that the environment a module sees at IMPORT time already resolves away from the shared log, and that a hook driven with that snapshot lands its records in the isolated directory.Root cause: The oracle was global mutable state. The assertion compared
st_mtime_nsacross a directory every live Claude Code session on the machine writes to, so any concurrent session's hook flipped an mtime inside the window. The failure message then blamed the suite, which is the expensive part — the honest diagnosis cost a re-run plus a directory listing.Decisions/conventions changed: Added
testing.suite-isolated-from-production-hook-fire-log, which states the rule as establish the property structurally, never by observing the shared directory.Reusable knowledge: The three options this issue listed were scope-the-assertion, lock/subdir, and make-the-path-unreachable. The third is the only one that holds — but not via
XDG_CACHE_HOMEas suggested:scripts/hook-fire-log.shderives its default from$HOME, notXDG_CACHE_HOME, so redirecting the latter changes nothing. Two further gotchas: a Python reimplementation of a shell${VAR:-default}fails open if it drifts, so it is differential-tested against the shell function every run; and it must comparerealpathidentity rather than bytes, because shell concatenation andpathlibrender the same directory differently (/x/vs/x,$HOME//.cachevs$HOME/.cache).Verification: The race was reproduced on demand rather than argued: with a thread touching a file in the shared directory, the withdrawn mtime oracle goes RED (
a test run modified the production hook-fire log) while the replacement stays GREEN and the records provably land in the isolated dir. The invariant still fails when genuinely violated — the launch guard rejects a launch carrying no isolated dir, one pointed at either shared log, one pointed inside a shared log, and one whose relative value resolves onto a shared log via the child'scwd; and the nested-pytest mutation proof shows records leaking into a fakeHOMEwhen the pre-collection isolation is removed. Full suite 1228 passed / 2 skipped.Deferred: none. (Guard blind spots are enumerated under #809.)
Docs updated:
docs/guard-inventory.md, the new decision record + regenerated catalog,docs/README.md, and docstrings intest_worktree_ownership_guard.py.