use aion::ActivityDispatcher;
use aion_package::contract::CommandBodyCapture;
use aion_package::{
ActionBodyContract, ArgvSlot, CommandLineContract, 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 {
let mut slots: Vec<ArgvSlot> = program
.iter()
.map(|word| slot(FillTemplate::literal((*word).to_owned()), word))
.collect();
slots.extend(args);
DeclaredCommandContract {
name: "say_hello".to_owned(),
parameters: vec![CommandParameterContract {
name: "name".to_owned(),
default: None,
}],
lines: vec![CommandLineContract { slots }],
env: Vec::new(),
cwd: None,
prior_form_refusal: 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_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: "Ada".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 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_multi_line_body_concatenates_stdout_on_the_server_path() -> TestResult {
let mut command = contract(
&["echo"],
vec![slot(FillTemplate::literal("first"), "word")],
);
command.parameters.clear();
command.lines.push(CommandLineContract {
slots: vec![
slot(FillTemplate::literal("echo"), "echo"),
slot(FillTemplate::literal("second"), "word"),
],
});
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("both", "{}")));
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!("first\nsecond"));
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(())
}