//! テスト用のAPIレスポンス記録・再生機能 //! //! 実際のAPIレスポンスをタイムスタンプ付きで記録し、 //! テスト時に再生できるようにする。 use std::fs::File; use std::io::{BufRead, BufReader, BufWriter, Write}; use std::path::Path; use std::time::{Instant, SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; /// 記録されたSSEイベント #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RecordedEvent { /// イベント受信からの経過時間 (ミリ秒) pub elapsed_ms: u64, /// SSEイベントタイプ pub event_type: String, /// SSEイベントデータ pub data: String, } /// セッションメタデータ #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SessionMetadata { /// 記録開始タイムスタンプ (Unix epoch秒) pub timestamp: u64, /// モデル名 pub model: String, /// リクエストの説明 pub description: String, } /// SSEイベントレコーダー /// /// 実際のAPIレスポンスを記録し、後でテストに使用できるようにする pub struct EventRecorder { start_time: Instant, events: Vec, metadata: SessionMetadata, } impl EventRecorder { /// 新しいレコーダーを作成 pub fn new(model: impl Into, description: impl Into) -> Self { let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); Self { start_time: Instant::now(), events: Vec::new(), metadata: SessionMetadata { timestamp, model: model.into(), description: description.into(), }, } } /// イベントを記録 pub fn record(&mut self, event_type: &str, data: &str) { let elapsed = self.start_time.elapsed(); self.events.push(RecordedEvent { elapsed_ms: elapsed.as_millis() as u64, event_type: event_type.to_string(), data: data.to_string(), }); } /// 記録をファイルに保存 /// /// フォーマット: JSONL (1行目: metadata, 2行目以降: events) pub fn save(&self, path: impl AsRef) -> std::io::Result<()> { let file = File::create(path)?; let mut writer = BufWriter::new(file); // メタデータを書き込み let metadata_json = serde_json::to_string(&self.metadata)?; writeln!(writer, "{}", metadata_json)?; // イベントを書き込み for event in &self.events { let event_json = serde_json::to_string(event)?; writeln!(writer, "{}", event_json)?; } writer.flush()?; Ok(()) } /// 記録されたイベント数を取得 pub fn event_count(&self) -> usize { self.events.len() } } /// SSEイベントプレイヤー /// /// 記録されたイベントを読み込み、テストで使用する pub struct EventPlayer { metadata: SessionMetadata, events: Vec, current_index: usize, } impl EventPlayer { /// ファイルから読み込み pub fn load(path: impl AsRef) -> std::io::Result { let file = File::open(path)?; let reader = BufReader::new(file); let mut lines = reader.lines(); // メタデータを読み込み let metadata_line = lines .next() .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "Empty file"))??; let metadata: SessionMetadata = serde_json::from_str(&metadata_line)?; // イベントを読み込み let mut events = Vec::new(); for line in lines { let line = line?; if !line.is_empty() { let event: RecordedEvent = serde_json::from_str(&line)?; events.push(event); } } Ok(Self { metadata, events, current_index: 0, }) } /// メタデータを取得 pub fn metadata(&self) -> &SessionMetadata { &self.metadata } /// 全イベントを取得 pub fn events(&self) -> &[RecordedEvent] { &self.events } /// イベント数を取得 pub fn event_count(&self) -> usize { self.events.len() } /// 次のイベントを取得(Iterator的に使用) pub fn next_event(&mut self) -> Option<&RecordedEvent> { if self.current_index < self.events.len() { let event = &self.events[self.current_index]; self.current_index += 1; Some(event) } else { None } } /// インデックスをリセット pub fn reset(&mut self) { self.current_index = 0; } } #[cfg(test)] mod tests { use super::*; use std::io::Write; use tempfile::NamedTempFile; #[test] fn test_record_and_playback() { // レコーダーを作成して記録 let mut recorder = EventRecorder::new("claude-sonnet-4-20250514", "Test recording"); recorder.record("message_start", r#"{"type":"message_start"}"#); recorder.record( "content_block_start", r#"{"type":"content_block_start","index":0}"#, ); recorder.record( "content_block_delta", r#"{"type":"content_block_delta","delta":{"type":"text_delta","text":"Hello"}}"#, ); // 一時ファイルに保存 let temp_file = NamedTempFile::new().unwrap(); recorder.save(temp_file.path()).unwrap(); // 読み込んで確認 let player = EventPlayer::load(temp_file.path()).unwrap(); assert_eq!(player.metadata().model, "claude-sonnet-4-20250514"); assert_eq!(player.event_count(), 3); assert_eq!(player.events()[0].event_type, "message_start"); assert_eq!(player.events()[2].event_type, "content_block_delta"); } #[test] fn test_player_iteration() { // テストデータを直接作成 let mut temp_file = NamedTempFile::new().unwrap(); writeln!( temp_file, r#"{{"timestamp":1704067200,"model":"test","description":"test"}}"# ) .unwrap(); writeln!( temp_file, r#"{{"elapsed_ms":0,"event_type":"ping","data":"{{}}"}}"# ) .unwrap(); writeln!( temp_file, r#"{{"elapsed_ms":100,"event_type":"message_stop","data":"{{}}"}}"# ) .unwrap(); temp_file.flush().unwrap(); let mut player = EventPlayer::load(temp_file.path()).unwrap(); let first = player.next_event().unwrap(); assert_eq!(first.event_type, "ping"); let second = player.next_event().unwrap(); assert_eq!(second.event_type, "message_stop"); assert!(player.next_event().is_none()); // リセット後は最初から player.reset(); assert_eq!(player.next_event().unwrap().event_type, "ping"); } }