use assert_cmd::Command;
use predicates::prelude::*;
use std::path::Path;
fn roba_in(dir: &Path) -> Command {
let mut cmd = Command::cargo_bin("roba").expect("cargo-built roba binary");
cmd.args([
"-C",
dir.to_str().expect("utf-8 tempdir path"),
"--model",
"haiku",
]);
cmd
}
fn fresh_dir() -> tempfile::TempDir {
tempfile::tempdir().expect("create test tempdir")
}
fn fixture_with_config(content: &str) -> tempfile::TempDir {
let tmp = fresh_dir();
std::fs::create_dir_all(tmp.path().join(".git")).expect(".git marker");
std::fs::write(tmp.path().join("roba.toml"), content).expect("write roba.toml");
tmp
}
fn empty_user_home() -> tempfile::TempDir {
fresh_dir()
}
#[test]
#[ignore]
fn live_smoke_prompt() {
let dir = fresh_dir();
roba_in(dir.path())
.arg("respond with the single word: pong")
.assert()
.success()
.stdout(predicate::str::contains("pong"));
}
#[test]
#[ignore]
fn live_smoke_cwd_scopes_session_to_path() {
let dir = fresh_dir();
roba_in(dir.path())
.arg("remember the word: aurora")
.assert()
.success();
let out = roba_in(dir.path())
.args(["-c", "-p", "what word did I ask you to remember"])
.output()
.expect("run roba -c");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.to_lowercase().contains("aurora"),
"expected -C to scope sessions to the tmp dir, got: {stdout}"
);
}
#[test]
#[ignore]
fn live_output_quiet_no_metadata() {
let dir = fresh_dir();
let out = roba_in(dir.path())
.args(["-q", "respond with the single word: hush"])
.output()
.expect("run roba");
assert!(out.status.success(), "roba failed: {out:?}");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("cost"),
"expected no cost footer with -q, got stderr: {stderr}"
);
}
#[test]
#[ignore]
fn live_output_json_valid() {
let dir = fresh_dir();
let out = roba_in(dir.path())
.args(["--json", "respond with the single word: jay"])
.output()
.expect("run roba");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("--json produced non-JSON stdout");
assert_eq!(parsed["version"].as_u64(), Some(1));
assert!(parsed.get("result").is_some());
assert!(parsed["result"].get("session_id").is_some());
assert!(parsed["result"].get("duration_ms").is_some());
assert!(parsed.get("refusal").is_some());
}
#[test]
#[ignore]
fn live_output_refusal_in_json_envelope() {
let dir = fresh_dir();
let out = roba_in(dir.path())
.args([
"--json",
"--quiet",
"what is 2+2? answer with just the number.",
])
.output()
.expect("run roba --json");
assert!(out.status.success(), "roba failed: {out:?}");
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("--json produced non-JSON stdout");
assert_eq!(parsed["version"].as_u64(), Some(1));
assert_eq!(
parsed["refusal"].as_bool(),
Some(false),
"a normal answer must not be flagged as a refusal, got: {stdout}"
);
}
#[test]
#[ignore]
fn live_output_code_strips_fences() {
let dir = fresh_dir();
let out = roba_in(dir.path())
.args([
"write exactly one rust function called id that takes i32 and returns it. fenced code block, no other prose.",
"--code",
])
.output()
.expect("run roba");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
!stdout.contains("```"),
"--code did not strip fences: {stdout}"
);
assert!(
stdout.contains("fn id"),
"expected fn id in output, got: {stdout}"
);
}
#[test]
#[ignore]
fn live_output_out_writes_file_and_stdout() {
let dir = fresh_dir();
let target = dir.path().join("out.md");
let out = roba_in(dir.path())
.args([
"respond with the single word: saved",
"--out",
target.to_str().unwrap(),
])
.output()
.expect("run roba --out");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.to_lowercase().contains("saved"),
"expected 'saved' in stdout with --out, got: {stdout}"
);
let file_contents = std::fs::read_to_string(&target).expect("read saved file");
assert!(
file_contents.to_lowercase().contains("saved"),
"expected 'saved' in saved file, got: {file_contents}"
);
}
#[test]
#[ignore]
fn live_output_out_json_extension() {
let dir = fresh_dir();
let target = dir.path().join("out.json");
roba_in(dir.path())
.args([
"respond with the single word: jp",
"--out",
target.to_str().unwrap(),
])
.assert()
.success();
let file_contents = std::fs::read_to_string(&target).expect("read saved file");
let parsed: serde_json::Value =
serde_json::from_str(&file_contents).expect("saved file should be JSON");
assert_eq!(parsed["version"].as_u64(), Some(1));
assert!(parsed["result"].get("session_id").is_some());
}
#[test]
#[ignore]
fn live_session_continue_carries_context() {
let dir = fresh_dir();
roba_in(dir.path())
.arg("remember the word: zenith")
.assert()
.success();
let out = roba_in(dir.path())
.args(["-c", "-p", "what word did I ask you to remember"])
.output()
.expect("run roba -c");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.to_lowercase().contains("zenith"),
"expected 'zenith' from continued session, got: {stdout}"
);
}
#[test]
#[ignore]
fn live_session_resume_fork_new_id() {
let dir = fresh_dir();
let seed = roba_in(dir.path())
.args(["--json", "respond with the single word: seed"])
.output()
.expect("seed run");
let seed_json: serde_json::Value = serde_json::from_slice(&seed.stdout).expect("json");
let seed_id = seed_json["result"]["session_id"]
.as_str()
.expect("session_id")
.to_string();
let resume_arg = format!("-c={seed_id}");
let fork = roba_in(dir.path())
.args([
"--json",
&resume_arg,
"--fork",
"respond with the single word: forked",
])
.output()
.expect("fork run");
let fork_json: serde_json::Value = serde_json::from_slice(&fork.stdout).expect("json");
let fork_id = fork_json["result"]["session_id"]
.as_str()
.expect("session_id");
assert_ne!(
seed_id, fork_id,
"expected fork to produce a new session id"
);
}
#[test]
#[ignore]
fn live_stream_emits_to_stdout() {
let dir = fresh_dir();
let out = roba_in(dir.path())
.args(["respond with the single word: streamed", "--stream"])
.output()
.expect("run roba --stream");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.to_lowercase().contains("streamed"),
"expected 'streamed' on stdout, got: {stdout}"
);
}
#[test]
#[ignore]
fn live_stream_session_id_on_stderr() {
let dir = fresh_dir();
let out = roba_in(dir.path())
.args(["--stream", "respond with the single word: ping"])
.output()
.expect("run roba --stream");
assert!(out.status.success(), "roba --stream failed: {out:?}");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("[roba] session:"),
"expected [roba] session: on stderr with --stream, got stderr: {stderr}"
);
let quiet_out = roba_in(dir.path())
.args(["--stream", "--quiet", "respond with the single word: ping"])
.output()
.expect("run roba --stream --quiet");
assert!(
quiet_out.status.success(),
"roba --stream --quiet failed: {quiet_out:?}"
);
let quiet_stderr = String::from_utf8_lossy(&quiet_out.stderr);
assert!(
!quiet_stderr.contains("[roba] session:"),
"expected no [roba] session: on stderr with --quiet, got stderr: {quiet_stderr}"
);
}
#[test]
#[ignore]
fn live_trace_writes_jsonl() {
let dir = fresh_dir();
let trace = dir.path().join("run.jsonl");
let out = roba_in(dir.path())
.args([
"respond with the single word: traced",
"--trace",
trace.to_str().unwrap(),
])
.output()
.expect("run roba --trace");
assert!(out.status.success(), "roba --trace failed: {out:?}");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.to_lowercase().contains("traced"),
"expected 'traced' on stdout with --trace, got: {stdout}"
);
let body = std::fs::read_to_string(&trace).expect("read trace file");
let mut lines = 0usize;
let mut saw_assistant = false;
let mut saw_result = false;
for line in body.lines().filter(|l| !l.trim().is_empty()) {
lines += 1;
let ev: serde_json::Value = serde_json::from_str(line)
.unwrap_or_else(|e| panic!("non-JSON trace line {line:?}: {e}"));
match ev["type"].as_str() {
Some("assistant") => saw_assistant = true,
Some("result") => saw_result = true,
_ => {}
}
}
assert!(lines >= 1, "expected at least one trace line, got none");
assert!(saw_assistant, "expected an assistant event in the trace");
assert!(saw_result, "expected a result event in the trace");
}
#[test]
#[ignore]
fn live_perms_readonly_blocks_edit() {
let dir = fresh_dir();
let target = dir.path().join("subject.txt");
std::fs::write(&target, "original").expect("seed file");
roba_in(dir.path())
.arg(format!(
"edit the file at {} to replace its contents with the single word: changed. \
if you cannot, briefly say so.",
target.display()
))
.assert()
.success();
let contents = std::fs::read_to_string(&target).expect("read target");
assert_eq!(
contents.trim(),
"original",
"readonly default should keep the file unchanged, got: {contents}"
);
}
#[test]
#[ignore]
fn live_perms_writable_enables_edit() {
let dir = fresh_dir();
let target = dir.path().join("subject.txt");
std::fs::write(&target, "original").expect("seed file");
roba_in(dir.path())
.args([
"--writable",
&format!(
"edit the file at {} so its contents are exactly the single word: changed",
target.display()
),
])
.assert()
.success();
let contents = std::fs::read_to_string(&target).expect("read target");
assert!(
contents.contains("changed"),
"--writable should allow edits, got: {contents}"
);
}
#[test]
#[ignore]
fn live_perms_deny_tools_blocks_modification() {
let dir = fresh_dir();
let target = dir.path().join("subject.txt");
std::fs::write(&target, "original").expect("seed file");
roba_in(dir.path())
.args([
"--writable",
"--deny-tool",
"Edit",
"--deny-tool",
"Write",
&format!(
"edit the file at {} to replace its contents with the single word: changed. \
if you cannot, briefly say so.",
target.display()
),
])
.assert()
.success();
let contents = std::fs::read_to_string(&target).expect("read target");
assert_eq!(
contents.trim(),
"original",
"denying Edit + Write should block all file modifications, got: {contents}"
);
}
#[test]
#[ignore]
fn live_perms_full_auto_enables_bash() {
let dir = fresh_dir();
let target = dir.path().join("flag.txt");
roba_in(dir.path())
.args([
"--full-auto",
&format!(
"use the Bash tool to write the literal string `bypassed` into the file at {}. \
just run the shell command; no other prose.",
target.display()
),
])
.assert()
.success();
let contents = std::fs::read_to_string(&target).expect("read target");
assert!(
contents.contains("bypassed"),
"--full-auto should allow Bash to write the file, got: {contents}"
);
}
#[test]
#[ignore]
fn live_compose_attach_files_visible() {
let dir = fresh_dir();
let attach_path = dir.path().join("greeting.txt");
std::fs::write(&attach_path, "secret word: kazoo").expect("write attach file");
let out = roba_in(dir.path())
.args([
"--attach",
attach_path.to_str().unwrap(),
"what is the secret word in the attached file? answer with just the word.",
])
.output()
.expect("run roba --attach");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.to_lowercase().contains("kazoo"),
"expected 'kazoo' to be referenced from attached file, got: {stdout}"
);
}
#[test]
#[ignore]
fn live_compose_var_substitution() {
let dir = fresh_dir();
let tpl = dir.path().join("tpl.md");
std::fs::write(&tpl, "Respond with exactly: {{TARGET}}").expect("write tpl");
let out = roba_in(dir.path())
.args(["-f", tpl.to_str().unwrap(), "--var", "TARGET=lighthouse"])
.output()
.expect("run roba -f --var");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.to_lowercase().contains("lighthouse"),
"expected substituted value to reach the model, got: {stdout}"
);
}
#[test]
#[ignore]
fn live_profile_writable_via_top_level() {
let dir = fixture_with_config("writable = true\n");
let user = empty_user_home();
let target = dir.path().join("t.txt");
std::fs::write(&target, "original").expect("seed");
roba_in(dir.path())
.env("XDG_CONFIG_HOME", user.path())
.arg(format!(
"edit the file at {} so its contents are exactly the single word: changed",
target.display()
))
.assert()
.success();
let contents = std::fs::read_to_string(&target).expect("read");
assert!(
contents.contains("changed"),
"top-level writable should apply, got: {contents}"
);
}
#[test]
#[ignore]
fn live_profile_named_overlay_via_flag() {
let dir = fixture_with_config("[profile.edit]\nwritable = true\n");
let user = empty_user_home();
let target = dir.path().join("t.txt");
std::fs::write(&target, "original").expect("seed");
roba_in(dir.path())
.env("XDG_CONFIG_HOME", user.path())
.args([
"--profile",
"edit",
&format!(
"edit the file at {} so its contents are exactly the single word: changed",
target.display()
),
])
.assert()
.success();
let contents = std::fs::read_to_string(&target).expect("read");
assert!(
contents.contains("changed"),
"--profile edit overlay should apply writable, got: {contents}"
);
}
#[test]
#[ignore]
fn live_profile_default_auto_applies() {
let dir = fixture_with_config("[profile.default]\nwritable = true\n");
let user = empty_user_home();
let target = dir.path().join("t.txt");
std::fs::write(&target, "original").expect("seed");
roba_in(dir.path())
.env("XDG_CONFIG_HOME", user.path())
.env_remove("ROBA_PROFILE")
.arg(format!(
"edit the file at {} so its contents are exactly the single word: changed",
target.display()
))
.assert()
.success();
let contents = std::fs::read_to_string(&target).expect("read");
assert!(
contents.contains("changed"),
"[profile.default] should auto-apply, got: {contents}"
);
}
#[test]
#[ignore]
fn live_profile_no_default_skips_auto() {
let dir = fixture_with_config("[profile.default]\nwritable = true\n");
let user = empty_user_home();
let target = dir.path().join("t.txt");
std::fs::write(&target, "original").expect("seed");
roba_in(dir.path())
.env("XDG_CONFIG_HOME", user.path())
.env_remove("ROBA_PROFILE")
.args([
"--no-default-profile",
&format!(
"edit the file at {} to replace its contents with the single word: changed. \
if you cannot, briefly say so.",
target.display()
),
])
.assert()
.success();
let contents = std::fs::read_to_string(&target).expect("read");
assert_eq!(
contents.trim(),
"original",
"--no-default-profile should skip auto-apply, got: {contents}"
);
}
#[test]
#[ignore]
fn live_env_writable_enables_edit() {
let dir = fresh_dir();
let user = empty_user_home();
let target = dir.path().join("t.txt");
std::fs::write(&target, "original").expect("seed");
roba_in(dir.path())
.env("XDG_CONFIG_HOME", user.path())
.env("ROBA_WRITABLE", "1")
.arg(format!(
"edit the file at {} so its contents are exactly the single word: changed",
target.display()
))
.assert()
.success();
let contents = std::fs::read_to_string(&target).expect("read");
assert!(
contents.contains("changed"),
"ROBA_WRITABLE=1 should enable Edit, got: {contents}"
);
}
#[test]
#[ignore]
fn live_env_var_per_key_substitution() {
let dir = fresh_dir();
let user = empty_user_home();
let tpl = dir.path().join("tpl.md");
std::fs::write(&tpl, "Respond with exactly: {{TARGET}}").expect("seed tpl");
let out = roba_in(dir.path())
.env("XDG_CONFIG_HOME", user.path())
.env("ROBA_VAR_TARGET", "spruce")
.args(["-f", tpl.to_str().unwrap()])
.output()
.expect("run");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.to_lowercase().contains("spruce"),
"ROBA_VAR_TARGET=spruce should reach the model, got: {stdout}"
);
}
#[test]
#[ignore]
fn live_env_fresh_cancels_continue() {
let dir = fresh_dir();
let user = empty_user_home();
let seed = roba_in(dir.path())
.env("XDG_CONFIG_HOME", user.path())
.args(["--json", "respond with the single word: anchor"])
.output()
.expect("seed run");
let seed_json: serde_json::Value = serde_json::from_slice(&seed.stdout).expect("seed json");
let seed_id = seed_json["result"]["session_id"]
.as_str()
.expect("session_id")
.to_string();
let fresh = roba_in(dir.path())
.env("XDG_CONFIG_HOME", user.path())
.env("ROBA_CONTINUE", "1")
.args(["--fresh", "--json", "respond with the single word: cedar"])
.output()
.expect("fresh run");
let fresh_json: serde_json::Value = serde_json::from_slice(&fresh.stdout).expect("fresh json");
let fresh_id = fresh_json["result"]["session_id"]
.as_str()
.expect("session_id");
assert_ne!(
seed_id, fresh_id,
"--fresh should produce a new session id even with ROBA_CONTINUE=1"
);
}
#[test]
#[ignore]
fn live_effort_low_succeeds() {
let dir = fresh_dir();
roba_in(dir.path())
.args(["--effort", "low", "respond with the single word: done"])
.assert()
.success()
.stdout(predicate::str::contains("done"));
}
#[test]
#[ignore]
fn live_effort_max_succeeds() {
let dir = fresh_dir();
roba_in(dir.path())
.args(["--effort", "max", "respond with the single word: done"])
.assert()
.success()
.stdout(predicate::str::contains("done"));
}
#[test]
#[ignore]
fn live_system_prompt_succeeds() {
let dir = fresh_dir();
let user = empty_user_home();
roba_in(dir.path())
.env("XDG_CONFIG_HOME", user.path())
.args([
"--system-prompt",
"You are a helpful assistant.",
"what is 1+1",
])
.assert()
.success()
.stdout(predicate::str::is_empty().not());
}
#[test]
#[ignore]
fn live_append_system_prompt_stacks() {
let dir = fresh_dir();
roba_in(dir.path())
.args([
"--append-system-prompt",
"Always end your response with the token: [APPENDED]",
"what is 1+1",
])
.assert()
.success()
.stdout(predicate::str::contains("[APPENDED]"));
}
#[test]
#[ignore]
fn live_perms_mode_dont_ask_succeeds() {
let dir = fresh_dir();
roba_in(dir.path())
.args([
"--writable",
"--permission-mode",
"dontAsk",
"respond with the single word: ok",
])
.assert()
.success();
}
#[test]
#[ignore]
fn live_perms_mode_via_profile() {
let dir =
fixture_with_config("[profile.testmode]\npermission_mode = \"dontAsk\"\nwritable = true\n");
let user = empty_user_home();
roba_in(dir.path())
.env("XDG_CONFIG_HOME", user.path())
.args(["--profile", "testmode", "respond with: ok"])
.assert()
.success();
}
#[test]
#[ignore]
fn live_perms_mode_via_env() {
let dir = fresh_dir();
roba_in(dir.path())
.env("ROBA_PERMISSION_MODE", "dontAsk")
.args(["--writable", "respond with the single word: ok"])
.assert()
.success();
}
#[test]
#[ignore]
fn live_bare_succeeds() {
if std::env::var_os("ANTHROPIC_API_KEY").is_none() {
eprintln!(
"skipping live_bare_succeeds: --bare authenticates via ANTHROPIC_API_KEY only, \
which is not set in this environment"
);
return;
}
let dir = fresh_dir();
let user = empty_user_home();
roba_in(dir.path())
.env("XDG_CONFIG_HOME", user.path())
.args(["--bare", "respond with the single word: bare"])
.assert()
.success()
.stdout(predicate::str::contains("bare"));
}