use clap::Args;
use leviath_providers::InferenceRequest;
use leviath_runtime::{ContextWindow, ProviderRegistry, context_setup};
use serde::Deserialize;
use std::fs;
use std::path::Path;
use std::sync::Arc;
use crate::config::Config;
use leviath_core::manifest::parse_manifest;
use leviath_core::truncate_at_boundary;
#[derive(Args)]
pub struct TestArgs {
#[arg(value_name = "PATH")]
pub path: Option<String>,
#[arg(short, long)]
pub filter: Option<String>,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Debug, Deserialize)]
struct TestCase {
name: String,
input: String,
#[serde(default)]
expect_contains: Option<String>,
#[serde(default)]
expect_tool_call: Option<String>,
#[serde(default)]
max_tokens: Option<usize>,
}
#[derive(Debug, Deserialize)]
struct TestFile {
test: Vec<TestCase>,
}
pub async fn execute(args: TestArgs) -> anyhow::Result<()> {
execute_with_registry(args, Box::new(build_registry_from_config)).await
}
fn build_registry_from_config(
config: &Config,
) -> Result<ProviderRegistry, leviath_providers::ProviderError> {
build_registry_from_config_with(config, &leviath_providers::provider::build_http_client)
}
fn build_registry_from_config_with(
config: &Config,
build_client: leviath_providers::provider::HttpClientFactory<'_>,
) -> Result<ProviderRegistry, leviath_providers::ProviderError> {
let mut reg = ProviderRegistry::new();
let client = build_client(None)
.map_err(|e| leviath_providers::ProviderError::ClientBuild(e.to_string()))?;
if let Some(ref key) = config.providers.anthropic_api_key {
reg.register(
"anthropic".to_string(),
Arc::new(leviath_providers::AnthropicProvider::new(
client.clone(),
key.clone(),
)),
);
}
if let Some(ref key) = config.providers.openai_api_key {
reg.register(
"openai".to_string(),
Arc::new(leviath_providers::OpenAIProvider::new(
client.clone(),
key.clone(),
)),
);
}
if let Some(ref key) = config.providers.google_api_key {
reg.register(
"google".to_string(),
Arc::new(leviath_providers::GeminiProvider::new(
client.clone(),
key.clone(),
)),
);
}
if let Some(ref key) = config.openrouter_api_key {
reg.register(
"openrouter".to_string(),
Arc::new(leviath_providers::OpenRouterProvider::new(
client.clone(),
key.clone(),
)),
);
}
let ollama_url = config
.ollama_base_url
.as_deref()
.unwrap_or("http://localhost:11434");
reg.register(
"ollama".to_string(),
Arc::new(leviath_providers::OllamaProvider::with_base_url(
client.clone(),
ollama_url.to_string(),
)),
);
Ok(reg)
}
type RegistryBuilder =
Box<dyn FnOnce(&Config) -> Result<ProviderRegistry, leviath_providers::ProviderError>>;
async fn execute_with_registry(
args: TestArgs,
build_registry: RegistryBuilder,
) -> anyhow::Result<()> {
let path = args.path.unwrap_or_else(|| ".".to_string());
tracing::info!(path = %path, "Running agent tests");
let project_path = Path::new(&path);
let manifest_path = project_path.join("agent.leviath");
if !manifest_path.exists() {
anyhow::bail!(
"No agent.leviath found in '{}'. Not an agent project.",
project_path.display()
);
}
let tests_dir = project_path.join("tests");
if !tests_dir.exists() {
println!("No tests directory found. Create tests/ with .toml or .rhai files.");
println!("\nExample test file (tests/basic.toml):");
println!(" [[test]]");
println!(" name = \"basic_response\"");
println!(" input = \"Hello\"");
println!(" expect_contains = \"hello\"");
println!("\nAlso available: expect_tool_call, max_tokens.");
println!("See https://leviath.dev/docs/cli#lev-test-path for what each does.");
return Ok(());
}
if args.dry_run {
println!("Dry run mode: validating test structure only (no API calls)\n");
}
let manifest_content = fs::read_to_string(&manifest_path)?;
let blueprint = parse_manifest(&manifest_content)?;
let region_scripts =
crate::daemon::spawn::resolve_region_scripts(&blueprint, &manifest_path.to_string_lossy())
.map_err(|e| anyhow::anyhow!(e))?;
let registry = if !args.dry_run {
let config = Config::load()?;
Some(build_registry(&config)?)
} else {
None
};
let mut total = 0;
let mut passed = 0;
let mut failed = 0;
let mut failures: Vec<String> = Vec::new();
for entry in fs::read_dir(&tests_dir)?.flatten() {
let test_path = entry.path();
if test_path.extension().and_then(|e| e.to_str()) == Some("toml") {
let file_name = test_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
println!("Running test file: {}", file_name);
let content = fs::read_to_string(&test_path)?;
let test_file: TestFile = toml::from_str(&content)
.map_err(|e| anyhow::anyhow!("Failed to parse test file '{}': {}", file_name, e))?;
for test_case in &test_file.test {
if let Some(ref filter) = args.filter
&& !test_case.name.contains(filter.as_str())
{
continue;
}
total += 1;
if args.dry_run {
let test_valid = validate_test_case(test_case);
if test_valid {
passed += 1;
println!(" PASS (dry-run): {}", test_case.name);
} else {
failed += 1;
let msg = format!("{}: test case validation failed", test_case.name);
println!(" FAIL (dry-run): {}", msg);
failures.push(msg);
}
} else {
let registry = registry
.as_ref()
.expect("registry should exist in non-dry-run");
match run_test_case(&blueprint, registry, test_case, ®ion_scripts).await {
Ok(true) => {
passed += 1;
println!(" PASS: {}", test_case.name);
}
Ok(false) => {
failed += 1;
let msg = format!("{}: assertions failed", test_case.name);
println!(" FAIL: {}", msg);
failures.push(msg);
}
Err(e) => {
failed += 1;
let msg = format!("{}: {}", test_case.name, e);
println!(" FAIL: {}", msg);
failures.push(msg);
}
}
}
}
} else if test_path.extension().and_then(|e| e.to_str()) == Some("rhai") {
let file_name = test_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
if let Some(ref filter) = args.filter
&& !file_name.contains(filter.as_str())
{
continue;
}
total += 1;
println!("Running script: {}", file_name);
let script = fs::read_to_string(&test_path)?;
let engine = leviath_scripting::ScriptEngine::new();
let mut scope = rhai::Scope::new();
match engine.execute(&script, &mut scope) {
Ok(result) => {
if let Ok(success) = result.as_bool() {
if success {
passed += 1;
println!(" PASS: {}", file_name);
} else {
failed += 1;
let msg = format!("{}: script returned false", file_name);
println!(" FAIL: {}", msg);
failures.push(msg);
}
} else {
passed += 1;
println!(" PASS: {} (returned: {})", file_name, result);
}
}
Err(e) => {
failed += 1;
let msg = format!("{}: {}", file_name, e);
println!(" FAIL: {}", msg);
failures.push(msg);
}
}
}
}
println!("\n--- Results ---");
println!("{} passed, {} failed, {} total", passed, failed, total);
if !failures.is_empty() {
println!("\nFailures:");
for f in &failures {
println!(" - {}", f);
}
anyhow::bail!("{} test(s) failed", failed);
}
if total == 0 {
println!("No test files found in tests/ directory.");
}
Ok(())
}
fn resolved_max_tokens(case_cap: Option<usize>, ceiling: usize) -> usize {
match case_cap {
Some(cap) => cap.min(ceiling),
None => ceiling,
}
}
fn stage_tools(stage: &leviath_core::Stage) -> Vec<leviath_providers::Tool> {
let builtins =
leviath_tools::BuiltinTools::new(leviath_tools::ToolContext::new(std::env::temp_dir()));
let mut defs = builtins.tool_defs();
defs.extend(leviath_tools::BuiltinTools::subagent_tool_defs());
stage
.available_tools
.iter()
.filter_map(|name| defs.iter().find(|d| d.name == *name).cloned())
.collect()
}
async fn run_test_case(
blueprint: &leviath_core::Blueprint,
registry: &ProviderRegistry,
test: &TestCase,
region_scripts: &std::collections::HashMap<
String,
std::sync::Arc<leviath_scripting::region_hook::RegionScript>,
>,
) -> anyhow::Result<bool> {
let stage = blueprint
.stages
.first()
.ok_or(anyhow::anyhow!("Blueprint has no stages"))?;
let provider_name = stage.model.provider();
let model_name = stage.model.model();
let provider = registry.get(provider_name).ok_or_else(|| {
anyhow::anyhow!(
"Provider '{}' is not configured. Set API key in ~/.leviath/config.toml",
provider_name
)
})?;
let mut window = ContextWindow::new(blueprint.context_layout.total_budget_tokens);
window.region_scripts = region_scripts.clone();
context_setup::init_window(&mut window, blueprint, &test.input);
let assembled = window.assemble_with_meta(&leviath_runtime::custom_region::AssembleMeta {
stage_name: stage.name.clone(),
stage_iterations: 0,
model: model_name.to_string(),
});
let caps = provider.capabilities(model_name);
let remaining = window.max_tokens.saturating_sub(window.current_tokens);
let max_tokens = resolved_max_tokens(test.max_tokens, remaining.min(caps.max_output_tokens));
let temperature = if caps.supports_temperature { 0.7 } else { 0.0 };
let request = InferenceRequest {
system: assembled.system_blocks,
messages: assembled.messages,
model: model_name.to_string(),
max_tokens,
temperature,
tools: stage_tools(stage),
extra: serde_json::Value::Null,
request_timeout_secs: None,
};
let response = provider
.infer(&request)
.await
.map_err(|e| anyhow::anyhow!("Inference failed: {}", e))?;
let mut all_passed = true;
if let Some(ref expected) = test.expect_contains {
let content_lower = response.content.to_lowercase();
let expected_lower = expected.to_lowercase();
if !content_lower.contains(&expected_lower) {
println!(
" expect_contains failed: response does not contain '{}'",
expected
);
println!(" response: {}", truncate_str(&response.content, 200));
all_passed = false;
}
}
if let Some(ref expected_tool) = test.expect_tool_call {
let has_tool = response
.tool_calls
.iter()
.any(|tc| tc.name == *expected_tool);
if !has_tool {
println!(
" expect_tool_call failed: no tool call to '{}'",
expected_tool
);
let tool_names: Vec<&str> = response
.tool_calls
.iter()
.map(|tc| tc.name.as_str())
.collect();
println!(" actual tool calls: {:?}", tool_names);
all_passed = false;
}
}
Ok(all_passed)
}
fn validate_test_case(test: &TestCase) -> bool {
if test.name.is_empty() {
return false;
}
if test.input.is_empty() {
return false;
}
if test.expect_contains.is_none() && test.expect_tool_call.is_none() {
return false;
}
true
}
fn truncate_str(s: &str, max: usize) -> String {
if s.len() <= max {
s.to_string()
} else {
format!("{}...", truncate_at_boundary(s, max))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::{with_tracing, write_test_agent};
#[test]
fn validate_test_case_valid_with_expect_contains() {
let tc = TestCase {
name: "basic".to_string(),
input: "hello".to_string(),
expect_contains: Some("world".to_string()),
expect_tool_call: None,
max_tokens: None,
};
assert!(validate_test_case(&tc));
}
#[test]
fn validate_test_case_valid_with_expect_tool_call() {
let tc = TestCase {
name: "tool_test".to_string(),
input: "do something".to_string(),
expect_contains: None,
expect_tool_call: Some("bash".to_string()),
max_tokens: None,
};
assert!(validate_test_case(&tc));
}
#[test]
fn validate_test_case_valid_with_both_assertions() {
let tc = TestCase {
name: "both".to_string(),
input: "test".to_string(),
expect_contains: Some("output".to_string()),
expect_tool_call: Some("read_file".to_string()),
max_tokens: Some(100),
};
assert!(validate_test_case(&tc));
}
#[test]
fn validate_test_case_empty_name_fails() {
let tc = TestCase {
name: String::new(),
input: "hello".to_string(),
expect_contains: Some("world".to_string()),
expect_tool_call: None,
max_tokens: None,
};
assert!(!validate_test_case(&tc));
}
#[test]
fn validate_test_case_empty_input_fails() {
let tc = TestCase {
name: "test".to_string(),
input: String::new(),
expect_contains: Some("world".to_string()),
expect_tool_call: None,
max_tokens: None,
};
assert!(!validate_test_case(&tc));
}
#[test]
fn validate_test_case_no_assertions_fails() {
let tc = TestCase {
name: "test".to_string(),
input: "hello".to_string(),
expect_contains: None,
expect_tool_call: None,
max_tokens: None,
};
assert!(!validate_test_case(&tc));
}
#[test]
fn truncate_str_short() {
assert_eq!(truncate_str("hello", 10), "hello");
}
#[test]
fn truncate_str_exact() {
assert_eq!(truncate_str("hello", 5), "hello");
}
#[test]
fn truncate_str_long() {
assert_eq!(truncate_str("hello world", 5), "hello...");
}
#[test]
fn truncate_str_empty() {
assert_eq!(truncate_str("", 5), "");
}
#[test]
fn parse_test_file_toml() {
let toml_content = r#"
[[test]]
name = "greeting"
input = "Say hello"
expect_contains = "hello"
[[test]]
name = "tool_use"
input = "Read file.txt"
expect_tool_call = "read_file"
max_tokens = 500
"#;
let test_file: TestFile = toml::from_str(toml_content).unwrap();
assert_eq!(test_file.test.len(), 2);
assert_eq!(test_file.test[0].name, "greeting");
assert_eq!(test_file.test[0].input, "Say hello");
assert_eq!(test_file.test[0].expect_contains.as_deref(), Some("hello"));
assert!(test_file.test[0].expect_tool_call.is_none());
assert!(test_file.test[0].max_tokens.is_none());
assert_eq!(test_file.test[1].name, "tool_use");
assert_eq!(
test_file.test[1].expect_tool_call.as_deref(),
Some("read_file")
);
assert_eq!(test_file.test[1].max_tokens, Some(500));
}
fn test_model() -> leviath_core::blueprint::ModelConfig {
leviath_core::blueprint::ModelConfig::new("anthropic".to_string(), "m".to_string())
}
#[test]
fn a_case_max_tokens_narrows_the_ceiling_and_never_widens_it() {
let ceiling = 4_000;
assert_eq!(
resolved_max_tokens(Some(500), ceiling),
500,
"a smaller case cap wins"
);
assert_eq!(
resolved_max_tokens(Some(99_000), ceiling),
ceiling,
"a case may not ask for more than the model allows"
);
assert_eq!(
resolved_max_tokens(None, ceiling),
ceiling,
"no cap means the full ceiling"
);
}
#[test]
fn a_stage_advertises_its_tools_so_a_tool_call_is_possible() {
let mut stage = leviath_core::Stage::new("s".to_string(), test_model());
stage.available_tools = vec!["read_file".to_string(), "write_file".to_string()];
let tools = stage_tools(&stage);
let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
assert!(names.contains(&"read_file"), "got {names:?}");
assert!(names.contains(&"write_file"), "got {names:?}");
}
#[test]
fn a_stage_with_no_tools_advertises_none() {
let stage = leviath_core::Stage::new("s".to_string(), test_model());
assert!(stage_tools(&stage).is_empty());
}
#[test]
fn an_unknown_tool_name_is_not_advertised() {
let mut stage = leviath_core::Stage::new("s".to_string(), test_model());
stage.available_tools = vec!["definitely_not_a_tool".to_string()];
assert!(stage_tools(&stage).is_empty());
}
#[test]
fn parse_test_file_minimal() {
let toml_content = r#"
[[test]]
name = "min"
input = "test"
expect_contains = "ok"
"#;
let test_file: TestFile = toml::from_str(toml_content).unwrap();
assert_eq!(test_file.test.len(), 1);
}
#[test]
fn parse_test_file_invalid_toml_errors() {
let result: Result<TestFile, _> = toml::from_str("not valid toml {{{{");
assert!(result.is_err());
}
#[tokio::test]
async fn dry_run_with_temp_project() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
let test_toml = r#"
[[test]]
name = "valid_test"
input = "hello"
expect_contains = "world"
"#;
std::fs::write(tests_dir.join("basic.toml"), test_toml).unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: true,
};
let result = execute(args).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn dry_run_no_tests_dir() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: true,
};
let result = execute(args).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn execute_no_manifest_errors() {
let dir = tempfile::tempdir().unwrap();
let args = TestArgs {
path: Some(dir.path().to_str().unwrap().to_string()),
filter: None,
dry_run: true,
};
let result = execute(args).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("agent.leviath"));
}
#[test]
fn test_case_all_fields_from_toml() {
let toml_content = r#"
[[test]]
name = "full_test"
input = "full input"
expect_contains = "expected"
expect_tool_call = "bash"
max_tokens = 1000
"#;
let test_file: TestFile = toml::from_str(toml_content).unwrap();
let tc = &test_file.test[0];
assert_eq!(tc.name, "full_test");
assert_eq!(tc.input, "full input");
assert_eq!(tc.expect_contains.as_deref(), Some("expected"));
assert_eq!(tc.expect_tool_call.as_deref(), Some("bash"));
assert_eq!(tc.max_tokens, Some(1000));
}
#[test]
fn test_case_minimal_from_toml() {
let toml_content = r#"
[[test]]
name = "min"
input = "hello"
expect_contains = "world"
"#;
let test_file: TestFile = toml::from_str(toml_content).unwrap();
let tc = &test_file.test[0];
assert!(tc.expect_tool_call.is_none());
assert!(tc.max_tokens.is_none());
}
#[test]
fn test_file_multiple_cases() {
let toml_content = r#"
[[test]]
name = "case1"
input = "a"
expect_contains = "b"
[[test]]
name = "case2"
input = "c"
expect_tool_call = "read_file"
[[test]]
name = "case3"
input = "d"
expect_contains = "e"
expect_tool_call = "bash"
max_tokens = 500
"#;
let test_file: TestFile = toml::from_str(toml_content).unwrap();
assert_eq!(test_file.test.len(), 3);
}
#[test]
fn validate_test_case_whitespace_name_passes() {
let tc = TestCase {
name: " ".to_string(),
input: "hello".to_string(),
expect_contains: Some("world".to_string()),
expect_tool_call: None,
max_tokens: None,
};
assert!(validate_test_case(&tc));
}
#[test]
fn truncate_str_one_char_max() {
assert_eq!(truncate_str("hello", 1), "h...");
}
#[test]
fn truncate_str_unicode() {
assert_eq!(truncate_str("abcde", 3), "abc...");
assert_eq!(truncate_str("abc🎉def", 4), "abc...");
assert_eq!(truncate_str("abc🎉def", 6), "abc...");
assert_eq!(truncate_str("abc🎉def", 7), "abc🎉...");
assert_eq!(truncate_str("🎉🎉", 2), "...");
}
#[tokio::test]
async fn dry_run_with_filter_matches() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
let test_toml = r#"
[[test]]
name = "alpha_test"
input = "hello"
expect_contains = "world"
[[test]]
name = "beta_test"
input = "hello"
expect_contains = "world"
"#;
std::fs::write(tests_dir.join("basic.toml"), test_toml).unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: Some("alpha".to_string()),
dry_run: true,
};
let result = execute(args).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn dry_run_failing_test_case() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
let test_toml = r#"
[[test]]
name = "bad_test"
input = "hello"
"#;
std::fs::write(tests_dir.join("fail.toml"), test_toml).unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: true,
};
let result = execute(args).await;
assert!(result.is_err()); }
#[test]
fn validate_test_case_with_max_tokens_only_and_no_assertion_fails() {
let tc = TestCase {
name: "has-max-tokens".to_string(),
input: "test".to_string(),
expect_contains: None,
expect_tool_call: None,
max_tokens: Some(500),
};
assert!(!validate_test_case(&tc));
}
#[test]
fn validate_test_case_with_only_tool_call_assertion() {
let tc = TestCase {
name: "tool-only".to_string(),
input: "do it".to_string(),
expect_contains: None,
expect_tool_call: Some("write_file".to_string()),
max_tokens: None,
};
assert!(validate_test_case(&tc));
}
#[test]
fn truncate_str_zero_max() {
assert_eq!(truncate_str("hello", 0), "...");
}
#[test]
fn truncate_str_large_max() {
let s = "short";
assert_eq!(truncate_str(s, 1000), "short");
}
#[test]
fn parse_test_file_empty_tests_array() {
let toml_content = r#"
test = []
"#;
let test_file: TestFile = toml::from_str(toml_content).unwrap();
assert!(test_file.test.is_empty());
}
#[test]
fn parse_test_file_missing_test_key_errors() {
let result: Result<TestFile, _> = toml::from_str("something_else = 42");
assert!(result.is_err());
}
#[tokio::test]
async fn dry_run_with_filter_no_match() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
let test_toml = r#"
[[test]]
name = "alpha_test"
input = "hello"
expect_contains = "world"
"#;
std::fs::write(tests_dir.join("basic.toml"), test_toml).unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: Some("nonexistent_filter".to_string()),
dry_run: true,
};
let result = execute(args).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn dry_run_with_rhai_script_passing() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
std::fs::write(tests_dir.join("pass_test.rhai"), "true").unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: true,
};
let result = execute(args).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn dry_run_with_rhai_script_returning_false() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
std::fs::write(tests_dir.join("fail_test.rhai"), "false").unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: true,
};
let result = execute(args).await;
assert!(result.is_err()); }
#[tokio::test]
async fn dry_run_with_rhai_script_error() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
std::fs::write(
tests_dir.join("error_test.rhai"),
"throw \"intentional error\"",
)
.unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: true,
};
let result = execute(args).await;
assert!(result.is_err()); }
#[tokio::test]
async fn dry_run_with_rhai_non_bool_result_passes() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
std::fs::write(tests_dir.join("nonbool_test.rhai"), "42").unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: true,
};
let result = execute(args).await;
assert!(result.is_ok()); }
#[tokio::test]
async fn dry_run_with_rhai_filter_matches() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
std::fs::write(tests_dir.join("fail_test.rhai"), "false").unwrap();
std::fs::write(tests_dir.join("good_test.rhai"), "true").unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: Some("good".to_string()),
dry_run: true,
};
let result = execute(args).await;
assert!(result.is_ok()); }
#[tokio::test]
async fn dry_run_with_multiple_test_files() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
let test1 = r#"
[[test]]
name = "test_a"
input = "hello"
expect_contains = "world"
"#;
let test2 = r#"
[[test]]
name = "test_b"
input = "foo"
expect_tool_call = "bar"
"#;
std::fs::write(tests_dir.join("file1.toml"), test1).unwrap();
std::fs::write(tests_dir.join("file2.toml"), test2).unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: true,
};
let result = execute(args).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn dry_run_with_invalid_toml_file() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
std::fs::write(tests_dir.join("bad.toml"), "not valid {{{ toml").unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: true,
};
let result = execute(args).await;
assert!(result.is_err());
}
use leviath_providers::{
FinishReason, InferenceRequest, InferenceResponse, Provider, TokenUsage, ToolCall,
};
struct MockProvider {
content: String,
tool_calls: Vec<ToolCall>,
}
#[async_trait::async_trait]
impl Provider for MockProvider {
async fn infer(
&self,
_request: &InferenceRequest,
) -> leviath_providers::Result<InferenceResponse> {
Ok(InferenceResponse {
content: self.content.clone(),
tool_calls: self.tool_calls.clone(),
tokens_used: TokenUsage {
prompt_tokens: 1,
completion_tokens: 1,
total_tokens: 2,
cached_tokens: 0,
cache_write_tokens: 0,
},
finish_reason: FinishReason::Complete,
})
}
async fn count_tokens(&self, text: &str, _model: &str) -> usize {
text.len()
}
fn max_context_tokens(&self, _model: &str) -> usize {
8192
}
fn name(&self) -> &str {
"mock"
}
fn capabilities(&self, _model: &str) -> leviath_providers::ModelCapabilities {
leviath_providers::ModelCapabilities::default()
}
}
struct NoTemperatureProvider;
#[async_trait::async_trait]
impl Provider for NoTemperatureProvider {
async fn infer(
&self,
_request: &InferenceRequest,
) -> leviath_providers::Result<InferenceResponse> {
Ok(InferenceResponse {
content: "cold hello".to_string(),
tool_calls: vec![],
tokens_used: TokenUsage {
prompt_tokens: 1,
completion_tokens: 1,
total_tokens: 2,
cached_tokens: 0,
cache_write_tokens: 0,
},
finish_reason: FinishReason::Complete,
})
}
async fn count_tokens(&self, text: &str, _model: &str) -> usize {
text.len()
}
fn max_context_tokens(&self, _model: &str) -> usize {
8192
}
fn name(&self) -> &str {
"no-temperature"
}
fn capabilities(&self, _model: &str) -> leviath_providers::ModelCapabilities {
leviath_providers::ModelCapabilities {
supports_temperature: false,
..Default::default()
}
}
}
struct ErrorProvider;
#[async_trait::async_trait]
impl Provider for ErrorProvider {
async fn infer(
&self,
_request: &InferenceRequest,
) -> leviath_providers::Result<InferenceResponse> {
Err(leviath_providers::ProviderError::ApiError(
"simulated inference error".to_string(),
))
}
async fn count_tokens(&self, text: &str, _model: &str) -> usize {
text.len()
}
fn max_context_tokens(&self, _model: &str) -> usize {
8192
}
fn name(&self) -> &str {
"error-provider"
}
fn capabilities(&self, _model: &str) -> leviath_providers::ModelCapabilities {
leviath_providers::ModelCapabilities::default()
}
}
fn basic_blueprint() -> leviath_core::Blueprint {
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
parse_manifest(manifest).unwrap()
}
fn blueprint_with_tool_results_region() -> leviath_core::Blueprint {
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
[context.regions.tool_results]
kind = "temporary"
max_tokens = 5000
"#;
parse_manifest(manifest).unwrap()
}
struct RecordingProvider {
seen: std::sync::Arc<std::sync::Mutex<Option<InferenceRequest>>>,
}
#[async_trait::async_trait]
impl Provider for RecordingProvider {
async fn infer(
&self,
request: &InferenceRequest,
) -> leviath_providers::Result<InferenceResponse> {
*self.seen.lock().unwrap() = Some(request.clone());
Ok(InferenceResponse {
content: "recorded".to_string(),
tool_calls: vec![],
tokens_used: TokenUsage {
prompt_tokens: 1,
completion_tokens: 1,
total_tokens: 2,
cached_tokens: 0,
cache_write_tokens: 0,
},
finish_reason: FinishReason::Complete,
})
}
async fn count_tokens(&self, text: &str, _model: &str) -> usize {
text.len()
}
fn max_context_tokens(&self, _model: &str) -> usize {
8192
}
fn name(&self) -> &str {
"recording"
}
fn capabilities(&self, _model: &str) -> leviath_providers::ModelCapabilities {
leviath_providers::ModelCapabilities::default()
}
}
#[tokio::test]
async fn run_test_case_renders_custom_region_through_its_script() {
let manifest = r#"
[agent]
name = "custom-test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
[context.regions.task]
kind = "pinned"
max_tokens = 4000
[context.regions.brain]
kind = "custom"
script = "hooks/brain.rhai"
max_tokens = 4000
"#;
let blueprint = parse_manifest(manifest).unwrap();
let scripts = std::collections::HashMap::from([(
"hooks/brain.rhai".to_string(),
std::sync::Arc::new(
leviath_scripting::region_hook::compile(
"hooks/brain.rhai",
"fn render(ctx) { `<brain stage=${ctx.stage_name} model=${ctx.model}>` }",
)
.unwrap(),
),
)]);
let seen = std::sync::Arc::new(std::sync::Mutex::new(None));
let mut registry = ProviderRegistry::new();
registry.register(
"anthropic".to_string(),
Arc::new(RecordingProvider { seen: seen.clone() }),
);
let tc = TestCase {
name: "custom_render".to_string(),
input: "hi".to_string(),
expect_contains: Some("recorded".to_string()),
expect_tool_call: None,
max_tokens: None,
};
let passed = run_test_case(&blueprint, ®istry, &tc, &scripts)
.await
.unwrap();
assert!(passed);
let request = seen.lock().unwrap().take().expect("provider saw a request");
let system_texts: Vec<&String> = request.system.iter().map(|b| &b.text).collect();
let rendered = system_texts
.iter()
.any(|t| t.as_str() == "<brain stage=main model=claude-sonnet-4-6>");
assert!(
rendered,
"custom region rendered with stage metadata; system blocks: {system_texts:?}"
);
let provider = registry.get("anthropic").unwrap();
assert_eq!(provider.count_tokens("abcd", "m").await, 4);
assert_eq!(provider.max_context_tokens("m"), 8192);
assert_eq!(provider.name(), "recording");
}
#[tokio::test]
async fn execute_fails_fast_on_a_broken_custom_region_script() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
std::fs::write(
project.join("agent.leviath"),
r#"
[agent]
name = "broken-custom"
version = "0.1.0"
description = "d"
[stages.main]
model = { provider = "anthropic", model = "m" }
[context.regions.brain]
kind = "custom"
script = "hooks/missing.rhai"
max_tokens = 4000
"#,
)
.unwrap();
std::fs::create_dir(project.join("tests")).unwrap();
std::fs::write(
project.join("tests/basic.toml"),
"[[test]]\nname = \"t\"\ninput = \"hi\"\n",
)
.unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: true,
};
let err = execute(args).await.unwrap_err().to_string();
assert!(err.contains("region 'brain'"), "{err}");
assert!(err.contains("hooks/missing.rhai"), "{err}");
}
#[tokio::test]
async fn run_test_case_inference_error_propagates() {
let blueprint = basic_blueprint();
let mut registry = ProviderRegistry::new();
registry.register("anthropic".to_string(), Arc::new(ErrorProvider));
let tc = TestCase {
name: "inference_error".to_string(),
input: "hi".to_string(),
expect_contains: Some("x".to_string()),
expect_tool_call: None,
max_tokens: None,
};
let result = run_test_case(&blueprint, ®istry, &tc, &Default::default()).await;
let err = result.unwrap_err().to_string();
assert!(err.contains("Inference failed"));
}
#[tokio::test]
async fn run_test_case_blueprint_with_no_stages_errors() {
use leviath_core::{Blueprint, layout::ContextLayout};
let blueprint = Blueprint::new(
"no-stages".to_string(),
"test".to_string(),
vec![],
ContextLayout::new(vec![], 4096),
);
let registry = ProviderRegistry::new();
let tc = TestCase {
name: "no_stages".to_string(),
input: "hi".to_string(),
expect_contains: Some("x".to_string()),
expect_tool_call: None,
max_tokens: None,
};
let result = run_test_case(&blueprint, ®istry, &tc, &Default::default()).await;
let err = result.unwrap_err().to_string();
assert!(err.contains("Blueprint has no stages"));
}
#[tokio::test]
async fn run_test_case_with_preexisting_tool_results_region() {
let blueprint = blueprint_with_tool_results_region();
let mut registry = ProviderRegistry::new();
registry.register(
"anthropic".to_string(),
Arc::new(MockProvider {
content: "hello world".to_string(),
tool_calls: vec![],
}),
);
let tc = TestCase {
name: "has_tool_results_region".to_string(),
input: "hi".to_string(),
expect_contains: Some("world".to_string()),
expect_tool_call: None,
max_tokens: None,
};
let result = run_test_case(&blueprint, ®istry, &tc, &Default::default()).await;
assert!(result.unwrap());
}
#[tokio::test]
async fn execute_with_registry_manifest_unreadable_errors() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
std::fs::create_dir_all(project.join("agent.leviath")).unwrap();
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: true,
};
let result = execute_with_registry(args, Box::new(build_registry_from_config)).await;
assert!(result.is_err());
}
#[tokio::test]
async fn execute_with_registry_config_load_fails_errors() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
crate::config::with_isolated_config_path_async(
"test-cmd-config-fail",
|fake_dir| async move {
let bad_config = fake_dir.join("config.toml");
std::fs::write(&bad_config, "not valid toml {{{").unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: false, };
let result =
execute_with_registry(args, Box::new(build_registry_from_config)).await;
assert!(result.is_err());
},
)
.await;
}
#[tokio::test]
async fn execute_with_registry_manifest_invalid_toml_errors() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
std::fs::write(project.join("agent.leviath"), "not valid toml {{{").unwrap();
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: false,
};
let result =
execute_with_registry(args, Box::new(mock_registry_builder("irrelevant", vec![])))
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn execute_with_registry_tests_dir_unreadable_errors() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
std::fs::write(project.join("tests"), "not a dir").unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: true,
};
let result = execute_with_registry(args, Box::new(build_registry_from_config)).await;
assert!(result.is_err());
}
#[tokio::test]
async fn execute_with_registry_toml_unreadable_errors() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
std::fs::create_dir_all(tests_dir.join("unreadable.toml")).unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: true,
};
let result = execute_with_registry(args, Box::new(build_registry_from_config)).await;
assert!(result.is_err());
}
#[tokio::test]
async fn execute_with_registry_rhai_unreadable_errors() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
std::fs::create_dir_all(tests_dir.join("unreadable.rhai")).unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: true,
};
let result = execute_with_registry(args, Box::new(build_registry_from_config)).await;
assert!(result.is_err());
}
#[tokio::test]
async fn run_test_case_with_no_pinned_region_still_runs() {
use leviath_core::Blueprint;
use leviath_core::layout::ContextLayout;
let blueprint = Blueprint::new(
"no-regions".to_string(),
"test".to_string(),
vec![leviath_core::Stage::new(
"main".to_string(),
leviath_core::blueprint::ModelConfig::new(
"anthropic".to_string(),
"claude-sonnet-4-6".to_string(),
),
)],
ContextLayout::new(vec![], 4096),
);
let mut registry = ProviderRegistry::new();
registry.register(
"anthropic".to_string(),
Arc::new(MockProvider {
content: "hello world".to_string(),
tool_calls: vec![],
}),
);
let tc = TestCase {
name: "no_pinned".to_string(),
input: "hi".to_string(),
expect_contains: Some("world".to_string()),
expect_tool_call: None,
max_tokens: None,
};
assert!(
run_test_case(&blueprint, ®istry, &tc, &Default::default())
.await
.unwrap()
);
}
#[tokio::test]
async fn run_test_case_passes_with_expect_contains() {
let blueprint = basic_blueprint();
let mut registry = ProviderRegistry::new();
registry.register(
"anthropic".to_string(),
Arc::new(MockProvider {
content: "Hello, world!".to_string(),
tool_calls: vec![],
}),
);
let tc = TestCase {
name: "greeting".to_string(),
input: "say hello".to_string(),
expect_contains: Some("world".to_string()),
expect_tool_call: None,
max_tokens: None,
};
let result = run_test_case(&blueprint, ®istry, &tc, &Default::default()).await;
assert!(result.unwrap());
}
#[tokio::test]
async fn run_test_case_fails_expect_contains_mismatch() {
let blueprint = basic_blueprint();
let mut registry = ProviderRegistry::new();
registry.register(
"anthropic".to_string(),
Arc::new(MockProvider {
content: "Goodbye".to_string(),
tool_calls: vec![],
}),
);
let tc = TestCase {
name: "greeting".to_string(),
input: "say hello".to_string(),
expect_contains: Some("world".to_string()),
expect_tool_call: None,
max_tokens: None,
};
let result = run_test_case(&blueprint, ®istry, &tc, &Default::default()).await;
assert!(!result.unwrap());
}
#[tokio::test]
async fn run_test_case_passes_with_expect_tool_call() {
let blueprint = basic_blueprint();
let mut registry = ProviderRegistry::new();
registry.register(
"anthropic".to_string(),
Arc::new(MockProvider {
content: String::new(),
tool_calls: vec![ToolCall {
id: "call_1".to_string(),
name: "bash".to_string(),
arguments: serde_json::json!({}),
thought_signature: None,
}],
}),
);
let tc = TestCase {
name: "tool_test".to_string(),
input: "run a command".to_string(),
expect_contains: None,
expect_tool_call: Some("bash".to_string()),
max_tokens: None,
};
let result = run_test_case(&blueprint, ®istry, &tc, &Default::default()).await;
assert!(result.unwrap());
}
#[tokio::test]
async fn run_test_case_fails_expect_tool_call_missing() {
let blueprint = basic_blueprint();
let mut registry = ProviderRegistry::new();
registry.register(
"anthropic".to_string(),
Arc::new(MockProvider {
content: "no tools here".to_string(),
tool_calls: vec![ToolCall {
id: "call_1".to_string(),
name: "write_file".to_string(),
arguments: serde_json::json!({}),
thought_signature: None,
}],
}),
);
let tc = TestCase {
name: "tool_test".to_string(),
input: "run a command".to_string(),
expect_contains: None,
expect_tool_call: Some("bash".to_string()),
max_tokens: None,
};
let result = run_test_case(&blueprint, ®istry, &tc, &Default::default()).await;
assert!(!result.unwrap());
}
#[tokio::test]
async fn run_test_case_fails_both_assertions() {
let blueprint = basic_blueprint();
let mut registry = ProviderRegistry::new();
registry.register(
"anthropic".to_string(),
Arc::new(MockProvider {
content: "unrelated content".to_string(),
tool_calls: vec![],
}),
);
let tc = TestCase {
name: "both".to_string(),
input: "do stuff".to_string(),
expect_contains: Some("expected".to_string()),
expect_tool_call: Some("write_file".to_string()),
max_tokens: None,
};
let result = run_test_case(&blueprint, ®istry, &tc, &Default::default()).await;
assert!(!result.unwrap());
}
#[tokio::test]
async fn run_test_case_no_assertions_always_passes() {
let blueprint = basic_blueprint();
let mut registry = ProviderRegistry::new();
registry.register(
"anthropic".to_string(),
Arc::new(MockProvider {
content: "anything".to_string(),
tool_calls: vec![],
}),
);
let tc = TestCase {
name: "no_assertions".to_string(),
input: "hi".to_string(),
expect_contains: None,
expect_tool_call: None,
max_tokens: None,
};
let result = run_test_case(&blueprint, ®istry, &tc, &Default::default()).await;
assert!(result.unwrap());
}
#[tokio::test]
async fn run_test_case_provider_not_registered_errors() {
let blueprint = basic_blueprint();
let registry = ProviderRegistry::new();
let tc = TestCase {
name: "no_provider".to_string(),
input: "hi".to_string(),
expect_contains: Some("x".to_string()),
expect_tool_call: None,
max_tokens: None,
};
let result = run_test_case(&blueprint, ®istry, &tc, &Default::default()).await;
let err = result.unwrap_err().to_string();
assert!(err.contains("not configured"));
}
#[tokio::test]
async fn no_temperature_provider_metadata_is_exercised() {
let p = NoTemperatureProvider;
assert_eq!(p.name(), "no-temperature");
assert_eq!(p.count_tokens("abcd", "m").await, 4);
assert_eq!(p.max_context_tokens("m"), 8192);
}
#[tokio::test]
async fn run_test_case_omits_temperature_when_provider_lacks_it() {
let blueprint = basic_blueprint();
let mut registry = ProviderRegistry::new();
registry.register("anthropic".to_string(), Arc::new(NoTemperatureProvider));
let tc = TestCase {
name: "no_temp".to_string(),
input: "hi".to_string(),
expect_contains: Some("cold".to_string()),
expect_tool_call: None,
max_tokens: None,
};
assert!(
run_test_case(&blueprint, ®istry, &tc, &Default::default())
.await
.unwrap()
);
}
#[tokio::test]
async fn run_test_case_long_input_runs() {
let blueprint = basic_blueprint();
let mut registry = ProviderRegistry::new();
registry.register(
"anthropic".to_string(),
Arc::new(MockProvider {
content: "response text mentioning keyword".to_string(),
tool_calls: vec![],
}),
);
let tc = TestCase {
name: "long_input".to_string(),
input: "x".repeat(500),
expect_contains: Some("keyword".to_string()),
expect_tool_call: None,
max_tokens: None,
};
let result = run_test_case(&blueprint, ®istry, &tc, &Default::default()).await;
assert!(result.unwrap());
}
fn mock_registry_builder(
content: &'static str,
tool_calls: Vec<ToolCall>,
) -> impl FnOnce(&Config) -> Result<ProviderRegistry, leviath_providers::ProviderError> {
move |_config: &Config| {
let mut reg = ProviderRegistry::new();
reg.register(
"anthropic".to_string(),
Arc::new(MockProvider {
content: content.to_string(),
tool_calls,
}),
);
Ok(reg)
}
}
fn write_project_with_test_file(project: &std::path::Path, test_toml: &str) {
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
std::fs::write(tests_dir.join("basic.toml"), test_toml).unwrap();
}
#[tokio::test]
async fn execute_with_registry_non_dry_run_all_pass() {
crate::config::with_isolated_config_path_async(
"test-rs-non-dry-run-all-pass",
|_fake_dir| async move {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
write_project_with_test_file(
project,
r#"
[[test]]
name = "greeting"
input = "say hello"
expect_contains = "world"
"#,
);
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: false,
};
let result = with_tracing(|| {
execute_with_registry(
args,
Box::new(mock_registry_builder("Hello, world!", vec![])),
)
})
.await;
assert!(result.is_ok());
},
)
.await;
}
#[tokio::test]
async fn execute_with_registry_none_path_defaults_to_current_dir() {
let args = TestArgs {
path: None,
filter: None,
dry_run: true,
};
let result = execute_with_registry(args, Box::new(build_registry_from_config)).await;
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("No agent.leviath found")
);
}
#[tokio::test]
async fn execute_with_registry_non_dry_run_failure_bails_with_count() {
crate::config::with_isolated_config_path_async(
"test-rs-non-dry-run-failure-bails-with-count",
|_fake_dir| async move {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
write_project_with_test_file(
project,
r#"
[[test]]
name = "greeting"
input = "say hello"
expect_contains = "world"
"#,
);
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: false,
};
let result =
execute_with_registry(args, Box::new(mock_registry_builder("goodbye", vec![])))
.await;
let err = result.unwrap_err().to_string();
assert!(err.contains("1 test(s) failed"));
},
)
.await;
}
#[tokio::test]
async fn execute_with_registry_non_dry_run_applies_filter() {
crate::config::with_isolated_config_path_async(
"test-rs-non-dry-run-applies-filter",
|_fake_dir| async move {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
write_project_with_test_file(
project,
r#"
[[test]]
name = "keep_me"
input = "say hello"
expect_contains = "world"
[[test]]
name = "skip_me"
input = "say hello"
expect_contains = "unmatchable content"
"#,
);
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: Some("keep".to_string()),
dry_run: false,
};
let result = execute_with_registry(
args,
Box::new(mock_registry_builder("Hello, world!", vec![])),
)
.await;
assert!(result.is_ok());
},
)
.await;
}
#[tokio::test]
async fn execute_with_registry_non_dry_run_tool_call_assertion() {
crate::config::with_isolated_config_path_async(
"test-rs-non-dry-run-tool-call-assertion",
|_fake_dir| async move {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
write_project_with_test_file(
project,
r#"
[[test]]
name = "tool_test"
input = "run a command"
expect_tool_call = "bash"
"#,
);
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: false,
};
let tool_calls = vec![ToolCall {
id: "call_1".to_string(),
name: "bash".to_string(),
arguments: serde_json::json!({}),
thought_signature: None,
}];
let result =
execute_with_registry(args, Box::new(mock_registry_builder("", tool_calls)))
.await;
assert!(result.is_ok());
},
)
.await;
}
#[tokio::test]
async fn execute_with_registry_non_dry_run_provider_error_counts_as_failure() {
crate::config::with_isolated_config_path_async(
"test-rs-non-dry-run-provider-error-counts-as-failure",
|_fake_dir| async move {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
write_project_with_test_file(
project,
r#"
[[test]]
name = "no_such_provider"
input = "hi"
expect_contains = "x"
"#,
);
std::fs::write(
project.join("agent.leviath"),
r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "nonexistent-provider", model = "x" }
"#,
)
.unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: false,
};
let result = execute_with_registry(
args,
Box::new(mock_registry_builder("irrelevant", vec![])),
)
.await;
let err = result.unwrap_err().to_string();
assert!(err.contains("1 test(s) failed"));
},
)
.await;
}
#[tokio::test]
async fn execute_with_registry_non_dry_run_toml_malformed_errors() {
crate::config::with_isolated_config_path_async(
"test-rs-non-dry-run-toml-malformed-errors",
|_fake_dir| async move {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
write_project_with_test_file(project, "not valid {{{ toml");
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: false,
};
let result = execute_with_registry(
args,
Box::new(mock_registry_builder("irrelevant", vec![])),
)
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Failed to parse"));
},
)
.await;
}
#[tokio::test]
async fn execute_with_registry_rhai_script_passes() {
crate::config::with_isolated_config_path_async(
"test-rs-rhai-script-passes",
|_fake_dir| async move {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
std::fs::write(tests_dir.join("script.rhai"), "true").unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: false,
};
let result =
execute_with_registry(args, Box::new(mock_registry_builder("unused", vec![])))
.await;
assert!(result.is_ok());
},
)
.await;
}
#[tokio::test]
async fn execute_with_registry_rhai_script_returns_false_fails() {
crate::config::with_isolated_config_path_async(
"test-rs-rhai-script-returns-false-fails",
|_fake_dir| async move {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
std::fs::write(tests_dir.join("script.rhai"), "false").unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: false,
};
let result =
execute_with_registry(args, Box::new(mock_registry_builder("unused", vec![])))
.await;
let err = result.unwrap_err().to_string();
assert!(err.contains("1 test(s) failed"));
},
)
.await;
}
#[tokio::test]
async fn execute_with_registry_rhai_script_error_fails() {
crate::config::with_isolated_config_path_async(
"test-rs-rhai-script-error-fails",
|_fake_dir| async move {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
std::fs::write(tests_dir.join("script.rhai"), "this is not valid rhai (((")
.unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: false,
};
let result =
execute_with_registry(args, Box::new(mock_registry_builder("unused", vec![])))
.await;
assert!(result.is_err());
},
)
.await;
}
#[tokio::test]
async fn execute_with_registry_rhai_script_non_bool_return_passes() {
crate::config::with_isolated_config_path_async(
"test-rs-rhai-script-non-bool-return-passes",
|_fake_dir| async move {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
std::fs::write(tests_dir.join("script.rhai"), "42").unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: false,
};
let result =
execute_with_registry(args, Box::new(mock_registry_builder("unused", vec![])))
.await;
assert!(result.is_ok());
},
)
.await;
}
#[tokio::test]
async fn execute_with_registry_rhai_script_filter_excludes_all() {
crate::config::with_isolated_config_path_async(
"test-rs-rhai-script-filter-excludes-all",
|_fake_dir| async move {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
std::fs::write(tests_dir.join("script.rhai"), "false").unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: Some("no-such-script".to_string()),
dry_run: false,
};
let result =
execute_with_registry(args, Box::new(mock_registry_builder("unused", vec![])))
.await;
assert!(result.is_ok());
},
)
.await;
}
#[test]
fn build_registry_from_config_registers_all_providers() {
let config = Config {
default_provider: "anthropic".to_string(),
providers: crate::config::ProviderConfig {
anthropic_api_key: Some("fake-anthropic-key".to_string()),
openai_api_key: Some("fake-openai-key".to_string()),
google_api_key: Some("fake-google-key".to_string()),
claude_code_enabled: false,
claude_code_binary: None,
claude_code_effort: None,
anthropic_cache_ttl: None,
fallback_order: Vec::new(),
},
openrouter_api_key: Some("fake-openrouter-key".to_string()),
ollama_base_url: Some("http://localhost:12345".to_string()),
..Config::default()
};
let registry =
build_registry_from_config(&config).expect("an HTTPS client builds in tests");
assert!(registry.has("anthropic"));
assert!(registry.has("openai"));
assert!(registry.has("google"));
assert!(registry.has("openrouter"));
assert!(registry.has("ollama"));
}
#[test]
fn build_registry_from_config_no_keys_still_registers_ollama_with_default_url() {
let config = Config::default();
let registry =
build_registry_from_config(&config).expect("an HTTPS client builds in tests");
assert!(!registry.has("anthropic"));
assert!(!registry.has("openai"));
assert!(!registry.has("google"));
assert!(!registry.has("openrouter"));
assert!(registry.has("ollama"));
}
#[tokio::test]
async fn execute_with_registry_ignores_non_test_files_in_tests_dir() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "test"
[stages.main]
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
"#;
write_test_agent(project, manifest);
let tests_dir = project.join("tests");
std::fs::create_dir_all(&tests_dir).unwrap();
std::fs::write(tests_dir.join("readme.txt"), "this file should be ignored").unwrap();
let args = TestArgs {
path: Some(project.to_str().unwrap().to_string()),
filter: None,
dry_run: true,
};
let result = execute_with_registry(args, Box::new(build_registry_from_config)).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn error_provider_trivial_trait_methods() {
let provider = ErrorProvider;
assert_eq!(provider.count_tokens("hello", "any-model").await, 5);
assert_eq!(provider.max_context_tokens("any-model"), 8192);
assert_eq!(provider.name(), "error-provider");
let caps = provider.capabilities("any-model");
let _ = caps; }
#[tokio::test]
async fn mock_provider_trivial_trait_methods() {
let provider = MockProvider {
content: "x".to_string(),
tool_calls: vec![],
};
assert_eq!(provider.count_tokens("hello", "any-model").await, 5);
assert_eq!(provider.max_context_tokens("any-model"), 8192);
assert_eq!(provider.name(), "mock");
}
#[test]
fn a_registry_needs_an_https_client_it_can_build() {
let mut config = Config::default();
config.providers.anthropic_api_key = Some("k".to_string());
let err = build_registry_from_config_with(&config, &|_t| {
Err(leviath_providers::provider::malformed_url_error())
})
.err()
.expect("a failing client factory should fail the registry");
assert!(err.to_string().contains("root certificate store"));
}
#[tokio::test]
async fn a_real_run_stops_when_the_registry_will_not_build() {
crate::config::with_isolated_config_path_async(
"test-a_real_run_stops_when_the_registry_will_not_build",
|_fake_dir| async move {
let dir = tempfile::tempdir().expect("tempdir");
let project = dir.path();
write_project_with_test_file(project, "[[test]]\nname = \"t\"\ninput = \"hi\"\n");
let args = TestArgs {
path: Some(project.to_str().expect("utf-8 path").to_string()),
filter: None,
dry_run: false,
};
let failing: RegistryBuilder = Box::new(|_config: &Config| {
Err(leviath_providers::ProviderError::ClientBuild(
"no roots".to_string(),
))
});
let err = execute_with_registry(args, failing)
.await
.expect_err("a failing registry builder should stop the run");
assert!(err.to_string().contains("no roots"), "{err}");
},
)
.await;
}
}