//! provider 模块(流事件类型)的集成测试。 //! Integration tests for the provider module (stream event types). use focus_core::model::{AssistantMessage, StopReason, Usage}; use focus_core::provider::{ProviderEventReducer, StreamEvent}; #[test] fn stream_event_display() { assert_eq!( StreamEvent::Start { partial: AssistantMessage { content: vec![], model: "m".into(), usage: Usage::default(), stop_reason: StopReason::Stop, error_message: None, timestamp: 0, } } .to_string(), "start" ); } /// 文本与工具调用交错时,参数必须归到正确的调用槽(回归测试)。 /// When text and tool calls interleave, arguments must land in the correct /// call slot (regression test). #[test] fn reducer_interleaved_text_and_tool_calls() { let mut r = ProviderEventReducer::new("m"); let mut events = r.text_delta("Let me check. "); events.extend(r.tool_call_start(0, "read")); events.extend(r.tool_call_delta(0, r#"{"path":"a""#)); // 第二个工具调用与第一个交错。 // Second tool call interleaved with the first. events.extend(r.tool_call_start(1, "write")); events.extend(r.tool_call_delta(1, r#"{"path":"b""#)); events.extend(r.tool_call_delta(0, r#"}"#)); events.extend(r.tool_call_delta(1, r#"}"#)); events.extend(r.text_delta(" Done.")); r.set_stop(StopReason::ToolUse); events.extend(r.finalize_events()); let msg = r.finish().expect("final message"); // 内容顺序:文本块 + 两个工具调用(后续文本追加到同一文本块)。 // Content order: one text block + two tool calls (later text appends to // the same text block). assert_eq!(msg.content.len(), 3); let head = msg.content[0].as_text().expect("leading text"); assert_eq!(head.text, "Let me check. Done."); let tc0 = msg.content[1].as_tool_call().expect("tool call 0"); let tc1 = msg.content[2].as_tool_call().expect("tool call 1"); assert_eq!(tc0.name, "read"); assert_eq!(tc0.arguments.get_str("path"), Some("a")); assert_eq!(tc1.name, "write"); assert_eq!(tc1.arguments.get_str("path"), Some("b")); assert_eq!(msg.stop_reason, StopReason::ToolUse); // 事件流必须包含每个工具调用的 end 事件。 // The event stream must include an end event per tool call. let end_events = events .iter() .filter(|e| matches!(e, StreamEvent::ToolCallEnd { .. })) .count(); assert_eq!(end_events, 2); } /// 思考块出现在文本之前时,两者都必须正确累积。 /// When a thinking block precedes text, both must accumulate correctly. #[test] fn reducer_thinking_then_text() { let mut r = ProviderEventReducer::new("m"); let mut events = r.thinking_delta("Let me reason"); events.extend(r.thinking_delta(" step by step.")); events.extend(r.text_delta("Answer: 42")); events.extend(r.finalize_events()); let msg = r.finish().expect("final message"); assert_eq!(msg.content.len(), 2); let thinking = match &msg.content[0] { focus_core::model::ContentBlock::Thinking(t) => t.thinking.clone(), other => panic!("expected thinking block, got {:?}", other), }; assert_eq!(thinking, "Let me reason step by step."); let text = msg.content[1].as_text().expect("text block"); assert_eq!(text.text, "Answer: 42"); // 思考必须有自己的 start/delta/end 事件。 // Thinking must have its own start/delta/end events. assert!(events .iter() .any(|e| matches!(e, StreamEvent::ThinkingStart { .. }))); assert!(events .iter() .any(|e| matches!(e, StreamEvent::ThinkingEnd { .. }))); } /// 工具调用 id 可显式指定(API 真实 id),空时退回自动生成。 /// Tool-call ids can be set explicitly (the API's real id); empty falls back /// to auto-generation. #[test] fn reducer_tool_call_id_override() { let mut r = ProviderEventReducer::new("m"); let _ = r.tool_call_start_with_id(0, "shell", "call_00_realId"); let _ = r.tool_call_start_with_id(1, "read", ""); let msg = r.finish().expect("final message"); let tc0 = msg.content[0].as_tool_call().expect("call 0"); let tc1 = msg.content[1].as_tool_call().expect("call 1"); assert_eq!(tc0.id, "call_00_realId"); assert_eq!(tc1.id, "call_1"); }