use std::process::Command;
use std::time::Duration;
use tempfile::TempDir;
#[path = "common.rs"]
mod common;
use common::{
ensure_test_binaries_built, run_torc_standalone, run_torc_standalone_ok, torc_binary_path,
torc_server_binary_path,
};
#[test]
fn standalone_exec_creates_and_runs_workflow() {
ensure_test_binaries_built();
let work = TempDir::new().expect("tempdir");
let db = work.path().join("torc_output").join("torc.db");
let out = run_torc_standalone_ok(work.path(), &db, &["exec", "-c", "echo hello-standalone"]);
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("Started standalone torc-server"),
"stderr should log server startup; got:\n{}",
stderr
);
assert!(
stdout.contains("Created workflow"),
"stdout should announce workflow creation; got:\n{}",
stdout
);
assert!(db.exists(), "database at {:?} was not created", db);
}
#[test]
fn standalone_persists_workflow_across_invocations() {
ensure_test_binaries_built();
let work = TempDir::new().expect("tempdir");
let db = work.path().join("torc.db");
let first = run_torc_standalone_ok(work.path(), &db, &["exec", "-c", "echo persist-me"]);
assert!(
String::from_utf8_lossy(&first.stdout).contains("Created workflow"),
"first invocation should create a workflow"
);
let second = run_torc_standalone_ok(work.path(), &db, &["-f", "json", "workflows", "list"]);
let stdout = String::from_utf8_lossy(&second.stdout).to_string();
let parsed: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("workflows list JSON parse failed: {}\n---\n{}", e, stdout));
let items = parsed
.get("workflows")
.and_then(|v| v.as_array())
.unwrap_or_else(|| panic!("expected workflows[] in list response: {}", stdout));
assert!(
!items.is_empty(),
"expected ≥1 workflow in standalone DB after exec run; got {}",
stdout
);
}
#[test]
fn standalone_invalid_server_bin_fails_cleanly() {
ensure_test_binaries_built();
let work = TempDir::new().expect("tempdir");
let db = work.path().join("torc.db");
let out = Command::new(torc_binary_path())
.current_dir(work.path())
.args([
"-s",
"--torc-server-bin",
"/nonexistent/torc-server-bogus",
"--db",
db.to_str().unwrap(),
"exec",
"-c",
"echo no-server",
])
.env_remove("TORC_API_URL")
.env("RUST_LOG", "warn")
.output()
.expect("failed to spawn torc");
assert!(
!out.status.success(),
"bogus --torc-server-bin should fail; stdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("Error starting standalone torc-server")
|| stderr.contains("failed to spawn"),
"stderr should explain the failure; got:\n{}",
stderr,
);
}
#[test]
fn standalone_creates_missing_db_parent_directory() {
ensure_test_binaries_built();
let work = TempDir::new().expect("tempdir");
let db = work.path().join("nested").join("subdir").join("torc.db");
assert!(!db.parent().unwrap().exists());
run_torc_standalone_ok(work.path(), &db, &["exec", "-c", "echo nested-ok"]);
assert!(
db.parent().unwrap().exists(),
"standalone should have created parent dir for --db path"
);
assert!(db.exists(), "db file should exist at {:?}", db);
}
#[test]
fn standalone_default_db_is_torc_output_torc_db() {
ensure_test_binaries_built();
let work = TempDir::new().expect("tempdir");
let server_bin = torc_server_binary_path();
let out = Command::new(torc_binary_path())
.current_dir(work.path())
.args([
"-s",
"--torc-server-bin",
server_bin.to_str().unwrap(),
"exec",
"-c",
"echo default-db",
])
.env_remove("TORC_API_URL")
.env("RUST_LOG", "warn")
.output()
.expect("failed to spawn torc");
assert!(
out.status.success(),
"default-db exec should succeed. stderr:\n{}",
String::from_utf8_lossy(&out.stderr)
);
let default_db = work.path().join("torc_output").join("torc.db");
assert!(
default_db.exists(),
"expected default DB at {:?} after `torc -s exec`",
default_db
);
}
#[test]
fn standalone_no_op_for_local_command_prints_notice() {
ensure_test_binaries_built();
let work = TempDir::new().expect("tempdir");
let fake_metrics_db = work.path().join("no-such.db");
let out = Command::new(torc_binary_path())
.current_dir(work.path())
.args([
"-s",
"--torc-server-bin",
"/definitely/not/a/real/path",
"plot-resources",
fake_metrics_db.to_str().unwrap(),
])
.env_remove("TORC_API_URL")
.env("RUST_LOG", "warn")
.output()
.expect("failed to spawn torc");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("--standalone has no effect"),
"expected '--standalone has no effect' notice; stderr:\n{}",
stderr
);
assert!(
!stderr.contains("Error starting standalone torc-server"),
"should not have attempted to launch the bogus server; stderr:\n{}",
stderr
);
}
#[cfg(unix)]
#[test]
fn standalone_server_shuts_down_after_command_exits() {
ensure_test_binaries_built();
let work = TempDir::new().expect("tempdir");
let db = work.path().join("torc.db");
run_torc_standalone_ok(work.path(), &db, &["exec", "-c", "echo shutdown-test"]);
std::thread::sleep(Duration::from_millis(500));
let ps = Command::new("ps")
.args(["-Ao", "args="])
.output()
.expect("ps failed");
let listing = String::from_utf8_lossy(&ps.stdout);
let db_str = db.to_string_lossy();
let lingering: Vec<&str> = listing
.lines()
.filter(|l| l.contains(&*db_str) && l.contains("torc-server"))
.collect();
assert!(
lingering.is_empty(),
"expected no torc-server subprocess after `torc -s exec` exits; found: {:#?}",
lingering
);
}
#[cfg(unix)]
#[test]
fn standalone_server_shuts_down_when_client_exits_via_process_exit() {
ensure_test_binaries_built();
let work = TempDir::new().expect("tempdir");
let db = work.path().join("process_exit.db");
let out = run_torc_standalone(work.path(), &db, &["exec"]);
assert!(
!out.status.success(),
"`torc -s exec` with no commands should fail. stderr:\n{}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
String::from_utf8_lossy(&out.stderr).contains("Started standalone torc-server"),
"the server must have been started before the failure for this test to be meaningful"
);
std::thread::sleep(Duration::from_secs(2));
let ps = Command::new("ps")
.args(["-Ao", "args="])
.output()
.expect("ps failed");
let listing = String::from_utf8_lossy(&ps.stdout);
let db_str = db.to_string_lossy();
let lingering: Vec<&str> = listing
.lines()
.filter(|l| l.contains(&*db_str) && l.contains("torc-server"))
.collect();
assert!(
lingering.is_empty(),
"torc-server subprocess leaked after client exited via process::exit; found: {:#?}",
lingering
);
}
#[test]
fn non_standalone_does_not_start_server() {
ensure_test_binaries_built();
let work = TempDir::new().expect("tempdir");
let out = Command::new(torc_binary_path())
.current_dir(work.path())
.args([
"--url",
"http://127.0.0.1:1/torc-service/v1",
"workflows",
"list",
])
.env_remove("TORC_API_URL")
.env("RUST_LOG", "warn")
.output()
.expect("failed to spawn torc");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("Started standalone torc-server"),
"non-standalone invocation must not start a server; stderr:\n{}",
stderr
);
}
#[cfg(unix)]
fn run_torc_in_memory(
work_dir: &std::path::Path,
db_path: &std::path::Path,
extra_args: &[&str],
args: &[&str],
) -> std::process::Output {
let server_bin = torc_server_binary_path();
assert!(
server_bin.exists(),
"torc-server binary missing at {:?}",
server_bin
);
let target_debug = std::env::current_dir().expect("cwd").join("target/debug");
let existing = std::env::var_os("PATH").unwrap_or_default();
let mut entries: Vec<std::path::PathBuf> = vec![target_debug];
entries.extend(std::env::split_paths(&existing));
let path_var = std::env::join_paths(entries).expect("join PATH entries");
let mut cmd = Command::new(torc_binary_path());
cmd.current_dir(work_dir)
.arg("-s")
.arg("--in-memory")
.args(["--torc-server-bin", server_bin.to_str().unwrap()])
.args(["--db", db_path.to_str().unwrap()])
.args(extra_args)
.args(args)
.env_remove("TORC_API_URL")
.env("RUST_LOG", "warn")
.env("PATH", path_var);
cmd.output().expect("failed to spawn torc")
}
#[cfg(unix)]
#[test]
fn standalone_in_memory_snapshot_is_queryable() {
ensure_test_binaries_built();
let work = TempDir::new().expect("tempdir");
let db = work.path().join("snap.db");
let out = run_torc_in_memory(work.path(), &db, &[], &["exec", "-c", "echo in-mem-ok"]);
assert!(
out.status.success(),
"torc -s --in-memory exec failed (status {:?}):\n--- stdout ---\n{}\n--- stderr ---\n{}",
out.status.code(),
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
assert!(
db.exists(),
"snapshot DB at {:?} should exist after --in-memory exec returns",
db
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("timed out waiting for final snapshot"),
"final snapshot timed out unexpectedly; stderr:\n{}",
stderr
);
let listed = run_torc_standalone_ok(work.path(), &db, &["-f", "json", "workflows", "list"]);
let stdout = String::from_utf8_lossy(&listed.stdout).to_string();
let parsed: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("workflows list JSON parse failed: {}\n---\n{}", e, stdout));
let items = parsed
.get("workflows")
.and_then(|v| v.as_array())
.unwrap_or_else(|| panic!("expected workflows[] in list response: {}", stdout));
assert!(
!items.is_empty(),
"expected ≥1 workflow in --in-memory snapshot DB; got {}",
stdout
);
}
#[cfg(unix)]
#[test]
fn standalone_in_memory_periodic_snapshot_lands_before_exit() {
ensure_test_binaries_built();
let work = TempDir::new().expect("tempdir");
let db = work.path().join("periodic-snap.db");
let out = run_torc_in_memory(
work.path(),
&db,
&["--snapshot-interval-seconds", "1"],
&["exec", "-c", "sleep 1.5 && echo periodic-ok"],
);
assert!(
out.status.success(),
"torc -s --in-memory --snapshot-interval-seconds 1 exec failed:\n--- stderr ---\n{}",
String::from_utf8_lossy(&out.stderr),
);
assert!(db.exists(), "snapshot DB at {:?} should exist", db);
let listed = run_torc_standalone_ok(work.path(), &db, &["-f", "json", "workflows", "list"]);
let stdout = String::from_utf8_lossy(&listed.stdout);
assert!(
stdout.contains("\"workflows\""),
"expected workflows list response; got {}",
stdout
);
}
#[cfg(unix)]
#[test]
fn standalone_in_memory_rejected_for_read_only_command() {
ensure_test_binaries_built();
let work = TempDir::new().expect("tempdir");
let db = work.path().join("readonly.db");
let out = run_torc_in_memory(work.path(), &db, &[], &["workflows", "list"]);
assert!(
!out.status.success(),
"--in-memory + workflows list should fail; stdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("--in-memory is only supported with"),
"stderr should explain the restriction; got:\n{}",
stderr
);
assert!(
!db.exists(),
"rejected --in-memory must not create the snapshot file; got {:?}",
db
);
}