diff --git a/docs/active-work.md b/docs/active-work.md index 1ba0422..f3346b2 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -161,6 +161,44 @@ form. All four steps ran clean. **The two-argument form still does not work** for a remote-only branch (`fatal: invalid reference`), which is why every lane below spells out the `-b` form. +## Leaked daemons from `gpu_invocation_acceptance` — NEEDS A LANE + +**Found 2026-08-05 while cleaning up after the tree-primitive work. No +branch, no framing.** + +- **42 orphaned `pmacs --daemon` processes** were resident on the + development machine, **the oldest 3 days 23 hours old**. All had been + **reparented to systemd** (`ppid=1`) and all had **deleted sockets**, + so nothing could ever reach or reap them. +- **Source: `tests/gpu_invocation_acceptance.rs`** — the one-command + tests, whose daemons carry `--socket /one-command.sock`. The + tempdir is cleaned up; the daemon is not. +- **Rate measured, not estimated: 3 per sweep.** A single isolated + `--features luajit,crdt` sweep leaked exactly three. 42 is what + several days of sweeps accumulate to. +- **This predates the tree work** — the oldest is four days old — so it + is a standing leak, not something a current lane introduced. + +**Why it belongs to the reap-ledger family.** This is precisely the +shape that lane exists for: a process that outlives its supervisor with +nothing left watching it. The ledger arms only for `spec.group`, and +these are daemons spawned by a test harness rather than by compile mode, +so **nothing in the existing ledger covers them**. + +**Why it matters beyond tidiness.** Dozens of resident daemons were +present during every local sweep run this week, including the one that +produced the unclassified failure recorded in the tree lane below. That +makes them a **rival explanation** to the shared-target-dir mechanism +for that occurrence, and neither can be tested against it now — the +signatures were not captured. A leak that quietly changes the +environment of every subsequent test run is a measurement problem as +well as a resource one. + +**First questions for whoever takes it:** does the test harness fail to +reap, or does the daemon fail to exit when its socket disappears? Those +have different fixes, and the second would be a product defect rather +than a test one. + ## macOS CI signal integrity — STAGE 1 IN REVIEW, PR #215 **This file requires a lane for every open PR** (see the #171/#174 note diff --git a/docs/agent-handoff.md b/docs/agent-handoff.md index b165193..17938ca 100644 --- a/docs/agent-handoff.md +++ b/docs/agent-handoff.md @@ -315,6 +315,23 @@ someone forgot. `gpu_invocation_acceptance` tests fail on a missing `pmacs-gpu` binary. `cargo build --workspace --no-default-features --features luajit,crdt` is the invocation that produces both binaries. +- **A shared `CARGO_TARGET_DIR` makes concurrent sweeps unattributable.** + Two worktrees both defaulting to it means one lane's + `cargo test --workspace` **overwrites `target/debug/pmacs` mid-sweep** + in the other, and every real-daemon suite then spawns the wrong + binary. Observed twice on 2026-08-05, from both sides: the failing + text named its own cause ("start the daemon built with the `crdt` + feature"), and re-running the same suites with a dedicated target dir + gave 41/41. **Give a second worktree its own target dir**, and treat + any red from a sweep that overlapped another build as unattributable + rather than as evidence. +- **A local sweep leaks daemons, and they accumulate across days.** + `gpu_invocation_acceptance`'s one-command tests leave ~3 orphaned + `pmacs --daemon` processes per sweep, reparented to systemd with + deleted sockets; 42 were resident at one point, the oldest four days + old. They are a rival explanation for any load-sensitive local + failure, so **check `pgrep -f "pmacs --daemon"` before trusting a + local red**. Lane recorded in `docs/active-work.md`. - **A local sweep is blind to whichever feature configuration it does not build.** Stage 3's census and every verification sweep ran `--features luajit` WITHOUT `crdt`, so no crdt-gated suite was diff --git a/docs/tree-primitive-framing.md b/docs/tree-primitive-framing.md index 2aeaea7..d243a05 100644 --- a/docs/tree-primitive-framing.md +++ b/docs/tree-primitive-framing.md @@ -515,6 +515,63 @@ reports as one suite. --- +## 6a. Verification record, including one unclassified occurrence + +**The luajit sweep is 3453 / 0** and the count reconciles exactly: +`main` is 3450 (Stage 3's 3449 sweep predated its own capability-fallback +pin) plus this lane's three listview tests and one m4 test. + +**The crdt sweep is 3722 / 0**, likewise +4 on `main`'s 3718. + +### An UNCLASSIFIED, UNCAPTURED local occurrence + +The **first** local crdt sweep of this branch reported **7 failures**. +**It is recorded here as unclassified and it is deliberately NOT a row +in `docs/ci-red-signatures.md`** — that registry keys on a normalized +signature, and this occurrence has none to match, so a row would confer +recognisability it cannot support. + +**The signatures were destroyed before they were read.** The sweep was +piped through an aggregation that emitted only totals. That is the exact +failure the registry exists to prevent, committed one lane after writing +it — and it is why the cause cannot now be established rather than +merely being unknown. + +**Re-runs, with what each does and does not support:** + +| run | isolated? | result | +|---|---|---| +| first | no — concurrent with another lane's build | **7 failed, signatures lost** | +| second | no | 3722 / 0 | +| A | **no** — the isolation guard printed "aborting" and did not abort | 3722 / 0 | +| B | **yes** — verified idle | 3722 / 0 | +| C | **yes** — verified idle | 3722 / 0 | + +Two genuinely isolated runs, both clean. **That supports repeatability +under isolation. It does not establish what caused the original.** + +### Two NON-CAUSAL hypotheses, neither testable now + +Both are mechanisms known to have been present. Neither is offered as an +explanation, because the occurrence's signatures no longer exist to test +either against: + +1. **Shared `CARGO_TARGET_DIR`.** Another lane's worktree shared + `/home/jeans/build/cargo-target`, so its `cargo test --workspace` + overwrote `target/debug/pmacs` mid-sweep. That lane observed the + reciprocal case independently, caught the concurrent build with + `pgrep`, and its failing text named its own cause ("start the daemon + built with the `crdt` feature"). +2. **Resident leaked daemons.** ~40 orphaned `pmacs --daemon` processes + were present, some four days old (see the lane in + `docs/active-work.md`). Isolated sweeps leak 3–4 each, so the + population was growing throughout. + +**Having two plausible mechanisms and no way to discriminate is the +result.** Reporting either as *the* cause would be the reasoning this +project has rejected repeatedly: concluding something about an +occurrence from something that was not about that occurrence. + ## 7. Branch plan Q#TR1 is decided, so the listview-extension shape applies: diff --git a/tests/m4_acceptance.rs b/tests/m4_acceptance.rs index 68b1825..bf6b7f2 100644 --- a/tests/m4_acceptance.rs +++ b/tests/m4_acceptance.rs @@ -8025,6 +8025,99 @@ fn open_against_fake(path: &std::path::Path) -> pmacs::editor::EditorState { /// hierarchical documentSymbol response ("Outer" class > "inner" /// method): open, depth-indented rows, RET jump-ring visit to the /// symbol's selectionRange, M-, back to the outline row, q restore. +/// Tree primitive, acceptance 5 — the FLAT listview consumers render +/// **byte-identically** after the depth/collapse extension. +/// +/// This exists because the weaker claim was not true. `listview_ +/// acceptance` says in its own header that the references panel "needs +/// a live LSP and is validated manually / via the m4 harness", so it +/// does not exercise `*references*` at all; and the hover test asserts +/// content *presence*, not exact output. Neither would notice a flat +/// consumer silently gaining an indent column — which is precisely the +/// regression a tree extension can introduce. +/// +/// So the assertion is on the **exact rendered bytes**, through the +/// real entry points, against the fake language server. +#[test] +fn flat_listview_consumers_render_byte_identically_after_the_tree_extension() { + let dir = tempfile::tempdir().expect("tempdir"); + let a_path = dir.path().join("r.rs"); + std::fs::write(&a_path, b"fn main() {}\n").expect("write r"); + let mut state = open_against_fake(&a_path); + + let body = |state: &pmacs::editor::EditorState| -> String { + state + .lua_host + .lua() + .load("local b = pmacs.window.buffer() return b:slice(0, b:len())") + .eval() + .expect("panel text") + }; + + // --- *references* (on_visit, no depth) --- + state + .lua_host + .lua() + .load("pmacs.lsp.find_references()") + .exec() + .expect("invoke find_references"); + assert!( + pump_lua_flag( + &mut state, + "pmacs.describe.buffer(pmacs.window.buffer()).name == '*references*'", + 5, + ), + "the references panel opened" + ); + let refs = body(&state); + let (header, rows) = refs.split_once('\n').expect("header then rows"); + assert_eq!( + header, "1 reference RET visit n/p move q quit", + "the header is unchanged — no fold affordance is advertised on a \ + flat panel" + ); + // EXACT: the row is the location string and nothing else. An added + // indent column, tree gutter or fold marker would all fail here. + assert_eq!( + rows, + format!("{}:12:3", a_path.display()), + "the flat references row renders verbatim" + ); + + // --- *lsp* (on_refresh, no depth) --- + state + .lua_host + .lua() + .load("pmacs.command.invoke('lsp.status')") + .exec() + .expect("invoke lsp.status"); + let status_body = body(&state); + let (status_header, status_rows) = status_body.split_once('\n').expect("header then rows"); + assert_eq!( + status_header, "LSP status g refresh q quit", + "the one panel WITH refresh keeps its exact header" + ); + // `*lsp*` formats its OWN indentation — two spaces on detail lines — + // so "starts with a space" is not a violation here. What must hold + // is that the primitive reproduces the consumer's text EXACTLY: a + // prefix added by render would shift this line and break the match. + // + // Matched as a whole line rather than a substring, because a + // substring would still be found inside a further-indented version + // of itself. Volatile parts (pid, elapsed) are deliberately not + // included. + assert!( + status_rows + .lines() + .any(|l| l == " capabilities: sync, hover, completion, definition, diagnostics"), + "the consumer's own two-space indentation survives verbatim; got:\n{status_rows}" + ); + assert!( + status_rows.lines().any(|l| l == "Servers:"), + "an unindented row stays unindented; got:\n{status_rows}" + ); +} + #[test] #[allow( clippy::too_many_lines,