mod common;
use common::{missing, Repo};
use std::io::Write;
use std::process::{Command, Stdio};
fn push_range(r: &Repo, from: &str, to: &str) -> i32 {
let line = format!("refs/heads/main {to} refs/heads/main {from}\n");
let mut child = Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("--hooks-dir")
.arg(r.path(".git/hooks"))
.arg("pre-push-run-tests-js")
.current_dir(&r.dir)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn");
child
.stdin
.as_mut()
.unwrap()
.write_all(line.as_bytes())
.unwrap();
child.wait().expect("wait").code().unwrap_or(-1)
}
fn head(r: &Repo) -> String {
String::from_utf8_lossy(&r.git(&["rev-parse", "HEAD"]).stdout)
.trim()
.to_string()
}
#[test]
fn runs_a_packages_gate_and_passes_when_it_succeeds() {
if missing("npm") {
return;
}
let r = Repo::new();
r.stage(
"package.json",
r#"{"name":"t","scripts":{"test":"exit 0"}}"#,
);
r.commit("chore: base");
let base = head(&r);
r.stage("src/a.js", "const x = 1;\n");
r.commit("feat: a");
assert_eq!(push_range(&r, &base, &head(&r)), 0);
}
#[test]
fn fails_when_the_gate_fails() {
if missing("npm") {
return;
}
let r = Repo::new();
r.stage(
"package.json",
r#"{"name":"t","scripts":{"test":"exit 1"}}"#,
);
r.commit("chore: base");
let base = head(&r);
r.stage("src/a.js", "const x = 1;\n");
r.commit("feat: a");
assert_ne!(push_range(&r, &base, &head(&r)), 0);
}
#[test]
fn runs_the_gate_in_order_and_never_lint() {
if missing("npm") {
return;
}
let r = Repo::new();
r.stage(
"package.json",
r#"{"name":"t","scripts":{"typecheck":"echo typecheck>>ran.txt","lint":"echo lint>>ran.txt","test:unit":"echo unit>>ran.txt","test":"echo test>>ran.txt"}}"#,
);
r.commit("chore: base");
let base = head(&r);
r.stage("src/a.js", "const x = 1;\n");
r.commit("feat: a");
assert_eq!(push_range(&r, &base, &head(&r)), 0);
let ran = std::fs::read_to_string(r.path("ran.txt")).unwrap_or_default();
assert_eq!(
ran.lines().map(str::trim).collect::<Vec<_>>(),
vec!["typecheck", "unit", "test"]
);
assert!(
!ran.contains("lint"),
"lint belongs to pre-commit, not here"
);
}
#[test]
fn a_failure_skips_the_rest_of_the_gate() {
if missing("npm") {
return;
}
let r = Repo::new();
r.stage(
"package.json",
r#"{"name":"t","scripts":{"typecheck":"exit 1","test:unit":"echo unit>>ran.txt","test":"echo test>>ran.txt"}}"#,
);
r.commit("chore: base");
let base = head(&r);
r.stage("src/a.js", "const x = 1;\n");
r.commit("feat: a");
assert_ne!(push_range(&r, &base, &head(&r)), 0);
assert!(
!r.path("ran.txt").exists(),
"nothing after the failure should run"
);
}
fn push_new_branch(r: &Repo, branch: &str, tip: &str) -> i32 {
let zero = "0".repeat(tip.len());
let line = format!("refs/heads/{branch} {tip} refs/heads/{branch} {zero}\n");
let mut child = Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("--hooks-dir")
.arg(r.path(".git/hooks"))
.arg("pre-push-run-tests-js")
.current_dir(&r.dir)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn");
child
.stdin
.as_mut()
.unwrap()
.write_all(line.as_bytes())
.unwrap();
child.wait().expect("wait").code().unwrap_or(-1)
}
fn with_origin(r: &Repo) {
let origin = r.path(".git/test-origin.git");
r.git(&["init", "-q", "--bare", origin.to_str().expect("utf8")]);
r.git(&["remote", "add", "origin", origin.to_str().expect("utf8")]);
r.git(&["push", "-q", "--no-verify", "origin", "main"]);
}
#[test]
fn a_new_branch_is_judged_by_every_commit_not_just_its_tip() {
if missing("npm") {
return;
}
let r = Repo::new();
r.stage(
"package.json",
r#"{"name":"t","scripts":{"test":"exit 1"}}"#,
);
r.commit("chore: base");
with_origin(&r);
r.git(&["checkout", "-q", "-b", "feat/x"]);
r.stage("src/a.js", "const x = 1;\n");
r.commit("feat: the js change");
r.stage("README.md", "docs\n");
r.commit("docs: on top of it");
assert_ne!(
push_new_branch(&r, "feat/x", &head(&r)),
0,
"the failing package was two commits back and the gate never ran"
);
}
#[test]
fn a_file_reverted_later_in_the_same_push_still_selects_its_package() {
if missing("npm") {
return;
}
let r = Repo::new();
r.stage(
"package.json",
r#"{"name":"t","scripts":{"test":"exit 1"}}"#,
);
r.stage("src/a.js", "const x = 1;\n");
r.commit("chore: base");
let base = head(&r);
r.stage("src/a.js", "const x = 2;\n");
r.commit("feat: touch the js");
r.stage("src/a.js", "const x = 1;\n");
r.commit("revert: put it back");
assert_ne!(
push_range(&r, &base, &head(&r)),
0,
"the endpoints' trees are identical, but a pushed commit touched .js"
);
}
#[test]
fn a_non_js_change_runs_no_gate() {
let r = Repo::new();
r.stage(
"package.json",
r#"{"name":"t","scripts":{"test":"exit 1"}}"#,
);
r.commit("chore: seed");
let base = head(&r);
r.stage("README.md", "docs\n");
r.commit("docs: readme");
assert_eq!(push_range(&r, &base, &head(&r)), 0);
}
fn push_range_out(r: &Repo, from: &str, to: &str) -> (i32, String) {
let line = format!("refs/heads/main {to} refs/heads/main {from}\n");
let mut child = Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("--hooks-dir")
.arg(r.path(".git/hooks"))
.arg("pre-push-run-tests-js")
.current_dir(&r.dir)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn");
child
.stdin
.as_mut()
.unwrap()
.write_all(line.as_bytes())
.unwrap();
let out = child.wait_with_output().expect("wait");
(
out.status.code().unwrap_or(-1),
format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
),
)
}
fn repo_with_failing_typecheck() -> (Repo, String) {
let r = Repo::new();
r.stage(
"package.json",
r#"{"name":"t","scripts":{"typecheck":"exit 1","test":"exit 0"}}"#,
);
r.commit("chore: base");
let base = head(&r);
r.stage("src/a.ts", "export const x = 1\n");
r.commit("feat: add a ts file");
(r, base)
}
fn trust_manifest(r: &Repo) {
let out = Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("trust")
.current_dir(&r.dir)
.output()
.expect("amont trust");
assert!(
out.status.success(),
"could not trust the manifest: {}",
String::from_utf8_lossy(&out.stderr)
);
}
fn install_hooks(r: &Repo) {
let out = Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("init")
.current_dir(&r.dir)
.stdin(Stdio::null())
.output()
.expect("amont init");
assert!(
out.status.success(),
"could not install hooks: {}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
}
fn verified_commit(r: &Repo, msg: &str) {
let out = r.git(&["commit", "-q", "-m", msg]);
assert!(
out.status.success(),
"verified commit failed: {}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
}
fn stamped_repo() -> (Repo, String) {
let r = Repo::new();
r.stage(
"package.json",
r#"{"name":"t","scripts":{"typecheck":"node tc.js","test":"exit 0"}}"#,
);
r.stage("tc.js", "require('fs').appendFileSync('tc.log','x')\n");
r.commit("chore: base");
let base = head(&r);
r.stage(
"amont.conf",
"pre-commit typecheck *.ts,*.tsx block node tc.js\n",
);
r.commit("chore: typecheck at commit time");
trust_manifest(&r);
install_hooks(&r);
(r, base)
}
fn typecheck_runs(r: &Repo) -> usize {
std::fs::read_to_string(r.dir.join("tc.log"))
.map(|s| s.len())
.unwrap_or(0)
}
#[test]
fn a_declared_pre_commit_check_removes_that_script_from_the_push_gate() {
if missing("npm") {
return;
}
let (r, base) = stamped_repo();
r.stage("src/a.ts", "export const x = 1\n");
verified_commit(&r, "feat: add a ts file");
assert_eq!(
typecheck_runs(&r),
1,
"the commit itself should have run typecheck exactly once"
);
let (code, out) = push_range_out(&r, &base, &head(&r));
assert_eq!(
code, 0,
"the push failed despite a stamped, gated typecheck:\n{out}"
);
assert!(
out.contains("typecheck") && out.contains("gated at commit"),
"the push did not say typecheck had moved:\n{out}"
);
assert_eq!(
typecheck_runs(&r),
1,
"the push re-ran a script the commit already stamped:\n{out}"
);
}
#[test]
fn a_no_verify_commit_brings_the_push_gate_back() {
if missing("npm") {
return;
}
let (r, base) = stamped_repo();
r.stage("src/a.ts", "export const x = 1\n");
r.commit("feat: dodge the hooks");
let (code, out) = push_range_out(&r, &base, &head(&r));
assert_eq!(
code, 0,
"typecheck passes when run — only WHO runs it moved:\n{out}"
);
assert!(
out.contains("no record of it"),
"the push must say why the gate is running:\n{out}"
);
assert!(
!out.contains("gated at commit"),
"claimed commit-time cover for a commit that dodged the hooks:\n{out}"
);
assert_eq!(
typecheck_runs(&r),
1,
"the gate did not run typecheck for the unchecked commit:\n{out}"
);
}
#[test]
fn one_unstamped_commit_re_runs_the_gate_for_the_push() {
if missing("npm") {
return;
}
let (r, base) = stamped_repo();
r.stage("src/a.ts", "export const x = 1\n");
verified_commit(&r, "feat: checked");
r.stage("src/b.ts", "export const y = 2\n");
r.commit("feat: unchecked");
let (code, out) = push_range_out(&r, &base, &head(&r));
assert_eq!(code, 0, "{out}");
assert!(
out.contains("no record of it"),
"one unstamped commit must bring the gate back:\n{out}"
);
assert_eq!(
typecheck_runs(&r),
2,
"commit-time once, push-time once for the unchecked commit:\n{out}"
);
}
#[test]
fn an_untrusted_declaration_leaves_the_push_gate_alone() {
if missing("npm") {
return;
}
let (r, base) = repo_with_failing_typecheck();
r.stage(
"amont.conf",
"pre-commit typecheck *.ts,*.tsx block npm run typecheck\n",
);
r.commit("chore: declare typecheck, untrusted");
let (code, out) = push_range_out(&r, &base, &head(&r));
assert_ne!(
code, 0,
"an untrusted declaration removed the push gate — nothing typechecks now:\n{out}"
);
}
#[test]
fn a_skipped_declaration_leaves_the_push_gate_alone() {
if missing("npm") {
return;
}
let (r, base) = repo_with_failing_typecheck();
r.stage(
"amont.conf",
"pre-commit typecheck *.ts,*.tsx block npm run typecheck\n",
);
r.commit("chore: typecheck at commit time");
trust_manifest(&r);
r.git(&["config", "--add", "hook.skip", "pre-commit-typecheck"]);
let (code, out) = push_range_out(&r, &base, &head(&r));
assert_ne!(
code, 0,
"a skipped declaration removed the push gate — nothing typechecks now:\n{out}"
);
}
#[test]
fn a_pre_push_declaration_does_not_count_as_commit_time() {
if missing("npm") {
return;
}
let (r, base) = repo_with_failing_typecheck();
r.stage(
"amont.conf",
"pre-push typecheck *.ts,*.tsx block npm run typecheck\n",
);
r.commit("chore: declare on the wrong stage");
trust_manifest(&r);
let (code, out) = push_range_out(&r, &base, &head(&r));
assert_ne!(
code, 0,
"a pre-push declaration was treated as commit-time cover:\n{out}"
);
}
#[test]
fn with_no_declaration_the_gate_is_unchanged() {
if missing("npm") {
return;
}
let (r, base) = repo_with_failing_typecheck();
let (code, out) = push_range_out(&r, &base, &head(&r));
assert_ne!(code, 0, "typecheck did not run:\n{out}");
assert!(
!out.contains("gated at commit"),
"said something about commit-time gating with nothing declared:\n{out}"
);
}
#[test]
fn a_warn_declaration_leaves_the_push_gate_alone() {
if missing("npm") {
return;
}
let (r, base) = repo_with_failing_typecheck();
r.stage(
"amont.conf",
"pre-commit typecheck *.ts,*.tsx warn npm run typecheck\n",
);
r.commit("chore: typecheck at commit time, warn only");
trust_manifest(&r);
let (code, out) = push_range_out(&r, &base, &head(&r));
assert_ne!(
code, 0,
"a warn declaration removed the push gate — a failing commit and a green push:\n{out}"
);
assert!(
!out.contains("gated at commit"),
"claimed commit-time cover a warn declaration cannot give:\n{out}"
);
}
#[test]
fn a_severity_override_to_warn_leaves_the_push_gate_alone() {
if missing("npm") {
return;
}
let (r, base) = repo_with_failing_typecheck();
r.stage(
"amont.conf",
"pre-commit typecheck *.ts,*.tsx block npm run typecheck\n",
);
r.commit("chore: typecheck at commit time");
trust_manifest(&r);
r.git(&["config", "amont.severity.pre-commit-typecheck", "warn"]);
let (code, out) = push_range_out(&r, &base, &head(&r));
assert_ne!(
code, 0,
"a severity override downgraded the commit check and the push gate still deferred to it:\n{out}"
);
}
#[test]
fn a_push_outside_the_declared_scope_runs_the_full_gate() {
if missing("npm") {
return;
}
let r = Repo::new();
r.stage(
"package.json",
r#"{"name":"t","scripts":{"typecheck":"exit 1","test":"exit 0"}}"#,
);
r.commit("chore: base");
let base = head(&r);
r.stage("src/legacy.js", "const x = 1;\n");
r.commit("feat: a js change the ts scope never saw");
r.stage(
"amont.conf",
"pre-commit typecheck *.ts,*.tsx block npm run typecheck\n",
);
r.commit("chore: typecheck at commit time");
trust_manifest(&r);
let (code, out) = push_range_out(&r, &base, &head(&r));
assert_ne!(
code, 0,
"a .js push was excused by a .ts-scoped declaration — checked at neither end:\n{out}"
);
assert!(
!out.contains("gated at commit"),
"claimed commit-time cover for files outside the declared scope:\n{out}"
);
}
#[test]
fn a_root_declaration_does_not_excuse_a_sub_package() {
if missing("npm") {
return;
}
let r = Repo::new();
r.stage(
"package.json",
r#"{"name":"root","scripts":{"typecheck":"exit 0","test":"exit 0"}}"#,
);
r.stage(
"packages/b/package.json",
r#"{"name":"b","scripts":{"typecheck":"exit 1"}}"#,
);
r.commit("chore: base");
let base = head(&r);
r.stage("packages/b/src/a.ts", "export const x = 1\n");
r.commit("feat: change the sub-package");
r.stage(
"amont.conf",
"pre-commit typecheck *.ts,*.tsx block npm run typecheck\n",
);
r.commit("chore: typecheck at commit time");
trust_manifest(&r);
let (code, out) = push_range_out(&r, &base, &head(&r));
assert_ne!(
code, 0,
"the root declaration excused packages/b's own typecheck:\n{out}"
);
}
#[cfg(unix)]
#[test]
fn a_blocked_attempt_cannot_vouch_for_a_no_verify_retry() {
if missing("npm") {
return;
}
let r = Repo::new();
r.stage(
"package.json",
r#"{"name":"t","scripts":{"typecheck":"node tc.js","test":"exit 0"}}"#,
);
r.stage("tc.js", "require('fs').appendFileSync('tc.log','x')\n");
r.stage("deny.sh", "#!/bin/sh\nexit 1\n");
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(r.path("deny.sh"), std::fs::Permissions::from_mode(0o755))
.expect("chmod");
r.git(&["add", "deny.sh"]);
r.commit("chore: base");
let base = head(&r);
r.stage(
"amont.conf",
"pre-commit typecheck *.ts,*.tsx block node tc.js\n\
pre-commit deny * block ./deny.sh\n",
);
r.commit("chore: declare");
trust_manifest(&r);
install_hooks(&r);
r.stage("src/a.ts", "export const x = 1\n");
let attempt = r.git(&["commit", "-q", "-m", "feat: blocked attempt"]);
assert!(
!attempt.status.success(),
"the deny check should have blocked the commit"
);
assert_eq!(typecheck_runs(&r), 1, "typecheck itself ran and passed");
r.commit("feat: same tree, hooks dodged"); let (code, out) = push_range_out(&r, &base, &head(&r));
assert_eq!(code, 0, "{out}");
assert!(
out.contains("no record of it"),
"a blocked attempt's marker vouched for the retry:\n{out}"
);
assert_eq!(
typecheck_runs(&r),
2,
"the push gate must re-run typecheck for the unstamped commit:\n{out}"
);
}
#[test]
fn a_commit_dash_a_still_earns_its_stamp() {
if missing("npm") {
return;
}
let (r, base) = stamped_repo();
r.stage("src/a.ts", "export const x = 1\n");
verified_commit(&r, "feat: tracked first");
r.write("src/a.ts", "export const x = 2\n"); let out = r.git(&["commit", "-q", "-am", "feat: via commit -a"]);
assert!(
out.status.success(),
"commit -a failed: {}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let (code, out) = push_range_out(&r, &base, &head(&r));
assert_eq!(code, 0, "{out}");
assert!(
out.contains("gated at commit") && !out.contains("no record of it"),
"a commit -a lost its stamp — write-tree read the wrong index:\n{out}"
);
assert_eq!(
typecheck_runs(&r),
2,
"typecheck at each commit, never re-run at push:\n{out}"
);
}
#[test]
fn a_linked_worktree_commit_earns_a_stamp_the_push_sees() {
if missing("npm") {
return;
}
let (r, base) = stamped_repo();
r.stage("src/a.ts", "export const x = 1\n");
verified_commit(&r, "feat: main checkout");
let wt = r
.dir
.parent()
.expect("parent")
.join(format!("stamp-wt-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&wt);
let out = r.git(&[
"worktree",
"add",
"-b",
"feat/wt",
wt.to_str().expect("utf8"),
]);
assert!(out.status.success(), "worktree add failed");
std::fs::create_dir_all(wt.join("src")).expect("mkdir");
std::fs::write(wt.join("src/b.ts"), "export const y = 2\n").expect("write");
let wt_git = |args: &[&str]| {
let mut cmd = Command::new("git");
cmd.arg("-C").arg(&wt).args(args);
common::Repo::strip_git_env_impl(&mut cmd);
cmd.output().expect("git in worktree")
};
assert!(wt_git(&["add", "src/b.ts"]).status.success());
let out = wt_git(&["commit", "-q", "-m", "feat: from the worktree"]);
assert!(
out.status.success(),
"worktree commit failed: {}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let tip = String::from_utf8_lossy(&wt_git(&["rev-parse", "HEAD"]).stdout)
.trim()
.to_string();
let line = format!("refs/heads/feat/wt {tip} refs/heads/feat/wt {base}\n");
let mut child = Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("--hooks-dir")
.arg(r.path(".git/hooks"))
.arg("pre-push-run-tests-js")
.current_dir(&wt)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn");
child
.stdin
.as_mut()
.expect("stdin")
.write_all(line.as_bytes())
.expect("write");
let out = child.wait_with_output().expect("wait");
let text = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let _ = r.git(&["worktree", "remove", "--force", wt.to_str().expect("utf8")]);
assert_eq!(out.status.code(), Some(0), "{text}");
assert!(
text.contains("gated at commit") && !text.contains("no record of it"),
"a worktree commit's stamp was invisible to the push:\n{text}"
);
}
fn ledger(r: &Repo) -> Option<String> {
std::fs::read_to_string(r.path(".git/amont-bypasses")).ok()
}
#[test]
fn a_stamped_repo_starts_with_an_empty_ledger() {
let (r, _base) = stamped_repo();
assert_eq!(
ledger(&r),
None,
"the fixture's pre-install commits must not register"
);
}
#[test]
fn a_no_verify_commit_a_gate_covers_is_recorded_as_unverified() {
let (r, _base) = stamped_repo();
r.stage("src/b.ts", "export const y = 2\n");
r.commit("feat: dodge the gate"); let text = ledger(&r).expect("a bypassed gate leaves a ledger");
assert!(text.starts_with("amont-bypass-v1"), "{text:?}");
assert!(text.contains(" typecheck"), "{text:?}");
assert_eq!(text.lines().count(), 2, "one header, one event: {text:?}");
}
#[test]
fn a_verified_commit_records_nothing() {
if missing("node") {
return;
}
let (r, _base) = stamped_repo();
r.stage("src/b.ts", "export const y = 2\n");
verified_commit(&r, "feat: through the gate");
assert_eq!(ledger(&r), None, "a stamped commit is not a bypass");
}
#[test]
fn a_commit_outside_the_declarations_scope_records_nothing() {
let (r, _base) = stamped_repo();
r.stage("README.md", "docs only\n");
r.commit("docs: no ts touched");
assert_eq!(
ledger(&r),
None,
"the gate never covered this commit, so nothing was dodged"
);
}
#[test]
fn a_repo_that_declares_no_gate_records_nothing() {
let r = Repo::new();
r.stage("a.txt", "hello\n");
r.commit("chore: base");
install_hooks(&r);
r.stage("b.txt", "more\n");
r.commit("chore: more");
assert_eq!(ledger(&r), None, "no declaration, no bypass to count");
}
#[test]
fn a_blocked_commit_retried_with_no_verify_is_recorded() {
if missing("node") {
return;
}
let r = Repo::new();
r.stage("tc.js", "process.exit(1)\n");
r.commit("chore: base");
r.stage(
"amont.conf",
"pre-commit typecheck *.ts,*.tsx block node tc.js\n",
);
r.commit("chore: gate at commit time");
trust_manifest(&r);
install_hooks(&r);
r.stage("src/a.ts", "export const x = 1\n");
let attempt = r.git(&["commit", "-q", "-m", "feat: blocked"]);
assert!(!attempt.status.success(), "the gate must block the attempt");
r.commit("feat: bypassed after the block");
let text = ledger(&r).expect("the retry dodged a gate that said no");
assert!(text.contains(" typecheck"), "{text:?}");
}
#[test]
fn a_partially_stamped_commit_records_only_the_missing_script() {
if missing("node") {
return;
}
let (r, _base) = stamped_repo();
r.stage(
"amont.conf",
"pre-commit typecheck *.ts,*.tsx block node tc.js\n\
pre-commit test *.ts,*.tsx block amont-no-such-tool-xyz\n",
);
r.stage("src/b.ts", "export const y = 2\n");
trust_manifest(&r);
verified_commit(&r, "feat: half a gate");
let text = ledger(&r).expect("the unavailable half goes unverified");
assert!(text.contains(" test"), "{text:?}");
assert!(
!text.contains(" typecheck"),
"the stamped half must not be counted: {text:?}"
);
}
#[test]
fn amont_list_json_reports_the_bypass_ledger() {
let (r, _base) = stamped_repo();
r.stage("src/b.ts", "export const y = 2\n");
r.commit("feat: dodge the gate");
let out = r.run(&["list", "--json"]);
assert_eq!(out.code, 0, "{}", out.output());
assert!(
out.stdout.contains("\"bypasses\":{\"total\":1"),
"{}",
out.stdout
);
assert!(
out.stdout.contains("\"script\":\"typecheck\",\"count\":1"),
"{}",
out.stdout
);
}
#[test]
fn amont_list_text_names_the_dodged_script() {
let (r, _base) = stamped_repo();
r.stage("src/b.ts", "export const y = 2\n");
r.commit("feat: dodge the gate");
let out = r.run(&["list"]);
assert!(out.stdout.contains("unverified commits"), "{}", out.stdout);
assert!(out.stdout.contains("typecheck"), "{}", out.stdout);
}
#[test]
fn amont_uninstall_forgets_the_bypass_ledger() {
let (r, _base) = stamped_repo();
r.stage("src/b.ts", "export const y = 2\n");
r.commit("feat: dodge the gate");
assert!(ledger(&r).is_some());
let out = r.run(&["uninstall"]);
assert_eq!(out.code, 0, "{}", out.output());
assert_eq!(ledger(&r), None, "the ledger is OUR bookkeeping — gone");
}
#[test]
fn amont_record_bypasses_false_records_nothing() {
let (r, _base) = stamped_repo();
r.git(&["config", "amont.recordBypasses", "false"]);
r.stage("src/b.ts", "export const y = 2\n");
r.commit("feat: dodge the gate, off the record");
assert_eq!(ledger(&r), None, "the documented switch must be honoured");
}