Skip to content

Commit e378d8a

Browse files
committed
test: stabilize two CI flakes hit by the PR #44 merge run
- codex_adapter: route every wrapper-script `start_session` call through a new `start_session_resilient` helper that retries ETXTBSY ("Text file busy") from the kernel. Cause: tokio's fork+exec inherits sibling tests' in-flight write fds in the same test binary, and Linux's `i_writecount` check rejects the exec until the fd drains. Pure test-infra artifact — production spawns a pre-installed `codex` binary. 25/25 stress runs clean. - new-workspace-dialog: widen the chip-assertion `waitFor` to 5s. Windows CI under load stretches paste → mock → setAttachments → commit past the default 1s; Linux/macOS resolve well under 100ms.
1 parent c404d50 commit e378d8a

2 files changed

Lines changed: 69 additions & 27 deletions

File tree

src-tauri/tests/codex_adapter.rs

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ async fn starts_session_and_reports_ready() {
205205
evs
206206
});
207207

208-
let session = provider.start_session(start_input("t-ready")).await.unwrap();
208+
let session = start_session_resilient(&provider, start_input("t-ready")).await.unwrap();
209209
assert_eq!(session.thread_id.0, "t-ready");
210210
assert!(matches!(session.status, SessionStatus::Ready));
211211

@@ -233,7 +233,7 @@ async fn send_turn_emits_turn_started_then_delta_then_completed() {
233233
("FAKE_CODEX_SCRIPT", &script.to_string_lossy()),
234234
]);
235235
let provider = provider_with_fixture_and_binary(wrapper.to_path_buf());
236-
provider.start_session(start_input("t-seq")).await.unwrap();
236+
start_session_resilient(&provider, start_input("t-seq")).await.unwrap();
237237
let mut stream = provider.event_stream();
238238

