diff --git a/.github/workflows/rust-clippy.yml b/.github/workflows/rust-clippy.yml index e44ddcbdb..a6daf1a70 100644 --- a/.github/workflows/rust-clippy.yml +++ b/.github/workflows/rust-clippy.yml @@ -98,7 +98,7 @@ jobs: - name: Rust workspace tests env: FRESHELL_SERVER_BIN: ${{ github.workspace }}/target/debug/freshell-server - run: cargo test --workspace --locked + run: cargo test --workspace --locked --no-fail-fast # Dedicated Tauri smoke with --nocapture so the CI log visibly shows # "using server binary:" (confirming the test exercised the real binary, diff --git a/crates/freshell-ws/tests/pane_ledger_triggers.rs b/crates/freshell-ws/tests/pane_ledger_triggers.rs index 6db3c7da6..5e114f7dc 100644 --- a/crates/freshell-ws/tests/pane_ledger_triggers.rs +++ b/crates/freshell-ws/tests/pane_ledger_triggers.rs @@ -1787,3 +1787,17 @@ async fn a_kill_whose_row_projection_fails_still_ends_the_terminal_and_converges registry.kill(&terminal_id); std::fs::remove_dir_all(&dir).ok(); } + +/// Regression test for the shared `common::sleeper_cli_spec` uniqueness fix: +/// two calls with the same name must not share a script path, or parallel +/// tests race ETXTBSY ("Text file busy") when one writes while another execves. +#[test] +fn shared_sleeper_cli_spec_paths_are_unique_per_call() { + let first = sleeper_cli_spec("claude"); + let second = sleeper_cli_spec("claude"); + assert_ne!( + first.default_cmd, second.default_cmd, + "same-name specs from common::sleeper_cli_spec must not share a script path -- \ + a shared path lets a later write race an earlier spawn's execve (ETXTBSY)" + ); +} diff --git a/crates/freshell-ws/tests/restore_spawn_gate.rs b/crates/freshell-ws/tests/restore_spawn_gate.rs index 77efed847..c2ad0228e 100644 --- a/crates/freshell-ws/tests/restore_spawn_gate.rs +++ b/crates/freshell-ws/tests/restore_spawn_gate.rs @@ -44,9 +44,16 @@ fn test_settings_value() -> serde_json::Value { /// A minimal always-present CLI spec (`/bin/sh` sleeper script) so non-shell /// creates genuinely spawn — the same recording-script convention as /// `session_identity_frames.rs` (these tests assert on wire frames, not argv). +/// +/// Each call writes a **unique** script path (PID + atomic counter) so parallel +/// tests in the same binary never collide with ETXTBSY ("Text file busy") when +/// one test writes the script while another is executing a prior copy. +static SLEEPER_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + fn sleeper_cli_spec(name: &str) -> freshell_platform::CliCommandSpec { + let seq = SLEEPER_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); let script_path = std::env::temp_dir().join(format!( - "freshell-restore-gate-sleeper-{name}-{}.sh", + "freshell-restore-gate-sleeper-{name}-{}-{seq}.sh", std::process::id() )); std::fs::write(&script_path, "#!/bin/sh\nexec sleep 30\n").expect("write sleeper script");