//! [`TurnStop`]: the handle that stops a turn from outside the call driving it. use hive_claude::Cancel; use crate::Canceller; /// Stops the turn a claude process or an ACP agent has in flight. Cheap to /// clone. /// /// Every stop is SIGINT, directly (claude) or after `session/cancel` went /// unanswered (ACP): claude shuts down the commands it started when it gets /// SIGINT, and nothing here escalates past it or waits for the turn to end. #[derive(Clone)] pub enum TurnStop { /// Signals the turn's `claude` process group. Claude(Cancel), /// Sends the ACP agent `session/cancel`; the runtime sends SIGINT to one /// that ignores it for its own grace period. Acp(Canceller), } impl From for TurnStop { fn from(cancel: Cancel) -> Self { Self::Claude(cancel) } } impl From for TurnStop { fn from(canceller: Canceller) -> Self { Self::Acp(canceller) } } impl TurnStop { /// Ask the turn to stop, without waiting for it to. Returns `false` when /// no turn was in flight and nothing was signalled. A claude handle exists /// only for a spawned process, so it always returns `true`. #[must_use] pub fn stop(&self) -> bool { match self { Self::Claude(cancel) => { cancel.cancel(false); true } Self::Acp(canceller) => canceller.cancel(), } } } #[cfg(all(test, unix))] mod tests { use std::path::Path; use std::time::Duration; use hive_claude::{Attach, Claude, Config}; use super::TurnStop; /// A `claude` that records its SIGINT in `$MARK` and exits 0, and says it /// is listening for one in `$READY`. const FAKE_CLAUDE: &str = "#!/bin/sh\n\ trap ': > \"$MARK\"; exit 0' INT\n\ : > \"$READY\"\n\ sleep 30 >/dev/null 2>&1 &\n\ wait\n"; async fn exists(path: &Path) { for _ in 0..250 { if path.exists() { return; } tokio::time::sleep(Duration::from_millis(20)).await; } panic!("{} never appeared", path.display()); } #[tokio::test] async fn a_claude_stop_is_a_sigint_to_the_process() { use std::os::unix::fs::PermissionsExt as _; let dir = tempfile::tempdir().unwrap(); let program = dir.path().join("fake-claude"); std::fs::write(&program, FAKE_CLAUDE).unwrap(); std::fs::set_permissions(&program, std::fs::Permissions::from_mode(0o755)).unwrap(); let (ready, mark) = (dir.path().join("ready"), dir.path().join("int")); let config = Config { program: Some(program), env: vec![ ("READY".to_owned(), ready.display().to_string()), ("MARK".to_owned(), mark.display().to_string()), ], ..Config::default() }; let running = Claude::spawn(&config, &Attach::Create("n".to_owned())).unwrap(); let stop = TurnStop::from(running.cancel_handle()); exists(&ready).await; assert!(stop.stop()); exists(&mark).await; } }