#[allow(unused_imports)]
use crate::sync_util::LockExt;
use std::sync::{Arc, Mutex};
use super::message::{LoopMessage, UserMessage};
use super::result::LoopToolResult;
use super::types::GateMode;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VerificationStatus {
NoCodeEdited,
VerifiedGreen,
VerifiedRed,
Unverified,
FastGreenOnly,
}
pub const VERIFY_TAG: &str = "[verify-before-done]";
const VERIFY_NUDGE: &str = "[verify-before-done] You changed code this run but didn't run the tests or build to check it. Verify it works before reporting done — or, if there's nothing to run or you verified another way, say so briefly and finish. Don't re-edit just to look busy.";
const FAILED_NUDGE: &str = "[verify-before-done] Your last build or test command failed after you changed code. Don't report done on a red build — fix the failure. If it's pre-existing or expected, say so explicitly before finishing.";
const FULL_SUITE_NUDGE: &str = "[verify-before-done] Fast checks passed but the full test suite never ran this run. Run it once before reporting done — or, if there is no broader suite or you verified end-to-end another way, say so briefly and finish.";
const MAX_TIER_ESCALATIONS: u8 = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum VerificationTier {
Fast,
Slow,
}
#[derive(Debug)]
pub struct VerifierGate {
inner: Mutex<Inner>,
}
#[derive(Debug, Default)]
struct Inner {
edited_code: bool,
ran_verification: bool,
verification_failed: bool,
fired: bool,
ran_fast: bool,
ran_slow: bool,
edits_since_verify: u32,
escalations: u8,
ci_commands: Vec<String>,
project_gate: Option<GateSignature>,
ran_project_gate: bool,
}
impl Inner {
fn is_fast_green_only(&self) -> bool {
self.ran_fast && !self.ran_slow && !self.verification_failed
}
}
fn escalation_cap(mode: GateMode) -> u8 {
match mode {
GateMode::Off => 0,
GateMode::Advisory => 1,
GateMode::Blocking => MAX_TIER_ESCALATIONS,
}
}
impl VerifierGate {
#[allow(dead_code)]
pub fn new() -> Arc<Self> {
Arc::new(Self {
inner: Mutex::new(Inner::default()),
})
}
#[allow(dead_code)]
pub fn with_project_gate(project_gate: Option<String>) -> Arc<Self> {
Self::with_project_gate_and_ci(project_gate, Vec::new())
}
pub fn with_project_gate_and_ci(
project_gate: Option<String>,
ci_commands: Vec<String>,
) -> Arc<Self> {
Arc::new(Self {
inner: Mutex::new(Inner {
project_gate: project_gate.as_deref().and_then(gate_signature),
ci_commands,
..Inner::default()
}),
})
}
pub fn record_outcome(
&self,
tool_name: &str,
args: &serde_json::Value,
result: &LoopToolResult,
is_error: bool,
) {
let mut inner = self.inner.lock_ignore_poison();
match tool_name {
"write" | "edit" | "apply_patch" | "edit_minified" if touches_code_file(args) => {
inner.edited_code = true;
inner.edits_since_verify = inner
.edits_since_verify
.saturating_add(code_paths_touched(args).max(1));
}
"bash" => {
let command = args.get("command").and_then(|v| v.as_str()).unwrap_or("");
if is_verification_command(command) {
let failed = is_error || result_indicates_failure(result);
if masks_failure(command) && !failed {
return;
}
inner.ran_verification = true;
inner.verification_failed = failed;
inner.edits_since_verify = 0;
if !failed {
if let Some(gate) = &inner.project_gate
&& gate_signatures(command).iter().any(|s| s == gate)
{
inner.ran_project_gate = true;
}
match verification_tier(command) {
Some(VerificationTier::Fast) => inner.ran_fast = true,
Some(VerificationTier::Slow) => inner.ran_slow = true,
None => {}
}
}
}
}
_ => {}
}
}
pub fn status(&self, mode: GateMode) -> VerificationStatus {
let inner = self.inner.lock_ignore_poison();
if !inner.edited_code {
return VerificationStatus::NoCodeEdited;
}
if inner.ran_verification && inner.verification_failed {
return VerificationStatus::VerifiedRed;
}
if !inner.ran_verification {
return VerificationStatus::Unverified;
}
if mode != GateMode::Off && inner.edits_since_verify > 0 {
return VerificationStatus::Unverified;
}
if inner.project_gate.is_some() && !inner.ran_project_gate {
return VerificationStatus::FastGreenOnly;
}
if mode != GateMode::Off && inner.is_fast_green_only() {
return VerificationStatus::FastGreenOnly;
}
VerificationStatus::VerifiedGreen
}
pub fn edits_since_verify(&self) -> u32 {
self.inner.lock_ignore_poison().edits_since_verify
}
pub fn is_fresh_green(&self) -> bool {
let inner = self.inner.lock_ignore_poison();
inner.ran_verification && !inner.verification_failed && inner.edits_since_verify == 0
}
pub fn check_before_finalize(&self, mode: GateMode) -> Vec<LoopMessage> {
let mut inner = self.inner.lock_ignore_poison();
if !inner.edited_code {
return Vec::new();
}
if !inner.fired {
let nudge = if inner.verification_failed {
Some(FAILED_NUDGE)
} else if !inner.ran_verification {
Some(VERIFY_NUDGE)
} else {
None };
if let Some(text) = nudge {
inner.fired = true;
let hint = ci_hint(&inner.ci_commands);
return vec![LoopMessage::User(UserMessage::text(format!(
"{text}{hint}"
)))];
}
}
if inner.is_fast_green_only() && inner.escalations < escalation_cap(mode) {
inner.escalations += 1;
return vec![LoopMessage::User(UserMessage::text(FULL_SUITE_NUDGE))];
}
Vec::new()
}
}
pub fn ci_verification_commands(repo_root: &std::path::Path) -> Vec<String> {
let dir = repo_root.join(".github").join("workflows");
let Ok(entries) = std::fs::read_dir(&dir) else {
return Vec::new();
};
let mut files: Vec<std::path::PathBuf> = entries
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| {
matches!(
p.extension().and_then(|e| e.to_str()),
Some("yml") | Some("yaml")
)
})
.collect();
files.sort();
let mut out: Vec<String> = Vec::new();
for f in files {
let Ok(text) = std::fs::read_to_string(&f) else {
continue;
};
for cmd in run_step_commands(&text) {
if cmd.contains("${{") || !is_verification_command(&cmd) {
continue;
}
let sig = gate_signature(&cmd);
if out.iter().any(|existing| gate_signature(existing) == sig) {
continue;
}
out.push(cmd);
}
}
out
}
fn run_step_commands(text: &str) -> Vec<String> {
let mut out = Vec::new();
let mut block: Option<usize> = None; for line in text.lines() {
let indent = line.len() - line.trim_start().len();
let trimmed = line.trim_start();
if let Some(block_indent) = block {
if trimmed.is_empty() {
continue;
}
if indent > block_indent {
out.push(trimmed.to_string());
continue;
}
block = None;
}
let key = trimmed.strip_prefix("- ").unwrap_or(trimmed);
let Some(rest) = key.strip_prefix("run:") else {
continue;
};
let rest = rest.trim();
if rest == "|" || rest == ">" || rest == "|-" || rest == ">-" {
block = Some(indent);
} else if !rest.is_empty() {
out.push(rest.to_string());
}
}
out
}
pub fn ci_hint(commands: &[String]) -> String {
if commands.is_empty() {
return String::new();
}
let list = commands
.iter()
.map(|c| format!("`{c}`"))
.collect::<Vec<_>>()
.join(", ");
format!(
" This project's CI runs: {list} — a green check that isn't one of those may not be what gets enforced."
)
}
fn masks_failure(command: &str) -> bool {
let bytes = command.as_bytes();
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'|' => {
return true;
}
b';' => {
if command[i + 1..].trim().is_empty() {
return false;
}
return true;
}
b'&' => {
if bytes.get(i + 1) == Some(&b'&') {
i += 2;
continue;
}
if i > 0 && bytes[i - 1] == b'>' {
i += 1;
continue;
}
return true;
}
_ => {}
}
i += 1;
}
false
}
fn result_text(result: &LoopToolResult) -> String {
result
.content
.iter()
.filter_map(|b| b.get("text").and_then(|v| v.as_str()))
.collect::<Vec<_>>()
.join("\n")
}
fn result_indicates_failure(result: &LoopToolResult) -> bool {
result_text(result).lines().any(exit_code_line_is_failure)
}
fn exit_code_line_is_failure(line: &str) -> bool {
line.trim()
.strip_prefix("Exit code:")
.and_then(|rest| rest.trim().parse::<i64>().ok())
.is_some_and(|code| code != 0)
}
fn is_verification_command(command: &str) -> bool {
command
.split(['&', '|', ';', '\n'])
.any(segment_is_verification)
}
const WORD_MARKERS: &[&str] = &[
"test",
"build",
"check",
"lint",
"compile",
"cargo",
"npm",
"pnpm",
"yarn",
"pytest",
"tox",
"make",
"gradle",
"mvn",
"ctest",
"cmake",
"rustc",
"tsc",
"jest",
"vitest",
"mocha",
"clippy",
"eslint",
"golangci-lint",
"prettier",
"ruff",
"flake8",
"mypy",
"shellcheck",
"rubocop",
];
const NON_VERIFY: &[&str] = &["checkout", "install", "add", "remove", "uninstall"];
const FAST_LINTERS: &[&str] = &[
"eslint",
"ruff",
"mypy",
"flake8",
"shellcheck",
"rubocop",
"golangci-lint",
"prettier",
"clippy",
];
const FAST_SCRIPT_WORDS: &[&str] = &["lint", "check", "typecheck", "tsc", "format", "fmt"];
const CARGO_VALUE_FLAGS: &[&str] = &[
"-p",
"--package",
"--exclude",
"--features",
"-j",
"--jobs",
"--target",
"--manifest-path",
"--message-format",
"--profile",
"--bin",
"--test",
"--example",
];
pub fn verification_tier(command: &str) -> Option<VerificationTier> {
command
.split(['&', '|', ';', '\n'])
.filter(|segment| segment_is_verification(segment))
.map(segment_tier)
.max()
}
fn segment_tier(segment: &str) -> VerificationTier {
let owned: Vec<String> = segment
.split_whitespace()
.map(|t| t.to_ascii_lowercase())
.collect();
let tokens: Vec<&str> = owned
.iter()
.map(String::as_str)
.skip_while(|t| t.contains('='))
.collect();
let Some((command, args)) = tokens.split_first() else {
return VerificationTier::Slow;
};
let base = command.rsplit('/').next().unwrap_or(command);
match base {
"cargo" => cargo_tier(args),
"pytest" => pytest_tier(args),
"npm" | "pnpm" | "yarn" => node_script_tier(args),
"go" => go_tier(args),
"jest" | "vitest" | "mocha" => js_runner_tier(args),
"tsc" | "rustc" => VerificationTier::Fast,
_ if FAST_LINTERS.contains(&base) => VerificationTier::Fast,
_ => VerificationTier::Slow,
}
}
fn cargo_tier(args: &[&str]) -> VerificationTier {
let Some(sub) = args.iter().find(|t| !t.starts_with('-')) else {
return VerificationTier::Slow;
};
match *sub {
"check" | "clippy" | "fmt" => VerificationTier::Fast,
"test" | "bench" if cargo_has_filter(args) => VerificationTier::Fast,
_ => VerificationTier::Slow,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct GateSignature {
program: String,
subcommand: Option<String>,
}
fn shell_words(segment: &str) -> Option<Vec<String>> {
let mut words = Vec::new();
let mut current = String::new();
let mut quote: Option<char> = None;
let mut in_word = false;
for c in segment.chars() {
match quote {
Some(q) if c == q => quote = None,
Some(_) => current.push(c),
None => match c {
'"' | '\'' => {
quote = Some(c);
in_word = true;
}
c if c.is_whitespace() => {
if in_word {
words.push(std::mem::take(&mut current));
in_word = false;
}
}
c => {
current.push(c);
in_word = true;
}
},
}
}
if in_word {
words.push(current);
}
(!words.is_empty()).then_some(words)
}
fn segment_signature(segment: &str) -> Option<GateSignature> {
let words = shell_words(segment)?;
let mut rest = words.iter().skip_while(|w| w.contains('='));
let program = rest.next()?.clone();
let subcommand = rest.find(|w| !w.starts_with('-')).cloned();
Some(GateSignature {
program,
subcommand,
})
}
fn gate_signatures(command: &str) -> Vec<GateSignature> {
command
.split(['&', '|', ';', '\n'])
.filter_map(segment_signature)
.collect()
}
fn gate_signature(command: &str) -> Option<GateSignature> {
gate_signatures(command).pop()
}
fn cargo_has_filter(args: &[&str]) -> bool {
let head = match args.iter().position(|t| *t == "--") {
Some(i) => &args[..i],
None => args,
};
let mut rest = head.iter();
let mut seen_subcommand = false;
while let Some(token) = rest.next() {
if token.starts_with('-') {
if CARGO_VALUE_FLAGS.contains(token) {
rest.next();
}
continue;
}
if !seen_subcommand {
seen_subcommand = true;
continue;
}
return true;
}
false
}
fn pytest_tier(args: &[&str]) -> VerificationTier {
let targeted = args
.iter()
.any(|t| *t == "-k" || *t == "-m" || t.contains("::") || t.ends_with(".py"));
if targeted {
VerificationTier::Fast
} else {
VerificationTier::Slow
}
}
fn node_script_tier(args: &[&str]) -> VerificationTier {
let Some(first) = args.iter().find(|t| !t.starts_with('-')) else {
return VerificationTier::Slow;
};
if *first == "test" {
return VerificationTier::Slow;
}
let script = if *first == "run" {
args.iter()
.skip_while(|t| **t != "run")
.nth(1)
.copied()
.unwrap_or("")
} else {
*first
};
if FAST_SCRIPT_WORDS.iter().any(|w| script.contains(w)) {
VerificationTier::Fast
} else {
VerificationTier::Slow
}
}
fn go_tier(args: &[&str]) -> VerificationTier {
let Some(sub) = args.iter().find(|t| !t.starts_with('-')) else {
return VerificationTier::Slow;
};
match *sub {
"vet" => VerificationTier::Fast,
"run" => VerificationTier::Fast,
"test" if args.iter().any(|t| *t == "-run" || t.starts_with("-run=")) => {
VerificationTier::Fast
}
_ => VerificationTier::Slow,
}
}
fn js_runner_tier(args: &[&str]) -> VerificationTier {
let targeted = args.iter().any(|t| !t.starts_with('-') && *t != "run");
if targeted {
VerificationTier::Fast
} else {
VerificationTier::Slow
}
}
const PAIR_MARKERS: &[(&str, &str)] = &[("go", "vet"), ("go", "run"), ("go", "test")];
fn segment_is_verification(segment: &str) -> bool {
let tokens: Vec<String> = segment
.split_whitespace()
.map(|t| t.to_ascii_lowercase())
.collect();
if tokens.iter().any(|t| NON_VERIFY.contains(&t.as_str())) {
return false;
}
if tokens
.iter()
.any(|t| WORD_MARKERS.contains(&t.trim_start_matches('-')))
{
return true;
}
if tokens
.windows(2)
.any(|w| PAIR_MARKERS.contains(&(w[0].as_str(), w[1].as_str())))
{
return true;
}
command_word(&tokens).is_some_and(script_name_is_verification)
}
fn command_word(tokens: &[String]) -> Option<&str> {
tokens.iter().map(|t| t.as_str()).find(|t| !t.contains('='))
}
fn script_name_is_verification(token: &str) -> bool {
if !token.contains('/') {
return false;
}
let basename = token.rsplit('/').next().unwrap_or(token);
basename.split(['-', '_', '.']).any(|piece| {
WORD_MARKERS.contains(&piece)
|| piece
.strip_suffix('s')
.is_some_and(|p| WORD_MARKERS.contains(&p))
})
}
fn touches_code_file(args: &serde_json::Value) -> bool {
code_paths_touched(args) > 0
}
fn code_paths_touched(args: &serde_json::Value) -> u32 {
let Some(obj) = args.as_object() else {
return 0;
};
let mut paths: Vec<&str> = Vec::new();
for key in ["path", "file_path", "file"] {
if let Some(s) = obj.get(key).and_then(|v| v.as_str()) {
paths.push(s);
}
}
if let Some(ops) = obj.get("operations").and_then(|v| v.as_array()) {
for op in ops {
for key in ["path", "new_path"] {
if let Some(s) = op.get(key).and_then(|v| v.as_str()) {
paths.push(s);
}
}
}
}
paths.sort_unstable();
paths.dedup();
paths.iter().filter(|p| is_code_path(p)).count() as u32
}
const CODE_EXTS: &[&str] = &[
"rs", "py", "ts", "tsx", "js", "jsx", "mjs", "cjs", "go", "rb", "java", "kt", "kts", "c", "h",
"cc", "cpp", "hpp", "cxx", "cs", "swift", "php", "scala", "clj", "cljs", "cljc", "ex", "exs",
"sh", "bash", "lua", "pl", "hs", "ml", "sql", "vue", "svelte",
];
fn is_code_path(path: &str) -> bool {
match path.rsplit_once('.') {
Some((_, ext)) => CODE_EXTS.contains(&ext.to_ascii_lowercase().as_str()),
None => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn ok_result() -> LoopToolResult {
LoopToolResult {
content: vec![json!({"type": "text", "text": "ok"})],
details: json!(null),
terminate: None,
}
}
fn failed_result() -> LoopToolResult {
LoopToolResult {
content: vec![json!({"type": "text", "text": "test failed\nExit code: 101"})],
details: json!(null),
terminate: None,
}
}
fn nudge(gate: &VerifierGate) -> Option<String> {
gate.check_before_finalize(GateMode::Off)
.into_iter()
.next()
.map(|m| match m {
LoopMessage::User(u) => u.text_joined(),
_ => panic!("expected user message"),
})
}
#[test]
fn edited_code_without_running_nudges_to_verify() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/auth.rs"}), &ok_result(), false);
let n = nudge(&g).expect("should nudge");
assert!(n.contains("didn't run the tests"), "verify nudge: {n}");
}
#[test]
fn edit_minified_counts_as_a_code_edit() {
let g = VerifierGate::new();
g.record_outcome(
"edit_minified",
&json!({"path": "src/auth.rs"}),
&ok_result(),
false,
);
let n = nudge(&g).expect("edit_minified should arm the verify nudge");
assert!(n.contains("didn't run the tests"), "verify nudge: {n}");
}
#[test]
fn edited_code_then_passing_test_is_silent() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/auth.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo test"}),
&ok_result(),
false,
);
assert!(
nudge(&g).is_none(),
"passing verification should stay silent"
);
}
#[test]
fn edited_code_then_failing_test_nudges_to_fix() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/auth.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo test"}),
&failed_result(),
false,
);
let n = nudge(&g).expect("should nudge on red build");
assert!(n.contains("failed"), "fix-it nudge: {n}");
assert!(
n.contains("red build"),
"should mention not finishing on red: {n}"
);
}
#[test]
fn rerun_green_after_failure_clears_the_nudge() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/auth.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo test"}),
&failed_result(),
false,
);
g.record_outcome(
"bash",
&json!({"command": "cargo test"}),
&ok_result(),
false,
);
assert!(
nudge(&g).is_none(),
"a subsequent green run should clear the failure"
);
}
#[test]
fn non_verification_command_does_not_count_as_verified() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/auth.rs"}), &ok_result(), false);
g.record_outcome("bash", &json!({"command": "ls -la"}), &ok_result(), false);
let n = nudge(&g).expect("ls is not verification");
assert!(n.contains("didn't run the tests"));
}
#[test]
fn tool_execution_error_counts_as_failure() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/auth.rs"}), &ok_result(), false);
g.record_outcome("bash", &json!({"command": "make test"}), &ok_result(), true);
let n = nudge(&g).expect("errored verification is a failure");
assert!(n.contains("failed"));
}
#[test]
fn doc_only_edit_never_nudges() {
let g = VerifierGate::new();
g.record_outcome("write", &json!({"path": "README.md"}), &ok_result(), false);
assert!(nudge(&g).is_none());
}
#[test]
fn no_edits_never_nudges() {
let g = VerifierGate::new();
g.record_outcome("read", &json!({"path": "src/auth.rs"}), &ok_result(), false);
assert!(nudge(&g).is_none());
}
#[test]
fn nudge_fires_at_most_once() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/auth.rs"}), &ok_result(), false);
assert!(nudge(&g).is_some());
assert!(nudge(&g).is_none(), "bounded to once per run");
}
#[test]
fn apply_patch_with_code_operation_counts_as_edit() {
let g = VerifierGate::new();
g.record_outcome(
"apply_patch",
&json!({"operations": [{"type": "update", "path": "src/lib.rs"}]}),
&ok_result(),
false,
);
assert!(nudge(&g).is_some());
}
#[test]
fn status_reflects_run_signals() {
let g = VerifierGate::new();
assert_eq!(g.status(GateMode::Off), VerificationStatus::NoCodeEdited);
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
assert_eq!(g.status(GateMode::Off), VerificationStatus::Unverified);
g.record_outcome(
"bash",
&json!({"command": "cargo test"}),
&failed_result(),
false,
);
assert_eq!(g.status(GateMode::Off), VerificationStatus::VerifiedRed);
g.record_outcome(
"bash",
&json!({"command": "cargo test"}),
&ok_result(),
false,
);
assert_eq!(g.status(GateMode::Off), VerificationStatus::VerifiedGreen);
}
#[test]
fn status_does_not_consume_the_nudge() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
assert_eq!(g.status(GateMode::Off), VerificationStatus::Unverified);
let _ = g.status(GateMode::Off);
assert!(nudge(&g).is_some(), "status() must not arm `fired`");
}
#[test]
fn is_code_path_recognizes_common_extensions() {
assert!(is_code_path("src/main.rs"));
assert!(is_code_path("app/Foo.TS"));
assert!(!is_code_path("README.md"));
assert!(!is_code_path("Makefile"));
}
fn bash_result(text: &str) -> LoopToolResult {
LoopToolResult {
content: vec![json!({"type": "text", "text": text})],
details: json!(null),
terminate: None,
}
}
#[test]
fn echoed_exit_code_zero_is_not_a_failure() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "make test"}),
&bash_result("make test\nall passed\nExit code: 0"),
false,
);
assert_eq!(g.status(GateMode::Off), VerificationStatus::VerifiedGreen);
assert!(nudge(&g).is_none(), "echoed 'Exit code: 0' must stay green");
}
#[test]
fn exit_code_in_prose_is_not_a_failure() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo test"}),
&bash_result("the wrapper prints 'Exit code: N' on error\ndone"),
false,
);
assert_eq!(g.status(GateMode::Off), VerificationStatus::VerifiedGreen);
}
#[test]
fn harness_nonzero_marker_is_a_failure_anywhere() {
for text in ["boom\nExit code: 101", "Exit code: 137\nhead\ntail"] {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo test"}),
&bash_result(text),
false,
);
assert_eq!(
g.status(GateMode::Off),
VerificationStatus::VerifiedRed,
"non-zero marker in {text:?} should be red"
);
}
}
#[test]
fn non_build_subcommands_are_not_verification() {
for cmd in [
"git checkout main",
"npm install",
"cargo add serde",
"ls tests/",
"yarn add left-pad",
] {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome("bash", &json!({"command": cmd}), &ok_result(), false);
assert_eq!(
g.status(GateMode::Off),
VerificationStatus::Unverified,
"`{cmd}` must not count as verification"
);
}
}
#[test]
fn linters_and_scripts_count_as_verification() {
for cmd in [
"eslint .",
"golangci-lint run",
"prettier --check .",
"./run-tests.sh",
"scripts/lint.sh --fast",
] {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome("bash", &json!({"command": cmd}), &ok_result(), false);
assert_eq!(
g.status(GateMode::Off),
VerificationStatus::VerifiedGreen,
"`{cmd}` should register as verification"
);
}
}
#[test]
fn real_build_commands_still_count() {
for cmd in [
"cargo test",
"make check",
"npm run build",
"go vet ./...",
"pytest -q",
"RUST_LOG=debug cargo clippy",
] {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome("bash", &json!({"command": cmd}), &ok_result(), false);
assert_eq!(
g.status(GateMode::Off),
VerificationStatus::VerifiedGreen,
"`{cmd}` should register as verification"
);
}
}
#[test]
fn cargo_check_clippy_are_fast() {
for cmd in ["cargo check", "cargo clippy", "RUST_LOG=debug cargo clippy"] {
assert_eq!(
verification_tier(cmd),
Some(VerificationTier::Fast),
"`{cmd}`"
);
}
}
#[test]
fn cargo_test_with_filter_is_fast() {
for cmd in [
"cargo test my_test",
"cargo test verifier::tests::status_reflects_run_signals",
"cargo test foo -- --exact",
] {
assert_eq!(
verification_tier(cmd),
Some(VerificationTier::Fast),
"`{cmd}`"
);
}
}
#[test]
fn bare_cargo_test_is_slow() {
for cmd in [
"cargo test",
"cargo test --workspace",
"cargo test --all-features",
] {
assert_eq!(
verification_tier(cmd),
Some(VerificationTier::Slow),
"`{cmd}`"
);
}
}
#[test]
fn cargo_test_package_flag_value_is_not_a_filter() {
for cmd in [
"cargo test -p mycrate",
"cargo test --package mycrate",
"cargo test --features foo",
] {
assert_eq!(
verification_tier(cmd),
Some(VerificationTier::Slow),
"`{cmd}`"
);
}
}
#[test]
fn cargo_build_is_slow() {
for cmd in ["cargo build", "cargo build --release"] {
assert_eq!(
verification_tier(cmd),
Some(VerificationTier::Slow),
"`{cmd}`"
);
}
}
#[test]
fn pytest_targeting() {
for cmd in [
"pytest tests/foo.py::test_bar",
"pytest tests/foo.py",
"pytest -k bar",
] {
assert_eq!(
verification_tier(cmd),
Some(VerificationTier::Fast),
"`{cmd}`"
);
}
for cmd in ["pytest", "pytest -q", "pytest tests/"] {
assert_eq!(
verification_tier(cmd),
Some(VerificationTier::Slow),
"`{cmd}`"
);
}
}
#[test]
fn npm_tiers() {
assert_eq!(verification_tier("npm test"), Some(VerificationTier::Slow));
for cmd in [
"npm run lint",
"npm run typecheck",
"npm run check",
"pnpm run lint",
"yarn lint",
] {
assert_eq!(
verification_tier(cmd),
Some(VerificationTier::Fast),
"`{cmd}`"
);
}
for cmd in ["npm run build", "npm run deploy-docs"] {
assert_eq!(
verification_tier(cmd),
Some(VerificationTier::Slow),
"`{cmd}`"
);
}
}
#[test]
fn bare_linters_are_fast() {
for cmd in [
"eslint .",
"ruff check .",
"mypy src/",
"prettier --check .",
"shellcheck x.sh",
"golangci-lint run",
"flake8 src/",
"rubocop",
] {
assert_eq!(
verification_tier(cmd),
Some(VerificationTier::Fast),
"`{cmd}`"
);
}
}
#[test]
fn tsc_and_rustc_are_fast() {
for cmd in ["tsc --noEmit", "tsc"] {
assert_eq!(
verification_tier(cmd),
Some(VerificationTier::Fast),
"`{cmd}`"
);
}
}
#[test]
fn make_is_always_slow() {
for cmd in ["make", "make check", "make test"] {
assert_eq!(
verification_tier(cmd),
Some(VerificationTier::Slow),
"`{cmd}`"
);
}
}
#[test]
fn go_tiers() {
assert_eq!(
verification_tier("go vet ./..."),
Some(VerificationTier::Fast)
);
assert_eq!(
verification_tier("go test ./..."),
Some(VerificationTier::Slow)
);
assert_eq!(
verification_tier("go test -run TestFoo ./..."),
Some(VerificationTier::Fast)
);
}
#[test]
fn jest_vitest_mocha_tiers() {
for cmd in ["jest src/foo.test.ts", "vitest run foo"] {
assert_eq!(
verification_tier(cmd),
Some(VerificationTier::Fast),
"`{cmd}`"
);
}
for cmd in ["jest", "vitest run", "mocha"] {
assert_eq!(
verification_tier(cmd),
Some(VerificationTier::Slow),
"`{cmd}`"
);
}
}
#[test]
fn script_paths_default_slow() {
for cmd in ["./run-tests.sh", "scripts/lint.sh"] {
assert_eq!(
verification_tier(cmd),
Some(VerificationTier::Slow),
"`{cmd}`"
);
}
}
#[test]
fn ambiguous_verification_defaults_slow() {
for cmd in ["ctest", "gradle test", "mvn test", "tox", "make check"] {
assert_eq!(
verification_tier(cmd),
Some(VerificationTier::Slow),
"`{cmd}`"
);
}
}
#[test]
fn non_verification_has_no_tier() {
for cmd in [
"ls tests/",
"git checkout main",
"npm install",
"cargo add serde",
"yarn add left-pad",
] {
assert_eq!(verification_tier(cmd), None, "`{cmd}`");
}
}
#[test]
fn chain_tier_is_strongest_segment() {
assert_eq!(
verification_tier("cargo check && cargo test"),
Some(VerificationTier::Slow)
);
assert_eq!(
verification_tier("cargo check && cargo clippy"),
Some(VerificationTier::Fast)
);
assert_eq!(
verification_tier("cargo test || echo nope"),
Some(VerificationTier::Slow)
);
}
#[test]
fn off_mode_status_is_legacy() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo check"}),
&ok_result(),
false,
);
assert_eq!(g.status(GateMode::Off), VerificationStatus::VerifiedGreen);
}
#[test]
fn fast_only_is_fast_green_only_when_tiered() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo check"}),
&ok_result(),
false,
);
assert_eq!(
g.status(GateMode::Advisory),
VerificationStatus::FastGreenOnly
);
assert_eq!(
g.status(GateMode::Blocking),
VerificationStatus::FastGreenOnly
);
}
#[test]
fn slow_green_is_verified_green_when_tiered() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo check"}),
&ok_result(),
false,
);
g.record_outcome(
"bash",
&json!({"command": "cargo test"}),
&ok_result(),
false,
);
assert_eq!(
g.status(GateMode::Advisory),
VerificationStatus::VerifiedGreen
);
}
#[test]
fn red_any_tier_is_verified_red() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo clippy"}),
&failed_result(),
false,
);
assert_eq!(
g.status(GateMode::Advisory),
VerificationStatus::VerifiedRed
);
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo test"}),
&ok_result(),
false,
);
g.record_outcome(
"bash",
&json!({"command": "cargo clippy"}),
&failed_result(),
false,
);
assert_eq!(
g.status(GateMode::Advisory),
VerificationStatus::VerifiedRed
);
}
#[test]
fn rerun_slow_green_after_fast_red_clears() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo clippy"}),
&failed_result(),
false,
);
g.record_outcome(
"bash",
&json!({"command": "cargo test"}),
&ok_result(),
false,
);
assert_eq!(
g.status(GateMode::Advisory),
VerificationStatus::VerifiedGreen
);
}
#[test]
fn unverified_unchanged_in_all_modes() {
for mode in [GateMode::Off, GateMode::Advisory, GateMode::Blocking] {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
assert_eq!(g.status(mode), VerificationStatus::Unverified);
}
}
#[test]
fn green_goes_stale_when_edits_follow_it() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo test"}),
&ok_result(),
false,
);
assert_eq!(
g.status(GateMode::Advisory),
VerificationStatus::VerifiedGreen
);
g.record_outcome("edit", &json!({"path": "src/b.rs"}), &ok_result(), false);
assert_eq!(
g.status(GateMode::Advisory),
VerificationStatus::Unverified,
"a post-green edit is uncovered"
);
assert_eq!(
g.status(GateMode::Off),
VerificationStatus::VerifiedGreen,
"off mode keeps the legacy latched green"
);
g.record_outcome(
"bash",
&json!({"command": "cargo test"}),
&ok_result(),
false,
);
assert_eq!(
g.status(GateMode::Advisory),
VerificationStatus::VerifiedGreen
);
}
#[test]
fn doc_edit_after_green_does_not_go_stale() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo test"}),
&ok_result(),
false,
);
g.record_outcome("write", &json!({"path": "README.md"}), &ok_result(), false);
assert_eq!(
g.status(GateMode::Advisory),
VerificationStatus::VerifiedGreen
);
}
#[test]
fn edits_since_verify_counts_code_edits_only() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome("write", &json!({"path": "src/b.rs"}), &ok_result(), false);
g.record_outcome(
"apply_patch",
&json!({"operations": [{"type": "update", "path": "src/c.rs"}]}),
&ok_result(),
false,
);
g.record_outcome(
"edit_minified",
&json!({"path": "src/d.rs"}),
&ok_result(),
false,
);
g.record_outcome("write", &json!({"path": "README.md"}), &ok_result(), false);
g.record_outcome("read", &json!({"path": "src/e.rs"}), &ok_result(), false);
assert_eq!(g.edits_since_verify(), 4);
}
#[test]
fn batched_apply_patch_counts_each_code_file() {
let g = VerifierGate::new();
g.record_outcome(
"apply_patch",
&json!({"operations": [
{"type": "update", "path": "src/alpha.rs"},
{"type": "update", "path": "src/beta.rs"},
{"type": "update", "path": "src/gamma.rs"},
{"type": "update", "path": "src/delta.rs"},
]}),
&ok_result(),
false,
);
assert_eq!(g.edits_since_verify(), 4, "four files, not one call");
}
#[test]
fn batched_patch_ignores_docs_and_dedupes() {
let g = VerifierGate::new();
g.record_outcome(
"apply_patch",
&json!({"operations": [
{"type": "update", "path": "src/alpha.rs"},
{"type": "update", "path": "src/alpha.rs"},
{"type": "update", "path": "README.md"},
{"type": "update", "path": "Cargo.toml"},
]}),
&ok_result(),
false,
);
assert_eq!(g.edits_since_verify(), 1, "one distinct code file");
}
#[test]
fn single_file_edit_counts_once() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
assert_eq!(g.edits_since_verify(), 1);
}
#[test]
fn edits_since_verify_resets_on_any_verification() {
let g = VerifierGate::new();
for _ in 0..3 {
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
}
assert_eq!(g.edits_since_verify(), 3);
g.record_outcome(
"bash",
&json!({"command": "cargo check"}),
&ok_result(),
false,
);
assert_eq!(g.edits_since_verify(), 0);
}
#[test]
fn edits_since_verify_not_reset_by_non_verification_bash() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome("bash", &json!({"command": "ls -la"}), &ok_result(), false);
assert_eq!(g.edits_since_verify(), 2);
}
#[test]
fn is_fresh_green_only_when_verified_and_unedited_since() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
assert!(!g.is_fresh_green(), "edit alone is not green");
g.record_outcome(
"bash",
&json!({"command": "cargo test"}),
&ok_result(),
false,
);
assert!(
g.is_fresh_green(),
"passing test with no edits since is fresh green"
);
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
assert!(!g.is_fresh_green(), "edit after green makes it stale");
g.record_outcome(
"bash",
&json!({"command": "cargo test"}),
&failed_result(),
false,
);
assert!(!g.is_fresh_green(), "failing verification is not green");
}
fn tiered_nudge(gate: &VerifierGate, mode: GateMode) -> Option<String> {
gate.check_before_finalize(mode)
.into_iter()
.next()
.map(|m| match m {
LoopMessage::User(u) => u.text_joined(),
_ => panic!("expected user message"),
})
}
#[test]
fn off_mode_fast_only_finalize_is_silent() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo check"}),
&ok_result(),
false,
);
assert!(tiered_nudge(&g, GateMode::Off).is_none());
}
#[test]
fn advisory_fast_only_finalize_escalates_once() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo check"}),
&ok_result(),
false,
);
let n = tiered_nudge(&g, GateMode::Advisory).expect("fast-only should escalate");
assert!(n.contains(VERIFY_TAG), "escalation carries the tag: {n}");
assert!(n.contains("full test suite"), "names the full suite: {n}");
assert!(
tiered_nudge(&g, GateMode::Advisory).is_none(),
"advisory escalation is one-shot"
);
}
#[test]
fn blocking_fast_only_finalize_escalates_up_to_cap() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo check"}),
&ok_result(),
false,
);
assert!(tiered_nudge(&g, GateMode::Blocking).is_some());
assert!(tiered_nudge(&g, GateMode::Blocking).is_some());
assert!(
tiered_nudge(&g, GateMode::Blocking).is_none(),
"bounded by MAX_TIER_ESCALATIONS"
);
}
#[test]
fn slow_green_finalize_silent_all_modes() {
for mode in [GateMode::Advisory, GateMode::Blocking] {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo test"}),
&ok_result(),
false,
);
assert!(tiered_nudge(&g, mode).is_none());
}
}
#[test]
fn red_and_unverified_nudges_unchanged_by_mode() {
for mode in [GateMode::Off, GateMode::Advisory, GateMode::Blocking] {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo test"}),
&failed_result(),
false,
);
assert!(
tiered_nudge(&g, mode).is_some_and(|n| n.contains("failed")),
"red nudge in {mode:?}"
);
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
assert!(
tiered_nudge(&g, mode).is_some_and(|n| n.contains("didn't run the tests")),
"unverified nudge in {mode:?}"
);
}
}
#[test]
fn unverified_nudge_does_not_spend_the_escalation_budget() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
assert!(tiered_nudge(&g, GateMode::Advisory).is_some());
g.record_outcome(
"bash",
&json!({"command": "cargo check"}),
&ok_result(),
false,
);
let n = tiered_nudge(&g, GateMode::Advisory)
.expect("fast-green escalation still fires after the legacy nudge");
assert!(n.contains("full test suite"), "{n}");
assert!(tiered_nudge(&g, GateMode::Advisory).is_none());
}
#[test]
fn gate_signature_handles_env_prefix_and_flag_placement() {
assert_eq!(
gate_signature(r#"RUSTFLAGS="-D warnings" cargo clippy --all-targets"#),
Some(GateSignature {
program: "cargo".into(),
subcommand: Some("clippy".into()),
})
);
assert_eq!(
gate_signature("cargo clippy --all-targets -- -D warnings"),
Some(GateSignature {
program: "cargo".into(),
subcommand: Some("clippy".into()),
})
);
}
#[test]
fn gate_signature_quoted_env_values_and_flags_before_subcommand() {
assert_eq!(
gate_signature(r#"CC="ccache gcc" RUSTFLAGS="-D warnings" cargo clippy"#),
Some(GateSignature {
program: "cargo".into(),
subcommand: Some("clippy".into()),
})
);
assert_eq!(
gate_signature("cargo --locked --offline clippy --all-targets"),
Some(GateSignature {
program: "cargo".into(),
subcommand: Some("clippy".into()),
})
);
}
#[test]
fn gate_signature_bare_program_and_empty() {
assert_eq!(
gate_signature("make"),
Some(GateSignature {
program: "make".into(),
subcommand: None,
})
);
assert_eq!(gate_signature(""), None);
assert_eq!(gate_signature(" "), None);
}
#[test]
fn gate_signature_spec_chain_takes_last_segment() {
assert_eq!(
gate_signature("cargo check && cargo clippy --all-targets"),
Some(GateSignature {
program: "cargo".into(),
subcommand: Some("clippy".into()),
})
);
assert_eq!(
gate_signature("cargo fmt\ncargo clippy --all-targets"),
Some(GateSignature {
program: "cargo".into(),
subcommand: Some("clippy".into()),
})
);
}
#[test]
fn observed_chain_yields_every_segment_signature() {
let sigs = gate_signatures("cargo clippy --all-targets && cargo test");
assert_eq!(
sigs,
vec![
GateSignature {
program: "cargo".into(),
subcommand: Some("clippy".into()),
},
GateSignature {
program: "cargo".into(),
subcommand: Some("test".into()),
},
]
);
}
#[test]
fn gate_green_in_first_chain_segment_is_verified_green() {
let g = VerifierGate::with_project_gate(Some("cargo clippy".into()));
g.record_outcome("edit", &json!({"path": "src/auth.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo clippy --all-targets && cargo test"}),
&ok_result(),
false,
);
assert_eq!(g.status(GateMode::Off), VerificationStatus::VerifiedGreen);
}
#[test]
fn configured_gate_run_green_is_verified_green() {
let g = VerifierGate::with_project_gate(Some(
r#"RUSTFLAGS="-D warnings" cargo clippy --all-targets"#.into(),
));
g.record_outcome("edit", &json!({"path": "src/auth.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": r#"RUSTFLAGS="-D warnings" cargo clippy --all-targets"#}),
&ok_result(),
false,
);
assert_eq!(g.status(GateMode::Off), VerificationStatus::VerifiedGreen);
}
#[test]
fn configured_gate_not_run_downgrades_green_to_fast_only() {
let g = VerifierGate::with_project_gate(Some("cargo clippy --all-targets".into()));
g.record_outcome("edit", &json!({"path": "src/auth.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo test"}),
&ok_result(),
false,
);
assert_eq!(g.status(GateMode::Off), VerificationStatus::FastGreenOnly);
}
#[test]
fn failing_gate_run_never_sets_ran_project_gate() {
let g = VerifierGate::with_project_gate(Some("cargo clippy --all-targets".into()));
g.record_outcome("edit", &json!({"path": "src/auth.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo clippy --all-targets"}),
&failed_result(),
false,
);
assert_eq!(g.status(GateMode::Off), VerificationStatus::VerifiedRed);
g.record_outcome(
"bash",
&json!({"command": "cargo test"}),
&ok_result(),
true,
);
assert_eq!(g.status(GateMode::Off), VerificationStatus::VerifiedRed);
}
#[test]
fn unconfigured_gate_keeps_off_mode_byte_identical() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/auth.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo test"}),
&ok_result(),
false,
);
assert_eq!(g.status(GateMode::Off), VerificationStatus::VerifiedGreen);
}
fn scratch(tag: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"dirge-ci-{tag}-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn wf(dir: &std::path::Path, name: &str, body: &str) {
let w = dir.join(".github").join("workflows");
std::fs::create_dir_all(&w).unwrap();
std::fs::write(w.join(name), body).unwrap();
}
#[test]
fn no_github_dir_yields_nothing() {
let t = scratch("nogh");
assert!(ci_verification_commands(t.as_path()).is_empty());
}
#[test]
fn one_line_and_block_scalar_run_steps_both_parse() {
let t = scratch("block");
wf(
t.as_path(),
"ci.yml",
"jobs:\n a:\n steps:\n - run: cargo clippy --all-targets\n \n b:\n steps:\n - run: |\n cargo nextest run\n",
);
let got = ci_verification_commands(t.as_path());
assert!(got.iter().any(|c| c.contains("clippy")), "{got:?}");
assert!(got.iter().any(|c| c.contains("nextest")), "{got:?}");
}
#[test]
fn non_verification_steps_are_ignored() {
let t = scratch("nonverif");
wf(
t.as_path(),
"ci.yml",
"steps:\n - run: actions/checkout@v4\n - run: echo hello\n - run: cargo clippy\n",
);
let got = ci_verification_commands(t.as_path());
assert_eq!(got.len(), 1, "only the real check survives: {got:?}");
assert!(got[0].contains("clippy"));
}
#[test]
fn interpolated_commands_are_skipped() {
let t = scratch("interp");
wf(
t.as_path(),
"ci.yml",
"steps:\n - run: cargo build ${{ matrix.features }}\n - run: cargo clippy\n",
);
let got = ci_verification_commands(t.as_path());
assert_eq!(got.len(), 1, "{got:?}");
assert!(got[0].contains("clippy"));
}
#[test]
fn same_signature_is_reported_once() {
let t = scratch("samesig");
wf(
t.as_path(),
"ci.yml",
"steps:\n - run: cargo clippy --all-targets -- -D warnings\n \n - run: cargo clippy --features sandbox-microvm --all-targets -- -D warnings\n",
);
assert_eq!(ci_verification_commands(t.as_path()).len(), 1);
}
#[test]
fn this_repos_real_ci_names_clippy() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let got = ci_verification_commands(root);
assert!(
got.iter().any(|c| c.contains("clippy")),
"must surface the gate the motivating incident missed: {got:?}"
);
let hint = ci_hint(&got);
assert!(hint.contains("clippy"), "{hint}");
assert!(hint.contains("CI runs"), "{hint}");
}
#[test]
fn no_ci_commands_leaves_the_nudge_unchanged() {
assert_eq!(ci_hint(&[]), "");
let g = VerifierGate::with_project_gate_and_ci(None, Vec::new());
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
let n = nudge(&g).expect("nudge fires");
assert_eq!(n, VERIFY_NUDGE, "no CI → the original text, exactly");
}
#[test]
fn nudge_names_the_ci_commands() {
let g = VerifierGate::with_project_gate_and_ci(
None,
vec!["cargo clippy --all-targets -- -D warnings".to_string()],
);
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
let n = nudge(&g).expect("nudge fires");
assert!(n.starts_with(VERIFY_NUDGE), "original text is preserved");
assert!(n.contains("cargo clippy"), "and CI is named: {n}");
}
#[test]
fn masking_shapes_are_detected() {
for cmd in [
"cargo clippy --all-targets | tail -2",
"cargo test || true",
"cargo test || echo ignored",
"cargo test; echo done",
"cargo test &",
"cargo clippy 2>&1 | head -20",
] {
assert!(masks_failure(cmd), "should be detected as masking: {cmd}");
}
}
#[test]
fn non_masking_shapes_are_not_flagged() {
for cmd in [
"cargo clippy --all-targets -- -D warnings",
"cargo fmt --all --check && cargo clippy --all-targets",
"cargo test 2>&1",
"RUSTFLAGS=\"-D warnings\" cargo clippy --all-targets",
"cargo test;",
] {
assert!(!masks_failure(cmd), "must not be flagged: {cmd}");
}
}
#[test]
fn masked_success_does_not_latch_green() {
for cmd in [
"cargo test || true",
"cargo clippy --all-targets | tail -2",
"cargo test; echo done",
] {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome("bash", &json!({"command": cmd}), &ok_result(), false);
assert_eq!(
g.status(GateMode::Off),
VerificationStatus::Unverified,
"a masked success proves nothing and must not read as green: {cmd}"
);
}
}
#[test]
fn masked_failure_is_still_recorded_as_red() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo test | tail -2"}),
&failed_result(),
false,
);
assert_eq!(g.status(GateMode::Off), VerificationStatus::VerifiedRed);
}
#[test]
fn unmasked_success_still_latches_green() {
let g = VerifierGate::new();
g.record_outcome("edit", &json!({"path": "src/a.rs"}), &ok_result(), false);
g.record_outcome(
"bash",
&json!({"command": "cargo clippy --all-targets -- -D warnings"}),
&ok_result(),
false,
);
assert_eq!(g.status(GateMode::Off), VerificationStatus::VerifiedGreen);
}
#[test]
fn masked_command_does_not_clear_edits_since_verify() {
let g = VerifierGate::new();
for i in 0..3 {
g.record_outcome(
"edit",
&json!({ "path": format!("src/f{i}.rs") }),
&ok_result(),
false,
);
}
let before = g.edits_since_verify();
g.record_outcome(
"bash",
&json!({"command": "cargo test || true"}),
&ok_result(),
false,
);
assert_eq!(
g.edits_since_verify(),
before,
"a masked check did not verify anything, so the counter must stand"
);
}
}