use pushkin_core::envelope::{CheckResult, Decision, Severity, Violation};
use pushkin_core::events::{EventLog, Telemetry};
use pushkin_core::waivers::WaiverSet;
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;
fn violation(rule: &str, file: &str) -> Violation {
Violation {
file: file.to_owned(),
line: 1,
rule: rule.to_owned(),
contract: None,
fix_hint: "fix".to_owned(),
suggestions: Vec::new(),
severity: Severity::Error,
}
}
fn block(rule: &str, file: &str) -> CheckResult {
CheckResult {
decision: Decision::Block,
violations: vec![violation(rule, file)],
duration_ms: 0.0,
}
}
fn now() -> Option<OffsetDateTime> {
OffsetDateTime::parse("2026-08-14T00:00:00Z", &Rfc3339).ok()
}
fn legacy_waiver_file(dir: &tempfile::TempDir, rule: &str) -> std::io::Result<std::path::PathBuf> {
let path = dir.path().join("waivers.toml");
let record = format!(
"[[waivers]]\n\
id = \"legacy-w1\"\n\
rule = \"{rule}\"\n\
path = \"app/api/**\"\n\
reason = \"granted pre-rename\"\n\
author = \"human\"\n\
granted_at = \"2026-08-01T00:00:00Z\"\n\
expires_at = \"2099-01-01T00:00:00Z\"\n"
);
std::fs::write(&path, record)?;
Ok(path)
}
#[test]
fn pre_rename_waiver_suppresses_the_renamed_rule() {
let dir = tempfile::tempdir().expect("tempdir");
let path = legacy_waiver_file(&dir, "sentinel.suppression.new").unwrap();
let set = WaiverSet::load(&path).expect("waiver file parses");
let result = set.apply(
block("pushkin.suppression.new", "app/api/users/route.ts"),
now().unwrap(),
);
assert_eq!(
result.decision,
Decision::Allow,
"a waiver granted pre-rename must suppress its matching deny post-rename"
);
assert!(result.violations.is_empty());
}
#[test]
fn pre_rename_protected_path_waiver_still_never_suppresses() {
let dir = tempfile::tempdir().expect("tempdir");
let path = legacy_waiver_file(&dir, "sentinel.protected_path").unwrap();
let set = WaiverSet::load(&path).expect("waiver file parses");
let result = set.apply(
block("pushkin.protected_path", "app/api/users/route.ts"),
now().unwrap(),
);
assert_eq!(
result.decision,
Decision::Block,
"the protected-path rule is unwaivable in either spelling"
);
}
fn insert_legacy_row(
db: &std::path::Path,
(session, seq): (&str, u64),
decision: &str,
rule: &str,
payload: &str,
) -> rusqlite::Result<usize> {
let conn = rusqlite::Connection::open(db)?;
conn.execute(
"INSERT INTO events (session, seq, ts, decision, rule, file, payload)
VALUES (?1, ?2, '2026-08-01T00:00:00Z', ?3, ?4, 'x.ts', ?5)",
(session, seq, decision, rule, payload),
)
}
#[test]
fn stats_group_mixed_rule_spellings_as_one_rule() {
let dir = tempfile::tempdir().expect("tempdir");
let db = dir.path().join("events.db");
let log = EventLog::open(&db).expect("log opens");
let session = log.begin_session().expect("session begins");
log.append(&session, &block("pushkin.board.claimed_path", "x.ts"))
.expect("modern block appends");
log.append_telemetry(
&session,
Telemetry {
rule: "pushkin.compression",
payload: r#"{"saved_chars": 5}"#.to_owned(),
},
)
.expect("modern telemetry appends");
insert_legacy_row(
&db,
("old-sess", 1),
"block",
"sentinel.board.claimed_path",
"{}",
)
.unwrap();
insert_legacy_row(
&db,
("old-sess", 2),
"telemetry",
"sentinel.compression",
r#"{"saved_chars": 10}"#,
)
.unwrap();
let stats = log.stats().expect("stats query");
assert_eq!(
stats.blocks_by_rule,
vec![("pushkin.board.claimed_path".to_owned(), 2)],
"one grouped row under the modern id, never two spellings"
);
assert_eq!(stats.compression_events, 2);
assert_eq!(stats.compression_saved_chars, 15);
}
#[test]
fn decision_counts_exclude_legacy_escalation_rows() {
let dir = tempfile::tempdir().expect("tempdir");
let db = dir.path().join("events.db");
let log = EventLog::open(&db).expect("log opens");
let session = log.begin_session().expect("session begins");
log.append(&session, &block("pushkin.board.claimed_path", "x.ts"))
.expect("modern block appends");
insert_legacy_row(
&db,
("old-sess", 1),
"block",
"sentinel.escalation",
r#"{"escalation": true}"#,
)
.unwrap();
let (checks, denials) = log.decision_counts().expect("counts query");
assert_eq!(
(checks, denials),
(1, 1),
"legacy escalation marker rows must not inflate the statusline counts"
);
}
#[test]
fn latest_session_escalation_recognizes_the_legacy_spelling() {
let dir = tempfile::tempdir().expect("tempdir");
let db = dir.path().join("events.db");
let log = EventLog::open(&db).expect("log opens");
insert_legacy_row(
&db,
("old-sess", 1),
"block",
"sentinel.escalation",
r#"{"escalation": true}"#,
)
.unwrap();
assert!(
log.latest_session_escalated()
.expect("escalation query runs"),
"an escalation recorded pre-rename is still an escalation"
);
}