use super::*;
fn manifest(stages_toml: &str) -> String {
format!(
r#"
[agent]
name = "lint-fixture"
version = "0.1.0"
description = "a fixture"
{stages_toml}
[context.regions]
system = {{ kind = "pinned", max_tokens = 1000 }}
conversation = {{ kind = "sliding_window", max_items = 50, max_tokens = 10000 }}
"#
)
}
fn lint(content: &str, env: &LintEnv) -> Vec<LintFinding> {
let bp = leviath_core::manifest::parse_manifest(content).expect("fixture parses");
lint_manifest(content, &bp, env)
}
fn codes(findings: &[LintFinding]) -> Vec<&'static str> {
findings.iter().map(|f| f.code).collect()
}
fn with_code<'a>(findings: &'a [LintFinding], code: &str) -> Vec<&'a LintFinding> {
findings.iter().filter(|f| f.code == code).collect()
}
const CLEAN_STAGE: &str = r#"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
description = "Main"
max_iterations = 10
available_tools = ["read_file"]
"#;
fn known_tools(names: &[&str]) -> HashSet<String> {
names.iter().map(|n| (*n).to_string()).collect()
}
#[test]
fn a_fully_declared_stage_reports_nothing() {
let findings = lint(&manifest(CLEAN_STAGE), &LintEnv::default());
assert!(findings.is_empty(), "{:?}", codes(&findings));
}
#[test]
fn an_empty_env_skips_every_environment_dependent_check() {
let toml = manifest(
r#"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "madeup", model = "no-such-model" }] }
max_iterations = 10
available_tools = ["raed_file"]
"#,
);
let findings = lint(&toml, &LintEnv::default());
assert!(findings.is_empty(), "{:?}", codes(&findings));
}
#[test]
fn a_stage_with_no_mode_is_warned_about() {
let toml = manifest(
r#"
[stages.main]
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 10
"#,
);
let findings = lint(&toml, &LintEnv::default());
assert_eq!(codes(&findings), ["stage-missing-mode"]);
assert_eq!(findings[0].stage.as_deref(), Some("main"));
assert_eq!(findings[0].severity, LintSeverity::Warning);
assert!(findings[0].message.contains("autonomous"), "{findings:?}");
}
#[test]
fn a_stage_with_no_model_block_is_warned_about() {
let toml = manifest(
r#"
[stages.main]
mode = "autonomous"
max_iterations = 10
"#,
);
let findings = lint(&toml, &LintEnv::default());
assert_eq!(codes(&findings), ["stage-missing-model"]);
assert!(
findings[0].message.contains("default_provider"),
"{findings:?}"
);
let fix = findings[0].fix.as_deref().expect("the fix names the block");
assert!(fix.contains("[stages.main]"), "{fix}");
}
#[test]
fn the_legacy_inline_model_spelling_counts_as_declared() {
let toml = manifest(
r#"
[stages.main]
mode = "autonomous"
model = { provider = "anthropic", model = "claude-sonnet-5" }
max_iterations = 10
"#,
);
let findings = lint(&toml, &LintEnv::default());
assert!(findings.is_empty(), "{:?}", codes(&findings));
}
#[test]
fn a_stage_with_no_max_iterations_is_warned_about() {
let toml = manifest(
r#"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
"#,
);
let findings = lint(&toml, &LintEnv::default());
assert_eq!(codes(&findings), ["stage-missing-max-iterations"]);
assert!(
findings[0].message.contains("default_max_iterations"),
"{findings:?}"
);
}
#[test]
fn a_fan_out_stage_needs_no_max_iterations() {
let toml = manifest(
r#"
[stages.split]
mode = "fan_out"
worker_stage = "work"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
[stages.work]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 5
allow_as_worker = true
"#,
);
let findings = lint(&toml, &LintEnv::default());
assert!(findings.is_empty(), "{:?}", codes(&findings));
}
#[test]
fn an_agent_level_model_block_is_warned_about() {
let toml = format!(
"{}\n[model]\nprovider = \"anthropic\"\nmodel = \"claude-opus-5\"\n",
manifest(CLEAN_STAGE)
);
let findings = lint(&toml, &LintEnv::default());
assert_eq!(codes(&findings), ["agent-model-block-ignored"]);
assert!(findings[0].stage.is_none(), "{findings:?}");
}
#[test]
fn unreadable_text_reports_every_key_as_declared() {
let declared = Declared::from_text("not valid toml [[[");
assert!(!declared.agent_model_block);
let keys = declared.stage("anything");
assert!(keys.mode);
assert!(keys.model);
}
#[test]
fn a_stage_absent_from_the_text_reports_as_declared() {
let keys = Declared::from_text("[agent]\nname = \"x\"\n").stage("ghost");
assert!(keys.mode);
assert!(keys.model);
}
#[test]
fn text_with_no_stages_table_has_no_stage_keys() {
let declared = Declared::from_text("[agent]\nname = \"x\"\n");
assert!(declared.stages.is_empty());
assert!(!declared.opaque);
}
#[test]
fn a_misspelled_tool_is_an_error() {
let toml = manifest(
r#"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 10
available_tools = ["read_file", "raed_file"]
"#,
);
let env = LintEnv {
known_tools: known_tools(&["read_file"]),
..LintEnv::default()
};
let findings = lint(&toml, &env);
assert_eq!(codes(&findings), ["unknown-tool"]);
assert!(findings[0].is_error());
assert!(findings[0].message.contains("raed_file"), "{findings:?}");
}
#[test]
fn an_mcp_tool_name_is_never_unknown() {
let toml = manifest(
r#"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 10
available_tools = ["github__create_issue"]
"#,
);
let env = LintEnv {
known_tools: known_tools(&["read_file"]),
..LintEnv::default()
};
assert!(lint(&toml, &env).is_empty());
}
#[test]
fn a_permission_for_an_ungranted_tool_is_an_error() {
let toml = manifest(
r#"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 10
available_tools = ["read_file"]
[stages.main.tool_permissions]
write_file = "allow"
"#,
);
let findings = lint(&toml, &LintEnv::default());
assert_eq!(codes(&findings), ["orphan-stage-permission"]);
assert!(findings[0].is_error());
assert!(findings[0].message.contains("write_file"), "{findings:?}");
}
#[test]
fn a_permission_for_a_granted_tool_is_fine() {
let toml = manifest(
r#"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 10
available_tools = ["read_file"]
[stages.main.tool_permissions]
read_file = "allow"
"#,
);
assert!(lint(&toml, &LintEnv::default()).is_empty());
}
#[test]
fn an_autonomous_stage_granting_an_ask_tool_is_warned_about() {
let toml = manifest(
r#"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 10
available_tools = ["ask_user_text"]
"#,
);
let findings = lint(&toml, &LintEnv::default());
assert_eq!(codes(&findings), ["blocking-tool-in-autonomous-stage"]);
assert!(
findings[0].message.contains("until a person answers"),
"{findings:?}"
);
}
#[test]
fn every_blocking_interaction_tool_is_flagged() {
for tool in BLOCKING_INTERACTION_TOOLS {
let toml = manifest(&format!(
r#"
[stages.main]
mode = "autonomous"
model = {{ models = [{{ provider = "anthropic", model = "claude-sonnet-5" }}] }}
max_iterations = 10
available_tools = ["{tool}"]
"#
));
assert_eq!(
codes(&lint(&toml, &LintEnv::default())),
["blocking-tool-in-autonomous-stage"],
"{tool}"
);
}
}
#[test]
fn allow_blocking_tools_silences_the_warning() {
let toml = manifest(
r#"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 10
available_tools = ["ask_user_text"]
allow_blocking_tools = true
"#,
);
assert!(lint(&toml, &LintEnv::default()).is_empty());
}
#[test]
fn required_tools_silences_the_warning_for_that_tool() {
let toml = manifest(
r#"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 10
available_tools = ["ask_user_text", "ask_user_confirm"]
required_tools = ["ask_user_text"]
"#,
);
let findings = lint(&toml, &LintEnv::default());
assert_eq!(codes(&findings), ["blocking-tool-in-autonomous-stage"]);
assert!(
findings[0].message.contains("ask_user_confirm"),
"{:?}",
findings[0].message
);
}
#[test]
fn an_interactive_stage_may_grant_ask_tools_freely() {
let toml = manifest(
r#"
[stages.main]
mode = "interactive"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 10
available_tools = ["ask_user_text"]
"#,
);
assert!(lint(&toml, &LintEnv::default()).is_empty());
}
#[test]
fn a_shell_grant_with_no_policy_is_warned_about() {
let toml = manifest(
r#"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 10
available_tools = ["bash"]
"#,
);
let findings = lint(&toml, &LintEnv::default());
assert_eq!(codes(&findings), ["implicit-shell-policy"]);
assert!(findings[0].message.contains("'bash'"), "{findings:?}");
}
#[test]
fn the_canonical_shell_spelling_is_checked_too() {
let toml = manifest(
r#"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 10
available_tools = ["shell"]
"#,
);
assert_eq!(
codes(&lint(&toml, &LintEnv::default())),
["implicit-shell-policy"]
);
}
#[test]
fn a_stage_level_shell_policy_settles_it() {
let toml = manifest(
r#"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 10
available_tools = ["bash"]
[stages.main.tool_permissions]
bash = "allow"
"#,
);
assert!(lint(&toml, &LintEnv::default()).is_empty());
}
#[test]
fn a_permission_written_under_the_other_spelling_is_warned_about() {
let toml = format!(
"{}\n[tool_permissions]\nbash = \"ask\"\n",
manifest(
r#"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 10
available_tools = ["shell"]
"#,
)
);
let findings = lint(&toml, &LintEnv::default());
assert_eq!(codes(&findings), ["permission-name-mismatch"]);
assert!(
findings[0].message.contains("grants 'shell'"),
"{findings:?}"
);
assert!(findings[0].message.contains("'bash'"), "{findings:?}");
}
#[test]
fn the_mismatch_is_caught_in_either_direction() {
let toml = format!(
"{}\n[tool_permissions]\nshell = \"allow\"\n",
manifest(
r#"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 10
available_tools = ["bash"]
"#,
)
);
assert_eq!(
codes(&lint(&toml, &LintEnv::default())),
["permission-name-mismatch"]
);
}
#[test]
fn alias_siblings_never_include_the_name_itself() {
assert_eq!(alias_siblings("bash"), ["shell"]);
assert_eq!(alias_siblings("shell"), ["bash"]);
assert!(alias_siblings("read_file").is_empty());
}
#[test]
fn an_agent_level_shell_policy_settles_it() {
let toml = format!(
"{}\n[tool_permissions]\nbash = \"deny\"\n",
manifest(
r#"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 10
available_tools = ["bash"]
"#,
)
);
assert!(lint(&toml, &LintEnv::default()).is_empty());
}
#[test]
fn a_model_missing_from_a_known_catalog_is_warned_about() {
let toml = manifest(
r#"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-9" }] }
max_iterations = 10
"#,
);
let env = LintEnv {
known_models: vec![("anthropic".to_string(), "claude-sonnet-5".to_string())],
..LintEnv::default()
};
let findings = lint(&toml, &env);
assert_eq!(codes(&findings), ["unknown-model"]);
assert!(
findings[0].message.contains("anthropic/claude-sonnet-9"),
"{findings:?}"
);
}
#[test]
fn a_provider_with_no_catalog_is_not_checked() {
let toml = manifest(
r#"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "ollama", model = "qwen3.5:9b" }] }
max_iterations = 10
"#,
);
let env = LintEnv {
known_models: vec![("anthropic".to_string(), "claude-sonnet-5".to_string())],
..LintEnv::default()
};
assert!(lint(&toml, &env).is_empty());
}
#[test]
fn a_model_present_in_the_catalog_passes() {
let env = LintEnv {
known_models: vec![("anthropic".to_string(), "claude-sonnet-5".to_string())],
..LintEnv::default()
};
assert!(lint(&manifest(CLEAN_STAGE), &env).is_empty());
}
#[test]
fn a_stage_with_no_reachable_provider_is_warned_about() {
let toml = manifest(
r#"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }, { provider = "openai", model = "gpt-5.5" }] }
max_iterations = 10
"#,
);
let env = LintEnv {
available_providers: Some(known_tools(&["ollama"])),
..LintEnv::default()
};
let findings = lint(&toml, &env);
assert_eq!(codes(&findings), ["no-reachable-provider"]);
assert!(
findings[0].message.contains("anthropic, openai"),
"{findings:?}"
);
}
#[test]
fn an_unreachable_provider_is_fine_when_a_later_one_answers() {
let toml = manifest(
r#"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }, { provider = "ollama", model = "qwen3.5:9b" }] }
max_iterations = 10
"#,
);
let env = LintEnv {
available_providers: Some(known_tools(&["ollama"])),
..LintEnv::default()
};
assert!(lint(&toml, &env).is_empty());
}
#[test]
fn every_provider_reachable_reports_nothing() {
let env = LintEnv {
available_providers: Some(known_tools(&["anthropic"])),
..LintEnv::default()
};
assert!(lint(&manifest(CLEAN_STAGE), &env).is_empty());
}
#[test]
fn a_stage_with_an_empty_models_list_is_not_checked_for_reachability() {
let mut bp =
leviath_core::manifest::parse_manifest(&manifest(CLEAN_STAGE)).expect("the fixture parses");
bp.stages[0].model.models.clear();
let env = LintEnv {
available_providers: Some(HashSet::new()),
..LintEnv::default()
};
let findings = lint_manifest(&manifest(CLEAN_STAGE), &bp, &env);
assert!(findings.is_empty(), "{:?}", codes(&findings));
}
#[test]
fn read_path_declarations_are_noted_not_warned() {
let toml = format!(
"{}\n[read_paths]\nallow = [\"~/.leviath/runs\"]\n",
manifest(CLEAN_STAGE)
);
let findings = lint(&toml, &LintEnv::default());
assert_eq!(codes(&findings), ["read-paths-declared"]);
assert_eq!(findings[0].severity, LintSeverity::Note);
assert!(
findings[0].message.contains("~/.leviath/runs"),
"{findings:?}"
);
}
#[test]
fn no_read_paths_means_no_note() {
assert!(
!codes(&lint(&manifest(CLEAN_STAGE), &LintEnv::default())).contains(&"read-paths-declared")
);
}
fn read_paths_env(entries: &[&str], grants: &[&str]) -> (String, LintEnv) {
let listed = entries
.iter()
.map(|e| format!("\"{e}\""))
.collect::<Vec<_>>()
.join(", ");
let toml = format!(
"{}\n[read_paths]\nallow = [{listed}]\n",
manifest(CLEAN_STAGE)
);
let blueprint = leviath_core::manifest::parse_manifest(&toml).expect("the fixture parses");
let mut config = crate::config::Config::default();
config.security.read_paths = grants.iter().map(|s| s.to_string()).collect();
let env = LintEnv::default().with_read_paths(&blueprint, &config, Path::new("/work"));
(toml, env)
}
#[test]
fn ungranted_read_paths_are_warned_about_with_the_stanza_to_add() {
let (toml, env) = read_paths_env(&["/data/runs", "glob:/docs/**"], &[]);
let findings = lint(&toml, &env);
assert_eq!(
codes(&findings),
["read-paths-not-granted", "read-paths-declared"]
);
assert!(findings[0].message.contains("/data/runs"), "{findings:?}");
assert!(
findings[0].message.contains("glob:/docs/**"),
"{findings:?}"
);
let fix = findings[0].fix.as_deref().expect("a fix names the stanza");
assert!(fix.contains("[agent_read_paths.lint-fixture]"), "{fix}");
}
#[test]
fn a_partial_grant_names_only_what_is_still_refused() {
let (toml, env) = read_paths_env(&["/data/runs", "glob:/docs/**"], &["/data/runs"]);
let findings = lint(&toml, &env);
assert_eq!(
codes(&findings),
["read-paths-not-granted", "read-paths-declared"]
);
assert!(!findings[0].message.contains("/data/runs,"), "{findings:?}");
assert!(
findings[0].message.contains("glob:/docs/**"),
"{findings:?}"
);
assert!(
findings[1].message.contains("2 declared, 1 granted"),
"{findings:?}"
);
}
#[test]
fn granted_read_paths_are_a_note_only() {
let (toml, env) = read_paths_env(&["/data/runs"], &["/data/runs"]);
let findings = lint(&toml, &env);
assert_eq!(codes(&findings), ["read-paths-declared"]);
assert!(
findings[0].message.contains("1 declared, 1 granted"),
"{findings:?}"
);
assert!(
findings[0]
.fix
.as_deref()
.is_some_and(|f| f.contains("/data/runs: granted")),
"{findings:?}"
);
}
#[test]
fn the_blanket_override_is_named_on_the_note() {
let toml = format!(
"{}\n[read_paths]\nallow = [\"/data/runs\"]\n",
manifest(CLEAN_STAGE)
);
let blueprint = leviath_core::manifest::parse_manifest(&toml).expect("the fixture parses");
let mut config = crate::config::Config::default();
config.security.allow_blueprint_read_paths = true;
let env = LintEnv::default().with_read_paths(&blueprint, &config, Path::new("/work"));
let findings = lint(&toml, &env);
assert_eq!(codes(&findings), ["read-paths-declared"]);
assert!(
findings[0]
.fix
.as_deref()
.is_some_and(|f| f.contains("allow_blueprint_read_paths")),
"{findings:?}"
);
}
#[test]
fn an_uncheckable_entry_is_not_called_ungranted() {
let (toml, env) = read_paths_env(&["glob:/docs/[ab]/**"], &["/data/runs"]);
let findings = lint(&toml, &env);
assert_eq!(codes(&findings), ["read-paths-declared"]);
assert!(
findings[0]
.fix
.as_deref()
.is_some_and(|f| f.contains("cannot be checked")),
"{findings:?}"
);
}
#[test]
fn a_malformed_config_grant_is_warned_about() {
let (toml, env) = read_paths_env(&["/data/runs"], &["regex:relative/.*"]);
let findings = lint(&toml, &env);
assert_eq!(codes(&findings), ["read-paths-grant-invalid"]);
assert!(findings[0].message.contains("config.toml"), "{findings:?}");
}
#[test]
fn a_broad_read_path_entry_gets_its_own_warning() {
let toml = format!(
"{}\n[read_paths]\nallow = [\"~\", \"~/.leviath/runs\"]\n",
manifest(CLEAN_STAGE)
);
let findings = lint(&toml, &LintEnv::default());
assert_eq!(codes(&findings), ["broad-read-path", "read-paths-declared"]);
assert!(findings[0].message.contains("'~'"), "{findings:?}");
}
#[test]
fn the_broad_entry_heuristic_covers_each_shape() {
for entry in [
"~",
"~/",
"/",
"glob:**",
"glob:/**",
"regex:/.*",
"regex:/.+",
] {
assert!(read_path_entry_is_broad(entry), "{entry}");
}
for entry in [
"~/.leviath/runs",
"glob:~/docs/**",
"regex:/data/.*",
"../shared",
r"C:\data",
] {
assert!(!read_path_entry_is_broad(entry), "{entry}");
}
}
#[test]
fn command_seed_regions_are_named_in_one_note() {
let toml = r#"
[agent]
name = "scanner"
version = "0.1.0"
[stages.main]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
description = "Main stage"
max_iterations = 5
[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 findings = lint(toml, &LintEnv::default());
assert_eq!(codes(&findings), ["command-seed"]);
let message = &findings[0].message;
assert!(message.contains("2 region(s)"), "{message}");
assert!(message.contains("facts: git ls-files"), "{message}");
assert!(message.contains("tests: ls tests"), "{message}");
assert!(!message.contains("plain"), "{message}");
let fix = findings[0]
.fix
.as_deref()
.expect("a seed note offers a fix");
assert!(fix.contains("--no-seed-commands"), "{fix}");
assert!(fix.contains("allow_seed_commands"), "{fix}");
}
#[test]
fn no_command_seeds_means_no_note() {
assert!(!codes(&lint(&manifest(CLEAN_STAGE), &LintEnv::default())).contains(&"command-seed"));
}
#[test]
fn a_fully_reachable_graph_reports_nothing() {
let toml = manifest(
r#"
[stages.entry]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 5
entry = true
[stages.entry.transitions]
b = "true"
c = "true"
[stages.b]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 5
[stages.b.transitions]
d = "true"
[stages.c]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 5
[stages.c.transitions]
d = "true"
[stages.d]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 5
"#,
);
let findings = lint(&toml, &LintEnv::default());
assert!(findings.is_empty(), "{:?}", codes(&findings));
}
#[test]
fn fan_out_worker_and_merge_stages_are_reachable() {
let toml = manifest(
r#"
[stages.split]
mode = "fan_out"
worker_stage = "work"
merge_stage = "merge"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
entry = true
[stages.split.transitions]
[stages.work]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 5
allow_as_worker = true
[stages.merge]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 5
"#,
);
let findings = lint(&toml, &LintEnv::default());
assert!(findings.is_empty(), "{:?}", codes(&findings));
}
#[test]
fn a_stage_the_entry_cannot_reach_is_warned_about() {
let toml = manifest(
r#"
[stages.a]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 5
entry = true
[stages.a.transitions]
b = "true"
[stages.b]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 5
[stages.orphan]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 5
"#,
);
let findings = lint(&toml, &LintEnv::default());
assert_eq!(codes(&findings), ["unreachable-stage"]);
assert_eq!(findings[0].stage.as_deref(), Some("orphan"));
}
#[test]
fn a_cycle_with_no_revisit_cap_is_warned_about() {
let toml = manifest(
r#"
[stages.a]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 5
entry = true
[stages.a.transitions]
b = "true"
[stages.b]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 5
[stages.b.transitions]
a = "true"
"#,
);
let findings = lint(&toml, &LintEnv::default());
assert_eq!(
codes(&findings),
["cycle-without-max-revisits", "cycle-without-max-revisits"]
);
}
#[test]
fn a_cycle_with_max_revisits_is_fine() {
let toml = manifest(
r#"
[stages.a]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 5
max_revisits = 2
entry = true
[stages.a.transitions]
b = "true"
[stages.b]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 5
max_revisits = 3
[stages.b.transitions]
a = "true"
"#,
);
assert!(lint(&toml, &LintEnv::default()).is_empty());
}
#[test]
fn self_loops_and_terminal_stages_are_not_cycles() {
let toml = manifest(
r#"
[stages.a]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 5
entry = true
[stages.a.transitions]
a = "true"
b = "true"
[stages.b]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 5
[stages.b.transitions]
"#,
);
assert!(lint(&toml, &LintEnv::default()).is_empty());
}
#[test]
fn a_linear_blueprint_has_no_graph_findings() {
let toml = manifest(
r#"
[stages.a]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 5
[stages.b]
mode = "autonomous"
model = { models = [{ provider = "anthropic", model = "claude-sonnet-5" }] }
max_iterations = 5
"#,
);
assert!(lint(&toml, &LintEnv::default()).is_empty());
}
#[test]
fn the_graph_walk_steps_over_names_that_are_not_stages() {
use leviath_core::{Blueprint, ContextLayout, Stage, TransitionEdge};
let model = leviath_core::blueprint::ModelConfig::new(
"anthropic".to_string(),
"claude-sonnet-5".to_string(),
);
let mut only = Stage::new("a".to_string(), model.clone());
only.transitions = Some(HashMap::new());
only.max_iterations = Some(5);
let mut bp = Blueprint::new(
"t".to_string(),
"t".to_string(),
vec![only],
ContextLayout::new(Vec::new(), 1000),
);
bp.entry_stage = Some("ghost".to_string());
assert_eq!(
codes(&lint_manifest("", &bp, &LintEnv::default())),
["unreachable-stage"]
);
let mut dangling = Stage::new("a".to_string(), model);
dangling.max_iterations = Some(5);
dangling.transitions = Some(HashMap::from([(
"ghost".to_string(),
TransitionEdge {
target: "ghost".to_string(),
condition: Default::default(),
hint: None,
transform: Default::default(),
gate: None,
stuck: None,
},
)]));
let bp = Blueprint::new(
"t".to_string(),
"t".to_string(),
vec![dangling],
ContextLayout::new(Vec::new(), 1000),
);
assert!(lint_manifest("", &bp, &LintEnv::default()).is_empty());
}
#[test]
fn severity_labels_are_the_same_width() {
assert_eq!(
LintSeverity::Error.label().len(),
LintSeverity::Warning.label().len()
);
assert_eq!(LintSeverity::Error.label().trim(), "ERR");
assert_eq!(LintSeverity::Warning.label().trim(), "WARN");
}
#[test]
fn one_line_names_the_stage_when_there_is_one() {
let stageless = LintFinding::new(LintSeverity::Warning, "c", "something".to_string());
assert_eq!(stageless.one_line(), "something");
assert_eq!(
stageless.in_stage("main").one_line(),
"stage 'main': something"
);
}
#[test]
fn is_error_distinguishes_the_severities() {
assert!(LintFinding::new(LintSeverity::Error, "c", String::new()).is_error());
assert!(!LintFinding::new(LintSeverity::Warning, "c", String::new()).is_error());
}
#[test]
fn a_thoroughly_broken_stage_reports_each_defect_once() {
let toml = manifest(
r#"
[stages.scope]
available_tools = ["read_file", "bash", "ask_user_text", "raed_file"]
"#,
);
let env = LintEnv {
known_tools: known_tools(&["read_file", "bash", "shell", "ask_user_text"]),
..LintEnv::default()
};
let findings = lint(&toml, &env);
for code in [
"stage-missing-mode",
"stage-missing-model",
"stage-missing-max-iterations",
"unknown-tool",
"blocking-tool-in-autonomous-stage",
"implicit-shell-policy",
] {
assert_eq!(with_code(&findings, code).len(), 1, "{code}: {findings:?}");
}
assert_eq!(findings.iter().filter(|f| f.is_error()).count(), 1);
assert_eq!(findings.len(), 6, "{:?}", codes(&findings));
}