use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandClass {
CatLike,
GrepLike,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Decision {
Allow,
Deny { file_key: String, reason: String },
AlreadyConsulted { context: String },
Advisory { context: String },
Liability { staleness: f32, context: String },
Tombstone,
NoRecord,
NotFileRead,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HookEvent {
Hit { key: String },
Miss { key: String },
BlockedUnconsultedRead { key: String },
CodexShellBlocked { key: String },
ComplianceHit { key: String },
EditConsulted { key: String },
EditBlocked { key: String },
FloorConsultBlocked { key: String },
}
pub struct EnforcementInput {
pub rel_path: String,
pub file_record: Option<serde_json::Value>,
pub gotcha_records: HashMap<String, serde_json::Value>,
pub already_consulted: bool,
}
pub struct EnforcementResult {
pub decision: Decision,
pub events: Vec<HookEvent>,
}
const CAT_LIKE: &[&str] = &["cat", "less", "head", "tail", "bat"];
const GREP_LIKE: &[&str] = &["grep", "egrep", "fgrep", "rg", "sed", "awk"];
fn matches_command_word(trimmed: &str, word: &str) -> bool {
if trimmed.len() < word.len() {
return false;
}
if !trimmed.starts_with(word) {
return false;
}
if trimmed.len() == word.len() {
return true;
}
trimmed.as_bytes()[word.len()].is_ascii_whitespace()
}
const PREFIX_WORDS: &[&str] = &[
"sudo", "doas", "env", "nice", "ionice", "nohup", "setsid", "stdbuf", "command", "time",
];
fn is_env_assignment(tok: &str) -> bool {
match tok.find('=') {
Some(eq) if eq > 0 => {
let name = &tok[..eq];
name.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
&& name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
}
_ => false,
}
}
fn effective_command(cmd: &str) -> String {
let mut rest = cmd.trim_start();
loop {
let end = rest.find(char::is_whitespace).unwrap_or(rest.len());
let tok = &rest[..end];
if tok.is_empty() {
break;
}
if is_env_assignment(tok) || PREFIX_WORDS.contains(&tok) {
rest = rest[end..].trim_start();
continue;
}
break;
}
let end = rest.find(char::is_whitespace).unwrap_or(rest.len());
let (word, args) = rest.split_at(end);
let base = word.rsplit('/').next().unwrap_or(word);
let mut out = String::with_capacity(base.len() + args.len());
out.push_str(base);
out.push_str(args);
out
}
pub fn classify_command(cmd: &str) -> Option<CommandClass> {
let eff = effective_command(cmd);
let trimmed = eff.as_str();
for &word in CAT_LIKE {
if matches_command_word(trimmed, word) {
return Some(CommandClass::CatLike);
}
}
for &word in GREP_LIKE {
if matches_command_word(trimmed, word) {
return Some(CommandClass::GrepLike);
}
}
None
}
pub fn extract_file_path(cmd: &str, class: CommandClass) -> Option<String> {
let paths = extract_file_paths(cmd, class);
match class {
CommandClass::CatLike => paths.into_iter().next(),
CommandClass::GrepLike => paths.into_iter().next_back(),
}
}
pub fn extract_file_paths(cmd: &str, class: CommandClass) -> Vec<String> {
let eff = effective_command(cmd);
let cmd_part = split_at_shell_operator(&eff);
let positionals = positional_args(&shell_tokens(cmd_part));
match class {
CommandClass::CatLike => positionals,
CommandClass::GrepLike => {
if positionals.len() >= 2 {
positionals[1..].to_vec()
} else {
Vec::new()
}
}
}
}
fn split_at_shell_operator(s: &str) -> &str {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'|' => {
return &s[..i];
}
b';' => return &s[..i],
b'&' if i + 1 < bytes.len() && bytes[i + 1] == b'&' => {
return &s[..i];
}
b'"' => {
i += 1;
while i < bytes.len() && bytes[i] != b'"' {
i += 1;
}
}
b'\'' => {
i += 1;
while i < bytes.len() && bytes[i] != b'\'' {
i += 1;
}
}
_ => {}
}
i += 1;
}
s
}
fn shell_tokens(s: &str) -> Vec<String> {
let mut tokens = Vec::new();
let mut cur = String::new();
let mut in_token = false;
let mut chars = s.chars();
while let Some(c) = chars.next() {
match c {
'\'' | '"' => {
in_token = true;
let quote = c;
for q in chars.by_ref() {
if q == quote {
break;
}
cur.push(q);
}
}
c if c.is_whitespace() => {
if in_token {
tokens.push(std::mem::take(&mut cur));
in_token = false;
}
}
c => {
in_token = true;
cur.push(c);
}
}
}
if in_token {
tokens.push(cur);
}
tokens
}
fn positional_args(tokens: &[String]) -> Vec<String> {
let mut args = Vec::new();
let mut prev_was_flag = false;
for t in tokens.iter().skip(1) {
if t.starts_with('-') {
prev_was_flag = true;
continue;
}
if prev_was_flag && !t.is_empty() && t.bytes().all(|b| b.is_ascii_digit()) {
prev_was_flag = false;
continue;
}
prev_was_flag = false;
if !t.is_empty() {
args.push(t.clone());
}
}
args
}
pub const MAX_APPLY_PATCH_FILES: usize = 50;
pub fn extract_apply_patch_files(patch: &str) -> Vec<String> {
const MARKERS: &[&str] = &[
"*** Update File: ",
"*** Add File: ",
"*** Delete File: ",
"*** Move to: ",
];
let mut files: Vec<String> = Vec::new();
for line in patch.lines() {
for marker in MARKERS {
if let Some(rest) = line.strip_prefix(marker) {
let path = rest.trim();
if !path.is_empty() && !files.iter().any(|f| f == path) {
files.push(path.to_string());
}
break;
}
}
}
files
}
pub fn normalize_path(file_path: &str, repo_root: Option<&str>) -> String {
let stripped = match repo_root {
Some(root) => file_path
.strip_prefix(root)
.and_then(|s| s.strip_prefix('/'))
.unwrap_or(file_path),
None => file_path,
};
let mut components: Vec<&str> = Vec::new();
for part in stripped.split('/') {
match part {
"" | "." => continue,
".." => {
if components.pop().is_none() {
return stripped.to_string();
}
}
c => components.push(c),
}
}
if components.is_empty() {
".".to_string()
} else {
components.join("/")
}
}
pub fn evaluate(input: &EnforcementInput) -> EnforcementResult {
let file_key = format!("file:{}", input.rel_path);
let file_record = match &input.file_record {
Some(r) if r.is_object() => r,
_ => {
return EnforcementResult {
decision: Decision::NoRecord,
events: vec![HookEvent::Miss { key: file_key }],
};
}
};
let confidence = json_f32(file_record, "/confidence/value");
let quality = json_f32(file_record, "/quality/value");
let staleness = json_f32(file_record, "/staleness/value");
let staleness_tier = json_str(file_record, "/staleness/tier");
if staleness_tier == "tombstone" {
return EnforcementResult {
decision: Decision::Tombstone,
events: vec![],
};
}
if staleness_tier == "liability" {
return EnforcementResult {
decision: Decision::Liability {
staleness,
context: format!(
"WARNING: STALE record for {} is a liability (staleness {:.2}). \
Read the file directly — the cached record is too stale to trust.",
input.rel_path, staleness
),
},
events: vec![HookEvent::Hit { key: file_key }],
};
}
let purpose = json_str(file_record, "/value");
let mut context_lines: Vec<String> = Vec::new();
if !purpose.is_empty() {
context_lines.push(format!("Purpose: {purpose}"));
}
let mut deny_signal = false;
let gotcha_keys = json_string_array(file_record, "/payload/gotcha_keys");
for gkey in &gotcha_keys {
let grec = match input.gotcha_records.get(gkey.as_str()) {
Some(r) if r.is_object() => r,
_ => continue,
};
let confirmed = json_bool(grec, "/payload/confirmed");
let gconfidence = json_f32(grec, "/confidence/value");
let gquality = json_f32(grec, "/quality/value");
let rule = json_str(grec, "/value");
if confirmed && gconfidence >= 0.6 && gquality >= 0.4 {
deny_signal = true;
if !rule.is_empty() {
context_lines.push(format!("\u{26a0} {rule}"));
}
}
}
if staleness >= 0.4 {
context_lines.push(format!(
"Warning: record staleness {staleness:.2} — verify critical details."
));
}
{
let blast_tier = json_str(file_record, "/payload/blast_radius/tier");
if blast_tier == "high" || blast_tier == "critical" {
let blast_direct = file_record
.pointer("/payload/blast_radius/direct")
.and_then(|v| v.as_u64())
.unwrap_or(0);
context_lines.push(format!(
"\u{26a0} Blast radius: {blast_direct} direct importers ({blast_tier}) — modify carefully"
));
}
}
if deny_signal {
if input.already_consulted {
let context = if context_lines.is_empty() {
format!(
"Gotcha exists for {} — proceed with awareness",
input.rel_path
)
} else {
context_lines.join("\n")
};
return EnforcementResult {
decision: Decision::AlreadyConsulted { context },
events: vec![HookEvent::ComplianceHit { key: file_key }],
};
}
let safe_path = &input.rel_path;
let staleness_note = if staleness >= 0.4 {
format!(" (staleness {staleness:.2} — verify critical details)")
} else {
String::new()
};
return EnforcementResult {
decision: Decision::Deny {
file_key: file_key.clone(),
reason: format!(
"[mati] Confirmed gotcha on {safe_path} — \
call mem_get(\"file:{safe_path}\") and read the record \
before accessing this file.{staleness_note}"
),
},
events: vec![HookEvent::BlockedUnconsultedRead { key: file_key }],
};
}
if confidence >= 0.3 && quality >= 0.4 {
let context = if context_lines.is_empty() {
format!(
"Record exists for {} — confidence {confidence:.2}",
input.rel_path
)
} else {
context_lines.join("\n")
};
return EnforcementResult {
decision: Decision::Advisory { context },
events: vec![HookEvent::Hit { key: file_key }],
};
}
EnforcementResult {
decision: Decision::Allow,
events: vec![],
}
}
fn json_f32(val: &serde_json::Value, pointer: &str) -> f32 {
val.pointer(pointer)
.and_then(|v| v.as_f64())
.map(|f| f as f32)
.unwrap_or(0.0)
}
fn json_str(val: &serde_json::Value, pointer: &str) -> String {
val.pointer(pointer)
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string()
}
fn json_bool(val: &serde_json::Value, pointer: &str) -> bool {
val.pointer(pointer)
.and_then(|v| v.as_bool())
.unwrap_or(false)
}
fn json_string_array(val: &serde_json::Value, pointer: &str) -> Vec<String> {
val.pointer(pointer)
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn apply_patch_single_update() {
let patch =
"*** Begin Patch\n*** Update File: src/main.rs\n@@\n-old\n+new\n*** End Patch\n";
assert_eq!(extract_apply_patch_files(patch), vec!["src/main.rs"]);
}
#[test]
fn apply_patch_multi_file_add_update_delete() {
let patch = "*** Begin Patch\n\
*** Update File: src/a.rs\n@@\n+x\n\
*** Add File: src/b.rs\n+y\n\
*** Delete File: src/c.rs\n\
*** End Patch\n";
assert_eq!(
extract_apply_patch_files(patch),
vec!["src/a.rs", "src/b.rs", "src/c.rs"]
);
}
#[test]
fn apply_patch_rename_includes_source_and_destination() {
let patch =
"*** Begin Patch\n*** Update File: src/old.rs\n*** Move to: src/new.rs\n@@\n+x\n*** End Patch\n";
assert_eq!(
extract_apply_patch_files(patch),
vec!["src/old.rs", "src/new.rs"]
);
}
#[test]
fn apply_patch_ignores_marker_inside_diff_body() {
let patch = "*** Begin Patch\n\
*** Update File: src/real.rs\n@@\n\
+*** Update File: src/fake.rs\n\
+ *** Add File: src/also_fake.rs\n\
*** End Patch\n";
assert_eq!(extract_apply_patch_files(patch), vec!["src/real.rs"]);
}
#[test]
fn apply_patch_dedups_repeated_path() {
let patch =
"*** Begin Patch\n*** Update File: src/a.rs\n*** Update File: src/a.rs\n*** End Patch\n";
assert_eq!(extract_apply_patch_files(patch), vec!["src/a.rs"]);
}
#[test]
fn apply_patch_empty_or_no_markers() {
assert!(extract_apply_patch_files("").is_empty());
assert!(extract_apply_patch_files("just some text\nno markers here").is_empty());
assert!(extract_apply_patch_files("*** Begin Patch\n*** End Patch\n").is_empty());
}
#[test]
fn apply_patch_trims_trailing_whitespace() {
let patch = "*** Update File: src/spaced.rs \n";
assert_eq!(extract_apply_patch_files(patch), vec!["src/spaced.rs"]);
}
#[test]
fn classify_cat() {
assert_eq!(
classify_command("cat src/main.rs"),
Some(CommandClass::CatLike)
);
}
#[test]
fn classify_head_with_flag() {
assert_eq!(
classify_command("head -n 10 file.rs"),
Some(CommandClass::CatLike)
);
}
#[test]
fn classify_leading_whitespace() {
assert_eq!(classify_command(" cat file"), Some(CommandClass::CatLike));
}
#[test]
fn classify_less() {
assert_eq!(
classify_command("less README.md"),
Some(CommandClass::CatLike)
);
}
#[test]
fn classify_tail() {
assert_eq!(
classify_command("tail -f log.txt"),
Some(CommandClass::CatLike)
);
}
#[test]
fn classify_bat() {
assert_eq!(
classify_command("bat src/lib.rs"),
Some(CommandClass::CatLike)
);
}
#[test]
fn classify_grep() {
assert_eq!(
classify_command("grep -rn pattern src/"),
Some(CommandClass::GrepLike)
);
}
#[test]
fn classify_rg() {
assert_eq!(
classify_command("rg TODO src/"),
Some(CommandClass::GrepLike)
);
}
#[test]
fn classify_sed() {
assert_eq!(
classify_command("sed -i 's/a/b/' file.rs"),
Some(CommandClass::GrepLike)
);
}
#[test]
fn classify_awk() {
assert_eq!(
classify_command("awk '{print $1}' file.rs"),
Some(CommandClass::GrepLike)
);
}
#[test]
fn classify_ls_is_none() {
assert_eq!(classify_command("ls -la"), None);
}
#[test]
fn classify_cd_is_none() {
assert_eq!(classify_command("cd /tmp"), None);
}
#[test]
fn classify_catch_is_none() {
assert_eq!(classify_command("catch errors"), None);
}
#[test]
fn classify_catalog_is_none() {
assert_eq!(classify_command("catalog"), None);
}
#[test]
fn classify_grep_bare_is_none() {
assert_eq!(classify_command("grep"), Some(CommandClass::GrepLike));
}
#[test]
fn extract_cat_simple() {
assert_eq!(
extract_file_path("cat src/main.rs", CommandClass::CatLike),
Some("src/main.rs".into())
);
}
#[test]
fn extract_cat_with_flag() {
assert_eq!(
extract_file_path("cat -n src/main.rs", CommandClass::CatLike),
Some("src/main.rs".into())
);
}
#[test]
fn extract_cat_quoted_path() {
assert_eq!(
extract_file_path(r#"cat "path with spaces/file.rs""#, CommandClass::CatLike),
Some("path with spaces/file.rs".into())
);
}
#[test]
fn extract_cat_with_pipe() {
assert_eq!(
extract_file_path("cat file.rs | grep foo", CommandClass::CatLike),
Some("file.rs".into())
);
}
#[test]
fn extract_cat_with_semicolon() {
assert_eq!(
extract_file_path("cat file.rs; echo done", CommandClass::CatLike),
Some("file.rs".into())
);
}
#[test]
fn extract_cat_with_and() {
assert_eq!(
extract_file_path("cat file.rs && echo ok", CommandClass::CatLike),
Some("file.rs".into())
);
}
#[test]
fn extract_grep_last_arg() {
assert_eq!(
extract_file_path("grep -rn pattern src/main.rs", CommandClass::GrepLike),
Some("src/main.rs".into())
);
}
#[test]
fn extract_grep_quoted_file() {
assert_eq!(
extract_file_path(r#"grep pattern "src/main.rs""#, CommandClass::GrepLike),
Some("src/main.rs".into())
);
}
#[test]
fn extract_grep_strips_single_quotes() {
assert_eq!(
extract_file_path("grep 'pattern' file.rs", CommandClass::GrepLike),
Some("file.rs".into())
);
}
#[test]
fn extract_no_args() {
assert_eq!(extract_file_path("cat", CommandClass::CatLike), None);
}
#[test]
fn extract_only_flags() {
assert_eq!(extract_file_path("cat -n -v", CommandClass::CatLike), None);
}
#[test]
fn normalize_strips_prefix() {
assert_eq!(
normalize_path("/home/user/project/src/main.rs", Some("/home/user/project")),
"src/main.rs"
);
}
#[test]
fn normalize_dot_slash() {
assert_eq!(normalize_path("./src/main.rs", None), "src/main.rs");
}
#[test]
fn normalize_dotdot() {
assert_eq!(normalize_path("src/../src/main.rs", None), "src/main.rs");
}
#[test]
fn normalize_already_relative() {
assert_eq!(normalize_path("src/main.rs", None), "src/main.rs");
}
#[test]
fn normalize_no_repo_root() {
assert_eq!(
normalize_path("/abs/path/file.rs", None),
"abs/path/file.rs"
);
}
#[test]
fn normalize_trailing_slash_root() {
assert_eq!(
normalize_path("/project/src/file.rs", Some("/project")),
"src/file.rs"
);
}
#[test]
fn normalize_leading_dotdot_returns_unchanged() {
assert_eq!(normalize_path("../other/file.rs", None), "../other/file.rs");
}
#[test]
fn normalize_deep_dotdot_escape_returns_unchanged() {
assert_eq!(normalize_path("foo/../../bar.rs", None), "foo/../../bar.rs");
}
#[test]
fn normalize_dotdot_within_scope_ok() {
assert_eq!(normalize_path("src/../lib/file.rs", None), "lib/file.rs");
}
fn make_file_record(
confidence: f32,
quality: f32,
staleness: f32,
staleness_tier: &str,
gotcha_keys: &[&str],
) -> serde_json::Value {
json!({
"value": "Test file purpose",
"confidence": { "value": confidence },
"quality": { "value": quality },
"staleness": { "value": staleness, "tier": staleness_tier },
"payload": {
"gotcha_keys": gotcha_keys,
}
})
}
fn make_gotcha(confirmed: bool, confidence: f32, quality: f32) -> serde_json::Value {
json!({
"value": "Do not use unwrap here",
"confidence": { "value": confidence },
"quality": { "value": quality },
"payload": { "confirmed": confirmed }
})
}
#[test]
fn eval_no_record() {
let input = EnforcementInput {
rel_path: "src/main.rs".into(),
file_record: None,
gotcha_records: HashMap::new(),
already_consulted: false,
};
let result = evaluate(&input);
assert_eq!(result.decision, Decision::NoRecord);
assert_eq!(result.events.len(), 1);
assert!(matches!(&result.events[0], HookEvent::Miss { key } if key == "file:src/main.rs"));
}
#[test]
fn eval_tombstone() {
let input = EnforcementInput {
rel_path: "src/old.rs".into(),
file_record: Some(make_file_record(0.8, 0.5, 0.95, "tombstone", &[])),
gotcha_records: HashMap::new(),
already_consulted: false,
};
let result = evaluate(&input);
assert_eq!(result.decision, Decision::Tombstone);
assert!(result.events.is_empty());
}
#[test]
fn eval_liability() {
let input = EnforcementInput {
rel_path: "src/stale.rs".into(),
file_record: Some(make_file_record(0.8, 0.5, 0.85, "liability", &[])),
gotcha_records: HashMap::new(),
already_consulted: false,
};
let result = evaluate(&input);
assert!(
matches!(&result.decision, Decision::Liability { staleness, .. } if *staleness > 0.8)
);
assert_eq!(result.events.len(), 1);
assert!(matches!(&result.events[0], HookEvent::Hit { .. }));
}
#[test]
fn eval_confirmed_gotcha_denies() {
let mut gotchas = HashMap::new();
gotchas.insert("gotcha:test".to_string(), make_gotcha(true, 0.7, 0.5));
let input = EnforcementInput {
rel_path: "src/main.rs".into(),
file_record: Some(make_file_record(0.7, 0.5, 0.1, "fresh", &["gotcha:test"])),
gotcha_records: gotchas,
already_consulted: false,
};
let result = evaluate(&input);
assert!(matches!(&result.decision, Decision::Deny { .. }));
assert!(matches!(
&result.events[0],
HookEvent::BlockedUnconsultedRead { key } if key == "file:src/main.rs"
));
}
#[test]
fn eval_unconfirmed_gotcha_allows() {
let mut gotchas = HashMap::new();
gotchas.insert("gotcha:test".to_string(), make_gotcha(false, 0.7, 0.5));
let input = EnforcementInput {
rel_path: "src/main.rs".into(),
file_record: Some(make_file_record(0.7, 0.5, 0.1, "fresh", &["gotcha:test"])),
gotcha_records: gotchas,
already_consulted: false,
};
let result = evaluate(&input);
match &result.decision {
Decision::Advisory { context } => assert!(
!context.contains("Do not use unwrap here"),
"unconfirmed gotcha rule leaked into injected context: {context:?}"
),
other => panic!("expected Advisory, got {other:?}"),
}
}
#[test]
fn eval_low_confidence_gotcha_allows() {
let mut gotchas = HashMap::new();
gotchas.insert("gotcha:test".to_string(), make_gotcha(true, 0.4, 0.5));
let input = EnforcementInput {
rel_path: "src/main.rs".into(),
file_record: Some(make_file_record(0.7, 0.5, 0.1, "fresh", &["gotcha:test"])),
gotcha_records: gotchas,
already_consulted: false,
};
let result = evaluate(&input);
assert!(matches!(&result.decision, Decision::Advisory { .. }));
}
#[test]
fn eval_low_quality_gotcha_allows() {
let mut gotchas = HashMap::new();
gotchas.insert("gotcha:test".to_string(), make_gotcha(true, 0.7, 0.2));
let input = EnforcementInput {
rel_path: "src/main.rs".into(),
file_record: Some(make_file_record(0.7, 0.5, 0.1, "fresh", &["gotcha:test"])),
gotcha_records: gotchas,
already_consulted: false,
};
let result = evaluate(&input);
assert!(matches!(&result.decision, Decision::Advisory { .. }));
}
#[test]
fn eval_consulted_downgrades_deny() {
let mut gotchas = HashMap::new();
gotchas.insert("gotcha:test".to_string(), make_gotcha(true, 0.7, 0.5));
let input = EnforcementInput {
rel_path: "src/main.rs".into(),
file_record: Some(make_file_record(0.7, 0.5, 0.1, "fresh", &["gotcha:test"])),
gotcha_records: gotchas,
already_consulted: true,
};
let result = evaluate(&input);
assert!(matches!(
&result.decision,
Decision::AlreadyConsulted { .. }
));
assert!(matches!(&result.events[0], HookEvent::ComplianceHit { .. }));
}
#[test]
fn eval_medium_confidence_advisory() {
let input = EnforcementInput {
rel_path: "src/main.rs".into(),
file_record: Some(make_file_record(0.45, 0.5, 0.1, "fresh", &[])),
gotcha_records: HashMap::new(),
already_consulted: false,
};
let result = evaluate(&input);
assert!(matches!(&result.decision, Decision::Advisory { .. }));
assert!(matches!(&result.events[0], HookEvent::Hit { .. }));
}
#[test]
fn eval_low_everything_allows() {
let input = EnforcementInput {
rel_path: "src/main.rs".into(),
file_record: Some(make_file_record(0.1, 0.1, 0.1, "fresh", &[])),
gotcha_records: HashMap::new(),
already_consulted: false,
};
let result = evaluate(&input);
assert_eq!(result.decision, Decision::Allow);
assert!(result.events.is_empty());
}
#[test]
fn eval_staleness_warning_appended() {
let input = EnforcementInput {
rel_path: "src/main.rs".into(),
file_record: Some(make_file_record(0.5, 0.5, 0.5, "stale", &[])),
gotcha_records: HashMap::new(),
already_consulted: false,
};
let result = evaluate(&input);
if let Decision::Advisory { context } = &result.decision {
assert!(context.contains("staleness 0.50"));
} else {
panic!("expected Advisory, got {:?}", result.decision);
}
}
#[test]
fn eval_multiple_gotchas_one_deny() {
let mut gotchas = HashMap::new();
gotchas.insert("gotcha:safe".to_string(), make_gotcha(false, 0.7, 0.5));
gotchas.insert("gotcha:danger".to_string(), make_gotcha(true, 0.8, 0.6));
let input = EnforcementInput {
rel_path: "src/main.rs".into(),
file_record: Some(make_file_record(
0.7,
0.5,
0.1,
"fresh",
&["gotcha:safe", "gotcha:danger"],
)),
gotcha_records: gotchas,
already_consulted: false,
};
let result = evaluate(&input);
assert!(matches!(&result.decision, Decision::Deny { .. }));
}
#[test]
fn eval_deny_includes_staleness_note() {
let mut gotchas = HashMap::new();
gotchas.insert("gotcha:test".to_string(), make_gotcha(true, 0.7, 0.5));
let input = EnforcementInput {
rel_path: "src/main.rs".into(),
file_record: Some(make_file_record(0.7, 0.5, 0.5, "stale", &["gotcha:test"])),
gotcha_records: gotchas,
already_consulted: false,
};
let result = evaluate(&input);
if let Decision::Deny { reason, .. } = &result.decision {
assert!(reason.contains("staleness"));
} else {
panic!("expected Deny");
}
}
#[test]
fn eval_invalid_json_allows() {
let input = EnforcementInput {
rel_path: "src/main.rs".into(),
file_record: Some(json!("not an object")),
gotcha_records: HashMap::new(),
already_consulted: false,
};
let result = evaluate(&input);
assert_eq!(result.decision, Decision::NoRecord);
}
#[test]
fn eval_never_produces_fail_open() {
let cases: Vec<EnforcementInput> = vec![
EnforcementInput {
rel_path: "x".into(),
file_record: None,
gotcha_records: HashMap::new(),
already_consulted: false,
},
EnforcementInput {
rel_path: "x".into(),
file_record: Some(json!(null)),
gotcha_records: HashMap::new(),
already_consulted: false,
},
EnforcementInput {
rel_path: "x".into(),
file_record: Some(json!({})),
gotcha_records: HashMap::new(),
already_consulted: false,
},
];
for input in cases {
let result = evaluate(&input);
assert!(matches!(
result.decision,
Decision::Allow
| Decision::Deny { .. }
| Decision::AlreadyConsulted { .. }
| Decision::Advisory { .. }
| Decision::Liability { .. }
| Decision::Tombstone
| Decision::NoRecord
| Decision::NotFileRead
));
}
}
#[test]
fn eval_context_includes_purpose_and_rules() {
let mut gotchas = HashMap::new();
gotchas.insert("gotcha:test".to_string(), make_gotcha(true, 0.7, 0.5));
let input = EnforcementInput {
rel_path: "src/main.rs".into(),
file_record: Some(make_file_record(0.7, 0.5, 0.1, "fresh", &["gotcha:test"])),
gotcha_records: gotchas,
already_consulted: true,
};
let result = evaluate(&input);
if let Decision::AlreadyConsulted { context } = &result.decision {
assert!(context.contains("Purpose: Test file purpose"));
assert!(context.contains("Do not use unwrap here"));
} else {
panic!("expected AlreadyConsulted, got {:?}", result.decision);
}
}
#[test]
fn eval_blast_radius_warning_for_critical_file() {
let mut file_record = make_file_record(0.5, 0.5, 0.1, "fresh", &[]);
file_record
.as_object_mut()
.unwrap()
.get_mut("payload")
.unwrap()
.as_object_mut()
.unwrap()
.insert(
"blast_radius".into(),
json!({ "direct": 45, "transitive": 10, "score": 48.0, "tier": "critical" }),
);
let input = EnforcementInput {
rel_path: "src/core.rs".into(),
file_record: Some(file_record),
gotcha_records: HashMap::new(),
already_consulted: false,
};
let result = evaluate(&input);
if let Decision::Advisory { context } = &result.decision {
assert!(
context.contains("Blast radius"),
"advisory context must include blast radius warning, got: {context}"
);
assert!(context.contains("45"), "warning must include direct count");
assert!(context.contains("critical"), "warning must include tier");
} else {
panic!("expected Advisory, got {:?}", result.decision);
}
}
#[test]
fn eval_no_blast_warning_for_low_file() {
let mut file_record = make_file_record(0.5, 0.5, 0.1, "fresh", &[]);
file_record
.as_object_mut()
.unwrap()
.get_mut("payload")
.unwrap()
.as_object_mut()
.unwrap()
.insert(
"blast_radius".into(),
json!({ "direct": 2, "transitive": 0, "score": 2.0, "tier": "low" }),
);
let input = EnforcementInput {
rel_path: "src/leaf.rs".into(),
file_record: Some(file_record),
gotcha_records: HashMap::new(),
already_consulted: false,
};
let result = evaluate(&input);
if let Decision::Advisory { context } = &result.decision {
assert!(
!context.contains("Blast radius"),
"low blast radius file should NOT have warning, got: {context}"
);
} else {
panic!("expected Advisory, got {:?}", result.decision);
}
}
#[test]
fn classify_strips_sudo_prefix() {
assert_eq!(
classify_command("sudo cat src/secret.rs"),
Some(CommandClass::CatLike)
);
}
#[test]
fn classify_strips_env_assignment_prefix() {
assert_eq!(
classify_command("env LOG=1 cat src/secret.rs"),
Some(CommandClass::CatLike)
);
assert_eq!(
classify_command("LOG=1 DEBUG=2 cat src/secret.rs"),
Some(CommandClass::CatLike)
);
}
#[test]
fn classify_reduces_absolute_path_to_basename() {
assert_eq!(
classify_command("/bin/cat src/secret.rs"),
Some(CommandClass::CatLike)
);
}
#[test]
fn classify_prefix_on_non_read_stays_none() {
assert_eq!(classify_command("sudo rm -rf build"), None);
assert_eq!(classify_command("env X=1 ls"), None);
}
#[test]
fn extract_through_sudo_prefix() {
assert_eq!(
extract_file_path("sudo cat src/secret.rs", CommandClass::CatLike),
Some("src/secret.rs".to_string())
);
}
#[test]
fn extract_through_abs_path() {
assert_eq!(
extract_file_path("/bin/cat src/secret.rs", CommandClass::CatLike),
Some("src/secret.rs".to_string())
);
}
#[test]
fn extract_skips_numeric_flag_value() {
assert_eq!(
extract_file_path("tail -n 100 src/secret.rs", CommandClass::CatLike),
Some("src/secret.rs".to_string())
);
assert_eq!(
extract_file_path("head -c 5 src/secret.rs", CommandClass::CatLike),
Some("src/secret.rs".to_string())
);
}
#[test]
fn extract_keeps_attached_numeric_flag() {
assert_eq!(
extract_file_path("head -5 src/db.rs", CommandClass::CatLike),
Some("src/db.rs".to_string())
);
}
#[test]
fn sudo_with_flags_is_a_known_gap() {
assert_eq!(classify_command("sudo -u root cat src/secret.rs"), None);
}
#[test]
fn extract_grep_quoted_pattern_picks_the_path() {
assert_eq!(
extract_file_path("grep -r \"secret\" src/db.rs", CommandClass::GrepLike),
Some("src/db.rs".to_string())
);
assert_eq!(
extract_file_path("grep \"pat\" \"src/db.rs\"", CommandClass::GrepLike),
Some("src/db.rs".to_string())
);
}
#[test]
fn extract_grep_without_file_reads_stdin() {
assert_eq!(
extract_file_path("grep \"secret\"", CommandClass::GrepLike),
None
);
}
#[test]
fn extract_cat_quoted_path_with_spaces() {
assert_eq!(
extract_file_path("cat \"src/with space.rs\"", CommandClass::CatLike),
Some("src/with space.rs".to_string())
);
}
#[test]
fn shell_tokens_honor_quotes() {
assert_eq!(
shell_tokens("grep -r \"a b\" file.rs"),
vec!["grep", "-r", "a b", "file.rs"]
);
assert_eq!(
shell_tokens("awk '{print $1}' src/db.rs"),
vec!["awk", "{print $1}", "src/db.rs"]
);
}
#[test]
fn extract_file_paths_cat_returns_all_files() {
assert_eq!(
extract_file_paths("cat src/a.rs src/b.rs", CommandClass::CatLike),
vec!["src/a.rs", "src/b.rs"]
);
assert_eq!(
extract_file_paths("cat -n src/only.rs", CommandClass::CatLike),
vec!["src/only.rs"]
);
}
#[test]
fn extract_file_paths_grep_drops_the_pattern() {
assert_eq!(
extract_file_paths("grep -i secret src/a.rs src/b.rs", CommandClass::GrepLike),
vec!["src/a.rs", "src/b.rs"]
);
assert!(extract_file_paths("grep secret", CommandClass::GrepLike).is_empty());
}
#[test]
fn extract_file_path_is_the_primary_of_paths() {
assert_eq!(
extract_file_path("cat a.rs b.rs", CommandClass::CatLike).as_deref(),
Some("a.rs")
);
assert_eq!(
extract_file_path("grep pat f1 f2", CommandClass::GrepLike).as_deref(),
Some("f2")
);
}
}