mod common;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use async_trait::async_trait;
use common::MockLlmClient;
use llm_worker::Worker;
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta};
fn fixtures_dir() -> std::path::PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/anthropic")
}
#[derive(Clone)]
struct MockWeatherTool {
call_count: Arc<AtomicUsize>,
}
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)
}
fn definition(&self) -> ToolDefinition {
let tool = self.clone();
Arc::new(move || {
let meta = ToolMeta::new("get_weather")
.description("Get the current weather for a city")
.input_schema(serde_json::json!({
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name"
}
},
"required": ["city"]
}));
(meta, Arc::new(tool.clone()) as Arc<dyn Tool>)
})
}
}
#[async_trait]
impl Tool for MockWeatherTool {
async fn execute(&self, input_json: &str) -> Result<String, ToolError> {
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))
}
}
#[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());
}
#[test]
fn test_mock_client_from_events() {
use llm_worker::llm_client::event::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);
}
#[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 result = worker.run("Hello").await;
assert!(result.is_ok(), "Worker should complete successfully");
}
#[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.definition()).unwrap();
let _result = worker.run("What's the weather in Tokyo?").await;
let call_count = tool_for_check.get_call_count();
println!("Tool was called {} times", call_count);
}
#[tokio::test]
async fn test_worker_with_programmatic_events() {
use llm_worker::llm_client::event::{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 result = worker.run("Greet me").await;
assert!(result.is_ok(), "Worker should complete successfully");
}
#[tokio::test]
async fn test_tool_call_collector_integration() {
use llm_worker::llm_client::event::Event;
use llm_worker::timeline::{Timeline, ToolCallCollector};
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 {
let timeline_event: llm_worker::timeline::event::Event = event.clone().into();
timeline.dispatch(&timeline_event);
}
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");
}