Skip to content

fix(test): derive the prune cut from recorded timestamps, not a fixed 6s - #5990

Open
Hmbown wants to merge 2 commits into
mainfrom
fix/snapshot-prune-windows-flake
Open

fix(test): derive the prune cut from recorded timestamps, not a fixed 6s#5990
Hmbown wants to merge 2 commits into
mainfrom
fix/snapshot-prune-windows-flake

Conversation

@Hmbown

@Hmbown Hmbown commented Sep 7, 2026

Copy link
Copy Markdown
Owner

No-Issue: windows-only test flake found while verifying an unrelated PR; sibling of the flake work in #5929/#5980 but not listed there.

snapshot::repo::tests::prune_older_than_keeps_the_newest_and_drops_only_the_old_tail fails intermittently on windows-latest:

assertion `left == right` failed: only the old tail should be removed
  left: 3
 right: 2

Why

The fixture builds two old snapshots, sleeps 8s, then two new ones 1.1s apart, and cuts at a hardcoded 6s. That assumes repo.snapshot() is fast — new:0 is only ~1.2s plus one git subprocess older than prune time, so on a loaded Windows runner that subprocess alone carries it past the 6s line and it gets pruned with the old pair.

The existing fixture guard could not catch this: it asserts on before[0] and before[2], and before[1] is the entry that drifts.

The fix

The cut is now derived from the timestamps the repo actually recorded — aim at the midpoint of the gap between the oldest survivor and the newest victim, leaving ~4s of slack in both directions instead of depending on wall-clock luck. The gap itself is asserted first, so a collapsed fixture says so plainly rather than failing later as a confusing count mismatch.

Behaviour under test is unchanged: two removed, new:1 and new:0 survive. No production code is touched.

cargo clippy -p codewhale-tui --lib          -> 0 errors
cargo test -p codewhale-tui --lib -- prune_older_than
  -> test result: ok. 3 passed; 0 failed

Found when it failed the windows leg of #5987 — a PR containing zero Rust files (TypeScript, CI config and .gitignore only), so it cannot have been caused there.

🤖 Generated with Claude Code

https://claude.ai/code/session_01D4rk4NXwyy6wmvii9Lp84P


Devin Review

Note

Low Risk
Only adjusts a unit test fixture; snapshot pruning behavior in production is untouched.

Overview
Fixes intermittent failures of prune_older_than_keeps_the_newest_and_drops_only_the_old_tail on slow Windows CI, where a hardcoded 6s prune_older_than cut could prune new:0 along with the old pair when repo.snapshot() (git subprocess) ran long enough.

The test now computes max_age from listed commit timestamps: it checks the gap between before[1] and before[2] (the boundary between new and old pairs), uses the midpoint of that gap as the cutoff, then prunes with that derived age. The old wall-clock guard on before[0] / before[2] is removed because it did not catch drift on before[1]. Expected behavior is unchanged: two removals, new:1 and new:0 survive.

Test-only — no production snapshot/prune logic changes.

Reviewed by Cursor Bugbot for commit 1e95daf. Bugbot is set up for automated code reviews on this repo. Configure here.

`prune_older_than_keeps_the_newest_and_drops_only_the_old_tail` fails
intermittently on windows-latest with

  assertion `left == right` failed: only the old tail should be removed
    left: 3
   right: 2

The fixture builds two old snapshots, sleeps 8s, then two new ones 1.1s
apart, and cuts at a hardcoded 6s. That assumes `repo.snapshot()` is fast:
`new:0` is only ~1.2s plus one git subprocess older than prune time, so on
a loaded Windows runner that subprocess alone carries it past the 6s line
and it is pruned with the old pair.

The existing fixture guard could not catch it — it asserts on `before[0]`
and `before[2]`, and `before[1]` is the entry that drifts.

The cut is now computed from the timestamps the repo actually recorded:
aim at the midpoint of the gap between the oldest survivor and the newest
victim, which leaves ~4s of slack in both directions instead of depending
on wall-clock luck. The gap itself is asserted first, so a fixture that
collapsed says so plainly rather than failing later as a count mismatch.

Behaviour under test is unchanged: two removed, `new:1` and `new:0`
survive. No production code is touched.

  cargo clippy -p codewhale-tui --lib -> 0 errors
  cargo test -p codewhale-tui --lib -- prune_older_than
    -> test result: ok. 3 passed; 0 failed

