codex-helper-core 0.15.0

Core library for codex-helper.
Documentation
use super::*;

use pretty_assertions::assert_eq;

#[test]
fn session_cwd_parent_of_current_dir_matches() {
    let base = std::env::current_dir().expect("cwd");
    let project = base.join("codex_project_parent");
    let child = project.join("subdir");
    let session_cwd = project.to_str().expect("project path utf8").to_string();

    assert!(
        path_matches_current_dir(&session_cwd, &child),
        "session cwd should match when it is a parent of current dir"
    );
}

#[test]
fn session_cwd_child_of_current_dir_matches() {
    let base = std::env::current_dir().expect("cwd");
    let project = base.join("codex_project_child");
    let child = project.join("subdir");
    let session_cwd = child.to_str().expect("child path utf8").to_string();

    assert!(
        path_matches_current_dir(&session_cwd, &project),
        "session cwd should match when it is a child of current dir"
    );
}

#[test]
fn unrelated_paths_do_not_match() {
    let base = std::env::current_dir().expect("cwd");
    let project = base.join("codex_project_main");
    let other = base.join("other_project_main");
    let session_cwd = other.to_str().expect("other path utf8").to_string();

    assert!(
        !path_matches_current_dir(&session_cwd, &project),
        "unrelated paths should not match"
    );
}

#[test]
fn parse_rollout_filename_splits_uuid_correctly() {
    let name = "rollout-2025-12-20T16-01-02-550e8400-e29b-41d4-a716-446655440000.jsonl";
    let (ts, uuid) = parse_timestamp_and_uuid(name).expect("should parse");
    assert_eq!(ts, "2025-12-20T16-01-02");
    assert_eq!(uuid, "550e8400-e29b-41d4-a716-446655440000");
}

#[tokio::test]
async fn summarize_session_tracks_rounds_and_last_response() {
    let dir = std::env::temp_dir().join(format!("codex-helper-test-{}", uuid::Uuid::new_v4()));
    std::fs::create_dir_all(&dir).expect("create tmp dir");
    let path = dir.join("rollout-2025-12-22T00-00-00-00000000-0000-0000-0000-000000000000.jsonl");
    let cwd = dir.join("project");
    std::fs::create_dir_all(&cwd).expect("create cwd dir");
    let cwd_str = cwd.to_str().expect("cwd utf8");

    let meta_line = serde_json::json!({
        "timestamp": "2025-12-22T00:00:00.000Z",
        "type": "session_meta",
        "payload": {
            "id": "sid-1",
            "cwd": cwd_str,
            "timestamp": "2025-12-22T00:00:00.000Z"
        }
    })
    .to_string();
    let lines = [
            meta_line,
            r#"{"timestamp":"2025-12-22T00:00:01.000Z","type":"event_msg","payload":{"type":"user_message","message":"hi"}}"#.to_string(),
            r#"{"timestamp":"2025-12-22T00:00:02.000Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"hello"}]}}"#.to_string(),
            r#"{"timestamp":"2025-12-22T00:00:03.000Z","type":"event_msg","payload":{"type":"user_message","message":"next"}}"#.to_string(),
            r#"{"timestamp":"2025-12-22T00:00:04.000Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"ok"}]}}"#.to_string(),
        ]
        .join("\n");
    std::fs::write(&path, lines).expect("write session file");

    let summary = summarize_session_for_current_dir(&path, &cwd)
        .await
        .expect("summarize ok")
        .expect("some summary");

    assert_eq!(
        summary.user_turns, 2,
        "should count user_message events as user turns"
    );
    assert_eq!(
        summary.assistant_turns, 2,
        "should count assistant response_item messages"
    );
    assert_eq!(summary.rounds, 2, "rounds should match assistant turns");
    assert_eq!(
        summary.last_response_at.as_deref(),
        Some("2025-12-22T00:00:04.000Z")
    );
    assert_eq!(
        summary.updated_at.as_deref(),
        Some("2025-12-22T00:00:04.000Z"),
        "updated_at should prefer last_response_at"
    );
}

#[tokio::test]
async fn read_codex_session_transcript_extracts_messages_and_tail() {
    let dir = std::env::temp_dir().join(format!("codex-helper-test-{}", uuid::Uuid::new_v4()));
    std::fs::create_dir_all(&dir).expect("create tmp dir");
    let path = dir.join("rollout-2025-12-22T00-00-00-00000000-0000-0000-0000-000000000000.jsonl");

    let meta_line = serde_json::json!({
        "timestamp": "2025-12-22T00:00:00.000Z",
        "type": "session_meta",
        "payload": {
            "id": "00000000-0000-0000-0000-000000000000",
            "cwd": "G:/code/project",
            "timestamp": "2025-12-22T00:00:00.000Z"
        }
    })
    .to_string();

    let lines = [
            meta_line,
            r#"{"timestamp":"2025-12-22T00:00:01.000Z","type":"event_msg","payload":{"type":"user_message","message":"hi"}}"#.to_string(),
            r#"{"timestamp":"2025-12-22T00:00:02.000Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"hello"}]}}"#.to_string(),
            r#"{"timestamp":"2025-12-22T00:00:03.000Z","type":"event_msg","payload":{"type":"user_message","message":"next"}}"#.to_string(),
            r#"{"timestamp":"2025-12-22T00:00:04.000Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"ok"}]}}"#.to_string(),
        ]
        .join("\n");
    std::fs::write(&path, lines).expect("write session file");

    let all = read_codex_session_transcript(&path, None)
        .await
        .expect("read transcript ok");
    assert_eq!(all.len(), 4);
    assert_eq!(all[0].role, "User");
    assert_eq!(all[0].text, "hi");
    assert_eq!(all[1].role, "Assistant");
    assert_eq!(all[1].text, "hello");

    let tail = read_codex_session_transcript(&path, Some(2))
        .await
        .expect("read tail ok");
    assert_eq!(tail.len(), 2);
    assert_eq!(tail[0].text, "next");
    assert_eq!(tail[1].text, "ok");

    assert!(
        codex_session_transcript_tail_contains_query(&path, "HELLO", 3)
            .await
            .expect("search ok"),
        "should match case-insensitively within tail"
    );
    assert!(
        !codex_session_transcript_tail_contains_query(&path, "missing", 10)
            .await
            .expect("search ok"),
        "should return false when not found"
    );
}

