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 FLOOR: &str = "lefthook.yml";
const FOREIGN_SCALAR_QUOTING_HEADER: &str = "pre-commit:\n commands:\n deploy:\n \
run: |\n echo one\n \
# GENERATED by pushkin init — a foreign script quoting our header\n \
echo two\n \
# pushkin:begin pushkin-v2 — GENERATED by pushkin init; do not hand-edit\n \
pushkin:\n run: pushkin check --staged --json\n \
# pushkin:end pushkin-v2\n";
const FOREIGN_SCALAR_QUOTING_MARKERS: &str = "pre-commit:\n commands:\n deploy:\n \
run: |\n echo before\n \
# pushkin:begin pushkin-v2 — GENERATED by pushkin init; do not hand-edit\n \
echo inside\n \
# pushkin:end pushkin-v2\n \
echo after\n";
const WIDE_INDENT_FOREIGN: &str = "pre-commit:\n commands:\n ruff:\n \
run: uv run ruff check {staged_files}\n";
const COMMENTS_ONLY_USER_FILE: &str = "# team notes\n# more notes\npre-commit:\n commands:\n";
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 binary_dir() -> Result<String, Box<dyn std::error::Error>> {
Ok(Path::new(env!("CARGO_BIN_EXE_pushkin"))
.parent()
.ok_or("binary under test has no parent")?
.to_string_lossy()
.into_owned())
}
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("PATH", binary_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 read_floor(dir: &Path) -> Result<String, Box<dyn std::error::Error>> {
Ok(fs::read_to_string(dir.join(FLOOR))?)
}
fn entry_indent(content: &str, key: &str) -> Option<usize> {
content
.lines()
.find(|line| line.trim() == key)
.map(|line| line.len() - line.trim_start().len())
}
#[test]
fn a_pushkin_comment_quoted_inside_a_foreign_scalar_survives_an_upgrade() -> TestResult {
let dir = repo()?;
fs::write(dir.path().join(FLOOR), FOREIGN_SCALAR_QUOTING_HEADER)?;
let (code, output) = run(dir.path(), &["init", "--agent", "lefthook"])?;
assert_eq!(code, Some(0), "upgrade must succeed: {output}");
let content = read_floor(dir.path())?;
assert!(
content.contains("a foreign script quoting our header"),
"the quoted line inside the foreign scalar is the user's content \
and must survive byte-for-byte: {content}"
);
assert!(
content.contains("echo one") && content.contains("echo two"),
"the foreign scalar body is untouched: {content}"
);
assert_eq!(
content.matches("# pushkin:begin").count(),
1,
"the real v2 block was replaced by exactly one current block: {content}"
);
assert!(
!content.contains("pushkin-v2"),
"the real v2 markers are gone; only current markers remain: {content}"
);
Ok(())
}
#[test]
fn marker_text_inside_a_foreign_scalar_is_not_a_block_boundary() -> TestResult {
let dir = repo()?;
fs::write(dir.path().join(FLOOR), FOREIGN_SCALAR_QUOTING_MARKERS)?;
let (code, output) = run(dir.path(), &["init", "--agent", "lefthook"])?;
assert_eq!(code, Some(0), "install must succeed: {output}");
let content = read_floor(dir.path())?;
assert!(
content.contains("echo before")
&& content.contains("echo inside")
&& content.contains("echo after"),
"the foreign scalar must survive intact — marker-shaped text inside \
it is not a pushkin block: {content}"
);
assert!(
content.contains("pushkin:begin pushkin-v2") && content.contains("pushkin:end pushkin-v2"),
"the quoted marker lines are the user's content and must survive: {content}"
);
assert!(
content.contains("pushkin-v3") && content.contains("command -v pushkin"),
"our block is installed alongside, not spliced into the scalar: {content}"
);
Ok(())
}
fn lefthook_available() -> bool {
StdCommand::new("lefthook")
.arg("version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|status| status.success())
}
#[test]
fn insertion_respects_the_commands_entry_indent() -> TestResult {
let dir = repo()?;
fs::write(dir.path().join(FLOOR), WIDE_INDENT_FOREIGN)?;
let (code, output) = run(dir.path(), &["init", "--agent", "lefthook"])?;
assert_eq!(code, Some(0), "install must succeed: {output}");
let content = read_floor(dir.path())?;
let ruff = entry_indent(&content, "ruff:").ok_or("ruff entry missing")?;
let pushkin = entry_indent(&content, "pushkin:").ok_or("pushkin entry missing")?;
assert_eq!(
pushkin, ruff,
"our entry must sit at the same indent as its siblings: {content}"
);
if lefthook_available() {
let git = StdCommand::new("git")
.current_dir(dir.path())
.args(["init", "-q", "."])
.status()?;
assert!(git.success(), "fixture git init must succeed");
let validate = StdCommand::new("lefthook")
.current_dir(dir.path())
.arg("validate")
.output()?;
assert!(
validate.status.success(),
"real `lefthook validate` must accept the wide-indent merge: {}{}",
String::from_utf8_lossy(&validate.stdout),
String::from_utf8_lossy(&validate.stderr)
);
}
Ok(())
}
#[test]
fn remove_keeps_a_user_file_that_carries_its_own_comments() -> TestResult {
let dir = repo()?;
fs::write(dir.path().join(FLOOR), COMMENTS_ONLY_USER_FILE)?;
run(dir.path(), &["init", "--agent", "lefthook"])?;
let (code, output) = run(dir.path(), &["init", "--remove-agent", "lefthook"])?;
assert_eq!(code, Some(0), "remove must succeed: {output}");
assert!(
dir.path().join(FLOOR).exists(),
"a file carrying the user's own comments is not pushkin's alone \
and must not be deleted"
);
let content = read_floor(dir.path())?;
assert!(
content.contains("# team notes") && content.contains("# more notes"),
"the user's comments survive removal: {content}"
);
assert!(
!content.contains("pushkin"),
"no pushkin trace survives removal: {content}"
);
Ok(())
}