use clap::Args;
use std::path::PathBuf;
#[derive(Args)]
pub struct ValidateArgs {
#[arg(default_value = ".")]
pub(crate) path: String,
}
#[derive(Debug)]
enum ManifestCheckError {
Io(anyhow::Error),
Parse(String),
Validation(String),
}
fn check_manifest(path: &std::path::Path) -> Result<leviath_core::Blueprint, ManifestCheckError> {
let manifest_path = if path.is_file() {
path.to_path_buf()
} else {
let p = path.join("agent.leviath");
if !p.exists() {
return Err(ManifestCheckError::Io(anyhow::anyhow!(
"No agent.leviath found at {}",
path.display()
)));
}
p
};
let content = std::fs::read_to_string(&manifest_path).map_err(|e| {
ManifestCheckError::Io(anyhow::anyhow!(
"Failed to read {}: {}",
manifest_path.display(),
e
))
})?;
let blueprint = leviath_core::manifest::parse_manifest(&content)
.map_err(|e| ManifestCheckError::Parse(e.to_string()))?;
blueprint
.validate()
.map_err(|e| ManifestCheckError::Validation(e.to_string()))?;
crate::daemon::spawn::resolve_region_scripts(&blueprint, &manifest_path.to_string_lossy())
.map_err(ManifestCheckError::Validation)?;
Ok(blueprint)
}
fn print_success(blueprint: &leviath_core::Blueprint) {
println!("✓ Blueprint '{}' is valid.", blueprint.name);
println!(
" {} stages, version {}",
blueprint.stages.len(),
blueprint.version
);
let is_graph = blueprint.stages.iter().any(|s| s.transitions.is_some());
if is_graph {
let entry = blueprint.resolve_entry_stage_name();
println!(" Graph mode: entry stage '{}'", entry);
for stage in &blueprint.stages {
let transitions_info = match &stage.transitions {
Some(t) if !t.is_empty() => {
let targets: Vec<&str> = t.keys().map(|k| k.as_str()).collect();
format!(" → {}", targets.join(", "))
}
Some(_) => " (terminal)".to_string(),
None => " (linear)".to_string(),
};
let revisits = stage
.max_revisits
.map(|n| format!(" (max_revisits: {})", n))
.unwrap_or_default();
println!(" - {}{}{}", stage.name, transitions_info, revisits);
}
} else {
println!(
" Linear mode: {}",
blueprint
.stages
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join(" → ")
);
}
for line in command_seed_report(blueprint) {
println!("{line}");
}
print_warnings(blueprint);
}
fn command_seed_report(blueprint: &leviath_core::Blueprint) -> Vec<String> {
let seeds: Vec<(&str, &str)> = blueprint
.context_layout
.regions
.iter()
.filter_map(|r| match &r.seed {
Some(leviath_core::layout::RegionSeed::Command { command }) => {
Some((r.name.as_str(), command.as_str()))
}
_ => None,
})
.collect();
if seeds.is_empty() {
return Vec::new();
}
let mut lines = vec![format!(
" ⚠ {} region(s) run a shell command at spawn, before the first \
inference and before any tool-approval prompt:",
seeds.len()
)];
lines.extend(
seeds
.iter()
.map(|(region, command)| format!(" {region}: {command}")),
);
lines.push(
" Disable with `--no-seed-commands`, or machine-wide via \
`[security] allow_seed_commands = false`."
.to_string(),
);
lines
}
enum ValidateOutcome {
Success,
ParseError(String),
ValidationError(String),
}
fn execute_reporting_outcome(args: &ValidateArgs) -> anyhow::Result<ValidateOutcome> {
let path = PathBuf::from(&args.path);
let blueprint = match check_manifest(&path) {
Ok(bp) => bp,
Err(ManifestCheckError::Io(e)) => return Err(e),
Err(ManifestCheckError::Parse(e)) => return Ok(ValidateOutcome::ParseError(e)),
Err(ManifestCheckError::Validation(e)) => return Ok(ValidateOutcome::ValidationError(e)),
};
print_success(&blueprint);
print_script_tool_report(&path);
Ok(ValidateOutcome::Success)
}
fn print_script_tool_report(path: &std::path::Path) {
let agent_dir = if path.is_file() {
path.parent().unwrap_or(path).to_path_buf()
} else {
path.to_path_buf()
};
let tools_dir = agent_dir.join("tools");
if !tools_dir.is_dir() {
return;
}
let (set, skipped) = leviath_scripting::ScriptToolSet::discover(&[tools_dir]);
if !set.is_empty() {
println!(" {} script tool(s) in tools/", set.len());
}
for meta in set.metas() {
if !crate::daemon::spawn::current_platform_satisfies(&meta.required_caps) {
println!(
" ⚠ Warning: script tool '{}' won't load here (unsatisfiable @requires: {})",
meta.name,
meta.required_caps.join(", ")
);
}
}
for s in &skipped {
println!(
" ⚠ Warning: script tool '{}' skipped: {}",
s.path.display(),
s.reason
);
}
}
pub async fn execute(args: ValidateArgs) -> anyhow::Result<()> {
match execute_reporting_outcome(&args)? {
ValidateOutcome::Success => Ok(()),
ValidateOutcome::ParseError(e) => anyhow::bail!("✗ Parse error: {}", e),
ValidateOutcome::ValidationError(e) => anyhow::bail!("✗ Validation failed: {}", e),
}
}
fn print_warnings(blueprint: &leviath_core::Blueprint) {
let stage_names: std::collections::HashSet<&str> =
blueprint.stages.iter().map(|s| s.name.as_str()).collect();
let is_graph = blueprint.stages.iter().any(|s| s.transitions.is_some());
if !is_graph {
return;
}
let entry = blueprint.resolve_entry_stage_name();
let mut reachable = std::collections::HashSet::new();
let mut queue = std::collections::VecDeque::new();
queue.push_back(entry.clone());
while let Some(name) = queue.pop_front() {
if !reachable.insert(name.clone()) {
continue;
}
let Some(stage) = blueprint.find_stage(&name) else {
continue;
};
let Some(ref transitions) = stage.transitions else {
continue;
};
for target in transitions.keys() {
if !reachable.contains(target.as_str()) && stage_names.contains(target.as_str()) {
queue.push_back(target.clone());
}
}
}
for stage in &blueprint.stages {
if !reachable.contains(stage.name.as_str()) {
println!(
" ⚠ Warning: stage '{}' is unreachable from entry stage '{}'",
stage.name, entry
);
}
}
for stage in &blueprint.stages {
let Some(ref transitions) = stage.transitions else {
continue;
};
for target in transitions.keys() {
if target == &stage.name {
continue;
}
let Some(target_stage) = blueprint.find_stage(target) else {
continue;
};
let Some(ref t2) = target_stage.transitions else {
continue;
};
if t2.contains_key(&stage.name) && target_stage.max_revisits.is_none() {
#[rustfmt::skip]
println!(" ⚠ Warning: stage '{}' is in a cycle but has no max_revisits set", target);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::write_test_agent;
fn make_blueprint_toml(stages_toml: &str) -> String {
format!(
r#"
[agent]
name = "test"
version = "0.1.0"
description = "test blueprint"
{}
[context.regions]
system = {{ kind = "pinned", max_tokens = 1000 }}
conversation = {{ kind = "sliding_window", max_items = 50, max_tokens = 10000 }}
"#,
stages_toml
)
}
fn parse(toml: &str) -> leviath_core::Blueprint {
leviath_core::manifest::parse_manifest(toml).unwrap()
}
#[test]
fn check_manifest_verifies_custom_region_scripts() {
let dir = tempfile::tempdir().unwrap();
let manifest_path = dir.path().join("agent.leviath");
let toml = r#"
[agent]
name = "custom-validate"
version = "0.1.0"
description = "d"
[stages.main]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-5" }
description = "Main stage"
[context.regions]
system = { kind = "pinned", max_tokens = 1000 }
conversation = { kind = "sliding_window", max_items = 50, max_tokens = 10000 }
brain = { kind = "custom", script = "hooks/brain.rhai", max_tokens = 1000 }
"#;
std::fs::write(&manifest_path, toml).unwrap();
let err = format!("{:?}", check_manifest(&manifest_path).unwrap_err());
assert!(err.starts_with("Validation"), "{err}");
assert!(err.contains("region 'brain'"), "{err}");
std::fs::create_dir(dir.path().join("hooks")).unwrap();
std::fs::write(
dir.path().join("hooks/brain.rhai"),
"fn render(ctx) { \"ok\" }",
)
.unwrap();
let bp = check_manifest(&manifest_path).unwrap();
assert_eq!(bp.name, "custom-validate");
}
#[test]
fn command_seed_report_is_empty_without_command_seeds() {
let bp = parse(&make_blueprint_toml(
r#"
[stages.main]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-5" }
description = "Main stage"
"#,
));
assert!(command_seed_report(&bp).is_empty());
}
#[test]
fn command_seed_report_names_every_region_and_command() {
let toml = r#"
[agent]
name = "scanner"
version = "0.1.0"
[stages.main]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-5" }
description = "Main stage"
[context.regions]
facts = { kind = "pinned", max_tokens = 1000, seed = { command = "git ls-files" } }
tests = { kind = "pinned", max_tokens = 1000, seed = { command = "ls tests" } }
plain = { kind = "pinned", max_tokens = 1000 }
conversation = { kind = "sliding_window", max_items = 50, max_tokens = 10000 }
"#;
let report = command_seed_report(&parse(toml)).join("\n");
assert!(report.contains("2 region(s)"), "got: {report}");
assert!(report.contains("facts: git ls-files"), "got: {report}");
assert!(report.contains("tests: ls tests"), "got: {report}");
assert!(report.contains("--no-seed-commands"), "got: {report}");
assert!(report.contains("allow_seed_commands"), "got: {report}");
assert!(!report.contains("plain"), "got: {report}");
print_success(&parse(toml));
}
#[test]
fn print_warnings_linear_mode_no_panic() {
let toml = make_blueprint_toml(
r#"
[stages.main]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Main stage"
max_iterations = 10
"#,
);
let bp = parse(&toml);
print_warnings(&bp);
}
#[test]
fn print_warnings_graph_all_reachable() {
let toml = make_blueprint_toml(
r#"
[stages.a]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Stage A"
max_iterations = 5
entry = true
[stages.a.transitions]
b = "true"
[stages.b]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Stage B"
max_iterations = 5
"#,
);
let bp = parse(&toml);
print_warnings(&bp);
}
#[test]
fn validate_args_default_path() {
let args = ValidateArgs {
path: ".".to_string(),
};
assert_eq!(args.path, ".");
}
#[test]
fn print_warnings_unreachable_stage_no_panic() {
let toml = make_blueprint_toml(
r#"
[stages.a]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Stage A"
max_iterations = 5
entry = true
[stages.a.transitions]
b = "true"
[stages.b]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Stage B"
max_iterations = 5
[stages.orphan]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Unreachable stage"
max_iterations = 5
"#,
);
let bp = parse(&toml);
print_warnings(&bp);
}
#[test]
fn print_warnings_cycle_without_max_revisits_no_panic() {
let toml = make_blueprint_toml(
r#"
[stages.a]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Stage A"
max_iterations = 5
entry = true
[stages.a.transitions]
b = "true"
[stages.b]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Stage B"
max_iterations = 5
[stages.b.transitions]
a = "true"
"#,
);
let bp = parse(&toml);
print_warnings(&bp);
}
#[test]
fn print_warnings_cycle_with_max_revisits_no_panic() {
let toml = make_blueprint_toml(
r#"
[stages.a]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Stage A"
max_iterations = 5
entry = true
[stages.a.transitions]
b = "true"
[stages.b]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Stage B"
max_iterations = 5
max_revisits = 3
[stages.b.transitions]
a = "true"
"#,
);
let bp = parse(&toml);
print_warnings(&bp);
}
#[test]
fn print_warnings_terminal_stage_no_panic() {
let toml = make_blueprint_toml(
r#"
[stages.a]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Stage A"
max_iterations = 5
entry = true
[stages.a.transitions]
b = "true"
[stages.b]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Terminal stage"
max_iterations = 5
[stages.b.transitions]
"#,
);
let bp = parse(&toml);
print_warnings(&bp);
}
#[test]
fn print_warnings_self_loop_with_max_revisits_no_panic() {
let toml = make_blueprint_toml(
r#"
[stages.a]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Stage A"
max_iterations = 5
entry = true
max_revisits = 3
[stages.a.transitions]
a = "true"
"#,
);
let bp = parse(&toml);
print_warnings(&bp);
}
fn make_model() -> leviath_core::blueprint::ModelConfig {
leviath_core::blueprint::ModelConfig::new(
"anthropic".to_string(),
"claude-sonnet-4-6".to_string(),
)
}
#[test]
fn print_warnings_entry_stage_missing_no_panic() {
use leviath_core::{Blueprint, ContextLayout, Stage};
let mut stage_a = Stage::new("a".to_string(), make_model());
stage_a.transitions = Some(std::collections::HashMap::new());
let layout = ContextLayout::new(Vec::new(), 1000);
let mut bp = Blueprint::new(
"test".to_string(),
"test".to_string(),
vec![stage_a],
layout,
);
bp.entry_stage = Some("ghost".to_string());
print_warnings(&bp);
}
#[test]
fn print_warnings_transition_target_missing_no_panic() {
use leviath_core::{Blueprint, ContextLayout, Stage, TransitionEdge};
let mut transitions = std::collections::HashMap::new();
transitions.insert(
"ghost".to_string(),
TransitionEdge {
target: "ghost".to_string(),
condition: Default::default(),
hint: None,
transform: Default::default(),
gate: None,
stuck: None,
},
);
let mut stage_a = Stage::new("a".to_string(), make_model());
stage_a.transitions = Some(transitions);
let layout = ContextLayout::new(Vec::new(), 1000);
let bp = Blueprint::new(
"test".to_string(),
"test".to_string(),
vec![stage_a],
layout,
);
print_warnings(&bp);
}
#[tokio::test]
async fn execute_parse_error_returns_error() {
let dir = tempfile::tempdir().unwrap();
write_manifest(dir.path(), "not valid toml [[[");
let args = ValidateArgs {
path: dir.path().to_str().unwrap().to_string(),
};
let err = execute(args).await.unwrap_err();
assert!(err.to_string().contains("Parse error"));
}
#[tokio::test]
async fn execute_validation_error_returns_error() {
let dir = tempfile::tempdir().unwrap();
let manifest = r#"
[agent]
name = "bad-entry-agent"
version = "0.1.0"
description = "Entry stage does not exist"
entry_stage = "does-not-exist"
[stages.main]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Main"
max_iterations = 5
[context.regions]
system = { kind = "pinned", max_tokens = 1000 }
"#;
write_manifest(dir.path(), manifest);
let args = ValidateArgs {
path: dir.path().to_str().unwrap().to_string(),
};
let err = execute(args).await.unwrap_err();
assert!(err.to_string().contains("Validation failed"));
}
#[tokio::test]
async fn execute_no_manifest_errors() {
let dir = tempfile::tempdir().unwrap();
let args = ValidateArgs {
path: dir.path().to_str().unwrap().to_string(),
};
let result = execute(args).await;
assert!(result.is_err());
}
#[tokio::test]
async fn execute_valid_manifest_file_path() {
let dir = tempfile::tempdir().unwrap();
let manifest = r#"
[agent]
name = "test-agent"
version = "0.1.0"
description = "A test agent"
[stages.main]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Main"
max_iterations = 5
[context.regions]
system = { kind = "pinned", max_tokens = 1000 }
conversation = { kind = "sliding_window", max_items = 50, max_tokens = 10000 }
"#;
let manifest_path = dir.path().join("agent.leviath");
std::fs::write(&manifest_path, manifest).unwrap();
let args = ValidateArgs {
path: manifest_path.to_str().unwrap().to_string(),
};
let result = execute(args).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn execute_valid_manifest_directory_path() {
let dir = tempfile::tempdir().unwrap();
let manifest = r#"
[agent]
name = "dir-agent"
version = "0.2.0"
description = "A directory agent"
[stages.main]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Main"
max_iterations = 5
[context.regions]
system = { kind = "pinned", max_tokens = 1000 }
conversation = { kind = "sliding_window", max_items = 50, max_tokens = 10000 }
"#;
write_test_agent(dir.path(), manifest);
let args = ValidateArgs {
path: dir.path().to_str().unwrap().to_string(),
};
let result = execute(args).await;
assert!(result.is_ok());
}
fn assert_is_parse_error(outcome: &ValidateOutcome) {
assert!(matches!(outcome, ValidateOutcome::ParseError(_)));
}
#[test]
#[should_panic(expected = "assertion failed")]
fn assert_is_parse_error_panics_on_non_parse_error() {
assert_is_parse_error(&ValidateOutcome::Success);
}
#[test]
fn execute_reporting_outcome_malformed_toml_is_parse_error() {
let dir = tempfile::tempdir().unwrap();
write_manifest(dir.path(), "not valid toml [[[");
let args = ValidateArgs {
path: dir.path().to_str().unwrap().to_string(),
};
let outcome = execute_reporting_outcome(&args).unwrap();
assert_is_parse_error(&outcome);
}
#[test]
fn execute_reporting_outcome_bad_entry_stage_is_validation_error() {
let dir = tempfile::tempdir().unwrap();
let manifest = r#"
[agent]
name = "bad-entry-agent"
version = "0.1.0"
description = "Entry stage does not exist"
entry_stage = "does-not-exist"
[stages.main]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Main"
max_iterations = 5
[context.regions]
system = { kind = "pinned", max_tokens = 1000 }
"#;
write_manifest(dir.path(), manifest);
let args = ValidateArgs {
path: dir.path().to_str().unwrap().to_string(),
};
let outcome = execute_reporting_outcome(&args).unwrap();
assert_is_validation_error(&outcome);
}
fn assert_is_validation_error(outcome: &ValidateOutcome) {
assert!(matches!(outcome, ValidateOutcome::ValidationError(_)));
}
#[test]
#[should_panic(expected = "assertion failed")]
fn assert_is_validation_error_panics_on_non_validation_error() {
assert_is_validation_error(&ValidateOutcome::Success);
}
#[test]
fn execute_reporting_outcome_missing_manifest_is_io_error() {
let dir = tempfile::tempdir().unwrap();
let args = ValidateArgs {
path: dir.path().to_str().unwrap().to_string(),
};
assert!(execute_reporting_outcome(&args).is_err());
}
#[test]
fn execute_reporting_outcome_valid_manifest_is_success() {
let dir = tempfile::tempdir().unwrap();
let manifest = r#"
[agent]
name = "ok-agent"
version = "0.1.0"
description = "Valid"
[stages.main]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Main"
max_iterations = 5
[context.regions]
system = { kind = "pinned", max_tokens = 1000 }
conversation = { kind = "sliding_window", max_items = 50, max_tokens = 10000 }
"#;
write_manifest(dir.path(), manifest);
let args = ValidateArgs {
path: dir.path().to_str().unwrap().to_string(),
};
let outcome = execute_reporting_outcome(&args).unwrap();
assert_is_success(&outcome);
}
fn assert_is_success(outcome: &ValidateOutcome) {
assert!(matches!(outcome, ValidateOutcome::Success));
}
#[test]
fn execute_reporting_outcome_reports_agent_script_tools() {
let dir = tempfile::tempdir().unwrap();
let manifest = r#"
[agent]
name = "with-tools"
version = "0.1.0"
description = "has script tools"
[stages.main]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Main"
max_iterations = 5
[context.regions]
system = { kind = "pinned", max_tokens = 1000 }
"#;
write_manifest(dir.path(), manifest);
let tools = dir.path().join("tools");
std::fs::create_dir(&tools).unwrap();
std::fs::write(tools.join("ok.rhai"), "// @tool ok\nparams.x").unwrap();
std::fs::write(tools.join("bad.rhai"), "no directive\nlet").unwrap();
std::fs::write(tools.join("gpu.rhai"), "// @tool gpu\n// @requires gpu\n1").unwrap();
let args = ValidateArgs {
path: dir.path().to_str().unwrap().to_string(),
};
let outcome = execute_reporting_outcome(&args).unwrap();
assert_is_success(&outcome);
}
#[test]
fn print_script_tool_report_no_tools_dir_is_silent() {
let dir = tempfile::tempdir().unwrap();
let manifest = write_manifest(dir.path(), "unused");
print_script_tool_report(&manifest);
}
#[test]
fn print_script_tool_report_only_broken_scripts_warns_without_count() {
let dir = tempfile::tempdir().unwrap();
let tools = dir.path().join("tools");
std::fs::create_dir(&tools).unwrap();
std::fs::write(tools.join("bad.rhai"), "no directive\nlet").unwrap();
print_script_tool_report(dir.path());
}
#[test]
#[should_panic(expected = "assertion failed")]
fn assert_is_success_panics_on_non_success() {
assert_is_success(&ValidateOutcome::ParseError("x".to_string()));
}
#[test]
fn print_warnings_chain_all_reachable() {
let toml = make_blueprint_toml(
r#"
[stages.a]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "A"
max_iterations = 5
entry = true
[stages.a.transitions]
b = "true"
[stages.b]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "B"
max_iterations = 5
[stages.b.transitions]
c = "true"
[stages.c]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "C"
max_iterations = 5
"#,
);
let bp = parse(&toml);
print_warnings(&bp);
}
#[test]
fn print_warnings_diamond_graph_revisits_shared_target_no_panic() {
let toml = make_blueprint_toml(
r#"
[stages.entry]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Entry"
max_iterations = 5
entry = true
[stages.entry.transitions]
b = "true"
c = "true"
[stages.b]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "B"
max_iterations = 5
[stages.b.transitions]
d = "true"
[stages.c]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "C"
max_iterations = 5
[stages.c.transitions]
d = "true"
[stages.d]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "D"
max_iterations = 5
"#,
);
let bp = parse(&toml);
print_warnings(&bp);
}
fn write_manifest(dir: &std::path::Path, content: &str) -> std::path::PathBuf {
let path = dir.join("agent.leviath");
std::fs::write(&path, content).unwrap();
path
}
fn unwrap_io_err(err: ManifestCheckError) -> anyhow::Error {
let ManifestCheckError::Io(e) = err else {
panic!("expected ManifestCheckError::Io, got {err:?}");
};
e
}
#[test]
#[should_panic(expected = "expected ManifestCheckError::Io")]
fn unwrap_io_err_panics_on_parse_variant() {
let dir = tempfile::tempdir().unwrap();
write_manifest(dir.path(), "not valid toml [[[");
let err = check_manifest(dir.path()).unwrap_err();
unwrap_io_err(err);
}
#[test]
fn check_manifest_missing_directory_manifest_is_io_error() {
let dir = tempfile::tempdir().unwrap();
let err = check_manifest(dir.path()).unwrap_err();
let e = unwrap_io_err(err);
assert!(e.to_string().contains("No agent.leviath found"));
}
#[test]
fn check_manifest_unreadable_file_path_is_io_error() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("nonexistent-subdir");
let err = check_manifest(&missing).unwrap_err();
unwrap_io_err(err);
}
#[test]
fn check_manifest_unreadable_file_is_io_error() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("agent.leviath")).unwrap();
let result = check_manifest(dir.path());
let err = result.unwrap_err();
let e = unwrap_io_err(err);
assert!(e.to_string().contains("Failed to read"));
}
#[test]
fn check_manifest_malformed_toml_is_parse_error() {
let dir = tempfile::tempdir().unwrap();
write_manifest(dir.path(), "not valid toml [[[");
let err = check_manifest(dir.path()).unwrap_err();
assert_is_manifest_parse_error(&err);
}
fn assert_is_manifest_parse_error(err: &ManifestCheckError) {
assert!(matches!(err, ManifestCheckError::Parse(_)));
}
#[test]
#[should_panic(expected = "assertion failed")]
fn assert_is_manifest_parse_error_panics_on_non_parse_error() {
assert_is_manifest_parse_error(&ManifestCheckError::Io(anyhow::anyhow!("x")));
}
#[test]
fn check_manifest_direct_file_path_is_accepted() {
let dir = tempfile::tempdir().unwrap();
let toml = make_blueprint_toml(
r#"
[stages.main]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Main stage"
max_iterations = 5
"#,
);
let manifest_path = write_manifest(dir.path(), &toml);
let blueprint = check_manifest(&manifest_path).unwrap();
assert_eq!(blueprint.name, "test");
}
#[test]
fn check_manifest_valid_linear_blueprint_succeeds() {
let dir = tempfile::tempdir().unwrap();
let toml = make_blueprint_toml(
r#"
[stages.main]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Main stage"
max_iterations = 5
"#,
);
write_manifest(dir.path(), &toml);
let blueprint = check_manifest(dir.path()).unwrap();
assert_eq!(blueprint.name, "test");
assert_eq!(blueprint.stages.len(), 1);
}
#[test]
fn print_success_linear_mode_no_panic() {
let toml = make_blueprint_toml(
r#"
[stages.main]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Main stage"
max_iterations = 5
[stages.review]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "Review stage"
max_iterations = 5
"#,
);
let bp = parse(&toml);
print_success(&bp);
}
#[test]
fn print_success_graph_mode_with_terminal_and_revisits_no_panic() {
let toml = make_blueprint_toml(
r#"
[stages.a]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "A"
max_iterations = 5
entry = true
max_revisits = 3
[stages.a.transitions]
b = "true"
[stages.b]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "B"
max_iterations = 5
"#,
);
let bp = parse(&toml);
print_success(&bp);
}
#[test]
fn print_success_graph_mode_terminal_stage_no_panic() {
let toml = make_blueprint_toml(
r#"
[stages.a]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "A"
max_iterations = 5
entry = true
[stages.a.transitions]
b = "true"
[stages.b]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-4-6" }
description = "B"
max_iterations = 5
[stages.b.transitions]
"#,
);
let bp = parse(&toml);
let b = bp.find_stage("b").unwrap();
assert!(matches!(&b.transitions, Some(t) if t.is_empty()));
print_success(&bp);
}
}