use super::{RunArgs, maybe_run_post_done_audit};
use crate::batch::parse_batch_file;
use crate::project::{ProjectAuditConfig, ProjectConfig};
use crate::store::Store;
use crate::types::{AgentKind, Task, TaskId, TaskStatus, VerifyStatus};
use chrono::Local;
use std::env;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::sync::Arc;
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
crate::aic::test_env_lock()
}
fn done_task(task_id: &str) -> Task {
Task {
id: TaskId(task_id.to_string()),
agent: AgentKind::Codex,
custom_agent_name: None,
prompt: "audit task".to_string(),
resolved_prompt: None,
category: None,
status: TaskStatus::Done,
parent_task_id: None,
workgroup_id: None,
caller_kind: None,
caller_session_id: None,
agent_session_id: None,
repo_path: None,
worktree_path: None,
worktree_branch: None,
start_sha: None,
log_path: None,
output_path: None,
tokens: None,
prompt_tokens: None,
duration_ms: Some(1_000),
model: None,
cost_usd: None,
exit_code: Some(0),
created_at: Local::now(),
completed_at: Some(Local::now()),
verify: None,
verify_status: VerifyStatus::Skipped,
pending_reason: None,
read_only: false,
budget: false,
audit_verdict: None,
audit_report_path: None,
delivery_assessment: None,
}
}
fn install_aic_shim(dir: &Path, body: &str) {
let path = dir.join("aic");
fs::write(&path, format!("#!/bin/sh\n{body}\n")).unwrap();
let mut perms = fs::metadata(&path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&path, perms).unwrap();
}
fn set_env(key: &str, value: impl AsRef<std::ffi::OsStr>) {
unsafe { env::set_var(key, value) }
}
fn remove_env(key: &str) {
unsafe { env::remove_var(key) }
}
fn audit_args(enabled: bool) -> RunArgs {
RunArgs {
agent_name: "codex".to_string(),
prompt: "audit".to_string(),
audit: enabled,
audit_explicit: enabled,
..Default::default()
}
}
fn run_audit_for_task(store: &Store, task_id: &str, args: &RunArgs) {
maybe_run_post_done_audit(store, &TaskId(task_id.to_string()), args, None, None).unwrap();
}
#[test]
fn audit_skipped_when_aic_not_found() {
let _guard = env_lock();
set_env("AIC_TEST_PRESENT", "0");
let store = Store::open_memory().unwrap();
store.insert_task(&done_task("t-audit-skip")).unwrap();
run_audit_for_task(&store, "t-audit-skip", &audit_args(true));
let task = store.get_task("t-audit-skip").unwrap().unwrap();
assert_eq!(task.audit_verdict.as_deref(), Some("skipped"));
assert_eq!(task.audit_report_path, None);
let events = store.get_events("t-audit-skip").unwrap();
assert!(events.iter().any(|event| event.detail == "audit skipped: aic binary not found"));
remove_env("AIC_TEST_PRESENT");
}
#[test]
fn audit_records_pass_verdict() {
let _guard = env_lock();
let temp = tempfile::tempdir().unwrap();
install_aic_shim(
temp.path(),
"if [ \"$1\" = \"--version\" ]; then exit 0; fi\nprintf 'report: /tmp/foo.md\\n'\nexit 0",
);
set_env("AIC_TEST_BINARY", temp.path().join("aic"));
let store = Store::open_memory().unwrap();
store.insert_task(&done_task("t-audit-pass")).unwrap();
run_audit_for_task(&store, "t-audit-pass", &audit_args(true));
let task = store.get_task("t-audit-pass").unwrap().unwrap();
assert_eq!(task.audit_verdict.as_deref(), Some("pass"));
assert_eq!(task.audit_report_path.as_deref(), Some("/tmp/foo.md"));
remove_env("AIC_TEST_BINARY");
}
#[test]
fn audit_records_fail_verdict() {
let _guard = env_lock();
let temp = tempfile::tempdir().unwrap();
install_aic_shim(
temp.path(),
"if [ \"$1\" = \"--version\" ]; then exit 0; fi\nprintf 'report: /tmp/fail.md\\n'\nexit 1",
);
set_env("AIC_TEST_BINARY", temp.path().join("aic"));
let store = Store::open_memory().unwrap();
store.insert_task(&done_task("t-audit-fail")).unwrap();
run_audit_for_task(&store, "t-audit-fail", &audit_args(true));
let task = store.get_task("t-audit-fail").unwrap().unwrap();
assert_eq!(task.audit_verdict.as_deref(), Some("fail"));
remove_env("AIC_TEST_BINARY");
}
#[test]
fn audit_records_error_verdict() {
let _guard = env_lock();
let temp = tempfile::tempdir().unwrap();
install_aic_shim(
temp.path(),
"if [ \"$1\" = \"--version\" ]; then exit 0; fi\nexit 200",
);
set_env("AIC_TEST_BINARY", temp.path().join("aic"));
let store = Store::open_memory().unwrap();
store.insert_task(&done_task("t-audit-error")).unwrap();
run_audit_for_task(&store, "t-audit-error", &audit_args(true));
let task = store.get_task("t-audit-error").unwrap().unwrap();
assert_eq!(task.audit_verdict.as_deref(), Some("error"));
remove_env("AIC_TEST_BINARY");
}
#[test]
fn audit_respects_timeout() {
let _guard = env_lock();
let temp = tempfile::tempdir().unwrap();
install_aic_shim(
temp.path(),
"if [ \"$1\" = \"--version\" ]; then exit 0; fi\nsleep 2\nprintf 'report: /tmp/late.md\\n'\nexit 0",
);
set_env("AIC_TEST_BINARY", temp.path().join("aic"));
set_env("AID_AUDIT_TIMEOUT_SECS", "1");
let store = Store::open_memory().unwrap();
store.insert_task(&done_task("t-audit-timeout")).unwrap();
run_audit_for_task(&store, "t-audit-timeout", &audit_args(true));
let task = store.get_task("t-audit-timeout").unwrap().unwrap();
assert_eq!(task.audit_verdict.as_deref(), Some("error"));
remove_env("AID_AUDIT_TIMEOUT_SECS");
remove_env("AIC_TEST_BINARY");
}
#[test]
fn project_audit_auto_triggers_without_cli_flag() {
let _guard = env_lock();
let temp = tempfile::tempdir().unwrap();
install_aic_shim(
temp.path(),
"if [ \"$1\" = \"--version\" ]; then exit 0; fi\nprintf 'report: /tmp/project.md\\n'\nexit 0",
);
set_env("AIC_TEST_BINARY", temp.path().join("aic"));
let store = Store::open_memory().unwrap();
store.insert_task(&done_task("t-project-audit")).unwrap();
let mut args = RunArgs::default();
let project = ProjectConfig { id: "demo".to_string(), audit: ProjectAuditConfig { auto: true }, ..Default::default() };
super::run_dispatch_resolve::apply_project_defaults(&mut args, Some(&project));
run_audit_for_task(&store, "t-project-audit", &args);
let task = store.get_task("t-project-audit").unwrap().unwrap();
assert_eq!(task.audit_verdict.as_deref(), Some("pass"));
remove_env("AIC_TEST_BINARY");
}
#[test]
fn no_audit_overrides_project_audit_auto() {
let _guard = env_lock();
let temp = tempfile::tempdir().unwrap();
install_aic_shim(
temp.path(),
"if [ \"$1\" = \"--version\" ]; then exit 0; fi\nprintf 'report: /tmp/no-audit.md\\n'\nexit 0",
);
set_env("AIC_TEST_BINARY", temp.path().join("aic"));
let store = Store::open_memory().unwrap();
store.insert_task(&done_task("t-no-audit")).unwrap();
let mut args = RunArgs { no_audit: true, ..Default::default() };
let project = ProjectConfig {
id: "demo".to_string(),
audit: ProjectAuditConfig { auto: true },
..Default::default()
};
super::run_dispatch_resolve::apply_project_defaults(&mut args, Some(&project));
run_audit_for_task(&store, "t-no-audit", &args);
let task = store.get_task("t-no-audit").unwrap().unwrap();
assert_eq!(task.audit_verdict, None);
remove_env("AIC_TEST_BINARY");
}
#[test]
fn batch_task_level_audit_override_wins() {
let _guard = env_lock();
let temp = tempfile::tempdir().unwrap();
install_aic_shim(
temp.path(),
"if [ \"$1\" = \"--version\" ]; then exit 0; fi\nprintf 'report: /tmp/batch.md\\n'\nexit 0",
);
set_env("AIC_TEST_BINARY", temp.path().join("aic"));
let batch_file = temp.path().join("tasks.toml");
fs::write(&batch_file, "[defaults]\nagent = \"codex\"\naudit = false\n[[tasks]]\nname = \"plain\"\nprompt = \"plain\"\n[[tasks]]\nname = \"audited\"\nprompt = \"audited\"\naudit = true\n").unwrap();
let config = parse_batch_file(&batch_file).unwrap();
let store = Arc::new(Store::open_memory().unwrap());
let plain_args = RunArgs {
agent_name: config.tasks[0].agent.clone(),
prompt: config.tasks[0].prompt.clone(),
audit: config.tasks[0].audit.unwrap_or(false),
audit_explicit: config.tasks[0].audit.is_some(),
..Default::default()
};
let audited_args = RunArgs {
agent_name: config.tasks[1].agent.clone(),
prompt: config.tasks[1].prompt.clone(),
audit: config.tasks[1].audit.unwrap_or(false),
audit_explicit: config.tasks[1].audit.is_some(),
..Default::default()
};
assert!(!plain_args.audit);
assert!(audited_args.audit);
assert!(audited_args.audit_explicit);
store.insert_task(&done_task("t-batch-audit")).unwrap();
run_audit_for_task(store.as_ref(), "t-batch-audit", &audited_args);
let task = store.get_task("t-batch-audit").unwrap().unwrap();
assert_eq!(task.audit_verdict.as_deref(), Some("pass"));
remove_env("AIC_TEST_BINARY");
}
#[test]
fn show_header_includes_audit_verdict_when_present() {
let store = Arc::new(Store::open_memory().unwrap());
let mut task = done_task("t-show-audit");
task.audit_verdict = Some("pass".to_string());
task.audit_report_path = Some("/tmp/report.md".to_string());
store.insert_task(&task).unwrap();
let summary = crate::cmd::show::summary_text(&store, task.id.as_str()).unwrap();
assert!(summary.contains("Audit: pass (report: /tmp/report.md)"));
}