238 lines
9.8 KiB
Markdown
238 lines
9.8 KiB
Markdown
# Framing — integration tests read the developer's real config
|
||
|
||
**Revision 1.** Status: awaiting review round 1. Proposed lane:
|
||
`test-ambient-config-isolation`, worktree `../pmacs-test-isolation`,
|
||
based on `githubsucks/main` @ `4cd4a7b` (a reading; re-measure at branch
|
||
time).
|
||
|
||
**The suite is green in CI and red on a developer machine that has a
|
||
real `~/.config/pmacs/init.lua`.** Not flaky — deterministic, and
|
||
attributed to whatever branch happens to be checked out.
|
||
|
||
## 0. Coherence impact (COHERENCE §20)
|
||
|
||
- **No journey step, no user-facing behaviour.** This is test
|
||
infrastructure.
|
||
- **Serves §9 (worker model) indirectly**: a gate that fails for reasons
|
||
unrelated to the change under test destroys the signal the gate exists
|
||
to give.
|
||
- **Interaction islands: none. Config registry: not adopted.
|
||
Background-work attribution: unchanged.**
|
||
- **No audited claim in COHERENCE.md changes**; under §25 no COHERENCE
|
||
edit rides this PR.
|
||
|
||
|
||
## 1. Ground truth (verified at `4cd4a7b`)
|
||
|
||
### 1.1 The observation
|
||
|
||
On 2026-07-30, `cargo test --all-targets --no-default-features --features
|
||
lua54` on a developer machine failed **11 of 67** in
|
||
`compile_mode_acceptance`, every one with:
|
||
|
||
```
|
||
[@/home/jeans/.config/pmacs/init.lua] command "find-file" is already
|
||
defined (refusing to overwrite)
|
||
stack traceback:
|
||
[C]: in field 'define'
|
||
/home/jeans/.config/pmacs/init.lua:3: in main chunk
|
||
```
|
||
|
||
Failing tests: `acc14`, `acc24`, `acc25a`, `acc27`, `acc29`, `r1f2`,
|
||
`r1f5`, `r2f1`, `r3f1`, `r4f1` (×2). With `XDG_CONFIG_HOME` pointed at an
|
||
empty directory the same suite is **67/67**.
|
||
|
||
### 1.2 The mechanism, and why the existing guard misses
|
||
|
||
`src/editor.rs:770` guards user-config loading:
|
||
|
||
```rust
|
||
#[cfg(not(test))]
|
||
{
|
||
crate::config::load_user_config(&mut lua_host);
|
||
lua_host.set_init_complete();
|
||
```
|
||
|
||
with a comment stating the intent exactly: *"Skipped under `cfg(test)` so
|
||
the lib's own test suite doesn't pick up the developer's real
|
||
`~/.config/pmacs/init.lua` and turn into a flaky environment-dependent
|
||
run."*
|
||
|
||
**`cfg(test)` is set only when compiling the crate's own unit tests.**
|
||
An integration test in `tests/` links `pmacs` as an ordinary dependency,
|
||
compiled *without* `cfg(test)` — so the guard is inactive for every one
|
||
of them. `cargo test --lib` is protected; `cargo test --test <name>` is
|
||
not.
|
||
|
||
The hazard was identified, a mitigation was written, and its scope does
|
||
not match the threat. That is the finding — not that nobody thought
|
||
about it.
|
||
|
||
### 1.3 There are two populations, and they need different fixes
|
||
|
||
- **Spawned.** The test launches `pmacs --daemon` as a child process.
|
||
Isolation works by setting the child's environment.
|
||
- **In-process.** The test constructs `pmacs::editor::EditorState`
|
||
directly in the test binary. `compile_mode_acceptance` is this kind
|
||
(`tests/compile_mode_acceptance.rs:14`).
|
||
|
||
**The spawned case is already solved, once.** `tests/m5_7_acceptance.rs:132`:
|
||
|
||
```rust
|
||
fn spawn_pmacs_daemon(socket_path: &Path) -> Child {
|
||
// Isolate user config: HOME and XDG_CONFIG_HOME both point at the
|
||
// (currently empty) socket parent directory, so the daemon won't
|
||
// try to read the developer's real `init.lua`.
|
||
...
|
||
.env("HOME", isolated_home)
|
||
.env("XDG_CONFIG_HOME", isolated_home)
|
||
```
|
||
|
||
Both variables, not just one — `config_dir` falls back from
|
||
`XDG_CONFIG_HOME` to `$HOME/.config`, so setting one alone leaves the
|
||
other path live.
|
||
|
||
### 1.4 In-process tests cannot isolate themselves
|
||
|
||
The obvious fix — set the variable in test setup — **is unavailable**.
|
||
`std::env::set_var` is `unsafe` in the 2024 edition and the crate is
|
||
`#![forbid(unsafe_code)]`.
|
||
|
||
This is already established in the codebase rather than inferred:
|
||
`src/packages/installer.rs:368` carries a `root_override` field
|
||
explicitly *because* of it — *"the project forbids `unsafe_code`, so
|
||
mutating `XDG_DATA_HOME` directly is not an option"*.
|
||
|
||
So an in-process test can only be isolated by (a) the environment it is
|
||
launched with, or (b) an injection point in the code under test. There
|
||
is precedent for (b) in the same repository.
|
||
|
||
### 1.5 Scale
|
||
|
||
96 files in `tests/`. **18** reference `EditorState`, `Editor::new`,
|
||
`load_user_config`, or `TestDaemon` and are therefore candidates. Only a
|
||
handful set any isolating variable today.
|
||
|
||
**The 18 is a candidate count from a name-based grep, not a census.**
|
||
It is an upper bound on nothing and a lower bound on nothing; §3 Bet 1
|
||
replaces it with a real classification. Recorded this way deliberately —
|
||
a previous lane in this repo built a classification from a truncated
|
||
grep and misclassified five rows.
|
||
|
||
### 1.6 What is NOT established
|
||
|
||
- **Whether any test *writes* into the developer's real directories.**
|
||
`XDG_DATA_HOME` and `XDG_STATE_HOME` back autosave
|
||
(`src/autosave.rs`), minibuffer history (`src/minibuffer.rs:724`),
|
||
builtin packages (`src/builtin_packages.rs:142`) and the package
|
||
installer (`src/packages/installer.rs:96`) — all with `HOME`
|
||
fallbacks. Read-only pollution is a failed gate; *write* pollution
|
||
would touch real user data. **No such write has been observed**, and
|
||
this lane does not claim one. Bet 2 goes looking, because the cost of
|
||
being wrong is asymmetric.
|
||
- **Whether CI is genuinely unaffected**, as opposed to merely having no
|
||
config today. A CI image that ever grows a `$HOME/.config/pmacs` would
|
||
break the same way, silently.
|
||
- **Whether the 11 failures are the whole blast radius.** The run
|
||
aborted at the first failing binary, so every suite ordered after
|
||
`compile_mode_acceptance` never executed.
|
||
|
||
|
||
## 2. Questions
|
||
|
||
- **Q#TI1** — Should the `cfg(test)` guard be widened, or should
|
||
isolation be the test harness's job? *Proposed: the harness's.
|
||
Widening the guard means production code deciding it is under test,
|
||
which is exactly the shape that lets a test pass against behaviour
|
||
production never runs.*
|
||
- **Q#TI2** — For in-process tests, injection point or launched
|
||
environment? *Proposed: an explicit constructor that does not load
|
||
user config, following `Installer::root_override`'s precedent. A
|
||
wrapper script that sets the variable fixes the symptom for whoever
|
||
remembers to use it.*
|
||
- **Q#TI3** — Should CI arm a check that the isolation is real?
|
||
*Proposed: yes — otherwise this recurs the moment a CI image grows a
|
||
config file, and recurs invisibly.*
|
||
- **Q#TI4** — Does anything write outside its temp dir? *Unknown; Bet 2.*
|
||
|
||
|
||
## 3. Bets
|
||
|
||
- **Bet 1 — the population is classifiable.** Every file in `tests/`
|
||
is classified as spawned, in-process, or neither, by reading each
|
||
candidate's construction site rather than by grepping for a name.
|
||
- *Falsified if* a file is both, or constructs the editor indirectly
|
||
through a helper that hides which it is. Then the classification is
|
||
reported with that ambiguity rather than forced.
|
||
|
||
- **Bet 2 — the exposure is read-only.** A test run under an
|
||
instrumented `HOME`/`XDG_*` pointing at a fresh directory leaves no
|
||
writes behind.
|
||
- *Falsified if* anything appears there — which upgrades this lane's
|
||
priority sharply, from "gates lie locally" to "tests touch real user
|
||
data".
|
||
- **A clean result is evidence about the suites that ran**, not a
|
||
guarantee; the run must be recorded with which binaries executed.
|
||
|
||
- **Bet 3 — isolation is verifiable by a test that fails without it.**
|
||
A positive control: a fixture writes an `init.lua` that would break a
|
||
known assertion, and the suite stays green because the isolation holds.
|
||
- *Falsified if* no such fixture can be built without the very env
|
||
mutation §1.4 rules out. Then isolation is asserted structurally
|
||
(no candidate constructs an editor without the non-loading path) and
|
||
labelled as the weaker check it is.
|
||
|
||
- **Bet 4 — the fix does not change production behaviour.** The
|
||
non-loading constructor is additive; the existing one is untouched.
|
||
- *Falsified if* any production call site has to change.
|
||
|
||
|
||
## 4. Acceptance
|
||
|
||
1. A classification of every `tests/*.rs` file into spawned /
|
||
in-process / neither, with counts stated and the method named
|
||
(read, not grepped).
|
||
2. In-process tests construct the editor through a path that does not
|
||
load user config, by explicit choice at the call site rather than by
|
||
a `cfg` the caller cannot see.
|
||
3. Spawned tests set **both** `HOME` and `XDG_CONFIG_HOME`, following
|
||
`m5_7_acceptance.rs:132`. Any that set only one are fixed.
|
||
4. `compile_mode_acceptance` passes with a real
|
||
`~/.config/pmacs/init.lua` present that defines `find-file` — the
|
||
exact condition that produced §1.1.
|
||
5. Bet 2's write-probe result recorded, listing which binaries ran.
|
||
6. `cfg(test)`-only guards are not widened into a
|
||
production-decides-it-is-under-test shape (Q#TI1).
|
||
7. The `src/editor.rs:770` comment is corrected: it claims a protection
|
||
it does not provide for integration tests.
|
||
8. README or the handoff records that a local full-suite run needs an
|
||
isolated `XDG_CONFIG_HOME` until this lands, so the next person does
|
||
not spend the afternoon I did attributing 11 failures to their branch.
|
||
|
||
|
||
## 5. Parked
|
||
|
||
- **Any change to how production resolves config paths.** Out of scope;
|
||
the defect is in the tests.
|
||
- **The `crdt` half of the corpus being dark in CI** — a different
|
||
coverage hole with its own lane in `docs/active-work.md`.
|
||
- **Whether CI should install a hostile `init.lua` deliberately** to
|
||
keep this honest. Attractive, but it is a CI-policy decision and this
|
||
lane is already load-bearing enough.
|
||
|
||
|
||
## 6. Gates
|
||
|
||
Standard suite, each its own step with a real exit status and nothing
|
||
after the command that could mask it. **Run twice: once with an isolated
|
||
`XDG_CONFIG_HOME`, and once with a deliberately hostile `init.lua` in
|
||
place.** A lane about ambient state that is only ever verified in a clean
|
||
environment has not been verified at all.
|
||
|
||
|
||
## 7. Branch plan
|
||
|
||
One branch, one PR. Bet 1's classification first and alone — it decides
|
||
how large the change is, and its answer belongs in review before any
|
||
mechanical edit rides on it.
|