use super::*;
use crate::paths::AidHomeGuard;
use crate::store::Store;
use crate::types::{AgentKind, Task, TaskId, TaskStatus, VerifyStatus};
use chrono::Local;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
fn task(id: &str, status: TaskStatus) -> Task {
Task {
id: TaskId(id.to_string()),
agent: AgentKind::Codex,
custom_agent_name: None,
prompt: "prompt".to_string(),
resolved_prompt: None,
category: None,
status,
parent_task_id: None,
workgroup_id: None,
caller_kind: None,
caller_session_id: None,
agent_session_id: None,
repo_path: None, project_id: Some("proj".to_string()),
worktree_path: None, effective_dir: None,
worktree_branch: None,
final_head_sha: Some("0123456789abcdef".to_string()),
final_branch: None,
start_sha: None,
log_path: None,
output_path: None,
tokens: None,
prompt_tokens: None,
duration_ms: None,
requested_model: None, observed_model: None, attribution_source: None,
cost_usd: None,
exit_code: None,
created_at: Local::now(),
completed_at: None,
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 event(task_id: &str, kind: EventKind, detail: &str) -> TaskEvent {
TaskEvent {
task_id: TaskId(task_id.to_string()),
timestamp: Local::now(),
event_kind: kind,
detail: detail.to_string(),
metadata: None,
}
}
fn failing_gws(home: &Path) -> std::path::PathBuf {
fake_gws(home, "echo 'not signed in' >&2\nexit 2")
}
fn working_gws(home: &Path) -> std::path::PathBuf {
fake_gws(home, "echo '{\"id\":\"fake123\"}'")
}
fn fake_gws(home: &Path, body: &str) -> std::path::PathBuf {
let log = home.join("gws.log");
let binary = home.join("gws");
let script = format!("#!/bin/sh\necho \"$*\" >> '{}'\n{body}\n", log.display());
fs::write(&binary, script).unwrap();
fs::set_permissions(&binary, fs::Permissions::from_mode(0o755)).unwrap();
fs::write(
crate::paths::config_path(),
format!("[backup.gdrive]\nbinary = '{}'\n", binary.display()),
)
.unwrap();
log
}
fn save_args(store: &Store, task_id: &str, args: crate::cmd::run::RunArgs) {
store.update_task_dispatch_args(task_id, &args.dispatch_args_json().unwrap()).unwrap();
}
pub(super) fn settle(store: &Store, task_id: &str, status: TaskStatus) {
store
.db()
.execute("UPDATE tasks SET status = ?1 WHERE id = ?2", rusqlite::params![status.as_str(), task_id])
.unwrap();
on_settled(store, task_id);
}
#[test]
fn settled_done_task_uploads_exactly_once() {
let _permit = crate::test_subprocess::acquire();
let home = tempfile::tempdir().unwrap();
let _guard = AidHomeGuard::set(home.path());
let log = working_gws(home.path());
let store = Store::open_memory().unwrap();
store.insert_task(&task("t-done", TaskStatus::Done)).unwrap();
save_args(&store, "t-done", crate::cmd::run::RunArgs { backup: Some("gdrive:audits".into()), ..Default::default() });
on_settled(&store, "t-done");
let calls = fs::read_to_string(&log).unwrap();
assert_eq!(calls.lines().filter(|l| l.contains("--upload")).count(), 1, "{calls}");
let events = store.get_events("t-done").unwrap();
let markers: Vec<_> = events.iter().filter_map(|e| e.metadata.as_ref()?.get("backup")?.as_str()).collect();
assert_eq!(markers, vec!["uploaded"], "{events:?}");
assert!(store.backup_url("t-done").unwrap().is_some_and(|url| url.contains("fake123")));
assert_eq!(store.get_task("t-done").unwrap().unwrap().status, TaskStatus::Done);
}
#[test]
fn task_failed_by_fail_active_execution_alone_is_not_backed_up() {
let home = tempfile::tempdir().unwrap();
let _guard = AidHomeGuard::set(home.path());
let log = working_gws(home.path());
let store = Store::open_memory().unwrap();
store.insert_task(&task("t-reaped", TaskStatus::Running)).unwrap();
save_args(&store, "t-reaped", crate::cmd::run::RunArgs { backup: Some("gdrive".into()), ..Default::default() });
assert!(crate::task_lifecycle::fail_active_execution(&store, "t-reaped").unwrap());
assert_eq!(store.get_task("t-reaped").unwrap().unwrap().status, TaskStatus::Failed);
assert!(!log.exists(), "gws must never run");
assert!(!already_attempted(&store, "t-reaped"));
assert!(store.get_events("t-reaped").unwrap().iter().all(|e| e.metadata.is_none()));
}
#[test]
fn task_without_saved_dispatch_args_is_never_backed_up() {
let home = tempfile::tempdir().unwrap();
let _guard = AidHomeGuard::set(home.path());
let repo = tempfile::tempdir().unwrap();
fs::create_dir_all(repo.path().join(".git")).unwrap();
fs::create_dir_all(repo.path().join(".aid")).unwrap();
fs::write(repo.path().join(".aid/project.toml"), "[project]\nid = 'proj'\n[backup]\ntarget = 'nope'\n").unwrap();
let store = Store::open_memory().unwrap();
let mut failed = task("t-setup-fail", TaskStatus::Failed);
failed.repo_path = Some(repo.path().display().to_string());
store.insert_task(&failed).unwrap();
settle(&store, "t-setup-fail", TaskStatus::Failed);
assert!(store.get_events("t-setup-fail").unwrap().is_empty());
assert!(!already_attempted(&store, "t-setup-fail"));
save_args(&store, "t-setup-fail", crate::cmd::run::RunArgs::default());
settle(&store, "t-setup-fail", TaskStatus::Failed);
let events = store.get_events("t-setup-fail").unwrap();
assert!(events.iter().any(|e| e.detail.contains("unknown backup target 'nope'")), "{events:?}");
}
#[test]
fn backup_runs_at_most_once_even_after_a_failed_attempt() {
let home = tempfile::tempdir().unwrap();
let _guard = AidHomeGuard::set(home.path());
let log = failing_gws(home.path());
let store = Store::open_memory().unwrap();
store.insert_task(&task("t-once", TaskStatus::Done)).unwrap();
let args = crate::cmd::run::RunArgs { backup: Some("gdrive:audits".into()), ..Default::default() };
save_args(&store, "t-once", args);
settle(&store, "t-once", TaskStatus::Done);
assert!(already_attempted(&store, "t-once"));
settle(&store, "t-once", TaskStatus::Done);
settle(&store, "t-once", TaskStatus::Failed);
let calls = fs::read_to_string(&log).unwrap();
assert_eq!(calls.lines().count(), 1, "one gws call for the whole task: {calls}");
let events = store.get_events("t-once").unwrap();
let attempts: Vec<_> = events.iter().filter(|e| e.detail.starts_with("Backup failed")).collect();
assert_eq!(attempts.len(), 1, "{events:?}");
assert!(attempts[0].detail.contains("not signed in"), "{}", attempts[0].detail);
assert!(store.backup_url("t-once").unwrap().is_none());
assert_eq!(store.get_task("t-once").unwrap().unwrap().status, TaskStatus::Failed);
}
#[test]
fn failed_backup_keeps_the_agent_error_as_latest_error() {
let home = tempfile::tempdir().unwrap();
let _guard = AidHomeGuard::set(home.path());
failing_gws(home.path());
let store = Store::open_memory().unwrap();
store.insert_task(&task("t-agent-err", TaskStatus::Failed)).unwrap();
store.insert_event(&event("t-agent-err", EventKind::Error, "agent exited with code 1")).unwrap();
save_args(&store, "t-agent-err", crate::cmd::run::RunArgs { backup: Some("gdrive".into()), ..Default::default() });
settle(&store, "t-agent-err", TaskStatus::Failed);
assert_eq!(store.latest_error("t-agent-err").as_deref(), Some("agent exited with code 1"));
let events = store.get_events("t-agent-err").unwrap();
let warning = events.iter().find(|e| e.detail.starts_with("Backup failed")).unwrap();
assert_eq!(warning.event_kind, EventKind::Milestone);
assert_eq!(warning.metadata.as_ref().unwrap()["backup"], "failed");
assert_eq!(store.get_task("t-agent-err").unwrap().unwrap().status, TaskStatus::Failed);
}