use super::*;
fn output(dir: &Path, args: &[&str]) -> String {
let output = Command::new("git").arg("-C").arg(dir).args(args).output().unwrap();
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
String::from_utf8(output.stdout).unwrap().trim().to_string()
}
#[tokio::test]
async fn shared_settlement_keeps_dirty_work_and_records_recovery_for_done_and_failed_tasks() {
let _permit = test_subprocess::acquire();
for status in [TaskStatus::Done, TaskStatus::Failed] {
let dir = init_repo();
git(dir.path(), &["config", "user.name", "Test"]);
git(dir.path(), &["config", "user.email", "test@example.invalid"]);
write_path(dir.path(), "base.txt", "base");
git(dir.path(), &["add", "."]);
git(dir.path(), &["commit", "-m", "base"]);
let head = output(dir.path(), &["rev-parse", "HEAD"]);
write_path(dir.path(), "result.txt", "task result");
let store = Arc::new(Store::open_memory().unwrap());
let row = task("t-shared-settlement", status);
store.insert_task(&row).unwrap();
let action = post_agent_dirty_worktree_cleanup(
&store, &row.id, &RunArgs::default(), dir.path().to_str().unwrap(), None,
).await.unwrap();
assert_eq!(action, DirtyWorktreeAction::Continue);
assert_eq!(store.get_task(row.id.as_str()).unwrap().unwrap().status, status);
assert_eq!(output(dir.path(), &["rev-parse", "HEAD"]), head);
assert_eq!(output(dir.path(), &["status", "--porcelain"]), "?? result.txt");
let events = store.get_events(row.id.as_str()).unwrap();
let metadata = events.iter().find_map(|event| event.metadata.as_ref()
.filter(|data| data.get("recovery_ref").is_some())).unwrap();
let reference = metadata["recovery_ref"].as_str().unwrap();
assert_eq!(output(dir.path(), &["show", &format!("{reference}:result.txt")]), "task result");
}
}
#[tokio::test]
async fn shared_checkpoint_failure_marks_task_failed_without_commit_or_retry() {
let _permit = test_subprocess::acquire();
let dir = init_repo();
git(dir.path(), &["config", "user.name", "Test"]);
git(dir.path(), &["config", "user.email", "test@example.invalid"]);
write_path(dir.path(), "base.txt", "base");
git(dir.path(), &["add", "."]);
git(dir.path(), &["commit", "-m", "base"]);
let head = output(dir.path(), &["rev-parse", "HEAD"]);
git(dir.path(), &["update-ref", "refs/aid/recovery/t-shared-fail", &head]);
write_path(dir.path(), "result.txt", "keep me");
let store = Arc::new(Store::open_memory().unwrap());
let row = task("t-shared-fail", TaskStatus::Done);
store.insert_task(&row).unwrap();
let action = post_agent_dirty_worktree_cleanup(
&store, &row.id, &RunArgs::default(), dir.path().to_str().unwrap(), None,
).await.unwrap();
assert_eq!(action, DirtyWorktreeAction::Failed);
assert_eq!(store.get_task(row.id.as_str()).unwrap().unwrap().status, TaskStatus::Failed);
assert_eq!(output(dir.path(), &["rev-parse", "HEAD"]), head);
assert_eq!(std::fs::read_to_string(dir.path().join("result.txt")).unwrap(), "keep me");
}
#[tokio::test]
async fn read_only_shared_checkout_never_creates_a_recovery_ref() {
let _permit = test_subprocess::acquire();
let dir = init_repo();
write_path(dir.path(), "result.txt", "read only");
let store = Arc::new(Store::open_memory().unwrap());
let row = task("t-shared-readonly", TaskStatus::Done);
store.insert_task(&row).unwrap();
let baseline = crate::worktree::capture_worktree_snapshot(dir.path()).unwrap()
.read_only_state(dir.path()).unwrap();
let args = RunArgs { read_only: true, ..Default::default() };
let action = post_agent_dirty_worktree_cleanup(
&store, &row.id, &args, dir.path().to_str().unwrap(), Some(&baseline),
).await.unwrap();
assert_eq!(action, DirtyWorktreeAction::Continue);
assert!(store.get_events(row.id.as_str()).unwrap().is_empty());
assert_eq!(output(dir.path(), &["for-each-ref", "--format=%(refname)", "refs/aid/recovery/"]), "");
}
#[tokio::test]
async fn shared_checkpoint_does_not_skip_failed_verification() {
let _permit = test_subprocess::acquire();
let home = tempfile::tempdir().unwrap();
let _home_guard = crate::paths::AidHomeGuard::set(home.path());
let dir = init_repo();
write_path(dir.path(), "result.txt", "task output");
let store = Arc::new(Store::open_memory().unwrap());
let mut row = task("t-shared-verify", TaskStatus::Done);
row.verify = Some("false".to_string());
row.repo_path = Some(dir.path().display().to_string());
store.insert_task(&row).unwrap();
let args = RunArgs {
dir: row.repo_path.clone(), repo: row.repo_path.clone(),
verify: row.verify.clone(), ..Default::default()
};
post_run_lifecycle(
LifecycleMode::Background, &store, &row.id, &args, AgentKind::Codex,
"codex", args.dir.as_ref(), args.repo.as_ref(), None, None, &[],
&prompt_bundle(), TaskStatus::Done, None,
).await.unwrap();
let settled = store.get_task(row.id.as_str()).unwrap().unwrap();
assert_eq!(settled.verify_status, VerifyStatus::Failed);
assert_eq!(settled.status, TaskStatus::Failed);
assert_eq!(std::fs::read_to_string(dir.path().join("result.txt")).unwrap(), "task output");
assert!(!dir.path().join(".git/index").exists());
assert!(store.get_events(row.id.as_str()).unwrap().iter().any(|event|
event.metadata.as_ref().is_some_and(|data| data.get("recovery_ref").is_some())));
}
#[tokio::test]
async fn mismatched_task_worktree_is_rejected_before_rescue() {
let _permit = test_subprocess::acquire();
let owned = init_repo();
let shared = init_repo();
write_path(shared.path(), "result.txt", "do not commit");
let store = Arc::new(Store::open_memory().unwrap());
let mut row = task("t-wrong-checkout", TaskStatus::Done);
row.worktree_path = Some(owned.path().display().to_string());
store.insert_task(&row).unwrap();
let error = post_agent_dirty_worktree_cleanup(
&store, &row.id, &RunArgs::default(), shared.path().to_str().unwrap(), None,
).await.unwrap_err();
assert!(error.to_string().contains("does not match settlement directory"));
assert!(!shared.path().join(".git/index").exists());
assert!(shared.path().join("result.txt").exists());
}
#[tokio::test]
async fn read_only_missing_baseline_fails_without_recovery() {
let _permit = test_subprocess::acquire();
let dir = init_repo();
write_path(dir.path(), "evidence.txt", "retain");
let store = Arc::new(Store::open_memory().unwrap());
let mut row = task("t-missing-baseline", TaskStatus::Done);
row.read_only = true;
store.insert_task(&row).unwrap();
let args = RunArgs { read_only: true, ..Default::default() };
let action = post_agent_dirty_worktree_cleanup(
&store, &row.id, &args, dir.path().to_str().unwrap(), None,
).await.unwrap();
assert_eq!(action, DirtyWorktreeAction::Failed);
assert_eq!(store.get_task(row.id.as_str()).unwrap().unwrap().status, TaskStatus::Failed);
assert!(store.get_events(row.id.as_str()).unwrap().iter().any(|event|
event.event_kind == EventKind::Error && event.detail.contains("Dispatch snapshot is unavailable")));
assert_eq!(std::fs::read_to_string(dir.path().join("evidence.txt")).unwrap(), "retain");
assert_eq!(output(dir.path(), &["for-each-ref", "--format=%(refname)", "refs/aid/recovery/"]), "");
}