use crate::task::follow_up_session;
use crate::test_support::prelude::*;
use crate::test_support::*;
#[test]
fn actionable_follow_up_is_committed_and_can_start_a_fresh_git_run() {
for folders in [false, true] {
let root = temp_root("git-todo-follow-up");
let project_root = root.join("project");
let state_dir = root.join("state");
init_tasks(&project_root, folders).unwrap();
let board = TaskBoard::for_project(&project_root);
board
.insert_content(
TaskStatus::Doing,
None,
"Implement feature codex:parent-session",
)
.unwrap();
initialize_test_git_repository(&project_root);
let project_root = fs::canonicalize(project_root).unwrap();
let store = agent::TursoAgentStore::open_blocking(&state_dir).unwrap();
store
.register_project_blocking(&project_root, "project")
.unwrap();
store
.set_project_git_mode_for_path_blocking(&project_root, AgentGitMode::Commit)
.unwrap();
let project = store.list_projects_blocking().unwrap().remove(0);
store
.mark_session_running_blocking(
project.id,
"parent-session",
123,
"parent-run",
&root.join("out"),
&root.join("err"),
)
.unwrap();
let start = capture_agent_git_start_state(&project_root, AgentGitMode::Commit).unwrap();
ensure_agent_git_working_record(
&store,
&project,
"parent-session",
"parent-run",
Some(&start),
)
.unwrap();
bind_agent_git_working_task_identity(&store, &project, "parent-session", "parent-run")
.unwrap();
let workflow = ManagedTaskWorkflow::new(&project_root);
for _ in 0..2 {
assert_eq!(
workflow
.add_follow_up(
TaskStatus::Doing,
"1",
"Fix existing lint warnings.",
"Same failure on starting revision; feature checks pass",
None
)
.unwrap(),
TaskStatus::Todo
);
}
assert_eq!(board.entries(TaskStatus::Todo).unwrap().len(), 1);
let follow_up = board.entry(TaskStatus::Todo, 1).unwrap();
assert_eq!(
follow_up_session(&follow_up.content),
Some("parent-session")
);
assert!(!task_entry_is_blocked(&follow_up));
assert!(
workflow
.add_follow_up(
TaskStatus::Doing,
"1",
"Duplicate follow-up.",
"Evidence",
Some("No runtime")
)
.is_err()
);
let parent = board.entry(TaskStatus::Doing, 1).unwrap();
board.write_entry_content(TaskStatus::Doing, &parent, "Implement feature — COMPLETED 2026-09-05: verified; lint cleanup queued codex:parent-session").unwrap();
fs::write(project_root.join("feature.txt"), "implemented\n").unwrap();
run_test_git(&project_root, &["add", "feature.txt", "tasks"]);
board
.insert_content(TaskStatus::Todo, None, "Concurrent human task")
.unwrap();
let context = AutomatedAgentChildContext {
project_id: project.id,
run_token: "parent-run".to_string(),
};
move_task_to_done_with_agent_store(&project_root, TaskStatus::Doing, "1", &context, &store)
.unwrap();
let paths = if folders {
["tasks/doing", "tasks/done"]
} else {
["tasks/doing.md", "tasks/done.md"]
};
run_test_git(&project_root, &["add", "--", paths[0], paths[1]]);
run_test_agent_git(
&project_root,
&[
"commit",
"-m",
"Implement feature and queue lint cleanup",
"-m",
"CLT-Task: codex:parent-session",
],
);
let sealed = store
.git_finalization_blocking(project.id, "parent-session")
.unwrap()
.unwrap();
let completed = reconcile_agent_git_finalization(
&store,
&project_root,
sealed,
Some("parent-run"),
None,
)
.unwrap();
assert_eq!(completed.state, GitFinalizationState::Completed);
let entries = git_ref_task_entries(&project_root, "HEAD").unwrap();
assert_eq!(
entries
.iter()
.filter(|entry| entry.status == "todo"
&& follow_up_session(&entry.content) == Some("parent-session"))
.count(),
1
);
assert!(
!entries
.iter()
.any(|entry| entry.content.contains("Concurrent human task"))
);
let scan = scan_agent_project(&project_root);
assert_eq!(scan.blocked_task_count(), 0);
assert!(scan.has_pending_task());
assert_eq!(
automated_codex_session_to_resume(&project_root, AgentTaskSelection::NextTodo).unwrap(),
None
);
let next_start = prepare_agent_git_start_state_for_run(
&store,
&project,
AgentTaskSelection::NextTodo,
false,
false,
"follow-up-run",
)
.unwrap()
.unwrap();
assert_ne!(next_start.starting_head, start.starting_head);
move_task(&project_root, TaskStatus::Todo, TaskStatus::Doing, "1").unwrap();
assert!(
attach_codex_session_to_active_task(
&project_root,
AgentTaskSelection::NextTodo,
&[],
&[],
"follow-up-session"
)
.unwrap()
);
assert_eq!(
task_status_for_codex_session(&project_root, "parent-session").unwrap(),
Some(TaskStatus::Done)
);
assert_eq!(
task_status_for_codex_session(&project_root, "follow-up-session").unwrap(),
Some(TaskStatus::Doing)
);
fs::remove_dir_all(root).unwrap();
}
}
#[test]
fn blocked_follow_up_is_sealed_resealable_and_recoverable_in_both_board_formats() {
for folders in [false, true] {
let root = temp_root("git-blocked-follow-up");
let project_root = root.join("project");
let state_dir = root.join("state");
init_tasks(&project_root, folders).unwrap();
let board = TaskBoard::for_project(&project_root);
board
.insert_content(
TaskStatus::Doing,
None,
"Implement feature — BLOCKED 2026-09-03: GPU harness fails codex:session-follow-up",
)
.unwrap();
board
.insert_content(TaskStatus::Doing, None, "Keep another task unchanged")
.unwrap();
initialize_test_git_repository(&project_root);
let project_root = fs::canonicalize(project_root).unwrap();
let store = agent::TursoAgentStore::open_blocking(&state_dir).unwrap();
store
.register_project_blocking(&project_root, "project")
.unwrap();
store
.set_project_git_mode_for_path_blocking(&project_root, AgentGitMode::Commit)
.unwrap();
let project = store.list_projects_blocking().unwrap().remove(0);
store
.mark_session_running_blocking(
project.id,
"session-follow-up",
123,
"run-follow-up",
&root.join("out"),
&root.join("err"),
)
.unwrap();
let start = capture_agent_git_start_state(&project_root, AgentGitMode::Commit).unwrap();
ensure_agent_git_working_record(
&store,
&project,
"session-follow-up",
"run-follow-up",
Some(&start),
)
.unwrap();
bind_agent_git_working_task_identity(
&store,
&project,
"session-follow-up",
"run-follow-up",
)
.unwrap();
let original = board.entry(TaskStatus::Doing, 1).unwrap();
let other = board.entry(TaskStatus::Doing, 2).unwrap();
let mut recovery_job = AgentRunJob {
state_dir: state_dir.clone(),
project: project.clone(),
holder: "recovery".to_string(),
worker_token: None,
max_global_jobs: 1,
task_selection: AgentTaskSelection::RecoverBlocked,
resume_session_id: Some("session-follow-up".to_string()),
blocked_task_count_before: 1,
done_task_contents_before: completed_task_contents(&project_root).unwrap(),
blocked_task_snapshots_before: blocked_task_snapshots(&project_root).unwrap(),
};
assert!(blocked_recovery_made_no_progress(&recovery_job));
ManagedTaskWorkflow::new(&project_root)
.add_follow_up(
TaskStatus::Doing,
"1",
"Repair GPU harness.",
"GPU harness fails identically at starting revision; requires working GPU runtime",
Some("GPU harness fails identically at starting revision; requires working GPU runtime"),
)
.unwrap();
let preserved = board.entry(TaskStatus::Doing, 2).unwrap();
assert_eq!(preserved.source, other.source);
assert_eq!(preserved.content, other.content);
board.write_entry_content(TaskStatus::Doing, &original, "Implement feature — COMPLETED 2026-09-04: feature tests passed; GPU baseline failure recorded in linked follow-up codex:session-follow-up").unwrap();
fs::write(project_root.join("feature.txt"), "implemented\n").unwrap();
run_test_git(&project_root, &["add", "feature.txt", "tasks"]);
board
.insert_content(TaskStatus::Todo, None, "Concurrent human task")
.unwrap();
let context = AutomatedAgentChildContext {
project_id: project.id,
run_token: "run-follow-up".to_string(),
};
move_task_to_done_with_agent_store(&project_root, TaskStatus::Doing, "1", &context, &store)
.unwrap();
let doing_path = if folders {
"tasks/doing"
} else {
"tasks/doing.md"
};
let done_path = if folders {
"tasks/done"
} else {
"tasks/done.md"
};
run_test_git(&project_root, &["add", "--", doing_path, done_path]);
let pending = store
.git_finalization_blocking(project.id, "session-follow-up")
.unwrap()
.unwrap();
fs::write(
project_root.join("feature.txt"),
"formatted implementation\n",
)
.unwrap();
run_test_git(&project_root, &["add", "feature.txt"]);
let manifest = capture_agent_git_resealed_manifest(
AgentGitProofContext {
store: &store,
project_id: project.id,
},
&project_root,
&pending.worktree_baseline,
"session-follow-up",
pending.task_identity.as_deref().unwrap(),
pending.starting_head.as_deref().unwrap(),
pending.branch_ref.as_deref(),
)
.unwrap();
assert!(
store
.reseal_git_finalization_manifest_blocking(
project.id,
"session-follow-up",
pending.generation,
pending.task_identity.as_deref().unwrap(),
&manifest,
"run-follow-up",
"200"
)
.unwrap()
);
run_test_agent_git(
&project_root,
&[
"commit",
"-m",
"Implement feature and track GPU failure",
"-m",
"CLT-Task: codex:session-follow-up",
],
);
let sealed = store
.git_finalization_blocking(project.id, "session-follow-up")
.unwrap()
.unwrap();
let completed = reconcile_agent_git_finalization(
&store,
&project_root,
sealed,
Some("run-follow-up"),
None,
)
.unwrap();
assert_eq!(completed.state, GitFinalizationState::Completed);
assert_eq!(
run_test_git(
&project_root,
&[
"rev-list",
"--count",
&format!("{}..HEAD", start.starting_head)
]
),
"1"
);
let entries = git_ref_task_entries(&project_root, "HEAD").unwrap();
assert_eq!(
entries
.iter()
.filter(|entry| entry.status == "doing"
&& follow_up_session(&entry.content) == Some("session-follow-up"))
.count(),
1
);
assert!(
!entries
.iter()
.any(|entry| entry.content.contains("Concurrent human task"))
);
let scan = scan_agent_project(&project_root);
assert_eq!(scan.blocked_doing_count, 1);
assert!(!blocked_recovery_made_no_progress(&recovery_job));
recovery_job.blocked_task_snapshots_before[0].content =
"Different blocked task".to_string();
assert!(blocked_recovery_made_no_progress(&recovery_job));
let follow_up = board
.entries(TaskStatus::Doing)
.unwrap()
.into_iter()
.find(task_entry_is_blocked)
.unwrap();
assert_eq!(
recoverable_codex_session_id_from_task_content(&follow_up.content),
None
);
let doing_before = task_contents_for_status(&project_root, TaskStatus::Doing).unwrap();
let blocked_before = blocked_task_snapshots(&project_root).unwrap();
assert_eq!(
automated_codex_session_to_resume(&project_root, AgentTaskSelection::RecoverBlocked)
.unwrap(),
None
);
assert!(
attach_codex_session_to_active_task(
&project_root,
AgentTaskSelection::RecoverBlocked,
&doing_before,
&blocked_before,
"follow-up-own-session"
)
.unwrap()
);
assert_eq!(
task_status_for_codex_session(&project_root, "session-follow-up").unwrap(),
Some(TaskStatus::Done)
);
assert_eq!(
task_status_for_codex_session(&project_root, "follow-up-own-session").unwrap(),
Some(TaskStatus::Doing)
);
fs::remove_dir_all(root).unwrap();
}
}
#[test]
fn follow_up_allowance_preserves_exact_board_scope() {
for folders in [false, true] {
for blocked in [false, true] {
let follow_up_status = if blocked {
TaskStatus::Doing
} else {
TaskStatus::Todo
};
let follow_up_index = if blocked { 3 } else { 1 };
for mutation in [
"unrelated",
"wrong-state",
"second-follow-up",
"wrong-parent",
"duplicate-session",
"header",
"existing",
"attachment",
"symlink",
] {
let root = temp_root("follow-up-scope");
init_tasks(&root, folders).unwrap();
let board = TaskBoard::for_project(&root);
board
.insert_content(
TaskStatus::Doing,
None,
"Finish feature codex:session-scope",
)
.unwrap();
board
.insert_content(TaskStatus::Doing, None, "Existing task")
.unwrap();
initialize_test_git_repository(&root);
let parent = run_test_git(&root, &["rev-parse", "HEAD"]);
let identity = durable_task_identity("Finish feature").unwrap();
let scope =
agent_git_task_scope_without_selected(&root, &parent, &identity).unwrap();
let selected = board.entry(TaskStatus::Doing, 1).unwrap();
board
.write_entry_content(
TaskStatus::Doing,
&selected,
"Finish feature — COMPLETED 2026-09-04: verified codex:session-scope",
)
.unwrap();
ManagedTaskWorkflow::new(&root)
.add_follow_up(
TaskStatus::Doing,
"1",
"Repair harness.",
"baseline failure; requires runtime",
blocked.then_some("baseline failure; requires runtime"),
)
.unwrap();
match mutation {
"second-follow-up" => board
.insert_content(
TaskStatus::Todo,
None,
"Another follow-up clt-follow-up:session-scope",
)
.unwrap(),
"unrelated" => board
.insert_content(TaskStatus::Todo, None, "Unrelated addition")
.unwrap(),
"header" => {
fs::write(root.join("tasks/unrelated.txt"), "extra board payload\n")
.unwrap()
}
"attachment" if folders => {
let follow_up = board.entry(follow_up_status, follow_up_index).unwrap();
let TaskSource::Path { path, .. } = follow_up.source else {
unreachable!()
};
fs::remove_file(&path).unwrap();
fs::create_dir(&path).unwrap();
fs::write(path.join("task.md"), follow_up.content).unwrap();
fs::write(path.join("attachment.txt"), "unsealed attachment").unwrap();
}
"attachment" => fs::write(
root.join("tasks/doing.md"),
format!(
"{}\nExtra footer\n",
fs::read_to_string(root.join("tasks/doing.md")).unwrap()
),
)
.unwrap(),
"symlink" => {
#[cfg(unix)]
if folders {
let follow_up = board.entry(follow_up_status, follow_up_index).unwrap();
let TaskSource::Path { path, .. } = follow_up.source else {
unreachable!()
};
let target = root.join("linked.txt");
fs::write(&target, follow_up.content).unwrap();
fs::remove_file(&path).unwrap();
std::os::unix::fs::symlink(target, &path).unwrap();
} else {
board
.insert_content(TaskStatus::Todo, None, "Unrelated task")
.unwrap();
}
#[cfg(not(unix))]
board
.insert_content(TaskStatus::Todo, None, "Unrelated task")
.unwrap();
}
_ => {
let (status, index) = if mutation == "existing" {
(TaskStatus::Doing, 2)
} else {
(follow_up_status, follow_up_index)
};
let entry = board.entry(status, index).unwrap();
let replacement = match mutation {
"wrong-state" if !blocked => {
"Repair harness — BLOCKED 2026-09-04: failure clt-follow-up:session-scope"
}
"wrong-state" => {
"Repair harness — UNBLOCKED 2026-09-04: ready clt-follow-up:session-scope"
}
"wrong-parent" => {
"Repair harness — BLOCKED 2026-09-04: failure clt-follow-up:other-session"
}
"duplicate-session" => {
"Repair harness — BLOCKED 2026-09-04: failure clt-follow-up:session-scope codex:session-scope"
}
"existing" => "Rewritten existing task",
_ => unreachable!(),
};
board
.write_entry_content(status, &entry, replacement)
.unwrap();
}
}
run_test_git(&root, &["add", "tasks"]);
let staged = run_test_git(&root, &["write-tree"]);
assert!(
project_agent_git_completed_tree(
&root,
&staged,
"session-scope",
&identity,
&parent,
&scope
)
.is_err(),
"accepted {mutation} with folders={folders}, blocked={blocked}"
);
fs::remove_dir_all(root).unwrap();
}
}
}
}
#[test]
fn completed_note_cannot_hide_a_later_blocker() {
assert!(!task_content_has_completed_note(
"Feature — COMPLETED 2026-09-03: implemented — BLOCKED 2026-09-04: acceptance fails"
));
assert!(task_content_has_completed_note(
"Feature — BLOCKED 2026-09-03: acceptance fails — COMPLETED 2026-09-04: fixed and verified"
));
}