malvin 0.2.5

Non-interactive research and coding agent
use std::time::{Duration, Instant};

use super::CursorSdkClient;
use super::client_mock_tests::{
    clear_mock_bridge_env, install_mock_bridge_env, mock_bridge_path, mock_client, prompt_once,
};
use crate::bridge_sdk::SDK_BRIDGE_MAX_AGE;

struct EnsureFixture {
    client: CursorSdkClient,
    tmp: tempfile::TempDir,
    started: Instant,
}

fn bridge_started_at(client: &CursorSdkClient) -> Instant {
    crate::agent_backend::live_session(client)
        .and_then(|s| s.as_cursor())
        .expect("open")
        .started_at
}

fn backdate_bridge(client: &mut CursorSdkClient, started: Instant) {
    crate::agent_backend::live_session_mut(client)
        .and_then(|s| s.as_cursor_mut())
        .expect("open")
        .started_at = started
        .checked_sub(SDK_BRIDGE_MAX_AGE + Duration::from_secs(1))
        .expect("backdate");
}

fn count_agent_start_log_lines(stdout_log: &std::path::Path, model: &str) -> usize {
    let text = std::fs::read_to_string(stdout_log).unwrap_or_default();
    let delim = crate::output::format_who_tag_delim(crate::output::WHO_A);
    let needle = format!("{delim}{model}");
    text.lines().filter(|line| line.contains(&needle)).count()
}

async fn open_ensure_fixture() -> EnsureFixture {
    install_mock_bridge_env(&mock_bridge_path());
    let tmp = tempfile::tempdir().expect("tmp");
    let stdout_log = tmp.path().join("stdout.log");
    crate::output::set_stdout_log_path(Some(stdout_log.clone()));
    let mut client = mock_client(tmp.path());
    let _ = client.attach_run_timing_for_session();
    let started_new = client
        .ensure_coder_session(tmp.path())
        .await
        .expect("ensure");
    assert!(
        started_new.is_fresh(),
        "first ensure must create a fresh agent context"
    );
    assert_eq!(
        count_agent_start_log_lines(&stdout_log, &client.model.canonical()),
        1,
        "fresh agent start must log a|<model>"
    );
    let started = bridge_started_at(&client);
    EnsureFixture {
        client,
        tmp,
        started,
    }
}

async fn end_fixture(mut fixture: EnsureFixture) {
    fixture.client.end_coder_session().await.expect("end");
    crate::output::set_stdout_log_path(None);
    clear_mock_bridge_env();
}

#[tokio::test]
async fn cursor_sdk_ensure_reuses_fresh_bridge() {
    let _guard = crate::test_utils::test_env_lock();
    let mut fixture = open_ensure_fixture().await;
    let stdout_log = fixture.tmp.path().join("stdout.log");
    let model = fixture.client.model.canonical();
    assert!(!crate::agent_backend::sdk_bridge_needs_restart(
        &fixture.client
    ));
    let started = fixture
        .client
        .ensure_coder_session(fixture.tmp.path())
        .await
        .expect("ensure again");
    assert!(
        !started.is_fresh(),
        "fresh bridge must report reuse, not a fresh agent context"
    );
    assert_eq!(
        fixture.started,
        bridge_started_at(&fixture.client),
        "fresh bridge must not restart"
    );
    assert_eq!(
        count_agent_start_log_lines(&stdout_log, &model),
        1,
        "reused open session must not emit another a| line"
    );
    end_fixture(fixture).await;
}

#[tokio::test]
async fn cursor_sdk_ensure_restarts_stale_bridge() {
    let _guard = crate::test_utils::test_env_lock();
    let mut fixture = open_ensure_fixture().await;
    let stdout_log = fixture.tmp.path().join("stdout.log");
    let model = fixture.client.model.canonical();
    backdate_bridge(&mut fixture.client, fixture.started);
    assert!(crate::agent_backend::sdk_bridge_needs_restart(
        &fixture.client
    ));
    let fresh_context = fixture
        .client
        .ensure_coder_session(fixture.tmp.path())
        .await
        .expect("ensure restart");
    assert!(
        !fresh_context.is_fresh(),
        "stale bridge resume continues the prior agent; do not send header.md again"
    );
    assert!(bridge_started_at(&fixture.client) > fixture.started);
    assert!(!crate::agent_backend::sdk_bridge_needs_restart(
        &fixture.client
    ));
    assert_eq!(
        count_agent_start_log_lines(&stdout_log, &model),
        1,
        "resume restart must not emit another a| line"
    );
    prompt_once(&mut fixture.client, &fixture.tmp.path().join("prompts.log")).await;
    end_fixture(fixture).await;
}