clt-rs 0.9.3

File-backed task manager with a TUI Kanban board and multi-project Codex agent registry
use super::*;
use crate::{
    agent::TursoAgentStore,
    task::{TaskBoard, init_tasks},
    test_support::temp_root,
    worker::tests::reserve_test_worker,
};
use std::{fs, path::PathBuf};

fn fixture(folders: bool) -> (PathBuf, TursoAgentStore, AgentWorkerRecord) {
    let root = temp_root("worker-activity");
    let project = root.join("project");
    init_tasks(&project, folders).unwrap();
    let store = TursoAgentStore::open_blocking(&root.join("state")).unwrap();
    store
        .register_project_blocking(&project, "project")
        .unwrap();
    let id = store.list_projects_blocking().unwrap()[0].id;
    assert!(
        store
            .try_acquire_lease_blocking(id, "scheduler", "100", "9999999999")
            .unwrap()
    );
    assert!(reserve_test_worker(
        &store,
        id,
        "worker",
        "scheduler",
        "101",
        1
    ));
    assert!(
        store
            .claim_worker_blocking("worker", std::process::id(), "102")
            .unwrap()
    );
    let worker = store.list_active_workers_blocking().unwrap().remove(0);
    (root, store, worker)
}

#[test]
fn activity_follows_the_exact_worker_session_across_board_moves() {
    for folders in [false, true] {
        let (root, store, mut worker) = fixture(folders);
        let board = TaskBoard::new(get_tasks_dir(&worker.project_path));
        board
            .insert_content(TaskStatus::Todo, None, "Other task codex:other")
            .unwrap();
        board
            .insert_content(TaskStatus::Todo, None, "Feather mask codex:owned")
            .unwrap();
        for (session, token) in [("other", "other-worker"), ("owned", "worker")] {
            store
                .mark_session_running_blocking(
                    worker.project_id,
                    session,
                    std::process::id(),
                    token,
                    &root.join("out"),
                    &root.join("err"),
                )
                .unwrap();
        }
        let controls = store
            .session_controls_for_project_blocking(worker.project_id)
            .unwrap();
        let activity = describe_worker_activity(&worker, &controls);
        assert_eq!(activity.session_id.as_deref(), Some("owned"));
        assert_eq!(activity.summary, "Working on: TODO #2 — Feather mask");
        board
            .move_task_after_lock(TaskStatus::Todo, TaskStatus::Doing, 2)
            .unwrap();
        worker.task_selection = "resume_session".into();
        worker.resume_session_id = Some("owned".into());
        let activity = describe_worker_activity(&worker, &controls);
        assert_eq!(activity.phase, WorkerActivityPhase::Resuming);
        assert_eq!(activity.summary, "Resuming: DOING #1 — Feather mask");
        board
            .move_task_after_lock(TaskStatus::Doing, TaskStatus::Done, 1)
            .unwrap();
        worker.state = "finalizing".into();
        let activity = describe_worker_activity(&worker, &controls);
        assert_eq!(activity.phase, WorkerActivityPhase::Settling);
        assert!(activity.summary.contains("DONE #1 — Feather mask"));
        fs::remove_dir_all(root).unwrap();
    }
}

#[test]
fn activity_does_not_guess_a_task_from_queue_order_or_reviewer_session() {
    let (root, store, mut worker) = fixture(false);
    TaskBoard::new(get_tasks_dir(&worker.project_path))
        .insert_content(TaskStatus::Doing, None, "Unrelated task")
        .unwrap();
    let activity = describe_worker_activity(&worker, &[]);
    assert_eq!(activity.phase, WorkerActivityPhase::Starting);
    assert!(activity.summary.contains("waiting for task ownership"));
    assert!(!activity.summary.contains("Unrelated"));
    worker.task_selection = "supervisor_review".into();
    let activity = describe_worker_activity(&worker, &[]);
    assert_eq!(activity.phase, WorkerActivityPhase::Reviewing);
    assert_eq!(
        activity.summary,
        "Running: supervisor review of blocked tasks"
    );
    worker.task_selection = "resume_session".into();
    worker.resume_session_id = Some("missing-session".into());
    assert!(
        describe_worker_activity(&worker, &[])
            .summary
            .contains("waiting for its task link")
    );
    store
        .mark_session_running_blocking(
            worker.project_id,
            "conflicting-session",
            std::process::id(),
            "worker",
            &root.join("out"),
            &root.join("err"),
        )
        .unwrap();
    let controls = store
        .session_controls_for_project_blocking(worker.project_id)
        .unwrap();
    let ambiguous = describe_worker_activity(&worker, &controls);
    assert!(ambiguous.summary.contains("ambiguous"));
    assert!(ambiguous.session_id.is_none());
    fs::remove_dir_all(root).unwrap();
}

#[test]
fn activity_locates_nested_tasks_and_reports_duplicate_links() {
    let (root, _store, mut worker) = fixture(true);
    let nested = get_tasks_dir(&worker.project_path).join("doing/0001-epic");
    fs::create_dir_all(&nested).unwrap();
    fs::write(nested.join("task.md"), "Parent epic").unwrap();
    fs::write(nested.join("todo.md"), "- Nested task codex:owned\n").unwrap();
    worker.resume_session_id = Some("owned".into());
    let activity = describe_worker_activity(&worker, &[]);
    assert!(
        activity
            .summary
            .contains("doing/0001-epic / TODO #1 — Nested task")
    );
    fs::write(nested.join("doing.md"), "- Duplicate task codex:owned\n").unwrap();
    assert!(
        describe_worker_activity(&worker, &[])
            .summary
            .contains("multiple task links")
    );
    fs::remove_dir_all(root).unwrap();
}