fabro/lib/crates/fabro-cli/tests/it/cmd/exec.rs
Bryan Helmkamp b5e5b91d51
Default TestContext::command() working directory to temp_dir
Tests were flaky because command() inherited the real repo as the
working directory. When the repo was clean, detached runs attempted
git worktree creation against it, sometimes failing and injecting
extra warning lines into snapshots.

Now command() defaults to the non-git temp_dir, eliminating this
class of flakiness. Tests needing a specific directory override with
.current_dir(). Also canonicalizes fixture paths and adds a
[FIXTURES] snapshot filter via test_context!() macro.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 20:36:48 -04:00

243 lines
7.7 KiB
Rust

use fabro_test::{fabro_snapshot, test_context};
#[test]
fn help() {
let context = test_context!();
let mut cmd = context.command();
cmd.args(["exec", "--help"]);
fabro_snapshot!(context.filters(), cmd, @"
success: true
exit_code: 0
----- stdout -----
Run an agentic coding session
Usage: fabro exec [OPTIONS] <PROMPT>
Arguments:
<PROMPT> Task prompt
Options:
--json Output as JSON [env: FABRO_JSON=]
--provider <PROVIDER> LLM provider (anthropic, openai, gemini, kimi, zai, minimax, inception)
--model <MODEL> Model name (defaults per provider)
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--permissions <PERMISSIONS> Permission level for tool execution [possible values: read-only, read-write, full]
--auto-approve Skip interactive prompts; deny tools outside permission level
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--debug Print LLM request/response debug info to stderr
--storage-dir <STORAGE_DIR> Storage directory (default: ~/.fabro) [env: FABRO_STORAGE_DIR=[STORAGE_DIR]]
--verbose Print full LLM request/response JSON to stderr
--skills-dir <SKILLS_DIR> Directory containing skill files (overrides default discovery)
--output-format <OUTPUT_FORMAT> Output format (text for human-readable, json for NDJSON event stream) [possible values: text, json]
-h, --help Print help
----- stderr -----
");
}
#[test]
fn invalid_permissions() {
let context = test_context!();
let mut cmd = context.exec_cmd();
cmd.args(["--permissions", "bogus", "test prompt"]);
fabro_snapshot!(context.filters(), cmd, @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'bogus' for '--permissions <PERMISSIONS>'
[possible values: read-only, read-write, full]
For more information, try '--help'.
");
}
#[test]
fn no_prompt() {
let context = test_context!();
fabro_snapshot!(context.filters(), context.exec_cmd(), @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: the following required arguments were not provided:
<PROMPT>
Usage: fabro exec --no-upgrade-check --storage-dir <STORAGE_DIR> <PROMPT>
For more information, try '--help'.
");
}
#[test]
fn exec_missing_api_key_exits_with_error() {
let context = test_context!();
let mut cmd = context.exec_cmd();
cmd.arg("test prompt");
cmd.env_clear();
cmd.env("HOME", &context.home_dir);
fabro_snapshot!(context.filters(), cmd, @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
error: API key not set for provider 'anthropic'
");
}
#[test]
fn exec_uses_user_config_defaults() {
let context = test_context!();
context.write_home(
".fabro/user.toml",
"[exec]\nprovider = \"openai\"\nmodel = \"gpt-4.1-mini\"\npermissions = \"read-only\"\noutput_format = \"json\"\n",
);
let mut cmd = context.exec_cmd();
cmd.arg("test prompt");
cmd.env_clear();
cmd.env("HOME", &context.home_dir);
cmd.env("FABRO_STORAGE_DIR", &context.storage_dir);
cmd.env("FABRO_NO_UPGRADE_CHECK", "true");
fabro_snapshot!(context.filters(), cmd, @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
error: API key not set for provider 'openai'
");
}
#[fabro_macros::e2e_test(live("ANTHROPIC_API_KEY"))]
fn exec_creates_file() {
let context = test_context!();
context
.exec_cmd()
.args([
"--auto-approve",
"--permissions",
"full",
"--provider",
"anthropic",
"--model",
"claude-haiku-4-5",
"Create a file called hello.txt containing exactly 'Hello'",
])
.timeout(std::time::Duration::from_secs(120))
.assert()
.success();
let path = context.temp_dir.join("hello.txt");
assert!(path.exists(), "hello.txt should have been created");
let content = std::fs::read_to_string(&path).expect("read hello.txt");
assert!(
content.contains("Hello"),
"Expected 'Hello' in hello.txt, got: {content}"
);
}
#[fabro_macros::e2e_test(live("ANTHROPIC_API_KEY"))]
fn exec_shell_command() {
let context = test_context!();
context
.exec_cmd()
.args([
"--auto-approve",
"--permissions",
"full",
"--provider",
"anthropic",
"--model",
"claude-haiku-4-5",
"Run the shell command `echo arc_test_marker_42` and tell me what it printed",
])
.timeout(std::time::Duration::from_secs(120))
.assert()
.success();
}
#[fabro_macros::e2e_test(live("ANTHROPIC_API_KEY"))]
fn exec_read_only_blocks_write() {
let context = test_context!();
context
.exec_cmd()
.args([
"--auto-approve",
"--permissions",
"read-only",
"--provider",
"anthropic",
"--model",
"claude-haiku-4-5",
"Create a file called forbidden.txt containing 'should not exist'",
])
.timeout(std::time::Duration::from_secs(120))
.assert()
.success();
assert!(
!context.temp_dir.join("forbidden.txt").exists(),
"forbidden.txt should NOT exist under read-only permissions"
);
}
#[fabro_macros::e2e_test(live("ANTHROPIC_API_KEY"))]
fn exec_json_output_format() {
let context = test_context!();
let output = context
.exec_cmd()
.args([
"--auto-approve",
"--permissions",
"full",
"--output-format",
"json",
"--provider",
"anthropic",
"--model",
"claude-haiku-4-5",
"Create a file called test.txt containing 'test'",
])
.timeout(std::time::Duration::from_secs(120))
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(output).expect("valid utf8");
assert!(!stdout.trim().is_empty(), "json output should not be empty");
// Every non-empty line should be valid JSON
let lines: Vec<&str> = stdout.lines().filter(|l| !l.trim().is_empty()).collect();
assert!(!lines.is_empty(), "should have at least one NDJSON line");
let first: serde_json::Value =
serde_json::from_str(lines[0]).expect("first line should be valid JSON");
assert!(
first.get("event").is_some() || first.get("type").is_some(),
"NDJSON line should have an event or type field, got: {first}"
);
}
#[fabro_macros::e2e_test(live("ANTHROPIC_API_KEY"))]
fn exec_read_and_edit() {
let context = test_context!();
context.write_temp("data.txt", "old content");
context
.exec_cmd()
.args([
"--auto-approve",
"--permissions",
"full",
"--provider",
"anthropic",
"--model",
"claude-haiku-4-5",
"Read data.txt then replace its entire content with 'new content'",
])
.timeout(std::time::Duration::from_secs(120))
.assert()
.success();
let content =
std::fs::read_to_string(context.temp_dir.join("data.txt")).expect("read data.txt");
assert!(
content.contains("new content"),
"Expected 'new content' in data.txt, got: {content}"
);
}