ai-dispatch 10.24.0

Multi-AI CLI team orchestrator
// Literal aggregation tests for the TUI statistics snapshot.
// Covers day bucketing, range filtering, and project rollups.
// Deps: tui::stats and crate task types.

use super::*;
use crate::types::{AgentKind, Task, TaskId, TaskStatus, VerifyStatus};
use chrono::{DateTime, Local, TimeZone};

fn task(id: &str, created: &str, completed: Option<&str>, repo: Option<&str>, tokens: Option<i64>) -> Task {
    Task {
        id: TaskId(id.to_string()), agent: AgentKind::Codex, custom_agent_name: None,
        prompt: String::new(), resolved_prompt: None, category: None, status: TaskStatus::Done,
        parent_task_id: None, workgroup_id: None, caller_kind: None, caller_session_id: None,
        agent_session_id: None, repo_path: repo.map(str::to_string), worktree_path: None,
        worktree_branch: None, final_head_sha: None, final_branch: None, start_sha: None,
        log_path: None, output_path: None, tokens, prompt_tokens: None, duration_ms: None,
        requested_model: None, observed_model: None, attribution_source: None, cost_usd: None,
        exit_code: Some(0), created_at: at(created), completed_at: completed.map(at),
        verify: None, verify_status: VerifyStatus::Skipped, pending_reason: None,
        read_only: false, budget: false, audit_verdict: None, audit_report_path: None,
        delivery_assessment: None,
    }
}

fn at(value: &str) -> DateTime<Local> {
    Local.datetime_from_str(value, "%Y-%m-%d %H:%M:%S").expect("test timestamp")
}

#[test]
fn buckets_tasks_by_created_day_and_sums_recorded_tokens() {
    let tasks = vec![
        task("t-1", "2026-08-10 09:00:00", Some("2026-08-10 09:10:00"), Some("/repo/a"), Some(120)),
        task("t-2", "2026-08-10 12:00:00", None, Some("/repo/a"), None),
        task("t-3", "2026-08-11 12:00:00", Some("2026-08-11 12:20:00"), Some("/repo/b"), Some(80)),
    ];
    let snapshot = aggregate_tasks(&tasks, StatsRange::AllTime, at("2026-08-11 18:00:00"));

    assert_eq!(snapshot.total_tasks, 3);
    assert_eq!(snapshot.activity[0].date.to_string(), "2026-08-10");
    assert_eq!(snapshot.activity[0].task_count, 2);
    assert_eq!(snapshot.activity[0].tokens, 120);
    assert_eq!(snapshot.activity[1].task_count, 1);
    assert_eq!(snapshot.activity[1].tokens, 80);
}

#[test]
fn filters_tasks_to_selected_date_range() {
    let tasks = vec![
        task("t-old", "2026-07-01 09:00:00", Some("2026-07-01 09:10:00"), Some("/repo/a"), Some(10)),
        task("t-in", "2026-08-06 09:00:00", Some("2026-08-06 09:10:00"), Some("/repo/a"), Some(20)),
        task("t-today", "2026-08-12 09:00:00", None, Some("/repo/b"), Some(30)),
    ];
    let snapshot = aggregate_tasks(&tasks, StatsRange::Last7Days, at("2026-08-12 18:00:00"));

    assert_eq!(snapshot.total_tasks, 2);
    assert_eq!(snapshot.total_tokens, 50);
    assert_eq!(snapshot.activity.len(), 7);
    assert_eq!(snapshot.activity[0].date.to_string(), "2026-08-06");
    assert_eq!(snapshot.activity[6].task_count, 1);
}

#[test]
fn rolls_up_projects_and_ranks_by_task_count_then_tokens() {
    let tasks = vec![
        task("t-1", "2026-08-10 09:00:00", Some("2026-08-10 09:01:00"), Some("/repo/a"), Some(10)),
        task("t-2", "2026-08-10 10:00:00", Some("2026-08-10 10:03:00"), Some("/repo/a"), Some(30)),
        task("t-3", "2026-08-10 11:00:00", Some("2026-08-10 11:02:00"), Some("/repo/b"), Some(100)),
        task("t-4", "2026-08-10 12:00:00", None, None, None),
    ];
    let snapshot = aggregate_tasks(&tasks, StatsRange::AllTime, at("2026-08-10 18:00:00"));

    assert_eq!(snapshot.projects[0].name, "/repo/a");
    assert_eq!(snapshot.projects[0].task_count, 2);
    assert_eq!(snapshot.projects[0].tokens, 40);
    assert_eq!(snapshot.projects[0].duration_secs, 240);
    assert_eq!(snapshot.projects[1].name, "/repo/b");
    assert_eq!(snapshot.projects[2].name, "(no repo_path)");
    assert_eq!(snapshot.projects[2].token_task_count, 0);
}

#[test]
fn range_selector_cycles_in_display_order() {
    assert_eq!(StatsRange::AllTime.next(), StatsRange::Last30Days);
    assert_eq!(StatsRange::Last30Days.next(), StatsRange::Last7Days);
    assert_eq!(StatsRange::Last7Days.next(), StatsRange::Today);
    assert_eq!(StatsRange::Today.next(), StatsRange::AllTime);
    assert_eq!(StatsRange::AllTime.previous(), StatsRange::Today);
    assert_eq!(StatsRange::Today.label(), "Today");
}