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_session_id_assigns() {
let dir = fresh_dir();
let chosen = "5f3c1a2b-4d6e-4f80-9a1b-2c3d4e5f6071";
let out = roba_in(dir.path())
.args([
"--json",
"--session-id",
chosen,
"respond with the single word: assigned",
])
.output()
.expect("session-id run");
let parsed: serde_json::Value = serde_json::from_slice(&out.stdout).expect("json");
let returned = parsed["result"]["session_id"].as_str().expect("session_id");
assert_eq!(
returned, chosen,
"expected the returned session id to equal the supplied --session-id"
);
}
#[test]
#[ignore]
fn live_show_roundtrips() {
let dir = fresh_dir();
let seed = roba_in(dir.path())
.args(["--json", "respond with the single word: echo"])
.output()
.expect("run roba --json");
assert!(seed.status.success(), "seed failed: {seed:?}");
let seed_json: serde_json::Value =
serde_json::from_slice(&seed.stdout).expect("seed stdout is JSON");
let id = seed_json["result"]["session_id"]
.as_str()
.expect("session_id");
let out = Command::cargo_bin("roba")
.expect("cargo-built roba binary")
.args(["show", id, "--json"])
.output()
.expect("run roba show");
assert!(out.status.success(), "show failed: {out:?}");
let shown: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("show stdout is JSON");
assert_eq!(
shown["result"]["session_id"].as_str(),
Some(id),
"session id must roundtrip"
);
assert!(
!shown["result"]["result"]
.as_str()
.unwrap_or_default()
.is_empty(),
"reconstructed result must be non-empty"
);
}
#[test]
#[ignore]
fn live_show_wait_returns_completed_result() {
let dir = fresh_dir();
let seed = roba_in(dir.path())
.args(["--json", "respond with the single word: waited"])
.output()
.expect("run roba --json");
assert!(seed.status.success(), "seed failed: {seed:?}");
let seed_json: serde_json::Value =
serde_json::from_slice(&seed.stdout).expect("seed stdout is JSON");
let id = seed_json["result"]["session_id"]
.as_str()
.expect("session_id");
let out = Command::cargo_bin("roba")
.expect("cargo-built roba binary")
.args(["show", id, "--wait", "--timeout", "60", "--json"])
.output()
.expect("run roba show --wait");
assert!(out.status.success(), "show --wait failed: {out:?}");
let shown: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("show stdout is JSON");
assert_eq!(shown["result"]["session_id"].as_str(), Some(id));
assert!(
!shown["result"]["result"]
.as_str()
.unwrap_or_default()
.is_empty(),
"waited reconstructed result must be non-empty"
);
}
#[test]
#[ignore]
fn live_json_schema_accepted() {
let dir = fresh_dir();
let schema_path = dir.path().join("schema.json");
std::fs::write(
&schema_path,
r#"{"type":"object","properties":{"answer":{"type":"string"}},"required":["answer"]}"#,
)
.expect("write schema");
let out = roba_in(dir.path())
.args(["--json", "--json-schema"])
.arg(&schema_path)
.arg("respond with the single word: schema")
.output()
.expect("json-schema run");
assert!(out.status.success(), "roba failed: {out:?}");
let parsed: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("--json produced non-JSON stdout");
assert_eq!(parsed["version"].as_u64(), Some(1));
assert!(parsed.get("result").is_some());
}
#[test]
#[ignore]
fn live_json_schema_default_render() {
let dir = fresh_dir();
let schema_path = dir.path().join("schema.json");
std::fs::write(
&schema_path,
r#"{"type":"object","properties":{"answer":{"type":"string"}},"required":["answer"]}"#,
)
.expect("write schema");
let out = roba_in(dir.path())
.arg("--json-schema")
.arg(&schema_path)
.arg("capital of France?")
.output()
.expect("json-schema default run");
assert!(out.status.success(), "roba failed: {out:?}");
assert!(
!out.stdout.is_empty(),
"default path printed nothing; structured_output was dropped"
);
let parsed: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("default path produced non-JSON stdout");
assert!(
parsed.get("answer").is_some(),
"structured output missing `answer` key: {parsed}"
);
}
#[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_compose_stdin_with_prompt() {
let dir = fresh_dir();
let out = roba_in(dir.path())
.args(["--echo", "--plain", "what is the marker?"])
.write_stdin("MARKER LINE 42")
.output()
.expect("run roba --echo with piped stdin");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("MARKER LINE 42"),
"echoed prompt should contain the piped marker, got stderr: {stderr}"
);
assert!(
stderr.contains("what is the marker?"),
"echoed prompt should contain the positional question, got stderr: {stderr}"
);
}
#[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"));
}
#[test]
#[ignore]
fn live_exit_model_not_found_is_failure_not_auth() {
let dir = fresh_dir();
Command::cargo_bin("roba")
.expect("cargo-built roba")
.args([
"-C",
dir.path().to_str().expect("utf-8 path"),
"--model",
"totally-not-a-model-xyz",
"hi",
])
.assert()
.code(1);
}
#[test]
#[ignore]
fn live_exit_max_turns_returns_5() {
let dir = fresh_dir();
std::fs::write(dir.path().join("marker.txt"), "sentinel-42").expect("seed marker");
roba_in(dir.path())
.args([
"--json",
"--max-turns",
"1",
"Read the file marker.txt and report its exact contents.",
])
.assert()
.code(5);
}
#[test]
#[ignore]
fn live_exit_bare_missing_key_is_auth() {
let dir = fresh_dir();
let user = empty_user_home();
roba_in(dir.path())
.env("XDG_CONFIG_HOME", user.path())
.env_remove("ANTHROPIC_API_KEY")
.args(["--bare", "hi"])
.assert()
.code(2);
}
#[test]
#[ignore]
fn live_limits_flags_accepted() {
let dir = fresh_dir();
roba_in(dir.path())
.args([
"--max-turns",
"5",
"--max-budget-usd",
"10.0",
"respond with the single word: bounded",
])
.assert()
.success()
.stdout(predicate::str::contains("bounded"));
}
#[test]
#[ignore]
fn live_mcp_config_accepted() {
let dir = fresh_dir();
let cfg_path = dir.path().join("mcp.json");
std::fs::write(&cfg_path, r#"{"mcpServers":{}}"#).expect("write mcp config");
roba_in(dir.path())
.arg("--mcp-config")
.arg(&cfg_path)
.arg("respond with the single word: mcp")
.assert()
.success()
.stdout(predicate::str::contains("mcp"));
}
#[test]
#[ignore]
fn live_medtier_flags_accepted() {
let dir = fresh_dir();
let extra = fresh_dir();
roba_in(dir.path())
.arg("--add-dir")
.arg(extra.path())
.args([
"--fallback-model",
"haiku",
"--no-session-persistence",
"respond with the single word: medtier",
])
.assert()
.success()
.stdout(predicate::str::contains("medtier"));
}
#[test]
#[ignore]
fn live_alias_draft() {
let dir = fresh_dir();
let out = Command::cargo_bin("roba")
.expect("cargo-built roba binary")
.args([
"-C",
dir.path().to_str().expect("utf-8 tempdir path"),
"alias",
"draft",
"a verb that asks for a one-line summary of a file given as the first argument",
"--model",
"claude-haiku-4-5",
])
.output()
.expect("run roba alias draft");
assert!(
out.status.success(),
"draft should exit 0; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
#[derive(serde::Deserialize)]
struct Wrapper {
alias: std::collections::HashMap<String, roba::aliases::Alias>,
}
let parsed: Wrapper = toml::from_str(&stdout)
.unwrap_or_else(|e| panic!("draft stdout did not parse as an alias: {e}\n{stdout}"));
assert_eq!(
parsed.alias.len(),
1,
"expected exactly one alias block on stdout, got:\n{stdout}"
);
}
#[test]
#[ignore]
fn live_profile_draft() {
let dir = fresh_dir();
let out = Command::cargo_bin("roba")
.expect("cargo-built roba binary")
.args([
"-C",
dir.path().to_str().expect("utf-8 tempdir path"),
"profile",
"draft",
"a cheap fast one-shot profile",
"--model",
"claude-haiku-4-5",
])
.output()
.expect("run roba profile draft");
assert!(
out.status.success(),
"draft should exit 0; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
#[derive(serde::Deserialize)]
struct Wrapper {
profile: std::collections::HashMap<String, roba::profile::Profile>,
}
let parsed: Wrapper = toml::from_str(&stdout)
.unwrap_or_else(|e| panic!("draft stdout did not parse as a profile: {e}\n{stdout}"));
assert_eq!(
parsed.profile.len(),
1,
"expected exactly one profile block on stdout, got:\n{stdout}"
);
}
#[test]
#[ignore]
fn live_config_init() {
let dir = fresh_dir();
std::fs::create_dir_all(dir.path().join(".git")).expect(".git marker");
std::fs::write(
dir.path().join("README.md"),
"# widget\n\nA small Rust CLI for widgets.\n",
)
.expect("write README");
std::fs::create_dir_all(dir.path().join("src")).expect("src dir");
std::fs::write(
dir.path().join("src/main.rs"),
"fn main() { println!(\"widget\"); }\n",
)
.expect("write src");
let out = Command::cargo_bin("roba")
.expect("cargo-built roba binary")
.args([
"-C",
dir.path().to_str().expect("utf-8 tempdir path"),
"config",
"init",
"keep it minimal",
"--model",
"claude-haiku-4-5",
])
.output()
.expect("run roba config init");
assert!(
out.status.success(),
"config init should exit 0; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
roba::profile::pool::parse_config_str(&stdout).unwrap_or_else(|e| {
panic!("config init stdout did not parse as a config: {e:#}\n{stdout}")
});
}
#[test]
#[ignore]
fn live_detach_roundtrip() {
use std::process::{Command as StdCommand, Stdio};
let dir = fresh_dir();
let bin = assert_cmd::cargo::cargo_bin("roba");
let out = StdCommand::new(&bin)
.args([
"-C",
dir.path().to_str().expect("utf-8 tempdir path"),
"--model",
"haiku",
"--detach",
"respond with the single word: pong",
])
.stdin(Stdio::inherit()) .stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("spawn roba --detach");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
out.status.success(),
"--detach should exit 0 after spawning (stdin must be a TTY); stderr:\n{stderr}"
);
let handle = String::from_utf8_lossy(&out.stdout).trim().to_string();
assert_eq!(handle.len(), 36, "handle should be a UUID, got: {handle:?}");
assert_eq!(
handle.matches('-').count(),
4,
"handle should be a UUID, got: {handle:?}"
);
let show = Command::cargo_bin("roba")
.expect("cargo-built roba binary")
.args(["show", &handle, "--wait", "--timeout", "90"])
.assert()
.success();
let rendered = String::from_utf8_lossy(&show.get_output().stdout);
assert!(
!rendered.trim().is_empty(),
"show --wait should render a non-empty result for the detached run"
);
}