clt-rs 0.9.3

File-backed task manager with a TUI Kanban board and multi-project Codex agent registry
use super::*;
use crate::managed_git::retire_superseded_working_git_journals;

const OLD: &str = "old-feather-session";
const COMPLETED: &str = "completed-feather-session";
const RUN: &str = "replacement-run";
const TASK: &str = "Fix feather mask";

struct Fixture {
    root: PathBuf,
    state: PathBuf,
    project: agent::AgentProject,
    store: agent::TursoAgentStore,
    old: agent::GitFinalizationRecord,
    completed: agent::GitFinalizationRecord,
}

impl Fixture {
    fn new(folders: bool, sealed_field: Option<(&str, serde_json::Value)>, finish: bool) -> Self {
        let root = temp_root("superseded-working");
        let state = root.join("state");
        let path = root.join("project");
        init_tasks(&path, folders).unwrap();
        TaskBoard::for_project(&path)
            .insert_content(
                TaskStatus::Doing,
                None,
                &format!("{TASK} COMPLETED 2026-10-22: verified codex:{COMPLETED}"),
            )
            .unwrap();
        initialize_test_git_repository(&path);
        let path = fs::canonicalize(path).unwrap();
        let store = agent::TursoAgentStore::open_blocking(&state).unwrap();
        store.register_project_blocking(&path, "project").unwrap();
        store
            .set_project_git_mode_for_path_blocking(&path, AgentGitMode::Commit)
            .unwrap();
        let project = store.list_projects_blocking().unwrap().remove(0);
        store
            .mark_session_running_blocking(
                project.id,
                COMPLETED,
                123,
                RUN,
                &root.join("run.out"),
                &root.join("run.err"),
            )
            .unwrap();
        let start = capture_agent_git_start_state(&path, AgentGitMode::Commit).unwrap();
        ensure_agent_git_working_record(&store, &project, COMPLETED, RUN, Some(&start)).unwrap();
        bind_agent_git_working_task_identity(&store, &project, COMPLETED, RUN).unwrap();
        fs::write(path.join("feature.txt"), "verified implementation\n").unwrap();
        run_test_git(&path, &["add", "feature.txt"]);
        let context = AutomatedAgentChildContext {
            project_id: project.id,
            run_token: RUN.into(),
        };
        move_task_to_done_with_agent_store(&path, TaskStatus::Doing, "1", &context, &store)
            .unwrap();
        run_test_git(&path, &["add", "--all"]);
        run_test_agent_git(
            &path,
            &[
                "commit",
                "-m",
                "Fix feather mask",
                "-m",
                &format!("CLT-Task: codex:{COMPLETED}"),
            ],
        );
        let pending = store
            .git_finalization_blocking(project.id, COMPLETED)
            .unwrap()
            .unwrap();
        let completed = if finish {
            let completed =
                reconcile_agent_git_finalization(&store, &path, pending, Some(RUN), None).unwrap();
            assert_eq!(completed.state, GitFinalizationState::Completed);
            completed
        } else {
            pending
        };
        store
            .clear_running_session_control_blocking(project.id, COMPLETED, Some(RUN))
            .unwrap();
        let mut baseline: serde_json::Value =
            serde_json::from_str(&start.worktree_baseline).unwrap();
        if let Some((field, value)) = sealed_field {
            baseline[field] = value;
        }
        let baseline = serde_json::to_string(&baseline).unwrap();
        store
            .create_git_finalization_blocking(agent::NewGitFinalization {
                project_id: project.id,
                codex_session_id: OLD,
                git_mode: AgentGitMode::Commit,
                starting_head: Some(&start.starting_head),
                branch_ref: start.branch_ref.as_deref(),
                upstream_ref: start.upstream_ref.as_deref(),
                worktree_baseline: &baseline,
                task_identity: completed.task_identity.as_deref(),
                owner_run_token: None,
                created_at: "100",
            })
            .unwrap();
        store
            .ensure_pending_git_finalization_resume_requested_blocking(project.id, OLD)
            .unwrap();
        let old = store
            .git_finalization_blocking(project.id, OLD)
            .unwrap()
            .unwrap();
        Self {
            root,
            state,
            project,
            store,
            old,
            completed,
        }
    }

    fn retire(&self) -> usize {
        retire_superseded_working_git_journals(&self.state, &self.project).unwrap()
    }

    fn assert_unchanged(&self) {
        assert_eq!(
            self.store
                .git_finalization_blocking(self.project.id, OLD)
                .unwrap()
                .unwrap(),
            self.old
        );
        assert_eq!(
            self.store
                .git_finalization_blocking(self.project.id, COMPLETED)
                .unwrap()
                .unwrap(),
            self.completed
        );
        assert!(
            self.store
                .session_control_blocking(self.project.id, OLD)
                .unwrap()
                .is_some()
        );
    }

    fn cleanup(self) {
        drop(self.store);
        fs::remove_dir_all(self.root).unwrap();
    }
}