#[tokio::test]
async fn recent_sessions_filters_by_mtime_and_prefers_meta_id() {
    let tmp = std::env::temp_dir().join(format!("codex-helper-test-{}", uuid::Uuid::new_v4()));
    std::fs::create_dir_all(&tmp).expect("create tmp dir");

    let sessions = tmp.join("sessions").join("2026").join("02").join("01");
    std::fs::create_dir_all(&sessions).expect("create sessions dir");

    let file1 =
        sessions.join("rollout-2026-02-01T00-00-00-11111111-1111-1111-1111-111111111111.jsonl");
    let file2 =
        sessions.join("rollout-2026-02-01T00-00-01-22222222-2222-2222-2222-222222222222.jsonl");

    let meta1 = serde_json::json!({
        "timestamp": "2026-02-01T00:00:00.000Z",
        "type": "session_meta",
        "payload": {
            "id": "sid-old",
            "cwd": "G:/code/old",
            "timestamp": "2026-02-01T00:00:00.000Z"
        }
    })
    .to_string();
    std::fs::write(&file1, meta1).expect("write file1");

    std::thread::sleep(std::time::Duration::from_millis(50));

    let meta2 = serde_json::json!({
        "timestamp": "2026-02-01T00:00:01.000Z",
        "type": "session_meta",
        "payload": {
            "id": "sid-new",
            "cwd": "G:/code/new",
            "timestamp": "2026-02-01T00:00:01.000Z"
        }
    })
    .to_string();
    std::fs::write(&file2, meta2).expect("write file2");

    let recent = find_recent_codex_sessions_in_dir(
        &tmp.join("sessions"),
        Duration::from_secs(24 * 3600),
        10,
    )
    .await
    .expect("recent ok");
    assert_eq!(recent.len(), 2);
    assert_eq!(recent[0].id, "sid-new");
    assert_eq!(recent[1].id, "sid-old");

    let none = find_recent_codex_sessions_in_dir(&tmp.join("sessions"), Duration::from_secs(0), 10)
        .await
        .expect("recent ok");
    assert_eq!(none.len(), 0, "since=0 should filter everything out");
}

#[tokio::test]
async fn batch_find_session_files_resolves_uuid_and_meta_ids() {
    let tmp = std::env::temp_dir().join(format!("codex-helper-test-{}", uuid::Uuid::new_v4()));
    let sessions = tmp.join("sessions").join("2026").join("03").join("12");
    std::fs::create_dir_all(&sessions).expect("create sessions dir");

    let file_by_uuid =
        sessions.join("rollout-2026-03-12T00-00-00-11111111-1111-1111-1111-111111111111.jsonl");
    let file_by_meta =
        sessions.join("rollout-2026-03-12T00-00-01-22222222-2222-2222-2222-222222222222.jsonl");

    std::fs::write(
        &file_by_uuid,
        serde_json::json!({
            "timestamp": "2026-03-12T00:00:00.000Z",
            "type": "session_meta",
            "payload": {
                "id": "11111111-1111-1111-1111-111111111111",
                "cwd": "G:/code/by-uuid",
                "timestamp": "2026-03-12T00:00:00.000Z"
            }
        })
        .to_string(),
    )
    .expect("write uuid file");

    std::fs::write(
        &file_by_meta,
        serde_json::json!({
            "timestamp": "2026-03-12T00:00:01.000Z",
            "type": "session_meta",
            "payload": {
                "id": "sid-meta",
                "cwd": "G:/code/by-meta",
                "timestamp": "2026-03-12T00:00:01.000Z"
            }
        })
        .to_string(),
    )
    .expect("write meta file");

    let found = find_codex_session_files_by_ids_in_dir(
        &tmp.join("sessions"),
        &[
            "11111111-1111-1111-1111-111111111111".to_string(),
            "sid-meta".to_string(),
            "missing".to_string(),
        ],
    )
    .await
    .expect("batch find ok");

    assert_eq!(
        found
            .get("11111111-1111-1111-1111-111111111111")
            .expect("uuid match"),
        &file_by_uuid
    );
    assert_eq!(found.get("sid-meta").expect("meta match"), &file_by_meta);
    assert!(
        !found.contains_key("missing"),
        "missing session ids should not be included"
    );
}