use assert_cmd::Command as AssertCommand;
use std::fs;
use std::path::Path;
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
type TestResult = Result<(), Box<dyn std::error::Error>>;
const ROOT_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]
"#;
const PERMISSIVE_MANIFEST: &str = r#"
version = 1
canonical = "json-schema-2020-12"
authoring = "zod"
[[contracts]]
name = "user"
source = "contracts/user.zod.ts"
emit = ["zod"]
[[mappings]]
glob = "nothing/**/*.ts"
contracts = ["user"]
require = "boundary-validation"
[gates]
"#;
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 HANDLER_PATH: &str = "app/api/users/route.ts";
fn repo() -> Result<tempfile::TempDir, Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
fs::write(dir.path().join("pushkin.toml"), ROOT_MANIFEST)?;
fs::create_dir_all(dir.path().join("contracts"))?;
fs::write(
dir.path().join("contracts/user.zod.ts"),
"export const UserCreateSchema = 1;\n",
)?;
Ok(dir)
}
fn payload() -> String {
serde_json::json!({
"session_id": "f73p3",
"tool_name": "Write",
"tool_input": { "file_path": HANDLER_PATH, "content": NONCONFORMING }
})
.to_string()
}
struct Verdict {
decision: String,
served_warm: bool,
}
fn ask(dir: &Path, manifest_env: Option<&str>) -> Result<Verdict, Box<dyn std::error::Error>> {
let mut command = AssertCommand::cargo_bin("pushkin")?;
command.current_dir(dir).args(["hook", "claude"]);
command.env_remove("PUSHKIN_DAEMON");
if let Some(value) = manifest_env {
command.env("PUSHKIN_MANIFEST", value);
}
let output = command.write_stdin(payload()).output()?;
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
let decision = if stdout.trim().is_empty() {
"allow".to_owned()
} else {
let json: serde_json::Value = serde_json::from_str(&stdout)?;
json["hookSpecificOutput"]["permissionDecision"]
.as_str()
.unwrap_or("unparseable")
.to_owned()
};
Ok(Verdict {
decision,
served_warm: stderr.contains("warm daemon"),
})
}
struct Daemon {
child: Child,
}
impl Daemon {
fn start(dir: &Path, manifest_env: Option<&str>) -> Result<Self, Box<dyn std::error::Error>> {
let exe = AssertCommand::cargo_bin("pushkin")?;
let mut command = Command::new(exe.get_program());
command
.current_dir(dir)
.args(["daemon", "serve"])
.stdout(Stdio::null())
.stderr(Stdio::piped());
if let Some(value) = manifest_env {
command.env("PUSHKIN_MANIFEST", value);
}
let child = command.spawn()?;
let socket = dir.join(".pushkin/daemon.sock");
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
if socket.exists() {
return Ok(Self { child });
}
std::thread::sleep(Duration::from_millis(50));
}
Err("daemon never bound its socket".into())
}
fn stop(mut self) -> Result<String, Box<dyn std::error::Error>> {
self.child.kill()?;
let output = self.child.wait_with_output()?;
Ok(String::from_utf8_lossy(&output.stderr).into_owned())
}
}
fn settle() {
std::thread::sleep(Duration::from_millis(1200));
}
#[test]
fn a_change_to_the_resolved_manifest_reloads_and_the_warm_verdict_follows() -> TestResult {
let dir = repo()?;
let daemon = Daemon::start(dir.path(), None)?;
let before = ask(dir.path(), None)?;
assert!(before.served_warm, "the daemon must be the one answering");
assert_eq!(before.decision, "deny", "baseline under the root manifest");
fs::write(dir.path().join("pushkin.toml"), PERMISSIVE_MANIFEST)?;
settle();
let after = ask(dir.path(), None)?;
assert!(after.served_warm, "still warm after the reload");
assert_eq!(
after.decision, "allow",
"the RESOLVED manifest changed, so the warm verdict must follow it"
);
let _ = daemon.stop()?;
Ok(())
}
#[test]
fn a_change_to_a_nested_manifest_does_not_reload() -> TestResult {
let dir = repo()?;
fs::create_dir_all(dir.path().join("sub"))?;
let daemon = Daemon::start(dir.path(), None)?;
let before = ask(dir.path(), None)?;
assert!(before.served_warm);
assert_eq!(before.decision, "deny");
fs::write(dir.path().join("sub/pushkin.toml"), PERMISSIVE_MANIFEST)?;
settle();
let after = ask(dir.path(), None)?;
assert!(
after.served_warm,
"still warm — the daemon is still answering"
);
assert_eq!(
after.decision, "deny",
"a NESTED manifest is not the governing one; adopting it lets an agent \
disarm the gate with a write the gate permits"
);
let _ = daemon.stop()?;
Ok(())
}
#[test]
fn creating_a_nested_manifest_is_no_different_from_editing_one() -> TestResult {
let dir = repo()?;
fs::create_dir_all(dir.path().join("deep/er"))?;
let daemon = Daemon::start(dir.path(), None)?;
assert_eq!(ask(dir.path(), None)?.decision, "deny");
fs::write(dir.path().join("deep/er/pushkin.toml"), PERMISSIVE_MANIFEST)?;
settle();
let after = ask(dir.path(), None)?;
assert!(after.served_warm);
assert_eq!(
after.decision, "deny",
"creation and edit are the same event class; both must be declined"
);
let _ = daemon.stop()?;
Ok(())
}
#[test]
fn the_daemon_records_that_it_declined_to_reload() -> TestResult {
let dir = repo()?;
fs::create_dir_all(dir.path().join("sub"))?;
let daemon = Daemon::start(dir.path(), None)?;
let _ = ask(dir.path(), None)?;
fs::write(dir.path().join("sub/pushkin.toml"), PERMISSIVE_MANIFEST)?;
settle();
let log = daemon.stop()?;
assert!(
log.contains("declined"),
"a nested manifest changing and nothing happening is indistinguishable \
from a dead watcher; the daemon must SAY it declined: {log:?}"
);
assert!(
log.contains("pushkin.toml"),
"the record must name the file it declined, or it cannot be acted on: {log:?}"
);
Ok(())
}
#[test]
fn under_pushkin_manifest_only_that_file_reloads() -> TestResult {
let dir = repo()?;
let elsewhere = dir.path().join("elsewhere.toml");
fs::write(&elsewhere, ROOT_MANIFEST)?;
let env = elsewhere.to_string_lossy().into_owned();
let daemon = Daemon::start(dir.path(), Some(&env))?;
assert_eq!(ask(dir.path(), Some(&env))?.decision, "deny");
fs::write(dir.path().join("pushkin.toml"), PERMISSIVE_MANIFEST)?;
settle();
assert_eq!(
ask(dir.path(), Some(&env))?.decision,
"deny",
"PUSHKIN_MANIFEST is in force; the root manifest does not govern"
);
fs::write(&elsewhere, PERMISSIVE_MANIFEST)?;
settle();
let after = ask(dir.path(), Some(&env))?;
assert!(after.served_warm);
assert_eq!(
after.decision, "allow",
"the file PUSHKIN_MANIFEST names is the one that reloads"
);
let _ = daemon.stop()?;
Ok(())
}
#[test]
fn warm_and_cold_agree_in_every_case() -> TestResult {
let dir = repo()?;
let cases: [(&str, &str); 3] = [
("baseline", ""),
("nested edited", "sub"),
("nested created deeper", "deep/er"),
];
for (_, nested) in cases {
if !nested.is_empty() {
fs::create_dir_all(dir.path().join(nested))?;
}
}
let daemon = Daemon::start(dir.path(), None)?;
for (label, nested) in cases {
if !nested.is_empty() {
fs::write(
dir.path().join(nested).join("pushkin.toml"),
PERMISSIVE_MANIFEST,
)?;
settle();
}
let warm = ask(dir.path(), None)?;
assert!(warm.served_warm, "{label}: the daemon must be answering");
let mut cold = AssertCommand::cargo_bin("pushkin")?;
cold.current_dir(dir.path())
.args(["hook", "claude"])
.env("PUSHKIN_DAEMON", "off");
let out = cold.write_stdin(payload()).output()?;
let cold_decision = if String::from_utf8_lossy(&out.stdout).trim().is_empty() {
"allow"
} else {
"deny"
};
assert_eq!(
warm.decision, cold_decision,
"{label}: warm and cold must agree — divergence between them IS \
the defect class this pass closes"
);
}
let _ = daemon.stop()?;
Ok(())
}