Skip to content

Fix flaky diff_pipeline_computes_added_lines_and_ghost_blocks TUI test#13680

Draft
warp-dev-github-integration[bot] wants to merge 1 commit into
masterfrom
oz/fix-flaky-diff-pipeline-ghost-blocks-test
Draft

Fix flaky diff_pipeline_computes_added_lines_and_ghost_blocks TUI test#13680
warp-dev-github-integration[bot] wants to merge 1 commit into
masterfrom
oz/fix-flaky-diff-pipeline-ghost-blocks-test

Conversation

@warp-dev-github-integration

Copy link
Copy Markdown
Contributor

Description

Fixes the flaky test diff_pipeline_computes_added_lines_and_ghost_blocks in crates/warp_tui/src/tui_file_edits_view_tests.rs (intermittent CI failures, e.g. on a run for #13486). The fix is test-only synchronization plus one dev-dependency feature; no product code changes.

Note: this branch is intentionally based on 453f4d3 as part of an independent root-cause exercise, so it may show a conflict with the current master in this test file.

Root cause

The test stacked two races on top of each other:

  1. Stale first DiffUpdated (abort race). reset_content seeds the diff base and kicks off a base == new diff computation (task A), which DiffModel::compute_diff spawns on the background tokio thread pool (ModelContext::spawn runs futures off the main thread and marshals the callback back to the foreground executor). The subsequent apply_diffs aborts task A and spawns the real diff (task B) — but the abort is best-effort (it only takes effect on the task's next poll), and task A's body completes on its first poll since the identical texts produce no diff ops. When task A wins that race, its foreground callback installs an empty diff status and emits the first CodeEditorModelEvent::DiffUpdated. The test's oneshot fired on that first event, so expand_diffs ran (and rendered zero temporary blocks) before the real post-edit diff had landed.

  2. Bounded foreground poll vs. background layout hop. Ghost blocks are delivered via RenderState's async layout channel: add_temporary_blocks only enqueues a LayoutTemporaryBlock action; App::spawn_stream_local polls that channel on a background OS thread and forwards items to a foreground task (handle_layout_action) which finally stores the blocks on CharCellState. The test polled ghosts() for a fixed 100 × futures_lite::future::yield_now() iterations — but yield_now only drives the single-threaded foreground test executor and places no bound at all on when the background thread gets scheduled. Under CI load, the 100 foreground turns burn through in microseconds while the background diff round-trip (task B) plus the background layout hop are still pending, leaving ghosts() empty and failing assert_eq!(ghosts.len(), 1).

Reproduction on the base commit confirmed the flake at roughly 6 failures per 30 runs under CPU contention.

Fix

  • The DiffUpdated subscription now fires the oneshot only once the installed diff status actually reflects a change (added_or_changed_lines() non-empty). This ignores the spurious empty-diff event from the aborted base == new computation and releases the test only after the real post-edit diff has landed, so expand_diffs always runs against the correct diff state.
  • The fixed-iteration ghost poll is replaced with RenderState::layout_complete().await — the existing test utility that waits on the outstanding_layouts counter (incremented synchronously when a layout action is submitted, decremented after handle_layout_action processes it). This deterministically waits out the background layout hop regardless of OS scheduling, with no sleeps and no arbitrary iteration bounds.
  • crates/warp_tui/Cargo.toml enables warp_editor's test-util feature in dev-dependencies, which gates layout_complete().

Together these make the test's synchronization match the pipeline's actual async structure instead of guessing at scheduling, which is why the flake is eliminated rather than just made rarer.

Linked Issue

N/A — CI flake fix.

Testing

  • cargo test -p warp_tui --lib diff_pipeline_computes_added_lines_and_ghost_blocks: 30/30 consecutive passes.

  • Same test under CPU saturation (4× yes > /dev/null): 20/20 passes. (Workers' independent validation: 40/40, 30/30 under load, and 25/25 under load; the unfixed test failed ~6/30 under the same conditions.)

  • Full cargo test -p warp_tui --lib: 102/102 passed.

  • ./script/format: no changes; cargo clippy -p warp_tui --all-targets -- -D warnings: clean.

  • Automated test coverage (this PR fixes the test itself); not manually testable via ./script/run.

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI Agent Mode

CHANGELOG-NONE

Co-Authored-By: Oz oz-agent@warp.dev

Conversation: https://staging.warp.dev/conversation/90e7d2e2-1f45-4bb5-a8ce-3d79ac277b4c
Run: https://oz.staging.warp.dev/runs/019f5d5a-a061-7720-ba72-ef7af6dafb13

This PR was generated with Oz.

The test was flaky because its synchronization could not deterministically
wait for two background-thread hops:

1. Stale first DiffUpdated: reset_content seeds the diff base and triggers a
   base==new diff (task A) on the background executor; the subsequent
   apply_diffs aborts A and spawns the real diff (task B). The abort is
   best-effort (only effective on the next poll), and A's body has no
   yield_now (zero diff ops), so A can complete on its first poll before the
   abort is issued. When that happens A's callback fires a spurious first
   CodeEditorModelEvent::DiffUpdated for an *empty* diff. The test's oneshot
   fired on that first event, so expand_diffs ran before task B landed.

2. Background layout hop: ghost blocks are delivered via the render state's
   async layout channel, which spawn_stream_local polls on a *background*
   thread and forwards to a foreground task. The test's fixed 100x yield_now
   ghost poll only drives the foreground executor, so under load it could not
   reliably wait for the background diff round-trip (task B) plus the
   background layout hop.

Fix (test-only synchronization):
- Fire the oneshot only once the diff state actually reflects a change
  (added_or_changed_lines non-empty), checked inside the subscription
  callback. This ignores the stale empty DiffUpdated and signals on the real
  post-edit diff, so expand_diffs runs after task B.
- Replace the fixed-iteration ghost poll with RenderState::layout_complete(),
  which awaits the outstanding_layouts counter until every submitted layout
  action (including the LayoutTemporaryBlock from expand_diffs) has been
  processed, deterministically waiting for the background layout hop.
- Enable warp_editor/test-util in warp_tui dev-dependencies so
  layout_complete()/outstanding_layouts are available.

Co-Authored-By: Oz <oz-agent@warp.dev>
@cla-bot cla-bot Bot added the cla-signed label Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant