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_WITH_READ_ONLY: &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"]
read_only_paths = ["crates/**/tests/**"]
"#;
const COMMITTED_TEST: &str = "crates/pushkin-cli/tests/committed_suite.rs";
const NEW_TEST: &str = "crates/pushkin-cli/tests/brand_new_red_suite.rs";
fn git(dir: &Path, args: &[&str]) -> Result<(), Box<dyn std::error::Error>> {
let status = StdCommand::new("git")
.current_dir(dir)
.args(args)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()?;
if !status.success() {
return Err(format!("git {args:?} failed in fixture").into());
}
Ok(())
}
fn repo() -> Result<tempfile::TempDir, Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
fs::write(dir.path().join("pushkin.toml"), MANIFEST_WITH_READ_ONLY)?;
fs::create_dir_all(dir.path().join("crates/pushkin-cli/tests"))?;
fs::write(
dir.path().join(COMMITTED_TEST),
"// a committed, read-only suite\n",
)?;
git(dir.path(), &["init", "-q", "."])?;
git(dir.path(), &["add", "-A"])?;
git(
dir.path(),
&[
"-c",
"user.name=Gate Suite",
"-c",
"user.email=gate@test",
"commit",
"-qm",
"fixture",
],
)?;
Ok(dir)
}
fn check_write(
dir: &Path,
file_path: &str,
) -> Result<(Option<i32>, String), Box<dyn std::error::Error>> {
let payload = format!(
r#"{{"tool_name":"Write","tool_input":{{"file_path":"{file_path}","content":"// agent write"}}}}"#
);
let output = Command::cargo_bin("pushkin")?
.current_dir(dir)
.write_stdin(payload)
.arg("check")
.output()?;
Ok((
output.status.code(),
format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
),
))
}
#[test]
fn editing_a_committed_file_under_a_read_only_path_is_denied() -> TestResult {
let dir = repo()?;
let (code, output) = check_write(dir.path(), COMMITTED_TEST)?;
assert_eq!(
code,
Some(2),
"an agent edit to a committed test must be denied: {output}"
);
assert!(
output.contains("pushkin.read_only_path"),
"the deny names its own rule: {output}"
);
Ok(())
}
#[test]
fn creating_a_new_file_under_a_read_only_path_is_allowed() -> TestResult {
let dir = repo()?;
let (code, output) = check_write(dir.path(), NEW_TEST)?;
assert_eq!(
code,
Some(0),
"a NEW file under the glob is a phase-authored RED suite — allowed \
with no ceremony (the N10 mirror): {output}"
);
Ok(())
}
#[test]
fn an_uncommitted_file_stays_editable_until_committed() -> TestResult {
let dir = repo()?;
fs::write(dir.path().join(NEW_TEST), "// draft red suite\n")?;
let (code, output) = check_write(dir.path(), NEW_TEST)?;
assert_eq!(
code,
Some(0),
"the authoring window: a file on disk but not in HEAD is still the \
author's to iterate on: {output}"
);
Ok(())
}
#[test]
fn a_waiver_cannot_clear_a_read_only_path_denial() -> TestResult {
let dir = repo()?;
let waive = Command::cargo_bin("pushkin")?
.current_dir(dir.path())
.env("GIT_AUTHOR_NAME", "Read Only Suite")
.args([
"waive",
"pushkin.read_only_path",
"--path",
"crates/**/tests/**",
"--ttl",
"2h",
"--reason",
"read-only-paths gate suite",
])
.output()?;
assert!(
waive.status.success(),
"the grant itself records; suppression is what must refuse: {}",
String::from_utf8_lossy(&waive.stderr)
);
let (code, output) = check_write(dir.path(), COMMITTED_TEST)?;
assert_eq!(
code,
Some(2),
"read-only paths are unwaivable by construction, like protected \
paths: {output}"
);
assert!(
output.contains("pushkin.read_only_path"),
"the deny still names its rule: {output}"
);
Ok(())
}
#[test]
fn protected_paths_still_deny_alongside_the_new_gate() -> TestResult {
let dir = repo()?;
let (code, output) = check_write(dir.path(), "pushkin.toml")?;
assert_eq!(
code,
Some(2),
"the protected-path deny is unchanged beside read_only_paths: {output}"
);
assert!(
output.contains("pushkin.protected_path"),
"protected keeps its own rule id: {output}"
);
Ok(())
}