fix: pin test_channel_slots reconcile tests to a frozen clock so long shards cannot decay eligibility - #9009
Conversation
…instant test_channel_slots.py froze NOW at module import while the reconcile pass under test derives its recency cutoff from the live clock, giving the module a 30-minute shelf life: any CI shard running longer aged every default stamp out of the window, failing all 10 nonzero reconcile assertions while the == 0 assertions kept passing vacuously (kirodotdev#8968). A frozen_clock fixture now pins time.time to NOW for the three classes that drive the real reconcile pass (TestReconcilePass, TestReconcileMore, and TestFailedTranscriptReadDefers, whose 60-minute window was the same landmine one slow shard further out). Stamps and cutoff share one clock, so eligibility is pure arithmetic at any elapsed time. TestReconcileClockCoherence pins the invariant: a fresh stamp survives simulated hours of shard elapsed time, the window still filters under a frozen clock, and the boundary stays strict (modified < cutoff). Fixes kirodotdev#8968
GPT 5.6 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. |
Design Review (Fable 5, fork) — ✅ PASSDesign-level review of The fix checks out against the base tree: Design-Verdict: PASS Correct root-cause fix at the right layer: pinning the one clock both stamp and cutoff read, with regression pins that simulate the failing shard directly. [DESIGN-REVIEWED] 7507161 |
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed |
iamwhatever
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: test (1 file). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: test-only change to test/test_channel_slots.py adding a frozen_clock fixture so reconcile tests pin the same clock the production recency cutoff reads, fixing #8968 where eligibility decayed with elapsed shard time; no production file touched. CodeQL is not applicable on this fork PR (default-setup emits no check-run); SAST coverage is Semgrep only, latest run success with 0 annotations.
Problem / Motivation
Backend Tests (3.12, N)fails with 10 errors intest/test_channel_slots.py(allassert 0 == 1/assert 0 == 2) on any shard that runs longer than ~30 minutes. The file passes in seconds when run alone, so it reads as a flake — but it is deterministic in elapsed time, and shard composition reshuffles (no committed.test_durations, so pytest-split balances by count) decide which PRs get hit. Observed on run 34021088434, where the shard ran 37:27 and all 10 failed on every xdist worker.Why it matters
Every PR is exposed: the slowest 3.12 shard already runs ~29 minutes, within one minute of the 1800s window this module silently depends on. Any shard growth or reshuffle trips it, costing contributors a spurious red CI round and re-run time. Worse, past the window the suite's
== 0assertions keep passing vacuously — everything is ineligible, so the tests stop testing what they were written to test before they fail loudly.What changed (motivation → approach → change)
NOW = time.time()is captured at module import and stamps sessions via_session(), whilereconcile_channel_slotsderives its cutoff from the live clock (cutoff = time.time() - window_minutes * 60). A session stampedmodified=NOWis eligible only whiletime.time() - NOW < 1800.frozen_clockfixture pinstime.timeto the module'sNOWfor the three classes that drive the real reconcile pass, so stamps and cutoff read one clock and eligibility becomes pure arithmetic at any elapsed time. Applied toTestReconcilePass,TestReconcileMore, andTestFailedTranscriptReadDefers— the latter was not in the CI failure list only because its window is 60 minutes: the same landmine one slow shard further out (reproduced: shiftingNOWby −4000s failstest_a_genuinely_empty_transcript_still_surfacesonmain).cutoff=NOW - 1800(self-consistent) or that legitimately bracket production writes with live-clock reads (TestClosedAtStamp,TestCompareAndClear,TestMtimeOf) are deliberately not frozen — freezing them would change what they verify.time.timealso makes the previously wall-clock-coupled assertions exact: the snapshot-cutoff bracket (before <= cutoff <= after) collapses to equality, and tombstone TTL arithmetic is unchanged.asyncioscheduling usestime.monotonic, which is untouched.Tests
TestReconcileClockCoherence::test_a_fresh_stamp_survives_any_shard_elapsed_time— the defect's exact firing point: a session stamped at the pass's own now surfaces even with 7200 simulated seconds elapsed since import (fails onmain's shape).TestReconcileClockCoherence::test_the_window_still_filters_under_a_frozen_clock— a stamp older than the window is still filtered: the freeze does not disable the recency rule, so== 0verdicts stay meaningful (guards against the vacuous-pass failure mode).TestReconcileClockCoherence::test_a_stamp_exactly_at_the_cutoff_is_eligible— boundary pin: eligibility filters strictly (modified < cutoff); a stamp exactly at the edge surfaces.NOW = time.time() - 2000(the issue's reproduction)mainfails the same 10 tests byte-for-byte; with this fix the full file passes 73/73 at rest, under −2000s, and under −4000s.channel_slots/chat_persistencepass (5063 tests).Manual verification
N/A — unit coverage sufficient: the defect and fix are both fully expressed in the test process (clock perturbation reproduces CI's elapsed-time condition deterministically).
Screenshots / video
Test-only change to one backend test file; zero shipped pixels, no UI surface touched.
Related Issues
Fixes #8968
Pattern harvest
Rule candidate: lint
Pattern: module-level
NOW = time.time()in a test file whose subject reads the live clock — stamps decay against the code under test as shard elapsed time grows; flag module-scope wall-clock captures used as default fixture stamps.Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)Contribution License Agreement
I confirm this contribution is made under the same terms as my prior merged contributions to this repository (#8835).