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 ORDINARY: &str = "docs/notes.md";
const DUPES: &str = "app/api/dupes/route.ts";
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 RULE_PROTECTED: &str = "pushkin.protected_path";
const CONFORMING: &str = "import { UserCreateSchema } from \"contracts/user.zod\";\n\
export async function POST(req: Request) {\n \
const body = UserCreateSchema.parse(await req.json());\n \
return Response.json(body);\n}\n";
const BREAK_HUNK: &str = "@@\n\
\x20export async function POST(req: Request) {\n\
- const body = UserCreateSchema.parse(await req.json());\n\
+ const body = await req.json();\n\
\x20 return Response.json(body);\n";
const BREAK_HUNK_WITH_HEADER: &str = "@@ export async function POST(req: Request) {\n\
- const body = UserCreateSchema.parse(await req.json());\n\
+ const body = await req.json();\n\
\x20 return Response.json(body);\n";
const BENIGN_HUNK: &str = "@@\n\
- return Response.json(body);\n\
+ return Response.json({ ...body });\n\
\x20}\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), CONFORMING)?;
fs::create_dir_all(dir.path().join("app/api/dupes"))?;
fs::write(
dir.path().join(DUPES),
"import { UserCreateSchema } from \"contracts/user.zod\";\n\
export async function POST(req: Request) {\n \
const body = UserCreateSchema.parse(await req.json());\n \
log();\n \
log();\n \
return Response.json(body);\n}\n",
)?;
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(ORDINARY), "one\ntwo\none\n")?;
git(dir.path(), &["init", "-q", "."])?;
git(dir.path(), &["add", "-A"])?;
git(
dir.path(),
&[
"-c",
"user.name=F52 Suite",
"-c",
"user.email=f52@test",
"commit",
"-qm",
"fixture",
],
)?;
Ok(dir)
}
fn patch_payload(body: &str) -> String {
serde_json::json!({
"session_id": "f52",
"hook_event_name": "PreToolUse",
"tool_name": "apply_patch",
"tool_input": { "command": format!("*** Begin Patch\n{body}*** End Patch\n") },
})
.to_string()
}
fn update(path: &str, hunks: &str) -> String {
patch_payload(&format!("*** Update File: {path}\n{hunks}"))
}
fn write_payload(path: &str, content: &str) -> String {
serde_json::json!({
"session_id": "f52",
"hook_event_name": "PreToolUse",
"tool_name": "write",
"tool_input": { "file_path": path, "content": content },
})
.to_string()
}
fn hook(
dir: &Path,
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", "codex"]).output()?;
Ok(format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
))
}
#[test]
fn a_hunk_that_breaks_the_contract_is_denied_under_the_real_rule() -> TestResult {
let dir = repo()?;
let output = hook(dir.path(), &update(MAPPED, BREAK_HUNK), Some("off"))?;
assert!(
output.contains(RULE_UNVALIDATED),
"the reconstructed file drops the parse and must be judged on it: {output}"
);
assert!(
!output.contains(RULE_CONTENT_UNAVAILABLE),
"content IS available now — it was reconstructed from the hunk: {output}"
);
Ok(())
}
#[test]
fn a_hunk_that_keeps_the_contract_is_allowed() -> TestResult {
let dir = repo()?;
let output = hook(dir.path(), &update(MAPPED, BENIGN_HUNK), Some("off"))?;
assert!(
!output.contains(RULE_UNVALIDATED) && !output.contains(RULE_CONTENT_UNAVAILABLE),
"the reconstructed file still parses through the contract: {output}"
);
Ok(())
}
#[test]
fn a_hunk_and_the_equivalent_write_reach_the_same_verdict() -> TestResult {
let dir = repo()?;
let reconstructed = CONFORMING.replace(
" const body = UserCreateSchema.parse(await req.json());",
" const body = await req.json();",
);
let via_hunk = hook(dir.path(), &update(MAPPED, BREAK_HUNK), Some("off"))?;
let via_write = hook(
dir.path(),
&write_payload(MAPPED, &reconstructed),
Some("off"),
)?;
for output in [&via_hunk, &via_write] {
assert!(
output.contains(RULE_UNVALIDATED),
"both routes judge the same bytes: {output}"
);
assert!(
!output.contains(RULE_CONTENT_UNAVAILABLE),
"and judge them, rather than both refusing: {output}"
);
}
Ok(())
}
#[test]
fn multiple_hunks_are_judged_on_the_cumulative_result() -> TestResult {
let dir = repo()?;
let repair = "@@\n\
- const body = await req.json();\n\
+ const body = UserCreateSchema.parse(await req.json());\n\
\x20 return Response.json(body);\n";
let output = hook(
dir.path(),
&update(MAPPED, &format!("{BREAK_HUNK}{repair}")),
Some("off"),
)?;
assert!(
!output.contains(RULE_UNVALIDATED),
"the second hunk restored the parse; the end state conforms: {output}"
);
Ok(())
}
#[test]
fn a_scope_header_on_the_at_signs_is_not_matched_literally() -> TestResult {
let dir = repo()?;
let output = hook(
dir.path(),
&update(MAPPED, BREAK_HUNK_WITH_HEADER),
Some("off"),
)?;
assert!(
output.contains(RULE_UNVALIDATED),
"the header locates, it does not participate in the match: {output}"
);
assert!(!output.contains(RULE_CONTENT_UNAVAILABLE), "{output}");
Ok(())
}
#[test]
fn a_hunk_whose_context_is_not_in_the_file_refuses() -> TestResult {
let dir = repo()?;
let bogus = "@@\n\
\x20this line is nowhere in the file\n\
-also absent\n\
+replacement\n";
let output = hook(dir.path(), &update(MAPPED, bogus), Some("off"))?;
assert!(
output.contains(RULE_CONTENT_UNAVAILABLE),
"no faithful reconstruction is possible, so refuse: {output}"
);
Ok(())
}
#[test]
fn an_ambiguous_hunk_refuses_rather_than_picking_an_occurrence() -> TestResult {
let dir = repo()?;
let ambiguous = "@@\n\
- log();\n\
+ trace();\n";
let output = hook(dir.path(), &update(DUPES, ambiguous), Some("off"))?;
assert!(
output.contains(RULE_CONTENT_UNAVAILABLE),
"an ambiguous hunk must refuse: {output}"
);
Ok(())
}
#[test]
fn a_move_to_withholds_content_synthesis() -> TestResult {
let dir = repo()?;
let moved = format!("*** Update File: {MAPPED}\n*** Move to: docs/moved.ts\n{BREAK_HUNK}");
let output = hook(dir.path(), &patch_payload(&moved), Some("off"))?;
assert!(
output.contains(RULE_CONTENT_UNAVAILABLE),
"a rename is not modelled by this arm; refuse rather than judge the \
content against the wrong path: {output}"
);
Ok(())
}
#[test]
fn a_move_to_a_protected_path_is_still_denied() -> TestResult {
let dir = repo()?;
let moved = format!("*** Update File: {ORDINARY}\n*** Move to: pushkin.toml\n@@\n-one\n+uno\n");
let output = hook(dir.path(), &patch_payload(&moved), Some("off"))?;
assert!(output.contains(RULE_PROTECTED), "{output}");
Ok(())
}
#[test]
fn an_add_alongside_an_update_still_judges_the_added_content() -> TestResult {
let dir = repo()?;
let mixed = format!(
"*** Add File: app/api/new/route.ts\n\
+export async function POST(req: Request) {{\n\
+ const body = await req.json();\n\
+ return Response.json(body);\n\
+}}\n\
*** Update File: {ORDINARY}\n\
@@\n\
-two\n\
+three\n"
);
let output = hook(dir.path(), &patch_payload(&mixed), Some("off"))?;
assert!(
output.contains(RULE_UNVALIDATED),
"the added handler parses no contract and must be caught: {output}"
);
Ok(())
}
#[test]
fn a_read_only_path_denies_before_synthesis_is_attempted() -> TestResult {
let dir = repo()?;
let hunk = "@@\n-// committed suite\n+// tampered\n";
let output = hook(dir.path(), &update(COMMITTED_TEST, hunk), Some("off"))?;
assert!(output.contains(RULE_READ_ONLY), "path rule first: {output}");
Ok(())
}
#[test]
fn a_delete_only_patch_is_unaffected() -> TestResult {
let dir = repo()?;
let output = hook(
dir.path(),
&patch_payload("*** Delete File: pushkin.toml\n"),
Some("off"),
)?;
assert!(output.contains(RULE_PROTECTED), "{output}");
assert!(!output.contains(RULE_CONTENT_UNAVAILABLE), "{output}");
Ok(())
}
#[test]
fn an_add_only_patch_is_unaffected() -> TestResult {
let dir = repo()?;
let output = hook(
dir.path(),
&patch_payload(
"*** Add File: app/api/other/route.ts\n\
+export async function POST(req: Request) {\n\
+ const body = await req.json();\n\
+ return Response.json(body);\n\
+}\n",
),
Some("off"),
)?;
assert!(output.contains(RULE_UNVALIDATED), "{output}");
Ok(())
}