use assert_cmd::Command;
use std::fs;
use std::path::Path;
type TestResult = Result<(), Box<dyn std::error::Error>>;
const MANIFEST_WITH_RETRIEVAL: &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"]
retrieval_paths = ["crates/**/*.rs"]
retrieval_tool = "mcp__codebase-retrieval__codebase-retrieval"
"#;
const GATED_SOURCE: &str = "crates/pushkin-core/src/pipeline.rs";
const UNGATED_SOURCE: &str = "docs/notes.md";
fn repo() -> Result<tempfile::TempDir, Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
fs::write(dir.path().join("pushkin.toml"), MANIFEST_WITH_RETRIEVAL)?;
fs::create_dir_all(dir.path().join("crates/pushkin-core/src"))?;
fs::write(dir.path().join(GATED_SOURCE), "// gated source\n")?;
fs::create_dir_all(dir.path().join("docs"))?;
fs::write(dir.path().join(UNGATED_SOURCE), "notes\n")?;
Ok(dir)
}
fn check_read(
dir: &Path,
file_path: &str,
range: &str,
) -> Result<(Option<i32>, String), Box<dyn std::error::Error>> {
let payload =
format!(r#"{{"tool_name":"Read","tool_input":{{"file_path":"{file_path}"{range}}}}}"#);
let output = Command::cargo_bin("pushkin")?
.current_dir(dir)
.write_stdin(payload)
.args(["hook", "claude"])
.output()?;
Ok((
output.status.code(),
format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
),
))
}
#[test]
fn unbounded_read_of_a_gated_path_is_denied() -> TestResult {
let dir = repo()?;
let (_, output) = check_read(dir.path(), GATED_SOURCE, "")?;
assert!(
output.contains("pushkin.retrieval.raw_read"),
"an unbounded read of a gated path names its own rule: {output}"
);
assert!(
output.contains("deny"),
"the Claude dialect carries a deny verdict: {output}"
);
Ok(())
}
#[test]
fn the_deny_names_the_manifest_declared_retrieval_tool() -> TestResult {
let dir = repo()?;
let (_, output) = check_read(dir.path(), GATED_SOURCE, "")?;
assert!(
output.contains("mcp__codebase-retrieval__codebase-retrieval"),
"the fix hint names the tool the MANIFEST declares, not a \
hard-coded vendor — this is what lets Pushkin's own index take \
the slot later: {output}"
);
Ok(())
}
#[test]
fn a_bounded_range_read_of_a_gated_path_is_allowed() -> TestResult {
let dir = repo()?;
let (_, output) = check_read(dir.path(), GATED_SOURCE, r#","offset":40,"limit":30"#)?;
assert!(
!output.contains("pushkin.retrieval.raw_read"),
"a narrow range read is the deliberate shape the gate permits — \
it is also how an agent satisfies the host's read-before-edit \
requirement: {output}"
);
Ok(())
}
#[test]
fn reads_outside_the_retrieval_globs_are_untouched() -> TestResult {
let dir = repo()?;
let (_, output) = check_read(dir.path(), UNGATED_SOURCE, "")?;
assert!(
!output.contains("pushkin.retrieval.raw_read"),
"the gate is scoped to its globs; everything else reads freely: \
{output}"
);
Ok(())
}
#[test]
fn the_ladder_narrates_a_denied_read_as_a_read() -> TestResult {
let dir = repo()?;
let (_, first) = check_read(dir.path(), GATED_SOURCE, "")?;
assert!(
!first.contains("write blocked"),
"rung 1 must not call a read a write: {first}"
);
assert!(
first.contains("read blocked"),
"rung 1 names the action the agent actually took: {first}"
);
let (_, second) = check_read(dir.path(), GATED_SOURCE, "")?;
assert!(
!second.contains("same write"),
"rung 2 must not tell the agent to stop retrying a WRITE: {second}"
);
let (_, third) = check_read(dir.path(), GATED_SOURCE, "")?;
assert!(
third.contains("STOP") && !third.contains("This write"),
"rung 3 keeps the STOP shape without mislabeling the action: {third}"
);
Ok(())
}
#[test]
fn the_read_deny_does_not_dangle_a_human_waiver() -> TestResult {
let dir = repo()?;
let (_, output) = check_read(dir.path(), GATED_SOURCE, "")?;
assert!(
!output.contains("pushkin waive"),
"the agent can comply on its own — pointing at a human waiver is \
the wrong exit: {output}"
);
Ok(())
}
#[test]
fn the_read_gate_holds_across_the_evidenced_dialects() -> TestResult {
let dir = repo()?;
let claude = format!(
r#"{{"session_id":"p1","tool_name":"Read","tool_input":{{"file_path":"{GATED_SOURCE}"}}}}"#
);
let output = Command::cargo_bin("pushkin")?
.current_dir(dir.path())
.write_stdin(claude)
.args(["hook", "claude"])
.output()?;
let text = String::from_utf8_lossy(&output.stdout).to_string();
assert!(
text.contains("pushkin.retrieval.raw_read"),
"claude `Read` must gate: {text}"
);
let auggie = format!(
r#"{{"conversation_id":"p1","tool_name":"view","tool_input":{{"path":"{GATED_SOURCE}"}}}}"#
);
let output = Command::cargo_bin("pushkin")?
.current_dir(dir.path())
.write_stdin(auggie)
.args(["hook", "auggie"])
.output()?;
let text = String::from_utf8_lossy(&output.stdout).to_string();
assert!(
text.contains("pushkin.retrieval.raw_read"),
"auggie `view` must gate alike: {text}"
);
let opencode =
format!(r#"{{"sessionID":"p1","tool":"read","args":{{"filePath":"{GATED_SOURCE}"}}}}"#);
let output = Command::cargo_bin("pushkin")?
.current_dir(dir.path())
.write_stdin(opencode)
.args(["hook", "opencode"])
.output()?;
let text = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(
text.contains("pushkin.retrieval.raw_read"),
"opencode's documented read shape must gate alike: {text}"
);
Ok(())
}
#[test]
fn a_write_payload_is_unaffected_by_the_read_gate() -> TestResult {
let dir = repo()?;
let payload = format!(
r#"{{"tool_name":"Write","tool_input":{{"file_path":"{GATED_SOURCE}","content":"// edit"}}}}"#
);
let output = Command::cargo_bin("pushkin")?
.current_dir(dir.path())
.write_stdin(payload)
.args(["hook", "claude"])
.output()?;
let text = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(
!text.contains("pushkin.retrieval.raw_read"),
"the read gate must not leak onto the write path — a write to a \
gated path is a write, judged by the write rules: {text}"
);
Ok(())
}