239 lines
7.0 KiB
Rust
239 lines
7.0 KiB
Rust
//! テスト用の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<RecordedEvent>,
|
||
metadata: SessionMetadata,
|
||
}
|
||
|
||
impl EventRecorder {
|
||
/// 新しいレコーダーを作成
|
||
pub fn new(model: impl Into<String>, description: impl Into<String>) -> 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<Path>) -> 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<RecordedEvent>,
|
||
current_index: usize,
|
||
}
|
||
|
||
impl EventPlayer {
|
||
/// ファイルから読み込み
|
||
pub fn load(path: impl AsRef<Path>) -> std::io::Result<Self> {
|
||
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");
|
||
}
|
||
}
|