use super::{DEADLINE, NO_SUCH_TOOL, SIGNALLED, TIMED_OUT, execute, handoff};
use crate::config::Local;
use crate::invocation::{Capture, Invocation};
use crate::test_support::Scratch;
use crate::tools::Tool;
use serde_json::{Value, json};
use std::time::Duration;
mod placement;
mod bounds;
fn tool(name: &str, script: &str) -> Local {
Local {
tool: Tool {
name: name.to_owned(),
description: "a tool".to_owned(),
input_schema: json!({"type": "object"}),
subject_cwd: false,
},
command: vec!["/bin/sh".to_owned(), "-c".to_owned(), script.to_owned()],
cwd: None,
}
}
fn call(name: &str, input: Value) -> Invocation {
Invocation {
id: "i-1".to_owned(),
tool: name.to_owned(),
input,
cwd: None,
}
}
fn brief() -> Duration {
Duration::from_millis(150)
}
#[test]
fn the_input_reaches_the_command_on_stdin_and_its_output_comes_back() {
let set = [tool("Echo", "cat")];
let input = json!({"command": "echo hi", "n": 7});
let got = execute(&set, &call("Echo", input.clone()), DEADLINE);
assert_eq!(got.stdout, input.to_string());
assert_eq!(got.stderr, "");
assert_eq!(got.exit_code, 0);
}
#[test]
fn stderr_and_a_failing_exit_code_are_carried_as_they_are() {
let set = [tool("Fails", "printf out; printf err >&2; exit 3")];
let got = execute(&set, &call("Fails", json!({})), DEADLINE);
assert_eq!(
got,
Capture {
stdout: "out".to_owned(),
stderr: "err".to_owned(),
exit_code: 3,
}
);
}
#[test]
fn output_that_is_not_utf8_is_transcoded_once_and_lossily() {
let set = [tool("Binary", "printf '\\377\\376'")];
let got = execute(&set, &call("Binary", json!({})), DEADLINE);
assert_eq!(got.stdout, "\u{fffd}\u{fffd}");
assert_eq!(got.exit_code, 0);
}
#[test]
fn a_name_this_box_does_not_carry_is_refused_as_a_capture() {
let set = [tool("Echo", "cat")];
let got = execute(&set, &call("Write", json!({})), DEADLINE);
assert_eq!(got.exit_code, NO_SUCH_TOOL);
assert!(got.stdout.is_empty());
assert!(
got.stderr
.contains("does not carry a tool called \"Write\""),
"{got:?}"
);
}
#[test]
fn a_command_that_cannot_be_started_is_still_a_capture() {
let mut local = tool("Missing", "");
local.command = vec!["/nonexistent/thrall-test-tool".to_owned()];
let got = execute(&[local], &call("Missing", json!({})), DEADLINE);
assert_eq!(got.exit_code, NO_SUCH_TOOL);
assert!(
got.stderr.contains("/nonexistent/thrall-test-tool"),
"{got:?}"
);
}
#[test]
fn an_argv_with_no_program_in_it_names_nothing_to_run() {
let mut local = tool("Empty", "");
local.command = Vec::new();
let got = execute(&[local], &call("Empty", json!({})), DEADLINE);
assert_eq!(got.exit_code, NO_SUCH_TOOL);
assert!(got.stderr.contains("empty argv"), "{got:?}");
}
#[test]
fn a_tool_that_outruns_its_deadline_is_terminated_and_says_so() {
let set = [tool("Slow", "sleep 30")];
let got = execute(&set, &call("Slow", json!({})), brief());
assert_eq!(got.exit_code, TIMED_OUT);
assert!(got.stderr.contains("no answer within"), "{got:?}");
assert!(got.stderr.contains("terminated"), "{got:?}");
assert!(
!got.stderr.contains("killed"),
"it stopped when asked: {got:?}"
);
}
#[test]
fn a_tool_that_ignores_the_signal_is_killed_after_the_grace() {
let set = [tool("Stubborn", "trap '' TERM; sleep 30")];
let got = execute(&set, &call("Stubborn", json!({})), brief());
assert_eq!(got.exit_code, TIMED_OUT);
assert!(got.stderr.contains("none to the signal"), "{got:?}");
assert!(got.stderr.contains("killed"), "{got:?}");
}
#[test]
fn a_helper_the_tool_started_stops_with_the_tool() {
let scratch = Scratch::new();
let started = scratch.path().join("started");
let outlived = scratch.path().join("outlived");
let set = [tool(
"Forker",
&format!(
"( : > {}; sleep 1; : > {} ) & sleep 30",
started.display(),
outlived.display()
),
)];
let got = execute(&set, &call("Forker", json!({})), brief());
assert_eq!(got.exit_code, TIMED_OUT, "{got:?}");
assert!(started.exists(), "the helper never ran: {got:?}");
std::thread::sleep(Duration::from_millis(1_500));
assert!(
!outlived.exists(),
"the helper outlived the deadline that stopped its parent"
);
}
#[test]
fn a_child_a_signal_ended_says_a_signal_ended_it() {
let set = [tool("Dies", "kill -9 $$")];
let got = execute(&set, &call("Dies", json!({})), DEADLINE);
assert_eq!(got.exit_code, SIGNALLED);
}
#[test]
fn a_tool_that_ignores_its_input_still_answers() {
let set = [tool("Deaf", "printf done")];
let big = json!({"payload": "x".repeat(200_000)});
let got = execute(&set, &call("Deaf", big), DEADLINE);
assert_eq!(got.stdout, "done");
assert_eq!(got.exit_code, 0);
}
#[test]
fn a_tool_that_writes_more_than_a_pipe_holds_still_answers() {
let set = [tool("Loud", "yes hello | head -c 300000")];
let got = execute(&set, &call("Loud", json!({})), DEADLINE);
assert_eq!(got.stdout.len(), 300_000);
assert_eq!(got.exit_code, 0);
}
#[test]
fn the_handoff_is_the_executor_under_this_box_s_own_bound() {
let set = [tool("Echo", "cat")];
assert_eq!(
handoff(&set, &call("Echo", json!({"a": 1}))),
execute(&set, &call("Echo", json!({"a": 1})), DEADLINE)
);
}