use std::path::PathBuf;
use std::process::Command;
use assert_cmd::Command as AssertCmd;
use predicates::str::contains as pred_contains;
fn roboticus_bin() -> PathBuf {
std::env::var("CARGO_BIN_EXE_roboticus")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let mut exe = std::env::current_exe().expect("current exe");
exe.pop(); exe.pop(); exe.push("roboticus");
exe
})
}
fn roboticus_cmd() -> Command {
Command::new(roboticus_bin())
}
#[test]
fn version_shows_semver() {
let output = roboticus_cmd()
.arg("version")
.output()
.expect("failed to run roboticus-server version");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let out = format!("{stdout}{stderr}");
assert!(
out.contains("roboticus") || out.contains("0."),
"output: {out}"
);
}
#[test]
fn init_creates_config_file() {
let dir = tempfile::tempdir().unwrap();
let output = roboticus_cmd()
.arg("init")
.current_dir(dir.path())
.output()
.expect("failed to run init");
assert!(output.status.success() || String::from_utf8_lossy(&output.stderr).contains("already"));
assert!(dir.path().join("roboticus.toml").exists() || output.status.success());
}
#[test]
fn check_without_config_returns_error() {
let dir = tempfile::tempdir().unwrap();
let fake_home = dir.path().join("home");
std::fs::create_dir_all(&fake_home).unwrap();
let output = roboticus_cmd()
.arg("check")
.env("HOME", &fake_home)
.current_dir(dir.path())
.output()
.expect("failed to run check");
assert!(
!output.status.success(),
"check without config should exit non-zero"
);
}
#[test]
fn cli_help_shows_subcommands() {
AssertCmd::new(roboticus_bin())
.arg("--help")
.assert()
.success()
.stdout(pred_contains("init"))
.stdout(pred_contains("serve"))
.stdout(pred_contains("status"));
}
#[test]
fn cli_init_creates_config() {
let dir = tempfile::TempDir::new().unwrap();
let config_path = dir.path().join("roboticus.toml");
AssertCmd::new(roboticus_bin())
.current_dir(dir.path())
.arg("init")
.assert()
.success();
assert!(
config_path.exists(),
"roboticus.toml should exist at {:?}",
config_path
);
let raw = std::fs::read_to_string(&config_path).expect("read roboticus.toml");
assert!(
raw.contains("api_key = \"rk_"),
"init should generate [server] api_key by default"
);
}
#[test]
fn cli_check_validates_config() {
let dir = tempfile::TempDir::new().unwrap();
let config_path = dir.path().join("roboticus.toml");
AssertCmd::new(roboticus_bin())
.current_dir(dir.path())
.args(["init", "."])
.assert()
.success();
assert!(config_path.exists(), "init must create roboticus.toml");
AssertCmd::new(roboticus_bin())
.args(["check", "--config", config_path.to_str().unwrap()])
.assert()
.success();
}
#[test]
fn cli_status_handles_no_server() {
let out = AssertCmd::new(roboticus_bin())
.args(["status", "--url", "http://127.0.0.1:19999"])
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("not running")
|| stderr.contains("Start with")
|| out.status.code() != Some(0),
"status when server is down should warn or fail: {}",
stderr
);
}
#[test]
fn cli_config_show_handles_no_server() {
AssertCmd::new(roboticus_bin())
.args(["config", "show", "--url", "http://127.0.0.1:19999"])
.assert()
.failure();
}
#[test]
fn cli_wallet_handles_no_server() {
AssertCmd::new(roboticus_bin())
.args(["wallet", "show", "--url", "http://127.0.0.1:19999"])
.assert()
.failure();
}
#[test]
fn cli_sessions_handles_no_server() {
AssertCmd::new(roboticus_bin())
.args(["sessions", "list", "--url", "http://127.0.0.1:19999"])
.assert()
.failure();
}
#[test]
fn cli_metrics_handles_no_server() {
AssertCmd::new(roboticus_bin())
.args(["metrics", "--url", "http://127.0.0.1:19999"])
.assert()
.failure();
}
#[test]
fn cli_version_shows_version() {
AssertCmd::new(roboticus_bin())
.arg("version")
.assert()
.success()
.stderr(predicates::str::contains(env!("CARGO_PKG_VERSION")));
}
#[test]
fn cli_version_json_outputs_structured_fields() {
AssertCmd::new(roboticus_bin())
.args(["--json", "version"])
.assert()
.success()
.stdout(pred_contains("\"version\""))
.stdout(pred_contains("\"edition\""))
.stdout(pred_contains("\"target\""))
.stdout(pred_contains("\"os\""));
}
#[test]
fn cli_completion_variants_work() {
AssertCmd::new(roboticus_bin())
.args(["completion", "bash"])
.assert()
.success()
.stdout(pred_contains("completion"));
AssertCmd::new(roboticus_bin())
.args(["completion", "zsh"])
.assert()
.success()
.stdout(pred_contains("compctl"));
AssertCmd::new(roboticus_bin())
.args(["completion", "fish"])
.assert()
.success()
.stdout(pred_contains("complete -c roboticus"));
}
#[test]
fn cli_check_invalid_config_fails() {
let dir = tempfile::TempDir::new().unwrap();
let config_path = dir.path().join("roboticus.toml");
std::fs::write(&config_path, "not valid toml = [").unwrap();
AssertCmd::new(roboticus_bin())
.args(["check", "--config", config_path.to_str().unwrap()])
.assert()
.failure();
}
#[test]
fn cli_subcommand_help_paths_render() {
for args in [
vec!["sessions", "--help"],
vec!["memory", "--help"],
vec!["skills", "--help"],
vec!["schedule", "--help"],
vec!["metrics", "--help"],
vec!["wallet", "--help"],
vec!["config", "--help"],
vec!["models", "--help"],
vec!["plugins", "--help"],
vec!["agents", "--help"],
vec!["channels", "--help"],
vec!["security", "--help"],
vec!["auth", "--help"],
vec!["keystore", "--help"],
vec!["migrate", "--help"],
vec!["daemon", "--help"],
] {
AssertCmd::new(roboticus_bin())
.args(args)
.assert()
.success();
}
}
#[test]
fn cli_more_no_server_commands_fail_or_warn_cleanly() {
let no_server = "http://127.0.0.1:19999";
for args in [
vec!["agents", "list", "--url", no_server],
vec!["channels", "list", "--url", no_server],
vec!["channels", "dead-letter", "--url", no_server],
vec!["models", "list", "--url", no_server],
vec!["models", "scan", "--url", no_server],
vec!["plugins", "list", "--url", no_server],
vec!["circuit", "status", "--url", no_server],
vec!["circuit", "reset", "--url", no_server],
] {
let out = AssertCmd::new(roboticus_bin()).args(args).output().unwrap();
let stderr = String::from_utf8_lossy(&out.stderr).to_ascii_lowercase();
let stdout = String::from_utf8_lossy(&out.stdout).to_ascii_lowercase();
assert!(
!out.status.success()
|| stderr.contains("not running")
|| stderr.contains("not reachable")
|| stderr.contains("cannot reach")
|| stderr.contains("could not connect")
|| stdout.contains("not running")
|| stdout.contains("not reachable")
|| stdout.contains("cannot reach"),
"unexpected success output: stdout={stdout} stderr={stderr}"
);
}
}
#[test]
fn cli_check_json_reports_valid_payload() {
let dir = tempfile::TempDir::new().unwrap();
let config_path = dir.path().join("roboticus.toml");
AssertCmd::new(roboticus_bin())
.current_dir(dir.path())
.args(["init", "."])
.assert()
.success();
AssertCmd::new(roboticus_bin())
.args(["--json", "check", "--config", config_path.to_str().unwrap()])
.assert()
.success()
.stdout(pred_contains("\"valid\": true"))
.stdout(pred_contains("\"agent_name\""))
.stdout(pred_contains("\"memory_budget_sum_pct\""))
.stdout(pred_contains("\"warnings\""));
}
#[test]
fn cli_check_json_reports_invalid_payload_for_missing_config() {
let dir = tempfile::TempDir::new().unwrap();
let missing = dir.path().join("missing.toml");
AssertCmd::new(roboticus_bin())
.args(["--json", "check", "--config", missing.to_str().unwrap()])
.assert()
.failure()
.stdout(pred_contains("\"valid\":false"))
.stdout(pred_contains("\"config_path\""))
.stdout(pred_contains("\"error\""));
}
#[test]
fn cli_mcp_add_stdio_prints_toml_snippet() {
AssertCmd::new(roboticus_bin())
.args(["mcp", "add", "local-stdio", "--stdio", "echo"])
.assert()
.success()
.stdout(pred_contains("[[mcp.servers]]"))
.stdout(pred_contains("name = \"local-stdio\""))
.stdout(pred_contains("type = \"stdio\""))
.stdout(pred_contains("command = \"echo\""));
}
#[test]
fn cli_mcp_add_sse_prints_toml_snippet() {
AssertCmd::new(roboticus_bin())
.args([
"mcp",
"add",
"remote-sse",
"--sse",
"http://127.0.0.1:7001/mcp",
])
.assert()
.success()
.stdout(pred_contains("[[mcp.servers]]"))
.stdout(pred_contains("name = \"remote-sse\""))
.stdout(pred_contains("type = \"sse\""))
.stdout(pred_contains("url = \"http://127.0.0.1:7001/mcp\""));
}
#[test]
fn cli_mcp_remove_prints_manual_removal_guidance() {
let out = AssertCmd::new(roboticus_bin())
.args(["mcp", "remove", "legacy-server"])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
let combined = format!("{stdout}\n{stderr}");
assert!(combined.contains("legacy-server"));
assert!(combined.contains("[[mcp.servers]]"));
assert!(combined.contains("roboticus daemon restart"));
}
#[test]
fn cli_mcp_test_handles_unreachable_server_cleanly() {
let out = AssertCmd::new(roboticus_bin())
.args(["mcp", "test", "missing", "--url", "http://127.0.0.1:19999"])
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr).to_ascii_lowercase();
assert!(
stderr.contains("connection test: failed")
|| stderr.contains("is the roboticus server running")
|| stderr.contains("connect")
);
}
#[test]
fn cli_security_audit_runs_on_local_config() {
let dir = tempfile::TempDir::new().unwrap();
let home = dir.path().join("home");
std::fs::create_dir_all(home.join(".roboticus")).unwrap();
let config_path = dir.path().join("roboticus.toml");
std::fs::write(
&config_path,
r#"[agent]
name = "Test"
id = "test"
[server]
bind = "localhost"
port = 18789
[database]
path = ":memory:"
[models]
primary = "ollama/qwen3:8b"
"#,
)
.unwrap();
AssertCmd::new(roboticus_bin())
.env("HOME", home)
.args([
"security",
"audit",
"--config",
config_path.to_str().unwrap(),
])
.assert()
.success();
}
#[test]
fn cli_keystore_lifecycle_round_trips_non_interactively() {
let dir = tempfile::TempDir::new().unwrap();
let home = dir.path().join("home");
std::fs::create_dir_all(home.join(".roboticus")).unwrap();
let password = "test-passphrase";
AssertCmd::new(roboticus_bin())
.env("HOME", &home)
.args([
"keystore",
"set",
"demo_key",
"demo_value",
"--password",
password,
])
.assert()
.success()
.stderr(pred_contains("Stored secret 'demo_key'"));
AssertCmd::new(roboticus_bin())
.env("HOME", &home)
.args(["keystore", "get", "demo_key", "--password", password])
.assert()
.success()
.stdout(pred_contains("demo_value"));
AssertCmd::new(roboticus_bin())
.env("HOME", &home)
.args(["keystore", "list", "--password", password])
.assert()
.success()
.stderr(pred_contains("demo_key"))
.stderr(pred_contains("1 secret(s)"));
AssertCmd::new(roboticus_bin())
.env("HOME", &home)
.args(["keystore", "remove", "demo_key", "--password", password])
.assert()
.success()
.stderr(pred_contains("Removed 'demo_key'"));
AssertCmd::new(roboticus_bin())
.env("HOME", &home)
.args(["keystore", "list", "--password", password])
.assert()
.success()
.stderr(pred_contains("Keystore is empty"));
}
#[test]
fn cli_keystore_import_loads_json_entries() {
let dir = tempfile::TempDir::new().unwrap();
let home = dir.path().join("home");
std::fs::create_dir_all(home.join(".roboticus")).unwrap();
let password = "test-passphrase";
let import_path = dir.path().join("secrets.json");
std::fs::write(&import_path, r#"{"alpha":"one","beta":"two"}"#).unwrap();
AssertCmd::new(roboticus_bin())
.env("HOME", &home)
.args([
"keystore",
"import",
import_path.to_str().unwrap(),
"--password",
password,
])
.assert()
.success()
.stderr(pred_contains("Imported 2 secret(s)"));
AssertCmd::new(roboticus_bin())
.env("HOME", &home)
.args(["keystore", "list", "--password", password])
.assert()
.success()
.stderr(pred_contains("alpha"))
.stderr(pred_contains("beta"));
}