use serde_json::Value;
use start_command::execution_store::{
ExecutionRecord, ExecutionRecordOptions, ExecutionStore, ExecutionStoreOptions,
};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::process::Command;
use tempfile::TempDir;
const SEPARATOR: &str = "==================================================";
struct CliResult {
stdout: String,
stderr: String,
exit_code: i32,
}
fn create_test_store(app_folder: &Path) -> ExecutionStore {
ExecutionStore::with_options(ExecutionStoreOptions {
app_folder: Some(app_folder.to_path_buf()),
use_links: Some(false),
verbose: false,
})
}
fn run_cli(app_folder: &Path, args: &[&str]) -> CliResult {
let output = Command::new(env!("CARGO_BIN_EXE_start"))
.args(args)
.env("START_APP_FOLDER", app_folder)
.env("START_DISABLE_AUTO_ISSUE", "1")
.env("START_DISABLE_LOG_UPLOAD", "1")
.output()
.expect("failed to run the start binary");
CliResult {
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
exit_code: output.status.code().unwrap_or(-1),
}
}
fn save_record(
store: &ExecutionStore,
command: &str,
log_path: Option<&str>,
exit_code: Option<i32>,
options: &[(&str, Value)],
) -> ExecutionRecord {
let mut record = ExecutionRecord::with_options(ExecutionRecordOptions {
command: command.to_string(),
pid: Some(4242),
log_path: log_path.map(str::to_string),
working_directory: Some("/tmp".to_string()),
shell: Some("/bin/bash".to_string()),
options: Some(
options
.iter()
.map(|(key, value)| (key.to_string(), value.clone()))
.collect::<HashMap<_, _>>(),
),
..Default::default()
});
if let Some(code) = exit_code {
record.complete(code);
}
store.save(&record).unwrap();
record
}
fn detached_docker_options(session_name: &str) -> Vec<(&'static str, Value)> {
vec![
("isolated", Value::String("docker".to_string())),
("isolationMode", Value::String("detached".to_string())),
("sessionName", Value::String(session_name.to_string())),
]
}
#[test]
fn test_list_running_reports_only_executions_that_are_still_running() {
let temp = TempDir::new().unwrap();
let store = create_test_store(temp.path());
let finished = save_record(&store, "echo finished", None, Some(0), &[]);
let running = save_record(&store, "echo running", None, None, &[]);
let result = run_cli(
temp.path(),
&["--list", "--running", "--output-format", "json"],
);
assert_eq!(result.exit_code, 0, "stderr: {}", result.stderr);
let parsed: Value = serde_json::from_str(&result.stdout).unwrap();
let uuids: Vec<&str> = parsed["executions"]
.as_array()
.unwrap()
.iter()
.map(|record| record["uuid"].as_str().unwrap())
.collect();
assert!(uuids.contains(&running.uuid.as_str()));
assert!(!uuids.contains(&finished.uuid.as_str()));
}
#[test]
fn test_running_is_rejected_without_list() {
let temp = TempDir::new().unwrap();
let result = run_cli(temp.path(), &["--running", "--", "echo", "hi"]);
assert_ne!(result.exit_code, 0);
assert!(
result
.stderr
.contains("--running option is only valid with --list"),
"stderr: {}",
result.stderr
);
}
#[test]
fn test_attach_fails_with_a_clear_error_for_an_unknown_identifier() {
let temp = TempDir::new().unwrap();
create_test_store(temp.path());
let result = run_cli(temp.path(), &["--attach", "does-not-exist"]);
assert_eq!(result.exit_code, 1);
assert!(
result
.stderr
.contains("No execution found with UUID or session name: does-not-exist"),
"stderr: {}",
result.stderr
);
}
#[test]
fn test_attach_refuses_a_non_isolated_execution() {
let temp = TempDir::new().unwrap();
let store = create_test_store(temp.path());
let record = save_record(&store, "echo local", None, None, &[]);
let result = run_cli(temp.path(), &["--attach", &record.uuid]);
assert_eq!(result.exit_code, 1);
assert!(
result
.stderr
.contains("Execution record does not contain an isolation session name"),
"stderr: {}",
result.stderr
);
}
#[test]
fn test_attach_points_at_resume_when_the_session_is_already_gone() {
let temp = TempDir::new().unwrap();
let store = create_test_store(temp.path());
let record = save_record(
&store,
"echo gone",
None,
None,
&detached_docker_options("start-command-162-missing"),
);
let result = run_cli(temp.path(), &["--attach", &record.uuid]);
assert_eq!(result.exit_code, 1);
assert!(
result.stderr.contains("--resume"),
"stderr: {}",
result.stderr
);
}
#[test]
fn test_read_only_is_rejected_without_attach() {
let temp = TempDir::new().unwrap();
let result = run_cli(temp.path(), &["--read-only", "--", "echo", "hi"]);
assert_ne!(result.exit_code, 0);
assert!(
result
.stderr
.contains("--read-only option is only valid with --attach"),
"stderr: {}",
result.stderr
);
}
#[test]
fn test_resume_fails_with_a_clear_error_for_an_unknown_identifier() {
let temp = TempDir::new().unwrap();
create_test_store(temp.path());
let result = run_cli(temp.path(), &["--resume", "does-not-exist"]);
assert_eq!(result.exit_code, 1);
assert!(
result
.stderr
.contains("No execution found with UUID or session name: does-not-exist"),
"stderr: {}",
result.stderr
);
}
#[test]
fn test_resume_refuses_a_non_isolated_execution() {
let temp = TempDir::new().unwrap();
let store = create_test_store(temp.path());
let record = save_record(&store, "echo local", None, None, &[]);
let result = run_cli(temp.path(), &["--resume", &record.uuid]);
assert_eq!(result.exit_code, 1);
assert!(
result
.stderr
.contains("Execution record does not contain an isolation session name"),
"stderr: {}",
result.stderr
);
}
#[test]
fn test_resume_accepts_a_replacement_command_after_the_separator() {
let temp = TempDir::new().unwrap();
create_test_store(temp.path());
let result = run_cli(
temp.path(),
&["--resume", "does-not-exist", "--", "echo", "hi"],
);
assert_eq!(result.exit_code, 1);
assert!(
result
.stderr
.contains("No execution found with UUID or session name: does-not-exist"),
"stderr: {}",
result.stderr
);
}
#[test]
fn test_resume_all_succeeds_with_an_empty_report_when_nothing_is_running() {
let temp = TempDir::new().unwrap();
let store = create_test_store(temp.path());
save_record(&store, "echo finished", None, Some(0), &[]);
let result = run_cli(temp.path(), &["--resume-all", "--output-format", "json"]);
assert_eq!(result.exit_code, 0, "stderr: {}", result.stderr);
let parsed: Value = serde_json::from_str(&result.stdout).unwrap();
assert_eq!(parsed["count"], 0);
assert_eq!(parsed["executions"].as_array().unwrap().len(), 0);
}
#[test]
fn test_resume_all_reconciles_executions_whose_session_no_longer_exists() {
let temp = TempDir::new().unwrap();
let store = create_test_store(temp.path());
let log_path = temp.path().join("orphan.log");
fs::write(
&log_path,
format!("working...\n{SEPARATOR}\nFinished: 2026-09-03 10:00:00.000\nExit Code: 0\n"),
)
.unwrap();
let record = save_record(
&store,
"echo orphan",
Some(log_path.to_str().unwrap()),
None,
&detached_docker_options("start-command-162-orphan"),
);
let result = run_cli(temp.path(), &["--resume-all", "--output-format", "json"]);
assert_eq!(result.exit_code, 0, "stderr: {}", result.stderr);
let parsed: Value = serde_json::from_str(&result.stdout).unwrap();
assert_eq!(parsed["count"], 1);
assert_eq!(parsed["executions"][0]["uuid"], record.uuid);
assert_eq!(parsed["executions"][0]["action"], "reconciled");
let reloaded = create_test_store(temp.path()).get(&record.uuid).unwrap();
assert_eq!(reloaded.status.as_str(), "executed");
}
#[test]
fn test_status_explains_a_139_exit_caused_by_heap_exhaustion() {
let temp = TempDir::new().unwrap();
let store = create_test_store(temp.path());
let log_path = temp.path().join("oom.log");
fs::write(
&log_path,
format!(
"building...\n\n<--- Last few GCs --->\nFATAL ERROR: Reached heap limit \
Allocation failed - JavaScript heap out of memory\n\n{SEPARATOR}\n\
Finished: 2026-09-03 10:00:00.000\nExit Code: 139\n"
),
)
.unwrap();
let record = save_record(
&store,
"bun run build",
Some(log_path.to_str().unwrap()),
Some(139),
&[],
);
let result = run_cli(
temp.path(),
&["--status", &record.uuid, "--output-format", "json"],
);
assert_eq!(result.exit_code, 0, "stderr: {}", result.stderr);
let parsed: Value = serde_json::from_str(&result.stdout).unwrap();
assert_eq!(parsed["exitCode"], 139);
assert!(parsed.get("oomKilled").is_none() || parsed["oomKilled"].is_null());
assert_eq!(parsed["exitReason"], "memory-exhaustion (v8-heap-limit)");
}
#[test]
fn test_status_does_not_invent_a_reason_for_a_clean_exit() {
let temp = TempDir::new().unwrap();
let store = create_test_store(temp.path());
let log_path = temp.path().join("clean.log");
fs::write(&log_path, "all good\n").unwrap();
let record = save_record(
&store,
"echo ok",
Some(log_path.to_str().unwrap()),
Some(0),
&[],
);
let result = run_cli(
temp.path(),
&["--status", &record.uuid, "--output-format", "json"],
);
assert_eq!(result.exit_code, 0, "stderr: {}", result.stderr);
let parsed: Value = serde_json::from_str(&result.stdout).unwrap();
assert!(parsed.get("exitReason").is_none() || parsed["exitReason"].is_null());
}
#[test]
fn test_status_keeps_addressing_one_logical_session_by_its_previous_name() {
let temp = TempDir::new().unwrap();
let store = create_test_store(temp.path());
let mut options = detached_docker_options("start-command-162-b");
options.push((
"sessionNameHistory",
Value::Array(vec![Value::String("start-command-162-a".to_string())]),
));
let record = save_record(&store, "echo resumed", None, None, &options);
let result = run_cli(
temp.path(),
&["--status", "start-command-162-a", "--output-format", "json"],
);
assert_eq!(result.exit_code, 0, "stderr: {}", result.stderr);
let parsed: Value = serde_json::from_str(&result.stdout).unwrap();
assert_eq!(parsed["uuid"], record.uuid);
}