focus/crates/focus-core/tests/provider_tests.rs

110 lines
4.4 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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");
}