use std::collections::BTreeMap;
use std::future::Future;
use std::sync::{Arc, OnceLock};
use aion::{ActivityDispatch, ActivityDispatcher};
use aion_package::{ActionBodyContract, ContentHash};
use aion_worker::ActivityFailure;
use aion_worker::shell::{ShellAction, ShellOutcome};
use super::declared_body_ambiguity::{DeclaringVersion, ambiguous_body_refusal};
use super::declared_body_cancel::DeclaredCommandAttempts;
use super::declared_body_selection::select_declared_body;
use super::declared_body_transcript::publish_declared_transcript;
use super::workspace_root::{WORKSPACE_ROOT_PLACEHOLDER, WorkspaceRoot};
use crate::activity_publisher::ActivityEventPublisher;
#[derive(Clone, Debug)]
pub enum DeclaredBodyLookup {
None,
Declared(ActionBodyContract),
Ambiguous {
declaring: Vec<DeclaringVersion>,
},
Unreadable(String),
}
#[derive(Clone, Copy, Debug)]
pub struct DispatchingRun<'a> {
pub workflow_id: &'a aion_core::WorkflowId,
pub run_id: &'a aion_core::RunId,
}
pub trait DeclaredBodies: Send + Sync {
fn body_for(
&self,
task_queue: &str,
action: &str,
run: DispatchingRun<'_>,
) -> DeclaredBodyLookup;
}
#[derive(Clone, Default)]
pub struct DeclaredBodySource {
inner: Arc<OnceLock<Arc<dyn DeclaredBodies>>>,
}
impl std::fmt::Debug for DeclaredBodySource {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("DeclaredBodySource")
.field("installed", &self.inner.get().is_some())
.finish()
}
}
impl DeclaredBodySource {
pub fn install(&self, source: Arc<dyn DeclaredBodies>) {
if self.inner.set(source).is_err() {
tracing::warn!("declared body source already installed; ignoring duplicate set");
}
}
#[must_use]
pub fn body_for(
&self,
task_queue: &str,
action: &str,
run: DispatchingRun<'_>,
) -> DeclaredBodyLookup {
self.inner.get().map_or_else(
|| {
tracing::error!(
operation = "declared_command_dispatch",
task_queue,
action,
workflow_id = %run.workflow_id,
run_id = %run.run_id,
"declared body source consulted before it was installed; the dispatch \
falls through to the worker path and will park if the queue's only \
service is its declared bodies (#266 boot-ordering defect)"
);
DeclaredBodyLookup::None
},
|source| source.body_for(task_queue, action, run),
)
}
}
pub struct EngineDeclaredBodies {
engine: Arc<aion::Engine>,
}
impl EngineDeclaredBodies {
#[must_use]
pub const fn new(engine: Arc<aion::Engine>) -> Self {
Self { engine }
}
}
impl std::fmt::Debug for EngineDeclaredBodies {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("EngineDeclaredBodies")
}
}
impl EngineDeclaredBodies {
fn version_of(&self, run: DispatchingRun<'_>) -> Option<ContentHash> {
match self.engine.registry().get(run.workflow_id, run.run_id) {
Ok(Some(handle)) => Some(handle.loaded_version().clone()),
Ok(None) => {
tracing::warn!(
operation = "declared_command_dispatch",
workflow_id = %run.workflow_id,
run_id = %run.run_id,
"no registry handle for the dispatching run; resolving its body \
from the whole queue instead of from its own package version"
);
None
}
Err(error) => {
tracing::error!(
operation = "declared_command_dispatch",
workflow_id = %run.workflow_id,
run_id = %run.run_id,
%error,
"registry unreadable while resolving the dispatching run's version; \
resolving its body from the whole queue instead"
);
None
}
}
}
}
impl DeclaredBodies for EngineDeclaredBodies {
fn body_for(
&self,
task_queue: &str,
action: &str,
run: DispatchingRun<'_>,
) -> DeclaredBodyLookup {
let contracts = match self.engine.worker_contracts_for_queue(task_queue) {
Ok(contracts) => contracts,
Err(error) => return DeclaredBodyLookup::Unreadable(error.to_string()),
};
select_declared_body(&contracts, action, self.version_of(run).as_ref())
}
}
pub struct DeclaredCommandDispatcher {
inner: Arc<dyn ActivityDispatcher>,
bodies: DeclaredBodySource,
attempts: DeclaredCommandAttempts,
tokio: tokio::runtime::Handle,
workspace_root: WorkspaceRoot,
transcript: ActivityEventPublisher,
}
impl DeclaredCommandDispatcher {
#[must_use]
pub fn new(
inner: Arc<dyn ActivityDispatcher>,
bodies: DeclaredBodySource,
attempts: DeclaredCommandAttempts,
tokio: tokio::runtime::Handle,
workspace_root: WorkspaceRoot,
transcript: ActivityEventPublisher,
) -> Self {
Self {
inner,
bodies,
attempts,
tokio,
workspace_root,
transcript,
}
}
fn declared_action(
&self,
request: &ActivityDispatch,
command: &str,
) -> Result<ShellAction, String> {
let expanded = self.workspace_root.expand(command).map_err(|error| {
format!(
"terminal:declared body for action `{name}` uses the {placeholder} \
placeholder and cannot dispatch: {error}",
name = request.name,
placeholder = WORKSPACE_ROOT_PLACEHOLDER,
)
})?;
if let Some(expansion) = &expanded {
tracing::info!(
operation = "declared_command_dispatch",
workflow_id = %request.workflow_id,
activity_id = %request.activity_id,
activity_name = %request.name,
task_queue = %request.task_queue,
attempt = request.attempt,
workspace_root = %expansion.workspace_root,
"expanded the workspace-root placeholder in the declared command"
);
}
let command = expanded
.as_ref()
.map_or(command, |expansion| expansion.command.as_str());
ShellAction::new(command).map_err(|error| {
format!("terminal:declared command failed to parse at dispatch: {error}")
})
}
pub(super) fn join_cancel_path(
&self,
request: &ActivityDispatch,
cancellation: &aion_worker::ActivityCancellationHandle,
) -> Result<super::DeclaredAttemptRegistration, String> {
self.attempts
.register(
super::AttemptKey::new(
request.workflow_id.clone(),
request.run_id.clone(),
request.activity_id.clone(),
request.attempt,
),
cancellation.clone(),
)
.map_err(|error| match error {
crate::error::ServerError::DrainingRefusedDeclaredAttempt { .. } => {
tracing::info!(
operation = "declared_command_dispatch",
workflow_id = %request.workflow_id,
activity_id = %request.activity_id,
activity_name = %request.name,
task_queue = %request.task_queue,
attempt = request.attempt,
"declared command parked: this server is draining and starts no new work"
);
aion::PARKED_ACTIVITY_REASON.to_owned()
}
other => format!(
"terminal:declared body for action `{name}` cannot dispatch: the attempt \
could not join this server's cancel path, and a command a cancelled run \
could not stop must not be started: {other}",
name = request.name,
),
})
}
fn run_declared_command(
&self,
request: &ActivityDispatch,
command: &str,
) -> Result<String, String> {
let arguments = decode_arguments(&request.input)?;
let action = self.declared_action(request, command)?;
let (events, drain) = tokio::sync::mpsc::unbounded_channel();
let (context, cancellation) = aion_worker::ActivityContext::with_transcript(
request.workflow_id.clone(),
request.run_id.clone(),
request.activity_id.clone(),
request.attempt,
events,
);
let registration = self.join_cancel_path(request, &cancellation)?;
tracing::info!(
operation = "declared_command_dispatch",
workflow_id = %request.workflow_id,
activity_id = %request.activity_id,
activity_name = %request.name,
task_queue = %request.task_queue,
attempt = request.attempt,
"executing declared action body at the server"
);
crate::death_note::breadcrumb(&format!(
"declared-action start action={} workflow_id={} run_id={} activity_id={} attempt={}",
request.name, request.workflow_id, request.run_id, request.activity_id, request.attempt,
));
let bound = aion::activity_timeout_from_config(&request.config);
let transcript = self.transcript.clone();
let ended = self.tokio.block_on(async move {
let pump = tokio::spawn(publish_declared_transcript(transcript, drain));
let ended = run_bounded(action.run(&arguments, &context), &cancellation, bound).await;
drop(context);
if let Err(error) = pump.await {
tracing::warn!(
%error,
operation = "declared_command_dispatch",
"declared command transcript: the publishing task ended abnormally; some \
output lines may not have been retained"
);
}
ended
});
drop(registration);
encode_end(request, ended, |outcome| {
serde_json::to_string(&outcome).map_err(|error| {
ActivityFailure::terminal(format!(
"declared command result failed to encode: {error}"
))
})
})
}
}
pub(super) fn encode_end(
request: &ActivityDispatch,
ended: AttemptEnd,
shape: impl FnOnce(ShellOutcome) -> Result<String, ActivityFailure>,
) -> Result<String, String> {
let outcome = match ended {
AttemptEnd::Ran(outcome) => outcome,
AttemptEnd::Expired { bound, ran_anyway } => {
if let Some(exit_code) = ran_anyway {
tracing::warn!(
operation = "declared_command_dispatch",
workflow_id = %request.workflow_id,
activity_id = %request.activity_id,
activity_name = %request.name,
attempt = request.attempt,
exit_code,
bound_ms = bound.as_millis(),
"the declared command finished while it was being stopped on its \
authored bound; its result is discarded in favour of the timeout"
);
}
return Err(aion::activity_timeout_reason(bound));
}
};
match outcome.and_then(shape) {
Ok(encoded) => Ok(encoded),
Err(failure) => {
let prefix = match failure.classification() {
aion_worker::Classification::Retryable => "retryable",
aion_worker::Classification::PolicyRefused => "policy_refused",
aion_worker::Classification::Terminal => "terminal",
};
Err(format!("{prefix}:{}", failure.message()))
}
}
}
#[derive(Debug)]
pub(super) enum AttemptEnd {
Ran(Result<ShellOutcome, ActivityFailure>),
Expired {
bound: std::time::Duration,
ran_anyway: Option<i32>,
},
}
pub(super) async fn run_bounded(
run: impl Future<Output = Result<ShellOutcome, ActivityFailure>>,
cancellation: &aion_worker::ActivityCancellationHandle,
bound: Option<std::time::Duration>,
) -> AttemptEnd {
let Some(bound) = bound else {
return AttemptEnd::Ran(run.await);
};
tokio::pin!(run);
match tokio::time::timeout(bound, &mut run).await {
Ok(outcome) => AttemptEnd::Ran(outcome),
Err(_elapsed) => {
cancellation.cancel();
AttemptEnd::Expired {
bound,
ran_anyway: run.await.ok().map(|outcome| outcome.exit_code),
}
}
}
}
impl DeclaredCommandDispatcher {
pub(super) fn transcript(&self) -> ActivityEventPublisher {
self.transcript.clone()
}
pub(super) fn tokio(&self) -> &tokio::runtime::Handle {
&self.tokio
}
pub(super) const fn workspace_root(&self) -> &WorkspaceRoot {
&self.workspace_root
}
}
impl std::fmt::Debug for DeclaredCommandDispatcher {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("DeclaredCommandDispatcher")
.field("bodies", &self.bodies)
.finish_non_exhaustive()
}
}
impl ActivityDispatcher for DeclaredCommandDispatcher {
fn dispatch(&self, request: ActivityDispatch) -> Result<String, String> {
let run = DispatchingRun {
workflow_id: &request.workflow_id,
run_id: &request.run_id,
};
match self
.bodies
.body_for(&request.task_queue, &request.name, run)
{
DeclaredBodyLookup::None => self.inner.dispatch(request),
DeclaredBodyLookup::Unreadable(reason) => {
tracing::error!(
operation = "declared_command_dispatch",
workflow_id = %request.workflow_id,
activity_name = %request.name,
task_queue = %request.task_queue,
%reason,
"declared-body catalog read failed; delegating to the worker path"
);
self.inner.dispatch(request)
}
DeclaredBodyLookup::Ambiguous { declaring } => Err(ambiguous_body_refusal(
&request.name,
&request.task_queue,
&declaring,
)),
DeclaredBodyLookup::Declared(ActionBodyContract::Run { command }) => {
self.run_declared_command(&request, &command)
}
DeclaredBodyLookup::Declared(ActionBodyContract::Command { capture, command }) => {
self.run_declared_command_body(&request, capture, *command)
}
}
}
}
pub(super) fn decode_arguments(input: &str) -> Result<BTreeMap<String, serde_json::Value>, String> {
let value: serde_json::Value = serde_json::from_str(input)
.map_err(|error| format!("terminal:declared command input is not valid JSON: {error}"))?;
match value {
serde_json::Value::Object(members) => Ok(members.into_iter().collect()),
other => Err(format!(
"terminal:declared command input must be a JSON object binding the action's \
parameters by name; got {}",
json_kind(&other)
)),
}
}
const fn json_kind(value: &serde_json::Value) -> &'static str {
match value {
serde_json::Value::Null => "null",
serde_json::Value::Bool(_) => "a boolean",
serde_json::Value::Number(_) => "a number",
serde_json::Value::String(_) => "a string",
serde_json::Value::Array(_) => "an array",
serde_json::Value::Object(_) => "an object",
}
}
#[cfg(test)]
#[path = "declared_body_containment_tests.rs"]
mod declared_body_containment_tests;
#[cfg(test)]
pub(super) mod tests {
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use aion::{ActivityDispatch, ActivityDispatcher};
use aion_core::{ActivityId, RunId, WorkflowId};
use aion_package::ActionBodyContract;
use aion_core::ActivityEventKind;
use aion_store::ActivityStreamKey;
use super::super::workspace_root::{WorkspaceRoot, WorkspaceRootError};
use super::{
ActivityEventPublisher, DeclaredBodies, DeclaredBodyLookup, DeclaredBodySource,
DeclaredCommandAttempts, DeclaredCommandDispatcher, DeclaringVersion, DispatchingRun,
decode_arguments,
};
pub(crate) type TestResult = Result<(), Box<dyn std::error::Error>>;
struct RecordingInner {
reached: Arc<Mutex<Vec<String>>>,
reply: Result<String, String>,
}
impl ActivityDispatcher for RecordingInner {
fn dispatch(&self, request: ActivityDispatch) -> Result<String, String> {
match self.reached.lock() {
Ok(mut names) => names.push(request.name),
Err(poisoned) => poisoned.into_inner().push(request.name),
}
self.reply.clone()
}
}
struct FixedBodies {
lookup: DeclaredBodyLookup,
}
impl DeclaredBodies for FixedBodies {
fn body_for(
&self,
_task_queue: &str,
_action: &str,
_run: DispatchingRun<'_>,
) -> DeclaredBodyLookup {
self.lookup.clone()
}
}
struct RecordingBodies {
seen: Arc<Mutex<Vec<(WorkflowId, RunId)>>>,
}
impl DeclaredBodies for RecordingBodies {
fn body_for(
&self,
_task_queue: &str,
_action: &str,
run: DispatchingRun<'_>,
) -> DeclaredBodyLookup {
let observed = (run.workflow_id.clone(), run.run_id.clone());
match self.seen.lock() {
Ok(mut seen) => seen.push(observed),
Err(poisoned) => poisoned.into_inner().push(observed),
}
DeclaredBodyLookup::None
}
}
pub(crate) fn request(name: &str, input: &str) -> ActivityDispatch {
ActivityDispatch {
namespace: "default".to_owned(),
task_queue: "shell".to_owned(),
node: None,
workflow_id: WorkflowId::new_v4(),
run_id: RunId::new_v4(),
activity_id: ActivityId::from_sequence_position(1),
name: name.to_owned(),
input: input.to_owned(),
config: "{}".to_owned(),
attempt: 1,
labels: BTreeMap::new(),
advisory: false,
}
}
fn dispatcher(
lookup: DeclaredBodyLookup,
reply: Result<String, String>,
) -> (DeclaredCommandDispatcher, Arc<Mutex<Vec<String>>>) {
let (decorated, reached, _transcript) = dispatcher_with_root(
lookup,
reply,
WorkspaceRoot::from_resolution(Ok(std::env::temp_dir())),
);
(decorated, reached)
}
const TRANSCRIPT_CAPACITY: std::num::NonZeroUsize = match std::num::NonZeroUsize::new(64) {
Some(capacity) => capacity,
None => std::num::NonZeroUsize::MIN,
};
pub(crate) fn dispatcher_with_root(
lookup: DeclaredBodyLookup,
reply: Result<String, String>,
workspace_root: WorkspaceRoot,
) -> (
DeclaredCommandDispatcher,
Arc<Mutex<Vec<String>>>,
ActivityEventPublisher,
) {
dispatcher_with_attempts(
lookup,
reply,
workspace_root,
DeclaredCommandAttempts::new(crate::shutdown::DrainState::default()),
)
}
pub(super) fn dispatcher_with_attempts(
lookup: DeclaredBodyLookup,
reply: Result<String, String>,
workspace_root: WorkspaceRoot,
attempts: DeclaredCommandAttempts,
) -> (
DeclaredCommandDispatcher,
Arc<Mutex<Vec<String>>>,
ActivityEventPublisher,
) {
let reached = Arc::new(Mutex::new(Vec::new()));
let inner = RecordingInner {
reached: Arc::clone(&reached),
reply,
};
let bodies = DeclaredBodySource::default();
bodies.install(Arc::new(FixedBodies { lookup }));
let store: Arc<dyn aion_store::ObservabilityStore> =
Arc::new(aion_store::InMemoryObservabilityStore::default());
let transcript = ActivityEventPublisher::new(
store,
TRANSCRIPT_CAPACITY,
crate::activity_publisher::TranscriptBatchPolicy::UNBATCHED,
);
let decorated = DeclaredCommandDispatcher::new(
Arc::new(inner),
bodies,
attempts,
tokio::runtime::Handle::current(),
workspace_root,
transcript.clone(),
);
(decorated, reached, transcript)
}
pub(crate) fn reached_names(reached: &Arc<Mutex<Vec<String>>>) -> Vec<String> {
match reached.lock() {
Ok(names) => names.clone(),
Err(poisoned) => poisoned.into_inner().clone(),
}
}
#[tokio::test(flavor = "multi_thread")]
async fn a_bodiless_action_is_delegated_untouched() -> TestResult {
let (decorated, reached) =
dispatcher(DeclaredBodyLookup::None, Ok("\"worker-served\"".to_owned()));
let handle =
tokio::task::spawn_blocking(move || decorated.dispatch(request("plain", "{}")));
let result = handle.await?;
assert_eq!(result, Ok("\"worker-served\"".to_owned()));
assert_eq!(reached_names(&reached), vec!["plain".to_owned()]);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_declared_body_executes_without_touching_the_worker_path() -> TestResult {
let (decorated, reached) = dispatcher(
DeclaredBodyLookup::Declared(ActionBodyContract::Run {
command: "echo {{greeting}}".to_owned(),
}),
Err("terminal:the worker path must never be reached".to_owned()),
);
let handle = tokio::task::spawn_blocking(move || {
decorated.dispatch(request(
"greet",
"{\"greeting\":\"hello from the contract\"}",
))
});
let result = handle.await?;
let encoded = result.map_err(|error| format!("declared command failed: {error}"))?;
let outcome: serde_json::Value = serde_json::from_str(&encoded)?;
assert_eq!(outcome["stdout"], "hello from the contract");
assert_eq!(outcome["exit_code"], 0);
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_draining_server_parks_a_declared_dispatch_without_starting_it() -> TestResult {
let marker =
std::env::temp_dir().join(format!("aion-drain-park-{}", uuid::Uuid::new_v4().simple()));
let drain = crate::shutdown::DrainState::default();
let attempts = DeclaredCommandAttempts::new(drain.clone());
let (decorated, reached, _transcript) = dispatcher_with_attempts(
DeclaredBodyLookup::Declared(ActionBodyContract::Run {
command: format!("touch {}", marker.display()),
}),
Err("terminal:the worker path must never be reached".to_owned()),
WorkspaceRoot::from_resolution(Ok(std::env::temp_dir())),
attempts.clone(),
);
assert!(drain.begin(), "the first begin() must flip the latch");
let handle =
tokio::task::spawn_blocking(move || decorated.dispatch(request("touch_marker", "{}")));
let result = handle.await?;
assert_eq!(
result,
Err(aion::PARKED_ACTIVITY_REASON.to_owned()),
"a drained-over declared dispatch must wear the park sentinel, not a failure"
);
assert!(
!marker.exists(),
"the declared command must never start on a draining server"
);
assert!(
reached_names(&reached).is_empty(),
"the park must not fall through to the worker path"
);
assert!(
attempts
.executing()
.map_err(|error| format!("census read failed: {error}"))?
.is_empty(),
"a parked dispatch must leave no census entry to hold the drain gate open"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_declared_body_publishes_its_output_onto_the_transcript() -> TestResult {
let (decorated, reached, transcript) = dispatcher_with_root(
DeclaredBodyLookup::Declared(ActionBodyContract::Run {
command: "sh -c 'echo one; echo two; echo warned >&2'".to_owned(),
}),
Err("terminal:the worker path must never be reached".to_owned()),
WorkspaceRoot::from_resolution(Ok(std::env::temp_dir())),
);
let dispatch = request("noisy", "{}");
let key = ActivityStreamKey::new(
dispatch.workflow_id.clone(),
dispatch.run_id.clone(),
dispatch.activity_id.clone(),
dispatch.attempt,
);
let handle = tokio::task::spawn_blocking(move || decorated.dispatch(dispatch));
let encoded = handle
.await?
.map_err(|error| format!("declared command failed: {error}"))?;
let outcome: serde_json::Value = serde_json::from_str(&encoded)?;
assert_eq!(outcome["stdout"], "one\ntwo");
assert_eq!(outcome["stderr"], "warned");
assert!(reached_names(&reached).is_empty());
let retained = transcript.replay_from(&key, 0).await?;
let lines = retained
.iter()
.map(|record| match &record.event.kind {
ActivityEventKind::Message { text, .. } => {
(record.event.agent_role.clone(), text.clone())
}
other => (record.event.agent_role.clone(), format!("{other:?}")),
})
.collect::<Vec<_>>();
assert!(
lines.contains(&("command stdout".to_owned(), "one".to_owned()))
&& lines.contains(&("command stdout".to_owned(), "two".to_owned())),
"each stdout line must be its own transcript event: {lines:?}"
);
assert!(
lines.contains(&("command stderr".to_owned(), "warned".to_owned())),
"stderr must be on the transcript, labelled by its stream: {lines:?}"
);
let sequences = retained
.iter()
.map(|record| record.store_seq)
.collect::<Vec<_>>();
assert_eq!(
sequences,
(0..u64::try_from(retained.len())?).collect::<Vec<_>>(),
"the sequencer assigns a gap-free durable order"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_failing_declared_command_reports_retryable_with_its_stderr() -> TestResult {
let (decorated, _reached) = dispatcher(
DeclaredBodyLookup::Declared(ActionBodyContract::Run {
command: "sh -c 'echo boom >&2; exit 7'".to_owned(),
}),
Ok("unused".to_owned()),
);
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(())
}
#[test]
fn the_printed_hash_parses_back_as_a_content_hash() -> TestResult {
let version = aion_package::ContentHash::from_bytes([0x5a; 32]);
let routed = aion_package::ContentHash::from_bytes([0xa5; 32]);
let refusal = super::ambiguous_body_refusal(
"find_repositories",
"local",
&[
DeclaringVersion {
content_hash: version.to_string(),
workflow_types: vec!["sweeper".to_owned()],
route_active: false,
body: 0,
},
DeclaringVersion {
content_hash: routed.to_string(),
workflow_types: vec!["sweeper".to_owned()],
route_active: true,
body: 1,
},
],
);
let Some(command) = refusal.split("`aion unload sweeper ").nth(1) else {
return Err(format!("no unload command in the refusal: {refusal}").into());
};
let Some(printed) = command.split('`').next() else {
return Err(format!("the unload command is unterminated: {refusal}").into());
};
let parsed: aion_package::ContentHash = printed.parse()?;
assert_eq!(
parsed, version,
"the printed hash must round-trip to the version it names"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn ambiguous_bodies_refuse_terminally_by_name() -> TestResult {
let superseded = "1111111111111111111111111111111111111111111111111111111111111111";
let routed = "2222222222222222222222222222222222222222222222222222222222222222";
let (decorated, reached) = dispatcher(
DeclaredBodyLookup::Ambiguous {
declaring: vec![
DeclaringVersion {
content_hash: superseded.to_owned(),
workflow_types: vec!["sweeper".to_owned()],
route_active: false,
body: 0,
},
DeclaringVersion {
content_hash: routed.to_owned(),
workflow_types: vec!["sweeper".to_owned()],
route_active: true,
body: 1,
},
],
},
Ok(String::new()),
);
let handle = tokio::task::spawn_blocking(move || decorated.dispatch(request("torn", "{}")));
let Err(error) = handle.await? else {
return Err("ambiguous bodies must refuse".into());
};
assert!(error.starts_with("terminal:"), "{error}");
assert!(error.contains("torn"), "{error}");
assert!(
error.contains(&format!("`aion unload sweeper {superseded}`")),
"the dispatch refusal must name the version to retire: {error}"
);
assert!(reached_names(&reached).is_empty());
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_placeholder_bearing_body_executes_with_the_expanded_root() -> TestResult {
let scratch = tempfile::tempdir()?;
let root = scratch.path().join("clones");
let root_text = root.to_string_lossy().into_owned();
let (decorated, reached, _transcript) = dispatcher_with_root(
DeclaredBodyLookup::Declared(ActionBodyContract::Run {
command: "echo {workspace_root}".to_owned(),
}),
Err("terminal:the worker path must never be reached".to_owned()),
WorkspaceRoot::from_resolution(Ok(root.clone())),
);
let handle =
tokio::task::spawn_blocking(move || decorated.dispatch(request("provision", "{}")));
let result = handle.await?;
let encoded = result.map_err(|error| format!("declared command failed: {error}"))?;
let outcome: serde_json::Value = serde_json::from_str(&encoded)?;
assert_eq!(
outcome["stdout"], root_text,
"the command must observe the server-resolved root as its argv word"
);
assert_eq!(outcome["exit_code"], 0);
assert!(
root.is_dir(),
"dispatching a placeholder-bearing body must create the missing root"
);
assert!(reached_names(&reached).is_empty());
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_placeholder_bearing_body_refuses_terminally_when_the_root_is_unresolved()
-> TestResult {
let (decorated, reached, _transcript) = dispatcher_with_root(
DeclaredBodyLookup::Declared(ActionBodyContract::Run {
command: "echo {workspace_root}".to_owned(),
}),
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("provision", "{}")));
let Err(error) = handle.await? else {
return Err("an unresolved root must refuse a placeholder-bearing body".into());
};
assert!(error.starts_with("terminal:"), "{error}");
assert!(
error.contains("provision"),
"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_shape_changing_root_refuses_terminally_naming_the_action() -> TestResult {
let (decorated, reached, _transcript) = dispatcher_with_root(
DeclaredBodyLookup::Declared(ActionBodyContract::Run {
command: "echo {workspace_root}".to_owned(),
}),
Ok("unused".to_owned()),
WorkspaceRoot::from_resolution(Ok(std::path::PathBuf::from("/absolute/with{brace"))),
);
let handle =
tokio::task::spawn_blocking(move || decorated.dispatch(request("provision", "{}")));
let Err(error) = handle.await? else {
return Err("a shape-changing root must refuse a placeholder-bearing body".into());
};
assert!(error.starts_with("terminal:"), "{error}");
assert!(
error.contains("provision"),
"the refusal must name the action: {error}"
);
assert!(
error.contains("would change the parsed shape"),
"the refusal must carry the shape-changing diagnosis: {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 an_uncreatable_root_refuses_terminally_naming_the_action() -> TestResult {
let scratch = tempfile::tempdir()?;
let file = scratch.path().join("occupied");
std::fs::write(&file, b"not a directory")?;
let (decorated, reached, _transcript) = dispatcher_with_root(
DeclaredBodyLookup::Declared(ActionBodyContract::Run {
command: "echo {workspace_root}".to_owned(),
}),
Ok("unused".to_owned()),
WorkspaceRoot::from_resolution(Ok(file.join("clones"))),
);
let handle =
tokio::task::spawn_blocking(move || decorated.dispatch(request("provision", "{}")));
let Err(error) = handle.await? else {
return Err("an uncreatable root must refuse a placeholder-bearing body".into());
};
assert!(error.starts_with("terminal:"), "{error}");
assert!(
error.contains("provision"),
"the refusal must name the action: {error}"
);
assert!(
error.contains("could not be created"),
"the refusal must carry the creation-failure diagnosis: {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_body_without_the_placeholder_is_untouched_by_resolution_failure() -> TestResult {
let (decorated, _reached, _transcript) = dispatcher_with_root(
DeclaredBodyLookup::Declared(ActionBodyContract::Run {
command: "echo {{greeting}}".to_owned(),
}),
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("greet", "{\"greeting\":\"still served\"}"))
});
let result = handle.await?;
let encoded = result.map_err(|error| format!("declared command failed: {error}"))?;
let outcome: serde_json::Value = serde_json::from_str(&encoded)?;
assert_eq!(outcome["stdout"], "still served");
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn an_unreadable_catalog_delegates_to_the_worker_path() -> TestResult {
let (decorated, reached) = dispatcher(
DeclaredBodyLookup::Unreadable("catalog offline".to_owned()),
Ok("\"served anyway\"".to_owned()),
);
let handle =
tokio::task::spawn_blocking(move || decorated.dispatch(request("resilient", "{}")));
let result = handle.await?;
assert_eq!(result, Ok("\"served anyway\"".to_owned()));
assert_eq!(reached_names(&reached), vec!["resilient".to_owned()]);
Ok(())
}
#[test]
fn non_object_input_is_refused_terminally_by_shape() {
for (input, kind) in [
("[1,2]", "an array"),
("\"text\"", "a string"),
("3", "a number"),
("null", "null"),
("true", "a boolean"),
] {
let Err(error) = decode_arguments(input) else {
unreachable_refusal(input);
return;
};
assert!(error.starts_with("terminal:"), "{error}");
assert!(error.contains(kind), "{error} must name {kind}");
}
}
fn unreachable_refusal(input: &str) {
assert!(
input.is_empty(),
"input `{input}` must have been refused by shape"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn the_reader_is_asked_about_the_run_that_is_dispatching() -> TestResult {
let seen = Arc::new(Mutex::new(Vec::new()));
let bodies = DeclaredBodySource::default();
bodies.install(Arc::new(RecordingBodies {
seen: Arc::clone(&seen),
}));
let reached = Arc::new(Mutex::new(Vec::new()));
let decorated = DeclaredCommandDispatcher::new(
Arc::new(RecordingInner {
reached: Arc::clone(&reached),
reply: Ok("\"worker-served\"".to_owned()),
}),
bodies,
DeclaredCommandAttempts::new(crate::shutdown::DrainState::default()),
tokio::runtime::Handle::current(),
WorkspaceRoot::from_resolution(Ok(std::env::temp_dir())),
ActivityEventPublisher::new(
Arc::new(aion_store::InMemoryObservabilityStore::default()),
TRANSCRIPT_CAPACITY,
crate::activity_publisher::TranscriptBatchPolicy::UNBATCHED,
),
);
let dispatch = request("plain", "{}");
let expected = (dispatch.workflow_id.clone(), dispatch.run_id.clone());
let handle = tokio::task::spawn_blocking(move || decorated.dispatch(dispatch));
handle
.await?
.map_err(|error| format!("dispatch failed: {error}"))?;
let observed = match seen.lock() {
Ok(observed) => observed.clone(),
Err(poisoned) => poisoned.into_inner().clone(),
};
assert_eq!(
observed,
vec![expected],
"the body reader must be asked about the dispatching run itself"
);
Ok(())
}
}