Workerの自動キャッシュロック

This commit is contained in:
2026-04-11 18:47:33 +09:00
parent f241dafac8
commit 9b78c51d0a
23 changed files with 375 additions and 352 deletions
+5 -1
View File
@@ -46,8 +46,9 @@ async fn test_callback_text_block_events() {
});
});
// Mutable::run consumes self, returns (Locked, WorkerResult)
let result = worker.run("Greet me").await;
assert!(result.is_ok(), "Worker should complete: {:?}", result);
assert!(result.is_ok(), "Worker should complete");
let deltas = text_deltas.lock().unwrap();
assert_eq!(deltas.len(), 2);
@@ -91,6 +92,7 @@ async fn test_callback_tool_call_complete() {
});
});
// Mutable::run consumes self, returns (Locked, WorkerResult)
let _ = worker.run("Weather please").await;
let starts = tool_starts.lock().unwrap();
@@ -133,6 +135,7 @@ async fn test_callback_turn_events() {
ends.lock().unwrap().push(turn);
});
// Mutable::run consumes self, returns (Locked, WorkerResult)
let result = worker.run("Do something").await;
assert!(result.is_ok());
@@ -169,6 +172,7 @@ async fn test_callback_usage_events() {
usages.lock().unwrap().push(event.clone());
});
// Mutable::run consumes self, returns (Locked, WorkerResult)
let _ = worker.run("Hello").await;
let usages = usage_events.lock().unwrap();
+1 -1
View File
@@ -1,6 +1,6 @@
#[test]
fn compile_fail_state_constraints() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/cache_locked_register_tool.rs");
t.compile_fail("tests/ui/locked_register_tool.rs");
t.compile_fail("tests/ui/tool_server_handle_register_tool.rs");
}
@@ -102,11 +102,12 @@ async fn test_parallel_tool_execution() {
let tool2_clone = tool2.clone();
let tool3_clone = tool3.clone();
worker.register_tool(tool1.definition()).unwrap();
worker.register_tool(tool2.definition()).unwrap();
worker.register_tool(tool3.definition()).unwrap();
worker.register_tool(tool1.definition());
worker.register_tool(tool2.definition());
worker.register_tool(tool3.definition());
let start = Instant::now();
// Mutable::run consumes self, returns (Locked, WorkerResult)
let _result = worker.run("Run all tools").await;
let elapsed = start.elapsed();
@@ -150,8 +151,8 @@ async fn test_before_tool_call_skip() {
let allowed_clone = allowed_tool.clone();
let blocked_clone = blocked_tool.clone();
worker.register_tool(allowed_tool.definition()).unwrap();
worker.register_tool(blocked_tool.definition()).unwrap();
worker.register_tool(allowed_tool.definition());
worker.register_tool(blocked_tool.definition());
// Policy to skip "blocked_tool"
struct BlockingPolicy;
@@ -169,6 +170,7 @@ async fn test_before_tool_call_skip() {
worker.set_interceptor(BlockingPolicy);
// Mutable::run consumes self, returns (Locked, WorkerResult)
let _result = worker.run("Test hook").await;
// allowed_tool is called, but blocked_tool is not
@@ -230,7 +232,7 @@ async fn test_post_tool_call_modification() {
})
}
worker.register_tool(simple_tool_definition()).unwrap();
worker.register_tool(simple_tool_definition());
// Policy to modify results
struct ModifyingPolicy {
@@ -251,9 +253,10 @@ async fn test_post_tool_call_modification() {
modified_content: modified_content.clone(),
});
// Mutable::run consumes self, returns (Locked, WorkerResult)
let result = worker.run("Test modification").await;
assert!(result.is_ok(), "Worker should complete: {:?}", result);
assert!(result.is_ok(), "Worker should complete");
// Verify hook was called and content was modified
let content = modified_content.lock().unwrap().clone();
@@ -1,8 +1,8 @@
error[E0599]: no method named `register_tool` found for struct `Worker<OllamaClient, CacheLocked>` in the current scope
--> tests/ui/cache_locked_register_tool.rs:10:20
error[E0599]: no method named `register_tool` found for struct `Worker<OllamaClient, Locked>` in the current scope
--> tests/ui/locked_register_tool.rs:10:20
|
10 | let _ = locked.register_tool(def);
| ^^^^^^^^^^^^^ method not found in `Worker<OllamaClient, CacheLocked>`
| ^^^^^^^^^^^^^ method not found in `Worker<OllamaClient, Locked>`
|
= note: the method was found for
- `Worker<C>`
@@ -1,13 +1,10 @@
error[E0624]: method `register_tool` is private
--> tests/ui/tool_server_handle_register_tool.rs:10:20
|
10 | let _ = handle.register_tool(def);
| ^^^^^^^^^^^^^ private method
10 | let _ = handle.register_tool(def);
| ^^^^^^^^^^^^^ private method
|
::: src/tool_server.rs
|
| / pub(crate) fn register_tool(
| | &self,
| | factory: WorkerToolDefinition,
| | ) -> Result<(), ToolServerError> {
| |____________________________________- private method defined here
| pub(crate) fn register_tool(&self, factory: WorkerToolDefinition) {
| ----------------------------------------------------------------- private method defined here
+6 -5
View File
@@ -129,9 +129,9 @@ async fn test_worker_simple_text_response() {
}
let client = MockLlmClient::from_fixture(&fixture_path).unwrap();
let mut worker = Worker::new(client);
let worker = Worker::new(client);
// Send a simple message
// Send a simple message (Mutable::run consumes self, returns tuple)
let result = worker.run("Hello").await;
assert!(result.is_ok(), "Worker should complete successfully");
@@ -156,9 +156,9 @@ async fn test_worker_tool_call() {
// Register tool
let weather_tool = MockWeatherTool::new();
let tool_for_check = weather_tool.clone();
worker.register_tool(weather_tool.definition()).unwrap();
worker.register_tool(weather_tool.definition());
// Send message
// Send message (Mutable::run consumes self, returns tuple)
let _result = worker.run("What's the weather in Tokyo?").await;
// Verify tool was called
@@ -190,8 +190,9 @@ async fn test_worker_with_programmatic_events() {
];
let client = MockLlmClient::new(events);
let mut worker = Worker::new(client);
let worker = Worker::new(client);
// Mutable::run consumes self, returns tuple
let result = worker.run("Greet me").await;
assert!(result.is_ok(), "Worker should complete successfully");
+25 -24
View File
@@ -1,6 +1,6 @@
//! Worker state management tests
//!
//! Tests for state transitions using the Type-state pattern (Mutable/CacheLocked)
//! Tests for state transitions using the Type-state pattern (Mutable/Locked)
//! and state preservation between turns.
mod common;
@@ -11,7 +11,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use async_trait::async_trait;
use common::MockLlmClient;
use llm_worker::Item;
use llm_worker::Worker;
use llm_worker::{Worker, WorkerError};
use llm_worker::llm_client::event::{Event, ResponseStatus, StatusEvent};
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta};
@@ -147,15 +147,15 @@ fn test_mutable_can_register_tool() {
let mut worker = Worker::new(client);
let tool = CountingTool::new("count_tool");
let result = worker.register_tool(tool.definition());
assert!(result.is_ok(), "Mutable should allow tool registration");
// register_tool is infallible (factory deferred to run-time flush)
worker.register_tool(tool.definition());
}
// =============================================================================
// State Transition Tests
// =============================================================================
/// Verify that lock() transitions from Mutable -> CacheLocked state
/// Verify that lock() transitions from Mutable -> Locked state
#[test]
fn test_lock_transition() {
let client = MockLlmClient::new(vec![]);
@@ -168,13 +168,13 @@ fn test_lock_transition() {
// Lock
let locked_worker = worker.lock();
// History and system prompt are still accessible in CacheLocked state
// History and system prompt are still accessible in Locked state
assert_eq!(locked_worker.get_system_prompt(), Some("System"));
assert_eq!(locked_worker.history().len(), 2);
assert_eq!(locked_worker.locked_prefix_len(), 2);
}
/// Verify that unlock() transitions from CacheLocked -> Mutable state
/// Verify that unlock() transitions from Locked -> Mutable state
#[test]
fn test_unlock_transition() {
let client = MockLlmClient::new(vec![]);
@@ -198,7 +198,7 @@ fn test_unlock_transition() {
/// Verify that history is correctly updated after running a turn in Mutable state
#[tokio::test]
async fn test_mutable_run_updates_history() {
async fn test_mutable_run_updates_history() -> Result<(), WorkerError> {
let events = vec![
Event::text_block_start(0),
Event::text_delta(0, "Hello, I'm an assistant!"),
@@ -209,11 +209,10 @@ async fn test_mutable_run_updates_history() {
];
let client = MockLlmClient::new(events);
let mut worker = Worker::new(client);
let worker = Worker::new(client);
// Execute
let result = worker.run("Hi there").await;
assert!(result.is_ok());
// Execute (Mutable::run consumes self, returns (Locked, WorkerResult))
let (worker, _result) = worker.run("Hi there").await?;
// History is updated
let history = worker.history();
@@ -224,9 +223,11 @@ async fn test_mutable_run_updates_history() {
// Assistant message
assert_eq!(history[1].as_text(), Some("Hello, I'm an assistant!"));
Ok(())
}
/// Verify that history accumulates correctly over multiple turns in CacheLocked state
/// Verify that history accumulates correctly over multiple turns in Locked state
#[tokio::test]
async fn test_locked_multi_turn_history_accumulation() {
// Prepare responses for 2 requests
@@ -327,7 +328,7 @@ async fn test_locked_prefix_len_tracking() {
/// Verify that turn count is correctly incremented
#[tokio::test]
async fn test_turn_count_increment() {
async fn test_turn_count_increment() -> Result<(), WorkerError> {
let client = MockLlmClient::with_responses(vec![
vec![
Event::text_block_start(0),
@@ -347,15 +348,19 @@ async fn test_turn_count_increment() {
],
]);
let mut worker = Worker::new(client);
let worker = Worker::new(client);
assert_eq!(worker.turn_count(), 0);
worker.run("First").await.unwrap();
// First run consumes Mutable, returns Locked
let (mut worker, _) = worker.run("First").await?;
assert_eq!(worker.turn_count(), 1);
worker.run("Second").await.unwrap();
// Subsequent runs on Locked take &mut self
worker.run("Second").await?;
assert_eq!(worker.turn_count(), 2);
Ok(())
}
/// Verify that history can be edited after unlock and re-locked
@@ -430,9 +435,7 @@ async fn test_lock_unlock_relock_tools_remain_effective() {
let mut worker = Worker::new(client);
let tool_a = CountingTool::new("tool_a");
worker
.register_tool(tool_a.definition())
.expect("register tool_a should succeed");
worker.register_tool(tool_a.definition());
let mut locked = worker.lock();
locked.run("first").await.expect("first run");
@@ -440,9 +443,7 @@ async fn test_lock_unlock_relock_tools_remain_effective() {
let mut unlocked = locked.unlock();
let tool_b = CountingTool::new("tool_b");
unlocked
.register_tool(tool_b.definition())
.expect("register tool_b after unlock should succeed");
unlocked.register_tool(tool_b.definition());
let mut relocked = unlocked.lock();
relocked.run("second").await.expect("second run");
@@ -455,7 +456,7 @@ async fn test_lock_unlock_relock_tools_remain_effective() {
// System Prompt Preservation Tests
// =============================================================================
/// Verify that system prompt is preserved in CacheLocked state
/// Verify that system prompt is preserved in Locked state
#[test]
fn test_system_prompt_preserved_in_locked_state() {
let client = MockLlmClient::new(vec![]);