use assert_cmd::Command;
use serde_json::Value;
use std::fs;
use std::path::Path;
type TestResult = Result<(), Box<dyn std::error::Error>>;
const BASE: &str = r#"
version = 1
canonical = "json-schema-2020-12"
authoring = "zod"
[gates]
"#;
const RED_ON_STOP: &str = r#"
[[floor.commands]]
name = "guard"
run = ["git", "definitely-not-a-verb"]
scope = "whole_repo"
inputs = "repo"
on_stop = true
"#;
const GREEN_ON_STOP_WITH_MARKER: &str = r#"
[[floor.commands]]
name = "guard"
run = ["sh", "-c", "touch ran.marker"]
scope = "whole_repo"
inputs = "repo"
on_stop = true
"#;
const STOP_PAYLOAD: &str = r#"{"session_id":"s-f82","stop_hook_active":true}"#;
fn repo(floor: &str) -> Result<tempfile::TempDir, Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
fs::write(dir.path().join("pushkin.toml"), format!("{BASE}{floor}"))?;
Ok(dir)
}
struct Run {
code: i32,
stdout: String,
}
fn bin(dir: &Path) -> Result<Command, Box<dyn std::error::Error>> {
let mut command = Command::cargo_bin("pushkin")?;
command.current_dir(dir).env("PUSHKIN_DAEMON", "off");
Ok(command)
}
fn check_stop(dir: &Path) -> Result<Run, Box<dyn std::error::Error>> {
let output = bin(dir)?
.args(["check", "--json"])
.write_stdin(STOP_PAYLOAD.to_owned())
.output()?;
Ok(Run {
code: output.status.code().unwrap_or(-1),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
})
}
fn hook_stop(dir: &Path) -> Result<Run, Box<dyn std::error::Error>> {
let output = bin(dir)?
.args(["hook", "claude"])
.write_stdin(STOP_PAYLOAD.to_owned())
.output()?;
Ok(Run {
code: output.status.code().unwrap_or(-1),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
})
}
fn envelope(run: &Run) -> Result<(String, Vec<String>), Box<dyn std::error::Error>> {
let json: Value = serde_json::from_str(&run.stdout)?;
let decision = json
.get("decision")
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned();
let rules = json
.get("violations")
.and_then(Value::as_array)
.map(|violations| {
violations
.iter()
.filter_map(|v| v.get("rule").and_then(Value::as_str))
.map(str::to_owned)
.collect()
})
.unwrap_or_default();
Ok((decision, rules))
}
fn waive(dir: &Path, rule: &str) -> Result<(), Box<dyn std::error::Error>> {
let out = bin(dir)?
.args([
"waive",
rule,
"--path",
"**",
"--ttl",
"2h",
"--reason",
"f82 parity test",
])
.output()?;
assert!(
out.status.success(),
"waive must succeed: {}",
String::from_utf8_lossy(&out.stderr)
);
Ok(())
}
#[test]
fn check_and_hook_agree_on_a_red_on_stop_command() -> TestResult {
let dir = repo(RED_ON_STOP)?;
let hook = hook_stop(dir.path())?;
let hook_json: Value = serde_json::from_str(&hook.stdout)?;
assert_eq!(
hook_json.get("decision").and_then(Value::as_str),
Some("block"),
"hook must block on a red on-stop command; got:\n{}",
hook.stdout
);
assert!(
hook.stdout.contains("floor.guard"),
"hook must name the failing command by rule id; got:\n{}",
hook.stdout
);
let check = check_stop(dir.path())?;
let (decision, rules) = envelope(&check)?;
assert_eq!(
decision, "block",
"check must reach the same verdict as hook; got:\n{}",
check.stdout
);
assert!(
rules.iter().any(|rule| rule == "floor.guard"),
"check must carry the same rule id; got rules {rules:?}"
);
assert_eq!(check.code, 2, "check's block exit code is 2 (CI contract)");
Ok(())
}
#[test]
fn a_live_waiver_reaches_the_stop_floor_on_both_surfaces() -> TestResult {
let dir = repo(RED_ON_STOP)?;
let (before, _) = envelope(&check_stop(dir.path())?)?;
assert_eq!(before, "block", "un-waived check must block first");
let hook_before: Value = serde_json::from_str(&hook_stop(dir.path())?.stdout)?;
assert_eq!(
hook_before.get("decision").and_then(Value::as_str),
Some("block")
);
waive(dir.path(), "floor.guard")?;
let (after, rules) = envelope(&check_stop(dir.path())?)?;
assert_eq!(
after, "allow",
"a live waiver must reach check's Stop floor; rules left: {rules:?}"
);
let hook_after = hook_stop(dir.path())?;
assert!(
hook_after.stdout.trim().is_empty(),
"hook must honour the same waiver — silence, not a block; got:\n{}",
hook_after.stdout
);
Ok(())
}
#[test]
fn a_green_on_stop_command_runs_on_check_and_allows() -> TestResult {
let dir = repo(GREEN_ON_STOP_WITH_MARKER)?;
let check = check_stop(dir.path())?;
let (decision, rules) = envelope(&check)?;
assert_eq!(decision, "allow", "got:\n{}", check.stdout);
assert!(
rules.is_empty(),
"a green command adds no violation: {rules:?}"
);
assert!(
dir.path().join("ran.marker").exists(),
"check must RUN the opted-in on-stop command, not skip it"
);
Ok(())
}
#[test]
fn check_and_hook_agree_when_the_on_stop_command_is_green() -> TestResult {
let dir = repo(GREEN_ON_STOP_WITH_MARKER)?;
let (decision, _) = envelope(&check_stop(dir.path())?)?;
assert_eq!(decision, "allow");
let hook = hook_stop(dir.path())?;
assert_eq!(hook.code, 0);
assert!(
hook.stdout.trim().is_empty(),
"hook allows by silence; got:\n{}",
hook.stdout
);
Ok(())
}
#[test]
fn a_repo_with_no_floor_table_is_unchanged_on_check() -> TestResult {
let dir = repo("")?;
let check = check_stop(dir.path())?;
let (decision, rules) = envelope(&check)?;
assert_eq!(decision, "allow", "got:\n{}", check.stdout);
assert!(rules.is_empty());
assert_eq!(check.code, 0);
Ok(())
}