//! 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; 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, } 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 { 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); // メッセージを送信 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, input(JSON)を正しく抽出できることを検証する。 #[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"); }