diff --git a/crates/kebab-app/tests/ingest_cancel.rs b/crates/kebab-app/tests/ingest_cancel.rs index 6c328f4..bab94d8 100644 --- a/crates/kebab-app/tests/ingest_cancel.rs +++ b/crates/kebab-app/tests/ingest_cancel.rs @@ -80,11 +80,12 @@ fn cancel_mid_loop_after_first_asset_keeps_idempotent_resume() { let report = run_with(&env, cancel, Some(tx)); listener.join().unwrap(); - // Exactly 1 asset committed; remaining 2 skipped (untouched). - assert!( - report.new == 1 || report.new == 0 || report.new == 2, - "non-deterministic but must be < 3: {report:?}" - ); + // cancel-mid is timing-dependent: the listener flips cancel + // after the first AssetFinished, but the loop may have started + // 1 more asset by the time the next iteration check runs. + // 0 (race won by listener), 1 (first only), or 2 (one extra + // slipped in) are all valid outcomes; report.new == 3 means + // cancel never propagated and is the only failure mode. assert!(report.new < 3, "loop should have broken: {report:?}"); // Idempotent re-ingest finishes the job. diff --git a/crates/kebab-cli/src/cancel.rs b/crates/kebab-cli/src/cancel.rs index 7c41e66..8e11f9e 100644 --- a/crates/kebab-cli/src/cancel.rs +++ b/crates/kebab-cli/src/cancel.rs @@ -38,6 +38,14 @@ pub fn install_sigint_cancel() -> anyhow::Result> { let cancel_for_handler = cancel.clone(); // Per-process count of received SIGINTs. Static so the closure // owns no extra state; first signal flips cancel, second exits. + // + // Process-lifetime: never reset. ctrlc::set_handler rejects + // multi-install with `Err(MultipleHandlers)`, so this counter + // is effectively single-use per `kebab` invocation. A future + // command that needs its own cancel token (e.g. `kebab eval + // run --with-cancel`) must factor the install path into a + // helper that takes the token as an arg and shares it across + // callers — not call `install_sigint_cancel` twice. static SIGNAL_COUNT: AtomicU8 = AtomicU8::new(0); ctrlc::set_handler(move || { let prev = SIGNAL_COUNT.fetch_add(1, Ordering::Relaxed);