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
);
}