met 1.0.0-beta-preview

Metrale Engine: pure Rust LLM inference server (HTTP API, scheduler, TUI, CLI).
// SPDX-License-Identifier: MIT OR Apache-2.0

//! 2026-09-26: Event-sequence tests for the Anthropic streaming translator.
//!
//! Owner: server (Anthropic adapter) tests.
//! Invariants: none beyond the types.

use super::super::translator::{AnthropicTranslator, SseEvent};
use crate::ir::response::{FinishReason, Usage};
use crate::ir::stream::StreamDelta;

fn usage(prompt: usize, completion: usize) -> Usage {
    Usage {
        prompt_tokens: prompt,
        completion_tokens: completion,
        cached_prompt_tokens: 0,
        reasoning_tokens: 0,
        accepted_prediction_tokens: 0,
        time_to_first_token_ms: 0.0,
        decode_time_ms: 0.0,
        response_tokens_per_second: 0.0,
    }
}

fn drive(deltas: Vec<StreamDelta>) -> Vec<SseEvent> {
    let mut t = AnthropicTranslator::new("m".to_string());
    let mut out = Vec::new();
    for d in &deltas {
        t.on_delta(d, &mut out);
    }
    t.finalize(&mut out);
    out
}

fn names(evs: &[SseEvent]) -> Vec<&str> {
    evs.iter().map(|e| e.event.as_str()).collect()
}

#[test]
fn text_stream_framing() {
    let evs = drive(vec![
        StreamDelta::Content {
            text: "Hi".into(),
            token_ids: Vec::new(),
        },
        StreamDelta::Finish {
            reason: FinishReason::Stop,
            usage: usage(3, 1),
            token_ids: Vec::new(),
        },
    ]);
    assert_eq!(
        names(&evs),
        vec![
            "message_start",
            "content_block_start",
            "content_block_delta",
            "content_block_stop",
            "message_delta",
            "message_stop",
        ]
    );
    assert_eq!(evs[1].data["content_block"]["type"], "text");
    assert_eq!(evs[2].data["delta"]["type"], "text_delta");
    assert_eq!(evs[2].data["delta"]["text"], "Hi");
    assert_eq!(evs[4].data["delta"]["stop_reason"], "end_turn");
    assert_eq!(evs[4].data["usage"]["output_tokens"], 1);
    // 2026-09-26: The input count arrives only with the Finish delta, so
    // `message_delta` reports it.
    assert_eq!(evs[4].data["usage"]["input_tokens"], 3);
    assert_eq!(evs[4].data["usage"]["cache_read_input_tokens"], 0);
    assert_eq!(evs[0].data["message"]["usage"]["input_tokens"], 0);
    let id = evs[0].data["message"]["id"].as_str().unwrap();
    assert!(id.starts_with("msg_"), "unexpected id shape: {id}");
}

#[test]
fn thinking_then_text_framing() {
    let evs = drive(vec![
        StreamDelta::Reasoning {
            text: "think".into(),
            token_ids: Vec::new(),
        },
        StreamDelta::Content {
            text: "answer".into(),
            token_ids: Vec::new(),
        },
        StreamDelta::Finish {
            reason: FinishReason::Stop,
            usage: usage(0, 0),
            token_ids: Vec::new(),
        },
    ]);
    assert_eq!(
        names(&evs),
        vec![
            "message_start",
            "content_block_start",
            "content_block_delta",
            "content_block_stop",
            "content_block_start",
            "content_block_delta",
            "content_block_stop",
            "message_delta",
            "message_stop",
        ]
    );
    assert_eq!(evs[1].data["content_block"]["type"], "thinking");
    assert_eq!(evs[2].data["delta"]["type"], "thinking_delta");
    assert_eq!(evs[2].data["delta"]["thinking"], "think");
    assert_eq!(evs[5].data["delta"]["type"], "text_delta");
    assert_eq!(evs[5].data["delta"]["text"], "answer");
}

#[test]
fn tool_call_stream_framing() {
    let evs = drive(vec![
        StreamDelta::ToolCallStart {
            index: 0,
            id: "call_1".into(),
            name: "get_weather".into(),
        },
        StreamDelta::ToolCallArgs {
            index: 0,
            fragment: "{\"city\":\"SF\"}".into(),
            token_ids: Vec::new(),
        },
        StreamDelta::Finish {
            reason: FinishReason::ToolCalls,
            usage: usage(0, 0),
            token_ids: Vec::new(),
        },
    ]);
    assert_eq!(
        names(&evs),
        vec![
            "message_start",
            "content_block_start",
            "content_block_delta",
            "content_block_stop",
            "message_delta",
            "message_stop",
        ]
    );
    assert_eq!(evs[1].data["content_block"]["type"], "tool_use");
    assert_eq!(evs[1].data["content_block"]["name"], "get_weather");
    assert_eq!(evs[1].data["content_block"]["id"], "call_1");
    assert_eq!(evs[2].data["delta"]["type"], "input_json_delta");
    assert_eq!(evs[2].data["delta"]["partial_json"], "{\"city\":\"SF\"}");
    assert_eq!(evs[4].data["delta"]["stop_reason"], "tool_use");
}

