//! `scripts/gate` — the behaviour a shell script can be held to. //! //! Framing: `docs/gate-script-framing.md` §4 (revision 4, approved). //! //! # Why these tests exist, and why they are shaped like this //! //! The script exists to make two things unforgettable: a per-worktree //! `CARGO_TARGET_DIR` (because cargo locks it exclusively, so shared //! target directories make parallel worktrees *slower* than serial), //! and the fixed gate suite itself, which had been retyped by hand and //! gotten wrong twice in one session. //! //! # The recursion constraint shapes what is testable //! //! A test that ran `scripts/gate` for real would run the whole gate //! suite **inside** the gate suite. So every test here drives a path //! that **runs no gates** — which is stricter than "non-mutating", and //! is why `--init` exists: asserting the ownership marker is written //! needs something that *writes* it, a pure printer cannot, and a real //! gate run must not. `--init` shares the gate path's routine, so this //! is not a second implementation being tested. //! //! # The real managed root is unreachable from here //! //! Every test sets `PMACS_GATE_TARGET_ROOT` to a `tempdir`. That //! override exists for this file. Nothing here can touch //! `~/build/pmacs-gate-targets`, which matters most for the prune //! tests — a prune bug is unrecoverable. use std::path::{Path, PathBuf}; use std::process::Command; fn repo_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) } fn gate() -> PathBuf { repo_root().join("scripts/gate") } /// Run `scripts/gate` with an isolated managed root, from `cwd`. fn run_in(cwd: &Path, root: &Path, args: &[&str]) -> (String, String, bool) { let out = Command::new(gate()) .args(args) .current_dir(cwd) .env("PMACS_GATE_TARGET_ROOT", root) .output() .expect("run scripts/gate"); ( String::from_utf8_lossy(&out.stdout).into_owned(), String::from_utf8_lossy(&out.stderr).into_owned(), out.status.success(), ) } fn run(root: &Path, args: &[&str]) -> (String, String, bool) { run_in(&repo_root(), root, args) } // --- The plan matches handoff §3 ---------------------------------------- // // This is the direct test of the framing's named drift risk (Q#GS2): // the script is authoritative for the FIXED gates, so if it drifts from // §3, nothing else in the repository would notice. `--print-plan` // exists to make that checkable without executing anything. #[test] fn the_plan_sweeps_the_workspace_and_never_only_the_tests() { let root = tempfile::tempdir().expect("tempdir"); let (plan, _, ok) = run(root.path(), &["--print-plan"]); assert!(ok, "--print-plan must succeed"); assert!( plan.contains("cargo test --workspace --no-fail-fast -- --skip basedpyright"), "the sweep must be --workspace; plan was:\n{plan}" ); // The specific mistake §3 warns about: `--tests` selects 108 targets // where `--workspace` selects 110, dropping `pmacs_protocol` and // `pmacs_gpu`. A lane that had just written protocol tests swept // without running them. assert!( !plan.contains("--tests"), "`--tests` silently drops the protocol and GPU crates; plan was:\n{plan}" ); assert!( plan.contains("cargo fmt --check") && plan.contains("cargo clippy --workspace --all-targets -- -D warnings") && plan.contains("git diff --check"), "plan was:\n{plan}" ); } #[test] fn the_plan_runs_the_library_tests_in_both_feature_configurations() { let root = tempfile::tempdir().expect("tempdir"); let (plan, _, _) = run(root.path(), &["--print-plan"]); assert!(plan.contains("cargo test --lib\n"), "plan was:\n{plan}"); assert!( plan.contains("cargo test --lib --features crdt"), "the CRDT LIBRARY tests are unconditional — only the crdt \ WORKSPACE sweep is gated on --protocol; plan was:\n{plan}" ); } /// §3: "Touching `PROTOCOL_VERSION` STRENGTHENS the sweep line. It does /// not replace it." So `--protocol` must *add* a sweep, leaving the /// default one in place — and the default run must not carry it. #[test] fn the_crdt_workspace_sweep_is_added_by_protocol_and_absent_without_it() { let root = tempfile::tempdir().expect("tempdir"); let crdt_sweep = "cargo test --workspace --features crdt --no-fail-fast"; let (default_plan, _, _) = run(root.path(), &["--print-plan"]); assert!( !default_plan.contains(crdt_sweep), "a normal lane must not pay for the CRDT workspace sweep; plan was:\n{default_plan}" ); let (proto_plan, _, _) = run(root.path(), &["--protocol", "--print-plan"]); assert!( proto_plan.contains(crdt_sweep), "--protocol must add the CRDT workspace sweep; plan was:\n{proto_plan}" ); assert!( proto_plan.contains("cargo test --workspace --no-fail-fast -- --skip basedpyright"), "STRENGTHENS, not replaces — the default sweep must survive; plan was:\n{proto_plan}" ); } /// **The precondition the plan did not encode**, and the reason a green /// `--protocol` run could mean nothing. /// /// The crdt workspace sweep spawns `pmacs-gpu` as a *process*, and no /// `cargo test` run produces that binary — `pmacs-gpu` has no `tests/` /// directory, so cargo never uplifts its bin to `debug/pmacs-gpu`. On a /// cold target directory the sweep fails twelve /// `gpu_invocation_acceptance::crdt::*` tests on *"build pmacs-gpu /// before this acceptance suite"*. Before per-worktree target /// directories (#225) every worktree shared one that nearly always /// already held the binary, so the precondition was satisfied **by /// accident** — and the hazard was never the red gate, it was a green /// one decided by the build directory rather than by the diff. /// /// **The exact command is asserted, not just the step's name and /// position.** A `build-crdt` running plain `cargo build` would sit in /// the right place under the right name and leave the gate exactly as /// unsound: the crdt sweep needs *those* features, and the wrong ones /// produce a binary the sweep cannot use. /// /// **What this test cannot see: the names.** `--print-plan` strips them /// (`emit_plan | cut -f2-`), so everything below is an assertion about /// *commands in an order* — renaming the real build step to `sweep-crdt` /// leaves it green. The step's **name** is asserted by /// `the_crdt_build_step_carries_its_own_name_and_its_exact_command` /// below, which reads the plan in the form the runner reads it. #[test] fn the_crdt_sweep_is_immediately_preceded_by_the_build_that_produces_its_binary() { let root = tempfile::tempdir().expect("tempdir"); let build = "cargo build --workspace --no-default-features --features luajit,crdt"; let crdt_sweep = "cargo test --workspace --features crdt --no-fail-fast -- --skip basedpyright"; let (plan, err, ok) = run(root.path(), &["--protocol", "--print-plan"]); assert!(ok, "--protocol --print-plan must succeed; stderr:\n{err}"); let b = plan .find(build) .unwrap_or_else(|| panic!("the crdt sweep's build is missing; plan was:\n{plan}")); let s = plan .find(crdt_sweep) .unwrap_or_else(|| panic!("the crdt sweep is missing; plan was:\n{plan}")); // Ordering is asserted BEFORE the slice below, which would // otherwise panic with a byte-offset message ("begin > end (427 > // 282)") that names neither step. Mutation-tested: emitting the // build *after* the sweep produced exactly that, and a gate test // whose failure has to be decoded is a gate test nobody trusts. assert!( b < s, "the build must run BEFORE the crdt sweep, not after it — a sweep \ that builds its own precondition afterwards has already failed; \ plan was:\n{plan}" ); // IMMEDIATELY before: one newline between them and nothing else. A // build that merely appears *somewhere* earlier could be separated // from the sweep by a step that rewrites the same target directory. assert_eq!( &plan[b + build.len()..s], "\n", "the build must run IMMEDIATELY before the crdt sweep; plan was:\n{plan}" ); } /// **The witness that reaches the step it names**, and the reason this /// lane needed a second round. /// /// This lane exists to guarantee two things: that the crdt sweep is /// preceded by the build producing its binary, and that a build failure /// is attributed to **`build-crdt`** rather than to `sweep-crdt`. The /// first round shipped with neither guaranteed, because **neither /// witness could see a name**: /// /// - `--print-plan` renders `emit_plan | cut -f2-`, so the ordering test /// above compares commands and never sees the names beside them. /// - `--self-test` hardcodes the string `build-crdt` inside its **own /// synthetic** plan, so it proves things about the *runner* and /// nothing about the real emitter. /// /// Review demonstrated the consequence directly: **renaming the real /// build step to `sweep-crdt` left both tests passing** — a plan that /// reports a build failure under the sweep's name, which is exactly the /// misattribution the separate step exists to prevent, sitting green. /// /// So the pair is asserted **together, as one emitted line**, against /// `--print-plan-named` — the plan in the form the runner reads it back /// from `PLAN_FILE`. Name and command in the same `assert`, from the /// real emitter, is what makes a rename unable to pass; either half /// alone lets the other drift. /// /// The mode is a *rendering*, not a seam: `PLAN_FILE` stays /// uninjectable, because a test that supplied the runner's plan would /// turn its `eval` into a general command executor — the defect the /// `--acceptance` refusal below exists to prevent. #[test] fn the_crdt_build_step_carries_its_own_name_and_its_exact_command() { let root = tempfile::tempdir().expect("tempdir"); let build = "build-crdt\tcargo build --workspace --no-default-features --features luajit,crdt"; let sweep = "sweep-crdt\tcargo test --workspace --features crdt --no-fail-fast -- --skip basedpyright"; let (plan, err, ok) = run(root.path(), &["--protocol", "--print-plan-named"]); assert!( ok, "--protocol --print-plan-named must succeed; stderr:\n{err}" ); let lines: Vec<&str> = plan.lines().collect(); // Whole-line equality, not `contains`: the name, the tab, and the // command with nothing appended. A step is its (name, command) pair // and the plan is where both are decided. let b = lines.iter().position(|l| *l == build).unwrap_or_else(|| { panic!( "no plan line is exactly:\n {build}\nA build step under a \ different NAME misattributes its own failure; a build step \ with different FEATURES hands the sweep a binary it cannot \ use. Plan was:\n{plan}" ) }); // The sweep's own pair, for the same reason in the other direction: // asserting only the build's name lets a rename of the SWEEP slip // through the identical hole. let s = lines .iter() .position(|l| *l == sweep) .unwrap_or_else(|| panic!("no plan line is exactly:\n {sweep}\nPlan was:\n{plan}")); assert_eq!( s, b + 1, "the build must be the step IMMEDIATELY before the crdt sweep — a \ build merely somewhere earlier could be separated from it by a \ step that rewrites the same target directory. Plan was:\n{plan}" ); // Conditionality, on this rendering too: an ordinary lane must not // carry the step at all, not merely not carry its command. let (default_plan, _, ok) = run(root.path(), &["--print-plan-named"]); assert!(ok, "--print-plan-named must succeed"); assert!( !default_plan.contains("build-crdt"), "the default sweep never builds pmacs-gpu and never needs it, so no \ ordinary lane may pay for a workspace build; plan was:\n{default_plan}" ); } /// **The new rendering must be the same plan, or the assertion above /// pins a string only the test ever reads.** /// /// `--print-plan-named` and `--print-plan` both call one emitter, and /// the runner writes that same emitter to `PLAN_FILE` — so today they /// cannot disagree. This pins that from outside, where a later edit /// giving either mode its own plan text would be caught rather than /// producing a witness that asserts a name the runner never uses. /// /// It also pins the **shape** the runner depends on: the loop reads each /// line with `IFS= read -r name cmd`, so a plan line without its /// tab would silently run under an empty command. #[test] fn the_named_plan_is_the_printed_plan_with_its_names_removed() { let root = tempfile::tempdir().expect("tempdir"); for flags in [ vec![], vec!["--protocol"], vec!["--acceptance", "m4_acceptance"], ] { let mut named_args = flags.clone(); named_args.push("--print-plan-named"); let mut plain_args = flags.clone(); plain_args.push("--print-plan"); let (named, err, ok_named) = run(root.path(), &named_args); assert!(ok_named, "{named_args:?} must succeed; stderr:\n{err}"); let (plain, err, ok_plain) = run(root.path(), &plain_args); assert!(ok_plain, "{plain_args:?} must succeed; stderr:\n{err}"); let mut stripped = String::new(); for l in named.lines() { let (_name, cmd) = l.split_once('\t').unwrap_or_else(|| { panic!( "every plan line must be `namecommand` — the runner \ splits on that tab, so a line without one runs an empty \ command under the whole line's name. Line was:\n {l:?}" ) }); stripped.push_str(cmd); stripped.push('\n'); } assert_eq!( stripped, plain, "the two renderings must be one plan; with {flags:?} they diverged" ); } } /// **Conditionality, settled by measurement rather than by reading** — /// which is the whole methodological point of this lane, since the /// defect it repairs was a precondition nobody checked. /// /// Measured 2026-08-09 on a disposable target directory, with /// `debug/pmacs-gpu` asserted **absent** before each run and each sweep /// run alone from that same cold state: the default sweep exited **0** /// and left `debug/pmacs-gpu` **still absent** — it never builds the /// binary and never needs it — while the crdt sweep exited **101** with /// exactly twelve `gpu_invocation_acceptance::crdt::*` failures. /// /// So an unconditional build would be a real cost paid for nothing on /// every ordinary lane. #[test] fn the_crdt_build_is_absent_without_protocol() { let root = tempfile::tempdir().expect("tempdir"); let (plan, _, ok) = run(root.path(), &["--print-plan"]); assert!(ok, "--print-plan must succeed"); assert!( !plan.contains("cargo build"), "the default sweep passes on a tree with no pmacs-gpu at all, so a \ normal lane must not pay for a workspace build; plan was:\n{plan}" ); } /// **The attribution and continuation criteria, made observable.** /// /// Everything else in this file drives a no-gates path, so it can prove /// a step's name and its order and **nothing** about what the runner /// does when a step fails. `--self-test` closes that gap by handing the /// *real* runner loop a hardcoded three-line plan — a passing step, a /// failing one named `build-crdt`, and a passing sentinel after it. /// /// **Why `build-crdt` must be its own step** is exactly what this /// witnesses: folded into the sweep as `cargo build … && cargo test …`, /// a *build* failure would be reported under the name `sweep-crdt` — a /// wrong attribution in the one place this script exists to be /// trustworthy about. /// /// **The sentinel assertion is the load-bearing one.** With the failure /// last, a runner that aborts and one that continues produce identical /// output, so a two-line witness would pass on a runner doing the /// opposite of the stated `--no-fail-fast` policy. The sentinel's own /// log existing is the only thing that separates them — delete that /// assertion and this test stops testing continuation at all. /// /// The plan is a literal inside the script on purpose. Making /// `PLAN_FILE` injectable would let this test supply its own commands, /// and would turn the runner's `eval` into a general command executor — /// the same defect the `--acceptance` refusal above exists to prevent. #[test] fn self_test_names_the_failing_gate_and_the_suite_continues_past_it() { let root = tempfile::tempdir().expect("tempdir"); let (out, err, ok) = run(root.path(), &["--self-test"]); assert!( !ok, "a plan containing a failing step must exit non-zero; stdout:\n{out}stderr:\n{err}" ); assert!( out.contains("build-crdt"), "the failing gate must be named as it runs; stdout:\n{out}" ); assert!( err.contains("FAILED: build-crdt"), "the failing gate must be listed under FAILED: by its OWN name; stderr:\n{err}" ); // The runner claims a log path for the failure. Assert the file is // actually there: a tool that prints a path it did not write is // worse than one that prints nothing, because the absence is only // discovered while chasing a real failure. let claimed = err .lines() .find_map(|l| l.split_once("log: ").map(|(_, path)| path.trim())) .unwrap_or_else(|| panic!("the failing gate's log path must be printed; stderr:\n{err}")); assert!( claimed.ends_with("02-build-crdt.log"), "the log must be numbered and named for the gate that failed; was {claimed}" ); assert!( Path::new(claimed).is_file(), "the runner must WRITE the log it claims at {claimed}" ); let logdir = Path::new(claimed) .parent() .expect("the log lives in a log directory"); assert!( logdir.join("01-self-pass.log").is_file(), "the step before the failure must have its own log; dir was {}", logdir.display() ); // THE ASSERTION THE WHOLE MODE EXISTS FOR. assert!( logdir.join("03-self-sentinel.log").is_file(), "the suite must CONTINUE past a failed gate — the sentinel after \ build-crdt wrote no log, so this runner ABORTED. Stdout:\n{out}" ); assert!( out.contains("self-sentinel"), "the sentinel must be reported like any other gate; stdout:\n{out}" ); } /// The seam handoff §3 keeps authority over: a script cannot infer /// which acceptance suites a change touched, so it runs what it is /// handed — each one, in order. #[test] fn acceptance_suites_reach_the_plan_in_the_order_given() { let root = tempfile::tempdir().expect("tempdir"); let (plan, _, _) = run( root.path(), &[ "--acceptance", "alpha_acceptance", "--acceptance", "beta_acceptance", "--print-plan", ], ); let a = plan .find("cargo test --test alpha_acceptance") .unwrap_or_else(|| panic!("alpha missing from plan:\n{plan}")); let b = plan .find("cargo test --test beta_acceptance") .unwrap_or_else(|| panic!("beta missing from plan:\n{plan}")); assert!( a < b, "suites must keep their given order; plan was:\n{plan}" ); } // --- Derivation, marker, canonical paths -------------------------------- #[test] fn printing_the_target_dir_creates_nothing() { let root = tempfile::tempdir().expect("tempdir"); let (dir, _, ok) = run(root.path(), &["--print-target-dir"]); assert!(ok, "--print-target-dir must succeed"); assert!(!dir.trim().is_empty(), "it must print a path"); assert!( !Path::new(dir.trim()).exists(), "--print-target-dir must be pure — it printed {dir} and created it" ); } #[test] fn init_writes_the_ownership_marker_and_is_idempotent() { let root = tempfile::tempdir().expect("tempdir"); let (dir, _, ok) = run(root.path(), &["--init"]); assert!(ok, "--init must succeed"); let dir = PathBuf::from(dir.trim()); let marker = dir.join(".pmacs-gate-target"); assert!(marker.is_file(), "the ownership marker must exist"); let owner = std::fs::read_to_string(&marker).expect("read marker"); // Canonical form (§2.5): what prune compares against. let expected = repo_root().canonicalize().expect("canonicalize repo root"); assert_eq!( owner.trim(), expected.to_string_lossy(), "the marker must record the CANONICAL worktree path" ); run(root.path(), &["--init"]); let n = std::fs::read_dir(root.path()) .expect("read root") .filter(|e| e.as_ref().is_ok_and(|e| e.path().is_dir())) .count(); assert_eq!(n, 1, "--init must be idempotent"); } /// **The case that deletes a live lane's artifacts if derivation is not /// canonical.** Reaching one worktree through a symlink must derive the /// same directory. If the hash came from an uncanonicalized `$PWD`, the /// symlinked spelling would derive a *different* directory whose marker /// records the *canonical* path — a second build directory for a live /// worktree, indistinguishable from an orphan. /// /// **This currently passes for a reason the script does not control**, /// and saying so is more useful than implying otherwise: measured here, /// `git rev-parse --show-toplevel` already returns a resolved physical /// path, so the derivation is canonical before `canon()` touches it. /// Removing `canon()` does not make this test fail today. It pins the /// **property**, which is what must hold — not the mechanism, which is /// belt-and-braces against git's behaviour not being contractual. #[test] fn a_symlinked_spelling_of_a_worktree_derives_the_same_directory() { let root = tempfile::tempdir().expect("tempdir"); let link_home = tempfile::tempdir().expect("tempdir"); let link = link_home.path().join("via-symlink"); if std::os::unix::fs::symlink(repo_root(), &link).is_err() { return; // no symlink support; nothing to assert } let (direct, _, _) = run(root.path(), &["--print-target-dir"]); let (through_link, _, _) = run_in(&link, root.path(), &["--print-target-dir"]); assert_eq!( direct.trim(), through_link.trim(), "two spellings of one worktree must share one build directory" ); } // --- Pruning ------------------------------------------------------------ /// Build a managed root holding three entries: one orphan (eligible), /// one unmarked look-alike, and one owned by a live worktree. fn prune_fixture(root: &Path) -> (PathBuf, PathBuf, PathBuf) { let orphan = root.join("gone-00000000"); std::fs::create_dir_all(&orphan).expect("mkdir orphan"); std::fs::write( orphan.join(".pmacs-gate-target"), format!("{}\n", root.join("no-such-worktree").display()), ) .expect("write orphan marker"); let lookalike = root.join("pmacs-deadbeef"); std::fs::create_dir_all(&lookalike).expect("mkdir lookalike"); let (live, _, _) = run(root, &["--init"]); (orphan, lookalike, PathBuf::from(live.trim())) } #[test] fn prune_is_a_dry_run_by_default_and_deletes_nothing() { let root = tempfile::tempdir().expect("tempdir"); let (orphan, lookalike, live) = prune_fixture(root.path()); let (out, _, ok) = run(root.path(), &["--prune"]); assert!(ok, "--prune must succeed"); assert!( out.contains("WOULD delete") && out.contains(&orphan.to_string_lossy().to_string()), "the orphan must be named; output was:\n{out}" ); assert!( orphan.exists() && lookalike.exists() && live.exists(), "a dry run must delete nothing" ); } #[test] fn force_deletes_only_the_orphan() { let root = tempfile::tempdir().expect("tempdir"); let (orphan, lookalike, live) = prune_fixture(root.path()); let (out, _, ok) = run(root.path(), &["--prune", "--force"]); assert!(ok, "output was:\n{out}"); assert!(!orphan.exists(), "the orphan must be gone"); assert!( lookalike.exists(), "a directory that merely RESEMBLES a managed one must never be touched" ); assert!(live.exists(), "a live worktree's directory must survive"); } /// **A `prunable` worktree record counts as DEAD**, and nothing else in /// this suite would catch getting it wrong. /// /// `git worktree list --porcelain` keeps reporting a worktree that was /// registered but whose directory was deleted without /// `git worktree remove` — it adds a `prunable ` line to that /// record. Treating every *listed* path as live would make exactly the /// directories most worth reclaiming permanently ineligible, silently. /// /// The other prune tests use a marker pointing at a path git never knew /// about, so they cannot distinguish "absent from the list" from "listed /// but prunable". This one registers a real worktree first. /// /// **Guarded twice, deliberately.** `live_worktrees` also drops any path /// it cannot enter, so a deleted directory is excluded even if the /// `prunable` line were ignored — which is why mutating that line away /// does not fail this test. The check stays because `prunable` is /// reported for causes *other* than a missing directory (a gitdir file /// pointing elsewhere, for one), and those the path filter would miss. /// Deregisters probe worktrees on the way out **even if an assertion /// panics**. Cleanup written after the asserts would be skipped by the /// unwind, leaving the real repository carrying a stale record. struct WorktreePruneGuard; impl Drop for WorktreePruneGuard { fn drop(&mut self) { let _ = Command::new("git") .args(["worktree", "prune"]) .current_dir(repo_root()) .output(); } } #[test] fn a_registered_worktree_whose_directory_was_deleted_is_prunable() { let root = tempfile::tempdir().expect("tempdir"); let home = tempfile::tempdir().expect("tempdir"); let wt = home.path().join("gate-prunable-probe"); let added = Command::new("git") .args(["worktree", "add", "-q", "--detach"]) .arg(&wt) .arg("HEAD") .current_dir(repo_root()) .output() .expect("git worktree add"); // A HARD failure, not a silent return. Skipping here would make the // one test that covers `prunable` handling report green on a machine // where it never ran — the failure mode this whole suite exists to // avoid. assert!( added.status.success(), "could not register a probe worktree, so this test proved nothing:\n{}", String::from_utf8_lossy(&added.stderr) ); let _guard = WorktreePruneGuard; let (dir, _, ok) = run_in(&wt, root.path(), &["--init"]); let dir = PathBuf::from(dir.trim()); assert!(ok && dir.is_dir(), "--init in the probe worktree"); // Deleted WITHOUT `git worktree remove`: still registered, and now // reported with a `prunable` line. std::fs::remove_dir_all(&wt).expect("remove the worktree directory"); let (out, _, ok) = run(root.path(), &["--prune", "--force"]); assert!(ok, "prune must succeed; output was:\n{out}"); assert!( !dir.exists(), "a `prunable` record is not a live worktree — its build directory \ must be reclaimable, or orphans accumulate forever. Output was:\n{out}" ); } // --- Refusals: the two ways prune and the plan could do harm ----------- /// **The data-loss case.** Pruning decides what to delete by subtracting /// the live worktree set from the managed root. Run from outside any /// repository, that set cannot be established — and the first version of /// this script masked the failure with `|| true`, making the set *empty*, /// which marks **every** managed directory an orphan. `--prune --force` /// would then have deleted all of them, including live lanes' artifacts. /// /// The correct answer to "I cannot tell what is live" is to refuse. /// /// **Two guards, deliberately redundant.** The script refuses both when /// `git rev-parse --show-toplevel` fails and when `live_worktrees` /// cannot enumerate — either alone satisfies this test, so mutating /// away one at a time reads as "vacuous". Removing **both** fails it. /// Recorded so a later reader does not delete one of them on the /// grounds that no test noticed. #[test] fn prune_outside_a_repository_refuses_and_every_directory_survives() { let root = tempfile::tempdir().expect("tempdir"); let (orphan, lookalike, live) = prune_fixture(root.path()); let outside = tempfile::tempdir().expect("tempdir"); // Sanity: the fixture's orphan really is eligible from inside a repo. let (inside, _, _) = run(root.path(), &["--prune"]); assert!( inside.contains("WOULD delete"), "fixture is not discriminating — nothing was eligible:\n{inside}" ); let (out, err, ok) = run_in(outside.path(), root.path(), &["--prune", "--force"]); assert!( !ok, "pruning from outside a repository must FAIL, not proceed:\n{out}{err}" ); assert!( err.contains("refusing to prune"), "the refusal must say why; stderr was:\n{err}" ); assert!( orphan.exists() && lookalike.exists() && live.exists(), "nothing may be deleted when the live set is unknown" ); } /// `--acceptance` is interpolated into a command the runner evaluates, /// so a name carrying shell metacharacters is an injection. It must be /// refused rather than escaped, and refused at parse time — before any /// gate runs. #[test] fn acceptance_names_with_shell_metacharacters_are_refused() { let root = tempfile::tempdir().expect("tempdir"); let canary = root.path().join("canary"); std::fs::write(&canary, "intact").expect("write canary"); let hostile = [ format!("x; rm -f {}", canary.display()), format!("x$(rm -f {})", canary.display()), "x`id`".to_string(), "x && id".to_string(), "../escape".to_string(), "x y".to_string(), "-flag".to_string(), ]; for name in &hostile { let (out, err, ok) = run(root.path(), &["--acceptance", name, "--print-plan"]); assert!( !ok, "must refuse acceptance name {name:?}; stdout was:\n{out}" ); assert!( err.contains("refusing acceptance suite name") || err.contains("may not start with"), "refusal for {name:?} must say why; stderr was:\n{err}" ); assert!( !out.contains("rm -f") && !out.contains("id"), "a hostile name must never reach the plan; stdout was:\n{out}" ); } assert_eq!( std::fs::read_to_string(&canary).expect("read canary"), "intact", "no injected command may have executed" ); } /// A well-formed name still works — otherwise the validator could pass /// the test above by rejecting everything. #[test] fn ordinary_acceptance_names_are_accepted() { let root = tempfile::tempdir().expect("tempdir"); for name in ["m4_acceptance", "gate-script", "abc123_x"] { let (plan, err, ok) = run(root.path(), &["--acceptance", name, "--print-plan"]); assert!(ok, "{name} must be accepted; stderr was:\n{err}"); assert!( plan.contains(&format!("cargo test --test {name}")), "plan was:\n{plan}" ); } } /// The marker is documented as one line. Reading only its first line /// would accept a corrupted or hand-edited file and then delete a /// directory on the strength of a file the script did not understand. #[test] fn a_multi_line_marker_is_refused_rather_than_read_head_first() { let root = tempfile::tempdir().expect("tempdir"); let bad = root.path().join("bad-00000000"); std::fs::create_dir_all(&bad).expect("mkdir"); std::fs::write( bad.join(".pmacs-gate-target"), format!( "{}\nstray second line\n", root.path().join("gone").display() ), ) .expect("write marker"); let (out, _, ok) = run(root.path(), &["--prune", "--force"]); assert!(ok, "output was:\n{out}"); assert!( bad.exists(), "a malformed marker must not authorise deletion" ); assert!( out.contains("not exactly one line"), "the skip reason must name the problem; output was:\n{out}" ); } /// Skips are reported with reasons. A prune that quietly ignores things /// is how one learns too late that the marker was never written. #[test] fn skipped_directories_are_reported_with_a_reason() { let root = tempfile::tempdir().expect("tempdir"); let (_, lookalike, _) = prune_fixture(root.path()); let (out, _, _) = run(root.path(), &["--prune"]); assert!( out.contains(&lookalike.to_string_lossy().to_string()) && out.contains("no readable .pmacs-gate-target"), "the unmarked directory must be named with its reason; output was:\n{out}" ); assert!( out.contains("worktree is live"), "the live one must be named with its reason too; output was:\n{out}" ); }