mod common;
use rusqlite::{Connection, OptionalExtension, params};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
fn success(output: Output) -> String {
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout).trim().to_string()
}
fn git(repo: &Path, args: &[&str]) -> String {
success(
Command::new("git")
.arg("-C")
.arg(repo)
.args(args)
.output()
.unwrap(),
)
}
struct Fixture {
root: tempfile::TempDir,
repo: PathBuf,
worktree: PathBuf,
head: String,
db: Connection,
}
impl Fixture {
fn new() -> Self {
let root = tempfile::Builder::new()
.prefix("aid-wt-accept-")
.tempdir_in("/tmp")
.unwrap();
let repo = root.path().join("repo");
fs::create_dir(&repo).unwrap();
git(&repo, &["init", "-b", "main"]);
git(&repo, &["config", "user.name", "Test User"]);
git(&repo, &["config", "user.email", "test@example.invalid"]);
fs::write(repo.join("base"), "base").unwrap();
git(&repo, &["add", "base"]);
git(&repo, &["commit", "-m", "base"]);
let worktree = root.path().join("shared");
git(
&repo,
&[
"worktree",
"add",
"-b",
"shared",
worktree.to_str().unwrap(),
],
);
fs::write(worktree.join("delivery"), "delivered").unwrap();
git(&worktree, &["add", "delivery"]);
git(&worktree, &["commit", "-m", "delivery"]);
let head = git(&worktree, &["rev-parse", "HEAD"]);
success(
common::aid_cmd_in(root.path())
.args(["board", "--json"])
.output()
.unwrap(),
);
let db = Connection::open(root.path().join("aid.db")).unwrap();
Self {
root,
repo,
worktree,
head,
db,
}
}
fn task(&self, task: &str, head: Option<&str>) {
self.db
.execute(
"INSERT INTO tasks (id, agent, prompt, status, repo_path, worktree_path,
final_head_sha, final_branch, worktree_branch, parent_task_id, created_at)
VALUES (?1, 'codex', 'task', 'done', ?2, ?3, ?4, 'shared', 'shared', ?5, ?6)",
params![
task,
self.repo.to_str().unwrap(),
self.worktree.to_str().unwrap(),
head,
(task == "t-retry").then_some("t-original"),
chrono::Local::now().to_rfc3339()
],
)
.unwrap();
}
fn aid(&self, args: &[&str]) -> Output {
common::aid_cmd_in(self.root.path())
.args(args)
.output()
.unwrap()
}
fn accepted_head(&self, task: &str) -> Option<String> {
self.db
.query_row(
"SELECT accepted_head_sha FROM task_acceptance WHERE task_id = ?1",
[task],
|row| row.get(0),
)
.optional()
.unwrap()
}
fn collect_original(&self) {
self.task("t-original", Some(&self.head));
git(&self.repo, &["merge", "--ff-only", "shared"]);
success(self.aid(&["accept", "t-original"]));
success(self.aid(&["gc", "--task", "t-original"]));
git(&self.repo, &["branch", "-d", "shared"]);
assert!(!self.worktree.exists());
}
fn refusal(&self, expected: &str) {
let output = self.aid(&["accept", "t-retry"]);
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("worktree") && stderr.contains("missing"),
"{stderr}"
);
assert!(stderr.contains(expected), "{stderr}");
assert_eq!(self.accepted_head("t-retry"), None);
assert!(!self.aid(&["gc", "--task", "t-retry"]).status.success());
}
}
#[test]
fn accept_and_gc_retry_after_original_collected_shared_worktree() {
let fixture = Fixture::new();
fixture.task("t-retry", Some(&fixture.head));
fixture.collect_original();
success(fixture.aid(&["accept", "t-retry"]));
assert_eq!(fixture.accepted_head("t-retry"), Some(fixture.head.clone()));
let stdout = success(fixture.aid(&["gc", "--task", "t-retry"]));
assert!(stdout.contains("Worktree was already collected"));
for task in ["t-original", "t-retry"] {
let count: i64 = fixture
.db
.query_row(
"SELECT COUNT(*) FROM artifact_durability WHERE task_id = ?1",
[task],
|row| row.get(0),
)
.unwrap();
assert_eq!(count, 1);
}
}
#[test]
fn missing_final_commit_cannot_borrow_sibling_acceptance() {
let fixture = Fixture::new();
fixture.task("t-retry", None);
fixture.collect_original();
fixture.refusal("no recorded final commit");
}
#[test]
fn unreachable_final_commit_refuses_acceptance_after_sibling_gc() {
let fixture = Fixture::new();
fs::write(fixture.worktree.join("delivery"), "retry-only").unwrap();
git(&fixture.worktree, &["commit", "-am", "retry-only"]);
let retry_head = git(&fixture.worktree, &["rev-parse", "HEAD"]);
fixture.task("t-retry", Some(&retry_head));
git(&fixture.worktree, &["reset", "--hard", &fixture.head]);
fixture.collect_original();
fixture.refusal(&format!("Commit {retry_head} for '' has no durable ref"));
}
#[test]
fn absent_final_commit_object_refuses_acceptance() {
let fixture = Fixture::new();
let missing = "a".repeat(40);
fixture.task("t-retry", Some(&missing));
fixture.collect_original();
fixture.refusal(&format!(
"Commit {missing} for '' exists only in disposable storage"
));
}
#[test]
fn mutable_ref_is_not_recorded_final_commit_evidence() {
let fixture = Fixture::new();
fixture.task("t-retry", Some("main"));
fixture.collect_original();
fixture.refusal("no valid recorded final commit SHA");
}
#[test]
fn registered_missing_worktree_refuses_acceptance() {
let fixture = Fixture::new();
fixture.task("t-retry", Some(&fixture.head));
fs::remove_dir_all(&fixture.worktree).unwrap();
fixture.refusal("still registered");
}
#[test]
fn missing_repository_refuses_acceptance() {
let fixture = Fixture::new();
fixture.task("t-retry", Some(&fixture.head));
fixture.collect_original();
fixture
.db
.execute("UPDATE tasks SET repo_path = NULL WHERE id = 't-retry'", [])
.unwrap();
fixture.refusal("no repository for durability proof");
}
#[test]
fn missing_submodule_storage_refuses_acceptance() {
let fixture = Fixture::new();
git(
&fixture.worktree,
&[
"update-index",
"--add",
"--cacheinfo",
&format!("160000,{},child", fixture.head),
],
);
git(&fixture.worktree, &["commit", "-m", "gitlink"]);
let head = git(&fixture.worktree, &["rev-parse", "HEAD"]);
fixture.task("t-retry", Some(&head));
git(
&fixture.repo,
&[
"worktree",
"remove",
"--force",
fixture.worktree.to_str().unwrap(),
],
);
fixture.refusal("No durable object store for submodule 'child'");
}
#[test]
fn official_guide_documents_missing_worktree_acceptance_proof() {
let guide = include_str!("../default-skills/aid-guide/references/task-lifecycle.md");
for term in [
"recorded final commit and branch",
"Before recording acceptance",
"every recursive submodule commit",
"durable ref refuses acceptance",
"does not substitute for evidence of this task's final commit",
] {
assert!(guide.contains(term), "acceptance guide missing {term}");
}
}