239239
provider
@@ -292,7 +292,7 @@ async fn thread_resume_with_recoverable_error_falls_back_to_start() {
292292

293293
let mut input = start_input("t-resume");
294294
input.resume_cursor = Some(json!({"threadId":"old-thread"}));
295-
let session = provider.start_session(input).await.unwrap();
295+
let session = start_session_resilient(&provider, input).await.unwrap();
296296
assert!(matches!(session.status, SessionStatus::Ready));
297297

298298
// Look for the RuntimeWarning on the stream.
@@ -326,7 +326,7 @@ async fn unknown_notification_surfaces_as_runtime_warning() {
326326
("FAKE_CODEX_SCRIPT", &script.to_string_lossy()),
327327
]);
328328
let provider = provider_with_fixture_and_binary(wrapper.to_path_buf());
329-
provider.start_session(start_input("t-unk")).await.unwrap();
329+
start_session_resilient(&provider, start_input("t-unk")).await.unwrap();
330330
let mut stream = provider.event_stream();
331331
provider
332332
.send_turn(SendTurnInput {
@@ -372,7 +372,7 @@ async fn command_approval_request_roundtrip() {
372372
("FAKE_CODEX_SCRIPT", &script.to_string_lossy()),
373373
]);
374374
let provider = provider_with_fixture_and_binary(wrapper.to_path_buf());
375-
provider.start_session(start_input("t-ap")).await.unwrap();
375+
start_session_resilient(&provider, start_input("t-ap")).await.unwrap();
376376
let mut stream = provider.event_stream();
377377
provider
378378
.send_turn(SendTurnInput {
@@ -428,7 +428,7 @@ async fn command_approval_deny_roundtrip() {
428428
("FAKE_CODEX_SCRIPT", &script.to_string_lossy()),
429429
]);
430430
let provider = provider_with_fixture_and_binary(wrapper.to_path_buf());
431-
provider.start_session(start_input("t-deny")).await.unwrap();
431+
start_session_resilient(&provider, start_input("t-deny")).await.unwrap();
432432
let mut stream = provider.event_stream();
433433
provider
434434
.send_turn(SendTurnInput {
@@ -476,7 +476,7 @@ async fn interrupt_turn_sends_turn_interrupt() {
476476
("FAKE_CODEX_SCRIPT", &script.to_string_lossy()),
477477
]);
478478
let provider = provider_with_fixture_and_binary(wrapper.to_path_buf());
479-
provider.start_session(start_input("t-int")).await.unwrap();
479+
start_session_resilient(&provider, start_input("t-int")).await.unwrap();
480480
let res = provider
481481
.send_turn(SendTurnInput {
482482
thread_id: ThreadId("t-int".into()),
@@ -510,7 +510,7 @@ async fn interrupt_turn_with_wrong_turn_id_fails_validation() {
510510
("FAKE_CODEX_SCRIPT", &script.to_string_lossy()),
511511
]);
512512
let provider = provider_with_fixture_and_binary(wrapper.to_path_buf());
513-
provider.start_session(start_input("t-mm")).await.unwrap();
513+
start_session_resilient(&provider, start_input("t-mm")).await.unwrap();
514514
provider
515515
.send_turn(SendTurnInput {
516516
thread_id: ThreadId("t-mm".into()),
@@ -535,7 +535,7 @@ async fn interrupt_turn_with_wrong_turn_id_fails_validation() {
535535
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
536536
async fn set_permission_mode_returns_validation_error() {
537537
let provider = provider_with_fixture();
538-
provider.start_session(start_input("t-perm")).await.unwrap();
538+
start_session_resilient(&provider, start_input("t-perm")).await.unwrap();
539539
let err = provider
540540
.set_permission_mode(ThreadId("t-perm".into()), "acceptEdits".into())
541541
.await
@@ -547,7 +547,7 @@ async fn set_permission_mode_returns_validation_error() {
547547
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
548548
async fn set_model_updates_session_state() {
549549
let provider = provider_with_fixture();
550-
provider.start_session(start_input("t-model")).await.unwrap();
550+
start_session_resilient(&provider, start_input("t-model")).await.unwrap();
551551
provider
552552
.set_model(ThreadId("t-model".into()), "opus-4-7".into())
553553
.await
@@ -571,7 +571,7 @@ async fn set_model_updates_session_state() {
571571
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
572572
async fn stop_session_removes_from_list_and_closes_child() {
573573
let provider = provider_with_fixture();
574-
provider.start_session(start_input("t-close")).await.unwrap();
574+
start_session_resilient(&provider, start_input("t-close")).await.unwrap();
575575
assert_eq!(provider.list_sessions().await.unwrap().len(), 1);
576576
provider
577577
.stop_session(ThreadId("t-close".into()))
@@ -583,7 +583,7 @@ async fn stop_session_removes_from_list_and_closes_child() {
583583
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
584584
async fn stop_session_is_idempotent() {
585585
let provider = provider_with_fixture();
586-
provider.start_session(start_input("t-idem")).await.unwrap();
586+
start_session_resilient(&provider, start_input("t-idem")).await.unwrap();
587587
provider.stop_session(ThreadId("t-idem".into())).await.unwrap();
588588
let err = provider
589589
.stop_session(ThreadId("t-idem".into()))
@@ -612,8 +612,8 @@ async fn send_turn_on_nonexistent_thread_returns_session_not_found() {
612612
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
613613
async fn duplicate_start_session_returns_validation_error() {
614614
let provider = provider_with_fixture();
615-
provider.start_session(start_input("t-dup")).await.unwrap();
616-
let err = provider.start_session(start_input("t-dup")).await.unwrap_err();
615+
start_session_resilient(&provider, start_input("t-dup")).await.unwrap();
616+
let err = start_session_resilient(&provider, start_input("t-dup")).await.unwrap_err();
617617
assert!(matches!(err, ProviderError::ValidationError { .. }));
618618
provider.stop_session(ThreadId("t-dup".into())).await.ok();
619619
}
@@ -624,7 +624,7 @@ async fn child_process_crash_emits_error_state() {
624624
// acknowledged.
625625
let wrapper = wrapper_with_env(&[("FAKE_CODEX_EXIT_AFTER", "turn/start")]);
626626
let provider = provider_with_fixture_and_binary(wrapper.to_path_buf());
627-
provider.start_session(start_input("t-crash")).await.unwrap();
627+
start_session_resilient(&provider, start_input("t-crash")).await.unwrap();
628628
let mut stream = provider.event_stream();
629629
// turn/start will succeed then the fixture exits.
630630
let _ = provider
@@ -666,7 +666,7 @@ async fn concurrent_send_turn_returns_validation_error() {
666666
("FAKE_CODEX_SCRIPT", &script.to_string_lossy()),
667667
]);
668668
let provider = provider_with_fixture_and_binary(wrapper.to_path_buf());
669-
provider.start_session(start_input("t-busy")).await.unwrap();
669+
start_session_resilient(&provider, start_input("t-busy")).await.unwrap();
670670
provider
671671
.send_turn(SendTurnInput {
672672
thread_id: ThreadId("t-busy".into()),
@@ -701,7 +701,7 @@ async fn multiple_concurrent_sessions_are_isolated() {
701701
for i in 0..3 {
702702
let p = Arc::clone(&provider);
703703
handles.push(tokio::spawn(async move {
704-
p.start_session(start_input(&format!("t-iso-{i}"))).await
704+
start_session_resilient(&p, start_input(&format!("t-iso-{i}"))).await
705705
}));
706706
}
707707
for h in handles {
@@ -723,7 +723,7 @@ async fn event_stream_subscribers_each_receive_events() {
723723
let provider = provider_with_fixture();
724724
let mut a = provider.event_stream();
725725
let mut b = provider.event_stream();
726-
provider.start_session(start_input("t-sub")).await.unwrap();
726+
start_session_resilient(&provider, start_input("t-sub")).await.unwrap();
727727
let got_a = timeout(Duration::from_secs(2), a.next()).await.unwrap();
728728
let got_b = timeout(Duration::from_secs(2), b.next()).await.unwrap();
729729
assert!(got_a.is_some());
@@ -734,7 +734,7 @@ async fn event_stream_subscribers_each_receive_events() {
734734
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
735735
async fn late_subscriber_does_not_get_old_events() {
736736
let provider = provider_with_fixture();
737-
provider.start_session(start_input("t-late")).await.unwrap();
737+
start_session_resilient(&provider, start_input("t-late")).await.unwrap();
738738
// Wait for the first burst of events to pass.
739739
tokio::time::sleep(Duration::from_millis(100)).await;
740740

@@ -755,7 +755,7 @@ async fn translate_turn_completed_error_emits_both_events_end_to_end() {
755755
("FAKE_CODEX_SCRIPT", &script.to_string_lossy()),
756756
]);
757757
let provider = provider_with_fixture_and_binary(wrapper.to_path_buf());
758-
provider.start_session(start_input("t-fail")).await.unwrap();
758+
start_session_resilient(&provider, start_input("t-fail")).await.unwrap();
759759
let mut stream = provider.event_stream();
760760
provider
761761
.send_turn(SendTurnInput {
@@ -807,6 +807,37 @@ fn write_bash_script(_prefix: &str, body: &str) -> ScriptFile {
807807
ScriptFile { _dir: dir, path }
808808
}
809809

810+
/// Start a session, retrying transient ETXTBSY ("Text file busy")
811+
/// failures from the kernel.
812+
///
813+
/// Why this exists: cargo runs tests in parallel inside one binary,
814+
/// and tokio's `Command::spawn` falls back to fork+exec on Linux when
815+
/// it can't use `posix_spawn`. If another test's `File::create` for
816+
/// its own wrapper script is in-flight at the moment we fork, the
817+
/// child inherits that write fd briefly; then our `execve()` of OUR
818+
/// wrapper sees a non-zero `i_writecount` on the inode (the kernel
819+
/// doesn't distinguish "this exec target" from "any open writer
820+
/// across all fds the child inherited") and rejects with ETXTBSY.
821+
/// The race window is microseconds, but it's real and shows up under
822+
/// CI load. A small retry loop sidesteps it without changing
823+
/// production code.
824+
async fn start_session_resilient(
825+
provider: &CodexAgentProvider,
826+
input: StartSessionInput,
827+
) -> Result<codemux_lib::agent_provider::ProviderSession, ProviderError> {
828+
for _ in 0..10 {
829+
match provider.start_session(input.clone()).await {
830+
Ok(s) => return Ok(s),
831+
Err(e) if format!("{e:?}").contains("Text file busy") => {
832+
tokio::time::sleep(Duration::from_millis(50)).await;
833+
continue;
834+
}
835+
Err(e) => return Err(e),
836+
}
837+
}
838+
provider.start_session(input).await
839+
}
840+
810841
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
811842
async fn auth_probe_installed_returns_version_when_codex_works() {
812843
let wrapper = write_bash_script(
@@ -838,7 +869,9 @@ async fn bogus_response_to_unknown_jsonrpc_id_does_not_crash_adapter() {
838869
);
839870
let helper = write_bash_script("codex-bogus-", &body);
840871
let provider = provider_with_fixture_and_binary(helper.to_path_buf());
841-
provider.start_session(start_input("t-bogus")).await.unwrap();
872+
start_session_resilient(&provider, start_input("t-bogus"))
873+
.await
874+
.unwrap();
842875
// Adapter should still work after the spurious id.
843876
provider
844877
.send_turn(SendTurnInput {
@@ -857,7 +890,7 @@ async fn bogus_response_to_unknown_jsonrpc_id_does_not_crash_adapter() {
857890
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
858891
async fn dropping_provider_shuts_down_sessions() {
859892
let provider = provider_with_fixture();
860-
provider.start_session(start_input("t-drop")).await.unwrap();
893+
start_session_resilient(&provider, start_input("t-drop")).await.unwrap();
861894
let sessions = provider.list_sessions().await.unwrap();
862895
assert_eq!(sessions.len(), 1);
863896
drop(provider);
@@ -899,7 +932,7 @@ async fn shutdown_during_event_streaming_does_not_panic() {
899932
("FAKE_CODEX_SCRIPT", &script.to_string_lossy()),
900933
]);
901934
let provider = provider_with_fixture_and_binary(wrapper.to_path_buf());
902-
provider.start_session(start_input("t-race")).await.unwrap();
935+
start_session_resilient(&provider, start_input("t-race")).await.unwrap();
903936
// Start a turn that will emit events mid-flight, then stop quickly.
904937
provider
905938
.send_turn(SendTurnInput {

src/components/overlays/new-workspace-dialog.test.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -737,10 +737,19 @@ describe("Clipboard image paste", () => {
737737

738738
// The chip renders the trailing filename component (the existing
739739
// attachment-strip logic lifts it via `.split("/").pop()`).
740-
await waitFor(() => {
741-
const chips = within(dialog).getAllByText("paste-xyz.png");
742-
expect(chips.length).toBeGreaterThan(0);
743-
});
740+
//
741+
// The default 1000ms waitFor isn't enough on a busy Windows CI
742+
// runner: paste → await mock → setAttachments → React commit can
743+
// stretch past a second when the runner is under load. The fail
744+
// mode was Windows-only (Linux/macOS resolved well under 100ms),
745+
// so we widen the budget rather than pushing on a real bug.
746+
await waitFor(
747+
() => {
748+
const chips = within(dialog).getAllByText("paste-xyz.png");
749+
expect(chips.length).toBeGreaterThan(0);
750+
},
751+
{ timeout: 5000 },
752+
);
744753
});
745754

746755
it("stays silent when the OS clipboard has no image", async () => {

0 commit comments

Comments
 (0)