use assert_cmd::Command;
use std::fs;
use std::path::Path;
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"]
retrieval_paths = ["crates/**/*.rs"]
retrieval_tool = "mcp__codebase-retrieval__codebase-retrieval"
"#;
const GATED: &str = "crates/pushkin-core/src/pipeline.rs";
const RULE: &str = "pushkin.retrieval.raw_read";
fn seed(dir: &Path, manifest: &str) -> TestResult {
fs::write(dir.join("pushkin.toml"), manifest)?;
fs::create_dir_all(dir.join("crates/pushkin-core/src"))?;
fs::write(dir.join(GATED), "// gated\n")?;
Ok(())
}
fn repo() -> Result<tempfile::TempDir, Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
seed(dir.path(), MANIFEST)?;
Ok(dir)
}
fn unopted_repo() -> Result<tempfile::TempDir, Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let manifest = MANIFEST
.replace(r#"retrieval_paths = ["crates/**/*.rs"]"#, "")
.replace(
r#"retrieval_tool = "mcp__codebase-retrieval__codebase-retrieval""#,
"",
);
seed(dir.path(), &manifest)?;
Ok(dir)
}
fn resolved_root(dir: &Path) -> Result<String, Box<dyn std::error::Error>> {
Ok(dir.canonicalize()?.display().to_string())
}
fn bash(dir: &Path, command: &str) -> Result<String, Box<dyn std::error::Error>> {
let payload = serde_json::json!({
"session_id": "bash-gate-normalization",
"tool_name": "Bash",
"tool_input": { "command": command },
})
.to_string();
let output = Command::cargo_bin("pushkin")?
.current_dir(dir)
.write_stdin(payload)
.args(["hook", "claude"])
.output()?;
Ok(format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
))
}
#[test]
fn a_leading_dot_slash_is_the_same_read_and_is_denied() -> TestResult {
let dir = repo()?;
let output = bash(dir.path(), &format!("cat ./{GATED}"))?;
assert!(
output.contains(RULE),
"`./` is a spelling, not a different file: {output}"
);
Ok(())
}
#[test]
fn a_repeated_dot_slash_is_denied() -> TestResult {
let dir = repo()?;
let output = bash(dir.path(), &format!("cat ././{GATED}"))?;
assert!(
output.contains(RULE),
"the prefix strip repeats or it is trivially defeated: {output}"
);
Ok(())
}
#[test]
fn an_absolute_spelling_is_denied() -> TestResult {
let dir = repo()?;
let root = resolved_root(dir.path())?;
let output = bash(dir.path(), &format!("cat {root}/{GATED}"))?;
assert!(
output.contains(RULE),
"an absolute path names the same gated file: {output}"
);
Ok(())
}
#[test]
fn normalization_does_not_widen_the_net() -> TestResult {
let dir = repo()?;
for command in [
"cat README.md",
"ls -la",
"cat ./README.md",
"cat ././docs/charters/index.md",
"cat /etc/hosts",
] {
let output = bash(dir.path(), command)?;
assert!(
!output.contains(RULE),
"`{command}` names no gated path and must pass: {output}"
);
}
Ok(())
}
#[test]
fn an_unopted_repo_stays_inert_under_normalization() -> TestResult {
let dir = unopted_repo()?;
let output = bash(dir.path(), &format!("cat ./{GATED}"))?;
assert!(
!output.contains(RULE),
"opting out is the default and normalization must not undo it: {output}"
);
Ok(())
}
#[test]
fn a_sibling_directory_sharing_the_roots_name_is_not_inside_it() -> TestResult {
let base = tempfile::tempdir()?;
let root = base.path().join("repo");
let sibling = base.path().join("repo-other");
fs::create_dir_all(&root)?;
fs::create_dir_all(sibling.join("crates/pushkin-core/src"))?;
seed(&root, MANIFEST)?;
fs::write(sibling.join(GATED), "// outside the repo\n")?;
let outside = format!("{}/{GATED}", resolved_root(&sibling)?);
let output = bash(&root, &format!("cat {outside}"))?;
assert!(
!output.contains(RULE),
"`repo-other` is a different tree; byte-wise prefix stripping would \
gate it: {output}"
);
Ok(())
}
#[test]
fn the_deny_names_the_repo_relative_path_not_the_machine_path() -> TestResult {
let dir = repo()?;
let root = resolved_root(dir.path())?;
let output = bash(dir.path(), &format!("cat {root}/{GATED}"))?;
assert!(
output.contains(GATED),
"the deny must name a path the agent can act on: {output}"
);
assert!(
!output.contains(&root),
"the machine-specific prefix must not survive into the prose: {output}"
);
Ok(())
}