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"]
"#;
const NONCONFORMING: &str = "export async function POST(req: Request) {\n\
const body = await req.json();\n\
return Response.json({ name: body.name });\n\
}\n";
const CONFORMING: &str = "import { UserCreateSchema } from \"../../../contracts/user.zod\";\n\n\
export async function POST(req: Request) {\n\
const body = UserCreateSchema.parse(await req.json());\n\
return Response.json(body);\n\
}\n";
const ROUTE: &str = "app/api/users/route.ts";
fn git_repo() -> Result<tempfile::TempDir, Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
fs::write(dir.path().join("pushkin.toml"), MANIFEST)?;
for args in [
["init", "--initial-branch=main"].as_slice(),
["config", "user.email", "test@example.invalid"].as_slice(),
["config", "user.name", "Pushkin Test"].as_slice(),
["config", "commit.gpgsign", "false"].as_slice(),
] {
git(dir.path(), args)?;
}
Ok(dir)
}
fn git(dir: &Path, args: &[&str]) -> TestResult {
let output = StdCommand::new("git")
.args(args)
.current_dir(dir)
.output()?;
assert!(
output.status.success(),
"git {args:?}: {}",
String::from_utf8_lossy(&output.stderr)
);
Ok(())
}
fn write_at(dir: &Path, relative: &str, content: &str) -> TestResult {
let full = dir.join(relative);
if let Some(parent) = full.parent() {
fs::create_dir_all(parent)?;
}
fs::write(full, content)?;
Ok(())
}
fn stage(dir: &Path, relative: &str, content: &str) -> TestResult {
write_at(dir, relative, content)?;
git(dir, &["add", relative])
}
fn commit_all(dir: &Path, message: &str) -> TestResult {
git(dir, &["add", "-A"])?;
git(dir, &["commit", "-m", message])
}
struct Verdict {
code: Option<i32>,
stdout: String,
stderr: String,
}
fn check(dir: &Path, args: &[&str]) -> Result<Verdict, Box<dyn std::error::Error>> {
let output = Command::cargo_bin("pushkin")?
.current_dir(dir)
.arg("check")
.args(args)
.write_stdin("")
.output()?;
Ok(Verdict {
code: output.status.code(),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
})
}
#[test]
fn staged_nonconforming_mapped_file_blocks() -> TestResult {
let dir = git_repo()?;
stage(dir.path(), ROUTE, NONCONFORMING)?;
let verdict = check(dir.path(), &["--staged"])?;
assert_eq!(
verdict.code,
Some(2),
"staged violation blocks: {}",
verdict.stderr
);
assert!(
verdict.stderr.contains(ROUTE),
"envelope names the offending file: {}",
verdict.stderr
);
Ok(())
}
#[test]
fn preexisting_violation_in_untouched_file_does_not_block_a_clean_change() -> TestResult {
let dir = git_repo()?;
write_at(dir.path(), "app/api/legacy/route.ts", NONCONFORMING)?;
commit_all(dir.path(), "seed pre-existing violation")?;
stage(dir.path(), ROUTE, CONFORMING)?;
let verdict = check(dir.path(), &["--staged"])?;
assert_eq!(
verdict.code,
Some(0),
"untouched pre-existing violation must not block this commit: {}",
verdict.stderr
);
Ok(())
}
#[test]
fn nothing_staged_allows_without_reading_stdin() -> TestResult {
let dir = git_repo()?;
write_at(dir.path(), ROUTE, NONCONFORMING)?;
commit_all(dir.path(), "seed")?;
write_at(dir.path(), ROUTE, NONCONFORMING)?;
let verdict = check(dir.path(), &["--staged"])?;
assert_eq!(
verdict.code,
Some(0),
"empty staged set is an allow: {}",
verdict.stderr
);
Ok(())
}
#[test]
fn index_is_truth_when_index_and_worktree_diverge() -> TestResult {
let dir = git_repo()?;
stage(dir.path(), ROUTE, NONCONFORMING)?;
write_at(dir.path(), ROUTE, CONFORMING)?;
let verdict = check(dir.path(), &["--staged"])?;
assert_eq!(
verdict.code,
Some(2),
"index is truth — staged violation blocks: {}",
verdict.stderr
);
Ok(())
}
#[test]
fn clean_index_allows_even_when_worktree_is_dirty() -> TestResult {
let dir = git_repo()?;
stage(dir.path(), ROUTE, CONFORMING)?;
write_at(dir.path(), ROUTE, NONCONFORMING)?;
let verdict = check(dir.path(), &["--staged"])?;
assert_eq!(
verdict.code,
Some(0),
"unstaged worktree violation is not this commit's problem: {}",
verdict.stderr
);
Ok(())
}
#[test]
fn staged_deletion_is_skipped() -> TestResult {
let dir = git_repo()?;
write_at(dir.path(), ROUTE, CONFORMING)?;
commit_all(dir.path(), "seed")?;
git(dir.path(), &["rm", ROUTE])?;
let verdict = check(dir.path(), &["--staged"])?;
assert_eq!(
verdict.code,
Some(0),
"a deleted path has no content to gate: {}",
verdict.stderr
);
Ok(())
}
#[test]
fn unmapped_staged_files_are_ignored() -> TestResult {
let dir = git_repo()?;
stage(dir.path(), "README.md", "# not a mapped glob\n")?;
let verdict = check(dir.path(), &["--staged"])?;
assert_eq!(
verdict.code,
Some(0),
"only manifest-mapped files are gated: {}",
verdict.stderr
);
Ok(())
}
#[test]
fn staged_decision_is_appended_to_the_event_log() -> TestResult {
let dir = git_repo()?;
stage(dir.path(), ROUTE, NONCONFORMING)?;
let verdict = check(dir.path(), &["--staged"])?;
assert_eq!(verdict.code, Some(2));
assert!(
dir.path().join(".pushkin/events.db").exists(),
"the staged decision must reach the event log"
);
Ok(())
}
#[test]
fn json_flag_emits_the_uniform_envelope_on_stdout() -> TestResult {
let dir = git_repo()?;
stage(dir.path(), ROUTE, NONCONFORMING)?;
let verdict = check(dir.path(), &["--staged", "--json"])?;
assert_eq!(verdict.code, Some(2), "exit codes are unchanged by --json");
let envelope: serde_json::Value = serde_json::from_str(&verdict.stdout)?;
assert_eq!(
envelope.get("decision").and_then(serde_json::Value::as_str),
Some("block"),
"decision matches the verdict: {}",
verdict.stdout
);
let violations = envelope
.get("violations")
.and_then(serde_json::Value::as_array)
.ok_or("violations[] present")?;
assert!(
!violations.is_empty(),
"block carries violations: {}",
verdict.stdout
);
assert!(
envelope.get("durationMs").is_some(),
"§8.3 duration field present: {}",
verdict.stdout
);
Ok(())
}
#[test]
fn json_allow_envelope_is_wellformed() -> TestResult {
let dir = git_repo()?;
stage(dir.path(), ROUTE, CONFORMING)?;
let verdict = check(dir.path(), &["--staged", "--json"])?;
assert_eq!(verdict.code, Some(0));
let envelope: serde_json::Value = serde_json::from_str(&verdict.stdout)?;
assert_eq!(
envelope.get("decision").and_then(serde_json::Value::as_str),
Some("allow"),
"{}",
verdict.stdout
);
assert_eq!(
envelope
.get("violations")
.and_then(serde_json::Value::as_array)
.map(Vec::len),
Some(0),
"{}",
verdict.stdout
);
Ok(())
}
#[test]
fn json_composes_with_the_stdin_write_mode() -> TestResult {
let dir = git_repo()?;
let payload = r#"{"tool_name":"Write","tool_input":{"file_path":"app/api/users/route.ts","content":"export async function POST(req: Request) {\n const body = await req.json();\n return Response.json({ name: body.name });\n}\n"}}"#;
let output = Command::cargo_bin("pushkin")?
.current_dir(dir.path())
.args(["check", "--json"])
.write_stdin(payload)
.output()?;
assert_eq!(output.status.code(), Some(2));
let stdout = String::from_utf8_lossy(&output.stdout);
let envelope: serde_json::Value = serde_json::from_str(&stdout)?;
assert_eq!(
envelope.get("decision").and_then(serde_json::Value::as_str),
Some("block"),
"{stdout}"
);
Ok(())
}