use assert_cmd::Command;
use std::fs;
use std::path::Path;
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"]
"#;
fn repo() -> Result<tempfile::TempDir, Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
fs::write(dir.path().join("pushkin.toml"), MANIFEST)?;
Ok(dir)
}
fn run(dir: &Path, args: &[&str]) -> Result<(Option<i32>, String), Box<dyn std::error::Error>> {
let output = Command::cargo_bin("pushkin")?
.current_dir(dir)
.env("HERMES_HOME", dir.join(".hermes-home"))
.args(args)
.output()?;
Ok((
output.status.code(),
format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
),
))
}
fn install(dir: &Path, agent: &str) -> Result<String, Box<dyn std::error::Error>> {
let (code, output) = run(dir, &["init", "--agent", agent])?;
if code != Some(0) {
return Err(format!("init --agent {agent} failed: {output}").into());
}
Ok(output)
}
fn read(dir: &Path, relative: &str) -> Result<String, Box<dyn std::error::Error>> {
Ok(fs::read_to_string(dir.join(relative))?)
}
fn installing_binary() -> &'static str {
env!("CARGO_BIN_EXE_pushkin")
}
fn portable_artifacts(agent: &str) -> &'static [&'static str] {
match agent {
"claude" => &[".claude/settings.json"],
"codex" => &[".codex/hooks.json"],
"auggie" => &[".augment/hooks/pushkin.sh"],
"opencode" => &[".opencode/plugin/pushkin.ts"],
_ => &[],
}
}
#[test]
fn no_adapter_bakes_the_installing_binary_path_into_its_hook() -> TestResult {
let binary = installing_binary();
for agent in ["claude", "codex", "auggie", "opencode"] {
let dir = repo()?;
install(dir.path(), agent)?;
for artifact in portable_artifacts(agent) {
let content = read(dir.path(), artifact)?;
assert!(
!content.contains(binary),
"{agent}: {artifact} embeds the installing binary's absolute \
path, so it goes stale when the binary moves: {content}"
);
}
}
Ok(())
}
#[test]
fn every_adapter_emits_the_portable_hook_command() -> TestResult {
for agent in ["claude", "codex", "auggie", "opencode"] {
let dir = repo()?;
install(dir.path(), agent)?;
let expected = format!("pushkin hook {agent}");
let found = portable_artifacts(agent).iter().try_fold(
false,
|seen, artifact| -> Result<bool, Box<dyn std::error::Error>> {
Ok(seen || read(dir.path(), artifact)?.contains(&expected))
},
)?;
assert!(
found,
"{agent}: no generated artifact carries the A0-scheme command `{expected}`"
);
}
Ok(())
}
#[test]
fn hermes_plugin_invokes_the_portable_command() -> TestResult {
let dir = repo()?;
install(dir.path(), "hermes")?;
let plugin = read(dir.path(), ".hermes-home/plugins/pushkin-gate/__init__.py")?;
assert!(
!plugin.contains(installing_binary()),
"hermes plugin embeds the installing binary's absolute path: {plugin}"
);
assert!(
plugin.contains("pushkin"),
"hermes plugin must still invoke pushkin: {plugin}"
);
Ok(())
}
#[test]
fn auggie_settings_keeps_an_absolute_script_path_but_the_script_is_portable() -> TestResult {
let dir = repo()?;
install(dir.path(), "auggie")?;
let settings = read(dir.path(), ".augment/settings.json")?;
assert!(
settings.contains("pushkin.sh"),
"auggie settings must point at the wrapper script: {settings}"
);
assert!(
!settings.contains(installing_binary()),
"the script path is auggie's requirement; the BINARY path is not: {settings}"
);
let script = read(dir.path(), ".augment/hooks/pushkin.sh")?;
assert!(
script.contains("pushkin hook auggie"),
"the wrapper script body must use the portable command: {script}"
);
assert!(
!script.contains(installing_binary()),
"the wrapper script must not embed the installing binary path: {script}"
);
Ok(())
}
#[test]
fn claude_keeps_its_event_and_matcher_semantics() -> TestResult {
let dir = repo()?;
install(dir.path(), "claude")?;
let settings = read(dir.path(), ".claude/settings.json")?;
for needle in ["PreToolUse", "Stop", "SessionStart", "Write|Edit|MultiEdit"] {
assert!(
settings.contains(needle),
"claude semantics changed โ {needle} missing: {settings}"
);
}
Ok(())
}
#[test]
fn no_adapter_emits_an_echod_stdin_payload() -> TestResult {
for agent in ["claude", "codex", "auggie", "opencode"] {
let dir = repo()?;
install(dir.path(), agent)?;
for artifact in portable_artifacts(agent) {
let content = read(dir.path(), artifact)?;
assert!(
!content.contains("echo '{"),
"{agent}: {artifact} carries an echo'd payload, the quoting \
class L3(a) retired: {content}"
);
}
}
Ok(())
}
#[test]
fn claude_install_preserves_foreign_keys() -> TestResult {
let dir = repo()?;
fs::create_dir_all(dir.path().join(".claude"))?;
fs::write(
dir.path().join(".claude/settings.json"),
r#"{"model":"opus","hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"command","command":"my-own-hook"}]}]}}"#,
)?;
install(dir.path(), "claude")?;
let settings = read(dir.path(), ".claude/settings.json")?;
assert!(
settings.contains("\"model\""),
"foreign top-level key dropped: {settings}"
);
assert!(
settings.contains("my-own-hook"),
"foreign hook entry dropped: {settings}"
);
assert!(
settings.contains("pushkin hook claude"),
"our own entry missing: {settings}"
);
Ok(())
}
#[test]
fn codex_install_preserves_foreign_keys() -> TestResult {
let dir = repo()?;
fs::create_dir_all(dir.path().join(".codex"))?;
fs::write(
dir.path().join(".codex/hooks.json"),
r#"{"telemetry":false,"hooks":{"PreToolUse":[{"matcher":"shell","hooks":[{"type":"command","command":"team-hook"}]}]}}"#,
)?;
install(dir.path(), "codex")?;
let hooks = read(dir.path(), ".codex/hooks.json")?;
assert!(
hooks.contains("\"telemetry\""),
"foreign top-level key dropped: {hooks}"
);
assert!(
hooks.contains("team-hook"),
"foreign hook entry dropped: {hooks}"
);
Ok(())
}
#[test]
fn opencode_install_preserves_foreign_config_keys() -> TestResult {
let dir = repo()?;
fs::write(
dir.path().join("opencode.json"),
r#"{"theme":"dark","permission":{"edit":"allow"}}"#,
)?;
install(dir.path(), "opencode")?;
let config = read(dir.path(), "opencode.json")?;
assert!(
config.contains("\"theme\""),
"foreign top-level key dropped: {config}"
);
assert!(
config.contains("\"edit\""),
"foreign permission key dropped: {config}"
);
Ok(())
}
#[test]
fn reinstall_is_idempotent_for_every_adapter() -> TestResult {
for agent in ["claude", "codex", "auggie", "opencode"] {
let dir = repo()?;
install(dir.path(), agent)?;
let first: Vec<String> = portable_artifacts(agent)
.iter()
.map(|artifact| read(dir.path(), artifact))
.collect::<Result<_, _>>()?;
install(dir.path(), agent)?;
let second: Vec<String> = portable_artifacts(agent)
.iter()
.map(|artifact| read(dir.path(), artifact))
.collect::<Result<_, _>>()?;
assert_eq!(
first, second,
"{agent}: re-running init changed generated content โ not idempotent"
);
for content in &second {
assert!(
content.matches("pushkin hook").count() <= 3,
"{agent}: duplicated pushkin entries after re-run: {content}"
);
}
}
Ok(())
}
#[test]
fn stale_absolute_path_entry_is_replaced_not_duplicated() -> TestResult {
let dir = repo()?;
fs::create_dir_all(dir.path().join(".claude"))?;
let stale = format!(
r#"{{"hooks":{{"PreToolUse":[{{"_pushkin":"pushkin-v1","matcher":"Write|Edit|MultiEdit","hooks":[{{"type":"command","command":"{}/pushkin hook claude"}}]}}]}}}}"#,
"/Users/someone/target/release"
);
fs::write(dir.path().join(".claude/settings.json"), stale)?;
install(dir.path(), "claude")?;
let settings = read(dir.path(), ".claude/settings.json")?;
assert!(
!settings.contains("/Users/someone"),
"the stale absolute-path entry survived: {settings}"
);
assert_eq!(
settings.matches("Write|Edit|MultiEdit").count(),
1,
"the stale entry was duplicated instead of replaced: {settings}"
);
Ok(())
}
#[test]
fn fresh_target_gets_the_full_generated_file() -> TestResult {
let dir = repo()?;
install(dir.path(), "claude")?;
let settings = read(dir.path(), ".claude/settings.json")?;
for needle in ["PreToolUse", "Stop", "SessionStart"] {
assert!(
settings.contains(needle),
"fresh install must write the full pack โ {needle} missing: {settings}"
);
}
Ok(())
}