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

95 lines
3.7 KiB
Rust

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