use crate::brain::tools::plan_tool::{
CriteriaPolicy, CriteriaVerdict, RalphVerification, TaskTypeCommands, VerificationOutcome,
commands_for_type, criteria_verdict, truncate_output, verify_with,
};
fn cmds(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
fn fake<'a>(
table: Vec<(&'static str, i32, &'static str)>,
log: &'a mut Vec<String>,
) -> impl FnMut(&str) -> (i32, String) + 'a {
move |cmd: &str| {
log.push(cmd.to_string());
table
.iter()
.find(|(c, _, _)| *c == cmd)
.map(|(_, code, out)| (*code, out.to_string()))
.unwrap_or((0, String::new()))
}
}
#[test]
fn all_commands_passing_accepts_the_task() {
let mut log = Vec::new();
let mut run = fake(vec![("cargo test", 0, "ok")], &mut log);
assert!(verify_with("build", 1, &cmds(&["cargo test"]), true, &mut run).is_ok());
}
#[test]
fn a_nonzero_exit_rejects_the_task() {
let mut log = Vec::new();
let mut run = fake(
vec![("cargo clippy", 101, "error: mismatched types")],
&mut log,
);
let err = verify_with("build", 3, &cmds(&["cargo clippy"]), true, &mut run)
.expect_err("a failing command must reject");
assert!(err.contains("REJECTED"), "{err}");
}
#[test]
fn the_rejection_names_the_command_the_code_and_the_task() {
let mut log = Vec::new();
let mut run = fake(
vec![("cargo clippy", 101, "error: mismatched types")],
&mut log,
);
let err = verify_with("build", 7, &cmds(&["cargo clippy"]), true, &mut run).unwrap_err();
assert!(err.contains("cargo clippy"), "missing command: {err}");
assert!(err.contains("101"), "missing exit code: {err}");
assert!(err.contains("#7"), "missing task order: {err}");
assert!(err.contains("mismatched types"), "missing output: {err}");
}
#[test]
fn no_commands_means_nothing_to_verify() {
let mut log = Vec::new();
let mut run = fake(vec![], &mut log);
assert!(verify_with("docs", 1, &[], true, &mut run).is_ok());
}
#[test]
fn require_all_runs_every_command_and_reports_all_failures() {
let mut log = Vec::new();
{
let mut run = fake(
vec![("a", 1, "a failed"), ("b", 0, ""), ("c", 2, "c failed")],
&mut log,
);
let err = verify_with("build", 1, &cmds(&["a", "b", "c"]), true, &mut run).unwrap_err();
assert!(err.contains("a failed"), "first failure missing: {err}");
assert!(err.contains("c failed"), "later failure missing: {err}");
}
assert_eq!(
log,
vec!["a", "b", "c"],
"require_all must not short-circuit"
);
}
#[test]
fn without_require_all_the_first_failure_short_circuits() {
let mut log = Vec::new();
{
let mut run = fake(vec![("fast", 1, "fast failed"), ("slow", 0, "")], &mut log);
assert!(verify_with("build", 1, &cmds(&["fast", "slow"]), false, &mut run).is_err());
}
assert_eq!(log, vec!["fast"], "the remaining commands must not run");
}
#[test]
fn without_require_all_a_pass_still_continues_to_the_next() {
let mut log = Vec::new();
{
let mut run = fake(vec![("first", 0, ""), ("second", 1, "boom")], &mut log);
assert!(verify_with("build", 1, &cmds(&["first", "second"]), false, &mut run).is_err());
}
assert_eq!(log, vec!["first", "second"]);
}
#[test]
fn a_runner_error_is_a_failure_not_a_pass() {
let mut log = Vec::new();
let mut run = fake(vec![("missing-binary", -1, "Failed to execute")], &mut log);
assert!(verify_with("build", 1, &cmds(&["missing-binary"]), true, &mut run).is_err());
}
fn verification(enabled: bool, pairs: &[(&str, &[&str])]) -> RalphVerification {
RalphVerification {
enabled,
require_all_pass: true,
criteria_policy: Default::default(),
task_type_commands: pairs
.iter()
.map(|(t, c)| TaskTypeCommands {
task_type: t.to_string(),
commands: cmds(c),
})
.collect(),
}
}
#[test]
fn the_matching_task_type_selects_its_commands() {
let v = verification(
true,
&[("build", &["cargo clippy"]), ("test", &["cargo test"])],
);
assert_eq!(
commands_for_type(&v, "build"),
Some(cmds(&["cargo clippy"]))
);
assert_eq!(commands_for_type(&v, "test"), Some(cmds(&["cargo test"])));
}
#[test]
fn task_type_matching_is_case_insensitive() {
let v = verification(true, &[("build", &["cargo clippy"])]);
assert_eq!(
commands_for_type(&v, "BUILD"),
Some(cmds(&["cargo clippy"]))
);
}
#[test]
fn an_unmapped_task_type_verifies_nothing() {
let v = verification(true, &[("build", &["cargo clippy"])]);
assert_eq!(commands_for_type(&v, "docs"), None);
}
#[test]
fn an_empty_command_list_verifies_nothing() {
let v = verification(true, &[("build", &[])]);
assert_eq!(commands_for_type(&v, "build"), None);
}
#[test]
fn disabling_verification_turns_the_gate_off_entirely() {
let v = verification(false, &[("build", &["cargo clippy"])]);
assert_eq!(commands_for_type(&v, "build"), None);
}
#[test]
fn short_output_is_untouched() {
assert_eq!(truncate_output("brief", 500), "brief");
}
#[test]
fn long_output_is_truncated_and_marked() {
let long = "x".repeat(600);
let out = truncate_output(&long, 500);
assert!(out.ends_with("...[truncated]"));
assert!(out.len() < long.len() + 20);
}
#[test]
fn truncation_does_not_split_a_multibyte_character() {
for filler in ["─", "é", "🦀", "…"] {
let noisy = filler.repeat(400);
let out = truncate_output(&noisy, 500);
assert!(
out.ends_with("...[truncated]"),
"truncation failed for {filler:?}"
);
}
}
#[test]
fn truncating_at_an_exact_boundary_is_stable() {
let exact = "y".repeat(500);
assert_eq!(
truncate_output(&exact, 500),
exact,
"no marker when it fits"
);
}
#[test]
fn strict_rejects_an_unverified_claim_against_criteria() {
assert_eq!(
criteria_verdict(
CriteriaPolicy::Strict,
true,
VerificationOutcome::NotConfigured
),
CriteriaVerdict::Reject
);
}
#[test]
fn downgrade_accepts_but_flags_an_unverified_claim() {
assert_eq!(
criteria_verdict(
CriteriaPolicy::Downgrade,
true,
VerificationOutcome::NotConfigured
),
CriteriaVerdict::Downgrade
);
}
#[test]
fn off_keeps_the_pre_870_behaviour() {
assert_eq!(
criteria_verdict(
CriteriaPolicy::Off,
true,
VerificationOutcome::NotConfigured
),
CriteriaVerdict::Accept
);
}
#[test]
fn a_proven_completion_accepts_under_every_policy() {
for policy in [
CriteriaPolicy::Downgrade,
CriteriaPolicy::Strict,
CriteriaPolicy::Off,
] {
assert_eq!(
criteria_verdict(policy, true, VerificationOutcome::Verified),
CriteriaVerdict::Accept,
"policy {policy:?} must accept a proven completion"
);
}
}
#[test]
fn a_disabled_gate_accepts_even_under_strict() {
assert_eq!(
criteria_verdict(CriteriaPolicy::Strict, true, VerificationOutcome::Disabled),
CriteriaVerdict::Accept
);
}
#[test]
fn no_criteria_means_nothing_to_enforce() {
for policy in [
CriteriaPolicy::Downgrade,
CriteriaPolicy::Strict,
CriteriaPolicy::Off,
] {
for outcome in [
VerificationOutcome::Verified,
VerificationOutcome::NotConfigured,
VerificationOutcome::Disabled,
] {
assert_eq!(
criteria_verdict(policy, false, outcome),
CriteriaVerdict::Accept,
"policy {policy:?} / outcome {outcome:?} must accept a criteria-less task"
);
}
}
}
#[test]
fn criteria_policy_defaults_to_downgrade_when_absent() {
let v: RalphVerification = toml::from_str("enabled = true").expect("valid TOML");
assert_eq!(v.criteria_policy, CriteriaPolicy::Downgrade);
}
#[test]
fn criteria_policy_parses_each_variant_lowercase() {
let strict: RalphVerification =
toml::from_str("enabled = true\ncriteria_policy = \"strict\"").expect("valid TOML");
assert_eq!(strict.criteria_policy, CriteriaPolicy::Strict);
let off: RalphVerification =
toml::from_str("enabled = true\ncriteria_policy = \"off\"").expect("valid TOML");
assert_eq!(off.criteria_policy, CriteriaPolicy::Off);
let downgrade: RalphVerification =
toml::from_str("enabled = true\ncriteria_policy = \"downgrade\"").expect("valid TOML");
assert_eq!(downgrade.criteria_policy, CriteriaPolicy::Downgrade);
}