use crate::cmd::run::RunArgs;
use crate::cmd::show::{missing_owned_output_absence, owned_output_path, read_task_output};
use crate::paths::AidHomeGuard;
use crate::store::Store;
use rusqlite::Connection;
use std::path::Path;
fn create_pre_effective_dir_database(path: &Path) {
let conn = Connection::open(path).unwrap();
conn.execute_batch(
"CREATE TABLE tasks (
id TEXT PRIMARY KEY,
agent TEXT NOT NULL,
prompt TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
worktree_path TEXT,
worktree_branch TEXT,
log_path TEXT,
output_path TEXT,
tokens INTEGER,
duration_ms INTEGER,
created_at TEXT NOT NULL,
completed_at TEXT,
dispatch_args TEXT
);
CREATE TABLE events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL,
timestamp TEXT NOT NULL,
event_type TEXT NOT NULL,
detail TEXT NOT NULL
);",
)
.unwrap();
}
fn insert_legacy_task(conn: &Connection, id: &str, output: &str, dispatch_args: Option<&str>) {
conn
.execute(
"INSERT INTO tasks (id, agent, prompt, status, created_at, output_path, dispatch_args)
VALUES (?1, 'codex', 'audit', 'done', '2026-08-01T00:00:00+00:00', ?2, ?3)",
rusqlite::params![id, output, dispatch_args],
)
.unwrap();
}
fn has_column(conn: &Connection, name: &str) -> bool {
let mut stmt = conn.prepare("PRAGMA table_info(tasks)").unwrap();
stmt.query_map([], |row| row.get::<_, String>(1))
.unwrap()
.any(|col| col.ok().as_deref() == Some(name))
}
fn insert_legacy_rows(conn: &Connection, dir: &Path) {
let args = RunArgs {
dir: Some(dir.display().to_string()),
output: Some("report.md".to_string()),
..Default::default()
};
insert_legacy_task(
conn,
"t-legacy-dir",
"report.md",
Some(&args.dispatch_args_json().unwrap()),
);
insert_legacy_task(conn, "t-legacy-nodir", "report.md", Some(r#"{"prompt":"research"}"#));
insert_legacy_task(conn, "t-legacy-rel", "report.md", Some(r#"{"dir":"."}"#));
insert_legacy_task(
conn,
"t-legacy-missing-dir",
"report.md",
Some(r#"{"dir":"/tmp/aid-dir-that-no-longer-exists"}"#),
);
}
fn assert_legacy_rows_resolve(store: &Store, dir: &Path) {
let owner = store.get_task("t-legacy-dir").unwrap().unwrap();
assert_eq!(owner.effective_dir.as_deref(), Some(dir.to_str().unwrap()));
assert_eq!(read_task_output(&owner).unwrap(), "LEGACY_DIR_OWNED_REPORT\n");
let no_dir = store.get_task("t-legacy-nodir").unwrap().unwrap();
assert!(no_dir.effective_dir.is_none());
assert_eq!(owned_output_path(&no_dir), None);
assert!(
missing_owned_output_absence(&no_dir)
.unwrap()
.contains("No task-owned output file"),
"absence must stay honest when no directory was recorded"
);
let relative = store.get_task("t-legacy-rel").unwrap().unwrap();
assert!(relative.effective_dir.is_none(), "relative --dir is not usable without CWD");
let missing_dir = store.get_task("t-legacy-missing-dir").unwrap().unwrap();
assert_eq!(
missing_dir.effective_dir.as_deref(),
Some("/tmp/aid-dir-that-no-longer-exists")
);
assert!(read_task_output(&missing_dir).is_err());
}
#[test]
fn migrate_backfills_effective_dir_so_legacy_dir_task_resolves_report() {
let root = tempfile::tempdir().unwrap();
let aid_home = root.path().join("aid-home");
let _aid_home = AidHomeGuard::set(&aid_home);
let dir = root.path().join("audit-dir");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("report.md"), "LEGACY_DIR_OWNED_REPORT\n").unwrap();
let db_path = root.path().join("aid.db");
create_pre_effective_dir_database(&db_path);
let conn = Connection::open(&db_path).unwrap();
assert!(!has_column(&conn, "effective_dir"), "fixture must start without the column");
insert_legacy_rows(&conn, &dir);
drop(conn);
let store = Store::open(&db_path).unwrap();
assert_legacy_rows_resolve(&store, &dir);
store
.db()
.execute(
"UPDATE tasks SET effective_dir = ?1 WHERE id = 't-legacy-dir'",
rusqlite::params!["/tmp/already-populated-dir"],
)
.unwrap();
drop(store);
let reopened = Store::open(&db_path).unwrap();
let owner = reopened.get_task("t-legacy-dir").unwrap().unwrap();
assert_eq!(owner.effective_dir.as_deref(), Some("/tmp/already-populated-dir"));
}
#[test]
fn malformed_dispatch_args_do_not_abort_open_or_skip_valid_sibling() {
let root = tempfile::tempdir().unwrap();
let db_path = root.path().join("aid.db");
create_pre_effective_dir_database(&db_path);
let conn = Connection::open(&db_path).unwrap();
insert_legacy_task(
&conn,
"t-valid-dir",
"report.md",
Some(r#"{"dir":"/tmp/valid-historical-dir"}"#),
);
insert_legacy_task(
&conn,
"t-whitespace-dir",
"report.md",
Some(r#"{"dir":"/tmp/recorded-dir-with-space "}"#),
);
insert_legacy_task(&conn, "t-malformed", "report.md", Some("{not json"));
conn.execute(
"INSERT INTO tasks (id, agent, prompt, status, created_at, output_path, dispatch_args)
VALUES (X'00FF', 'codex', 'audit', 'done', '2026-08-01T00:00:00+00:00', 'report.md', ?1)",
rusqlite::params![r#"{"dir":"/tmp/non-text-id-dir"}"#],
)
.unwrap();
drop(conn);
let store = Store::open(&db_path).unwrap();
let valid = store.get_task("t-valid-dir").unwrap().unwrap();
assert_eq!(valid.effective_dir.as_deref(), Some("/tmp/valid-historical-dir"));
let whitespace = store.get_task("t-whitespace-dir").unwrap().unwrap();
assert_eq!(
whitespace.effective_dir.as_deref(),
Some("/tmp/recorded-dir-with-space")
);
let malformed = store.get_task("t-malformed").unwrap().unwrap();
assert!(malformed.effective_dir.is_none());
let non_text_id_dir: Option<String> = store
.db()
.query_row(
"SELECT effective_dir FROM tasks WHERE typeof(id) = 'blob'",
[],
|row| row.get(0),
)
.unwrap();
assert!(non_text_id_dir.is_none());
}