The mechanical half, riding on the census in the previous commit.
* 342 in-process construction sites in 65 files now take
`new_with_roots` / `open_with_roots` with `iso::roots()`. The isolated
base is a pure function of `CARGO_TARGET_TMPDIR` — no counter, no
`OnceLock` — so two copies of the module in one binary agree instead of
racing, and the tree lives somewhere `cargo clean` owns rather than
leaking into `/tmp` once per run. It is shared deliberately:
materialization is content-gated and idempotent, so a per-test
directory would repeat it ~330 times per run for a byte-identical
result.
* `journey_acceptance` keeps the ambient `EditorState::open`, because
proving the production entry point has a caller is the whole of what
that ratchet is for. Rev 2's "isolated by the environment its binary is
launched with" was not a mechanism — cargo launches each test binary
with the caller's environment, and a binary cannot re-point its own
roots before its tests run. Each test is now a thin parent that
re-execs this binary for its own name with controlled roots, and the
child runs the body against production's call. Two pins guard it: the
child asserts all four roots resolve inside the controlled base, and
the suite asserts against its own source that it has not quietly taken
the seam. The parent also asserts the child ran `1 passed` — a stale
`--exact` filter would otherwise hollow the whole thing out silently.
* The shared spawners take all five storage variables.
`spawn_daemon_process_with_env` set `HOME` and `XDG_CONFIG_HOME` only;
`HOME` is a FALLBACK, so it isolates a root only while the matching
`XDG_*` is unset — the harness's apparent adequacy was a property of
one developer's environment. The PTY spawner backfills whichever of the
five its caller did not pin. The 10 direct `Command::new` daemon and
attach spawns get the same treatment.
Three suites had `mod common;` behind `#[cfg(feature = "crdt")]`;
`common::iso` is needed in every build, so those are ungated. Files that
already pull in `common` reach `iso` through a `use` rather than a second
`#[path]` declaration — loading one file as two modules is
`clippy::duplicate_mod`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Five findings from the manual validation pass, all in-branch:
1. LSP-only words never queried the server: the auto-open path fired
request_completion only when the sync providers already produced
rows. An empty sweep now leaves a pending session and the request
always fires; isIncomplete responses re-request on further typing.
Corollary: attachment_for_request now flushes-if-attached but NEVER
attaches -- the first cut wrapped attached_for_active, which spawns
servers on demand, i.e. per-keystroke spawn attempts in unattached
buffers (wedged the parallel m4 suite; serial ran 3x slower).
Attachment stays buffer-open policy.
2. Cross-buffer LSP leak: the built-in provider's no-uri fallback was
the legacy global store drain, so scratch/unattached buffers could
show another file's cached completions. Strict now: no uri, no rows.
3. Pending prefixes own the keyboard: Action::Pending (C-x ...)
dismisses the popup and the popup shadow is guarded on an empty
dispatcher prefix, so the sequence's continuation and its C-g abort
reach the dispatcher instead of the popup.
4. Window-scoped sessions: CompletionPopupState.window_id (stamped by
completion_popup_open; Lua never sees it). Only the owning window's
overlay paints -- same-buffer splits each carry a persistent
overlay -- and a focus change invalidates the session.
5. Flaky worker test: the /proc thread-count probe and the idempotence
check both build EditorStates and could run concurrently, polluting
the baseline; merged into one test (non-Linux keeps a portable
idempotence variant).
Regression tests for 1-4; framing doc gains the as-built notes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every EditorState leaked its entire worker pool (cores-1 threads, each
waking every 100ms): the Rc<AsyncRuntime> is cloned into dozens of Lua
closures, and registries those closures capture store mlua::Function
values -- reference cycles through the Lua VM that keep the Rc from
ever reaching zero, so WorkerPool::Drop never ran. Harmless for one
editor per process; in the m4 acceptance suite (54 editor-building
tests) it accumulated 1000+ live threads (observed: 60 complete
pmacs-worker-0..14 pools at once) plus ~9k spurious wakeups/second.
Fix: WorkerPool::signal_shutdown() -- set the shutdown flag + wake the
parkers from a shared reference; parked workers exit within their
100ms park timeout. Exposed as AsyncRuntime::shutdown_workers(), called
from a new impl Drop for EditorState. Deliberately signal-only, NO
join on the drop path: a worker can be blocked publishing its reply
onto the bus only the main thread drains, and a first cut that joined
in Drop deadlocked the m4 suite at teardown (fake-LSP tests wedged
2h+). WorkerPool::shutdown() (signal + join) remains for owners with
no bus consumer to deadlock against; Drop delegates to it as before.
Measured on the m4 suite: peak live threads 1026 -> 122.
Regression: tests/worker_shutdown_acceptance.rs (thread-count probe is
/proc-based, Linux-gated; the idempotence test runs everywhere).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>