#[test]
fn multi_tool_calls_close_and_reopen_blocks() {
    // 2026-09-26: The second start closes the first tool block and opens a
    // new one at the next index.
    let evs = drive(vec![
        StreamDelta::ToolCallStart {
            index: 0,
            id: "c1".into(),
            name: "read".into(),
        },
        StreamDelta::ToolCallArgs {
            index: 0,
            fragment: "{}".into(),
            token_ids: Vec::new(),
        },
        StreamDelta::ToolCallStart {
            index: 1,
            id: "c2".into(),
            name: "bash".into(),
        },
        StreamDelta::ToolCallArgs {
            index: 1,
            fragment: "{\"command\":\"ls\"}".into(),
            token_ids: Vec::new(),
        },
        StreamDelta::Finish {
            reason: FinishReason::ToolCalls,
            usage: usage(0, 2),
            token_ids: Vec::new(),
        },
    ]);
    assert_eq!(
        names(&evs),
        vec![
            "message_start",
            "content_block_start",
            "content_block_delta",
            "content_block_stop",
            "content_block_start",
            "content_block_delta",
            "content_block_stop",
            "message_delta",
            "message_stop",
        ]
    );
    assert_eq!(evs[1].data["content_block"]["id"], "c1");
    assert_eq!(evs[1].data["index"], 0);
    assert_eq!(evs[4].data["content_block"]["id"], "c2");
    assert_eq!(evs[4].data["index"], 1);
}

#[test]
fn finalize_without_finish_reports_truncation_not_end_turn() {
    // 2026-09-26: No Finish delta arrived. `finalize` closes the block and
    // the message with `max_tokens`, the reason `convert_stop_reason` also
    // gives a deadline cut, because the turn is incomplete.
    let mut t = AnthropicTranslator::new("m".to_string());
    let mut out = Vec::new();
    t.on_delta(
        &StreamDelta::Content {
            text: "partial".into(),
            token_ids: Vec::new(),
        },
        &mut out,
    );
    t.finalize(&mut out);
    assert_eq!(
        names(&out),
        vec![
            "message_start",
            "content_block_start",
            "content_block_delta",
            "content_block_stop",
            "message_delta",
            "message_stop",
        ]
    );
    assert_eq!(
        out[4].data["delta"]["stop_reason"], "max_tokens",
        "an abrupt end must not be reported as a completed turn: {:?}",
        out[4].data
    );
}

/// 2026-09-26: A copy of the payload `api::chat_stream::handle_error` puts on
/// `StreamDelta::Error`.
fn core_error_envelope(msg: &str) -> StreamDelta {
    StreamDelta::Error {
        message: serde_json::json!({
            "error": {"message": msg, "type": "server_error", "code": 500}
        })
        .to_string(),
    }
}

#[test]
fn stream_error_terminates_the_turn_as_an_error_not_a_success() {
    // 2026-09-26: A failure after content was sent. The HTTP 200 is already
    // committed, so the terminal event is the client's only failure signal:
    // it must be `error`, with no success framing after it.
    let evs = drive(vec![
        StreamDelta::Content {
            text: "partial".into(),
            token_ids: Vec::new(),
        },
        core_error_envelope("KV cache exhausted"),
    ]);

    assert_eq!(
        names(&evs),
        vec![
            "message_start",
            "content_block_start",
            "content_block_delta",
            "content_block_stop",
            "error",
        ],
        "terminal framing after a stream error"
    );
    let err = &evs[4].data;
    assert_eq!(err["type"], "error");
    assert_eq!(
        err["error"]["message"], "KV cache exhausted",
        "the core's message must reach the client, unwrapped from the \
         OpenAI envelope: {err:?}"
    );
    assert!(
        !names(&evs).contains(&"message_stop"),
        "a failed turn must not be closed with the success terminator: {:?}",
        names(&evs)
    );
    assert!(
        !names(&evs).contains(&"message_delta"),
        "a failed turn must not carry a stop_reason: {:?}",
        names(&evs)
    );
}

#[test]
fn stream_error_before_any_content_still_reaches_the_client() {
    let evs = drive(vec![core_error_envelope("model load failed")]);
    assert_eq!(names(&evs), vec!["message_start", "error"]);
    assert_eq!(evs[1].data["error"]["message"], "model load failed");
}

#[test]
fn non_envelope_error_payload_is_passed_through() {
    let evs = drive(vec![StreamDelta::Error {
        message: "raw failure text".into(),
    }]);
    assert_eq!(evs[1].data["error"]["message"], "raw failure text");
}