Found when it failed the windows leg of #5987, a PR containing zero Rust
files (TypeScript, CI config and .gitignore only), so it cannot have been
caused there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D4rk4NXwyy6wmvii9Lp84P
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Copilot AI lite review requested due to automatic review settings September 7, 2026 08:27
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@cursor

cursor Bot commented Sep 7, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_ce0e407c-bf19-4a34-ab62-52fa8662bdfe)

@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 1m 39s —— View job


Review: fix(test) — derive prune cut from recorded timestamps

  • Gather context (diff, changed file)
  • Understand request (code review of the Windows flake fix)
  • Review correctness of the new prune-cut derivation logic
  • Check test coverage / edge cases
  • Post final review

Summary: This is a test-only change to prune_older_than_keeps_the_newest_and_drops_only_the_old_tail in crates/tui/src/snapshot/repo.rs. No production code is touched, matching the PR description.

I traced the new cut derivation against prune_older_than's actual semantics (crates/tui/src/snapshot/repo.rs:661-686, cutoff = now - max_age, entries pruned via timestamp <= cutoff):

  • survivor = before[1].timestamp (new:0), victim = before[2].timestamp (old:1) — correct given list() returns newest-first (git log default order), so before = [new:1, new:0, old:1, old:0].
  • midpoint = victim + (survivor - victim) / 2 with the survivor - victim >= 2 guard is enough to keep the midpoint strictly between the two timestamps even at the minimum allowed gap (diff=2 → midpoint = victim+1, strictly inside), so the boundary <= comparison in prune_older_than can't accidentally catch the survivor or miss the victim.
  • max_age = now - midpoint correctly converts the target cutoff back into the duration argument prune_older_than expects.
  • The one thing to note: the test computes now once, then prune_older_than recomputes now internally at call time — a few ms could pass in between, nudging the effective cutoff slightly later. Given the ~4s midpoint slack this is a complete non-issue, just flagging that it's not perfectly hermetic.

This correctly fixes the bug described: the old fixture asserted on before[0]/before[2] and hardcoded a 6s cutoff, which didn't account for new:0's own snapshot() git-subprocess latency pushing it past 6s under load. Deriving the cut from the actually-recorded gap (and asserting that gap exists first) removes the wall-clock dependency that caused the intermittent Windows failure, while preserving the same asserted behavior (2 removed, new:1/new:0 survive).

Minor nit (non-blocking): lines 1398-1403 (untouched by this diff) still describe the old fixed cutoff — "at prune time the newest pair is ~0-1.2s old against a 6s cutoff" — which is now stale since the cutoff is derived dynamically rather than fixed at 6s. Worth a follow-up tweak so a future reader isn't misled about which cutoff is actually in effect; not a functional problem. Fix this →

