Migrate 29 CLI tests from assert_cmd/insta to declarative trycmd

Model list tests (6): exact stdout snapshots in .trycmd files, replacing
insta .snap files. Error validation tests (5): TOML with stderr-to-stdout
checking specific error messages. Doctor tests (3): TOML with env.inherit
= false for env_clear(). Validate tests (7): TOML with fs.cwd/sandbox for
file path access. Dry-run tests (6): TOML with fs.cwd/sandbox. Exec help
and no-prompt tests (2): TOML with env config.

Remaining in cli.rs: negative-assertion tests, file-system side-effect
test, tmpdir+env test, and 11 ignored API-key-dependent tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-10 22:42:24 -04:00
parent bf807686f6
commit e248fc5651
39 changed files with 175 additions and 354 deletions

View file

@ -6,80 +6,8 @@ fn arc() -> Command {
Command::cargo_bin("arc").unwrap()
}
// == Models ===================================================================
#[test]
fn model_list_prints_all_models() {
let output = arc().args(["model", "list"]).output().expect("runs");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
insta::assert_snapshot!(stdout);
}
#[test]
fn model_list_filters_by_provider() {
let output = arc()
.args(["model", "list", "--provider", "anthropic"])
.output()
.expect("runs");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
insta::assert_snapshot!(stdout);
}
#[test]
fn model_list_filters_by_query() {
let output = arc()
.args(["model", "list", "--query", "opus"])
.output()
.expect("runs");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
insta::assert_snapshot!(stdout);
}
#[test]
fn model_list_query_is_case_insensitive() {
let output = arc()
.args(["model", "list", "--query", "OPUS"])
.output()
.expect("runs");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
insta::assert_snapshot!(stdout);
}
#[test]
fn model_list_query_matches_aliases() {
let output = arc()
.args(["model", "list", "--query", "codex"])
.output()
.expect("runs");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
insta::assert_snapshot!(stdout);
}
#[test]
fn model_bare_defaults_to_list() {
let output = arc().args(["model"]).output().expect("runs");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
insta::assert_snapshot!(stdout);
}
// == LLM: prompt ==============================================================
#[test]
fn prompt_errors_without_prompt_text() {
arc()
.args(["llm", "prompt"])
.write_stdin("")
.assert()
.failure()
.stderr(predicate::str::contains("no prompt provided"));
}
#[test]
fn prompt_reads_from_stdin() {
let result = arc()
@ -110,15 +38,6 @@ fn prompt_concatenates_stdin_and_arg() {
result.stderr(predicate::str::contains("no prompt provided").not());
}
#[test]
fn prompt_rejects_bad_option_format() {
arc()
.args(["llm", "prompt", "-o", "bad_option", "hello"])
.assert()
.failure()
.stderr(predicate::str::contains("expected key=value"));
}
#[test]
#[ignore = "requires API key"]
fn prompt_no_stream_generates_response() {
@ -170,24 +89,6 @@ fn prompt_usage_shows_tokens() {
.stderr(predicate::str::contains("Tokens:"));
}
#[test]
fn prompt_schema_rejects_invalid_json() {
arc()
.args([
"llm",
"prompt",
"--no-stream",
"-m",
"test-model",
"--schema",
"not json",
"hello",
])
.assert()
.failure()
.stderr(predicate::str::contains("--schema must be valid JSON"));
}
#[test]
#[ignore = "requires API key"]
fn prompt_schema_no_stream_generates_json() {
@ -274,24 +175,6 @@ fn chat_multi_turn_with_system_prompt() {
// == Exec =====================================================================
#[test]
fn exec_no_prompt_prints_usage() {
arc()
.args(["exec"])
.env_clear()
.assert()
.failure()
.stderr(predicate::str::contains("Usage:"));
}
#[test]
fn exec_help_flag_prints_help() {
let output = arc().args(["exec", "--help"]).output().expect("runs");
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
insta::assert_snapshot!(stdout);
}
#[test]
fn exec_missing_api_key_exits_with_error() {
let tmp = tempfile::tempdir().expect("tempdir");
@ -305,16 +188,6 @@ fn exec_missing_api_key_exits_with_error() {
.stderr(predicate::str::contains("API key not set"));
}
#[test]
fn exec_invalid_permissions_value() {
arc()
.args(["exec", "--permissions", "bogus", "test prompt"])
.env_clear()
.assert()
.failure()
.stderr(predicate::str::contains("invalid value"));
}
#[test]
#[ignore = "requires API key"]
fn exec_creates_file() {
@ -463,70 +336,6 @@ fn exec_read_and_edit() {
);
}
// == Arc: validate ======================================================
#[test]
fn validate_simple() {
arc()
.args(["validate", "../../../test/simple.dot"])
.assert()
.success()
.stderr(predicate::str::contains("Validation: OK"));
}
#[test]
fn validate_branching() {
arc()
.args(["validate", "../../../test/branching.dot"])
.assert()
.success()
.stderr(predicate::str::contains("Validation: OK"));
}
#[test]
fn validate_conditions() {
arc()
.args(["validate", "../../../test/conditions.dot"])
.assert()
.success()
.stderr(predicate::str::contains("Validation: OK"));
}
#[test]
fn validate_parallel() {
arc()
.args(["validate", "../../../test/parallel.dot"])
.assert()
.success()
.stderr(predicate::str::contains("Validation: OK"));
}
#[test]
fn validate_styled() {
arc()
.args(["validate", "../../../test/styled.dot"])
.assert()
.success()
.stderr(predicate::str::contains("Validation: OK"));
}
#[test]
fn validate_legacy_tool() {
arc()
.args(["validate", "../../../test/legacy_tool.dot"])
.assert()
.success()
.stderr(predicate::str::contains("Validation: OK"));
}
#[test]
fn validate_invalid() {
arc()
.args(["validate", "../../../test/invalid.dot"])
.assert()
.failure();
}
// == Arc: serve =========================================================
#[test]
@ -538,117 +347,8 @@ fn serve_help() {
insta::assert_snapshot!(stdout);
}
// == Arc: run --dry-run =================================================
#[test]
fn dry_run_simple() {
arc()
.args([
"run",
"--dry-run",
"--auto-approve",
"../../../test/simple.dot",
])
.assert()
.success();
}
#[test]
fn dry_run_branching() {
arc()
.args([
"run",
"--dry-run",
"--auto-approve",
"../../../test/branching.dot",
])
.assert()
.success();
}
#[test]
fn dry_run_conditions() {
arc()
.args([
"run",
"--dry-run",
"--auto-approve",
"../../../test/conditions.dot",
])
.assert()
.success();
}
#[test]
fn dry_run_parallel() {
arc()
.args([
"run",
"--dry-run",
"--auto-approve",
"../../../test/parallel.dot",
])
.assert()
.success();
}
#[test]
fn dry_run_styled() {
arc()
.args([
"run",
"--dry-run",
"--auto-approve",
"../../../test/styled.dot",
])
.assert()
.success();
}
#[test]
fn dry_run_legacy_tool() {
arc()
.args([
"run",
"--dry-run",
"--auto-approve",
"../../../test/legacy_tool.dot",
])
.assert()
.success();
}
// == Dotenv ===================================================================
#[test]
fn no_dotenv_flag_is_rejected() {
arc()
.args(["--no-dotenv", "doctor"])
.assert()
.failure()
.stderr(predicate::str::contains("unexpected argument"));
}
// == Doctor ===================================================================
#[test]
fn doctor_runs_and_prints_header() {
arc()
.args(["doctor"])
.env_clear()
.assert()
.stdout(predicate::str::contains("Arc Doctor"));
}
#[test]
fn doctor_verbose_runs_and_prints_header() {
arc()
.args(["doctor", "-v"])
.env_clear()
.assert()
.stdout(predicate::str::contains("Arc Doctor"));
}
#[test]
fn doctor_no_color_when_no_color_set() {
arc()
@ -659,15 +359,6 @@ fn doctor_no_color_when_no_color_set() {
.stdout(predicate::str::contains("\x1b[").not());
}
#[test]
fn doctor_dry_run_flag_accepted() {
arc()
.args(["doctor", "--dry-run"])
.env_clear()
.assert()
.stdout(predicate::str::contains("Arc Doctor"));
}
// == JSONL logging ============================================================
#[test]

View file

@ -0,0 +1,3 @@
...
[..]Arc Doctor[..]
...

View file

@ -0,0 +1,5 @@
bin.name = "arc"
args = ["doctor", "--dry-run"]
[env]
inherit = false

View file

@ -0,0 +1,3 @@
...
[..]Arc Doctor[..]
...

View file

@ -0,0 +1,5 @@
bin.name = "arc"
args = ["doctor"]
[env]
inherit = false

View file

@ -0,0 +1,3 @@
...
[..]Arc Doctor[..]
...

View file

@ -0,0 +1,5 @@
bin.name = "arc"
args = ["doctor", "-v"]
[env]
inherit = false

View file

@ -0,0 +1,3 @@
...
[..]invalid value[..]
...

View file

@ -0,0 +1,7 @@
bin.name = "arc"
args = ["exec", "--permissions", "bogus", "test prompt"]
status = "failed"
stderr-to-stdout = true
[env]
inherit = false

View file

@ -0,0 +1,3 @@
...
[..]Usage:[..]
...

View file

@ -0,0 +1,7 @@
bin.name = "arc"
args = ["exec"]
status = "failed"
stderr-to-stdout = true
[env]
inherit = false

View file

@ -0,0 +1,3 @@
...
[..]expected key=value[..]
...

View file

@ -0,0 +1,4 @@
bin.name = "arc"
args = ["llm", "prompt", "-o", "bad_option", "hello"]
status = "failed"
stderr-to-stdout = true

View file

@ -0,0 +1,3 @@
...
[..]no prompt provided[..]
...

View file

@ -0,0 +1,4 @@
bin.name = "arc"
args = ["llm", "prompt"]
status = "failed"
stderr-to-stdout = true

View file

@ -0,0 +1,3 @@
...
[..]--schema must be valid JSON[..]
...

View file

@ -0,0 +1,4 @@
bin.name = "arc"
args = ["llm", "prompt", "--no-stream", "-m", "test-model", "--schema", "not json", "hello"]
status = "failed"
stderr-to-stdout = true

View file

@ -1,7 +1,5 @@
---
source: lib/crates/arc-cli/tests/cli.rs
expression: stdout
---
```console
$ arc model
MODEL PROVIDER ALIASES CONTEXT COST SPEED
claude-opus-4-6 anthropic opus, claude-opus 1m $15.0 / $75.0 25 tok/s
claude-sonnet-4-5 anthropic 200k $3.0 / $15.0 50 tok/s
@ -21,3 +19,5 @@ kimi-k2.5 kimi kimi 262k
glm-4.7 zai glm, glm4 203k $0.6 / $2.2 100 tok/s
minimax-m2.5 minimax minimax 197k $0.3 / $1.2 45 tok/s
mercury-2 inception mercury 131k $0.2 / $0.8 1000 tok/s
```

View file

@ -1,9 +1,9 @@
---
source: lib/crates/arc-cli/tests/cli.rs
expression: stdout
---
```console
$ arc model list --provider anthropic
MODEL PROVIDER ALIASES CONTEXT COST SPEED
claude-opus-4-6 anthropic opus, claude-opus 1m $15.0 / $75.0 25 tok/s
claude-sonnet-4-5 anthropic 200k $3.0 / $15.0 50 tok/s
claude-sonnet-4-6 anthropic sonnet, claude-sonnet 200k $3.0 / $15.0 50 tok/s
claude-haiku-4-5 anthropic haiku, claude-haiku 200k $0.8 / $4.0 100 tok/s
```

View file

@ -1,8 +1,8 @@
---
source: lib/crates/arc-cli/tests/cli.rs
expression: stdout
---
```console
$ arc model list --query codex
MODEL PROVIDER ALIASES CONTEXT COST SPEED
gpt-5.2-codex openai 1m $1.8 / $14.0 100 tok/s
gpt-5.3-codex openai codex 1m $1.8 / $14.0 100 tok/s
gpt-5.3-codex-spark openai codex-spark 131k - / - 1000 tok/s
```

View file

@ -1,6 +1,6 @@
---
source: lib/crates/arc-cli/tests/cli.rs
expression: stdout
---
```console
$ arc model list --query OPUS
MODEL PROVIDER ALIASES CONTEXT COST SPEED
claude-opus-4-6 anthropic opus, claude-opus 1m $15.0 / $75.0 25 tok/s
```

View file

@ -1,6 +1,6 @@
---
source: lib/crates/arc-cli/tests/cli.rs
expression: stdout
---
```console
$ arc model list --query opus
MODEL PROVIDER ALIASES CONTEXT COST SPEED
claude-opus-4-6 anthropic opus, claude-opus 1m $15.0 / $75.0 25 tok/s
```

View file

@ -1,7 +1,5 @@
---
source: lib/crates/arc-cli/tests/cli.rs
expression: stdout
---
```console
$ arc model list
MODEL PROVIDER ALIASES CONTEXT COST SPEED
claude-opus-4-6 anthropic opus, claude-opus 1m $15.0 / $75.0 25 tok/s
claude-sonnet-4-5 anthropic 200k $3.0 / $15.0 50 tok/s
@ -21,3 +19,5 @@ kimi-k2.5 kimi kimi 262k
glm-4.7 zai glm, glm4 203k $0.6 / $2.2 100 tok/s
minimax-m2.5 minimax minimax 197k $0.3 / $1.2 45 tok/s
mercury-2 inception mercury 131k $0.2 / $0.8 1000 tok/s
```

View file

@ -0,0 +1,6 @@
bin.name = "arc"
args = ["run", "--dry-run", "--auto-approve", "../../../test/branching.dot"]
[fs]
sandbox = false
cwd = "../../.."

View file

@ -0,0 +1,6 @@
bin.name = "arc"
args = ["run", "--dry-run", "--auto-approve", "../../../test/conditions.dot"]
[fs]
sandbox = false
cwd = "../../.."

View file

@ -0,0 +1,6 @@
bin.name = "arc"
args = ["run", "--dry-run", "--auto-approve", "../../../test/legacy_tool.dot"]
[fs]
sandbox = false
cwd = "../../.."

View file

@ -0,0 +1,6 @@
bin.name = "arc"
args = ["run", "--dry-run", "--auto-approve", "../../../test/parallel.dot"]
[fs]
sandbox = false
cwd = "../../.."

View file

@ -0,0 +1,6 @@
bin.name = "arc"
args = ["run", "--dry-run", "--auto-approve", "../../../test/simple.dot"]
[fs]
sandbox = false
cwd = "../../.."

View file

@ -0,0 +1,6 @@
bin.name = "arc"
args = ["run", "--dry-run", "--auto-approve", "../../../test/styled.dot"]
[fs]
sandbox = false
cwd = "../../.."

View file

@ -0,0 +1,3 @@
...
[..]unexpected argument[..]
...

View file

@ -0,0 +1,4 @@
bin.name = "arc"
args = ["--no-dotenv", "doctor"]
status = "failed"
stderr-to-stdout = true

View file

@ -0,0 +1,6 @@
bin.name = "arc"
args = ["validate", "../../../test/branching.dot"]
[fs]
sandbox = false
cwd = "../../.."

View file

@ -0,0 +1,6 @@
bin.name = "arc"
args = ["validate", "../../../test/conditions.dot"]
[fs]
sandbox = false
cwd = "../../.."

View file

@ -0,0 +1,7 @@
bin.name = "arc"
args = ["validate", "../../../test/invalid.dot"]
status = "failed"
[fs]
sandbox = false
cwd = "../../.."

View file

@ -0,0 +1,6 @@
bin.name = "arc"
args = ["validate", "../../../test/legacy_tool.dot"]
[fs]
sandbox = false
cwd = "../../.."

View file

@ -0,0 +1,6 @@
bin.name = "arc"
args = ["validate", "../../../test/parallel.dot"]
[fs]
sandbox = false
cwd = "../../.."

View file

@ -0,0 +1,6 @@
bin.name = "arc"
args = ["validate", "../../../test/simple.dot"]
[fs]
sandbox = false
cwd = "../../.."

View file

@ -0,0 +1,6 @@
bin.name = "arc"
args = ["validate", "../../../test/styled.dot"]
[fs]
sandbox = false
cwd = "../../.."

View file

@ -1,21 +0,0 @@
---
source: lib/crates/arc-cli/tests/cli.rs
expression: stdout
---
Run an agentic coding session
Usage: arc exec [OPTIONS] <PROMPT>
Arguments:
<PROMPT> Task prompt
Options:
--provider <PROVIDER> LLM provider (anthropic, openai, gemini, kimi, zai, minimax, inception)
--model <MODEL> Model name (defaults per provider)
--permissions <PERMISSIONS> Permission level for tool execution [possible values: read-only, read-write, full]
--auto-approve Skip interactive prompts; deny tools outside permission level
--debug Print LLM request/response debug info to stderr
--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