use super::*;
use agent_client_protocol_schema::SessionId;
use std::path::Path;
use tokio_util::sync::CancellationToken;
fn ctx<'a>(session_id: &'a SessionId, cwd: &'a Path) -> HookCtx<'a> {
HookCtx::new(session_id, cwd, CancellationToken::new())
}
fn argv_spec(argv: Vec<&str>) -> CommandSpec {
CommandSpec::Argv {
argv: argv.into_iter().map(str::to_string).collect(),
argv_windows: None,
cwd: None,
env: BTreeMap::new(),
timeout_sec: None,
}
}
#[tokio::test]
async fn step_empty_stdout_is_no_verdict() {
if !Path::new("/bin/true").exists() {
return;
}
let h = CommandHandler::new(argv_spec(vec!["/bin/true"]));
let session_id = SessionId::new("s1");
let cwd = Path::new("/");
let env = serde_json::json!({"tool": "bash", "args": {"x": 1}});
let v = h
.handle_step(&env, ctx(&session_id, cwd))
.await
.expect("ok");
assert!(v.is_none());
}
#[tokio::test]
async fn step_non_json_stdout_is_no_verdict() {
if !Path::new("/bin/sh").exists() {
return;
}
let h = CommandHandler::new(argv_spec(vec!["/bin/sh", "-c", "echo audit-line"]));
let session_id = SessionId::new("s1");
let cwd = Path::new("/");
let env = serde_json::json!({"tool": "bash"});
let v = h
.handle_step(&env, ctx(&session_id, cwd))
.await
.expect("ok");
assert!(v.is_none());
}
#[tokio::test]
async fn step_json_stdout_becomes_verdict() {
if !Path::new("/bin/sh").exists() {
return;
}
let h = CommandHandler::new(argv_spec(vec![
"/bin/sh",
"-c",
r#"echo '{"control":"break"}'"#,
]));
let session_id = SessionId::new("s1");
let cwd = Path::new("/");
let env = serde_json::json!({"tool": "bash"});
let v = h
.handle_step(&env, ctx(&session_id, cwd))
.await
.expect("ok")
.expect("verdict");
assert_eq!(v["control"], "break");
}
#[tokio::test]
async fn step_exit_2_yields_veto() {
if !Path::new("/bin/sh").exists() {
return;
}
let h = CommandHandler::new(argv_spec(vec![
"/bin/sh",
"-c",
"echo 'tests failed' >&2; exit 2",
]));
let session_id = SessionId::new("s1");
let cwd = Path::new("/");
let env = serde_json::json!({"tool": "bash"});
let v = h
.handle_step(&env, ctx(&session_id, cwd))
.await
.expect("ok")
.expect("verdict");
assert_eq!(v["control"], "veto");
assert_eq!(v["additional_context"][0], "tests failed\n");
}
#[tokio::test]
async fn step_exit_2_vetoes_even_when_script_ignores_large_stdin() {
if !Path::new("/bin/sh").exists() {
return;
}
let h = CommandHandler::new(argv_spec(vec![
"/bin/sh",
"-c",
"echo 'tests failed' >&2; exit 2",
]));
let session_id = SessionId::new("s1");
let cwd = Path::new("/");
let env = serde_json::json!({"tool": "bash", "pad": "x".repeat(1024 * 1024)});
let v = h
.handle_step(&env, ctx(&session_id, cwd))
.await
.expect("ok")
.expect("verdict");
assert_eq!(v["control"], "veto");
assert_eq!(v["additional_context"][0], "tests failed\n");
}
#[tokio::test]
async fn step_nonzero_exit_is_handler_failed() {
if !Path::new("/bin/sh").exists() {
return;
}
let h = CommandHandler::new(argv_spec(vec!["/bin/sh", "-c", "exit 7"]));
let session_id = SessionId::new("s1");
let cwd = Path::new("/");
let env = serde_json::json!({"tool": "bash"});
let err = h
.handle_step(&env, ctx(&session_id, cwd))
.await
.expect_err("expected error");
assert!(matches!(err, HookError::HandlerFailed(_)));
}
#[tokio::test]
async fn step_cancellation_returns_timeout() {
if !Path::new("/bin/sh").exists() {
return;
}
let h = CommandHandler::new(argv_spec(vec!["/bin/sh", "-c", "sleep 5"]));
let session_id = SessionId::new("s1");
let cwd = Path::new("/");
let cancel = CancellationToken::new();
let cancel_for_drop = cancel.clone();
let hctx = HookCtx::new(&session_id, cwd, cancel);
let env = serde_json::json!({"tool": "bash"});
let fut = h.handle_step(&env, hctx);
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(200)).await;
cancel_for_drop.cancel();
});
let err = fut.await.expect_err("expected cancellation -> Timeout");
assert!(matches!(err, HookError::Timeout));
}
#[test]
fn shell_kind_dispatch_compiles() {
let kinds = [
ShellKind::Sh,
ShellKind::Bash,
ShellKind::Pwsh,
ShellKind::Cmd,
ShellKind::Custom {
program: "fish".into(),
args: vec!["-c".into()],
},
];
for k in &kinds {
let _ = build_shell_command(k, "echo hi");
}
}