add state constraint regression and compile-fail tests

This commit is contained in:
2026-02-19 18:31:49 +09:00
parent d63b4ea470
commit cba96e4f46
8 changed files with 266 additions and 0 deletions
+1
View File
@@ -26,3 +26,4 @@ schemars = "1.2"
tempfile = "3.24"
dotenv = "0.15"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
trybuild = "1.0.116"
+6
View File
@@ -0,0 +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/tool_server_handle_register_tool.rs");
}
@@ -0,0 +1,11 @@
use llm_worker::Worker;
use llm_worker::llm_client::providers::ollama::OllamaClient;
use std::sync::Arc;
fn main() {
let client = OllamaClient::new("dummy-model");
let worker = Worker::new(client);
let mut locked = worker.lock();
let def: llm_worker::tool::ToolDefinition = Arc::new(|| panic!("unused"));
let _ = locked.register_tool(def);
}
@@ -0,0 +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
|
10 | let _ = locked.register_tool(def);
| ^^^^^^^^^^^^^ method not found in `Worker<OllamaClient, CacheLocked>`
|
= note: the method was found for
- `Worker<C>`
@@ -0,0 +1,11 @@
use llm_worker::Worker;
use llm_worker::llm_client::providers::ollama::OllamaClient;
use std::sync::Arc;
fn main() {
let client = OllamaClient::new("dummy-model");
let worker = Worker::new(client);
let handle = worker.tool_server_handle();
let def: llm_worker::tool::ToolDefinition = Arc::new(|| panic!("unused"));
let _ = handle.register_tool(def);
}
@@ -0,0 +1,13 @@
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
|
::: src/tool_server.rs
|
| / pub(crate) fn register_tool(
| | &self,
| | factory: WorkerToolDefinition,
| | ) -> Result<(), ToolServerError> {
| |____________________________________- private method defined here
+116
View File
@@ -5,9 +5,14 @@
mod common;
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::llm_client::event::{Event, ResponseStatus, StatusEvent};
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta};
use llm_worker::Item;
// =============================================================================
@@ -91,6 +96,56 @@ fn test_mutable_extend_history() {
assert_eq!(worker.history().len(), 4);
}
#[derive(Clone)]
struct CountingTool {
name: String,
calls: Arc<AtomicUsize>,
}
impl CountingTool {
fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
calls: Arc::new(AtomicUsize::new(0)),
}
}
fn definition(&self) -> ToolDefinition {
let tool = self.clone();
Arc::new(move || {
(
ToolMeta::new(&tool.name)
.description("Counting tool")
.input_schema(serde_json::json!({"type":"object","properties":{}})),
Arc::new(tool.clone()) as Arc<dyn Tool>,
)
})
}
fn call_count(&self) -> usize {
self.calls.load(Ordering::SeqCst)
}
}
#[async_trait]
impl Tool for CountingTool {
async fn execute(&self, _input_json: &str) -> Result<String, ToolError> {
self.calls.fetch_add(1, Ordering::SeqCst);
Ok(format!("{}-ok", self.name))
}
}
/// Verify that tools can be registered in Mutable state.
#[test]
fn test_mutable_can_register_tool() {
let client = MockLlmClient::new(vec![]);
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");
}
// =============================================================================
// State Transition Tests
// =============================================================================
@@ -330,6 +385,67 @@ async fn test_unlock_edit_relock() {
assert_eq!(relocked.locked_prefix_len(), 1);
}
/// Verify that tools registered before lock and after unlock remain effective.
#[tokio::test]
async fn test_lock_unlock_relock_tools_remain_effective() {
let client = MockLlmClient::with_responses(vec![
vec![
Event::tool_use_start(0, "call_1", "tool_a"),
Event::tool_input_delta(0, r#"{}"#),
Event::tool_use_stop(0),
Event::Status(StatusEvent {
status: ResponseStatus::Completed,
}),
],
vec![
Event::text_block_start(0),
Event::text_delta(0, "done-a"),
Event::text_block_stop(0, None),
Event::Status(StatusEvent {
status: ResponseStatus::Completed,
}),
],
vec![
Event::tool_use_start(0, "call_2", "tool_b"),
Event::tool_input_delta(0, r#"{}"#),
Event::tool_use_stop(0),
Event::Status(StatusEvent {
status: ResponseStatus::Completed,
}),
],
vec![
Event::text_block_start(0),
Event::text_delta(0, "done-b"),
Event::text_block_stop(0, None),
Event::Status(StatusEvent {
status: ResponseStatus::Completed,
}),
],
]);
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");
let mut locked = worker.lock();
locked.run("first").await.expect("first run");
assert_eq!(tool_a.call_count(), 1, "tool_a should be called once");
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");
let mut relocked = unlocked.lock();
relocked.run("second").await.expect("second run");
assert_eq!(tool_a.call_count(), 1, "tool_a should not be called again");
assert_eq!(tool_b.call_count(), 1, "tool_b should be called once");
}
// =============================================================================
// System Prompt Preservation Tests
// =============================================================================