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"]
[[mappings]]
glob = "app/api/**/*.ts"
contracts = ["user"]
require = "boundary-validation"
[gates]
protected_paths = ["pushkin.toml"]
"#;
fn repo() -> std::io::Result<tempfile::TempDir> {
let dir = tempfile::tempdir()?;
fs::write(dir.path().join("pushkin.toml"), MANIFEST)?;
Ok(dir)
}
fn session_start(session: &str, source: &str) -> String {
serde_json::json!({
"session_id": session,
"hook_event_name": "SessionStart",
"source": source,
})
.to_string()
}
fn run_hook(dir: &tempfile::TempDir, payload: &str) -> (i32, String) {
let Ok(mut command) = Command::cargo_bin("pushkin") else {
return (-1, "cargo_bin resolution failed".to_owned());
};
let Ok(output) = command
.current_dir(dir.path())
.args(["hook", "claude"])
.write_stdin(payload.to_owned())
.output()
else {
return (-1, "spawn failed".to_owned());
};
(
output.status.code().unwrap_or(-1),
String::from_utf8_lossy(&output.stdout).into_owned(),
)
}
#[test]
fn session_start_injects_digest_once_per_epoch() {
let dir = repo().unwrap();
let (code, stdout) = run_hook(&dir, &session_start("s1", "startup"));
assert_eq!(code, 0);
let json: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|_| panic!("SessionStart emits JSON, got: {stdout}"));
let context = json["hookSpecificOutput"]["additionalContext"]
.as_str()
.unwrap_or_default();
assert!(
context.contains("app/api/**/*.ts"),
"digest carries manifest facts: {context}"
);
let (repeat_code, repeat_stdout) = run_hook(&dir, &session_start("s1", "resume"));
assert_eq!(repeat_code, 0);
assert_eq!(
repeat_stdout.trim(),
"",
"digest already delivered this epoch — never stacked (addendum §6)"
);
}
#[test]
fn compact_source_clears_and_reinjects() {
let dir = repo().unwrap();
run_hook(&dir, &session_start("s1", "startup"));
let (_, deduped) = run_hook(&dir, &session_start("s1", "resume"));
assert_eq!(deduped.trim(), "", "precondition: epoch already delivered");
let (code, stdout) = run_hook(&dir, &session_start("s1", "compact"));
assert_eq!(code, 0);
assert!(
stdout.contains("app/api/**/*.ts"),
"compaction cleared the scope — digest re-emitted in full (§7.3): {stdout}"
);
}
#[test]
fn init_claude_installs_session_start_entry() {
let dir = repo().unwrap();
let Ok(mut command) = Command::cargo_bin("pushkin") else {
panic!("cargo_bin resolution failed");
};
let status = command
.current_dir(dir.path())
.args(["init", "--agent", "claude"])
.output()
.unwrap()
.status;
assert!(status.success());
let settings = fs::read_to_string(dir.path().join(".claude/settings.json")).unwrap();
assert!(
settings.contains("SessionStart"),
"SessionStart entry installed for lifecycle injection: {settings}"
);
}