use clap::builder::styling::{AnsiColor, Styles};
use clap::{Args as ClapArgs, Parser, Subcommand};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
const STYLES: Styles = Styles::styled()
.header(AnsiColor::Green.on_default().bold())
.usage(AnsiColor::Green.on_default().bold())
.literal(AnsiColor::Cyan.on_default())
.placeholder(AnsiColor::BrightBlack.on_default());
const AFTER_HELP: &str = "\
Examples:
roba \"explain the borrow checker in 3 bullets\" one-shot question
cat err.log | roba \"what's wrong here?\" pipe a log + ask about it
roba --attach 'src/**/*.rs' \"audit error handling\" attach files
roba -c -p \"now add a test for that\" continue the last session
roba --writable \"rename foo to bar in src/\" let claude edit files
Full flag detail, env vars, and roba.toml config: roba --help";
const AFTER_LONG_HELP: &str = "\
Examples -- for humans (interactive, rich TTY):
roba \"explain the borrow checker in 3 bullets\" one-shot question
cat err.log | roba \"what's wrong here?\" pipe a log + ask about it
roba --attach 'src/**/*.rs' \"audit error handling\" attach files as context
roba -e compose in $EDITOR
roba -c -p \"now add a test for that\" continue the last session
Examples -- for agents & scripts (deterministic, pipe-clean):
roba --json \"list 3 risks\" | jq -r '.result.result' structured output -> jq
roba -q \"one-line summary\" > out.txt answer only, no metadata
roba --no-retry \"...\"; echo \"exit=$?\" typed exit codes
roba --session ci-bot \"follow up\" resume a named session
roba --full-auto -C repo -f task.md fire an unattended worker
Exit codes: 0 ok (refusals included), 1 failure (incl. --max-budget-usd
cap hits), 2 auth, 3 budget, 4 timeout, 5 max-turns (recoverable --
finish the lifecycle).
Unattended workers (composing the primitives):
--full-auto -C <dir> -f <file> edit the current checkout in place; the
orchestrator owns the branch and PR (-C
chdirs first, so -f resolves inside <dir>)
...add --worktree run in an isolated git worktree, for
parallel same-repo workers that must not
share a branch
git worktree add; roba -C <dir> own the branch you'll PR from, for the
orchestrator-owns-the-branch case; roba's
--worktree makes a claude-managed worktree
instead
--detach -C <dir> -f <file> fire a run that survives the caller;
prints the session handle (re-attach:
roba show <id> --wait)
Environment variables:
Most long flags have a ROBA_<FLAG> override (uppercased, '-' -> '_'):
--model -> ROBA_MODEL; bool flags take a truthy value (--writable ->
ROBA_WRITABLE=1; falsy values are ignored -- env can only enable, never
disable). One-shot flags (-p, -f, -e, --code, --out, --fork, --pick,
--fresh, --show-permissions, -C) have no env form. Special cases:
ROBA_PROFILE=NAME apply a profile (like --profile NAME)
ROBA_VAR_<KEY>=VALUE set a template var (like --var KEY=VALUE)
ROBA_CONTINUE=1|ID truthy = continue most recent; else a session id
ROBA_WORKTREE=1|NAME truthy = fresh anonymous worktree; else a name
ROBA_RATES_FILE=PATH override the rates table (footer + roba cost)
NO_COLOR=1 disable color (help and answer rendering)
Precedence: CLI > ROBA_* env > profile > top-level keys > built-in default.
Configuration (roba.toml):
Most flags are also keys under [profile.NAME] or at the top level of a
roba.toml -- discovered by walking up from the cwd, plus
~/.config/roba.toml. Closer-to-cwd files win; a `default` profile
auto-applies. [alias.NAME] defines shortcut verbs; [session] binds
NAME = \"uuid\" handles for --session NAME. roba-config.sample.toml
(written by `roba profile init`) lists every valid key. See the
`roba profile` and `roba alias` subcommands.";
pub(crate) fn no_prompt_blurb() -> String {
format!("roba -- ad-hoc Claude from your shell. No prompt given.\n\n{AFTER_HELP}")
}
fn after_long_help_styled() -> clap::builder::StyledStr {
use std::fmt::Write as _;
let header = STYLES.get_header();
let literal = STYLES.get_literal();
let mut out = clap::builder::StyledStr::new();
for (i, line) in AFTER_LONG_HELP.lines().enumerate() {
if i > 0 {
out.push_str("\n");
}
if is_section_header(line) {
let _ = write!(out, "{}{line}{}", header.render(), header.render_reset());
} else if let Some((indent, command, rest)) = split_two_column(line) {
let _ = write!(
out,
"{indent}{}{command}{}{rest}",
literal.render(),
literal.render_reset()
);
} else {
out.push_str(line);
}
}
out
}
fn is_section_header(line: &str) -> bool {
!line.is_empty() && !line.starts_with(char::is_whitespace) && line.ends_with(':')
}
fn split_two_column(line: &str) -> Option<(&str, &str, &str)> {
let indent_len = line.len() - line.trim_start().len();
if indent_len == 0 {
return None;
}
let (indent, body) = line.split_at(indent_len);
let gap = body.find(" ")?;
if gap == 0 {
return None;
}
Some((indent, &body[..gap], &body[gap..]))
}
#[derive(Parser, Debug)]
#[command(
version,
about,
long_about = None,
after_help = AFTER_HELP,
after_long_help = after_long_help_styled(),
styles = STYLES,
)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<SubCommand>,
#[command(flatten)]
pub ask: AskArgs,
#[arg(short = 'C', long, value_name = "PATH", global = true)]
pub cwd: Option<PathBuf>,
}
#[derive(Subcommand, Debug)]
pub enum SubCommand {
History(HistoryArgs),
Last(LastArgs),
Profile {
#[command(subcommand)]
action: ProfileAction,
},
Cost(CostArgs),
Doctor(DoctorArgs),
Alias {
#[command(subcommand)]
action: AliasAction,
},
Config {
#[command(subcommand)]
cmd: ConfigCmd,
},
Worktree {
#[command(subcommand)]
cmd: WorktreeCmd,
},
Show(ShowArgs),
#[command(verbatim_doc_comment)]
Completions {
shell: clap_complete::Shell,
},
#[command(external_subcommand)]
External(Vec<String>),
}
#[derive(Subcommand, Debug)]
pub enum AliasAction {
List,
Show {
name: String,
},
Path,
Draft(AliasDraftArgs),
}
#[derive(ClapArgs, Debug)]
pub struct AliasDraftArgs {
pub description: String,
#[arg(long, num_args = 0..=1, value_name = "PATH")]
pub write: Option<Option<PathBuf>>,
#[arg(long, value_name = "NAME")]
pub model: Option<String>,
}
#[derive(Subcommand, Debug)]
pub enum WorktreeCmd {
List(WorktreeListArgs),
}
#[derive(ClapArgs, Debug)]
pub struct WorktreeListArgs {
#[arg(long)]
pub json: bool,
}
#[derive(ClapArgs, Debug)]
pub struct ShowArgs {
pub session_id: String,
#[arg(long)]
pub metrics: bool,
#[arg(long)]
pub json: bool,
#[arg(long)]
pub wait: bool,
#[arg(long, value_name = "SECS")]
pub timeout: Option<u64>,
}
#[derive(ClapArgs, Debug)]
pub struct CostArgs {
#[arg(long)]
pub by_project: bool,
#[arg(long, value_name = "SLUG", allow_hyphen_values = true)]
pub project: Option<String>,
#[arg(short = 'n', long, value_name = "N")]
pub limit: Option<usize>,
#[arg(long)]
pub json: bool,
#[arg(long, value_name = "PATH")]
pub rates_file: Option<PathBuf>,
#[arg(long)]
pub no_dollars: bool,
}
#[derive(ClapArgs, Debug)]
pub struct DoctorArgs {
#[arg(long)]
pub json: bool,
}
#[derive(Subcommand, Debug)]
pub enum ProfileAction {
List,
Show {
name: String,
},
Init {
#[arg(long)]
force: bool,
},
Path,
Active,
Draft(ProfileDraftArgs),
}
#[derive(ClapArgs, Debug)]
pub struct ProfileDraftArgs {
pub description: String,
#[arg(long, num_args = 0..=1, value_name = "PATH")]
pub write: Option<Option<PathBuf>>,
#[arg(long, value_name = "NAME")]
pub model: Option<String>,
}
#[derive(Subcommand, Debug)]
pub enum ConfigCmd {
Init(ConfigInitArgs),
Lint(ConfigLintArgs),
}
#[derive(ClapArgs, Debug)]
pub struct ConfigLintArgs {
pub path: Option<PathBuf>,
#[arg(long)]
pub json: bool,
}
#[derive(ClapArgs, Debug)]
pub struct ConfigInitArgs {
pub description: Option<String>,
#[arg(long, num_args = 0..=1, value_name = "PATH")]
pub write: Option<Option<PathBuf>>,
#[arg(long, value_name = "NAME")]
pub model: Option<String>,
}
#[derive(ClapArgs, Debug)]
pub struct LastArgs {
#[arg(short = 'n', long = "number", value_name = "N")]
pub number: Option<usize>,
#[arg(long = "type", value_enum, default_value_t = LastKind::Text)]
pub kind: LastKind,
#[arg(long, value_name = "SLUG", allow_hyphen_values = true)]
pub project: Option<String>,
#[arg(long, conflicts_with = "project")]
pub all_projects: bool,
}
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
pub enum LastKind {
Text,
Tools,
All,
}
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum EffortLevel {
Low,
Medium,
High,
Xhigh,
Max,
}
impl EffortLevel {
pub fn as_str(self) -> &'static str {
match self {
EffortLevel::Low => "low",
EffortLevel::Medium => "medium",
EffortLevel::High => "high",
EffortLevel::Xhigh => "xhigh",
EffortLevel::Max => "max",
}
}
}
impl LastKind {
pub fn label(self) -> &'static str {
match self {
LastKind::Text => "text answers",
LastKind::Tools => "tool calls",
LastKind::All => "items",
}
}
}
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
pub enum PermMode {
#[value(name = "acceptEdits", alias = "accept-edits")]
AcceptEdits,
Auto,
#[value(name = "bypassPermissions", alias = "bypass-permissions")]
BypassPermissions,
Default,
#[value(name = "dontAsk", alias = "dont-ask")]
DontAsk,
Plan,
}
#[derive(ClapArgs, Debug)]
pub struct HistoryArgs {
#[arg(short = 'n', long, value_name = "N")]
pub limit: Option<usize>,
#[arg(long, conflicts_with = "limit")]
pub all: bool,
#[arg(long, value_name = "SLUG", allow_hyphen_values = true)]
pub project: Option<String>,
#[arg(long, conflicts_with = "project")]
pub all_projects: bool,
#[arg(long)]
pub json: bool,
#[arg(long, value_name = "N", num_args = 0..=1, require_equals = false)]
pub paths: Option<Option<usize>>,
#[arg(long, value_name = "NAME")]
pub worktree: Option<String>,
}
#[derive(ClapArgs, Debug)]
pub struct AskArgs {
#[arg(conflicts_with_all = ["file", "editor"])]
pub prompt: Option<String>,
#[arg(
short = 'p',
long = "prompt",
value_name = "TEXT",
conflicts_with_all = ["prompt", "file", "editor"],
help_heading = "Prompt sources"
)]
pub prompt_flag: Option<String>,
#[arg(
short,
long,
value_name = "PATH",
conflicts_with = "editor",
help_heading = "Prompt sources"
)]
pub file: Option<PathBuf>,
#[arg(short = 'e', long = "editor", help_heading = "Prompt sources")]
pub editor: bool,
#[arg(long, value_name = "N", help_heading = "Prompt sources")]
pub editor_history: Option<usize>,
#[arg(long, value_name = "PATH", help_heading = "Composition")]
pub prepend: Vec<PathBuf>,
#[arg(long, value_name = "PATH", help_heading = "Composition")]
pub append: Vec<PathBuf>,
#[arg(long, value_name = "GLOB", help_heading = "Composition")]
pub attach: Vec<String>,
#[arg(long, help_heading = "Composition")]
pub git_diff: bool,
#[arg(
long,
value_name = "N",
num_args(0..=1),
default_missing_value = "5",
help_heading = "Composition"
)]
pub git_log: Option<usize>,
#[arg(long, help_heading = "Composition")]
pub git_status: bool,
#[arg(
long,
value_name = "K=V",
value_parser = parse_kv,
help_heading = "Composition"
)]
pub var: Vec<(String, String)>,
#[arg(short = 'q', long, help_heading = "Output")]
pub quiet: bool,
#[arg(long, help_heading = "Output")]
pub json: bool,
#[arg(long, value_name = "PATH", help_heading = "Output")]
pub json_schema: Option<String>,
#[arg(
long,
value_name = "LANG",
num_args(0..=1),
default_missing_value = "",
conflicts_with = "json",
help_heading = "Output"
)]
pub code: Option<String>,
#[arg(short = 'o', long, value_name = "PATH", help_heading = "Output")]
pub out: Option<PathBuf>,
#[arg(long, value_name = "PATH", help_heading = "Output")]
pub trace: Option<PathBuf>,
#[arg(
long,
conflicts_with_all = ["json", "code", "out"],
help_heading = "Output"
)]
pub stream: bool,
#[arg(long, help_heading = "Output")]
pub show_thinking: bool,
#[arg(long, help_heading = "Output")]
pub echo: bool,
#[arg(long, help_heading = "Output")]
pub plain: bool,
#[arg(long, value_name = "PATH", help_heading = "Output")]
pub rates_file: Option<PathBuf>,
#[arg(long, help_heading = "Output")]
pub no_dollars: bool,
#[arg(long, help_heading = "Failure modes")]
pub no_retry: bool,
#[arg(long, value_name = "N", help_heading = "Limits")]
pub max_turns: Option<u32>,
#[arg(long, value_name = "USD", help_heading = "Limits")]
pub max_budget_usd: Option<f64>,
#[arg(long, help_heading = "Mode")]
pub bare: bool,
#[arg(long, value_name = "MODEL", help_heading = "Model")]
pub model: Option<String>,
#[arg(long, value_name = "MODEL", help_heading = "Model")]
pub fallback_model: Option<String>,
#[arg(long, value_name = "LEVEL", value_enum, help_heading = "Model")]
pub effort: Option<EffortLevel>,
#[arg(long, value_name = "TEXT", help_heading = "System prompt")]
pub system_prompt: Option<String>,
#[arg(long, value_name = "TEXT", help_heading = "System prompt")]
pub append_system_prompt: Option<String>,
#[arg(long, help_heading = "System prompt")]
pub no_agent_notice: bool,
#[arg(long, value_name = "TEXT", help_heading = "System prompt")]
pub agent_notice: Option<String>,
#[arg(
short = 'c',
long = "continue",
num_args = 0..=1,
value_name = "ID",
help_heading = "Sessions"
)]
pub continue_session: Option<Option<String>>,
#[arg(long, requires = "continue_session", help_heading = "Sessions")]
pub fork: bool,
#[arg(long, conflicts_with = "continue_session", help_heading = "Sessions")]
pub pick: bool,
#[arg(
long,
conflicts_with_all = ["continue_session", "pick"],
help_heading = "Sessions"
)]
pub fresh: bool,
#[arg(
long,
value_name = "NAME",
conflicts_with_all = ["continue_session", "pick", "fresh"],
help_heading = "Sessions"
)]
pub session: Option<String>,
#[arg(
long,
value_name = "UUID",
value_parser = parse_session_id,
conflicts_with_all = ["continue_session", "fork", "pick", "session"],
help_heading = "Sessions"
)]
pub session_id: Option<String>,
#[arg(
long,
conflicts_with_all = ["json", "stream", "code", "show_thinking", "editor", "pick"],
help_heading = "Sessions"
)]
pub detach: bool,
#[arg(
short = 'w',
long,
value_name = "NAME",
num_args(0..=1),
help_heading = "Sessions"
)]
pub worktree: Option<Option<String>>,
#[arg(long, value_name = "NAME", help_heading = "Sessions")]
pub agent: Option<String>,
#[arg(long, help_heading = "Sessions")]
pub no_session_persistence: bool,
#[arg(long, value_name = "MODE", value_enum, help_heading = "Permissions")]
pub permission_mode: Option<PermMode>,
#[arg(long, conflicts_with = "full_auto", help_heading = "Permissions")]
pub readonly: bool,
#[arg(long, conflicts_with = "full_auto", help_heading = "Permissions")]
pub writable: bool,
#[arg(long, help_heading = "Permissions")]
pub full_auto: bool,
#[arg(long = "allow-tool", value_name = "TOOL", help_heading = "Permissions")]
pub allow_tool: Vec<String>,
#[arg(long = "deny-tool", value_name = "TOOL", help_heading = "Permissions")]
pub deny_tool: Vec<String>,
#[arg(long = "add-dir", value_name = "DIR", help_heading = "Permissions")]
pub add_dir: Vec<String>,
#[arg(long, help_heading = "Permissions")]
pub show_permissions: bool,
#[arg(long, help_heading = "Permissions")]
pub no_agent_check: bool,
#[clap(skip)]
pub readonly_source: Option<String>,
#[clap(skip)]
pub writable_source: Option<String>,
#[clap(skip)]
pub full_auto_source: Option<String>,
#[clap(skip)]
pub allow_tool_sources: Vec<String>,
#[clap(skip)]
pub deny_tool_sources: Vec<String>,
#[clap(skip)]
pub add_dir_sources: Vec<String>,
#[clap(skip)]
pub permission_mode_source: Option<String>,
#[arg(long, value_name = "FILE", help_heading = "MCP")]
pub mcp_config: Vec<String>,
#[arg(long, help_heading = "MCP")]
pub strict_mcp_config: bool,
#[arg(long, value_name = "NAME", help_heading = "Profiles")]
pub profile: Option<String>,
#[arg(long, help_heading = "Profiles")]
pub no_default_profile: bool,
}
pub fn parse_session_id(s: &str) -> std::result::Result<String, String> {
uuid::Uuid::try_parse(s)
.map(|_| s.to_string())
.map_err(|_| format!("not a valid UUID: `{s}`"))
}
pub fn parse_kv(s: &str) -> std::result::Result<(String, String), String> {
let (k, v) = s
.split_once('=')
.ok_or_else(|| format!("expected K=V, got `{s}`"))?;
if k.is_empty() {
return Err(format!("empty key in `{s}`"));
}
Ok((k.to_string(), v.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_kv_splits_on_first_equals() {
assert_eq!(
parse_kv("foo=bar"),
Ok(("foo".to_string(), "bar".to_string()))
);
}
#[test]
fn parse_kv_keeps_equals_in_value() {
assert_eq!(
parse_kv("x=a=b=c"),
Ok(("x".to_string(), "a=b=c".to_string()))
);
}
#[test]
fn parse_kv_rejects_no_equals() {
assert!(parse_kv("foo").is_err());
}
#[test]
fn parse_kv_rejects_empty_key() {
assert!(parse_kv("=bar").is_err());
}
#[test]
fn parse_kv_accepts_empty_value() {
assert_eq!(parse_kv("k="), Ok(("k".to_string(), String::new())));
}
#[test]
fn session_id_valid_uuid_parses() {
let uuid = "550e8400-e29b-41d4-a716-446655440000";
let cli = Cli::try_parse_from(["roba", "--session-id", uuid, "hi"]).unwrap();
assert_eq!(cli.ask.session_id.as_deref(), Some(uuid));
}
#[test]
fn session_id_uppercase_uuid_parses() {
let uuid = "550E8400-E29B-41D4-A716-446655440000";
let cli = Cli::try_parse_from(["roba", "--session-id", uuid, "hi"]).unwrap();
assert_eq!(cli.ask.session_id.as_deref(), Some(uuid));
}
#[test]
fn session_id_rejects_non_uuid_at_parse_time() {
let err = Cli::try_parse_from(["roba", "--session-id", "not-a-uuid", "hi"]).unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::ValueValidation);
}
#[test]
fn out_and_json_compose() {
use clap::Parser;
let cli =
Cli::try_parse_from(["roba", "ask thing", "--out", "result.txt", "--json"]).unwrap();
assert_eq!(
cli.ask.out.as_deref(),
Some(std::path::Path::new("result.txt"))
);
assert!(cli.ask.json);
}
#[test]
fn doctor_parses_without_json() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "doctor"]).unwrap();
assert!(matches!(
cli.command,
Some(SubCommand::Doctor(DoctorArgs { json: false }))
));
}
#[test]
fn doctor_json_flag_parses() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "doctor", "--json"]).unwrap();
assert!(matches!(
cli.command,
Some(SubCommand::Doctor(DoctorArgs { json: true }))
));
}
#[test]
fn config_init_parses_without_description() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "config", "init"]).unwrap();
let Some(SubCommand::Config {
cmd: ConfigCmd::Init(args),
}) = cli.command
else {
panic!("expected config init");
};
assert!(args.description.is_none());
assert!(args.write.is_none());
}
#[test]
fn config_init_parses_with_description() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "config", "init", "focus on PR review"]).unwrap();
let Some(SubCommand::Config {
cmd: ConfigCmd::Init(args),
}) = cli.command
else {
panic!("expected config init");
};
assert_eq!(args.description.as_deref(), Some("focus on PR review"));
}
#[test]
fn config_init_write_bare_is_some_none() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "config", "init", "--write"]).unwrap();
let Some(SubCommand::Config {
cmd: ConfigCmd::Init(args),
}) = cli.command
else {
panic!("expected config init");
};
assert_eq!(args.write, Some(None));
}
#[test]
fn config_init_write_with_path() {
use clap::Parser;
let cli =
Cli::try_parse_from(["roba", "config", "init", "--write", "custom.toml"]).unwrap();
let Some(SubCommand::Config {
cmd: ConfigCmd::Init(args),
}) = cli.command
else {
panic!("expected config init");
};
assert_eq!(
args.write,
Some(Some(std::path::PathBuf::from("custom.toml")))
);
}
#[test]
fn config_lint_no_path_no_json() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "config", "lint"]).unwrap();
let Some(SubCommand::Config {
cmd: ConfigCmd::Lint(args),
}) = cli.command
else {
panic!("expected config lint");
};
assert!(args.path.is_none());
assert!(!args.json);
}
#[test]
fn config_lint_with_path() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "config", "lint", "some/roba.toml"]).unwrap();
let Some(SubCommand::Config {
cmd: ConfigCmd::Lint(args),
}) = cli.command
else {
panic!("expected config lint");
};
assert_eq!(args.path, Some(std::path::PathBuf::from("some/roba.toml")));
assert!(!args.json);
}
#[test]
fn config_lint_json_flag() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "config", "lint", "--json"]).unwrap();
let Some(SubCommand::Config {
cmd: ConfigCmd::Lint(args),
}) = cli.command
else {
panic!("expected config lint");
};
assert!(args.json);
assert!(args.path.is_none());
}
#[test]
fn worktree_missing_is_none() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "do thing"]).unwrap();
assert!(cli.ask.worktree.is_none());
}
#[test]
fn worktree_short_alone_is_presence() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "-w", "-p", "do thing"]).unwrap();
assert_eq!(cli.ask.worktree, Some(None));
assert_eq!(cli.ask.prompt_flag.as_deref(), Some("do thing"));
}
#[test]
fn worktree_long_alone_is_presence() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "--worktree", "-p", "do thing"]).unwrap();
assert_eq!(cli.ask.worktree, Some(None));
assert_eq!(cli.ask.prompt_flag.as_deref(), Some("do thing"));
}
#[test]
fn worktree_short_equals_name() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "-w=mybranch", "do thing"]).unwrap();
assert_eq!(cli.ask.worktree, Some(Some("mybranch".to_string())));
assert_eq!(cli.ask.prompt.as_deref(), Some("do thing"));
}
#[test]
fn worktree_long_equals_name() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "--worktree=mybranch", "do thing"]).unwrap();
assert_eq!(cli.ask.worktree, Some(Some("mybranch".to_string())));
}
#[test]
fn agent_parses_with_name() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "--agent", "reviewer", "prompt"]).unwrap();
assert_eq!(cli.ask.agent.as_deref(), Some("reviewer"));
assert_eq!(cli.ask.prompt.as_deref(), Some("prompt"));
}
#[test]
fn agent_omitted_is_none() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "prompt"]).unwrap();
assert!(cli.ask.agent.is_none());
}
#[test]
fn no_retry_parses_alone() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "--no-retry", "prompt"]).unwrap();
assert!(cli.ask.no_retry);
assert_eq!(cli.ask.prompt.as_deref(), Some("prompt"));
}
#[test]
fn no_retry_conflicts_with_nothing() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "--no-retry", "--quiet", "prompt"]).unwrap();
assert!(cli.ask.no_retry);
assert!(cli.ask.quiet);
let cli = Cli::try_parse_from(["roba", "--no-retry", "--json", "prompt"]).unwrap();
assert!(cli.ask.no_retry);
assert!(cli.ask.json);
}
#[test]
fn permission_mode_parses_all_variants() {
use clap::Parser;
for mode in &[
"acceptEdits",
"auto",
"bypassPermissions",
"default",
"dontAsk",
"plan",
"accept-edits",
"bypass-permissions",
"dont-ask",
] {
let cli = Cli::try_parse_from(["roba", "--permission-mode", mode, "prompt"])
.unwrap_or_else(|e| panic!("--permission-mode {mode} should parse: {e}"));
assert!(
cli.ask.permission_mode.is_some(),
"--permission-mode {mode} should be Some"
);
}
}
#[test]
fn permission_mode_coexists_with_writable() {
use clap::Parser;
let cli =
Cli::try_parse_from(["roba", "--writable", "--permission-mode", "plan", "prompt"])
.unwrap();
assert!(cli.ask.writable);
assert!(cli.ask.permission_mode.is_some());
}
#[test]
fn permission_mode_coexists_with_readonly() {
use clap::Parser;
let cli = Cli::try_parse_from([
"roba",
"--readonly",
"--permission-mode",
"dont-ask",
"prompt",
])
.unwrap();
assert!(cli.ask.readonly);
assert!(cli.ask.permission_mode.is_some());
}
#[test]
fn permission_mode_coexists_with_full_auto() {
use clap::Parser;
let cli =
Cli::try_parse_from(["roba", "--full-auto", "--permission-mode", "plan", "prompt"])
.unwrap();
assert!(cli.ask.full_auto);
assert!(cli.ask.permission_mode.is_some());
}
#[test]
fn bare_parses_alone() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "--bare", "prompt"]).unwrap();
assert!(cli.ask.bare);
}
#[test]
fn bare_is_orthogonal() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "--bare", "--quiet", "prompt"]).unwrap();
assert!(cli.ask.bare);
assert!(cli.ask.quiet);
let cli = Cli::try_parse_from(["roba", "--bare", "--json", "prompt"]).unwrap();
assert!(cli.ask.bare);
assert!(cli.ask.json);
}
#[test]
fn trace_flag_parses() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "--trace", "/tmp/x.jsonl", "prompt"]).unwrap();
assert_eq!(
cli.ask.trace.as_deref(),
Some(std::path::Path::new("/tmp/x.jsonl"))
);
assert_eq!(cli.ask.prompt.as_deref(), Some("prompt"));
}
#[test]
fn trace_omitted_is_none() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "prompt"]).unwrap();
assert!(cli.ask.trace.is_none());
}
#[test]
fn trace_conflicts_with_nothing() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "--trace", "/tmp/x", "--json", "prompt"]).unwrap();
assert_eq!(
cli.ask.trace.as_deref(),
Some(std::path::Path::new("/tmp/x"))
);
assert!(cli.ask.json);
let cli = Cli::try_parse_from(["roba", "--trace", "/tmp/x", "--quiet", "prompt"]).unwrap();
assert!(cli.ask.trace.is_some());
assert!(cli.ask.quiet);
let cli =
Cli::try_parse_from(["roba", "--trace", "/tmp/x", "--out", "r.txt", "prompt"]).unwrap();
assert!(cli.ask.trace.is_some());
assert_eq!(cli.ask.out.as_deref(), Some(std::path::Path::new("r.txt")));
let cli = Cli::try_parse_from(["roba", "--trace", "/tmp/x", "--stream", "prompt"]).unwrap();
assert!(cli.ask.trace.is_some());
assert!(cli.ask.stream);
}
#[test]
fn continue_parses_bare() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "-c", "-p", "prompt"]).unwrap();
assert_eq!(cli.ask.continue_session, Some(None));
assert_eq!(cli.ask.prompt_flag.as_deref(), Some("prompt"));
}
#[test]
fn continue_parses_with_id() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "-c=abc123", "prompt"]).unwrap();
assert_eq!(cli.ask.continue_session, Some(Some("abc123".to_string())));
assert_eq!(cli.ask.prompt.as_deref(), Some("prompt"));
}
#[test]
fn continue_long_parses_with_id() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "--continue=abc123", "prompt"]).unwrap();
assert_eq!(cli.ask.continue_session, Some(Some("abc123".to_string())));
}
#[test]
fn continue_without_equals_consumes_next_arg_as_id() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "-c", "prompt"]).unwrap();
assert_eq!(cli.ask.continue_session, Some(Some("prompt".to_string())));
assert!(cli.ask.prompt.is_none());
}
#[test]
fn continue_missing_is_none() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "prompt"]).unwrap();
assert!(cli.ask.continue_session.is_none());
}
#[test]
fn fork_requires_continue_at_parse_time() {
use clap::Parser;
let err = Cli::try_parse_from(["roba", "--fork", "prompt"]).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("continue") || msg.contains("required"),
"expected a requires error mentioning continue, got: {msg}"
);
}
#[test]
fn fork_with_specific_id_parses() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "-c=abc123", "--fork", "prompt"]).unwrap();
assert_eq!(cli.ask.continue_session, Some(Some("abc123".to_string())));
assert!(cli.ask.fork);
}
#[test]
fn pick_conflicts_with_continue() {
use clap::Parser;
assert!(Cli::try_parse_from(["roba", "--pick", "-c", "prompt"]).is_err());
}
#[test]
fn session_parses_with_name() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "--session", "meta", "-p", "hi"]).unwrap();
assert_eq!(cli.ask.session.as_deref(), Some("meta"));
assert_eq!(cli.ask.prompt_flag.as_deref(), Some("hi"));
}
#[test]
fn session_conflicts_with_continue() {
use clap::Parser;
assert!(Cli::try_parse_from(["roba", "--session", "meta", "-c", "-p", "hi"]).is_err());
}
#[test]
fn session_conflicts_with_pick_and_fresh() {
use clap::Parser;
assert!(Cli::try_parse_from(["roba", "--session", "meta", "--pick"]).is_err());
assert!(Cli::try_parse_from(["roba", "--session", "meta", "--fresh", "-p", "hi"]).is_err());
}
#[test]
fn fresh_conflicts_with_continue() {
use clap::Parser;
assert!(Cli::try_parse_from(["roba", "--fresh", "-c", "prompt"]).is_err());
}
#[test]
fn session_id_parses_alone() {
use clap::Parser;
let cli = Cli::try_parse_from([
"roba",
"--session-id",
"11111111-1111-4111-8111-111111111111",
"-p",
"hi",
])
.unwrap();
assert_eq!(
cli.ask.session_id.as_deref(),
Some("11111111-1111-4111-8111-111111111111")
);
assert_eq!(cli.ask.prompt_flag.as_deref(), Some("hi"));
}
#[test]
fn session_id_conflicts_with_resume() {
use clap::Parser;
assert!(Cli::try_parse_from(["roba", "--session-id", "x", "-c=y", "-p", "hi"]).is_err());
}
#[test]
fn session_id_conflicts_with_pick_and_session() {
use clap::Parser;
assert!(Cli::try_parse_from(["roba", "--session-id", "x", "--pick"]).is_err());
assert!(
Cli::try_parse_from(["roba", "--session-id", "x", "--session", "meta", "-p", "hi"])
.is_err()
);
}
#[test]
fn json_schema_parses() {
use clap::Parser;
let cli =
Cli::try_parse_from(["roba", "--json-schema", "/tmp/schema.json", "-p", "hi"]).unwrap();
assert_eq!(cli.ask.json_schema.as_deref(), Some("/tmp/schema.json"));
}
#[test]
fn session_id_composes_with_fresh() {
use clap::Parser;
let uuid = "550e8400-e29b-41d4-a716-446655440000";
let cli =
Cli::try_parse_from(["roba", "--session-id", uuid, "--fresh", "-p", "hi"]).unwrap();
assert_eq!(cli.ask.session_id.as_deref(), Some(uuid));
assert!(cli.ask.fresh);
}
#[test]
fn worktree_long_space_name_attaches_value() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "--worktree", "mybranch"]).unwrap();
assert_eq!(cli.ask.worktree, Some(Some("mybranch".to_string())));
assert!(cli.ask.prompt.is_none());
}
#[test]
fn external_subcommand_captures_unknown_leading_word() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "review", "42", "--readonly"]).unwrap();
assert_eq!(cli.ask.prompt.as_deref(), Some("review"));
match cli.command {
Some(SubCommand::External(rest)) => {
assert_eq!(rest, vec!["42".to_string(), "--readonly".to_string()]);
}
other => panic!("expected External, got {other:?}"),
}
}
#[test]
fn prompt_flag_parses() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "-p", "hello"]).unwrap();
assert_eq!(cli.ask.prompt_flag.as_deref(), Some("hello"));
assert!(cli.ask.prompt.is_none());
}
#[test]
fn prompt_flag_long_form_parses() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "--prompt", "hello"]).unwrap();
assert_eq!(cli.ask.prompt_flag.as_deref(), Some("hello"));
assert!(cli.ask.prompt.is_none());
}
#[test]
fn prompt_flag_conflicts_with_positional() {
use clap::Parser;
assert!(Cli::try_parse_from(["roba", "-p", "x", "positional"]).is_err());
}
#[test]
fn prompt_flag_conflicts_with_file() {
use clap::Parser;
assert!(Cli::try_parse_from(["roba", "-p", "x", "-f", "task.md"]).is_err());
}
#[test]
fn prompt_flag_conflicts_with_editor() {
use clap::Parser;
assert!(Cli::try_parse_from(["roba", "-p", "x", "-e"]).is_err());
}
#[test]
fn prompt_flag_alone_still_parses() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "-p", "x"]).unwrap();
assert_eq!(cli.ask.prompt_flag.as_deref(), Some("x"));
}
#[test]
fn continue_with_space_value_consumes_id() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "-c", "abc123"]).unwrap();
assert_eq!(cli.ask.continue_session, Some(Some("abc123".to_string())));
assert!(cli.ask.prompt.is_none());
}
#[test]
fn worktree_with_space_value_and_prompt_flag() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "-w", "mybranch", "-p", "the prompt"]).unwrap();
assert_eq!(cli.ask.worktree, Some(Some("mybranch".to_string())));
assert_eq!(cli.ask.prompt_flag.as_deref(), Some("the prompt"));
}
#[test]
fn continue_bare_then_p_flag_for_prompt() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "-c", "-p", "follow up"]).unwrap();
assert_eq!(cli.ask.continue_session, Some(None));
assert_eq!(cli.ask.prompt_flag.as_deref(), Some("follow up"));
}
#[test]
fn max_turns_parses_valid_value() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "--max-turns", "5", "prompt"]).unwrap();
assert_eq!(cli.ask.max_turns, Some(5));
assert_eq!(cli.ask.prompt.as_deref(), Some("prompt"));
}
#[test]
fn max_turns_rejects_non_numeric() {
use clap::Parser;
assert!(Cli::try_parse_from(["roba", "--max-turns", "abc", "prompt"]).is_err());
}
#[test]
fn max_budget_usd_parses_valid_value() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "--max-budget-usd", "10.5", "prompt"]).unwrap();
assert_eq!(cli.ask.max_budget_usd, Some(10.5));
}
#[test]
fn max_budget_usd_rejects_non_numeric() {
use clap::Parser;
assert!(Cli::try_parse_from(["roba", "--max-budget-usd", "lots", "prompt"]).is_err());
}
#[test]
fn limits_flags_compose() {
use clap::Parser;
let cli = Cli::try_parse_from([
"roba",
"--max-turns",
"5",
"--max-budget-usd",
"10",
"prompt",
])
.unwrap();
assert_eq!(cli.ask.max_turns, Some(5));
assert_eq!(cli.ask.max_budget_usd, Some(10.0));
}
#[test]
fn mcp_config_collects_repeated_values() {
use clap::Parser;
let cli = Cli::try_parse_from([
"roba",
"--mcp-config",
"a.json",
"--mcp-config",
"b.json",
"prompt",
])
.unwrap();
assert_eq!(
cli.ask.mcp_config,
vec!["a.json".to_string(), "b.json".to_string()]
);
assert_eq!(cli.ask.prompt.as_deref(), Some("prompt"));
}
#[test]
fn strict_mcp_config_parses() {
use clap::Parser;
let cli = Cli::try_parse_from([
"roba",
"--mcp-config",
"a.json",
"--strict-mcp-config",
"prompt",
])
.unwrap();
assert_eq!(cli.ask.mcp_config, vec!["a.json".to_string()]);
assert!(cli.ask.strict_mcp_config);
}
#[test]
fn mcp_config_omitted_is_empty() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "prompt"]).unwrap();
assert!(cli.ask.mcp_config.is_empty());
assert!(!cli.ask.strict_mcp_config);
}
#[test]
fn add_dir_collects_repeated_values() {
use clap::Parser;
let cli = Cli::try_parse_from([
"roba",
"--add-dir",
"/extra/a",
"--add-dir",
"/extra/b",
"prompt",
])
.unwrap();
assert_eq!(
cli.ask.add_dir,
vec!["/extra/a".to_string(), "/extra/b".to_string()]
);
assert_eq!(cli.ask.prompt.as_deref(), Some("prompt"));
}
#[test]
fn add_dir_omitted_is_empty() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "prompt"]).unwrap();
assert!(cli.ask.add_dir.is_empty());
}
#[test]
fn fallback_model_parses() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "--fallback-model", "haiku", "prompt"]).unwrap();
assert_eq!(cli.ask.fallback_model.as_deref(), Some("haiku"));
assert_eq!(cli.ask.prompt.as_deref(), Some("prompt"));
}
#[test]
fn no_session_persistence_parses() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "--no-session-persistence", "prompt"]).unwrap();
assert!(cli.ask.no_session_persistence);
assert_eq!(cli.ask.prompt.as_deref(), Some("prompt"));
}
#[test]
fn medtier_flags_compose() {
use clap::Parser;
let cli = Cli::try_parse_from([
"roba",
"--add-dir",
"/extra",
"--fallback-model",
"sonnet",
"--no-session-persistence",
"prompt",
])
.unwrap();
assert_eq!(cli.ask.add_dir, vec!["/extra".to_string()]);
assert_eq!(cli.ask.fallback_model.as_deref(), Some("sonnet"));
assert!(cli.ask.no_session_persistence);
}
#[test]
fn worktree_list_parses() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "worktree", "list"]).unwrap();
match cli.command {
Some(SubCommand::Worktree {
cmd: WorktreeCmd::List(args),
}) => assert!(!args.json),
other => panic!("expected worktree list, got {other:?}"),
}
}
#[test]
fn worktree_list_json_parses() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "worktree", "list", "--json"]).unwrap();
match cli.command {
Some(SubCommand::Worktree {
cmd: WorktreeCmd::List(args),
}) => assert!(args.json),
other => panic!("expected worktree list --json, got {other:?}"),
}
}
#[test]
fn worktree_list_honors_global_cwd() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "worktree", "list", "-C", "/some/repo"]).unwrap();
assert_eq!(cli.cwd.as_deref(), Some(std::path::Path::new("/some/repo")));
}
#[test]
fn show_parses_session_id() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "show", "abc-123"]).unwrap();
match cli.command {
Some(SubCommand::Show(args)) => {
assert_eq!(args.session_id, "abc-123");
assert!(!args.metrics);
assert!(!args.json);
}
other => panic!("expected show, got {other:?}"),
}
}
#[test]
fn show_parses_metrics_flag() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "show", "abc-123", "--metrics"]).unwrap();
match cli.command {
Some(SubCommand::Show(args)) => {
assert_eq!(args.session_id, "abc-123");
assert!(args.metrics);
}
other => panic!("expected show --metrics, got {other:?}"),
}
}
#[test]
fn show_parses_json_flag() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "show", "abc-123", "--json", "--metrics"]).unwrap();
match cli.command {
Some(SubCommand::Show(args)) => {
assert!(args.json);
assert!(args.metrics);
}
other => panic!("expected show --json --metrics, got {other:?}"),
}
}
#[test]
fn show_parses_wait_flag() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "show", "abc-123", "--wait"]).unwrap();
match cli.command {
Some(SubCommand::Show(args)) => {
assert!(args.wait);
assert_eq!(args.timeout, None);
}
other => panic!("expected show --wait, got {other:?}"),
}
}
#[test]
fn show_parses_wait_with_timeout() {
use clap::Parser;
let cli =
Cli::try_parse_from(["roba", "show", "abc-123", "--wait", "--timeout", "30"]).unwrap();
match cli.command {
Some(SubCommand::Show(args)) => {
assert!(args.wait);
assert_eq!(args.timeout, Some(30));
}
other => panic!("expected show --wait --timeout 30, got {other:?}"),
}
}
#[test]
fn show_honors_global_cwd() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "show", "abc-123", "-C", "/some/repo"]).unwrap();
assert_eq!(cli.cwd.as_deref(), Some(std::path::Path::new("/some/repo")));
}
#[test]
fn history_worktree_filter_parses() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "history", "--worktree", "agent-abc123"]).unwrap();
match cli.command {
Some(SubCommand::History(args)) => {
assert_eq!(args.worktree.as_deref(), Some("agent-abc123"));
}
other => panic!("expected history --worktree, got {other:?}"),
}
}
#[test]
fn single_bare_word_is_prompt_not_external() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "commit-msg"]).unwrap();
assert!(cli.command.is_none());
assert_eq!(cli.ask.prompt.as_deref(), Some("commit-msg"));
}
#[test]
fn alias_draft_parses_description() {
use clap::Parser;
let cli =
Cli::try_parse_from(["roba", "alias", "draft", "a read-only review verb"]).unwrap();
match cli.command {
Some(SubCommand::Alias {
action: AliasAction::Draft(args),
}) => {
assert_eq!(args.description, "a read-only review verb");
assert!(args.write.is_none());
assert!(args.model.is_none());
}
other => panic!("expected alias draft, got {other:?}"),
}
}
#[test]
fn alias_draft_write_without_path() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "alias", "draft", "desc", "--write"]).unwrap();
match cli.command {
Some(SubCommand::Alias {
action: AliasAction::Draft(args),
}) => assert!(matches!(args.write, Some(None))),
other => panic!("expected alias draft, got {other:?}"),
}
}
#[test]
fn alias_draft_write_with_path() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "alias", "draft", "desc", "--write", "/tmp/r.toml"])
.unwrap();
match cli.command {
Some(SubCommand::Alias {
action: AliasAction::Draft(args),
}) => assert_eq!(args.write, Some(Some(PathBuf::from("/tmp/r.toml")))),
other => panic!("expected alias draft, got {other:?}"),
}
}
#[test]
fn alias_draft_accepts_model() {
use clap::Parser;
let cli = Cli::try_parse_from([
"roba",
"alias",
"draft",
"desc",
"--model",
"claude-haiku-4-5",
])
.unwrap();
match cli.command {
Some(SubCommand::Alias {
action: AliasAction::Draft(args),
}) => assert_eq!(args.model.as_deref(), Some("claude-haiku-4-5")),
other => panic!("expected alias draft, got {other:?}"),
}
}
#[test]
fn profile_draft_parses_description() {
use clap::Parser;
let cli =
Cli::try_parse_from(["roba", "profile", "draft", "a long-running worker"]).unwrap();
match cli.command {
Some(SubCommand::Profile {
action: ProfileAction::Draft(args),
}) => {
assert_eq!(args.description, "a long-running worker");
assert!(args.write.is_none());
assert!(args.model.is_none());
}
other => panic!("expected profile draft, got {other:?}"),
}
}
#[test]
fn profile_draft_write_without_path() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "profile", "draft", "desc", "--write"]).unwrap();
match cli.command {
Some(SubCommand::Profile {
action: ProfileAction::Draft(args),
}) => assert!(matches!(args.write, Some(None))),
other => panic!("expected profile draft, got {other:?}"),
}
}
#[test]
fn profile_draft_write_with_path() {
use clap::Parser;
let cli =
Cli::try_parse_from(["roba", "profile", "draft", "desc", "--write", "/tmp/p.toml"])
.unwrap();
match cli.command {
Some(SubCommand::Profile {
action: ProfileAction::Draft(args),
}) => assert_eq!(args.write, Some(Some(PathBuf::from("/tmp/p.toml")))),
other => panic!("expected profile draft, got {other:?}"),
}
}
#[test]
fn profile_draft_accepts_model() {
use clap::Parser;
let cli = Cli::try_parse_from([
"roba",
"profile",
"draft",
"desc",
"--model",
"claude-haiku-4-5",
])
.unwrap();
match cli.command {
Some(SubCommand::Profile {
action: ProfileAction::Draft(args),
}) => assert_eq!(args.model.as_deref(), Some("claude-haiku-4-5")),
other => panic!("expected profile draft, got {other:?}"),
}
}
#[test]
fn detach_parses_alone() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "--detach", "prompt"]).unwrap();
assert!(cli.ask.detach);
assert_eq!(cli.ask.prompt.as_deref(), Some("prompt"));
}
#[test]
fn detach_omitted_is_false() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "prompt"]).unwrap();
assert!(!cli.ask.detach);
}
#[test]
fn detach_composes_with_session_id_and_out_and_trace() {
use clap::Parser;
let cli = Cli::try_parse_from([
"roba",
"--detach",
"--session-id",
"11111111-1111-4111-8111-111111111111",
"--out",
"/tmp/a.txt",
"--trace",
"/tmp/t.jsonl",
"prompt",
])
.unwrap();
assert!(cli.ask.detach);
assert!(cli.ask.session_id.is_some());
assert_eq!(
cli.ask.out.as_deref(),
Some(std::path::Path::new("/tmp/a.txt"))
);
assert!(cli.ask.trace.is_some());
}
#[test]
fn no_agent_notice_parses_alone() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "--no-agent-notice", "prompt"]).unwrap();
assert!(cli.ask.no_agent_notice);
assert!(cli.ask.agent_notice.is_none());
}
#[test]
fn agent_notice_parses_with_text() {
use clap::Parser;
let cli = Cli::try_parse_from(["roba", "--agent-notice", "custom text", "prompt"]).unwrap();
assert_eq!(cli.ask.agent_notice.as_deref(), Some("custom text"));
assert!(!cli.ask.no_agent_notice);
}
#[test]
fn detach_conflicts_with_attachment_flags() {
use clap::Parser;
for conflicting in [
vec!["--json"],
vec!["--stream"],
vec!["--code"],
vec!["--show-thinking"],
vec!["--editor"],
vec!["--pick"],
] {
let mut argv = vec!["roba", "--detach"];
argv.extend(conflicting.iter().copied());
argv.push("prompt");
assert!(
Cli::try_parse_from(&argv).is_err(),
"expected --detach to conflict with {conflicting:?}"
);
}
}
}