mod support;
fn submit_test_spec_src() -> &'static str {
r#"
spec submit_test = {
description = "Use when testing cuttlefish submit.";
model = Stub "";
data_policy = Local_only;
capabilities = [ ];
block = "block.wasm";
}
"#
}
fn run_submit(endpoint: &std::path::Path) -> std::process::Output {
std::process::Command::new(env!("CARGO_BIN_EXE_cuttlefish"))
.args(["submit", "--endpoint"])
.arg(endpoint)
.args(["--spec", "submit_test", "--input", "{}"])
.output()
.expect("cuttlefish submit failed to run")
}
fn run_submit_with(endpoint: &std::path::Path, spec: &str, input: &str) -> std::process::Output {
std::process::Command::new(env!("CARGO_BIN_EXE_cuttlefish"))
.args(["submit", "--endpoint"])
.arg(endpoint)
.args(["--spec", spec, "--input", input])
.output()
.expect("cuttlefish submit failed to run")
}
fn run_jobs(endpoint: &std::path::Path) -> std::process::Output {
std::process::Command::new(env!("CARGO_BIN_EXE_cuttlefish"))
.args(["jobs", "--endpoint"])
.arg(endpoint)
.output()
.expect("cuttlefish jobs failed to run")
}
fn run_resume(endpoint: &std::path::Path, job_id: &str) -> std::process::Output {
std::process::Command::new(env!("CARGO_BIN_EXE_cuttlefish"))
.args(["resume", "--endpoint"])
.arg(endpoint)
.arg(job_id)
.output()
.expect("cuttlefish resume failed to run")
}
fn run_cancel(endpoint: &std::path::Path, job_id: &str) -> std::process::Output {
std::process::Command::new(env!("CARGO_BIN_EXE_cuttlefish"))
.args(["cancel", "--endpoint"])
.arg(endpoint)
.arg(job_id)
.output()
.expect("cuttlefish cancel failed to run")
}
fn run_shutdown(endpoint: &std::path::Path) -> std::process::Output {
std::process::Command::new(env!("CARGO_BIN_EXE_cuttlefish"))
.args(["shutdown", "--endpoint"])
.arg(endpoint)
.output()
.expect("cuttlefish shutdown failed to run")
}
fn cancel_test_spec_src() -> &'static str {
r#"
spec cancel_test = {
description = "Use when testing cuttlefish cancel.";
model = Stub "";
data_policy = Local_only;
capabilities = [ Read "." ];
nodes = {
block = { block = "block.wasm"; repeat_until = "summary"; max_iterations = 1000000000; };
};
}
"#
}
#[tokio::test]
async fn submit_returns_a_job_id_immediately_without_waiting_for_completion() {
let _daemon_guard = support::daemon_test_guard().await;
support::ensure_test_cuttlefish_home();
let dir = tempfile::tempdir().unwrap();
let spec_path = dir.path().join("spec.cuttlefish");
std::fs::write(&spec_path, submit_test_spec_src()).unwrap();
std::fs::write(dir.path().join("block.wasm"), support::example_block()).unwrap();
let endpoint = support::unique_endpoint(dir.path());
let mut daemon = support::spawn_daemon(&spec_path, &endpoint).await;
let warmup_endpoint = endpoint.clone();
tokio::task::spawn_blocking(move || run_submit(&warmup_endpoint))
.await
.unwrap();
let endpoint_for_cmd = endpoint.clone();
let started = std::time::Instant::now();
let result = tokio::time::timeout(
std::time::Duration::from_secs(10),
tokio::task::spawn_blocking(move || run_submit(&endpoint_for_cmd)),
)
.await;
let elapsed = started.elapsed();
daemon.kill();
let output = result
.expect(
"`cuttlefish submit` did not return within 10s — it may have blocked waiting for \
the job to finish, like `run` does, instead of returning immediately",
)
.expect("the blocking task panicked");
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let job_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert!(
!job_id.is_empty(),
"expected a job_id on stdout, got nothing"
);
assert!(
uuid::Uuid::parse_str(&job_id).is_ok(),
"stdout wasn't a UUID: {job_id}"
);
assert!(
elapsed < std::time::Duration::from_millis(250),
"`cuttlefish submit` took {elapsed:?}, which is far more than a bare POST + print \
should ever take — it may have started waiting on the job's own completion again"
);
}
#[tokio::test]
async fn jobs_lists_a_submitted_job() {
let _daemon_guard = support::daemon_test_guard().await;
support::ensure_test_cuttlefish_home();
let dir = tempfile::tempdir().unwrap();
let spec_path = dir.path().join("spec.cuttlefish");
std::fs::write(&spec_path, submit_test_spec_src()).unwrap();
std::fs::write(dir.path().join("block.wasm"), support::example_block()).unwrap();
let endpoint = support::unique_endpoint(dir.path());
let mut daemon = support::spawn_daemon(&spec_path, &endpoint).await;
let submit_output = run_submit(&endpoint);
assert!(
submit_output.status.success(),
"{}",
String::from_utf8_lossy(&submit_output.stderr)
);
let job_id = String::from_utf8_lossy(&submit_output.stdout)
.trim()
.to_string();
let jobs_output = run_jobs(&endpoint);
daemon.kill();
assert!(
jobs_output.status.success(),
"{}",
String::from_utf8_lossy(&jobs_output.stderr)
);
let stdout = String::from_utf8_lossy(&jobs_output.stdout);
assert!(
stdout.contains(&job_id),
"expected job_id {job_id} to appear in `cuttlefish jobs` output:\n{stdout}"
);
}
#[tokio::test]
async fn resume_on_a_non_interrupted_job_reports_the_daemons_rejection() {
let _daemon_guard = support::daemon_test_guard().await;
support::ensure_test_cuttlefish_home();
let dir = tempfile::tempdir().unwrap();
let spec_path = dir.path().join("spec.cuttlefish");
std::fs::write(&spec_path, submit_test_spec_src()).unwrap();
std::fs::write(dir.path().join("block.wasm"), support::example_block()).unwrap();
let endpoint = support::unique_endpoint(dir.path());
let mut daemon = support::spawn_daemon(&spec_path, &endpoint).await;
let submit_output = run_submit(&endpoint);
assert!(
submit_output.status.success(),
"{}",
String::from_utf8_lossy(&submit_output.stderr)
);
let job_id = String::from_utf8_lossy(&submit_output.stdout)
.trim()
.to_string();
let resume_output = run_resume(&endpoint, &job_id);
daemon.kill();
assert!(
!resume_output.status.success(),
"expected `cuttlefish resume` to exit non-zero for a non-Interrupted job"
);
let stderr = String::from_utf8_lossy(&resume_output.stderr);
assert!(
stderr.contains("Interrupted"),
"expected the daemon's rejection message to surface on stderr, got:\n{stderr}"
);
}
#[tokio::test]
async fn cancel_stops_a_job() {
let _daemon_guard = support::daemon_test_guard().await;
support::ensure_test_cuttlefish_home();
let dir = tempfile::tempdir().unwrap();
let spec_path = dir.path().join("spec.cuttlefish");
std::fs::write(&spec_path, cancel_test_spec_src()).unwrap();
std::fs::write(dir.path().join("block.wasm"), support::example_block()).unwrap();
std::fs::write(dir.path().join("doc.txt"), "some document text").unwrap();
let endpoint = support::unique_endpoint(dir.path());
let mut daemon = support::spawn_daemon(&spec_path, &endpoint).await;
let input =
serde_json::json!({ "path": dir.path().join("doc.txt").to_str().unwrap() }).to_string();
let submit_output = run_submit_with(&endpoint, "cancel_test", &input);
assert!(
submit_output.status.success(),
"{}",
String::from_utf8_lossy(&submit_output.stderr)
);
let job_id = String::from_utf8_lossy(&submit_output.stdout)
.trim()
.to_string();
let cancel_output = run_cancel(&endpoint, &job_id);
assert!(
cancel_output.status.success(),
"{}",
String::from_utf8_lossy(&cancel_output.stderr)
);
let builder = reqwest::Client::builder();
#[cfg(unix)]
let builder = builder.unix_socket(endpoint.as_path());
#[cfg(windows)]
let builder = builder.windows_named_pipe(endpoint.as_path());
let client = builder.build().unwrap();
let mut status = None;
for _ in 0..500 {
let body: serde_json::Value = client
.get(format!("http://localhost/jobs/{job_id}"))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
let s = body["status"].as_str().unwrap_or("").to_string();
if matches!(s.as_str(), "cancelled" | "completed" | "failed") {
status = Some(s);
break;
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
daemon.kill();
assert_eq!(
status.as_deref(),
Some("cancelled"),
"job did not settle on `cancelled` within the bounded wait (got {status:?}) — \
`cuttlefish cancel` may not have actually reached the daemon"
);
}
#[tokio::test]
async fn shutdown_causes_the_daemon_process_to_exit() {
let _daemon_guard = support::daemon_test_guard().await;
support::ensure_test_cuttlefish_home();
let dir = tempfile::tempdir().unwrap();
let spec_path = dir.path().join("spec.cuttlefish");
std::fs::write(&spec_path, submit_test_spec_src()).unwrap();
std::fs::write(dir.path().join("block.wasm"), support::example_block()).unwrap();
let endpoint = support::unique_endpoint(dir.path());
let mut daemon = support::spawn_daemon(&spec_path, &endpoint).await;
let shutdown_output = run_shutdown(&endpoint);
assert!(
shutdown_output.status.success(),
"{}",
String::from_utf8_lossy(&shutdown_output.stderr)
);
let mut exited = false;
for _ in 0..500 {
if daemon
.try_wait()
.expect("polling the daemon process")
.is_some()
{
exited = true;
break;
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
assert!(
exited,
"cuttlefishd did not exit within the bounded wait after `cuttlefish shutdown`"
);
}