use std::process::Command;
fn koda_bin() -> String {
let mut path = std::env::current_exe().unwrap();
path.pop(); path.pop(); path.push("koda");
path.to_string_lossy().to_string()
}
#[test]
fn test_cli_version() {
let output = Command::new(koda_bin())
.arg("--version")
.output()
.expect("Failed to run koda --version");
assert!(output.status.success(), "koda --version should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("koda"),
"Version output should contain 'koda': {stdout}"
);
let version = env!("CARGO_PKG_VERSION");
assert!(
stdout.contains(version),
"Version output should contain '{version}': {stdout}"
);
assert!(
stdout.contains("sandbox:"),
"--version must include sandbox state for bug-report aid; got: {stdout}"
);
assert!(
stdout.contains("available") || stdout.contains("UNAVAILABLE"),
"--version sandbox state must say available or UNAVAILABLE; got: {stdout}"
);
assert!(
stdout.contains('[') && stdout.contains(']'),
"--version must include backend identifier in brackets; got: {stdout}"
);
}
#[test]
fn test_cli_help() {
let output = Command::new(koda_bin())
.arg("--help")
.output()
.expect("Failed to run koda --help");
assert!(output.status.success(), "koda --help should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("--agent"), "Help should mention --agent");
assert!(stdout.contains("--resume"), "Help should mention --resume");
assert!(
stdout.contains("--provider"),
"Help should mention --provider"
);
assert!(stdout.contains("--model"), "Help should mention --model");
}
#[test]
fn test_cli_invalid_flag() {
let output = Command::new(koda_bin())
.arg("--nonexistent-flag")
.output()
.expect("Failed to run koda with invalid flag");
assert!(
!output.status.success(),
"Invalid flag should exit with error"
);
}
#[test]
fn test_cli_help_mentions_headless() {
let output = Command::new(koda_bin())
.arg("--help")
.output()
.expect("Failed to run koda --help");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("--prompt") || stdout.contains("-p"),
"Help should mention -p/--prompt for headless mode: {stdout}"
);
assert!(
stdout.contains("--output-format"),
"Help should mention --output-format: {stdout}"
);
}
#[test]
fn test_cli_headless_piped_stdin_empty() {
let output = Command::new(koda_bin())
.arg("--help") .stdin(std::process::Stdio::null())
.output()
.expect("Failed to run koda with null stdin");
assert!(output.status.success());
}
#[test]
fn test_cli_help_snapshot() {
let output = Command::new(koda_bin())
.arg("--help")
.output()
.expect("Failed to run koda --help");
assert!(output.status.success());
let actual = String::from_utf8_lossy(&output.stdout).replace("\r\n", "\n");
let snapshot = include_str!("snapshots/help.snap").replace("\r\n", "\n");
assert_eq!(
actual.trim(),
snapshot.trim(),
concat!(
"\n\nkoda --help output changed — update snapshot + docs:\n",
" cargo run -p koda-cli -- --help ",
"> koda-cli/tests/snapshots/help.snap\n",
" # then edit docs/src/cli-reference.md\n"
)
);
}
#[test]
fn the_removed_server_command_says_so() {
for args in [
&["server"][..],
&["server", "--stdio"],
&["server", "--port", "9999"],
] {
let output = Command::new(koda_bin())
.args(args)
.output()
.expect("Failed to run koda");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!output.status.success(), "{args:?}");
assert!(stderr.contains("has been removed"), "{args:?}: {stderr}");
assert!(stderr.contains("acp.html"), "{args:?}: {stderr}");
}
}
#[test]
fn test_cli_output_format_validates() {
let output = Command::new(koda_bin())
.args(["--output-format", "invalid_format", "-p", "test"])
.output()
.expect("Failed to run koda");
assert!(
!output.status.success(),
"Invalid output-format should fail"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("invalid") || stderr.contains("possible values"),
"Should mention invalid value: {stderr}"
);
}
#[test]
fn test_cli_max_context_tokens_rejects_zero_and_non_numbers() {
for bad in ["0", "abc", "64k"] {
let flag = Command::new(koda_bin())
.args(["--max-context-tokens", bad, "-p", "test"])
.output()
.expect("Failed to run koda");
assert!(!flag.status.success(), "--max-context-tokens {bad}");
let stderr = String::from_utf8_lossy(&flag.stderr);
assert!(
stderr.contains("--max-context-tokens") && stderr.contains("context window"),
"flag {bad}: {stderr}"
);
let env = Command::new(koda_bin())
.env("KODA_MAX_CONTEXT_TOKENS", bad)
.args(["-p", "test"])
.output()
.expect("Failed to run koda");
assert!(!env.status.success(), "KODA_MAX_CONTEXT_TOKENS={bad}");
let stderr = String::from_utf8_lossy(&env.stderr);
assert!(
stderr.contains("--max-context-tokens") && stderr.contains("context window"),
"env {bad}: {stderr}"
);
}
}
#[test]
fn test_cli_refuses_effort_a_claude_model_does_not_take() {
let tmp = tempfile::tempdir().unwrap();
let project = tmp.path().join("project");
std::fs::create_dir_all(&project).unwrap();
let run = |extra: &[&str]| {
Command::new(koda_bin())
.args([
"-p",
"test",
"--provider",
"anthropic",
"--model",
"claude-haiku-5-5",
])
.args(["--base-url", "http://127.0.0.1:9"])
.args(extra)
.arg("--project-root")
.arg(&project)
.env("HOME", tmp.path())
.env("XDG_CONFIG_HOME", tmp.path())
.env_remove("ANTHROPIC_API_KEY")
.output()
.expect("Failed to run koda")
};
let effort = run(&["--effort", "ultra"]);
assert!(!effort.status.success());
let stderr = String::from_utf8_lossy(&effort.stderr);
assert!(
stderr.contains("claude-haiku-5-5 doesn't take effort \"ultra\"")
&& stderr.contains("low, medium, high, xhigh, max"),
"{stderr}"
);
let from_env = Command::new(koda_bin())
.args([
"-p",
"test",
"--provider",
"anthropic",
"--model",
"claude-haiku-5-5",
])
.args(["--base-url", "http://127.0.0.1:9"])
.arg("--project-root")
.arg(&project)
.env("HOME", tmp.path())
.env("XDG_CONFIG_HOME", tmp.path())
.env("KODA_REASONING_EFFORT", "ultra")
.env_remove("ANTHROPIC_API_KEY")
.output()
.expect("Failed to run koda");
assert!(!from_env.status.success());
let stderr = String::from_utf8_lossy(&from_env.stderr);
assert!(stderr.contains("doesn't take effort \"ultra\""), "{stderr}");
let budget = run(&["--thinking-budget", "8000"]);
assert!(!budget.status.success());
let stderr = String::from_utf8_lossy(&budget.stderr);
assert!(stderr.contains("takes no --thinking-budget"), "{stderr}");
}
#[test]
fn resume_with_an_unknown_id_stops_before_running() {
let home = tempfile::tempdir().unwrap();
let output = Command::new(koda_bin())
.args(["--resume", "28545", "-p", "hi"])
.args([
"--provider",
"lmstudio",
"--base-url",
"http://127.0.0.1:9/v1",
])
.args(["--model", "stub-model"])
.env("HOME", home.path())
.env("XDG_CONFIG_HOME", home.path().join(".config"))
.output()
.expect("failed to run koda");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("no session matches '28545'"), "{stderr}");
}