use assert_cmd::Command;
use std::fs;
const MANIFEST: &str = r#"
version = 1
canonical = "json-schema-2020-12"
authoring = "zod"
[[contracts]]
name = "user"
source = "contracts/user.zod.ts"
emit = ["zod", "pydantic", "rust", "sql"]
[[mappings]]
glob = "app/api/**/*.ts"
contracts = ["user"]
require = "boundary-validation"
[gates]
suppression_comments = "deny"
protected_paths = ["pushkin.toml", ".claude/settings.json", "schemas/**"]
"#;
const USER_SCHEMA: &str = r#"{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": { "type": "string", "minLength": 1, "maxLength": 200 },
"email": { "type": "string", "format": "email" },
"role": { "default": "member", "type": "string", "enum": ["member", "admin"] }
},
"required": ["name", "email"],
"additionalProperties": false
}"#;
fn repo_with_manifest() -> std::io::Result<tempfile::TempDir> {
let dir = tempfile::tempdir()?;
fs::write(dir.path().join("pushkin.toml"), MANIFEST)?;
fs::create_dir_all(dir.path().join("schemas"))?;
fs::write(dir.path().join("schemas/user.schema.json"), USER_SCHEMA)?;
Ok(dir)
}
fn pushkin() -> Result<Command, assert_cmd::cargo::CargoError> {
Command::cargo_bin("pushkin")
}
#[test]
fn check_reads_stdin_and_exits_2_on_block() {
let dir = repo_with_manifest().unwrap();
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 assert = pushkin()
.unwrap()
.current_dir(dir.path())
.arg("check")
.write_stdin(payload)
.assert()
.code(2);
let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string();
assert!(stderr.contains("contract.boundary.unvalidated_input"));
assert!(stderr.contains("fix"));
}
#[test]
fn check_exits_0_on_conforming_write() {
let dir = repo_with_manifest().unwrap();
let payload = r#"{"tool_name":"Write","tool_input":{"file_path":"app/api/users/route.ts","content":"import { UserCreateSchema } from \"../../../contracts/user.zod\";\n\nexport async function POST(req: Request) {\n const body = UserCreateSchema.parse(await req.json());\n return Response.json(body);\n}\n"}}"#;
pushkin()
.unwrap()
.current_dir(dir.path())
.arg("check")
.write_stdin(payload)
.assert()
.code(0);
}
#[test]
fn check_fails_open_on_malformed_stdin_but_logs_event() {
let dir = repo_with_manifest().unwrap();
pushkin()
.unwrap()
.current_dir(dir.path())
.arg("check")
.write_stdin("not json {{{")
.assert()
.code(0)
.stderr(predicates::str::contains("pushkin"));
let db = dir.path().join(".pushkin/events.db");
assert!(
db.exists(),
"fail-open must still open and write the event log"
);
}
#[test]
fn malformed_payload_with_protected_path_target_fails_closed() {
let dir = repo_with_manifest().unwrap();
let payload =
r#"{"tool_input":{"file_path":".claude/settings.json"},"unexpected":[1,2,{"deep":true}]}"#;
pushkin()
.unwrap()
.current_dir(dir.path())
.arg("check")
.write_stdin(payload)
.assert()
.code(2)
.stderr(predicates::str::contains("pushkin.protected_path"));
}
#[test]
fn compile_writes_all_four_targets() {
let dir = repo_with_manifest().unwrap();
pushkin()
.unwrap()
.current_dir(dir.path())
.arg("compile")
.assert()
.code(0);
for file in [
"generated/user.zod.gen.ts",
"generated/user_models.gen.py",
"generated/user.gen.rs",
"generated/user.gen.sql",
] {
let path = dir.path().join(file);
assert!(path.exists(), "missing {file}");
let content = fs::read_to_string(&path).unwrap();
assert!(
content.contains("pushkin-epoch:"),
"{file} missing epoch header"
);
}
}
#[test]
fn compile_twice_is_byte_identical() {
let dir = repo_with_manifest().unwrap();
pushkin()
.unwrap()
.current_dir(dir.path())
.arg("compile")
.assert()
.code(0);
let first = fs::read_to_string(dir.path().join("generated/user.gen.rs")).unwrap();
pushkin()
.unwrap()
.current_dir(dir.path())
.arg("compile")
.assert()
.code(0);
let second = fs::read_to_string(dir.path().join("generated/user.gen.rs")).unwrap();
assert_eq!(first, second);
}
#[test]
fn check_emits_events_to_sqlite() {
let dir = repo_with_manifest().unwrap();
let payload = r#"{"tool_name":"Write","tool_input":{"file_path":".claude/settings.json","content":"{}"}}"#;
pushkin()
.unwrap()
.current_dir(dir.path())
.arg("check")
.write_stdin(payload)
.assert()
.code(2);
let db = dir.path().join(".pushkin/events.db");
assert!(
db.exists(),
"every gate decision is an appended event (charter N7)"
);
}
#[test]
fn doctor_reports_missing_hook() {
let dir = repo_with_manifest().unwrap();
let assert = pushkin()
.unwrap()
.current_dir(dir.path())
.arg("doctor")
.assert()
.code(1);
let stdout = String::from_utf8_lossy(&assert.get_output().stdout).to_string();
assert!(
stdout.contains("hook"),
"doctor must name the missing hook: {stdout}"
);
}
#[test]
fn init_is_idempotent_at_same_consent_version() {
let dir = repo_with_manifest().unwrap();
pushkin()
.unwrap()
.current_dir(dir.path())
.arg("init")
.assert()
.code(0);
let settings_first = fs::read_to_string(dir.path().join(".claude/settings.json")).unwrap();
pushkin()
.unwrap()
.current_dir(dir.path())
.arg("init")
.assert()
.code(0);
let settings_second = fs::read_to_string(dir.path().join(".claude/settings.json")).unwrap();
assert_eq!(
settings_first, settings_second,
"second init must not duplicate hooks"
);
let binary_dir = std::path::Path::new(env!("CARGO_BIN_EXE_pushkin"))
.parent()
.unwrap();
pushkin()
.unwrap()
.current_dir(dir.path())
.env("PATH", binary_dir)
.arg("doctor")
.assert()
.code(0);
}