feat: Implement AnthropicClient
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
//! Anthropic フィクスチャベースの統合テスト
|
||||
//!
|
||||
//! 記録されたAPIレスポンスを使ってイベントパースをテストする
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::Path;
|
||||
|
||||
use worker_types::{BlockType, DeltaContent, Event, ResponseStatus};
|
||||
|
||||
/// フィクスチャファイルからEventを読み込む
|
||||
fn load_events_from_fixture(path: impl AsRef<Path>) -> Vec<Event> {
|
||||
let file = File::open(path).expect("Failed to open fixture file");
|
||||
let reader = BufReader::new(file);
|
||||
let mut lines = reader.lines();
|
||||
|
||||
// 最初の行はメタデータ、スキップ
|
||||
let _metadata = lines.next().expect("Empty fixture file").unwrap();
|
||||
|
||||
// 残りはイベント
|
||||
let mut events = Vec::new();
|
||||
for line in lines {
|
||||
let line = line.unwrap();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// RecordedEvent構造体をパース
|
||||
let recorded: serde_json::Value = serde_json::from_str(&line).unwrap();
|
||||
let data = recorded["data"].as_str().unwrap();
|
||||
|
||||
// data フィールドからEventをデシリアライズ
|
||||
let event: Event = serde_json::from_str(data).unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
events
|
||||
}
|
||||
|
||||
/// フィクスチャディレクトリからanthropic_*ファイルを検索
|
||||
fn find_anthropic_fixtures() -> Vec<std::path::PathBuf> {
|
||||
let fixtures_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
|
||||
|
||||
if !fixtures_dir.exists() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
std::fs::read_dir(&fixtures_dir)
|
||||
.unwrap()
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.path())
|
||||
.filter(|p| {
|
||||
p.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.is_some_and(|n| n.starts_with("anthropic_") && n.ends_with(".jsonl"))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fixture_events_deserialize() {
|
||||
let fixtures = find_anthropic_fixtures();
|
||||
assert!(!fixtures.is_empty(), "No anthropic fixtures found");
|
||||
|
||||
for fixture_path in fixtures {
|
||||
println!("Testing fixture: {:?}", fixture_path);
|
||||
let events = load_events_from_fixture(&fixture_path);
|
||||
|
||||
assert!(!events.is_empty(), "Fixture should contain events");
|
||||
|
||||
// 各イベントが正しくデシリアライズされているか確認
|
||||
for event in &events {
|
||||
// Debugトレイトで出力可能か確認
|
||||
let _ = format!("{:?}", event);
|
||||
}
|
||||
|
||||
println!(" Loaded {} events", events.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fixture_event_sequence() {
|
||||
let fixtures = find_anthropic_fixtures();
|
||||
if fixtures.is_empty() {
|
||||
println!("No fixtures found, skipping test");
|
||||
return;
|
||||
}
|
||||
|
||||
// 最初のフィクスチャをテスト
|
||||
let events = load_events_from_fixture(&fixtures[0]);
|
||||
|
||||
// 期待されるイベントシーケンスを検証
|
||||
// Usage -> BlockStart -> BlockDelta -> BlockStop -> Usage -> Status
|
||||
|
||||
// 最初のUsageイベント
|
||||
assert!(
|
||||
matches!(&events[0], Event::Usage(_)),
|
||||
"First event should be Usage"
|
||||
);
|
||||
|
||||
// BlockStartイベント
|
||||
if let Event::BlockStart(start) = &events[1] {
|
||||
assert_eq!(start.block_type, BlockType::Text);
|
||||
assert_eq!(start.index, 0);
|
||||
} else {
|
||||
panic!("Second event should be BlockStart");
|
||||
}
|
||||
|
||||
// BlockDeltaイベント
|
||||
if let Event::BlockDelta(delta) = &events[2] {
|
||||
assert_eq!(delta.index, 0);
|
||||
if let DeltaContent::Text(text) = &delta.delta {
|
||||
assert!(!text.is_empty(), "Delta text should not be empty");
|
||||
println!(" Text content: {}", text);
|
||||
} else {
|
||||
panic!("Delta should be Text");
|
||||
}
|
||||
} else {
|
||||
panic!("Third event should be BlockDelta");
|
||||
}
|
||||
|
||||
// BlockStopイベント
|
||||
if let Event::BlockStop(stop) = &events[3] {
|
||||
assert_eq!(stop.block_type, BlockType::Text);
|
||||
assert_eq!(stop.index, 0);
|
||||
} else {
|
||||
panic!("Fourth event should be BlockStop");
|
||||
}
|
||||
|
||||
// 最後のStatusイベント
|
||||
if let Event::Status(status) = events.last().unwrap() {
|
||||
assert_eq!(status.status, ResponseStatus::Completed);
|
||||
} else {
|
||||
panic!("Last event should be Status(Completed)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fixture_usage_tokens() {
|
||||
let fixtures = find_anthropic_fixtures();
|
||||
if fixtures.is_empty() {
|
||||
println!("No fixtures found, skipping test");
|
||||
return;
|
||||
}
|
||||
|
||||
let events = load_events_from_fixture(&fixtures[0]);
|
||||
|
||||
// Usageイベントを収集
|
||||
let usage_events: Vec<_> = events
|
||||
.iter()
|
||||
.filter_map(|e| {
|
||||
if let Event::Usage(u) = e {
|
||||
Some(u)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
!usage_events.is_empty(),
|
||||
"Should have at least one Usage event"
|
||||
);
|
||||
|
||||
// 最後のUsageイベントはトークン数を持つはず
|
||||
let last_usage = usage_events.last().unwrap();
|
||||
assert!(last_usage.input_tokens.is_some());
|
||||
assert!(last_usage.output_tokens.is_some());
|
||||
assert!(last_usage.total_tokens.is_some());
|
||||
|
||||
println!(
|
||||
" Token usage: {} input, {} output, {} total",
|
||||
last_usage.input_tokens.unwrap(),
|
||||
last_usage.output_tokens.unwrap(),
|
||||
last_usage.total_tokens.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fixture_with_timeline() {
|
||||
use std::sync::{Arc, Mutex};
|
||||
use worker::{Handler, TextBlockEvent, TextBlockKind, Timeline};
|
||||
|
||||
let fixtures = find_anthropic_fixtures();
|
||||
if fixtures.is_empty() {
|
||||
println!("No fixtures found, skipping test");
|
||||
return;
|
||||
}
|
||||
|
||||
let events = load_events_from_fixture(&fixtures[0]);
|
||||
|
||||
// テスト用ハンドラー
|
||||
struct TestCollector {
|
||||
texts: Arc<Mutex<Vec<String>>>,
|
||||
}
|
||||
|
||||
impl Handler<TextBlockKind> for TestCollector {
|
||||
type Scope = String;
|
||||
|
||||
fn on_event(&mut self, buffer: &mut String, event: &TextBlockEvent) {
|
||||
match event {
|
||||
TextBlockEvent::Start(_) => {}
|
||||
TextBlockEvent::Delta(text) => buffer.push_str(text),
|
||||
TextBlockEvent::Stop(_) => {
|
||||
let text = std::mem::take(buffer);
|
||||
self.texts.lock().unwrap().push(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let collected = Arc::new(Mutex::new(Vec::new()));
|
||||
let mut timeline = Timeline::new();
|
||||
timeline.on_text_block(TestCollector {
|
||||
texts: collected.clone(),
|
||||
});
|
||||
|
||||
// フィクスチャからのイベントをTimelineにディスパッチ
|
||||
for event in &events {
|
||||
timeline.dispatch(event);
|
||||
}
|
||||
|
||||
// テキストが収集されたことを確認
|
||||
let texts = collected.lock().unwrap();
|
||||
assert_eq!(texts.len(), 1, "Should have collected one text block");
|
||||
assert!(!texts[0].is_empty(), "Collected text should not be empty");
|
||||
println!(" Collected text: {}", texts[0]);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{"timestamp":1767624445,"model":"claude-sonnet-4-20250514","description":"Simple greeting test"}
|
||||
{"elapsed_ms":1697,"event_type":"Discriminant(1)","data":"{\"Usage\":{\"input_tokens\":24,\"output_tokens\":2,\"total_tokens\":26,\"cache_read_input_tokens\":0,\"cache_creation_input_tokens\":0}}"}
|
||||
{"elapsed_ms":1697,"event_type":"Discriminant(4)","data":"{\"BlockStart\":{\"index\":0,\"block_type\":\"Text\",\"metadata\":\"Text\"}}"}
|
||||
{"elapsed_ms":1697,"event_type":"Discriminant(5)","data":"{\"BlockDelta\":{\"index\":0,\"delta\":{\"Text\":\"Hello!\"}}}"}
|
||||
{"elapsed_ms":1885,"event_type":"Discriminant(6)","data":"{\"BlockStop\":{\"index\":0,\"block_type\":\"Text\",\"stop_reason\":null}}"}
|
||||
{"elapsed_ms":1929,"event_type":"Discriminant(1)","data":"{\"Usage\":{\"input_tokens\":24,\"output_tokens\":5,\"total_tokens\":29,\"cache_read_input_tokens\":0,\"cache_creation_input_tokens\":0}}"}
|
||||
{"elapsed_ms":1929,"event_type":"Discriminant(2)","data":"{\"Status\":{\"status\":\"Completed\"}}"}
|
||||
Reference in New Issue
Block a user