llm_worker_rs/worker/tests/worker_fixtures.rs

244 lines
8.3 KiB
Rust
Raw 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.

//! Workerフィクスチャベースの統合テスト
//!
//! 記録されたAPIレスポンスを使ってWorkerの動作をテストする。
//! APIキー不要でローカルで実行可能。
mod common;
use std::path::Path;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use async_trait::async_trait;
use common::MockLlmClient;
use worker::{Worker, WorkerConfig};
use worker_types::{Tool, ToolError};
/// フィクスチャディレクトリのパス
fn fixtures_dir() -> std::path::PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
}
/// シンプルなテスト用ツール
#[derive(Clone)]
struct MockWeatherTool {
call_count: Arc<AtomicUsize>,
}
impl MockWeatherTool {
fn new() -> Self {
Self {
call_count: Arc::new(AtomicUsize::new(0)),
}
}
fn get_call_count(&self) -> usize {
self.call_count.load(Ordering::SeqCst)
}
}
#[async_trait]
impl Tool for MockWeatherTool {
fn name(&self) -> &str {
"get_weather"
}
fn description(&self) -> &str {
"Get the current weather for a city"
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name"
}
},
"required": ["city"]
})
}
async fn execute(&self, input_json: &str) -> Result<String, ToolError> {
self.call_count.fetch_add(1, Ordering::SeqCst);
// 入力をパース
let input: serde_json::Value = serde_json::from_str(input_json)
.map_err(|e| ToolError::InvalidArgument(e.to_string()))?;
let city = input["city"]
.as_str()
.unwrap_or("Unknown");
// モックのレスポンスを返す
Ok(format!("Weather in {}: Sunny, 22°C", city))
}
}
// =============================================================================
// Basic Fixture Tests
// =============================================================================
/// MockLlmClientがJSONLフィクスチャファイルから正しくイベントをロードできることを確認
///
/// 既存のanthropic_*.jsonlファイルを使用し、イベントがパース・ロードされることを検証する。
#[test]
fn test_mock_client_from_fixture() {
// 既存のフィクスチャをロード
let fixture_path = fixtures_dir().join("anthropic_1767624445.jsonl");
if !fixture_path.exists() {
println!("Fixture not found, skipping test");
return;
}
let client = MockLlmClient::from_fixture(&fixture_path).unwrap();
assert!(client.event_count() > 0, "Should have loaded events");
println!("Loaded {} events from fixture", client.event_count());
}
/// MockLlmClientが直接指定されたイベントリストで正しく動作することを確認
///
/// fixtureファイルを使わず、プログラムでイベントを構築してクライアントを作成する。
#[test]
fn test_mock_client_from_events() {
use worker_types::Event;
// 直接イベントを指定
let events = vec![
Event::text_block_start(0),
Event::text_delta(0, "Hello!"),
Event::text_block_stop(0, None),
];
let client = MockLlmClient::new(events);
assert_eq!(client.event_count(), 3);
}
// =============================================================================
// Worker Tests with Fixtures
// =============================================================================
/// Workerがシンプルなテキストレスポンスを正しく処理できることを確認
///
/// simple_text.jsonlフィクスチャを使用し、ツール呼び出しなしのシナリオをテストする。
/// フィクスチャがない場合はスキップされる。
#[tokio::test]
async fn test_worker_simple_text_response() {
let fixture_path = fixtures_dir().join("simple_text.jsonl");
if !fixture_path.exists() {
println!("Fixture not found: {:?}, skipping test", fixture_path);
println!("Run: cargo run --example record_worker_test");
return;
}
let client = MockLlmClient::from_fixture(&fixture_path).unwrap();
let mut worker = Worker::new(client);
// シンプルなメッセージを送信
let messages = vec![worker_types::Message::user("Hello")];
let result = worker.run(messages).await;
assert!(result.is_ok(), "Worker should complete successfully");
}
/// Workerがツール呼び出しを含むレスポンスを正しく処理できることを確認
///
/// tool_call.jsonlフィクスチャを使用し、MockWeatherToolが呼び出されることをテストする。
/// max_turns=1に設定し、ツール実行後のループを防止。
#[tokio::test]
async fn test_worker_tool_call() {
let fixture_path = fixtures_dir().join("tool_call.jsonl");
if !fixture_path.exists() {
println!("Fixture not found: {:?}, skipping test", fixture_path);
println!("Run: cargo run --example record_worker_test");
return;
}
let client = MockLlmClient::from_fixture(&fixture_path).unwrap();
let mut worker = Worker::new(client);
// ツールを登録
let weather_tool = MockWeatherTool::new();
let tool_for_check = weather_tool.clone();
worker.register_tool(weather_tool);
// 設定: ツール実行後はターン終了(ループしない)
worker = worker.config(WorkerConfig { max_turns: 1 });
// メッセージを送信
let messages = vec![worker_types::Message::user("What's the weather in Tokyo?")];
let _result = worker.run(messages).await;
// ツールが呼び出されたことを確認
// Note: max_turns=1なのでツール結果後のリクエストは送信されない
let call_count = tool_for_check.get_call_count();
println!("Tool was called {} times", call_count);
// フィクスチャにToolUseが含まれていればツールが呼び出されるはず
// ただしmax_turns=1なので1回で終了
}
/// fixtureファイルなしでWorkerが動作することを確認
///
/// プログラムでイベントシーケンスを構築し、MockLlmClientに渡してテストする。
/// テストの独立性を高め、外部ファイルへの依存を排除したい場合に有用。
#[tokio::test]
async fn test_worker_with_programmatic_events() {
use worker_types::{Event, ResponseStatus, StatusEvent};
// プログラムでイベントシーケンスを構築
let events = vec![
Event::text_block_start(0),
Event::text_delta(0, "Hello, "),
Event::text_delta(0, "World!"),
Event::text_block_stop(0, None),
Event::Status(StatusEvent {
status: ResponseStatus::Completed,
}),
];
let client = MockLlmClient::new(events);
let mut worker = Worker::new(client);
let messages = vec![worker_types::Message::user("Greet me")];
let result = worker.run(messages).await;
assert!(result.is_ok(), "Worker should complete successfully");
}
/// ToolCallCollectorがToolUseブロックイベントから正しくToolCallを収集することを確認
///
/// Timelineにイベントをディスパッチし、ToolCallCollectorが
/// id, name, inputJSONを正しく抽出できることを検証する。
#[tokio::test]
async fn test_tool_call_collector_integration() {
use worker::ToolCallCollector;
use worker::Timeline;
use worker_types::Event;
// ToolUseブロックを含むイベントシーケンス
let events = vec![
Event::tool_use_start(0, "call_123", "get_weather"),
Event::tool_input_delta(0, r#"{"city":"#),
Event::tool_input_delta(0, r#""Tokyo"}"#),
Event::tool_use_stop(0),
];
let collector = ToolCallCollector::new();
let mut timeline = Timeline::new();
timeline.on_tool_use_block(collector.clone());
// イベントをディスパッチ
for event in &events {
timeline.dispatch(event);
}
// 収集されたToolCallを確認
let calls = collector.take_collected();
assert_eq!(calls.len(), 1, "Should collect one tool call");
assert_eq!(calls[0].name, "get_weather");
assert_eq!(calls[0].id, "call_123");
assert_eq!(calls[0].input["city"], "Tokyo");
}