9.0 KiB
R8 — the fixture boundary the LSP panel tests never set
Status: revision 2 — APPROVED 2026-08-08 and IMPLEMENTED. PR #226
open (https://github.com/levineuwirth/pmacs/pull/226), held for
review; not merged. The verification in §5 is no longer a plan: every
item was run, and the results are recorded in the lane in
docs/active-work.md and in the retired R8 row of
docs/ci-red-signatures.md.
Revision 2 corrects two counts I got wrong by miscounting, adds the
portable witness revision 1 lacked, and bounds Q#R8-1's answer. The
counts came from raw grep -c, which counted a function definition, a
comment, a test name and two assertion strings as if they were calls
— the same class of error as trusting a test-name match instead of a
signature, which this project has a whole registry about.
Test hermeticity. Not coherence-affecting — no journey step, no
interaction island, no config-registry setting, no background work
(COHERENCE.md §20). Named so the absence is a statement.
1. What is wrong, and what is not
docs/ci-red-signatures.md R8 fails deterministically on this
machine:
assertion `left == right` failed: the flat references row renders verbatim
left: ".tmpPZsycN/r.rs:12:3"
right: "/tmp/.tmpPZsycN/r.rs:12:3"
Diagnosed 2026-08-08:
builtin/runtime/lsp.lua:2397display_pathshortens a location against the detected project root before rendering it.pmacs.project.detectwalks upward for a marker. From/tmp/.tmpXXXXXX/r.rsit reaches/tmp.- This machine has a stray
/tmp/.git— an empty directory, not a repository. The.gitmarker is directory-only, so it matches. - Root resolves to
/tmp; the prefix is stripped; the row renders as observed.
Control: the same test with TMPDIR outside /tmp passes.
The product behaviour is correct and is not being changed. Shortening a location against its project root is the feature. A file that really does sit inside a project really should render relative to it.
The defect is that the fixture does not bound its own project
detection, so its assertion depends on whether the developer's /tmp
happens to contain a .git. That is a hermeticity bug in the test, and
it is what this lane fixes.
2. The mechanism already exists, and this suite already uses it
src/project.rs:208 documents detect_project_within(start, markers, stop_root) as existing
"so a stray marker in a temp-dir's ancestor (e.g. a developer's
/tmp/.git) can't leak into a fixture that lives below it."
It is reachable from Lua as pmacs.project.set_search_boundary(path)
(src/lua_bindings/mod.rs:12308), and eight test files already call
it — fourteen real calls between them, of which
tests/m4_acceptance.rs holds five. One of those five carries the
comment "(a developer's /tmp/.git, say) can't masquerade as the
root."
(Revision 1 said nine, from a raw grep -c that also counted a
comment, the name of a test about the binding, and two assertion
message strings.)
So this is not a missing capability, a design question, or a new pattern. It is one helper that missed an established one, and the hazard it missed is documented by name in the same file.
open_against_fake (tests/m4_acceptance.rs:7985) builds an
EditorState, declares frame geometry, points the rust server at the
fake, and opens the file — and never sets a boundary.
3. The change
Set the search boundary to the fixture's own temporary directory inside
open_against_fake, before the file is opened, matching the existing
call sites.
Three tests call open_against_fake (tests/m4_acceptance.rs:8048, 8137, 8261 — revision 1 said four, counting the definition at 7985).
All three must still pass, and the fix is expected to change the
rendering of exactly one — the one whose assertion spells the path out.
4. Open questions
Q#R8-1 — the boundary's value: the file's parent, or the tempdir root?
open_against_fake receives a path, not the TempDir. The parent
directory of that path is the fixture root in every current caller.
My vote: the file's parent directory, derived inside the helper. It needs no signature change, and it is what the other call sites in this file effectively use.
Its limitation, stated now rather than discovered later. This is
correct only while fixtures put the file as a direct child of the
fixture root, which all three callers do. A future nested fixture —
say <root>/crate/src/r.rs — that wants detection to reach its outer
root would not be served by this, and passing a deeper path cannot
fix it: the boundary is derived from the path's parent, so a deeper
path clamps the walk sooner, never later. That case needs an explicit
boundary argument or a second helper, and revision 1's suggestion that
a caller "can pass a deeper path" had the direction backwards.
Q#R8-2 — is this one helper, or a census?
The suite constructs state through EditorState::new_with_roots 113
times. An unknown number of those are equally unbounded, and the same
stray marker would affect any of them whose assertion renders a
path. Most do not, which is why only this one fails.
My vote: fix this helper in this lane; census separately. A 113-site audit is not a bug fix, and bundling it would make the regression-fixing change unreviewable. But the census should be a named follow-on rather than a good intention — this row cost real time twice, and the next one will look like a new mystery.
Recommend: file the census as a named backlog item in
docs/agent-handoff.md §6 when this lands.
Q#R8-3 — does anything depend on the unbounded walk?
If some test's expectation quietly relies on detection escaping its fixture, bounding it would break that test — and that would be a finding worth having rather than an obstacle.
Expected: no. To be established by running the suite, not asserted here.
5. Verification
5.1 The primary witness is PORTABLE and plants its own marker
Revision 1 rested the bite on /tmp/.git, which makes the proof a
property of this machine — the same mistake as a test that passes
only where the developer happens to be standing. A hermeticity fix
whose only evidence is one machine's stray directory is not
demonstrated; it is anecdotal.
So the primary witness builds the hazard itself:
<tempdir>/ <- an empty `.git` is PLANTED here
proj/ <- the file's parent; becomes the boundary
r.rs
- With the helper's boundary set to
proj, detection examinesproj, finds no marker, and stops — the planted.gitone level up is out of reach.display_pathfinds no root and falls back, so the rendered row is the absolute path. - Reverting the boundary lets the walk reach
<tempdir>, match the planted marker, and strip the prefix — deterministically, on every machine, with no dependence on/tmporTMPDIR.
That pair is the bite. It runs in CI, where /tmp/.git does not exist,
and it fails for the right reason if the helper regresses.
This is a new test rather than a rewrite of the existing one: the existing assertion's value is that it renders a real path verbatim, and changing its layout to carry a planted marker would blur two purposes into one fixture.
5.2 The machine observation stays, as confirmation only
- The existing test passes with
/tmp/.gitstill present — on the machine that reproduces R8, not merely on a clean one. The stray directory is deliberately not removed: deleting it would hide the hermeticity defect, and it is an external directory with unresolved provenance. - This is corroboration for the portable witness above, not the bite itself.
5.3 The rest
- The other two
open_against_faketests still pass, and the fullm4_acceptancesuite passes (Q#R8-3). - The standard gate suite, run by hand from
docs/agent-handoff.md§3.
scripts/gate is deliberately NOT a criterion here. This lane
branches from main, where that script does not exist — it is
unmerged on gate-script (#225). Naming it would make this lane's
verification depend on an artifact absent from its own base, and would
quietly couple two lanes that are meant to land independently.
The sequencing instead: R8 lands first, on its own merits. Then #225
rebases onto it, and "scripts/gate runs green" becomes #225's
re-gate criterion — which is where it belongs, since a green gate run
is the thing #225 ships.
What this does not prove: that the other 113 construction sites are hermetic. Q#R8-2's census is why.
6. Not in scope
Removing or altering /tmp/.git — and equally, settling its provenance.
Observations of its timestamps have disagreed, /tmp is a tmpfs whose
entries are touched by inspection, and nothing in this lane depends on
the answer: §5.1's witness plants its own marker precisely so the fix
does not rest on that directory at all. Changing display_path or project
detection semantics — the product behaviour is correct. The 113-site
census (Q#R8-2). Rebasing or merging #225, which follows this landing.