malvin 0.2.5

Non-interactive research and coding agent
use crate::acp::CoderPromptOptions;

use super::sdk_bug_helpers::{
    assert_err_has, bug_clear_env, bug_client, bug_client_noforce, bug_prepare, expect_prompt_err,
};

#[tokio::test]
async fn failed_create_drop_clears_sandbox_for_next_spawn() {
    let _guard = crate::test_utils::test_env_lock();
    let tmp = bug_prepare();
    let mut client = bug_client_noforce(tmp.path());
    let err = client
        .begin_coder_session(tmp.path())
        .await
        .expect_err("no-force create must fail");
    assert_err_has(&err, &["--no-force", "not supported"]);
    assert!(!client.has_open_coder_session());
    crate::malvin_sandbox::assert_dead_before_next_spawn()
        .expect("sandbox must be clear after failed BridgeSession drop");
    let mut client2 = bug_client(tmp.path(), 1);
    client2
        .begin_coder_session(tmp.path())
        .await
        .expect("second begin after failed create");
    client2.end_coder_session().await.expect("end");
    bug_clear_env();
}

#[tokio::test]
async fn agent_busy_after_resume_forgets_id_and_creates_fresh() {
    let _guard = crate::test_utils::test_env_lock();
    let tmp = bug_prepare();
    let mut client = bug_client(tmp.path(), 3);
    client.begin_coder_session(tmp.path()).await.expect("begin");
    assert_eq!(client.last_agent_id.as_deref(), Some("mock-agent"));
    let log = tmp.path().join("prompts.log");
    let err = expect_prompt_err(&mut client, "CLOSE_STDOUT", &log).await;
    assert_err_has(&err, &["bridge stdout closed", "stdout"]);
    assert!(!client.has_open_coder_session());
    assert_eq!(client.last_agent_id.as_deref(), Some("mock-agent"));
    client
        .run_coder_prompt(
            "AGENT_BUSY_ON_RESUME please",
            &log,
            "coder",
            CoderPromptOptions {
                llm_phase: Some(crate::run_timing::TimingPhase::Implement),
                ..CoderPromptOptions::default()
            },
        )
        .await
        .expect("retry after AgentBusy must create fresh agent");
    assert_eq!(
        client.last_coder_prompt_agent_response().as_deref(),
        Some("mock reply")
    );
    assert!(client.has_open_coder_session());
    client.end_coder_session().await.expect("end");
    bug_clear_env();
}

#[tokio::test]
async fn stale_authentication_teardown_resume_retries() {
    let _guard = crate::test_utils::test_env_lock();
    let tmp = bug_prepare();
    let mut client = bug_client(tmp.path(), 3);
    client.begin_coder_session(tmp.path()).await.expect("begin");
    assert_eq!(client.last_agent_id.as_deref(), Some("mock-agent"));
    let log = tmp.path().join("prompts.log");
    client
        .run_coder_prompt(
            "AUTH_ONCE please",
            &log,
            "coder",
            CoderPromptOptions {
                llm_phase: Some(crate::run_timing::TimingPhase::Implement),
                ..CoderPromptOptions::default()
            },
        )
        .await
        .expect("retry after stale auth resume");
    assert_eq!(
        client.last_coder_prompt_agent_response().as_deref(),
        Some("mock reply")
    );
    assert!(client.has_open_coder_session());
    client.end_coder_session().await.expect("end");
    bug_clear_env();
}

#[tokio::test]
async fn bridge_stdout_closed_single_attempt_tears_down_session() {
    let _guard = crate::test_utils::test_env_lock();
    let tmp = bug_prepare();
    let mut client = bug_client(tmp.path(), 2);
    client.begin_coder_session(tmp.path()).await.expect("begin");
    let log = tmp.path().join("prompts.log");
    let err = expect_prompt_err(&mut client, "CLOSE_STDOUT", &log).await;
    assert_err_has(&err, &["bridge stdout closed", "stdout"]);
    assert!(!client.has_open_coder_session());
    assert!(crate::agent_backend::begun_cwd(&client).is_some());
    client
        .run_coder_prompt(
            "hi",
            &log,
            "coder",
            CoderPromptOptions {
                llm_phase: Some(crate::run_timing::TimingPhase::Implement),
                ..CoderPromptOptions::default()
            },
        )
        .await
        .expect("retry after teardown");
    assert_eq!(
        client.last_coder_prompt_agent_response().as_deref(),
        Some("mock reply")
    );
    client.end_coder_session().await.expect("end");
    bug_clear_env();
}

#[tokio::test]
async fn cancelled_run_done_is_error() {
    let _guard = crate::test_utils::test_env_lock();
    let tmp = bug_prepare();
    let mut client = bug_client(tmp.path(), 1);
    client.begin_coder_session(tmp.path()).await.expect("begin");
    let log = tmp.path().join("prompts.log");
    let err = expect_prompt_err(&mut client, "CANCELLED_RUN", &log).await;
    assert_err_has(&err, &["cancel", "cancelled"]);
    client.end_coder_session().await.expect("end");
    bug_clear_env();
}

#[tokio::test]
async fn stream_fatal_only_fails_prompt() {
    let _guard = crate::test_utils::test_env_lock();
    let tmp = bug_prepare();
    let mut client = bug_client(tmp.path(), 1);
    client.begin_coder_session(tmp.path()).await.expect("begin");
    let log = tmp.path().join("prompts.log");
    let err = expect_prompt_err(&mut client, "STREAM_FATAL_ONLY", &log).await;
    assert_err_has(&err, &["stream error"]);
    client.end_coder_session().await.expect("end");
    bug_clear_env();
}

#[tokio::test]
async fn cancel_during_slow_send_is_honored() {
    let _guard = crate::test_utils::test_env_lock();
    let tmp = bug_prepare();
    let mut client = bug_client(tmp.path(), 1);
    client.begin_coder_session(tmp.path()).await.expect("begin");
    let session = crate::agent_backend::live_session(&client)
        .and_then(|s| s.as_cursor())
        .expect("session");
    let prompt_fut = session.send_prompt("SLOW_SEND please");
    let cancel_fut = async {
        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
        let _ = super::session_io_write_cancel_for_test(session).await;
    };
    let (prompt_res, ()) = tokio::join!(prompt_fut, cancel_fut);
    assert!(prompt_res.is_err(), "cancel must fail the slow send");
    client.end_coder_session().await.expect("end");
    bug_clear_env();
}

#[test]
fn kiss_cov_bug_regression_cases() {
    let _ = stringify!(failed_create_drop_clears_sandbox_for_next_spawn);
    let _ = stringify!(agent_busy_after_resume_forgets_id_and_creates_fresh);
    let _ = stringify!(bridge_stdout_closed_single_attempt_tears_down_session);
    let _ = stringify!(cancelled_run_done_is_error);
    let _ = stringify!(stream_fatal_only_fails_prompt);
    let _ = stringify!(cancel_during_slow_send_is_honored);
}

#[test]
fn bridge_transport_errors_require_coder_session_teardown() {
    use crate::acp::agent_error_requires_coder_session_teardown;
    for msg in [
        "bridge stdout closed",
        "bridge write: broken pipe",
        "bridge flush: broken pipe",
        "bridge read: connection reset",
        "bridge drain timed out waiting for run_done after 1s without a bridge event (bridge quiet; likely hung or stalled)",
        "bridge timed out waiting for run_done after 1s without a bridge event (bridge quiet; likely hung or stalled)",
        "bridge timed out waiting for run_done after turn ran 1192s (limit 6000s; turn budget exhausted)",
        "bridge timed out waiting for ok after 1s without a bridge event (bridge quiet; likely hung or stalled)",
        "Agent agent-7b61bfe2-fa7a-47bd-8f5b-96c158067bc8 already has active run",
    ] {
        assert!(agent_error_requires_coder_session_teardown(msg), "{msg}");
    }
}