everruns-contracts 0.47.0

Shared runtime, provider, capability, and model profile contracts for Everruns
Documentation
use super::*;
use serde_json::json;

fn with_tool_call(text: &str) -> RuntimeMessage {
    let mut message = RuntimeMessage::assistant(text);
    message
        .content
        .push(ContentPart::tool_call("call_1", "search", json!({})));
    message
}

#[test]
fn said_text_is_the_agent_text_and_nothing_else() {
    let cases: Vec<(&str, RuntimeMessage, Option<&str>)> = vec![
        (
            "plain answer",
            RuntimeMessage::assistant("Done."),
            Some("Done."),
        ),
        (
            "commentary",
            RuntimeMessage::assistant("thinking").with_phase(ExecutionPhase::Commentary),
            None,
        ),
        (
            "final answer phase",
            RuntimeMessage::assistant("Here.").with_phase(ExecutionPhase::FinalAnswer),
            Some("Here."),
        ),
        ("user message", RuntimeMessage::user("hi"), None),
        ("blank text", RuntimeMessage::assistant("  \n"), None),
        (
            "tool call preamble",
            with_tool_call("Let me check"),
            Some("Let me check"),
        ),
        ("tool call only", with_tool_call(""), None),
    ];
    for (name, message, expected) in cases {
        assert_eq!(said_text(&message).as_deref(), expected, "{name}");
    }
}

#[test]
fn spoken_text_joins_text_parts_and_drops_others() {
    let parts = vec![
        ContentPart::text("one"),
        ContentPart::tool_call("c", "t", json!({})),
        ContentPart::text(""),
        ContentPart::text("two"),
    ];
    assert_eq!(spoken_text(&parts), "one\ntwo");
}

#[test]
fn final_reply_prefers_the_last_message_that_was_not_a_preamble() {
    let said = |text: &str, with_tool_calls| SaidMessage {
        text: text.into(),
        with_tool_calls,
    };
    let cases: Vec<(&str, Vec<SaidMessage>, Option<&str>)> = vec![
        ("nothing said", vec![], None),
        (
            "preamble then answer",
            vec![said("Let me check", true), said("It is 4.", false)],
            Some("It is 4."),
        ),
        (
            "answer then trailing preamble",
            vec![said("Partial.", false), said("Checking more", true)],
            Some("Partial."),
        ),
        (
            "only preamble",
            vec![said("Checking", true)],
            Some("Checking"),
        ),
        (
            "several answers",
            vec![said("First.", false), said("Second.", false)],
            Some("Second."),
        ),
    ];
    for (name, said, expected) in cases {
        assert_eq!(final_reply(said).as_deref(), expected, "{name}");
    }
}

#[test]
fn said_text_in_event_agrees_with_the_typed_reader() {
    let messages = [
        RuntimeMessage::assistant("Done."),
        RuntimeMessage::assistant("thinking").with_phase(ExecutionPhase::Commentary),
        RuntimeMessage::assistant("Here.").with_phase(ExecutionPhase::FinalAnswer),
        RuntimeMessage::assistant(" "),
        with_tool_call("Let me check"),
    ];
    for message in messages {
        let data = json!({ "message": serde_json::to_value(&message).unwrap() });
        assert_eq!(
            said_text_in_event(&data),
            said_text(&message),
            "{message:?}"
        );
    }
    assert_eq!(
        said_text_in_event(&json!({"message": {"content": [{"type": "text", "text": "hi"}]}})),
        Some("hi".to_string())
    );
    assert_eq!(said_text_in_event(&json!({})), None);
}

#[test]
fn turn_reply_skips_blank_text_and_resets_on_take() {
    let mut reply = TurnReply::default();
    reply.push_text("  ", false);
    reply.push_text("Let me check.", true);
    reply.push_text("Here it is.", false);
    assert_eq!(reply.take(), "Here it is.");
    assert_eq!(reply.take(), "");
}