Commit Graph

3 Commits

Author SHA1 Message Date
Levi Neuwirth fb14dc9ec3 test(isolation): migrate the corpus off the ambient roots
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
2026-07-31 18:48:45 -04:00
Levi Neuwirth 8e8f281f0e fix(terminal): close review round 1 on Stage 1
Five findings, all real. The blocker and both majors are the same
mistake in three places: a claim asserted somewhere cheaper than where
it actually lives.

COHERENCE.md was stale in four places, not the three reported. Step 8
still read "no keybinding" and §11 still read "five settings", but §6's
dispatch table also still cited `is_terminal_escape_chord` — a symbol
this branch deletes. §25 requires that update to ride the PR, so a PR
changing audited ground truth has to re-grep the audit for its own
symbols, not only for its topic.

Acceptance 5 asserted a registry round-trip, which is a test of the
registry: it stayed green with the setting's only consumer deleted. It
now opens a real terminal whose child overflows the 24-row screen,
scrolls the view to its oldest retained row, and asserts LINE001 is
present at 10,000 and absent at 0.

Acceptance 8a waited for the session count to fall, which the rejected
editor-side cache map satisfies exactly — a map with no purge hook
leaks while sessions drain. Adds `TerminalManager::escape_caches()`, the
lifetime half of Q#TC4c's contract that `escape_parses` cannot cover.

`table.sort` over `pmacs.terminal.profiles` raised "attempt to compare
number with string" on the unknown-profile path whenever the user's
table held both a string and a numeric key, replacing the exact
diagnostic being asked for; `%q` raised likewise on a non-string
`profile` argument. Both are partial functions applied to user input on
a diagnostic path.

Also corrects the framing's status line, and a status message whose
embedded whitespace run had survived a rustfmt reflow.

Three new bites, each falsified by revert: deleting the scrollback
consumer fails acc5 and only acc5; restoring the raw-key sort
reproduces the comparison error verbatim; and implementing the rejected
map fails the new acc8a at left: 2, right: 1 while passing the old
session-count version.

Merges githubsucks/main @ ccf29e3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
2026-07-25 21:47:37 -04:00
Levi Neuwirth 664cc25d0c feat(terminal): profiles, scrollback, and a configurable escape key
Stage 1 of the terminal config/copy-mode arc. The terminal had no
configuration surface at all: the command hardcoded $SHELL, scrollback
was a per-open argument only, and the escape chord was a literal in
Rust. No protocol change.

Profiles are a raw Lua table, pmacs.terminal.profiles, not a registry
setting: ConfigValue is four scalars with no table kind, so profiles
join pmacs.lsp.config and pmacs.pair.sets until table-valued settings
exist. The registry gains three scalars whose defaults reproduce the
previous behavior exactly.

Field resolution is explicit open argument, then profile field, then
scalar setting, then $SHELL. env MERGES, with explicit entries
overriding the profile's, because first-wins there would silently drop
half a user's environment. An explicitly named profile that does not
exist is an error even when terminal.default-profile is valid, so a typo
cannot silently fall back.

The two open-time settings resolve through the GLOBAL chain, because
they are read before the identity buffer exists and no caller could have
pinned a local override on a buffer that does not yet exist. Only
terminal.escape-key resolves per buffer, which makes a per-terminal
escape a supported feature.

The escape key is parsed at most once per (terminal, config epoch), and
the cache lives on TerminalSession so its lifetime is the terminal's,
with no purge hook to forget. The epoch alone is not a sufficient key:
it does not advance when focus moves between two terminals with
different buffer-local values, so an epoch-only cache serves one
terminal's chord to the other. An unparseable value falls back to C-c
and reports once per terminal per effective invalid value through the
status line, because a terminal with no escape chord cannot be escaped
to fix the setting that broke it.

Repeating the escape now sends THAT chord to the child through the
ordinary key encoder, rather than a hardcoded ETX. With an escape of
C-x, the previous code sent Ctrl-C and made literal Ctrl-X unreachable.

C-c t opens a terminal. COHERENCE Priority 1 names a terminal
keybinding, and section 2 step 8 grades the terminal works-but-
undiscoverable; C-c is already a live global prefix, so this is a new
leaf rather than a shadow. It is unreachable from inside a terminal,
where C-c is the escape.

Acceptance is tests/terminal_config_acceptance.rs, deliberately NOT
crdt-gated so CI actually runs it. Four bites, each against a different
plausible wrong implementation: a hardcoded ETX fails acc6/9; an
epoch-only cache key fails acc7; a single last-entry cache fails acc8's
parse count; removing the invalid-value fallback fails acc10.

Two test-instrument notes worth keeping. cat -v is the echo probe
because the screen rejects C0 controls before they reach cells, so a raw
echoed Ctrl-X would be invisible. And the probe counts occurrences
rather than testing presence, because a single-character probe collides
with the child's own banner text.
2026-07-25 18:36:18 -04:00