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]
suppression_comments = "deny"
protected_paths = ["pushkin.toml"]
read_only_paths = ["crates/**/tests/**"]
"#;
const MAPPED: &str = "app/api/users/route.ts";
const COMMITTED_TEST: &str = "crates/pushkin-cli/tests/committed_suite.rs";
const UNGATED: &str = "docs/notes.md";
const RULE_CONTENT_UNAVAILABLE: &str = "pushkin.content_unavailable";
const RULE_UNVALIDATED: &str = "contract.boundary.unvalidated_input";
const RULE_READ_ONLY: &str = "pushkin.read_only_path";
const REAL_FILE: &str = "export async function POST(req: Request) {\n \
const body = await req.json();\n \
return Response.json(body);\n}\n";
fn git(dir: &Path, args: &[&str]) -> TestResult {
let status = StdCommand::new("git")
.current_dir(dir)
.args(args)
.status()?;
assert!(status.success(), "git {args:?} failed");
Ok(())
}
fn repo() -> Result<tempfile::TempDir, Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
fs::write(dir.path().join("pushkin.toml"), MANIFEST)?;
fs::create_dir_all(dir.path().join("contracts"))?;
fs::write(
dir.path().join("contracts/user.zod.ts"),
"export const user = 1;\n",
)?;
fs::create_dir_all(dir.path().join("app/api/users"))?;
fs::write(dir.path().join(MAPPED), REAL_FILE)?;
fs::create_dir_all(dir.path().join("crates/pushkin-cli/tests"))?;
fs::write(dir.path().join(COMMITTED_TEST), "// committed suite\n")?;
fs::create_dir_all(dir.path().join("docs"))?;
fs::write(dir.path().join(UNGATED), "notes\n")?;
git(dir.path(), &["init", "-q", "."])?;
git(dir.path(), &["add", "-A"])?;
git(
dir.path(),
&[
"-c",
"user.name=F49 Suite",
"-c",
"user.email=f49@test",
"commit",
"-qm",
"fixture",
],
)?;
Ok(dir)
}
fn hermes_patch(path: &str) -> String {
serde_json::json!({
"session_id": "f49-hermes",
"tool_name": "patch",
"tool_input": {
"mode": "replace",
"path": path,
"old_string": "one",
"new_string": "two",
},
})
.to_string()
}
fn hermes_write(path: &str, content: &str) -> String {
serde_json::json!({
"session_id": "f49-hermes",
"tool_name": "write",
"tool_input": { "path": path, "content": content },
})
.to_string()
}
fn opencode_edit(path: &str) -> String {
serde_json::json!({
"tool": "edit",
"sessionID": "f50-opencode",
"callID": "call_f50",
"args": { "filePath": path, "oldString": "one", "newString": "two" },
})
.to_string()
}
fn opencode_write(path: &str, content: &str) -> String {
serde_json::json!({
"tool": "write",
"sessionID": "f50-opencode",
"args": { "filePath": path, "content": content },
})
.to_string()
}
fn opencode_read(path: &str) -> String {
serde_json::json!({
"tool": "read",
"sessionID": "f50-opencode",
"args": { "filePath": path },
})
.to_string()
}
fn hook(
dir: &Path,
agent: &str,
payload: &str,
daemon: Option<&str>,
) -> Result<String, Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("pushkin")?;
cmd.current_dir(dir).write_stdin(payload.to_owned());
if let Some(mode) = daemon {
cmd.env("PUSHKIN_DAEMON", mode);
}
let output = cmd.args(["hook", agent]).output()?;
Ok(format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
))
}
fn stop_daemon(dir: &Path) {
let _ = Command::cargo_bin("pushkin")
.map(|mut c| c.current_dir(dir).args(["daemon", "stop"]).output());
}
#[test]
fn a_hermes_patch_to_a_mapped_path_denies_under_content_unavailable() -> TestResult {
let dir = repo()?;
let output = hook(dir.path(), "hermes", &hermes_patch(MAPPED), Some("off"))?;
assert!(
output.contains(RULE_CONTENT_UNAVAILABLE),
"a patch carries no file content; refuse rather than allow silently: {output}"
);
assert!(
!output.contains("failing open"),
"the payload is recognized, so nothing may narrate a fail-open: {output}"
);
Ok(())
}
#[test]
fn a_hermes_patch_to_a_read_only_path_denies_on_the_path() -> TestResult {
let dir = repo()?;
let output = hook(
dir.path(),
"hermes",
&hermes_patch(COMMITTED_TEST),
Some("off"),
)?;
assert!(
output.contains(RULE_READ_ONLY),
"a glob plus HEAD needs no content: {output}"
);
Ok(())
}
#[test]
fn the_hermes_refusal_names_the_patch_tool() -> TestResult {
let dir = repo()?;
let output = hook(dir.path(), "hermes", &hermes_patch(MAPPED), Some("off"))?;
assert!(output.contains("patch"), "name the tool refused: {output}");
assert!(output.contains("F48"), "cite the finding: {output}");
Ok(())
}
#[test]
fn a_hermes_write_carrying_content_is_unchanged() -> TestResult {
let dir = repo()?;
let nonconforming = "export async function POST(req) { const b = await req.json(); }\n";
let output = hook(
dir.path(),
"hermes",
&hermes_write(MAPPED, nonconforming),
Some("off"),
)?;
assert!(
output.contains(RULE_UNVALIDATED),
"whole-file content still meets the content rule: {output}"
);
assert!(
!output.contains(RULE_CONTENT_UNAVAILABLE),
"content WAS available; the interim refusal must not appear: {output}"
);
Ok(())
}
#[test]
fn a_hermes_patch_to_an_ungated_path_is_allowed() -> TestResult {
let dir = repo()?;
let output = hook(dir.path(), "hermes", &hermes_patch(UNGATED), Some("off"))?;
assert!(
!output.contains(RULE_CONTENT_UNAVAILABLE),
"no rule maps here, so there is nothing to refuse: {output}"
);
Ok(())
}
#[test]
fn an_opencode_edit_to_a_mapped_path_denies_under_content_unavailable() -> TestResult {
let dir = repo()?;
let output = hook(dir.path(), "opencode", &opencode_edit(MAPPED), Some("off"))?;
assert!(
output.contains(RULE_CONTENT_UNAVAILABLE),
"oldString/newString is not file content: {output}"
);
assert!(
!output.contains("failing open"),
"the payload is recognized: {output}"
);
Ok(())
}
#[test]
fn an_opencode_edit_to_a_read_only_path_denies_on_the_path() -> TestResult {
let dir = repo()?;
let output = hook(
dir.path(),
"opencode",
&opencode_edit(COMMITTED_TEST),
Some("off"),
)?;
assert!(
output.contains(RULE_READ_ONLY),
"path rule, no content needed: {output}"
);
Ok(())
}
#[test]
fn the_opencode_refusal_names_the_edit_tool() -> TestResult {
let dir = repo()?;
let output = hook(dir.path(), "opencode", &opencode_edit(MAPPED), Some("off"))?;
assert!(output.contains("edit"), "name the tool refused: {output}");
assert!(output.contains("F48"), "cite the finding: {output}");
Ok(())
}
#[test]
fn an_opencode_write_carrying_content_is_unchanged() -> TestResult {
let dir = repo()?;
let nonconforming = "export async function POST(req) { const b = await req.json(); }\n";
let output = hook(
dir.path(),
"opencode",
&opencode_write(MAPPED, nonconforming),
Some("off"),
)?;
assert!(
output.contains(RULE_UNVALIDATED),
"whole-file content still meets the content rule: {output}"
);
Ok(())
}
#[test]
fn the_opencode_read_arm_is_unchanged() -> TestResult {
let dir = repo()?;
let output = hook(dir.path(), "opencode", &opencode_read(MAPPED), Some("off"))?;
assert!(
!output.contains(RULE_CONTENT_UNAVAILABLE),
"a read is not a content-absent mutation: {output}"
);
Ok(())
}
#[test]
fn the_hermes_verdict_is_identical_warm_and_cold() -> TestResult {
let dir = repo()?;
let payload = hermes_patch(MAPPED);
let cold = hook(dir.path(), "hermes", &payload, Some("off"))?;
let warm = hook(dir.path(), "hermes", &payload, Some("auto"))?;
stop_daemon(dir.path());
assert!(cold.contains(RULE_CONTENT_UNAVAILABLE), "cold: {cold}");
assert!(warm.contains(RULE_CONTENT_UNAVAILABLE), "warm: {warm}");
Ok(())
}
#[test]
fn the_opencode_verdict_is_identical_warm_and_cold() -> TestResult {
let dir = repo()?;
let payload = opencode_edit(MAPPED);
let cold = hook(dir.path(), "opencode", &payload, Some("off"))?;
let warm = hook(dir.path(), "opencode", &payload, Some("auto"))?;
stop_daemon(dir.path());
assert!(cold.contains(RULE_CONTENT_UNAVAILABLE), "cold: {cold}");
assert!(warm.contains(RULE_CONTENT_UNAVAILABLE), "warm: {warm}");
Ok(())
}
#[test]
fn all_three_families_share_one_interim_rule_id() -> TestResult {
let dir = repo()?;
let hermes = hook(dir.path(), "hermes", &hermes_patch(MAPPED), Some("off"))?;
let opencode = hook(dir.path(), "opencode", &opencode_edit(MAPPED), Some("off"))?;
for output in [&hermes, &opencode] {
assert!(
output.contains(RULE_CONTENT_UNAVAILABLE),
"one id for the interim class: {output}"
);
}
Ok(())
}