llm_worker_rs/worker/src/llm_client/testing.rs

239 lines
7.0 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.

//! テスト用の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");
}
}