feat: refactor sgclaw around zeroclaw compat runtime
This commit is contained in:
62
third_party/zeroclaw/tests/support/assertions.rs
vendored
Normal file
62
third_party/zeroclaw/tests/support/assertions.rs
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
//! Declarative expectation verification for trace fixtures.
|
||||
|
||||
use super::trace::TraceExpects;
|
||||
|
||||
/// Verify trace expectations against actual test results.
|
||||
///
|
||||
/// - `expects`: declarative expectations from the trace fixture
|
||||
/// - `final_response`: the final text response from the agent
|
||||
/// - `tools_called`: names of tools that were actually called
|
||||
/// - `label`: test label for error messages
|
||||
pub fn verify_expects(
|
||||
expects: &TraceExpects,
|
||||
final_response: &str,
|
||||
tools_called: &[String],
|
||||
label: &str,
|
||||
) {
|
||||
for needle in &expects.response_contains {
|
||||
assert!(
|
||||
final_response.contains(needle),
|
||||
"[{label}] Expected response to contain \"{needle}\", got: {final_response}"
|
||||
);
|
||||
}
|
||||
|
||||
for needle in &expects.response_not_contains {
|
||||
assert!(
|
||||
!final_response.contains(needle),
|
||||
"[{label}] Expected response NOT to contain \"{needle}\", got: {final_response}"
|
||||
);
|
||||
}
|
||||
|
||||
for tool in &expects.tools_used {
|
||||
assert!(
|
||||
tools_called.iter().any(|t| t == tool),
|
||||
"[{label}] Expected tool \"{tool}\" to be used, but tools called were: {tools_called:?}"
|
||||
);
|
||||
}
|
||||
|
||||
for tool in &expects.tools_not_used {
|
||||
assert!(
|
||||
!tools_called.iter().any(|t| t == tool),
|
||||
"[{label}] Expected tool \"{tool}\" NOT to be used, but it was called"
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(max) = expects.max_tool_calls {
|
||||
assert!(
|
||||
tools_called.len() <= max,
|
||||
"[{label}] Expected at most {max} tool calls, got {}",
|
||||
tools_called.len()
|
||||
);
|
||||
}
|
||||
|
||||
for pattern in &expects.response_matches {
|
||||
let re = regex::Regex::new(pattern).unwrap_or_else(|e| {
|
||||
panic!("[{label}] Invalid regex pattern \"{pattern}\": {e}");
|
||||
});
|
||||
assert!(
|
||||
re.is_match(final_response),
|
||||
"[{label}] Expected response to match regex \"{pattern}\", got: {final_response}"
|
||||
);
|
||||
}
|
||||
}
|
||||
142
third_party/zeroclaw/tests/support/helpers.rs
vendored
Normal file
142
third_party/zeroclaw/tests/support/helpers.rs
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
//! Shared builder helpers for constructing test agents.
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use std::sync::Arc;
|
||||
use zeroclaw::agent::agent::Agent;
|
||||
use zeroclaw::agent::dispatcher::{NativeToolDispatcher, XmlToolDispatcher};
|
||||
use zeroclaw::agent::memory_loader::MemoryLoader;
|
||||
use zeroclaw::config::MemoryConfig;
|
||||
use zeroclaw::memory;
|
||||
use zeroclaw::memory::Memory;
|
||||
use zeroclaw::observability::{NoopObserver, Observer};
|
||||
use zeroclaw::providers::{ChatResponse, Provider, ToolCall};
|
||||
use zeroclaw::tools::Tool;
|
||||
|
||||
/// Create an in-memory "none" backend for tests.
|
||||
pub fn make_memory() -> Arc<dyn Memory> {
|
||||
let cfg = MemoryConfig {
|
||||
backend: "none".into(),
|
||||
..MemoryConfig::default()
|
||||
};
|
||||
Arc::from(memory::create_memory(&cfg, &std::env::temp_dir(), None).unwrap())
|
||||
}
|
||||
|
||||
/// Create a `NoopObserver` for tests.
|
||||
pub fn make_observer() -> Arc<dyn Observer> {
|
||||
Arc::from(NoopObserver {})
|
||||
}
|
||||
|
||||
/// Create a text-only `ChatResponse`.
|
||||
pub fn text_response(text: &str) -> ChatResponse {
|
||||
ChatResponse {
|
||||
text: Some(text.into()),
|
||||
tool_calls: vec![],
|
||||
usage: None,
|
||||
reasoning_content: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a `ChatResponse` with tool calls.
|
||||
pub fn tool_response(calls: Vec<ToolCall>) -> ChatResponse {
|
||||
ChatResponse {
|
||||
text: Some(String::new()),
|
||||
tool_calls: calls,
|
||||
usage: None,
|
||||
reasoning_content: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an agent with `NativeToolDispatcher`.
|
||||
pub fn build_agent(provider: Box<dyn Provider>, tools: Vec<Box<dyn Tool>>) -> Agent {
|
||||
Agent::builder()
|
||||
.provider(provider)
|
||||
.tools(tools)
|
||||
.memory(make_memory())
|
||||
.observer(make_observer())
|
||||
.tool_dispatcher(Box::new(NativeToolDispatcher))
|
||||
.workspace_dir(std::env::temp_dir())
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Build an agent with `XmlToolDispatcher`.
|
||||
pub fn build_agent_xml(provider: Box<dyn Provider>, tools: Vec<Box<dyn Tool>>) -> Agent {
|
||||
Agent::builder()
|
||||
.provider(provider)
|
||||
.tools(tools)
|
||||
.memory(make_memory())
|
||||
.observer(make_observer())
|
||||
.tool_dispatcher(Box::new(XmlToolDispatcher))
|
||||
.workspace_dir(std::env::temp_dir())
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Build an agent with optional custom `MemoryLoader`.
|
||||
pub fn build_recording_agent(
|
||||
provider: Box<dyn Provider>,
|
||||
tools: Vec<Box<dyn Tool>>,
|
||||
memory_loader: Option<Box<dyn MemoryLoader>>,
|
||||
) -> Agent {
|
||||
let mut builder = Agent::builder()
|
||||
.provider(provider)
|
||||
.tools(tools)
|
||||
.memory(make_memory())
|
||||
.observer(make_observer())
|
||||
.tool_dispatcher(Box::new(NativeToolDispatcher))
|
||||
.workspace_dir(std::env::temp_dir());
|
||||
|
||||
if let Some(loader) = memory_loader {
|
||||
builder = builder.memory_loader(loader);
|
||||
}
|
||||
|
||||
builder.build().unwrap()
|
||||
}
|
||||
|
||||
/// Build an agent with real `SqliteMemory` in a temporary directory.
|
||||
pub fn build_agent_with_sqlite_memory(
|
||||
provider: Box<dyn Provider>,
|
||||
tools: Vec<Box<dyn Tool>>,
|
||||
temp_dir: &std::path::Path,
|
||||
) -> Agent {
|
||||
let cfg = MemoryConfig {
|
||||
backend: "sqlite".into(),
|
||||
..MemoryConfig::default()
|
||||
};
|
||||
let mem = Arc::from(memory::create_memory(&cfg, temp_dir, None).unwrap());
|
||||
Agent::builder()
|
||||
.provider(provider)
|
||||
.tools(tools)
|
||||
.memory(mem)
|
||||
.observer(make_observer())
|
||||
.tool_dispatcher(Box::new(NativeToolDispatcher))
|
||||
.workspace_dir(std::env::temp_dir())
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Mock memory loader that returns a static context string.
|
||||
pub struct StaticMemoryLoader {
|
||||
context: String,
|
||||
}
|
||||
|
||||
impl StaticMemoryLoader {
|
||||
pub fn new(context: &str) -> Self {
|
||||
Self {
|
||||
context: context.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MemoryLoader for StaticMemoryLoader {
|
||||
async fn load_context(
|
||||
&self,
|
||||
_memory: &dyn Memory,
|
||||
_user_message: &str,
|
||||
_session_id: Option<&str>,
|
||||
) -> Result<String> {
|
||||
Ok(self.context.clone())
|
||||
}
|
||||
}
|
||||
86
third_party/zeroclaw/tests/support/mock_channel.rs
vendored
Normal file
86
third_party/zeroclaw/tests/support/mock_channel.rs
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
//! Mock channel for system-level tests.
|
||||
//!
|
||||
//! `TestChannel` implements the `Channel` trait with MPSC-based message
|
||||
//! injection and response capture for race-free testing.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use zeroclaw::channels::traits::{Channel, ChannelMessage, SendMessage};
|
||||
|
||||
/// A test channel that captures sent messages and supports message injection.
|
||||
pub struct TestChannel {
|
||||
name: String,
|
||||
sent_messages: Arc<Mutex<Vec<SendMessage>>>,
|
||||
typing_events: Arc<Mutex<Vec<TypingEvent>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TypingEvent {
|
||||
Start(String),
|
||||
Stop(String),
|
||||
}
|
||||
|
||||
impl TestChannel {
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
sent_messages: Arc::new(Mutex::new(Vec::new())),
|
||||
typing_events: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all messages sent through this channel.
|
||||
pub fn sent_messages(&self) -> Vec<SendMessage> {
|
||||
self.sent_messages.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
/// Get all typing events recorded by this channel.
|
||||
pub fn typing_events(&self) -> Vec<TypingEvent> {
|
||||
self.typing_events.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
/// Clear captured messages and events.
|
||||
pub fn clear(&self) {
|
||||
self.sent_messages.lock().unwrap().clear();
|
||||
self.typing_events.lock().unwrap().clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Channel for TestChannel {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
async fn send(&self, message: &SendMessage) -> anyhow::Result<()> {
|
||||
self.sent_messages.lock().unwrap().push(message.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn listen(&self, _tx: tokio::sync::mpsc::Sender<ChannelMessage>) -> anyhow::Result<()> {
|
||||
// System tests drive the agent via turn() rather than channel listen,
|
||||
// so this is a no-op. For channel-driven tests, messages are injected
|
||||
// via the MPSC sender directly.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn health_check(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn start_typing(&self, recipient: &str) -> anyhow::Result<()> {
|
||||
self.typing_events
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(TypingEvent::Start(recipient.to_string()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop_typing(&self, recipient: &str) -> anyhow::Result<()> {
|
||||
self.typing_events
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(TypingEvent::Stop(recipient.to_string()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
199
third_party/zeroclaw/tests/support/mock_provider.rs
vendored
Normal file
199
third_party/zeroclaw/tests/support/mock_provider.rs
vendored
Normal file
@@ -0,0 +1,199 @@
|
||||
//! Shared mock provider implementations for integration tests.
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use zeroclaw::providers::traits::{ChatMessage, TokenUsage};
|
||||
use zeroclaw::providers::{ChatRequest, ChatResponse, Provider, ToolCall};
|
||||
|
||||
use super::trace::{LlmTrace, TraceResponse};
|
||||
|
||||
/// Mock provider that returns scripted responses in FIFO order.
|
||||
pub struct MockProvider {
|
||||
responses: Mutex<Vec<ChatResponse>>,
|
||||
}
|
||||
|
||||
impl MockProvider {
|
||||
pub fn new(responses: Vec<ChatResponse>) -> Self {
|
||||
Self {
|
||||
responses: Mutex::new(responses),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Provider for MockProvider {
|
||||
async fn chat_with_system(
|
||||
&self,
|
||||
_system_prompt: Option<&str>,
|
||||
_message: &str,
|
||||
_model: &str,
|
||||
_temperature: f64,
|
||||
) -> Result<String> {
|
||||
Ok("fallback".into())
|
||||
}
|
||||
|
||||
async fn chat(
|
||||
&self,
|
||||
_request: ChatRequest<'_>,
|
||||
_model: &str,
|
||||
_temperature: f64,
|
||||
) -> Result<ChatResponse> {
|
||||
let mut guard = self.responses.lock().unwrap();
|
||||
if guard.is_empty() {
|
||||
return Ok(ChatResponse {
|
||||
text: Some("done".into()),
|
||||
tool_calls: vec![],
|
||||
usage: None,
|
||||
reasoning_content: None,
|
||||
});
|
||||
}
|
||||
Ok(guard.remove(0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Mock provider that returns scripted responses AND records every request.
|
||||
pub struct RecordingProvider {
|
||||
responses: Mutex<Vec<ChatResponse>>,
|
||||
recorded_requests: Arc<Mutex<Vec<Vec<ChatMessage>>>>,
|
||||
}
|
||||
|
||||
impl RecordingProvider {
|
||||
pub fn new(responses: Vec<ChatResponse>) -> (Self, Arc<Mutex<Vec<Vec<ChatMessage>>>>) {
|
||||
let recorded = Arc::new(Mutex::new(Vec::new()));
|
||||
let provider = Self {
|
||||
responses: Mutex::new(responses),
|
||||
recorded_requests: recorded.clone(),
|
||||
};
|
||||
(provider, recorded)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Provider for RecordingProvider {
|
||||
async fn chat_with_system(
|
||||
&self,
|
||||
_system_prompt: Option<&str>,
|
||||
_message: &str,
|
||||
_model: &str,
|
||||
_temperature: f64,
|
||||
) -> Result<String> {
|
||||
Ok("fallback".into())
|
||||
}
|
||||
|
||||
async fn chat(
|
||||
&self,
|
||||
request: ChatRequest<'_>,
|
||||
_model: &str,
|
||||
_temperature: f64,
|
||||
) -> Result<ChatResponse> {
|
||||
self.recorded_requests
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(request.messages.to_vec());
|
||||
|
||||
let mut guard = self.responses.lock().unwrap();
|
||||
if guard.is_empty() {
|
||||
return Ok(ChatResponse {
|
||||
text: Some("done".into()),
|
||||
tool_calls: vec![],
|
||||
usage: None,
|
||||
reasoning_content: None,
|
||||
});
|
||||
}
|
||||
Ok(guard.remove(0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider that replays responses from an `LlmTrace` fixture.
|
||||
///
|
||||
/// Each call to `chat()` returns the next step from the trace in FIFO order.
|
||||
/// If the agent calls the provider more times than there are steps, an error is returned.
|
||||
pub struct TraceLlmProvider {
|
||||
steps: Mutex<Vec<TraceResponse>>,
|
||||
trace_name: String,
|
||||
}
|
||||
|
||||
impl TraceLlmProvider {
|
||||
pub fn from_trace(trace: &LlmTrace) -> Self {
|
||||
let mut steps = Vec::new();
|
||||
for turn in &trace.turns {
|
||||
for step in &turn.steps {
|
||||
steps.push(step.response.clone());
|
||||
}
|
||||
}
|
||||
Self {
|
||||
steps: Mutex::new(steps),
|
||||
trace_name: trace.model_name.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Provider for TraceLlmProvider {
|
||||
async fn chat_with_system(
|
||||
&self,
|
||||
_system_prompt: Option<&str>,
|
||||
_message: &str,
|
||||
_model: &str,
|
||||
_temperature: f64,
|
||||
) -> Result<String> {
|
||||
Ok("fallback".into())
|
||||
}
|
||||
|
||||
async fn chat(
|
||||
&self,
|
||||
_request: ChatRequest<'_>,
|
||||
_model: &str,
|
||||
_temperature: f64,
|
||||
) -> Result<ChatResponse> {
|
||||
let mut guard = self.steps.lock().unwrap();
|
||||
if guard.is_empty() {
|
||||
anyhow::bail!(
|
||||
"TraceLlmProvider({}) exhausted: no more steps in trace",
|
||||
self.trace_name
|
||||
);
|
||||
}
|
||||
let step = guard.remove(0);
|
||||
match step {
|
||||
TraceResponse::Text {
|
||||
content,
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
} => Ok(ChatResponse {
|
||||
text: Some(content),
|
||||
tool_calls: vec![],
|
||||
usage: Some(TokenUsage {
|
||||
input_tokens: Some(input_tokens),
|
||||
output_tokens: Some(output_tokens),
|
||||
cached_input_tokens: None,
|
||||
}),
|
||||
reasoning_content: None,
|
||||
}),
|
||||
TraceResponse::ToolCalls {
|
||||
tool_calls,
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
} => {
|
||||
let calls = tool_calls
|
||||
.into_iter()
|
||||
.map(|tc| ToolCall {
|
||||
id: tc.id,
|
||||
name: tc.name,
|
||||
arguments: serde_json::to_string(&tc.arguments).unwrap_or_default(),
|
||||
})
|
||||
.collect();
|
||||
Ok(ChatResponse {
|
||||
text: Some(String::new()),
|
||||
tool_calls: calls,
|
||||
usage: Some(TokenUsage {
|
||||
input_tokens: Some(input_tokens),
|
||||
output_tokens: Some(output_tokens),
|
||||
cached_input_tokens: None,
|
||||
}),
|
||||
reasoning_content: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
152
third_party/zeroclaw/tests/support/mock_tools.rs
vendored
Normal file
152
third_party/zeroclaw/tests/support/mock_tools.rs
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
//! Shared mock tool implementations for integration tests.
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use serde_json::json;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use zeroclaw::tools::{Tool, ToolResult};
|
||||
|
||||
/// Simple tool that echoes its input argument.
|
||||
pub struct EchoTool;
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for EchoTool {
|
||||
fn name(&self) -> &str {
|
||||
"echo"
|
||||
}
|
||||
fn description(&self) -> &str {
|
||||
"Echoes the input message"
|
||||
}
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {"type": "string"}
|
||||
}
|
||||
})
|
||||
}
|
||||
async fn execute(&self, args: serde_json::Value) -> Result<ToolResult> {
|
||||
let msg = args
|
||||
.get("message")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("(empty)")
|
||||
.to_string();
|
||||
Ok(ToolResult {
|
||||
success: true,
|
||||
output: msg,
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool that tracks invocation count for verifying dispatch.
|
||||
pub struct CountingTool {
|
||||
count: Arc<Mutex<usize>>,
|
||||
}
|
||||
|
||||
impl CountingTool {
|
||||
pub fn new() -> (Self, Arc<Mutex<usize>>) {
|
||||
let count = Arc::new(Mutex::new(0));
|
||||
(
|
||||
Self {
|
||||
count: count.clone(),
|
||||
},
|
||||
count,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for CountingTool {
|
||||
fn name(&self) -> &str {
|
||||
"counter"
|
||||
}
|
||||
fn description(&self) -> &str {
|
||||
"Counts invocations"
|
||||
}
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
json!({"type": "object"})
|
||||
}
|
||||
async fn execute(&self, _args: serde_json::Value) -> Result<ToolResult> {
|
||||
let mut c = self.count.lock().unwrap();
|
||||
*c += 1;
|
||||
Ok(ToolResult {
|
||||
success: true,
|
||||
output: format!("call #{}", *c),
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool that always fails, simulating a broken external service.
|
||||
pub struct FailingTool;
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for FailingTool {
|
||||
fn name(&self) -> &str {
|
||||
"failing_tool"
|
||||
}
|
||||
fn description(&self) -> &str {
|
||||
"Always fails"
|
||||
}
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
json!({"type": "object"})
|
||||
}
|
||||
async fn execute(&self, _args: serde_json::Value) -> Result<ToolResult> {
|
||||
Ok(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some("Service unavailable: connection timeout".into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool that captures all arguments for assertion.
|
||||
pub struct RecordingTool {
|
||||
name: String,
|
||||
calls: Arc<Mutex<Vec<serde_json::Value>>>,
|
||||
}
|
||||
|
||||
impl RecordingTool {
|
||||
pub fn new(name: &str) -> (Self, Arc<Mutex<Vec<serde_json::Value>>>) {
|
||||
let calls = Arc::new(Mutex::new(Vec::new()));
|
||||
(
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
calls: calls.clone(),
|
||||
},
|
||||
calls,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for RecordingTool {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
fn description(&self) -> &str {
|
||||
"Records all arguments for assertion"
|
||||
}
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"input": {"type": "string"}
|
||||
}
|
||||
})
|
||||
}
|
||||
async fn execute(&self, args: serde_json::Value) -> Result<ToolResult> {
|
||||
self.calls.lock().unwrap().push(args.clone());
|
||||
let output = args
|
||||
.get("input")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("recorded")
|
||||
.to_string();
|
||||
Ok(ToolResult {
|
||||
success: true,
|
||||
output,
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
11
third_party/zeroclaw/tests/support/mod.rs
vendored
Normal file
11
third_party/zeroclaw/tests/support/mod.rs
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
#![allow(dead_code, unused_imports)]
|
||||
|
||||
pub mod assertions;
|
||||
pub mod helpers;
|
||||
pub mod mock_channel;
|
||||
pub mod mock_provider;
|
||||
pub mod mock_tools;
|
||||
pub mod trace;
|
||||
|
||||
pub use mock_provider::{MockProvider, RecordingProvider};
|
||||
pub use mock_tools::{CountingTool, EchoTool, FailingTool, RecordingTool};
|
||||
84
third_party/zeroclaw/tests/support/trace.rs
vendored
Normal file
84
third_party/zeroclaw/tests/support/trace.rs
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
//! JSON trace fixture types for deterministic LLM response replay.
|
||||
|
||||
use serde::Deserialize;
|
||||
use std::path::Path;
|
||||
|
||||
/// A complete LLM conversation trace loaded from a JSON fixture.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct LlmTrace {
|
||||
pub model_name: String,
|
||||
pub turns: Vec<TraceTurn>,
|
||||
#[serde(default)]
|
||||
pub expects: TraceExpects,
|
||||
}
|
||||
|
||||
/// A single conversation turn (user input + LLM response steps).
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct TraceTurn {
|
||||
pub user_input: String,
|
||||
pub steps: Vec<TraceStep>,
|
||||
}
|
||||
|
||||
/// A single LLM response step within a turn.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct TraceStep {
|
||||
pub response: TraceResponse,
|
||||
}
|
||||
|
||||
/// The response content — either plain text or tool calls.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum TraceResponse {
|
||||
#[serde(rename = "text")]
|
||||
Text {
|
||||
content: String,
|
||||
#[serde(default)]
|
||||
input_tokens: u64,
|
||||
#[serde(default)]
|
||||
output_tokens: u64,
|
||||
},
|
||||
#[serde(rename = "tool_calls")]
|
||||
ToolCalls {
|
||||
tool_calls: Vec<TraceToolCall>,
|
||||
#[serde(default)]
|
||||
input_tokens: u64,
|
||||
#[serde(default)]
|
||||
output_tokens: u64,
|
||||
},
|
||||
}
|
||||
|
||||
/// A tool call within a trace response.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct TraceToolCall {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub arguments: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Declarative expectations for trace verification.
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct TraceExpects {
|
||||
#[serde(default)]
|
||||
pub response_contains: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub response_not_contains: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub tools_used: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub tools_not_used: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub max_tool_calls: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub all_tools_succeeded: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub response_matches: Vec<String>,
|
||||
}
|
||||
|
||||
impl LlmTrace {
|
||||
/// Load a trace from a JSON file.
|
||||
pub fn from_file(path: &Path) -> anyhow::Result<Self> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let trace: LlmTrace = serde_json::from_str(&content)?;
|
||||
Ok(trace)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user