#[path = "common/mod.rs"]
mod common;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::{Duration, Instant};
use capsule_runtime::delegation::{DelegationStatus, Reporter, SpawnerHandle, SPAWNER_ENV};
use capsule_runtime::{
launch_child_capsule, ChildLaunchRequest, CompletionAddress, LaunchedChild, SpawnApproval,
Spawner, TrustClass,
};
use common::{component, mur_binary, Roost, ScriptedServer};
use serde_json::Value;
use tempfile::TempDir;
const PARENT_CAPSULE: &str = "completion-parent";
const APPROVER_SESSION: &str = "ses_completion00000000000approver";
const DRIVER: &str = "murmur-driver-anthropic";
const DRIVER_VERSION: &str = "0.1.4";
const MARKER_VAR: &str = "MURMUR_TEST_ALLOWED_VAR";
const MARKER: &str = "child-result-marker-91f3";
const PARENT_STDERR_TAIL_LINES: usize = 20;
struct Suite {
roost: Roost,
credential: String,
home: TempDir,
_inference: ScriptedServer,
_failing_inference: ScriptedServer,
}
fn suite() -> &'static Suite {
static SUITE: OnceLock<Suite> = OnceLock::new();
SUITE.get_or_init(|| {
let home = TempDir::new().unwrap();
std::env::set_var("HOME", home.path());
std::env::set_var(MARKER_VAR, MARKER);
let inference = ScriptedServer::always_replying("parent turn done");
let failing_inference =
ScriptedServer::always_answering(r#"{"content":[{"type":"text","text":"hel"#);
let registry_path = home.path().join(".murmur").join("artifacts");
std::fs::create_dir_all(®istry_path).unwrap();
let roost = Roost::start_at(®istry_path, &[PARENT_CAPSULE]);
common::publish_driver(
®istry_path,
DRIVER,
DRIVER_VERSION,
&common::fixture_path("drivers/anthropic/driver/murmur-driver-anthropic.wasm"),
);
common::publish_capsule(
®istry_path,
PARENT_CAPSULE,
"0.1.0",
&format!(
"artifacts:\n - name: {DRIVER}\n version: {DRIVER_VERSION}\n \
runtime: driver\n gateway:\n endpoint: {url}\n \
api_key: test-key\ncapabilities:\n \
network:\n allow: [{endpoint}, {failing}]\n \
env:\n allow: [{MARKER_VAR}]\n \
spawn:\n allow: [worker, waiting-worker, slow-worker, failing-worker]\n\
lifecycle:\n task_acceptance: queue\n after_task: sleep\n\
trace:\n capture: content\n\
inference:\n transport: http\n model: test-model\n \
driver:\n artifact: {DRIVER}\n",
endpoint = inference.authority(),
failing = failing_inference.authority(),
url = inference.endpoint,
),
None,
);
common::publish_capsule(
®istry_path,
"failing-worker",
"0.1.0",
&format!(
"artifacts:\n - name: {DRIVER}\n version: {DRIVER_VERSION}\n runtime: \
driver\n gateway:\n endpoint: {url}\n api_key: test-key\n\
capabilities:\n network:\n allow: [{endpoint}]\n\
lifecycle:\n task_acceptance: queue\n after_task: exit\n\
inference:\n transport: http\n model: test-model\n \
driver:\n artifact: {DRIVER}\n",
endpoint = failing_inference.authority(),
url = failing_inference.endpoint,
),
None,
);
common::publish_capsule(
®istry_path,
"worker",
"0.1.0",
&format!("artifacts: []\ncapabilities:\n env:\n allow: [{MARKER_VAR}]\n"),
Some(&component("capsule-env-echo.wasm")),
);
common::publish_capsule(
®istry_path,
"waiting-worker",
"0.1.0",
"artifacts: []\n",
Some(&component("capsule-path-reader.wasm")),
);
common::publish_capsule(
®istry_path,
"slow-worker",
"0.1.0",
&format!(
"mur_version: \"0.0.1\"\nartifacts:\n - name: {DRIVER}\n version: \
{DRIVER_VERSION}\n runtime: driver\n gateway:\n endpoint: {url}\n \
api_key: test-key\ncapabilities:\n network:\n \
allow: [{endpoint}]\nlifecycle:\n task_acceptance: queue\n after_task: sleep\n\
inference:\n transport: http\n model: test-model\n \
driver:\n artifact: {DRIVER}\n",
endpoint = inference.authority(),
url = inference.endpoint,
),
None,
);
let credential = roost.register(APPROVER_SESSION, PARENT_CAPSULE, "0.1.0");
std::env::set_var(capsule_runtime::MUR_BINARY_ENV, mur_binary());
Suite {
roost,
credential,
home,
_inference: inference,
_failing_inference: failing_inference,
}
})
}
struct RunningParent {
process: Option<Child>,
workdir: TempDir,
session_id: String,
url: String,
}
impl RunningParent {
fn start() -> Self {
let suite = suite();
let workdir = TempDir::new().unwrap();
let mut process = Command::new(mur_binary())
.args([
"run",
"--capsule",
PARENT_CAPSULE,
"--capsule-version",
"0.1.0",
"--workdir",
workdir.path().to_str().unwrap(),
"--json",
"--no-env-file",
])
.current_dir(workdir.path())
.env_clear()
.env("HOME", suite.home.path())
.env("PATH", std::env::var("PATH").unwrap_or_default())
.env("MURMUR_ROOST_URL", &suite.roost.url)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("the parent capsule process starts");
let stderr_tail = Arc::new(Mutex::new(Vec::<String>::new()));
let stderr = process.stderr.take().expect("the parent's stderr is piped");
let collector = Arc::clone(&stderr_tail);
std::thread::spawn(move || {
for line in BufReader::new(stderr).lines().map_while(Result::ok) {
let mut tail = collector.lock().unwrap_or_else(|e| e.into_inner());
if tail.len() == PARENT_STDERR_TAIL_LINES {
tail.remove(0);
}
tail.push(line);
}
});
let stdout = process.stdout.take().expect("the parent reports on stdout");
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let mut reader = BufReader::new(stdout);
let mut line = String::new();
let first = match reader.read_line(&mut line) {
Ok(0) | Err(_) => None,
Ok(_) => Some(line.trim_end().to_string()),
};
let _ = tx.send(first);
let mut rest = String::new();
while matches!(reader.read_line(&mut rest), Ok(read) if read > 0) {
rest.clear();
}
});
let tail = || {
let lines = stderr_tail.lock().unwrap_or_else(|e| e.into_inner());
if lines.is_empty() {
"(its stderr was empty)".to_string()
} else {
format!("its last stderr lines were:\n{}", lines.join("\n"))
}
};
let line = match rx.recv_timeout(Duration::from_secs(180)) {
Ok(Some(line)) => line,
Ok(None) => {
let status = match process.wait() {
Ok(status) => status.to_string(),
Err(error) => format!("its status could not be read: {error}"),
};
panic!(
"the parent exited without printing its --json launch line ({status}); {}",
tail()
)
}
Err(_) => panic!(
"the parent printed no --json launch line within 180s and is still running; {}",
tail()
),
};
let report: Value = serde_json::from_str(&line).unwrap_or_else(|error| {
panic!("the parent's launch line did not parse: {error}: {line}")
});
let session_id = report["session_id"]
.as_str()
.expect("the launch line carries a session id")
.to_string();
let url = report["url"]
.as_str()
.filter(|url| !url.is_empty())
.map(|url| format!("http://{url}"))
.expect("the launch line carries an address");
Self {
process: Some(process),
workdir,
session_id,
url,
}
}
fn spawner(&self, trust: TrustClass) -> Spawner {
Spawner {
session_id: self.session_id.clone(),
context_id: format!("ctx_{}", uuid_hex()),
report_to: Some(CompletionAddress {
url: self.url.clone(),
trust,
}),
}
}
fn trace_path(&self) -> PathBuf {
self.workdir
.path()
.join(".murmur")
.join(&self.session_id)
.join("trace.jsonl")
}
fn trace(&self) -> Vec<Value> {
std::fs::read_to_string(self.trace_path())
.unwrap_or_default()
.lines()
.filter(|line| !line.is_empty())
.filter_map(|line| serde_json::from_str(line).ok())
.collect()
}
fn wait_for_completion_task(&self, delegation_id: &str) -> Value {
let deadline = Instant::now() + Duration::from_secs(90);
loop {
if let Some(event) = self.trace().into_iter().find(|event| {
event["event_type"] == "task_start" && event["delegation_id"] == delegation_id
}) {
return event;
}
assert!(
Instant::now() < deadline,
"the parent never started a task for delegation {delegation_id}; trace:\n{}",
std::fs::read_to_string(self.trace_path()).unwrap_or_default()
);
std::thread::sleep(Duration::from_millis(100));
}
}
fn wait_for_task_end(&self, task_id: &str) {
let deadline = Instant::now() + Duration::from_secs(90);
loop {
if self
.trace()
.iter()
.any(|event| event["event_type"] == "task_end" && event["task_id"] == task_id)
{
return;
}
assert!(
Instant::now() < deadline,
"the parent never finished task {task_id}; trace:\n{}",
std::fs::read_to_string(self.trace_path()).unwrap_or_default()
);
std::thread::sleep(Duration::from_millis(100));
}
}
fn kill(&mut self) {
if let Some(mut process) = self.process.take() {
let _ = process.kill();
let _ = process.wait();
}
}
}
impl Drop for RunningParent {
fn drop(&mut self) {
self.kill();
}
}
fn uuid_hex() -> String {
uuid::Uuid::now_v7().simple().to_string()
}
fn agent_child_slot() -> std::sync::MutexGuard<'static, ()> {
static SLOT: std::sync::Mutex<()> = std::sync::Mutex::new(());
SLOT.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn launch(
parent_dir: &Path,
name: &str,
env_allow: &[&str],
spawner: Option<Spawner>,
) -> LaunchedChild {
launch_bounded(parent_dir, name, env_allow, spawner, None)
}
fn launch_bounded(
parent_dir: &Path,
name: &str,
env_allow: &[&str],
spawner: Option<Spawner>,
completion_deadline: Option<Duration>,
) -> LaunchedChild {
let suite = suite();
let answer = suite.roost.permission(&suite.credential, name, "0.1.0");
let approval = answer["approval"]
.as_str()
.unwrap_or_else(|| panic!("spawn was refused: {answer}"))
.to_string();
launch_child_capsule(ChildLaunchRequest {
parent_accessible_workdir: parent_dir.to_path_buf(),
capsule_name: name.to_string(),
capsule_version: "0.1.0".to_string(),
grant: SpawnApproval::new(approval),
child_env_allow: env_allow.iter().map(|name| name.to_string()).collect(),
roost_url: suite.roost.url.clone(),
spawner,
completion_deadline,
})
.unwrap_or_else(|error| panic!("launching '{name}' failed: {error}"))
}
fn wait_for_completion(child: &LaunchedChild) -> Value {
wait_for_completion_at(&child.workdir)
}
fn wait_for_completion_at(workdir: &Path) -> Value {
let path = workdir.join("completion.json");
let deadline = Instant::now() + Duration::from_secs(90);
loop {
if let Ok(raw) = std::fs::read_to_string(&path) {
if let Ok(parsed) = serde_json::from_str::<Value>(&raw) {
if is_settled(&parsed) {
return parsed;
}
}
}
assert!(
Instant::now() < deadline,
"no settled {} appeared for the child at {}; what is there: {}",
"completion.json",
workdir.display(),
std::fs::read_to_string(&path).unwrap_or_else(|_| "nothing".to_string())
);
std::thread::sleep(Duration::from_millis(100));
}
}
fn wait_for_delivered_completion(workdir: &Path) -> Value {
let path = workdir.join("completion.json");
let deadline = Instant::now() + Duration::from_secs(90);
loop {
if let Ok(raw) = std::fs::read_to_string(&path) {
if let Ok(parsed) = serde_json::from_str::<Value>(&raw) {
if parsed["delivered"] == Value::Bool(true) {
return parsed;
}
if !parsed["delivery_error"].is_null() {
panic!("the completion was refused: {parsed}");
}
}
}
assert!(
Instant::now() < deadline,
"no delivered completion appeared at {}; what is there: {}",
workdir.display(),
std::fs::read_to_string(&path).unwrap_or_else(|_| "nothing".to_string())
);
std::thread::sleep(Duration::from_millis(100));
}
}
fn is_settled(completion: &Value) -> bool {
completion["delivered"] == Value::Bool(true)
|| !completion["delivery_error"].is_null()
|| completion["status"] == DelegationStatus::Terminated.as_str()
}
fn wait_for(what: &str, timeout: Duration, mut ready: impl FnMut() -> bool) {
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
if ready() {
return;
}
std::thread::sleep(Duration::from_millis(50));
}
panic!("timed out waiting for {what}");
}
fn dirs_under(parent: &Path) -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir(parent) else {
return Vec::new();
};
entries.flatten().map(|entry| entry.path()).collect()
}
fn wait_for_new_dir(parent: &Path, before: &[PathBuf]) -> Option<PathBuf> {
let deadline = Instant::now() + Duration::from_secs(60);
while Instant::now() < deadline {
if let Some(fresh) = dirs_under(parent)
.into_iter()
.find(|dir| !before.contains(dir))
{
return Some(fresh);
}
std::thread::sleep(Duration::from_millis(20));
}
None
}
#[test]
fn a_child_knows_its_spawner_from_an_injected_value_and_not_an_inherited_one() {
if capsule_runtime::skip_without_host_support(
"a_child_knows_its_spawner_from_an_injected_value_and_not_an_inherited_one",
) {
return;
}
let parent_dir = TempDir::new().unwrap();
const DECOY_HANDLE: &str =
r#"{"session_id":"ses_decoy","context_id":"ctx_decoy","delegation_id":"dlg_decoy"}"#;
std::env::set_var(SPAWNER_ENV, DECOY_HANDLE);
let parent = RunningParent::start();
let spawner = parent.spawner(TrustClass::Trusted);
let delegated = launch(
parent_dir.path(),
"worker",
&[MARKER_VAR],
Some(spawner.clone()),
);
let undelegated = launch(parent_dir.path(), "worker", &[MARKER_VAR], None);
let allowlisting = launch(
parent_dir.path(),
"worker",
&[MARKER_VAR, SPAWNER_ENV],
Some(spawner.clone()),
);
fn injected(child: &LaunchedChild) -> Vec<&String> {
child
.env
.iter()
.filter(|(key, _)| key == SPAWNER_ENV)
.map(|(_, value)| value)
.collect()
}
let values = injected(&delegated);
assert_eq!(values.len(), 1, "{:?}", delegated.env);
let handle = SpawnerHandle::parse(values[0]).expect("the injected value parses as a handle");
let address = handle.report_to.as_ref().expect("the parent wants telling");
let expected = spawner
.report_to
.as_ref()
.expect("the parent wants telling");
assert_eq!(address.url, expected.url);
assert_eq!(handle.session_id, parent.session_id);
assert_eq!(handle.context_id, spawner.context_id);
assert_eq!(address.trust, TrustClass::Trusted);
assert_eq!(
Some(handle.delegation_id.clone()),
delegated.delegation_id,
"the launcher's delegation id is what was injected"
);
assert!(
handle.delegation_id.starts_with("dlg_"),
"{}",
handle.delegation_id
);
assert!(
injected(&undelegated).is_empty(),
"a child nobody delegated holds no handle: {:?}",
undelegated.env
);
assert_eq!(undelegated.delegation_id, None);
let allowlisted = injected(&allowlisting);
assert_eq!(allowlisted.len(), 1, "{:?}", allowlisting.env);
assert_ne!(allowlisted[0], DECOY_HANDLE);
assert_eq!(
SpawnerHandle::parse(allowlisted[0])
.expect("the injected value parses")
.session_id,
parent.session_id
);
assert_ne!(delegated.delegation_id, allowlisting.delegation_id);
std::env::remove_var(SPAWNER_ENV);
}
#[test]
fn a_finished_childs_completion_arrives_at_its_parent_as_a_completion_task() {
if capsule_runtime::skip_without_host_support(
"a_finished_childs_completion_arrives_at_its_parent_as_a_completion_task",
) {
return;
}
let parent = RunningParent::start();
let parent_dir = TempDir::new().unwrap();
let child = launch(
parent_dir.path(),
"worker",
&[MARKER_VAR],
Some(parent.spawner(TrustClass::Trusted)),
);
let delegation_id = child
.delegation_id
.clone()
.expect("a delegated launch has an id");
let completion = wait_for_completion(&child);
assert_eq!(completion["status"], DelegationStatus::Ok.as_str());
assert_eq!(completion["reported_by"], Reporter::Child.as_str());
assert_eq!(completion["delivered"], true, "{completion}");
assert_eq!(completion["result_path"], "out/result.txt");
assert_eq!(completion["delegation_id"], delegation_id);
assert_eq!(completion["capsule_name"], "worker");
assert_eq!(completion["session_id"], child.session_id);
assert_eq!(completion["workdir"], child.workdir.display().to_string());
let task_start = parent.wait_for_completion_task(&delegation_id);
assert_eq!(task_start["origin"], "completion");
assert_eq!(task_start["trust"], "trusted");
assert_eq!(task_start["lane"], "bg");
assert_eq!(task_start["delegation_id"], delegation_id);
assert_eq!(task_start["source"], "a2a");
let result = std::fs::read_to_string(child.workdir.join("out").join("result.txt"))
.expect("the child wrote the file the completion named");
assert!(result.contains(MARKER), "{result}");
let trace = std::fs::read_to_string(parent.trace_path()).unwrap_or_default();
assert!(
!trace.contains(MARKER),
"the child's own output must not reach the parent's trace:\n{trace}"
);
assert!(
task_start["message_parts_bytes"].as_u64().unwrap_or(0) > 0,
"a completion task carries a message"
);
parent.wait_for_task_end(task_start["task_id"].as_str().unwrap());
assert!(
parent_sent(&parent, "out/result.txt"),
"the completion's message names where the result is"
);
assert!(
!parent_sent(&parent, MARKER),
"the completion's message must name the result, never carry it"
);
let steps = Command::new(mur_binary())
.args(["trace", "steps", parent.trace_path().to_str().unwrap()])
.output()
.expect("mur trace steps runs");
let rendered = String::from_utf8_lossy(&steps.stdout).to_string();
let row = rendered
.lines()
.find(|line| line.contains("completion/trusted"))
.unwrap_or_else(|| panic!("no completion task row in:\n{rendered}"));
assert!(row.contains("lane bg"), "{row}");
assert!(
row.contains(&format!("delegation {delegation_id}")),
"{row}"
);
}
fn parent_sent(parent: &RunningParent, needle: &str) -> bool {
common::find_in_files(
&parent
.workdir
.path()
.join(".murmur")
.join(&parent.session_id),
needle,
)
.is_some()
}
#[test]
fn an_agent_child_whose_task_failed_reports_error() {
if capsule_runtime::skip_without_host_support("an_agent_child_whose_task_failed_reports_error")
{
return;
}
let parent = RunningParent::start();
let parent_dir = TempDir::new().unwrap();
let child = {
let _slot = agent_child_slot();
launch(
parent_dir.path(),
"failing-worker",
&[],
Some(parent.spawner(TrustClass::Trusted)),
)
};
let sent = common::request(
"POST",
&child.capsule_url,
Some(
&serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "message/send",
"params": {"message": {
"messageId": "msg-1",
"role": "user",
"parts": [{"text": "Say hello."}]
}}
})
.to_string(),
),
&[],
);
assert!(
sent.as_ref()
.is_some_and(|sent| sent["result"]["id"].is_string()),
"the child took the task: {sent:?}"
);
let completion = wait_for_completion(&child);
assert_eq!(
completion["status"],
DelegationStatus::Error.as_str(),
"{completion}"
);
assert_eq!(completion["reported_by"], Reporter::Child.as_str());
}
#[test]
fn a_child_killed_without_reporting_is_reported_by_its_launcher() {
if capsule_runtime::skip_without_host_support(
"a_child_killed_without_reporting_is_reported_by_its_launcher",
) {
return;
}
let parent = RunningParent::start();
let parent_dir = TempDir::new().unwrap();
let child = {
let _slot = agent_child_slot();
launch(
parent_dir.path(),
"slow-worker",
&[],
Some(parent.spawner(TrustClass::Trusted)),
)
};
let delegation_id = child
.delegation_id
.clone()
.expect("a delegated launch has an id");
let pid = child.pid();
assert!(pid > 0);
wait_for(
"the child to write its stderr line",
Duration::from_secs(60),
|| {
child
.stderr_tail()
.iter()
.any(|line| line.contains("manifest requires mur 0.0.1"))
},
);
let killed = Command::new("kill")
.args(["-9", &pid.to_string()])
.status()
.expect("kill runs");
assert!(killed.success(), "kill -9 {pid} failed");
let completion = wait_for_completion(&child);
assert_eq!(completion["status"], DelegationStatus::Crashed.as_str());
assert_eq!(completion["reported_by"], Reporter::Launcher.as_str());
assert_eq!(completion["delivered"], true, "{completion}");
let detail = completion["detail"]
.as_str()
.expect("a crash carries detail");
assert!(detail.contains("signal: 9"), "{detail}");
assert!(detail.contains("manifest requires mur 0.0.1"), "{detail}");
let task_start = parent.wait_for_completion_task(&delegation_id);
assert_eq!(task_start["origin"], "completion");
assert_eq!(task_start["lane"], "bg");
assert_eq!(task_start["delegation_id"], delegation_id);
parent.wait_for_task_end(task_start["task_id"].as_str().unwrap());
assert!(
parent_sent(&parent, "status: crashed"),
"the completion's message says what happened"
);
drop(child);
}
#[test]
fn a_completion_with_no_parent_left_is_recorded_rather_than_dropped() {
if capsule_runtime::skip_without_host_support(
"a_completion_with_no_parent_left_is_recorded_rather_than_dropped",
) {
return;
}
let mut parent = RunningParent::start();
let parent_dir = TempDir::new().unwrap();
let spawner = parent.spawner(TrustClass::Trusted);
let children_dir = parent_dir.path().join(".murmur").join("children");
let before = dirs_under(&children_dir);
let child = std::thread::scope(|scope| {
let launch_thread =
scope.spawn(|| launch(parent_dir.path(), "waiting-worker", &[], Some(spawner)));
let child_dir = wait_for_new_dir(&children_dir, &before).expect("the child's directory");
parent.kill();
std::fs::write(child_dir.join("targets.part"), "targets.txt").unwrap();
std::fs::rename(
child_dir.join("targets.part"),
child_dir.join("targets.txt"),
)
.unwrap();
launch_thread.join().unwrap()
});
let completion = wait_for_completion(&child);
assert_eq!(
completion["status"],
DelegationStatus::Ok.as_str(),
"the child's own session succeeded; only the delivery failed: {completion}"
);
assert_eq!(completion["reported_by"], Reporter::Child.as_str());
assert_eq!(completion["delivered"], false, "{completion}");
let reason = completion["delivery_error"]
.as_str()
.expect("the refusal is recorded");
assert!(reason.contains("failed to connect"), "{reason}");
wait_for(
"the child to say on its stderr that the completion went nowhere",
Duration::from_secs(60),
|| {
child
.stderr_tail()
.iter()
.any(|line| line.contains("could not be delivered"))
},
);
}
#[test]
fn a_completion_addressed_to_another_session_is_refused_and_recorded() {
if capsule_runtime::skip_without_host_support(
"a_completion_addressed_to_another_session_is_refused_and_recorded",
) {
return;
}
let addressed = RunningParent::start();
let elsewhere = RunningParent::start();
let parent_dir = TempDir::new().unwrap();
let before = addressed
.trace()
.iter()
.filter(|event| event["origin"] == "completion")
.count();
let misaddressed = Spawner {
session_id: elsewhere.session_id.clone(),
context_id: format!("ctx_{}", uuid_hex()),
report_to: Some(CompletionAddress {
url: addressed.url.clone(),
trust: TrustClass::Trusted,
}),
};
let child = launch(
parent_dir.path(),
"worker",
&[MARKER_VAR],
Some(misaddressed),
);
let completion = wait_for_completion(&child);
assert_eq!(completion["status"], DelegationStatus::Ok.as_str());
assert_eq!(completion["delivered"], false, "{completion}");
let reason = completion["delivery_error"]
.as_str()
.expect("the refusal is recorded");
assert!(
reason.contains(&elsewhere.session_id),
"the refusal names the addressed session: {reason}"
);
assert!(
!reason.contains(&addressed.session_id),
"the refusal must not name the session actually running here: {reason}"
);
std::thread::sleep(Duration::from_secs(2));
let after = addressed
.trace()
.iter()
.filter(|event| event["origin"] == "completion")
.count();
assert_eq!(
after,
before,
"the addressed capsule must file no completion task; trace:\n{}",
std::fs::read_to_string(addressed.trace_path()).unwrap_or_default()
);
}
#[test]
fn a_completions_trust_is_the_delegating_tasks_trust() {
if capsule_runtime::skip_without_host_support(
"a_completions_trust_is_the_delegating_tasks_trust",
) {
return;
}
let parent = RunningParent::start();
let parent_dir = TempDir::new().unwrap();
let mut observed = Vec::new();
for trust in [TrustClass::Untrusted, TrustClass::Trusted] {
let child = launch(
parent_dir.path(),
"worker",
&[MARKER_VAR],
Some(parent.spawner(trust)),
);
let delegation_id = child.delegation_id.clone().expect("a delegated launch");
let completion = wait_for_completion(&child);
assert_eq!(completion["delivered"], true, "{completion}");
let task_start = parent.wait_for_completion_task(&delegation_id);
parent.wait_for_task_end(task_start["task_id"].as_str().unwrap());
observed.push((
task_start["origin"]
.as_str()
.unwrap_or_default()
.to_string(),
task_start["trust"].as_str().unwrap_or_default().to_string(),
));
}
assert_eq!(
observed,
vec![
("completion".to_string(), "untrusted".to_string()),
("completion".to_string(), "trusted".to_string()),
]
);
assert!(
parent_sent(&parent, "<untrusted-content source=task:completion>"),
"an untrusted completion must be fenced like every other untrusted task"
);
}
#[test]
fn a_delegation_the_parent_ends_is_recorded_and_not_announced() {
if capsule_runtime::skip_without_host_support(
"a_delegation_the_parent_ends_is_recorded_and_not_announced",
) {
return;
}
let parent = RunningParent::start();
let parent_dir = TempDir::new().unwrap();
let mut child = {
let _slot = agent_child_slot();
launch(
parent_dir.path(),
"slow-worker",
&[],
Some(parent.spawner(TrustClass::Trusted)),
)
};
let delegation_id = child.delegation_id.clone().expect("a delegated launch");
let workdir = child.workdir.clone();
child.shutdown().expect("the parent ends the delegation");
let deadline = Instant::now() + Duration::from_secs(60);
let completion = loop {
if let Ok(raw) = std::fs::read_to_string(workdir.join("completion.json")) {
if let Ok(parsed) = serde_json::from_str::<Value>(&raw) {
break parsed;
}
}
assert!(Instant::now() < deadline, "no completion.json was recorded");
std::thread::sleep(Duration::from_millis(100));
};
assert_eq!(completion["status"], DelegationStatus::Terminated.as_str());
assert_eq!(completion["reported_by"], Reporter::Launcher.as_str());
assert_eq!(completion["delivered"], false, "{completion}");
assert!(completion["delivery_error"].is_null(), "{completion}");
std::thread::sleep(Duration::from_secs(2));
assert!(
!parent
.trace()
.iter()
.any(|event| event["delegation_id"] == delegation_id.as_str()),
"a terminated delegation is announced to nobody"
);
}
#[test]
fn a_started_child_that_never_ends_is_stopped_at_its_deadline() {
if capsule_runtime::skip_without_host_support(
"a_started_child_that_never_ends_is_stopped_at_its_deadline",
) {
return;
}
let parent = RunningParent::start();
let parent_dir = TempDir::new().unwrap();
let (delegation_id, workdir, pid) = {
let _slot = agent_child_slot();
let child = launch_bounded(
parent_dir.path(),
"slow-worker",
&[],
Some(parent.spawner(TrustClass::Untrusted)),
Some(Duration::from_secs(5)),
);
let named = (
child.delegation_id.clone().expect("a delegated launch"),
child.workdir.clone(),
child.pid(),
);
child.release();
named
};
let completion = wait_for_delivered_completion(&workdir);
assert_eq!(completion["status"], DelegationStatus::Terminated.as_str());
assert_eq!(completion["reported_by"], Reporter::Launcher.as_str());
let detail = completion["detail"].as_str().unwrap_or_default();
assert!(
detail.contains("5s") && detail.contains("deadline"),
"the detail names the bound in seconds: {completion}"
);
let start = parent.wait_for_completion_task(&delegation_id);
assert_eq!(start["origin"], "completion", "{start}");
assert_eq!(start["lane"], "bg", "{start}");
#[cfg(target_os = "linux")]
wait_for(
"the ended child to be reaped",
Duration::from_secs(30),
|| !Path::new(&format!("/proc/{pid}")).exists(),
);
let _ = pid;
}