clt-rs 0.9.5

File-backed task manager with a TUI Kanban board and multi-project Codex agent registry
use super::*;
use crate::{
    session_control::steering::tests::Fixture,
    task::{TaskStatus, read_task_entries},
};

fn selected_task(f: &Fixture, folders: bool, nested: bool) -> TaskEntry {
    let board = if folders && !nested {
        f.path.parent().unwrap().parent().unwrap()
    } else {
        f.path.parent().unwrap()
    };
    read_task_entries(board, TaskStatus::Doing)
        .unwrap()
        .remove(0)
}

fn snapshot(f: &Fixture) -> ProgressSnapshot {
    ProgressSnapshot {
        target: f.target.clone(),
        recorded_at: "2026-10-23T12:00:00Z".into(),
        update: Some("Implementation is ready; running the regression tests.".into()),
        activity: "Command running: cargo test".into(),
    }
}

#[test]
fn progress_reads_only_the_selected_run_without_changing_tasks_or_controls() {
    for folders in [false, true] {
        for nested in [false, true] {
            let f = Fixture::new(folders, nested);
            let task = selected_task(&f, folders, nested);
            let before_task = fs::read(&f.path).unwrap();
            let before_control = f
                .store
                .session_control_blocking(f.project.id, &f.target.session_id)
                .unwrap();
            let path = f.target.mailbox.join("progress.json");
            steering::write_json(&path, &snapshot(&f)).unwrap();
            f.store
                .mark_session_running_blocking(
                    f.project.id,
                    "other-session",
                    std::process::id(),
                    "other-run",
                    &f.temp.path().join("other.out"),
                    &f.temp.path().join("other.err"),
                )
                .unwrap();
            let summary = task_progress_at(&f.state, &f.project.path, &task).unwrap();
            assert!(summary.contains("Agent: running"));
            assert!(summary.contains("Implementation is ready"));
            assert!(summary.contains("cargo test"));
            assert!(summary.contains("2026-10-23T12:00:00Z"));
            assert!(!summary.contains("other-session"));
            assert_eq!(fs::read(&f.path).unwrap(), before_task);
            assert_eq!(
                f.store
                    .session_control_blocking(f.project.id, &f.target.session_id)
                    .unwrap(),
                before_control
            );
            assert_eq!(fs::read_dir(&f.target.mailbox).unwrap().count(), 2);
        }
    }
}

#[test]
fn progress_rejects_stale_runs_and_handles_missing_or_corrupt_snapshots() {
    let f = Fixture::new(false, false);
    let task = selected_task(&f, false, false);
    let path = f.target.mailbox.join("progress.json");
    for wrong in 0..6 {
        let mut progress = snapshot(&f);
        match wrong {
            0 => progress.target.run_token = "old-run".into(),
            1 => progress.target.session_id = "other-session".into(),
            2 => progress.target.project_id += 1,
            3 => progress.target.child_pid += 1,
            4 => progress.target.mailbox = f.temp.path().join("other-mailbox"),
            _ => {}
        }
        if wrong == 5 {
            fs::write(&path, "partial json").unwrap();
        } else {
            steering::write_json(&path, &progress).unwrap();
        }
        let summary = task_progress_at(&f.state, &f.project.path, &task).unwrap();
        assert!(summary.contains("No progress details"));
        assert!(!summary.contains("Implementation is ready"));
    }
    fs::remove_file(path).unwrap();
    assert!(
        task_progress_at(&f.state, &f.project.path, &task)
            .unwrap()
            .contains("No progress details")
    );
}

#[test]
fn progress_for_unlinked_or_stopped_tasks_never_starts_a_session() {
    let f = Fixture::new(false, false);
    let mut task = selected_task(&f, false, false);
    task.content = "Waiting task".into();
    let absent_state = f.temp.path().join("no-state");
    assert!(
        task_progress_at(&absent_state, &f.project.path, &task)
            .unwrap()
            .contains("No agent session")
    );
    task.content.push_str(" clt:stopped");
    assert!(
        task_progress_at(&absent_state, &f.project.path, &task)
            .unwrap()
            .contains("Stopped before starting")
    );
    assert!(!absent_state.exists());
}

#[test]
fn progress_text_is_bounded_and_unicode_safe() {
    let text = format!("\x1b\t{}\nsecond paragraph", "好".repeat(500));
    let shortened = short_text(&text, 400);
    assert!(shortened.ends_with('…'));
    assert!(shortened.chars().count() <= 401);
    assert!(!shortened.chars().any(char::is_control));
    assert_eq!(short_text("one\n\ntwo", 20), "one two");
}