use assert_cmd::Command;
use std::fs;
use std::path::Path;
const MANIFEST: &str = r#"
version = 1
canonical = "json-schema-2020-12"
authoring = "zod"
[[contracts]]
name = "user"
source = "contracts/user.zod.ts"
emit = ["zod"]
[[mappings]]
glob = "app/api/**/*.ts"
contracts = ["user"]
require = "boundary-validation"
[gates]
suppression_comments = "deny"
protected_paths = ["pushkin.toml"]
"#;
const NONCONFORMING: &str = "export async function POST(req: Request) {\n \
const body = await req.json();\n return Response.json({ name: body.name });\n}\n";
const CONFORMING: &str = "import { UserSchema } from \"../../generated/user.zod.gen\";\n\
export async function POST(req: Request) {\n \
const body = UserSchema.parse(await req.json());\n \
return Response.json({ name: body.name });\n}\n";
fn repo() -> Option<tempfile::TempDir> {
let dir = tempfile::tempdir().ok()?;
fs::write(dir.path().join("pushkin.toml"), MANIFEST).ok()?;
Some(dir)
}
fn hook_write(dir: &Path, session: &str, content: &str) -> Option<()> {
let payload = serde_json::json!({
"session_id": session,
"tool_name": "Write",
"tool_input": { "file_path": "app/api/users/route.ts", "content": content }
})
.to_string();
Command::cargo_bin("pushkin")
.ok()?
.args(["hook", "claude"])
.current_dir(dir)
.write_stdin(payload)
.output()
.ok()?;
Some(())
}
fn run_verb(dir: &Path, args: &[&str]) -> Option<(String, i32)> {
let output = Command::cargo_bin("pushkin")
.ok()?
.args(args)
.current_dir(dir)
.output()
.ok()?;
Some((
String::from_utf8_lossy(&output.stdout).into_owned(),
output.status.code().unwrap_or(-1),
))
}
#[test]
fn report_counts_denials_by_rule_and_mtc() {
let dir = repo().unwrap();
hook_write(dir.path(), "session-mtc", NONCONFORMING).unwrap();
hook_write(dir.path(), "session-mtc", NONCONFORMING).unwrap();
hook_write(dir.path(), "session-mtc", CONFORMING).unwrap();
let (stdout, code) = run_verb(dir.path(), &["report"]).unwrap();
assert_eq!(code, 0, "report must exit 0: {stdout}");
assert!(
stdout.contains("boundary-validation") && stdout.contains('2'),
"denials by rule must show 2 boundary-validation denials: {stdout}"
);
assert!(
stdout.to_lowercase().contains("mean-time-to-compliance")
|| stdout.to_lowercase().contains("attempts to fix"),
"report must surface the MTC metric: {stdout}"
);
}
#[test]
fn report_lists_active_waivers() {
let dir = repo().unwrap();
let out = Command::cargo_bin("pushkin")
.unwrap()
.args([
"waive",
"boundary-validation",
"--path",
"app/api/**",
"--ttl",
"2h",
"--reason",
"hotfix window",
])
.env("GIT_AUTHOR_NAME", "Report Tester")
.current_dir(dir.path())
.output()
.unwrap();
assert!(out.status.success());
let (stdout, _) = run_verb(dir.path(), &["report"]).unwrap();
assert!(
stdout.contains("hotfix window") && stdout.contains("app/api/**"),
"active waivers must appear with scope and reason: {stdout}"
);
}
#[test]
fn statusline_compact_segment_counts_checks_denials_waivers() {
let dir = repo().unwrap();
hook_write(dir.path(), "session-sl", NONCONFORMING).unwrap();
hook_write(dir.path(), "session-sl", CONFORMING).unwrap();
let (stdout, code) = run_verb(dir.path(), &["statusline"]).unwrap();
assert_eq!(code, 0);
let line = stdout.trim();
assert!(
line.starts_with("⛨ pushkin"),
"segment must open with the pushkin mark: {line}"
);
assert!(
line.contains("2 checks") && line.contains("1 denied"),
"segment must carry check and denial counts: {line}"
);
assert!(
!line.contains('\n'),
"statusline must be a single line: {line}"
);
}
#[test]
fn statusline_escalation_state_wins() {
let dir = repo().unwrap();
for _ in 0..3 {
hook_write(dir.path(), "session-esc", NONCONFORMING).unwrap();
}
let (stdout, _) = run_verb(dir.path(), &["statusline"]).unwrap();
assert!(
stdout.contains("ESCALATED"),
"three same-rule denials must flip the segment to ESCALATED: {stdout}"
);
}
#[test]
fn statusline_degrades_to_empty_without_event_log() {
let dir = repo().unwrap();
let (stdout, code) = run_verb(dir.path(), &["statusline"]).unwrap();
assert_eq!(code, 0, "no log must still exit 0");
assert_eq!(
stdout.trim(),
"",
"no event log = empty segment (the one place silence is correct)"
);
}