use super::*;
use crate::ai::sse::SseParser;
fn decode(stream: &str) -> Result<Vec<Event>, AiError> {
let (mut parser, mut events, mut out) = (SseParser::default(), Vec::new(), Vec::new());
let mut decoder = OpenAiDecoder::default();
parser.push(stream.as_bytes(), &mut events);
for event in &events {
decoder.feed(event, &mut out)?;
}
decoder.finish(&mut out);
Ok(out)
}
#[test]
fn texto_y_uso_final() {
let stream = "data: {\"choices\":[{\"delta\":{\"content\":\"Ho\"}}]}\n\n\
data: {\"choices\":[{\"delta\":{\"content\":\"la\"},\"finish_reason\":\"stop\"}]}\n\n\
data: {\"choices\":[],\"usage\":{\"prompt_tokens\":7,\"completion_tokens\":2}}\n\n\
data: [DONE]\n\n";
assert_eq!(
decode(stream).unwrap(),
[
Event::Text("Ho".into()),
Event::Text("la".into()),
Event::Usage(Usage { input_tokens: 7, output_tokens: 2 }),
Event::Done(StopReason::EndTurn),
]
);
}
#[test]
fn llamada_a_herramienta_con_argumentos_en_trozos() {
let stream = "data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"c1\",\"function\":{\"name\":\"grep\",\"arguments\":\"{\\\"q\\\":\"}}]}}]}\n\n\
data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\"fn\\\"}\"}}]}}]}\n\n\
data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"tool_calls\"}]}\n\n\
data: [DONE]\n\n";
let events = decode(stream).unwrap();
assert_eq!(events[0], Event::ToolUse { id: "c1".into(), name: "grep".into(), input: json!({"q": "fn"}) });
assert_eq!(events.last(), Some(&Event::Done(StopReason::ToolUse)));
}
#[test]
fn dos_herramientas_en_paralelo() {
let stream = "data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"a\",\"function\":{\"name\":\"x\",\"arguments\":\"{}\"}},{\"index\":1,\"id\":\"b\",\"function\":{\"name\":\"y\",\"arguments\":\"{}\"}}]},\"finish_reason\":\"tool_calls\"}]}\n\n";
let names: Vec<String> = decode(stream)
.unwrap()
.into_iter()
.filter_map(|event| if let Event::ToolUse { name, .. } = event { Some(name) } else { None })
.collect();
assert_eq!(names, ["x", "y"]);
}
#[test]
fn las_imagenes_usan_partes_de_contenido() {
let user = Message {
role: Role::User,
blocks: vec![
Block::Image { media_type: "image/jpeg".into(), data_base64: "AAA".into(), name: None },
Block::Text("mira".into()),
],
};
let all: Vec<Value> = messages(&user);
assert_eq!(all[0]["content"][0]["type"], "image_url");
assert_eq!(all[0]["content"][0]["image_url"]["url"], "data:image/jpeg;base64,AAA");
assert_eq!(all[0]["content"][1], json!({"type": "text", "text": "mira"}));
}
#[test]
fn mensajes_con_herramientas_se_aplanan_al_formato_openai() {
let history = [
Message::assistant(vec![
Block::Text("miro".into()),
Block::ToolUse { id: "c".into(), name: "ls".into(), input: json!({"p": 1}) },
]),
Message {
role: Role::User,
blocks: vec![Block::ToolResult { id: "c".into(), content: "ok".into(), is_error: false }],
},
];
let all: Vec<Value> = history.iter().flat_map(messages).collect();
assert_eq!(all[0]["tool_calls"][0]["function"]["arguments"], "{\"p\":1}");
assert_eq!(all[1], json!({"role": "tool", "tool_call_id": "c", "content": "ok"}));
assert_eq!(all.len(), 2, "un resultado no genera mensaje de usuario vacío");
}
#[test]
fn error_en_el_stream() {
assert!(matches!(decode("data: {\"error\":{\"message\":\"cuota\"}}\n\n"), Err(AiError::Api(_))));
}
#[test]
fn llamadas_sin_indice_ni_id_y_fin_con_stop_siguen_siendo_herramientas() {
let stream = "data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"function\":{\"name\":\"ls\",\"arguments\":\"{}\"}}]}}]}\n\n\
data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"id\":\"b\",\"function\":{\"name\":\"grep\",\"arguments\":\"{\\\"pattern\\\":\"}}]}}]}\n\n\
data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}]}\n\n";
let events = decode(stream).unwrap();
assert!(matches!(&events[0], Event::ToolUse { id, name, .. } if id == "call_0" && name == "ls"));
assert!(
matches!(&events[1], Event::ToolUse { id, input, .. } if id == "b" && input[crate::ai::wire::INVALID_ARGUMENTS].is_string())
);
assert_eq!(events.last(), Some(&Event::Done(StopReason::ToolUse)));
}