use aion::ActivityDispatcher;
use aion_package::contract::CommandBodyCapture;
use aion_package::{
ActionBodyContract, ArgvSlot, CommandParameterContract, DeclaredCommandContract,
EnvBindingContract, FillPiece, FillTemplate,
};
use super::super::declared_body::DeclaredBodyLookup;
use super::super::declared_body::tests::{
TestResult, dispatcher_with_root, reached_names, request,
};
use super::super::workspace_root::{WorkspaceRoot, WorkspaceRootError};
fn hole(parameter: &str) -> FillTemplate {
FillTemplate {
pieces: vec![FillPiece::Hole {
parameter: parameter.to_owned(),
}],
}
}
fn slot(fill: FillTemplate, label: &str) -> ArgvSlot {
ArgvSlot {
fill,
label: label.to_owned(),
admits_leading_dash: true,
}
}
fn contract(program: &[&str], args: Vec<ArgvSlot>) -> DeclaredCommandContract {
DeclaredCommandContract {
name: "say_hello".to_owned(),
parameters: vec![CommandParameterContract {
name: "name".to_owned(),
list: false,
default: None,
}],
program: program.iter().map(|word| (*word).to_owned()).collect(),
args,
env: Vec::new(),
cwd: None,
hardened_path: None,
timeout_ms: None,
timeout_owner: None,
}
}
fn lookup(capture: CommandBodyCapture, command: DeclaredCommandContract) -> DeclaredBodyLookup {
DeclaredBodyLookup::Declared(ActionBodyContract::Command {
capture,
command: Box::new(command),
})
}
fn greeter() -> DeclaredCommandContract {
contract(
&["printf"],
vec![
slot(FillTemplate::literal("--"), "--"),
slot(FillTemplate::literal("%s"), "%s"),
slot(hole("name"), "who"),
],
)
}
#[tokio::test(flavor = "multi_thread")]
async fn a_declared_command_body_executes_without_touching_the_worker_path() -> TestResult {
let (decorated, reached, _transcript) = dispatcher_with_root(
lookup(CommandBodyCapture::Text, greeter()),
Err("terminal:the worker path must never be reached".to_owned()),
WorkspaceRoot::from_resolution(Ok(std::env::temp_dir())),
);
let handle = tokio::task::spawn_blocking(move || {
decorated.dispatch(request("greet", "{\"name\":\"hello from the contract\"}"))
});
let encoded = handle
.await?
.map_err(|error| format!("declared command failed: {error}"))?;
let result: serde_json::Value = serde_json::from_str(&encoded)?;
assert_eq!(
result,
serde_json::json!("hello from the contract"),
"a text capture yields the command's trimmed stdout as one string"
);
assert!(
reached_names(&reached).is_empty(),
"the worker path must not be consulted for a bodied action"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_hostile_fill_arrives_as_exactly_one_literal_argv_word() -> TestResult {
let (decorated, _reached, _transcript) = dispatcher_with_root(
lookup(CommandBodyCapture::Text, greeter()),
Err("terminal:the worker path must never be reached".to_owned()),
WorkspaceRoot::from_resolution(Ok(std::env::temp_dir())),
);
let handle = tokio::task::spawn_blocking(move || {
decorated.dispatch(request(
"greet",
"{\"name\":\"$(boom); rm -rf / && echo PWNED\"}",
))
});
let encoded = handle
.await?
.map_err(|error| format!("declared command failed: {error}"))?;
let result: serde_json::Value = serde_json::from_str(&encoded)?;
assert_eq!(result, serde_json::json!("$(boom); rm -rf / && echo PWNED"));
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_json_capture_decodes_the_commands_output() -> TestResult {
let command = contract(
&["printf"],
vec![
slot(FillTemplate::literal("--"), "--"),
slot(FillTemplate::literal("{\"name\":\"%s\"}"), "%s"),
slot(hole("name"), "who"),
],
);
let (decorated, _reached, _transcript) = dispatcher_with_root(
lookup(CommandBodyCapture::Json, command),
Err("terminal:the worker path must never be reached".to_owned()),
WorkspaceRoot::from_resolution(Ok(std::env::temp_dir())),
);
let handle = tokio::task::spawn_blocking(move || {
decorated.dispatch(request("record", "{\"name\":\"Ada\"}"))
});
let encoded = handle
.await?
.map_err(|error| format!("declared command failed: {error}"))?;
let result: serde_json::Value = serde_json::from_str(&encoded)?;
assert_eq!(result, serde_json::json!({ "name": "Ada" }));
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_failing_declared_command_reports_retryable_with_its_stderr() -> TestResult {
let mut command = contract(
&["sh"],
vec![
slot(FillTemplate::literal("-c"), "-c"),
slot(FillTemplate::literal("echo boom >&2; exit 7"), "body"),
],
);
command.parameters.clear();
let (decorated, _reached, _transcript) = dispatcher_with_root(
lookup(CommandBodyCapture::Text, command),
Ok("unused".to_owned()),
WorkspaceRoot::from_resolution(Ok(std::env::temp_dir())),
);
let handle = tokio::task::spawn_blocking(move || decorated.dispatch(request("fails", "{}")));
let Err(error) = handle.await? else {
return Err("a non-zero exit must fail the dispatch".into());
};
assert!(
error.starts_with("retryable:"),
"a non-zero exit is retryable by default: {error}"
);
assert!(
error.contains("boom"),
"stderr must ride the failure: {error}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_declared_timeout_stops_the_command_here() -> TestResult {
let mut command = contract(
&["sleep"],
vec![slot(FillTemplate::literal("30"), "seconds")],
);
command.parameters.clear();
command.timeout_ms = Some(250);
command.timeout_owner = Some("release".to_owned());
let (decorated, _reached, _transcript) = dispatcher_with_root(
lookup(CommandBodyCapture::Text, command),
Ok("unused".to_owned()),
WorkspaceRoot::from_resolution(Ok(std::env::temp_dir())),
);
let started = std::time::Instant::now();
let handle = tokio::task::spawn_blocking(move || decorated.dispatch(request("slow", "{}")));
let Err(error) = handle.await? else {
return Err("a command outliving its declared timeout must fail".into());
};
assert!(error.starts_with("terminal:"), "{error}");
assert!(
error.contains("declared timeout") && error.contains("release"),
"the refusal must name the bound and its owner: {error}"
);
assert!(
started.elapsed() < std::time::Duration::from_secs(25),
"the bound must be enforced where the process is"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_declared_env_binding_reaches_the_child() -> TestResult {
let mut command = contract(
&["sh"],
vec![
slot(FillTemplate::literal("-c"), "-c"),
slot(FillTemplate::literal("printf %s \"$DECLARED_WHO\""), "body"),
],
);
command.env = vec![EnvBindingContract {
name: "DECLARED_WHO".to_owned(),
value: hole("name"),
}];
let (decorated, _reached, _transcript) = dispatcher_with_root(
lookup(CommandBodyCapture::Text, command),
Ok("unused".to_owned()),
WorkspaceRoot::from_resolution(Ok(std::env::temp_dir())),
);
let handle = tokio::task::spawn_blocking(move || {
decorated.dispatch(request("greet", "{\"name\":\"Ada\"}"))
});
let encoded = handle
.await?
.map_err(|error| format!("declared command failed: {error}"))?;
let result: serde_json::Value = serde_json::from_str(&encoded)?;
assert_eq!(result, serde_json::json!("Ada"));
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_workspace_rooted_cwd_runs_in_the_created_root() -> TestResult {
let scratch = tempfile::tempdir()?;
let root = scratch.path().join("clones");
let mut command = contract(&["pwd"], Vec::new());
command.parameters.clear();
command.cwd = Some(super::super::workspace_root::WORKSPACE_ROOT_PLACEHOLDER.to_owned());
let (decorated, _reached, _transcript) = dispatcher_with_root(
lookup(CommandBodyCapture::Text, command),
Ok("unused".to_owned()),
WorkspaceRoot::from_resolution(Ok(root.clone())),
);
let handle = tokio::task::spawn_blocking(move || decorated.dispatch(request("locate", "{}")));
let encoded = handle
.await?
.map_err(|error| format!("declared command failed: {error}"))?;
let result: serde_json::Value = serde_json::from_str(&encoded)?;
let observed = result.as_str().ok_or("a text capture yields a string")?;
assert!(
std::path::Path::new(observed).ends_with("clones"),
"the command must run in the server-resolved root, got {observed}"
);
assert!(
root.is_dir(),
"dispatching a placeholder-bearing cwd must create the missing root"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_workspace_rooted_cwd_refuses_terminally_when_the_root_is_unresolved() -> TestResult {
let mut command = contract(&["pwd"], Vec::new());
command.parameters.clear();
command.cwd = Some(super::super::workspace_root::WORKSPACE_ROOT_PLACEHOLDER.to_owned());
let (decorated, reached, _transcript) = dispatcher_with_root(
lookup(CommandBodyCapture::Text, command),
Ok("unused".to_owned()),
WorkspaceRoot::from_resolution(Err(WorkspaceRootError::Unresolvable {
reason: "cannot resolve Aion home: set AION_HOME or HOME".to_owned(),
})),
);
let handle = tokio::task::spawn_blocking(move || decorated.dispatch(request("locate", "{}")));
let Err(error) = handle.await? else {
return Err("an unresolved root must refuse a placeholder-bearing cwd".into());
};
assert!(error.starts_with("terminal:"), "{error}");
assert!(
error.contains("locate"),
"the refusal must name the action: {error}"
);
assert!(
error.contains("cannot resolve Aion home"),
"the refusal must carry the resolution failure's reason: {error}"
);
assert!(
reached_names(&reached).is_empty(),
"a refused body must not fall through to the worker path"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_parameter_nothing_fills_refuses_terminally_before_spawning() -> TestResult {
let (decorated, _reached, _transcript) = dispatcher_with_root(
lookup(CommandBodyCapture::Text, greeter()),
Ok("unused".to_owned()),
WorkspaceRoot::from_resolution(Ok(std::env::temp_dir())),
);
let handle = tokio::task::spawn_blocking(move || decorated.dispatch(request("greet", "{}")));
let Err(error) = handle.await? else {
return Err("an unfilled parameter must refuse".into());
};
assert!(error.starts_with("terminal:"), "{error}");
assert!(error.contains("name"), "{error}");
Ok(())
}