#[test]
fn superseded_working_attempt_retires_only_after_proven_completion() {
    for folders in [false, true] {
        let fixture = Fixture::new(folders, None, true);
        let now = agent_timestamp();
        fixture
            .store
            .record_run_outcome_blocking(agent::AgentRunOutcome {
                project_id: fixture.project.id,
                status: "failure",
                started_at: &now,
                finished_at: Some(&now),
                exit_code: None,
                log_dir: None,
                stdout_path: None,
                stderr_path: None,
                summary: Some("The task's Git journal remains WORKING"),
                codex_session_id: Some(OLD),
            })
            .unwrap();
        fs::write(
            fixture.project.path.join("shared.txt"),
            "shared staged work\n",
        )
        .unwrap();
        run_test_git(&fixture.project.path, &["add", "shared.txt"]);
        fs::write(
            fixture.project.path.join("feature.txt"),
            "concurrent edit\n",
        )
        .unwrap();
        let head = run_test_git(&fixture.project.path, &["rev-parse", "HEAD"]);
        let index = run_test_git(&fixture.project.path, &["write-tree"]);
        let diff = run_test_git(&fixture.project.path, &["diff", "HEAD"]);
        assert_eq!(
            reconcile_orphaned_agent_git_journals(&fixture.state, &fixture.project).unwrap(),
            1
        );
        assert_eq!(fixture.retire(), 0);
        let cancelled = fixture
            .store
            .git_finalization_blocking(fixture.project.id, OLD)
            .unwrap()
            .unwrap();
        assert_eq!(cancelled.state, GitFinalizationState::Cancelled);
        assert_eq!(cancelled.generation, fixture.old.generation + 1);
        assert_eq!(cancelled.task_identity, fixture.old.task_identity);
        assert_eq!(cancelled.starting_head, fixture.old.starting_head);
        assert_eq!(cancelled.worktree_baseline, fixture.old.worktree_baseline);
        assert!(cancelled.last_error.as_deref().unwrap().contains(COMPLETED));
        assert!(
            fixture
                .store
                .session_control_blocking(fixture.project.id, OLD)
                .unwrap()
                .is_none()
        );
        assert_eq!(
            fixture
                .store
                .git_finalization_blocking(fixture.project.id, COMPLETED)
                .unwrap()
                .unwrap(),
            fixture.completed
        );
        assert_eq!(
            fixture.store.list_projects_blocking().unwrap()[0].failure_count,
            0
        );
        assert_eq!(
            run_test_git(&fixture.project.path, &["rev-parse", "HEAD"]),
            head
        );
        assert_eq!(run_test_git(&fixture.project.path, &["write-tree"]), index);
        assert_eq!(run_test_git(&fixture.project.path, &["diff", "HEAD"]), diff);
        fixture.cleanup();
    }
}

#[test]
fn superseded_working_attempt_preserves_uncertain_or_live_ownership() {
    for case in [
        "unfinished-proof",
        "duplicate-task",
        "old-marker",
        "live-session",
        "lease",
        "worker",
        "launch",
        "missing-completion",
    ] {
        let fixture = Fixture::new(false, None, case != "unfinished-proof");
        let board = TaskBoard::for_project(&fixture.project.path);
        match case {
            "duplicate-task" => board.insert_content(TaskStatus::Todo, None, TASK).unwrap(),
            "old-marker" => board
                .insert_content(
                    TaskStatus::Backlog,
                    None,
                    &format!("Different text codex:{OLD}"),
                )
                .unwrap(),
            "live-session" => {
                fixture
                    .store
                    .mark_session_running_blocking(
                        fixture.project.id,
                        OLD,
                        123,
                        "live",
                        &fixture.root.join("out"),
                        &fixture.root.join("err"),
                    )
                    .unwrap();
            }
            "lease" | "worker" => {
                fixture
                    .store
                    .try_acquire_lease_blocking(
                        fixture.project.id,
                        "live",
                        &agent_timestamp(),
                        &agent_timestamp_after(600),
                    )
                    .unwrap();
                if case == "worker" {
                    assert!(reserve_test_worker(
                        &fixture.store,
                        fixture.project.id,
                        "live-worker",
                        "live",
                        &agent_timestamp(),
                        12
                    ));
                }
            }
            "launch" => {
                let start =
                    capture_agent_git_start_state(&fixture.project.path, AgentGitMode::Commit)
                        .unwrap();
                assert!(
                    fixture
                        .store
                        .record_git_launch_state_blocking(
                            fixture.project.id,
                            "pending-launch",
                            AgentGitMode::Commit,
                            &start,
                            &agent_timestamp(),
                        )
                        .unwrap()
                );
            }
            "missing-completion" => {
                let entry = board.entries(TaskStatus::Done).unwrap().remove(0);
                board
                    .write_entry_content(
                        TaskStatus::Done,
                        &entry,
                        &format!("Different task codex:{COMPLETED}"),
                    )
                    .unwrap();
            }
            _ => {}
        }
        assert_eq!(fixture.retire(), 0, "{case}");
        fixture.assert_unchanged();
        fixture.cleanup();
    }
}

#[test]
fn superseded_working_attempt_preserves_partial_and_malformed_seals() {
    for (field, value) in [
        ("staged_index_tree", serde_json::json!("tree")),
        ("manifest_parent_head", serde_json::json!("parent")),
        ("staged_non_task_patch_ids", serde_json::json!({})),
        ("staged_index_tree", serde_json::json!(false)),
    ] {
        let fixture = Fixture::new(false, Some((field, value)), true);
        assert_eq!(fixture.retire(), 0);
        fixture.assert_unchanged();
        fixture.cleanup();
    }
}

#[test]
fn superseded_working_retirement_rechecks_both_journal_snapshots() {
    let fixture = Fixture::new(false, None, true);
    for change_old in [false, true] {
        let mut old = fixture.old.clone();
        let mut completed = fixture.completed.clone();
        if change_old {
            old.generation += 1;
        } else {
            completed.generation += 1;
        }
        assert!(
            !fixture
                .store
                .retire_superseded_working_git_finalization_blocking(
                    &old,
                    &completed,
                    &agent_timestamp()
                )
                .unwrap()
        );
        fixture.assert_unchanged();
    }
    fixture.cleanup();
}