Skip to content

test(host-agent): wait for finalize-record deletion — don't race the job's post-persist steps - #629

Merged
nikhilunni merged 1 commit into
mainfrom
fix/finalize-test-race
Jul 10, 2026
Merged

nikhilunni merged 1 commit into
mainfrom
fix/finalize-test-race

Conversation

@nikhilunni

Copy link
Copy Markdown
Contributor

What broke

Main went red on the macOS lane after #615 merged (run 29059847354, 1527/1528 passed): snapshot_begin_completes_and_produces_an_eviction_final_checkpoint panicked on "the in-flight finalize record must be deleted on completion".

Root cause — a test race that #615's dir-fsync widened, not a product bug

The production ordering is correct and unchanged (eviction_finalize.rs): persist the durable CheckpointRecord → delete the EvictionFinalizeRecord → clear pending_finalizes → best-effort destroy. But the test waited only for the checkpoint file to appear, then immediately asserted the finalize record was gone. The checkpoint file becomes visible at rename time, while the deletion happens only after persist() returns — and #615 added a parent-directory fsync (correctly — the classic durable-rename gap) after the rename, which on macOS is an F_FULLFSYNC costing tens of ms. The test's poll now wins that window with real probability; the very first main run carrying the fsync tripped it.

Fix

Wait for the deletion and the pending_finalizes clear the same way the test already waits for the destroy call, instead of asserting a point-in-time snapshot of a still-running background job. Test-only change.

Validation

  • Fixed test: 10/10 repeated local runs green (macOS, where the fsync latency is worst).
  • cargo nextest run -p engram-host-agent: 343 passed. clippy -D warnings + fmt --check clean.

🤖 Generated with Claude Code

…job's post-persist steps

snapshot_begin_completes_and_produces_an_eviction_final_checkpoint
waited only for the CheckpointRecord file to appear, then immediately
asserted the EvictionFinalizeRecord was deleted and pending_finalizes
cleared. The checkpoint file becomes VISIBLE at rename time, but the
finalize job deletes the record only after persist() returns — which,
since the durable-record engine grew a parent-directory fsync (an
F_FULLFSYNC on macOS, tens of ms), happens measurably AFTER the rename.
The test's poll can now win that window: main went red on exactly this
assertion (run 29059847354) the first time the fsync landed.

The production ordering is correct and unchanged (checkpoint durable →
delete finalize record → clear pending_finalizes → destroy sandbox);
the test was asserting a point-in-time snapshot of a still-running job.
Wait for the deletion and the map-clear the same way the test already
waits for the destroy. Fixed test passes 10/10 repeated runs locally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nikhilunni
nikhilunni merged commit 605de6a into main Jul 10, 2026
18 checks passed
@nikhilunni
nikhilunni deleted the fix/finalize-test-race branch July 10, 2026 00:47
nikhilunni added a commit that referenced this pull request Jul 18, 2026
… oracle, the gap-family seeds

TTL clock -> now_mono: MigrationExport carries the injected clock;
created_at/last_activity are now_mono readings and expired() subtracts
against the injected clock — expiry DECIDES destroy/abort, so it is
decision-feeding time (D1), off the metrics_now carve-out it previously
rode. The prod constructors bind PooledBackend.clock; the Linux-only
peer page server (PeerExport, which shares the same Arc anchor — #216
Gap 2) converts with it, carrying the same injected clock. The paused
sim clock now drives the REAL expired() deterministically. (The musl
cross-check caught the Linux-only PeerExport half — the macOS sweep
can't see migrate_peer.rs.)

Sim (engram-dst-host): steps MigrationBegin (a REAL MigrationExport in
the REAL MigrationRegistry; deterministic entropy-minted export id —
the prod OsRng nonce must not launder into the replayable id stream;
the guest freezes exactly as the export's held capture lock excludes
writes/flushes/captures — and the Flow F interleaving steps now gate on
it, which also fixes a hang the swarm found: a flush step on a frozen
slot armed a seam an empty pipeline never reached), MigrationServeState
(the split-brain flag), MigrationTouch, MigrationTtlSweep (REAL
expired() + ttl_verdict over the scriptable coordinator's ownership
answer, applied as lib.rs does), MigrationCommit, MigrationAbort — in
both swarm profiles (pick roll widened 0..112; old seeds re-explore,
fine per the seed contract).

Oracle #7 (the #216 decision table): state_served => never
abort-unpause — a split-brain un-pause is structurally recorded by the
sweep/abort appliers, so a ttl_verdict regression or bypassing caller
fires it. Oracle #2 (no-plane-leak): migrating <=> an open registry
export, with a live backend — no frozen guest ever leaks without an
export to end it.

Seeds: ttl_expired_unshipped_export_aborts_in_place_zero_loss,
state_served_export_never_unpauses_then_destroys_on_ownership_flip,
actively_serving_export_never_expires_mid_transfer (#216 Gap 1),
unreachable_coordinator_stays_paused_never_guesses,
reattached_source_verdict_never_destroys_on_a_transient_binding (Gap 3).

Scope notes (recorded in the ADR row): #582/#598/#629 turned out to be
FC-lane/test-hygiene issues whose portable content P5/P7 already
absorbed — no hollow seeds manufactured. HostEffects::production
consolidation deliberately retired rather than done: every seam reaches
its flow through its own field; the bundle ctor remains the sim's
assembly point (the pooled TODO now says so).

ADR: P8 row -> Landed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant