malvin 0.2.5

Non-interactive research and coding agent
use pi::model::{
    AssistantMessage, AssistantMessageEvent, ContentBlock, Message, TextContent, Usage,
};
use pi::sdk::AgentEvent;

use super::map_agent_event::map_pi_agent_event;
use crate::bridge_protocol::BridgeEvent;

#[test]
fn maps_typed_text_and_thinking_deltas() {
    let assistant = AssistantMessage {
        content: vec![ContentBlock::Text(TextContent::new("hi"))],
        ..AssistantMessage::default()
    };
    let text = map_pi_agent_event(&AgentEvent::MessageUpdate {
        message: Message::assistant(assistant.clone()),
        assistant_message_event: AssistantMessageEvent::TextDelta {
            content_index: 0,
            delta: "hi".into(),
            partial: std::sync::Arc::new(assistant.clone()),
        },
    });
    assert!(matches!(
        text.as_slice(),
        [BridgeEvent::Assistant { text }] if text == "hi"
    ));
    let think = map_pi_agent_event(&AgentEvent::MessageUpdate {
        message: Message::assistant(assistant.clone()),
        assistant_message_event: AssistantMessageEvent::ThinkingDelta {
            content_index: 0,
            delta: "hmm".into(),
            partial: std::sync::Arc::new(assistant),
        },
    });
    assert!(matches!(
        think.as_slice(),
        [BridgeEvent::Thinking { text }] if text == "hmm"
    ));
}

#[test]
fn maps_typed_tool_and_agent_end() {
    let start = map_pi_agent_event(&AgentEvent::ToolExecutionStart {
        tool_call_id: "t1".into(),
        tool_name: "bash".into(),
        args: serde_json::json!({ "command": "ls -la" }),
    });
    assert!(matches!(
        start.as_slice(),
        [BridgeEvent::ToolCall { phase, name, summary, .. }]
            if phase == "start"
                && name.as_deref() == Some("bash")
                && summary.as_deref() == Some("Run ls -la")
    ));
    let end = map_pi_agent_event(&AgentEvent::ToolExecutionEnd {
        tool_call_id: "t1".into(),
        tool_name: "bash".into(),
        result: pi::sdk::ToolOutput {
            content: Vec::new(),
            details: None,
            is_error: false,
        },
        is_error: false,
    });
    assert!(matches!(
        end.as_slice(),
        [BridgeEvent::ToolCall { phase, .. }] if phase == "complete"
    ));
    let assistant = AssistantMessage {
        content: vec![ContentBlock::Text(TextContent::new("yo"))],
        usage: Usage {
            input: 10,
            output: 2,
            cost: pi::model::Cost {
                input: 0.01,
                output: 0.02,
                total: 0.03,
                ..pi::model::Cost::default()
            },
            ..Usage::default()
        },
        ..AssistantMessage::default()
    };
    let done = map_pi_agent_event(&AgentEvent::AgentEnd {
        session_id: "s".into(),
        messages: vec![
            Message::User(pi::model::UserMessage {
                content: pi::model::UserContent::Text("hi".into()),
                timestamp: 0,
            }),
            Message::assistant(assistant),
        ],
        error: None,
    });
    match done.as_slice() {
        [
            BridgeEvent::RunDone {
                status,
                result,
                usage,
                error,
                ..
            },
        ] => {
            assert_eq!(*status, crate::bridge_protocol::RunDoneStatus::Finished);
            assert_eq!(result.as_deref(), Some("yo"));
            assert!(error.is_none());
            assert_eq!(
                usage
                    .as_ref()
                    .and_then(|u| u.get("inputTokens"))
                    .and_then(serde_json::Value::as_u64),
                Some(10)
            );
            assert_eq!(
                usage
                    .as_ref()
                    .and_then(|u| u.get("costUsd"))
                    .and_then(|c| c.get("total"))
                    .and_then(serde_json::Value::as_f64),
                Some(0.03)
            );
        }
        other => panic!("unexpected {other:?}"),
    }
}

#[test]
fn typed_extension_error_is_fatal() {
    let evs = map_pi_agent_event(&AgentEvent::ExtensionError {
        extension_id: None,
        event: "ui".into(),
        error: "nope".into(),
    });
    assert!(matches!(
        evs.as_slice(),
        [BridgeEvent::Fatal {
            retryable: Some(false),
            ..
        }]
    ));
}