use super::*;
use crate::cli::{Cli, Commands};
use crate::agent::classifier::TaskCategory;
use crate::types::{TaskBudget, TaskDifficulty, TaskEgress, TaskRigor, TaskUrgency};
use clap::Parser;
fn cli_args(flags: &[&str]) -> run_args::RunArgs {
let cli = Cli::try_parse_from(["aid", "run", "qwen", "say hi"].into_iter()
.chain(flags.iter().copied())).expect("parse run flags");
let Some(Commands::Run(args)) = cli.command else { panic!("run command") };
args
}
fn parsed_args(flags: &[&str]) -> cmd::run::RunArgs {
run_args(cli_args(flags))
}
fn configured_args() -> cmd::run::RunArgs {
parsed_args(&[
"--repo", "repo", "--repo-root", "root", "--dir", "checkout",
"--output", "answer.txt", "--result-file", "report.md", "--model", "chosen-model",
"--difficulty", "complex", "--budget", "cheap", "--urgency", "urgent",
"--rigor", "critical", "--egress", "local", "--kind", "debugging",
"--worktree", "fix/task", "--team", "dev", "--group", "wg-test", "--verify",
"--iterate", "2", "--eval", "evaluate", "--eval-feedback-template", "feedback {output}",
"--judge", "--peer-review", "claude", "--retry", "3",
"--context", "one.rs", "two.rs", "--checklist", "first", "second",
"--scope", "src", "tests", "--remote-build", "selected-box", "--no-backup",
"--context-from", "t-one", "t-two", "--skill", "implementer", "auditor",
"--template", "template", "--on-done", "finish", "--cascade", "codex,claude",
"--hook", "done:hook", "--bg", "--dry-run", "--read-only", "--best-of", "2",
"--metric", "score", "--parent", "t-parent", "--id", "t-existing",
"--timeout", "601", "--idle-timeout", "121", "--audit", "--no-link-deps",
])
}
#[test]
fn cli_run_conversion_preserves_requested_profile_and_paths() {
let args = configured_args();
assert_eq!(args.agent_name, "qwen");
assert_eq!(args.prompt, "say hi");
assert_eq!(args.repo.as_deref(), Some("repo"));
assert_eq!(args.repo_root.as_deref(), Some("root"));
assert_eq!(args.dir.as_deref(), Some("checkout"));
assert_eq!(args.output.as_deref(), Some("answer.txt"));
assert_eq!(args.result_file.as_deref(), Some("report.md"));
assert_eq!(args.model.as_deref(), Some("chosen-model"));
assert_eq!(args.model_source, ModelSource::UserSupplied);
assert_eq!(args.declared_difficulty, Some(TaskDifficulty::Complex));
assert_eq!(args.declared_budget, Some(TaskBudget::Cheap));
assert_eq!(args.declared_urgency, Some(TaskUrgency::Urgent));
assert_eq!(args.declared_rigor, Some(TaskRigor::Critical));
assert_eq!(args.declared_egress, TaskEgress::Local);
assert_eq!(args.kind, Some(TaskCategory::Debugging));
assert_eq!(args.worktree.as_deref(), Some("fix/task"));
assert_eq!(args.team.as_deref(), Some("dev"));
assert_eq!(args.group.as_deref(), Some("wg-test"));
}
#[test]
fn cli_run_conversion_preserves_lifecycle_flags() {
let args = configured_args();
assert_eq!(args.verify.as_deref(), Some("auto"));
assert_eq!(args.iterate, Some(2));
assert_eq!(args.eval.as_deref(), Some("evaluate"));
assert_eq!(args.eval_feedback_template.as_deref(), Some("feedback {output}"));
assert_eq!(args.judge.as_deref(), Some("gemini"));
assert_eq!(args.peer_review.as_deref(), Some("claude"));
assert_eq!(args.retry, 3);
assert!(args.background && args.dry_run && args.announce && args.read_only);
assert_eq!(args.best_of, Some(2));
assert_eq!(args.metric.as_deref(), Some("score"));
assert_eq!(args.parent_task_id.as_deref(), Some("t-parent"));
assert_eq!(args.existing_task_id.as_ref().map(TaskId::as_str), Some("t-existing"));
assert_eq!(args.timeout, Some(601));
assert_eq!(args.idle_timeout_secs, Some(121));
assert!(args.audit && args.audit_explicit);
assert!(!args.no_audit && !args.link_deps);
}
#[test]
fn cli_run_conversion_preserves_extras_and_injected_files() {
let args = configured_args();
assert_eq!(args.context, ["one.rs", "two.rs"]);
assert_eq!(args.checklist, ["first", "second"]);
assert_eq!(args.scope, ["src", "tests"]);
assert_eq!(args.remote_build.as_deref(), Some("selected-box"));
assert!(args.no_backup);
assert_eq!(args.context_from, ["t-one", "t-two"]);
assert_eq!(args.skills, ["implementer", "auditor"]);
assert_eq!(args.template.as_deref(), Some("template"));
assert_eq!(args.on_done.as_deref(), Some("finish"));
assert_eq!(args.cascade, ["codex", "claude"]);
assert_eq!(args.hooks, ["done:hook"]);
}
#[test]
fn cli_run_conversion_keeps_default_model_resolution_and_optional_flags() {
let args = parsed_args(&[]);
assert_eq!(args.model, None, "dispatch resolves an unspecified model");
assert_eq!(args.model_source, ModelSource::AidResolved);
assert_eq!(args.declared_difficulty, None);
assert_eq!(args.declared_budget, None);
assert_eq!(args.declared_urgency, None);
assert_eq!(args.declared_rigor, None);
assert_eq!(args.declared_egress, TaskEgress::Any);
assert_eq!(args.retry, 0);
assert!(!args.background && !args.dry_run && !args.read_only);
assert!(args.announce && args.link_deps);
let args = parsed_args(&["--no-skill", "--no-audit", "--backup", "gdrive:reports", "--sandbox"]);
assert_eq!(args.skills, [cmd::run::NO_SKILL_SENTINEL]);
assert!(args.no_audit && args.sandbox);
assert_eq!(args.backup.as_deref(), Some("gdrive:reports"));
let args = parsed_args(&["--container", "dev:latest"]);
assert_eq!(args.container.as_deref(), Some("dev:latest"));
let args = parsed_args(&["--remote-build"]);
assert_eq!(args.remote_build.as_deref(), Some("auto"));
}
#[test]
fn cli_run_conversion_preserves_prompt_file_without_reading_files() {
let cli = Cli::try_parse_from([
"aid", "run", "qwen", "--prompt-file", "prompt.md", "--no-hint",
"--checklist", " inline item ", "--checklist-file", "missing-checklist.txt",
]).expect("parse run");
let Some(Commands::Run(args)) = cli.command else { panic!("run command") };
assert!(args.no_hint);
let args = run_args(args);
assert_eq!(args.prompt, "");
assert_eq!(args.prompt_file.as_deref(), Some("prompt.md"));
assert_eq!(args.checklist, [" inline item "]);
}
#[test]
fn cli_run_conversion_does_not_drop_any_configurable_field() {
let defaults = serde_json::to_value(cmd::run::RunArgs::default()).expect("defaults");
let fixtures = [
configured_args(),
parsed_args(&["--no-skill", "--no-audit", "--backup", "gdrive:x", "--sandbox"]),
parsed_args(&["--container", "dev:latest"]),
run_args(Cli::try_parse_from(["aid", "run", "qwen", "--prompt-file", "prompt.md"])
.expect("parse prompt file").command.and_then(|command| match command {
Commands::Run(args) => Some(args), _ => None,
}).expect("run command")),
].map(|args| serde_json::to_value(args).expect("fixture"));
let internal = [
"result_file_required", "base_branch", "setup", "max_duration_mins", "max_task_cost",
"foreground", "audit_report_mode", "budget", "session_id", "batch_siblings", "env",
"env_forward", "judge_retry", "timeout_policy", "suppress_nested_repo_warning",
"force_default_model",
];
for key in internal {
let default = defaults.get(key).expect("allowlisted field exists");
assert!(fixtures.iter().all(|fixture| fixture.get(key) == Some(default)),
"{key} is CLI-configurable; remove it from the allowlist");
}
for (key, default) in defaults.as_object().expect("default object") {
if internal.contains(&key.as_str()) || key == "model_source" { continue; }
assert!(fixtures.iter().any(|fixture| fixture.get(key) != Some(default)),
"CLI field {key} is dropped or missing from the fixtures");
}
}
#[tokio::test]
async fn cli_run_resolves_agent_before_reading_checklist_file() {
let temp = tempfile::tempdir().expect("tempdir");
let _home = crate::paths::AidHomeGuard::set(temp.path());
let store = Arc::new(store::Store::open_memory().expect("store"));
let mut cli = cli_args(&[
"--difficulty", "moderate", "--budget", "standard", "--urgency", "normal",
"--rigor", "standard", "--no-hint", "--checklist-file", "missing-checklist.txt",
]);
cli.agent = "auto".into();
let err = run(store, cli).await.expect_err("removed agent precedes checklist error");
assert!(err.to_string().contains("auto"), "{err:#}");
assert!(!err.to_string().contains("checklist"), "{err:#}");
}
#[tokio::test]
async fn cli_run_reads_checklist_file_after_agent_resolution() {
let temp = tempfile::tempdir().expect("tempdir");
let _home = crate::paths::AidHomeGuard::set(temp.path());
let store = Arc::new(store::Store::open_memory().expect("store"));
let cli = cli_args(&[
"--difficulty", "moderate", "--budget", "standard", "--urgency", "normal",
"--rigor", "standard", "--no-hint", "--checklist-file", "missing-checklist.txt",
]);
let err = run(store, cli).await.expect_err("checklist file must be read");
assert!(err.to_string().contains("checklist"), "{err:#}");
}
#[tokio::test]
async fn cli_run_dry_run_persists_merged_checklist_prompt_and_budget_mode() {
let temp = tempfile::tempdir().expect("tempdir");
let _home = crate::paths::AidHomeGuard::set(temp.path());
crate::paths::ensure_dirs().expect("aid dirs");
let agents = temp.path().join("agents");
std::fs::create_dir_all(&agents).expect("agents dir");
std::fs::write(agents.join("fixture.toml"),
"[agent]\nid = \"fixture\"\ndisplay_name = \"Fixture\"\ncommand = \"true\"\n")
.expect("fixture agent");
let checklist = temp.path().join("checklist.txt");
std::fs::write(&checklist, "# ignored\nfile item\n\n").expect("checklist");
let prompt = temp.path().join("prompt.txt");
std::fs::write(&prompt, "Inspect the requested configuration fields.").expect("prompt");
let store = Arc::new(store::Store::open_memory().expect("store"));
for (budget, configured_budget, expected_budget) in [
("standard", false, false), ("cheap", false, true), ("standard", true, true),
] {
std::fs::write(crate::paths::config_path(),
format!("[selection]\nbudget_mode = {configured_budget}\n")).expect("config");
let cli = Cli::try_parse_from([
"aid", "run", "fixture", "--prompt-file", prompt.to_str().expect("prompt path"),
"--checklist", " inline item ", "--checklist-file", checklist.to_str().expect("checklist path"),
"--difficulty", "moderate", "--budget", budget, "--urgency", "normal",
"--rigor", "standard", "--no-hint", "--no-skill", "--dry-run",
"--dir", temp.path().to_str().expect("dir"),
]).expect("parse run");
let Some(Commands::Run(cli)) = cli.command else { panic!("run command") };
let task_id = run(store.clone(), cli).await.expect("dry-run dispatch");
let saved = store.get_task_dispatch_args(task_id.as_str()).expect("saved args")
.expect("persisted dispatch");
let args: cmd::run::RunArgs = serde_json::from_str(&saved).expect("decode args");
assert_eq!(args.checklist, ["inline item", "file item"]);
assert_eq!(args.prompt, "Inspect the requested configuration fields.");
assert_eq!(args.prompt_file, None);
assert_eq!(args.budget, expected_budget, "{budget}, config={configured_budget}");
}
}