I was unable to execute cargo test/cargo clippy in this sandbox (Bash tool calls required approval that wasn't granted), so I verified the logic by manual trace against prune_older_than's implementation rather than by running the suite. The PR body's reported local run (3 passed; 0 failed) is consistent with that trace.

No provider/model/route safety concerns (unrelated code path), no reuse/duplication issues, no security concerns.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new fixture guard/cutoff computation has a couple of edge-case and intent-mismatch issues that can still yield confusing failures under clock skew or weakened timing gaps.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Updates the snapshot::repo pruning regression test to avoid Windows CI flakes by deriving the prune cutoff from the snapshot timestamps actually recorded by git, rather than relying on a hardcoded 6-second age threshold.

Changes:

  • Compute max_age from the midpoint between the newest “victim” and oldest “survivor” snapshot timestamps.
  • Add a fixture guard asserting there is a real time gap between the “old” and “new” snapshot pairs before pruning.
File summaries
File Description
crates/tui/src/snapshot/repo.rs Adjusts the pruning unit test to compute the cutoff from recorded commit timestamps to reduce timing-related flakes on slow runners.
Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +1435 to +1436
let midpoint = victim + (survivor - victim) / 2;
let max_age = Duration::from_secs((now - midpoint).max(0) as u64);
Comment on lines 1431 to 1434
assert!(
now - before[0].timestamp < 6 && now - before[2].timestamp > 6,
"fixture ages unusable for a 6s cut (newest {}s, oldest-surviving-pair {}s)",
now - before[0].timestamp,
now - before[2].timestamp
survivor - victim >= 2,
"fixture needs a real gap between the pairs (survivor {survivor}, victim {victim})"
);

@codewhale-agent codewhale-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codewhale review

Tightens a Windows-flaky snapshot prune test by deriving the prune age from the recorded snapshot timestamps instead of a hardcoded 6 seconds, with a guard that the two pairs are separated.

Findings

  • [INFO] Prune cutoff still uses a wall-clock read before the prune call (crates/tui/src/snapshot/repo.rs:1421)
    now is captured before the midpoint assertion, and max_age is computed from that value. repo.prune_older_than likely reads the current time internally, so any delay between the two time reads shifts the effective cutoff later by the same amount. The fixture's 8s gap leaves enough slack for this to be unlikely, but the new guard only requires a 2s gap, which would leave little margin if the fixture ever produced such a small gap.

Assessment

Test-only change is sound and addresses the root cause of the flake without touching production code. The remaining wall-clock race is minor and acceptable given the fixture timing.


Advisory review by Codewhale (codewhale review --pr 5990 --post, head 0983f67143b14b75e894c59e7d75c467c697b915). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.

// three snapshots were pruned instead of two. (The old fixture guard
// could not catch it either — it checked `before[0]` and `before[2]`,
// and `before[1]` is the entry that drifts.)
let now = std::time::SystemTime::now()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[INFO] Prune cutoff still uses a wall-clock read before the prune call

now is captured before the midpoint assertion, and max_age is computed from that value. repo.prune_older_than likely reads the current time internally, so any delay between the two time reads shifts the effective cutoff later by the same amount. The fixture's 8s gap leaves enough slack for this to be unlikely, but the new guard only requires a 2s gap, which would leave little margin if the fixture ever produced such a small gap.

@cursor

cursor Bot commented Sep 7, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_4f9c1305-4f7d-4f50-842e-8f5686fbd630)

@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 1m 38s —— View job


Codewhale review — PR #5990

  • Gather context (diff, changed file)
  • Understand request (code review of the prune-cut derivation, per repo custom review instructions)
  • Review correctness of the new prune-cut derivation logic
  • Check provider/model/route safety (N/A — no provider/model code touched)
  • Check reuse/simplification and test coverage
  • Post final review

Scope: This sync did not change the reviewed diff — HEAD (1e95daf) is just main merged into the branch on top of the same 0983f67 test fix already present. Re-reviewed crates/tui/src/snapshot/repo.rs against origin/main for correctness.

Summary: Test-only change to prune_older_than_keeps_the_newest_and_drops_only_the_old_tail. No production code is touched. I traced the derivation against prune_older_than's actual semantics (crates/tui/src/snapshot/repo.rs:661-686, cutoff = now - max_age, pruned when timestamp <= cutoff, newest-first list):

  • survivor = before[1].timestamp (new:0), victim = before[2].timestamp (old:1) is the correct pair given list() returns newest-first.
  • midpoint = victim + (survivor - victim) / 2 with the survivor - victim >= 2 guard keeps the midpoint strictly inside (victim, survivor) even at the minimum allowed gap, so the <= boundary in prune_older_than can't catch the survivor or miss the victim — verified this at the actual fixture gap (~8s) and at the guard's floor (2s).
  • Behavior under test is unchanged: old:0/old:1 removed, new:1/new:0 survive.

