use assert_cmd::Command;
use std::fs;
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 RULE: &str = "contract.boundary.unvalidated_input";
fn repo() -> std::io::Result<tempfile::TempDir> {
let dir = tempfile::tempdir()?;
fs::write(dir.path().join("pushkin.toml"), MANIFEST)?;
Ok(dir)
}
fn pushkin(dir: &tempfile::TempDir) -> Option<Command> {
let mut cmd = Command::cargo_bin("pushkin").ok()?;
cmd.current_dir(dir.path())
.env("GIT_AUTHOR_NAME", "Waiver Tester")
.env("GIT_AUTHOR_EMAIL", "waiver@example.com");
Some(cmd)
}
fn waive(dir: &tempfile::TempDir, extra: &[&str]) -> Option<std::process::Output> {
let mut args = vec![
"waive",
RULE,
"--path",
"app/api/**",
"--ttl",
"2h",
"--reason",
"phase-5 waiver suite",
];
args.extend_from_slice(extra);
pushkin(dir)?.args(args).output().ok()
}
fn hook_deny_reason(dir: &tempfile::TempDir, file_path: &str) -> Option<String> {
let payload = serde_json::json!({
"session_id": "waiver-suite",
"tool_name": "Write",
"tool_input": { "file_path": file_path, "content": NONCONFORMING }
})
.to_string();
let output = pushkin(dir)?
.args(["hook", "claude"])
.write_stdin(payload)
.output()
.ok()?;
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
if stdout.trim().is_empty() {
return None;
}
let json: serde_json::Value = serde_json::from_str(&stdout).ok()?;
Some(
json["hookSpecificOutput"]["permissionDecisionReason"]
.as_str()?
.to_owned(),
)
}
fn waivers_text(dir: &tempfile::TempDir) -> Option<String> {
fs::read_to_string(dir.path().join("pushkin/waivers.toml")).ok()
}
fn waiver_ids(dir: &tempfile::TempDir) -> Option<Vec<String>> {
let text = waivers_text(dir)?;
Some(
text.lines()
.filter_map(|line| line.trim().strip_prefix("id = \""))
.map(|rest| rest.trim_end_matches('"').to_owned())
.collect(),
)
}
fn doctor_stdout(dir: &tempfile::TempDir) -> Option<String> {
let output = pushkin(dir)?.arg("doctor").output().ok()?;
Some(String::from_utf8_lossy(&output.stdout).into_owned())
}
#[test]
fn waive_appends_signed_scoped_record() {
let dir = repo().unwrap();
let output = waive(&dir, &[]).unwrap();
assert!(
output.status.success(),
"waive must succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let text = waivers_text(&dir).expect("pushkin/waivers.toml must exist after waive");
for needle in [
RULE,
"app/api/**",
"phase-5 waiver suite",
"Waiver Tester",
"granted_at",
"expires_at",
"id = \"",
] {
assert!(text.contains(needle), "record must carry {needle}: {text}");
}
}
#[test]
fn unexpired_waiver_suppresses_matching_deny() {
let dir = repo().unwrap();
let denied = hook_deny_reason(&dir, "app/api/users/route.ts");
assert!(denied.is_some(), "precondition: write must deny unwaived");
assert!(waive(&dir, &[]).unwrap().status.success());
let after = hook_deny_reason(&dir, "app/api/users/route.ts");
assert!(
after.is_none(),
"unexpired scoped waiver must suppress the deny: {after:?}"
);
}
#[test]
fn waiver_outside_scope_does_not_suppress() {
let dir = repo().unwrap();
let out = pushkin(&dir)
.unwrap()
.args([
"waive",
RULE,
"--path",
"app/api/orders/**",
"--ttl",
"2h",
"--reason",
"narrow scope",
])
.output()
.unwrap();
assert!(out.status.success());
let reason = hook_deny_reason(&dir, "app/api/users/route.ts");
assert!(
reason.is_some(),
"a waiver scoped to app/api/orders/** must not cover app/api/users/"
);
}
#[test]
fn expired_waiver_does_not_suppress() {
let dir = repo().unwrap();
let out = pushkin(&dir)
.unwrap()
.args([
"waive",
RULE,
"--path",
"app/api/**",
"--ttl",
"0s",
"--reason",
"already expired",
])
.output()
.unwrap();
assert!(out.status.success());
std::thread::sleep(std::time::Duration::from_millis(1100));
let reason = hook_deny_reason(&dir, "app/api/users/route.ts");
assert!(
reason.is_some(),
"expired waiver must not suppress the deny"
);
}
#[test]
fn waiver_grant_emits_event() {
let dir = repo().unwrap();
assert!(waive(&dir, &[]).unwrap().status.success());
let conn = rusqlite::Connection::open(dir.path().join(".pushkin/events.db")).unwrap();
let count: u64 = conn
.query_row(
"SELECT COUNT(*) FROM events WHERE rule = 'pushkin.waiver.granted'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(count, 1, "waiver grant must be an event on the log");
}
#[test]
fn agent_write_to_waivers_toml_denied() {
let dir = repo().unwrap();
assert!(waive(&dir, &[]).unwrap().status.success());
let reason = hook_deny_reason(&dir, "pushkin/waivers.toml");
let reason = reason.expect("agent write to pushkin/waivers.toml must be denied");
assert!(
reason.contains("pushkin.protected_path"),
"denial must cite the protected-path rule: {reason}"
);
}
#[test]
fn superseded_entry_flagged_by_stale_lint() {
let dir = repo().unwrap();
assert!(waive(&dir, &[]).unwrap().status.success());
let first_id = waiver_ids(&dir).unwrap().first().cloned().unwrap();
let out = waive(&dir, &["--supersedes", &first_id]).unwrap();
assert!(
out.status.success(),
"waive --supersedes must succeed: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = doctor_stdout(&dir).unwrap();
assert!(
stdout.contains("superseded") && stdout.contains(&first_id),
"doctor must lint the superseded waiver by id: {stdout}"
);
}
#[test]
fn contradicts_link_surfaces_in_doctor_output() {
let dir = repo().unwrap();
assert!(waive(&dir, &[]).unwrap().status.success());
let first_id = waiver_ids(&dir).unwrap().first().cloned().unwrap();
let out = waive(&dir, &["--contradicts", &first_id]).unwrap();
assert!(
out.status.success(),
"waive --contradicts must succeed: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = doctor_stdout(&dir).unwrap();
assert!(
stdout.contains("contradicts") && stdout.contains(&first_id),
"doctor must surface the contradiction pair by id: {stdout}"
);
}
#[test]
fn expired_entry_flagged_by_stale_lint() {
let dir = repo().unwrap();
let out = pushkin(&dir)
.unwrap()
.args([
"waive",
RULE,
"--path",
"app/api/**",
"--ttl",
"0s",
"--reason",
"expires immediately",
])
.output()
.unwrap();
assert!(out.status.success());
std::thread::sleep(std::time::Duration::from_millis(1100));
let stdout = doctor_stdout(&dir).unwrap();
assert!(
stdout.contains("expired"),
"doctor must lint expired waivers: {stdout}"
);
}