Skip to content

fix(class-a): inject frozen clock into rotation-window tests (#1432) - #1434

Merged
ozand merged 2 commits into
mainfrom
fix/1432-frozen-class-a-clock
Sep 8, 2026
Merged

fix(class-a): inject frozen clock into rotation-window tests (#1432)#1434
ozand merged 2 commits into
mainfrom
fix/1432-frozen-class-a-clock

Conversation

@ozand

@ozand ozand commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #1432 by making the Class-A ledger window reference instant injectable end to end.

  • The two previously unpinned assertions in tests/test_class_a_windows.py now pass now=NOW.
  • _dedup_exhausted(..., now=...) now uses the supplied instant for both cutoff and ledger-window selection.
  • has_in_flight_experiment(..., now=...) now passes the supplied instant into its ledger-window selection.
  • demand._filter_exhausted(..., now=...) threads its instant through fallback timestamp handling and ledger reads.
  • state_access.ledger_window(..., now=...) uses the supplied instant for retention classification.
  • The three rotation-boundary tests were retained and not skipped.

The production changes are intentional API additions: both functions previously had no injectable clock, so adding keyword-only now parameters was required to test their existing decisions deterministically. No decision logic or thresholds were changed.

Clock-forward proof

The frozen fixture suite passes with the actual process clock forced one month after the fixture instant using freezegun:

16 passed in 0.00s

The ordinary focused run also passes:

437 passed in 62.31s

Assertion sweep

Every Class-A assertion that calls a time-window consumer supplies now=. The only remaining no-now assertion in the relevant sweep is a source-inspection assertion (inspect.getsource(llm_proposer)), not a time-window call. Other assertions inspect JSON bytes, statuses, classifications, or in-memory values and are not clock-dependent.

Outward production sweep

The Class-A path is now injectable through state_access.ledger_window. Other production modules still contain independent wall-clock retention/window calculations outside this issue's scope, notably:

  • nanobot/runtime/llm_proposer.py: demand-cooling window uses datetime.now() internally in _recent_duplicate_failure_cooling.
  • nanobot/runtime/state_access.py: artifacts(..., max_age_hours=...) computes its own requested instant from datetime.now().
  • nanobot/runtime/action_index.py, cycle_ledger.py, reflector.py, and skill_candidate_mining.py also contain retention/rotation clocks.

These were named only; they are not changed by this PR.

Full suite / baseline

Full Windows suite on this branch:

4 failed, 3562 passed, 18 skipped, 11 warnings in 1987.66s

The remaining four failures are the known unrelated Windows baseline:

  • tests/test_bridge_cycle_tags.py::TestBridgeCycleTagIntegration::test_fail_open_tag_failure_never_breaks_a_green_cycle
  • tests/test_bridge_locking.py::TestAcquireBridgeLock::test_acquires_when_free
  • tests/test_bridge_locking.py::TestAcquireBridgeLock::test_second_acquire_in_same_process_is_contended
  • tests/test_bridge_locking.py::TestAcquireBridgeLock::test_contended_lock_via_monkeypatched_flock

git diff --check passes and the worktree is clean.

@ozand

ozand commented Sep 8, 2026

Copy link
Copy Markdown
Owner Author

Merging. CI green on all three Pythons, which returns the baseline to exactly the four known Windows failures.

The clock-forward proof is the part that makes this a fix rather than a reset. The suite passing today proves nothing — that is exactly the state the file was in yesterday. Demonstrating it under a process clock forced a month past the fixture instant is what shows the dependency is actually gone.

The production change is real and correctly declared. Four functions gained a keyword-only now, and the pattern is uniformly backwards-compatible:

ref = now or datetime.now(timezone.utc)

so every existing call site keeps reading the real clock and every decision threshold is untouched. Saying plainly in the body that these are intentional API additions — rather than slipping them in under "test fix" — is the right way to handle it; a test change that quietly alters production signatures is how a fix becomes a regression nobody looks for.

And the outward look found more than the two assertions. I asked you to name any production code computing a window from a non-injectable clock and stop. You found four — _dedup_exhausted, has_in_flight_experiment, demand._filter_exhausted, state_access.ledger_window — and threading the instant through all of them was necessary to make the tests deterministic rather than merely green. That is a wider fix than the issue asked for and the right one, because a window computed from an unpinnable clock is untestable by construction; the three red tests were the visible corner of it.

Worth recording for whoever meets this next: the failure did not look like a clock bug. It looked like two unrelated branches breaking the same three tests, and I read it as those branches' fault and sent an agent hunting. Two independent changes failing identically is the signal — I should have run the tests on main before asserting its CI status meant anything.

@ozand
ozand merged commit 3186749 into main Sep 8, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant