236 lines
7.9 KiB
Rust
236 lines
7.9 KiB
Rust
//! Workerフィクスチャベースの統合テスト
|
||
//!
|
||
//! 記録されたAPIレスポンスを使ってWorkerの動作をテストする。
|
||
//! APIキー不要でローカルで実行可能。
|
||
|
||
mod common;
|
||
|
||
use std::path::Path;
|
||
use std::sync::Arc;
|
||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||
|
||
use async_trait::async_trait;
|
||
use common::MockLlmClient;
|
||
use worker::Worker;
|
||
use worker_types::{Tool, ToolError};
|
||
|
||
/// フィクスチャディレクトリのパス
|
||
fn fixtures_dir() -> std::path::PathBuf {
|
||
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/anthropic")
|
||
}
|
||
|
||
/// シンプルなテスト用ツール
|
||
#[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 result = worker.run("Hello").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);
|
||
|
||
// メッセージを送信
|
||
let _result = worker.run("What's the weather in Tokyo?").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 result = worker.run("Greet me").await;
|
||
|
||
assert!(result.is_ok(), "Worker should complete successfully");
|
||
}
|
||
|
||
/// ToolCallCollectorがToolUseブロックイベントから正しくToolCallを収集することを確認
|
||
///
|
||
/// Timelineにイベントをディスパッチし、ToolCallCollectorが
|
||
/// id, name, input(JSON)を正しく抽出できることを検証する。
|
||
#[tokio::test]
|
||
async fn test_tool_call_collector_integration() {
|
||
use worker::Timeline;
|
||
use worker::ToolCallCollector;
|
||
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");
|
||
}
|