Non-blocking observations (all already surfaced by prior automated reviews on this PR and still present, since the code hasn't changed since):

  1. crates/tui/src/snapshot/repo.rs:1398-1403 — the comment block above the 8s sleep still says "against a 6s cutoff" / describes fixed-cutoff slack math. That's stale now that the cutoff is derived dynamically; worth a follow-up wording pass so it doesn't mislead a future reader. Fix this →
  2. crates/tui/src/snapshot/repo.rs:1421-1436now is read once in the test, then prune_older_than re-reads SystemTime::now() internally; any delay between the two reads shifts the effective cutoff later by that amount, and max_age = (now - midpoint).max(0) would floor to a 0s retention (pruning everything) if that shift ever inverted the sign. At the fixture's real ~8s gap this has ~4s of slack and is a complete non-issue; it would only bite if the fixture's gap were shrunk toward the guard's 2s floor.
  3. The survivor - victim >= 2 guard is a sanity check on the fixture, not an enforcement of the "~4s slack" the neighboring comment describes — fine as-is since the real gap is ~8s, just noting the assert is looser than the comment implies.

None of these are blocking; they're pre-existing, low-severity documentation/robustness notes on a test-only file. No reuse/duplication issues, no security concerns, no provider/model/route code touched.

@codewhale-agent codewhale-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codewhale review

The PR replaces the hard-coded 6s prune threshold in a snapshot test with a midpoint derived from recorded timestamps, preventing slow Windows runners from pruning an extra new snapshot. The change is test-only and preserves expected surviving snapshots.

Findings

  • [INFO] Fixture guard does not enforce the claimed ~4s of slack (crates/tui/src/snapshot/repo.rs:1432)
    The new assertion only requires the boundary gap to be at least 2 seconds, so the midpoint could be only 1 second from each pair if the fixture is changed. Integer-second truncation of now plus the small delay before prune_older_than can shift the effective cutoff by up to about 1 second, so a 2-second gap would not be robust. Since the fixture sleeps 8 seconds, consider asserting survivor - victim >= 8 to preserve the intended slack.
  • [INFO] max(0) silently coerces an invalid fixture to a zero duration (crates/tui/src/snapshot/repo.rs:1436)
    If now is not after midpoint (e.g. system clock adjustment), (now - midpoint).max(0) produces Duration::ZERO, which can prune all or none of the snapshots and produce a confusing count mismatch. An explicit assertion that now > midpoint would fail the fixture clearly instead.

Suggestions

  • crates/tui/src/snapshot/repo.rs:1432 — Require the actual 8-second sleep gap so the midpoint has the intended ~4 seconds of slack on both sides, instead of allowing a 2-second gap that leaves only 1 second of slack.

                survivor - victim >= 8,
    
  • crates/tui/src/snapshot/repo.rs:1436 — Fail the fixture clearly when the derived cut would be in the future rather than coercing a negative age difference to zero.

            assert!(now > midpoint, "fixture timestamps are in the future");
            let max_age = Duration::from_secs((now - midpoint) as u64);
    

Assessment

The fix is a sensible test-only change that directly addresses the Windows flake. The only remaining concerns are minor fixture-guard hardening opportunities; no production behavior is affected.


Advisory review by Codewhale (codewhale review --pr 5990 --post, head 1e95daf39ee4e1e07d3a3aae95d1431f8e8a39ca). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.

"fixture ages unusable for a 6s cut (newest {}s, oldest-surviving-pair {}s)",
now - before[0].timestamp,
now - before[2].timestamp
survivor - victim >= 2,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[INFO] Fixture guard does not enforce the claimed ~4s of slack

The new assertion only requires the boundary gap to be at least 2 seconds, so the midpoint could be only 1 second from each pair if the fixture is changed. Integer-second truncation of now plus the small delay before prune_older_than can shift the effective cutoff by up to about 1 second, so a 2-second gap would not be robust. Since the fixture sleeps 8 seconds, consider asserting survivor - victim >= 8 to preserve the intended slack.

"fixture needs a real gap between the pairs (survivor {survivor}, victim {victim})"
);
let midpoint = victim + (survivor - victim) / 2;
let max_age = Duration::from_secs((now - midpoint).max(0) as u64);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[INFO] max(0) silently coerces an invalid fixture to a zero duration

If now is not after midpoint (e.g. system clock adjustment), (now - midpoint).max(0) produces Duration::ZERO, which can prune all or none of the snapshots and produce a confusing count mismatch. An explicit assertion that now > midpoint would fail the fixture clearly instead.

"fixture ages unusable for a 6s cut (newest {}s, oldest-surviving-pair {}s)",
now - before[0].timestamp,
now - before[2].timestamp
survivor - victim >= 2,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Require the actual 8-second sleep gap so the midpoint has the intended ~4 seconds of slack on both sides, instead of allowing a 2-second gap that leaves only 1 second of slack.

Suggested change
survivor - victim >= 2,
survivor - victim >= 8,

"fixture needs a real gap between the pairs (survivor {survivor}, victim {victim})"
);
let midpoint = victim + (survivor - victim) / 2;
let max_age = Duration::from_secs((now - midpoint).max(0) as u64);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fail the fixture clearly when the derived cut would be in the future rather than coercing a negative age difference to zero.

Suggested change
let max_age = Duration::from_secs((now - midpoint).max(0) as u64);
assert!(now > midpoint, "fixture timestamps are in the future");
let max_age = Duration::from_secs((now - midpoint) as u64);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants