#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use meerkat::*;
use schemars::JsonSchema;
#[cfg(feature = "mcp")]
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
mod llm_normalization {
use super::*;
use futures::StreamExt;
fn first_env(vars: &[&str]) -> Option<String> {
for name in vars {
if let Ok(value) = std::env::var(name) {
return Some(value);
}
}
None
}
#[tokio::test]
#[ignore = "lane:e2e-live"]
async fn e2e_anthropic_normalizes_to_llm_event() {
let Some(api_key) = first_env(&["RKAT_ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY"]) else {
eprintln!("Skipping: missing ANTHROPIC_API_KEY (or RKAT_ANTHROPIC_API_KEY)");
return;
};
let client = AnthropicClient::new(api_key).unwrap();
let request = LlmRequest::new(
"claude-opus-4-6",
vec![Message::User(UserMessage::text(
"Say 'hello' and nothing else".to_string(),
))],
);
let mut stream = client.stream(&request);
let mut got_text_delta = false;
let mut got_done = false;
while let Some(event) = stream.next().await {
match event {
Ok(LlmEvent::TextDelta { delta, .. }) => {
let _ = delta;
got_text_delta = true;
}
Ok(LlmEvent::Done {
outcome: LlmDoneOutcome::Success { stop_reason },
}) => {
assert!(matches!(
stop_reason,
StopReason::EndTurn | StopReason::MaxTokens | StopReason::StopSequence
));
got_done = true;
}
Ok(LlmEvent::Done {
outcome: LlmDoneOutcome::Error { error },
}) => panic!("Unexpected error outcome: {error:?}"),
Ok(LlmEvent::ToolCallDelta { .. }) => {
}
Ok(LlmEvent::ToolCallComplete { .. }) => {
}
Ok(LlmEvent::UsageUpdate { usage }) => {
assert!(usage.input_tokens > 0 || usage.output_tokens > 0);
}
Ok(LlmEvent::ReasoningDelta { .. } | LlmEvent::ReasoningComplete { .. }) => {
}
Ok(LlmEvent::ServerToolContent { .. }) => {
}
Err(e) => panic!("Unexpected error: {e:?}"),
}
}
assert!(
got_text_delta,
"Should have received at least one TextDelta"
);
assert!(got_done, "Should have received Done event");
}
#[cfg(feature = "openai")]
#[tokio::test]
#[ignore = "lane:e2e-live"]
async fn e2e_openai_normalizes_to_llm_event() {
let Some(api_key) = first_env(&["RKAT_OPENAI_API_KEY", "OPENAI_API_KEY"]) else {
eprintln!("Skipping: missing OPENAI_API_KEY (or RKAT_OPENAI_API_KEY)");
return;
};
let client = OpenAiClient::new(api_key);
let request = LlmRequest::new(
"gpt-5.4",
vec![Message::User(UserMessage::text(
"Say 'hello' and nothing else".to_string(),
))],
);
let mut stream = client.stream(&request);
let mut got_text_delta = false;
let mut got_done = false;
while let Some(event) = stream.next().await {
match event {
Ok(LlmEvent::TextDelta { .. }) => got_text_delta = true,
Ok(LlmEvent::Done { .. }) => got_done = true,
Ok(_) => {}
Err(e) => panic!("Unexpected error: {e:?}"),
}
}
assert!(
got_text_delta,
"Should have received at least one TextDelta"
);
assert!(got_done, "Should have received Done event");
}
#[cfg(feature = "gemini")]
#[tokio::test]
#[ignore = "lane:e2e-live"]
async fn e2e_gemini_normalizes_to_llm_event() {
let Some(api_key) = first_env(&["RKAT_GEMINI_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY"])
else {
eprintln!("Skipping: missing GOOGLE_API_KEY (or GEMINI_API_KEY/RKAT_GEMINI_API_KEY)");
return;
};
let client = GeminiClient::new(api_key);
let request = LlmRequest::new(
"gemini-2.0-flash",
vec![Message::User(UserMessage::text(
"Say 'hello' and nothing else".to_string(),
))],
);
let mut stream = client.stream(&request);
let mut got_text_delta = false;
let mut got_done = false;
while let Some(event) = stream.next().await {
match event {
Ok(LlmEvent::TextDelta { .. }) => got_text_delta = true,
Ok(LlmEvent::Done { .. }) => got_done = true,
Ok(_) => {}
Err(e) => panic!("Unexpected error: {e:?}"),
}
}
assert!(
got_text_delta,
"Should have received at least one TextDelta"
);
assert!(got_done, "Should have received Done event");
}
#[test]
fn test_provider_error_classification() {
let rate_limit = LlmError::RateLimited {
retry_after_ms: Some(30000),
};
assert!(rate_limit.is_retryable(), "Rate limit should be retryable");
let auth_error = LlmError::AuthenticationFailed {
message: "Invalid API key".to_string(),
};
assert!(
!auth_error.is_retryable(),
"Auth errors should not be retryable"
);
let overload = LlmError::ServerOverloaded;
assert!(
overload.is_retryable(),
"Server overload should be retryable"
);
let invalid = LlmError::InvalidRequest {
message: "Bad request".to_string(),
};
assert!(
!invalid.is_retryable(),
"Invalid request should not be retryable"
);
}
}
mod tool_dispatch {
use super::*;
#[derive(Debug, Clone, JsonSchema)]
#[allow(dead_code)]
struct ToolInput {
input: String,
}
#[test]
fn test_tool_discovery_validates_schema() {
let mut registry = ToolRegistry::new();
let valid_tool = ToolDef {
name: "test_tool".into(),
description: "A test tool".to_string(),
input_schema: meerkat_tools::schema_for::<ToolInput>(),
provenance: None,
};
registry.register(valid_tool);
assert!(
registry.get("test_tool").is_some(),
"Should find registered tool"
);
}
#[cfg(feature = "mcp")]
#[test]
fn test_tool_timeout_enforced() {
let router: Arc<dyn AgentToolDispatcher> = Arc::new(McpRouter::new());
let timeout = Duration::from_secs(30);
let dispatcher = ToolDispatcher::new(router).with_timeout(timeout);
assert!(std::mem::size_of_val(&dispatcher) > 0);
}
#[test]
fn test_tool_error_captured() {
let error = ToolError::execution_failed("Something went wrong with test_tool");
let error_str = format!("{error:?}");
assert!(error_str.contains("Something went wrong"));
assert!(error_str.contains("test_tool"));
let not_found = ToolError::not_found("missing_tool");
assert!(format!("{not_found:?}").contains("missing_tool"));
let timeout = ToolError::timeout("slow_tool", 5000);
assert!(format!("{timeout:?}").contains("slow_tool"));
let validation = ToolError::invalid_arguments("test_tool", "invalid params");
assert!(format!("{validation:?}").contains("invalid params"));
}
}
#[cfg(feature = "jsonl-store")]
mod session_persistence {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_checkpoint_atomic_write() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let store = JsonlStore::new(temp_dir.path().to_path_buf());
store.init().await.expect("Failed to init store");
let mut session = Session::new();
session.push(Message::User(UserMessage::text("Hello".to_string())));
session.push(Message::Assistant(AssistantMessage {
content: "Hi there!".to_string(),
tool_calls: vec![],
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
created_at: meerkat_core::types::message_timestamp_now(),
}));
session.push(Message::User(UserMessage::text("How are you?".to_string())));
let session_id = session.id().clone();
store.save(&session).await.expect("Save should succeed");
let loaded = store
.load(&session_id)
.await
.expect("Load should succeed")
.expect("Session should exist");
assert_eq!(loaded.messages().len(), 3);
assert!(matches!(loaded.messages()[0], Message::User(_)));
assert!(matches!(loaded.messages()[1], Message::Assistant(_)));
assert!(matches!(loaded.messages()[2], Message::User(_)));
}
#[tokio::test]
async fn test_resume_after_crash() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let store = JsonlStore::new(temp_dir.path().to_path_buf());
store.init().await.expect("Failed to init store");
let session_id = {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("Before crash".to_string())));
session.push(Message::Assistant(AssistantMessage {
content: "Response before crash".to_string(),
tool_calls: vec![],
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
created_at: meerkat_core::types::message_timestamp_now(),
}));
let id = session.id().clone();
store.save(&session).await.expect("Save should succeed");
id
};
let store2 = JsonlStore::new(temp_dir.path().to_path_buf());
let resumed = store2
.load(&session_id)
.await
.expect("Resume should succeed")
.expect("Session should exist");
assert_eq!(resumed.messages().len(), 2);
assert_eq!(*resumed.id(), session_id);
}
#[tokio::test]
async fn test_session_roundtrip() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let store = JsonlStore::new(temp_dir.path().to_path_buf());
store.init().await.expect("Failed to init store");
let mut session = Session::new();
session.push(Message::System(SystemMessage::new("You are helpful")));
session.push(Message::User(UserMessage::text("Hello".to_string())));
session.push(Message::Assistant(AssistantMessage {
content: "Hi!".to_string(),
tool_calls: vec![],
stop_reason: StopReason::EndTurn,
usage: Usage::default(),
created_at: meerkat_core::types::message_timestamp_now(),
}));
session.push(Message::User(UserMessage::text("Call a tool".to_string())));
session.push(Message::tool_results(vec![ToolResult::new(
"call_123".to_string(),
"Tool output".to_string(),
false,
)]));
let original_id = session.id().clone();
let original_len = session.messages().len();
store.save(&session).await.expect("Save failed");
let loaded = store
.load(&original_id)
.await
.expect("Load failed")
.expect("Session should exist");
assert_eq!(*loaded.id(), original_id);
assert_eq!(loaded.messages().len(), original_len);
}
}
mod config_loading {
use super::*;
#[test]
fn test_config_precedence() {
let default_config = Config::default();
assert!(
default_config.agent.max_tokens_per_turn > 0,
"Default config should have positive max_tokens"
);
}
#[test]
fn test_config_type_coercion() {
let toml_str = r#"
max_retries = 5
initial_delay = "1s"
max_delay = "1m"
multiplier = 2.0
"#;
let retry_config: RetryConfig = toml::from_str(toml_str).expect("Should parse TOML");
assert_eq!(retry_config.max_retries, 5);
assert_eq!(
retry_config.initial_delay,
std::time::Duration::from_secs(1)
);
assert_eq!(retry_config.max_delay, std::time::Duration::from_secs(60));
let budget_toml = r"
max_tokens = 100000
max_tool_calls = 50
";
let budget: BudgetLimits = toml::from_str(budget_toml).expect("Should parse budget TOML");
assert_eq!(budget.max_tokens, Some(100000));
assert_eq!(budget.max_tool_calls, Some(50));
}
}
mod retry_policy {
use super::*;
#[test]
fn test_retryable_errors_retry() {
let policy = RetryPolicy::default();
assert!(
policy.max_retries > 0,
"Default policy should allow retries"
);
let rate_limit = LlmError::RateLimited {
retry_after_ms: None,
};
assert!(rate_limit.is_retryable(), "Rate limit should trigger retry");
assert!(policy.should_retry(0), "Should retry on first attempt");
assert!(policy.should_retry(1), "Should retry on second attempt");
assert!(
!policy.should_retry(policy.max_retries),
"Should not retry after max attempts"
);
}
#[test]
fn test_non_retryable_fail_fast() {
let auth_error = LlmError::AuthenticationFailed {
message: "Invalid key".to_string(),
};
assert!(!auth_error.is_retryable(), "Auth errors should never retry");
let invalid = LlmError::InvalidRequest {
message: "Bad params".to_string(),
};
assert!(
!invalid.is_retryable(),
"Invalid requests should never retry"
);
}
#[test]
fn test_exponential_backoff() {
let policy = RetryPolicy::default();
let delay_0 = policy.delay_for_attempt(0);
assert_eq!(
delay_0,
Duration::ZERO,
"First attempt should have no delay"
);
let delay_1 = policy.delay_for_attempt(1);
let delay_2 = policy.delay_for_attempt(2);
let delay_3 = policy.delay_for_attempt(3);
assert!(delay_1 > Duration::ZERO, "Second attempt should have delay");
assert!(delay_2 > delay_1 / 2, "Delays should generally increase");
assert!(delay_3 > delay_2 / 2, "Delays should continue increasing");
let delay_100 = policy.delay_for_attempt(100);
let max_with_jitter = policy.max_delay + policy.max_delay / 10;
assert!(delay_100 <= max_with_jitter, "Delay should be capped");
}
}
mod budget_enforcement {
use super::*;
#[test]
fn test_budget_token_limit_enforced() {
let budget = Budget::new(BudgetLimits {
max_tokens: Some(1000),
max_duration: None,
max_tool_calls: None,
});
assert!(budget.check().is_ok(), "Budget check should pass initially");
budget.record_tokens(500);
assert!(
budget.check().is_ok(),
"Budget check should pass within limit"
);
assert_eq!(budget.token_usage(), Some((500, 1000)));
budget.record_tokens(600);
assert!(
budget.check().is_err(),
"Budget check should fail over limit"
);
assert!(budget.is_exhausted(), "Budget should be exhausted");
}
#[test]
fn test_budget_tool_call_limit_enforced() {
let budget = Budget::new(BudgetLimits {
max_tokens: None,
max_duration: None,
max_tool_calls: Some(3),
});
assert!(budget.check().is_ok());
budget.record_tool_call();
budget.record_tool_call();
budget.record_tool_call();
assert!(budget.check().is_err(), "Should fail at limit");
}
#[test]
fn test_budget_unlimited() {
let budget = Budget::new(BudgetLimits {
max_tokens: None,
max_duration: None,
max_tool_calls: None,
});
budget.record_tokens(1_000_000);
budget.record_tokens(1_000_000);
assert!(
budget.check().is_ok(),
"Unlimited budget should always pass"
);
for _ in 0..100 {
budget.record_tool_call();
}
assert!(
budget.check().is_ok(),
"Unlimited budget should always pass"
);
}
}
mod operation_injection {
use super::*;
#[test]
fn test_results_injected_at_turn_boundary() {
let op_result = OperationResult {
id: OperationId::new(),
content: "Tool output".to_string(),
is_error: false,
duration_ms: 100,
tokens_used: 50,
};
let json = serde_json::to_string(&op_result).expect("Should serialize");
assert!(json.contains("Tool output"));
let parsed: OperationResult = serde_json::from_str(&json).expect("Should deserialize");
assert_eq!(parsed.content, "Tool output");
assert!(!parsed.is_error);
assert_eq!(parsed.duration_ms, 100);
assert_eq!(parsed.tokens_used, 50);
}
#[test]
fn test_artifact_ref_resolution() {
let session_id = SessionId::new();
let artifact = ArtifactRef {
id: "artifact_123".to_string(),
session_id,
size_bytes: 1024,
ttl_seconds: Some(3600),
version: 1,
};
assert_eq!(artifact.id, "artifact_123");
assert_eq!(artifact.version, 1);
assert_eq!(artifact.size_bytes, 1024);
assert_eq!(artifact.ttl_seconds, Some(3600));
let json = serde_json::to_string(&artifact).expect("Should serialize");
assert!(json.contains("artifact_123"));
let parsed: ArtifactRef = serde_json::from_str(&json).expect("Should deserialize");
assert_eq!(parsed.id, artifact.id);
assert_eq!(parsed.version, artifact.version);
}
#[test]
fn test_event_ordering_preserved() {
let op_id = OperationId::new();
let started = OpEvent::Started {
id: op_id.clone(),
kind: WorkKind::ToolCall,
};
let progress = OpEvent::Progress {
id: op_id.clone(),
message: "Working...".to_string(),
percent: Some(0.5),
};
let result_id = op_id;
let completed = OpEvent::Completed {
id: result_id.clone(),
result: OperationResult {
id: result_id,
content: "Done".to_string(),
is_error: false,
duration_ms: 100,
tokens_used: 0,
},
};
for event in [&started, &progress, &completed] {
let json = serde_json::to_string(event).expect("Should serialize");
assert!(!json.is_empty());
}
}
}
mod tool_access_policy {
use super::*;
#[test]
fn test_allow_list_structure() {
let policy =
ToolAccessPolicy::AllowList(["safe_tool", "another_safe"].into_iter().collect());
let json = serde_json::to_value(&policy).expect("Should serialize");
assert_eq!(json["type"], "allow_list");
assert!(json["value"].is_array());
let parsed: ToolAccessPolicy = serde_json::from_value(json).expect("Should deserialize");
match parsed {
ToolAccessPolicy::AllowList(tools) => {
assert_eq!(tools.len(), 2);
assert!(tools.contains("safe_tool"));
assert!(tools.contains("another_safe"));
}
_ => panic!("Wrong variant"),
}
}
#[test]
fn test_deny_list_structure() {
let policy = ToolAccessPolicy::DenyList(["dangerous_tool"].into_iter().collect());
let json = serde_json::to_value(&policy).expect("Should serialize");
assert_eq!(json["type"], "deny_list");
let parsed: ToolAccessPolicy = serde_json::from_value(json).expect("Should deserialize");
match parsed {
ToolAccessPolicy::DenyList(tools) => {
assert_eq!(tools.len(), 1);
assert!(tools.contains("dangerous_tool"));
}
_ => panic!("Wrong variant"),
}
}
#[test]
fn test_inherit_policy() {
let policy = ToolAccessPolicy::Inherit;
let json = serde_json::to_value(&policy).expect("Should serialize");
assert_eq!(json["type"], "inherit");
let parsed: ToolAccessPolicy = serde_json::from_value(json).expect("Should deserialize");
assert!(matches!(parsed, ToolAccessPolicy::Inherit));
}
}
mod state_machine {
use super::*;
use meerkat::AgentError;
fn can_transition(from: &LoopState, next: &LoopState) -> bool {
use LoopState::{
CallingLlm, Cancelling, Completed, DrainingEvents, ErrorRecovery, WaitingForOps,
};
matches!(
(from, next),
(
CallingLlm,
WaitingForOps | DrainingEvents | Completed | ErrorRecovery | Cancelling
) | (WaitingForOps, DrainingEvents | Cancelling)
| (
DrainingEvents | ErrorRecovery,
CallingLlm | Completed | Cancelling
)
| (Cancelling, Completed)
)
}
fn transition(state: &mut LoopState, next: LoopState) -> Result<(), AgentError> {
if can_transition(state, &next) {
*state = next;
Ok(())
} else {
Err(AgentError::InvalidStateTransition {
from: format!("{state:?}"),
to: format!("{next:?}"),
})
}
}
#[test]
fn test_valid_state_transitions() {
let mut state = LoopState::CallingLlm;
assert!(transition(&mut state, LoopState::DrainingEvents).is_ok());
assert_eq!(state, LoopState::DrainingEvents);
assert!(transition(&mut state, LoopState::CallingLlm).is_ok());
assert_eq!(state, LoopState::CallingLlm);
assert!(transition(&mut state, LoopState::Completed).is_ok());
assert_eq!(state, LoopState::Completed);
assert!(state.is_terminal(), "Completed should be terminal");
}
#[test]
fn test_invalid_transitions_from_terminal() {
let mut state = LoopState::Completed;
assert!(
transition(&mut state, LoopState::CallingLlm).is_err(),
"Should not transition from terminal state"
);
}
#[test]
fn test_cancellation_path() {
let mut state = LoopState::CallingLlm;
assert!(transition(&mut state, LoopState::Cancelling).is_ok());
assert_eq!(state, LoopState::Cancelling);
assert!(transition(&mut state, LoopState::Completed).is_ok());
assert!(state.is_terminal(), "Completed should be terminal");
}
#[test]
fn test_error_recovery_path() {
let mut state = LoopState::CallingLlm;
assert!(transition(&mut state, LoopState::ErrorRecovery).is_ok());
assert!(transition(&mut state, LoopState::CallingLlm).is_ok());
transition(&mut state, LoopState::ErrorRecovery).ok();
assert!(transition(&mut state, LoopState::Completed).is_ok());
assert!(state.is_terminal());
}
#[test]
fn test_waiting_for_ops_path() {
let mut state = LoopState::CallingLlm;
assert!(transition(&mut state, LoopState::WaitingForOps).is_ok());
assert!(transition(&mut state, LoopState::DrainingEvents).is_ok());
assert!(transition(&mut state, LoopState::Completed).is_ok());
}
}
#[cfg(feature = "mcp")]
mod mcp_protocol {
use super::*;
#[test]
fn test_mcp_config_structure() {
let config = McpServerConfig::stdio(
"test-server",
"node",
vec!["test-server.js".to_string()],
HashMap::new(),
);
assert_eq!(config.name, "test-server");
match &config.transport {
meerkat_core::mcp_config::McpTransportConfig::Stdio(stdio) => {
assert_eq!(stdio.command, "node");
assert_eq!(stdio.args.len(), 1);
}
_ => panic!("Expected stdio transport"),
}
let json = serde_json::to_value(&config).expect("Should serialize");
assert_eq!(json["name"], "test-server");
assert_eq!(json["command"], "node");
}
#[tokio::test]
async fn test_mcp_router_creation() {
let router = McpRouter::new();
let tools = router.list_tools();
assert!(tools.is_empty(), "No tools without servers");
}
#[tokio::test]
#[ignore = "lane:e2e-system"]
async fn integration_real_mcp_tool_call_roundtrip() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
let workspace_root = std::path::Path::new(&manifest_dir)
.parent()
.unwrap_or(std::path::Path::new("."));
let server_path = std::env::var_os("CARGO_TARGET_DIR")
.map(std::path::PathBuf::from)
.and_then(|target_dir| {
[
target_dir.join("debug/mcp-test-server"),
target_dir.join("release/mcp-test-server"),
]
.into_iter()
.find(|path| path.exists())
})
.unwrap_or_else(|| workspace_root.join("target/debug/mcp-test-server"));
if !server_path.exists() {
eprintln!("Skipping: MCP test server not built (run cargo build -p mcp-test-server)");
return;
}
let config = McpServerConfig::stdio(
"test",
server_path.to_string_lossy().to_string(),
vec![],
HashMap::new(),
);
let connection = McpConnection::connect(&config)
.await
.expect("Should connect to test server");
let tools = connection
.list_tools("test-server")
.await
.expect("Should list tools");
assert!(!tools.is_empty(), "Test server should have tools");
let echo_tool = tools
.iter()
.find(|t| t.name == "echo")
.expect("Test server should have echo tool");
assert_eq!(echo_tool.name, "echo");
let blocks = connection
.call_tool("echo", &serde_json::json!({"message": "test"}))
.await
.expect("Tool call should succeed");
let result_text = meerkat_core::types::text_content(&blocks);
assert!(result_text.contains("test"), "Echo should return input");
connection.close().await.expect("Should close cleanly");
}
}
mod combined {
use super::*;
#[derive(Debug, Clone, JsonSchema)]
#[allow(dead_code)]
struct ReadFileArgs {
path: String,
}
#[derive(Debug, Clone, JsonSchema)]
#[allow(dead_code)]
struct WriteFileArgs {
path: String,
content: String,
}
#[test]
fn test_session_with_tool_results() {
let mut session = Session::new();
session.push(Message::User(UserMessage::text("Call a tool".to_string())));
let first_usage = Usage {
input_tokens: 100,
output_tokens: 50,
cache_creation_tokens: None,
cache_read_tokens: None,
};
session.push(Message::Assistant(AssistantMessage {
content: "".to_string(),
tool_calls: vec![ToolCall::new(
"tc_1".to_string(),
"test_tool".to_string(),
serde_json::json!({"input": "test"}),
)],
stop_reason: StopReason::ToolUse,
usage: first_usage.clone(),
created_at: meerkat_core::types::message_timestamp_now(),
}));
session.record_usage(first_usage);
session.push(Message::tool_results(vec![ToolResult::new(
"tc_1".to_string(),
"Tool result".to_string(),
false,
)]));
let second_usage = Usage {
input_tokens: 150,
output_tokens: 75,
cache_creation_tokens: None,
cache_read_tokens: None,
};
session.push(Message::Assistant(AssistantMessage {
content: "Based on the tool result...".to_string(),
tool_calls: vec![],
stop_reason: StopReason::EndTurn,
usage: second_usage.clone(),
created_at: meerkat_core::types::message_timestamp_now(),
}));
session.record_usage(second_usage);
assert_eq!(session.messages().len(), 4);
assert_eq!(session.tool_call_count(), 1);
assert_eq!(session.total_tokens(), 375); }
#[test]
fn test_budget_with_usage_recording() {
let budget = Budget::new(BudgetLimits::default().with_max_tokens(1000));
let usage = Usage {
input_tokens: 200,
output_tokens: 100,
cache_creation_tokens: None,
cache_read_tokens: None,
};
budget.record_usage(&usage);
assert_eq!(budget.token_usage(), Some((300, 1000)));
assert_eq!(budget.remaining_tokens(), Some(700));
}
#[test]
fn test_operation_spec_completeness() {
let spec = OperationSpec {
id: OperationId::new(),
kind: WorkKind::ToolCall,
result_shape: ResultShape::Single,
policy: OperationPolicy {
timeout_ms: Some(30000),
cancel_on_parent_cancel: true,
checkpoint_results: true,
},
budget_reservation: BudgetLimits::default().with_max_tokens(1000),
depth: 0,
depends_on: vec![],
context: Some(ContextStrategy::FullHistory),
tool_access: Some(ToolAccessPolicy::Inherit),
};
let json = serde_json::to_string(&spec).expect("Should serialize");
assert!(json.contains("tool_call"));
let parsed: OperationSpec = serde_json::from_str(&json).expect("Should deserialize");
assert_eq!(parsed.kind, spec.kind);
assert_eq!(parsed.result_shape, spec.result_shape);
}
#[test]
fn test_llm_request_with_tools() {
let tools = vec![
Arc::new(ToolDef {
name: "read_file".into(),
description: "Read a file".to_string(),
input_schema: meerkat_tools::schema_for::<ReadFileArgs>(),
provenance: None,
}),
Arc::new(ToolDef {
name: "write_file".into(),
description: "Write a file".to_string(),
input_schema: meerkat_tools::schema_for::<WriteFileArgs>(),
provenance: None,
}),
];
let request = LlmRequest::new(
"claude-opus-4-6",
vec![Message::User(UserMessage::text(
"Read the file".to_string(),
))],
)
.with_tools(tools)
.with_max_tokens(4096)
.with_temperature(0.7);
assert_eq!(request.model, "claude-opus-4-6");
assert_eq!(request.tools.len(), 2);
assert_eq!(request.max_tokens, 4096);
assert_eq!(request.temperature, Some(0.7));
}
}