use assert_cmd::Command;
use std::fs;
use std::path::Path;
use std::process::Command as StdCommand;
type TestResult = Result<(), Box<dyn std::error::Error>>;
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]
protected_paths = ["pushkin.toml", "schemas/**"]
"#;
fn git(root: &Path, args: &[&str]) -> Result<(), Box<dyn std::error::Error>> {
StdCommand::new("git")
.current_dir(root)
.args(args)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()?;
Ok(())
}
fn now_epoch() -> Result<i64, Box<dyn std::error::Error>> {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_secs();
Ok(i64::try_from(secs)?)
}
fn commit_at(root: &Path, epoch: i64) -> Result<(), Box<dyn std::error::Error>> {
let stamp = format!("@{epoch} +0000");
StdCommand::new("git")
.current_dir(root)
.args(["commit", "-qm", "human owns it", "--no-verify"])
.env("GIT_AUTHOR_DATE", &stamp)
.env("GIT_COMMITTER_DATE", &stamp)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()?;
Ok(())
}
fn repo() -> Result<tempfile::TempDir, Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let at = dir.path();
git(at, &["init", "-q", "."])?;
git(at, &["config", "user.email", "t@example.com"])?;
git(at, &["config", "user.name", "t"])?;
fs::write(at.join("pushkin.toml"), MANIFEST)?;
fs::create_dir_all(at.join("schemas"))?;
fs::write(at.join("schemas/user.json"), "{}\n")?;
git(at, &["add", "-A"])?;
git(at, &["commit", "-qm", "base"])?;
Ok(dir)
}
fn agent_write_denied(dir: &Path, path: &str) -> Result<(), Box<dyn std::error::Error>> {
let payload = serde_json::json!({
"session_id": "agent-1",
"tool_name": "Write",
"tool_input": { "file_path": path, "content": "// agent\n" },
})
.to_string();
let out = Command::cargo_bin("pushkin")?
.current_dir(dir)
.write_stdin(payload)
.args(["hook", "claude"])
.output()?;
let text = String::from_utf8_lossy(&out.stdout);
assert!(
text.contains("pushkin.protected_path"),
"fixture precondition: the write path must deny {path}: {text}"
);
Ok(())
}
fn floor(dir: &Path, path: &str) -> Result<(Option<i32>, String), Box<dyn std::error::Error>> {
git(dir, &["add", path])?;
let output = Command::cargo_bin("pushkin")?
.current_dir(dir)
.args(["check", "--staged"])
.output()?;
Ok((
output.status.code(),
format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
),
))
}
#[test]
fn a_human_edit_with_no_agent_deny_still_only_advises() -> TestResult {
let dir = repo()?;
fs::write(
dir.path().join("pushkin.toml"),
format!("{MANIFEST}\n# human\n"),
)?;
let (code, output) = floor(dir.path(), "pushkin.toml")?;
assert_eq!(
code,
Some(0),
"S2d: the floor never blocks the manifest's own maintenance: {output}"
);
assert!(
output.contains("protected surface staged"),
"the S2d advisory still fires: {output}"
);
Ok(())
}
#[test]
fn a_denied_agent_write_that_reaches_the_index_is_blocked() -> TestResult {
let dir = repo()?;
agent_write_denied(dir.path(), "pushkin.toml")?;
fs::write(
dir.path().join("pushkin.toml"),
format!("{MANIFEST}\n# shell\n"),
)?;
let (code, output) = floor(dir.path(), "pushkin.toml")?;
assert!(
output.contains("pushkin.protected_path"),
"the deny must name the protected rule: {output}"
);
assert_eq!(
code,
Some(2),
"a recorded deny plus a staged change is evidence, not a guess: {output}"
);
Ok(())
}
#[test]
fn the_evidence_does_not_spread_to_other_protected_paths() -> TestResult {
let dir = repo()?;
agent_write_denied(dir.path(), "pushkin.toml")?;
fs::write(dir.path().join("schemas/user.json"), "{\"human\":1}\n")?;
let (code, output) = floor(dir.path(), "schemas/user.json")?;
assert_eq!(
code,
Some(0),
"the deny was for pushkin.toml; schemas/user.json is a clean human edit: {output}"
);
Ok(())
}
#[test]
fn committing_the_file_resolves_the_evidence() -> TestResult {
let dir = repo()?;
agent_write_denied(dir.path(), "pushkin.toml")?;
fs::write(
dir.path().join("pushkin.toml"),
format!("{MANIFEST}\n# one\n"),
)?;
git(dir.path(), &["add", "pushkin.toml"])?;
commit_at(dir.path(), now_epoch()? + 2)?;
fs::write(
dir.path().join("pushkin.toml"),
format!("{MANIFEST}\n# two\n"),
)?;
let (code, output) = floor(dir.path(), "pushkin.toml")?;
assert_eq!(
code,
Some(0),
"a human who committed the file has taken ownership; later edits are theirs: {output}"
);
Ok(())
}
#[test]
fn an_unrelated_commit_does_not_clear_the_evidence() -> TestResult {
let dir = repo()?;
agent_write_denied(dir.path(), "pushkin.toml")?;
fs::write(
dir.path().join("pushkin.toml"),
format!("{MANIFEST}\n# bypass\n"),
)?;
fs::write(dir.path().join("README.md"), "unrelated\n")?;
git(dir.path(), &["add", "README.md"])?;
git(dir.path(), &["commit", "-qm", "unrelated", "--no-verify"])?;
let (code, output) = floor(dir.path(), "pushkin.toml")?;
assert_eq!(
code,
Some(2),
"HEAD moved but pushkin.toml is still unowned: {output}"
);
Ok(())
}
#[test]
fn the_floor_is_blind_to_the_write_mechanism() -> TestResult {
let dir = repo()?;
agent_write_denied(dir.path(), "schemas/user.json")?;
let patch = "--- a/schemas/user.json\n+++ b/schemas/user.json\n\
@@ -1 +1 @@\n-{}\n+{\"x\":1}\n";
fs::write(dir.path().join("p.patch"), patch)?;
git(dir.path(), &["apply", "p.patch"])?;
let (code, output) = floor(dir.path(), "schemas/user.json")?;
assert_eq!(
code,
Some(2),
"`git apply` is the same event as any other write here: {output}"
);
Ok(())
}
#[test]
fn the_deny_supersedes_the_advisory() -> TestResult {
let dir = repo()?;
agent_write_denied(dir.path(), "pushkin.toml")?;
fs::write(
dir.path().join("pushkin.toml"),
format!("{MANIFEST}\n# x\n"),
)?;
let (_, output) = floor(dir.path(), "pushkin.toml")?;
assert!(
!output.contains("protected surface staged"),
"the advisory and the deny must not both fire for one event: {output}"
);
Ok(())
}