Merge pull request #206 from levineuwirth/test-ambient-isolation-impl

test: isolate integration tests from the developer's ambient storage roots
This commit is contained in:
Levi Neuwirth 2026-07-31 20:09:39 -04:00 committed by GitHub
commit 28f878b734
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
82 changed files with 2574 additions and 341 deletions

View File

@ -414,13 +414,33 @@ compatible.
- **DAP waits for Stage 2, not Stage 1** — that dependency is now
satisfied.
## Test ambient-root isolation — FRAMING OPEN, revision 4
## Test ambient-root isolation — IMPLEMENTATION OPEN
- **Branch `test-ambient-config-isolation`**, worktree
`../pmacs-test-isolation`, based on `githubsucks/main` @ `4cd4a7b`.
**Framing only; no code, no PR yet.**
`docs/test-ambient-config-isolation-framing.md` revision 4, three review
rounds closed (eight blocking, six major, all accepted).
- **Framing MERGED (#201)**;
`docs/test-ambient-config-isolation-framing.md` **revision 5**
(revision 4 approved after three review rounds — eight blocking, six
major, all accepted; revision 5 rides the implementation PR and records
findings, not a new design round).
- **PR #206 OPEN**, one review round closed. **§7's branch plan was
consciously exceeded**: the whole-corpus migration rides this PR rather
than a follow-up lane, because splitting would either leave 65 suites
writing the real data root or ship acceptance 12's ratchet with a
~65-file allowlist that does not ratchet. Recorded in framing revision
5; accepted in review.
- **Implementation branch `test-ambient-isolation-impl`**, worktree
`../pmacs-test-isolation-impl`, based on `githubsucks/main` @
`54a092e`. The framing-only worktree `../pmacs-test-isolation`
(branch `test-ambient-config-isolation`) is spent — its doc is on
`main`.
- **What landed on the branch.** `pmacs::bootstrap::BootstrapRoots`
(config/data/state/cache, `ambient()` for production), reachable from
`EditorState::new_with_roots` **and** `open_with_roots` and consulted
again by `install_state_dirs`; all 342 in-process construction sites
in 65 files migrated; `journey_acceptance` keeps the ambient `open`
and re-execs itself per test with controlled roots instead; the shared
daemon and PTY spawners now set all five storage variables;
`tests/ambient_isolation_acceptance.rs` carries the hostile-environment
proof and the adoption ratchet.
- **What it is.** Integration tests use the developer's real ambient
roots. `#[cfg(not(test))]` guards config loading against the crate's
own unit tests only, so the **65** files in `tests/` that construct an
@ -455,9 +475,9 @@ compatible.
```sh
git fetch githubsucks
git worktree add ../pmacs-test-isolation \
-b test-ambient-config-isolation \
githubsucks/test-ambient-config-isolation
git worktree add ../pmacs-test-isolation-impl \
-b test-ambient-isolation-impl \
githubsucks/test-ambient-isolation-impl
```
## Reap-ledger silent failures — MERGED (#202); kept for its parked follow-ons

View File

@ -1639,6 +1639,35 @@ git diff --check
Machine-specific caveats — re-verify on a machine you haven't used
before trusting them:
- **Ambient storage roots: control all FIVE, not four.** Until the
ambient-root isolation lane lands
(`docs/test-ambient-config-isolation-framing.md`), the ~96 integration
suites read the developer's real `~/.config/pmacs/init.lua` and
**write** bundled packages into the real data root:
`#[cfg(not(test))]` guards the crate's own unit tests only, and
`EditorState::new` materializes packages outside every `cfg` guard.
A local full-suite run therefore needs, all pointed at a fresh
directory:
```
XDG_CONFIG_HOME XDG_DATA_HOME XDG_STATE_HOME XDG_CACHE_HOME
PMACS_STATE_HOME
```
**The fifth is not redundant.** `PMACS_STATE_HOME` outranks
`XDG_STATE_HOME` (`src/state.rs`), so redirecting the four XDG
variables on a machine that exports it leaves the real state root
live. `HOME` is deliberately left alone: it is the fallback for the
XDG roots (already covered once they are set) and separately drives
`~`-expansion, which `find_file_acceptance` pins on purpose.
A run isolating only `XDG_CONFIG_HOME` stops the `init.lua` reads and
still writes through the real data root — every local gate run in this
repo before 2026-07-31 had that hole.
**After the lane lands** the in-process population isolates itself
through `EditorState::new_with_roots(&crate::iso::roots())` and the
five variables are belt-and-braces rather than required.
- **basedpyright**: the desktop binary was **never broken** — this was a
real code defect, diagnosed and fixed 2026-07-29 (see §5, "A `Drop`
body runs before its fields"). `RuntimeHandles::drop` joined its reader

View File

@ -1,9 +1,11 @@
# Framing — integration tests use the developer's real ambient roots
**Revision 4.** Status: awaiting review round 4. Proposed lane:
`test-ambient-config-isolation`, worktree `../pmacs-test-isolation`,
based on `githubsucks/main` @ `4cd4a7b` (a reading; re-measure at branch
time).
**Revision 5.** Status: implemented on branch
`test-ambient-isolation-impl` (worktree `../pmacs-test-isolation-impl`,
based on `githubsucks/main` @ `54a092e`), PR #206. Revision 4 was
approved after three review rounds and merged as #201; revision 5 records
implementation findings and one **deliberate departure from §7's branch
plan**, not a new design round.
**The suite is green in CI and red on a developer machine that has a
real `~/.config/pmacs/init.lua`.** Not flaky — deterministic, and
@ -16,6 +18,51 @@ directory**. The lane is therefore about *ambient roots*, not about
## Revision history
**Revision 4 → 5**, at implementation. No design changed; two things are
recorded that a later reader would otherwise have to reconstruct.
### The §7 branch plan was consciously exceeded
**§7 said: classification first and alone, "its answer belongs in review
before any mechanical edit rides on it."** The classification came back
at **342 in-process construction sites across 66 of 97 files** — large
enough that §7's shape would naturally suggest splitting the mechanical
migration into its own lane. **It was not split, and that is deliberate.**
The alternatives were both worse, and both worse in the way this lane
exists to prevent:
- **Split, migrating nothing now.** The seam would land while 65 files
kept reading the developer's real `init.lua` and writing their real
data root. The lane's own defect would survive its own PR.
- **Split, with a broad temporary allowlist.** Acceptance 12's ratchet
would ship exempting ~65 files. A ratchet whose allowlist is most of
the corpus does not ratchet; it records. And the exemption would have
to be removed later by the same reviewer who granted it, with nothing
failing in the meantime to remind anyone.
So the whole-corpus migration is the atomic remediation and rides this
PR. §7's *ordering* is honoured — the classification is the first commit,
before any mechanical edit — while its implied *scoping* is not. The
qualification this leaves on acceptance 12 (the allowlist is narrow
because everything else moved, not because the change was small) was
raised in the PR and accepted in review round 1.
### The exemption shape acceptance 12 needs
Review round 1 found an ambient `EditorState::new()` in an ordinary
parent test of the isolation suite itself — the exposure, committed by
the suite written to remove it. **A file-level allowlist did not catch
it**, because the file was already exempted for its re-exec'd positive
control, and a bare exemption licenses a named file to grow new ambient
sites indefinitely.
The fix is that **every exemption carries its exact site count**, so an
added site fails even inside an exempted file. That is the shape
acceptance 12 needs; "a narrow, named allowlist" is not sufficient on its
own, because narrowness constrains which files are exempt and says
nothing about how far each exemption stretches.
**Revision 3 → 4**, after review round 3 (one blocking, two major). All
three accepted.

391
src/bootstrap.rs Normal file
View File

@ -0,0 +1,391 @@
// src/bootstrap.rs --- explicit bootstrap storage roots.
//! Where pmacs stores things at startup, as a value rather than as an
//! ambient property of the process environment.
//!
//! # Why this exists
//!
//! `EditorState::new` resolves two storage roots from the environment
//! before it returns:
//!
//! * the **data** root, which
//! [`crate::builtin_packages::bundled_runtime_dir`] resolves from
//! `XDG_DATA_HOME` (else `$HOME/.local/share`) and which
//! [`crate::builtin_packages::materialize_all`] then **writes into**
//! --- unconditionally, outside any `cfg` guard; and
//! * the **config** root, which [`crate::config::user_config_dir`]
//! resolves from `XDG_CONFIG_HOME` (else `$HOME/.config`) and from
//! which `init.lua` is read.
//!
//! An integration test in `tests/` links this crate as an ordinary
//! dependency, so it is compiled **without** `cfg(test)`: the
//! `#[cfg(not(test))]` guard around config loading is inactive for
//! every one of them. They read the developer's real `init.lua` and
//! write into the developer's real data root.
//!
//! They cannot fix that themselves. `std::env::set_var` has been
//! `unsafe` since Rust 2024 and this crate is `#![forbid(unsafe_code)]`
//! --- the same constraint that produced
//! [`crate::packages::installer::Installer::with_install_root_override`]
//! and [`crate::lua_bindings::PackageInstallOverride`]. So isolation has
//! to arrive as a **parameter**, which is what this type is.
//!
//! # Contract
//!
//! [`BootstrapRoots::ambient()`] is production: every root stays `None`
//! and every resolution goes to the environment exactly as before. A
//! root that is `Some` replaces the environment lookup for that root and
//! **only** that root.
//!
//! # Scope: storage roots only
//!
//! This type covers the four roots that decide where pmacs *stores*
//! things: config, data, state and cache. It deliberately does not
//! cover:
//!
//! * **`HOME`'s non-storage semantics.** `expand_tilde`
//! ([`crate::editor_core`]) resolves a leading `~` for ordinary path
//! entry, and `tests/find_file_acceptance.rs` consumes `HOME` on
//! purpose to pin that expansion. Redirecting a storage root is the
//! right fix for a storage root and the wrong fix for a
//! path-expansion root.
//! * **`XDG_RUNTIME_DIR`**, which addresses sockets rather than stored
//! data.
use std::path::{Path, PathBuf};
/// The bootstrap storage roots an [`crate::editor::EditorState`] is
/// constructed against.
///
/// Each field is the *base* directory --- the value `XDG_<X>_HOME`
/// would hold --- not the `pmacs/` subdirectory under it. `None` means
/// "resolve from the environment", which is what production does.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct BootstrapRoots {
config: Option<PathBuf>,
data: Option<PathBuf>,
state: Option<PathBuf>,
cache: Option<PathBuf>,
}
/// The version-keyed leaf `bundled_runtime_dir` materializes into.
fn bundled_leaf() -> String {
format!("v{}", env!("CARGO_PKG_VERSION"))
}
impl BootstrapRoots {
/// Production: every root resolves from the process environment.
#[must_use]
pub fn ambient() -> Self {
Self::default()
}
/// Every storage root redirected under `base`, into sibling
/// `config/`, `data/`, `state/` and `cache/` directories.
///
/// The layout mirrors the XDG one so a caller that also spawns a
/// child process can hand the same four paths to `XDG_CONFIG_HOME`,
/// `XDG_DATA_HOME`, `XDG_STATE_HOME` and `XDG_CACHE_HOME` (plus
/// `PMACS_STATE_HOME`, which outranks `XDG_STATE_HOME`) and get the
/// same tree from an in-process and a spawned editor.
#[must_use]
pub fn isolated_under(base: &Path) -> Self {
Self {
config: Some(base.join("config")),
data: Some(base.join("data")),
state: Some(base.join("state")),
cache: Some(base.join("cache")),
}
}
/// Builder: redirect the config root (the `XDG_CONFIG_HOME` value).
#[must_use]
pub fn with_config_root(mut self, root: PathBuf) -> Self {
self.config = Some(root);
self
}
/// Builder: redirect the data root (the `XDG_DATA_HOME` value).
#[must_use]
pub fn with_data_root(mut self, root: PathBuf) -> Self {
self.data = Some(root);
self
}
/// Builder: redirect the state root (the `XDG_STATE_HOME` value).
#[must_use]
pub fn with_state_root(mut self, root: PathBuf) -> Self {
self.state = Some(root);
self
}
/// Builder: redirect the cache root (the `XDG_CACHE_HOME` value).
#[must_use]
pub fn with_cache_root(mut self, root: PathBuf) -> Self {
self.cache = Some(root);
self
}
/// True when nothing is redirected --- i.e. this is production's
/// [`Self::ambient`].
#[must_use]
pub fn is_ambient(&self) -> bool {
self.config.is_none() && self.data.is_none() && self.state.is_none() && self.cache.is_none()
}
/// The config *base*, if redirected.
#[must_use]
pub fn config_root(&self) -> Option<&Path> {
self.config.as_deref()
}
/// The data *base*, if redirected.
#[must_use]
pub fn data_root(&self) -> Option<&Path> {
self.data.as_deref()
}
/// The state *base*, if redirected.
#[must_use]
pub fn state_root(&self) -> Option<&Path> {
self.state.as_deref()
}
/// The cache *base*, if redirected.
#[must_use]
pub fn cache_root(&self) -> Option<&Path> {
self.cache.as_deref()
}
/// The directory `init.lua` is read from --- `<config>/pmacs`, the
/// same shape [`crate::config::user_config_dir`] builds.
#[must_use]
pub fn config_dir(&self) -> Option<PathBuf> {
self.config.as_ref().map(|p| p.join("pmacs"))
}
/// Where bundled packages are materialized ---
/// `<data>/pmacs/builtin-packages/v<crate-version>`, the same shape
/// [`crate::builtin_packages::bundled_runtime_dir`] builds.
#[must_use]
pub fn bundled_runtime_dir(&self) -> Option<PathBuf> {
self.data.as_ref().map(|p| {
p.join("pmacs")
.join("builtin-packages")
.join(bundled_leaf())
})
}
/// The user-scope package install root --- `<data>/pmacs/packages`.
#[must_use]
pub fn package_install_root(&self) -> Option<PathBuf> {
self.data.as_ref().map(|p| p.join("pmacs").join("packages"))
}
/// The package fetcher's bare-mirror cache --- `<cache>/pmacs/git`.
#[must_use]
pub fn package_cache_dir(&self) -> Option<PathBuf> {
self.cache.as_ref().map(|p| p.join("pmacs").join("git"))
}
/// The editor state directory --- `<state>/pmacs`, the same shape
/// [`crate::state::user_state_dir`] builds.
#[must_use]
pub fn state_dir(&self) -> Option<PathBuf> {
self.state.as_ref().map(|p| p.join("pmacs"))
}
/// The minibuffer history directory --- `<state>/pmacs/history`.
#[must_use]
pub fn history_dir(&self) -> Option<PathBuf> {
self.state_dir().map(|d| d.join("history"))
}
/// The environment a **child** `pmacs` process must be given so it
/// resolves the same roots this value names.
///
/// An in-process caller passes the value; a caller that spawns
/// `pmacs --daemon` (or re-execs a test binary) cannot, and has to
/// go through the environment instead. This is that translation, so
/// the two paths cannot drift.
///
/// **Five variables, not four.** `PMACS_STATE_HOME` outranks
/// `XDG_STATE_HOME` ([`crate::state::user_state_dir`]), so a child
/// given only the four XDG variables still resolves the *inherited*
/// `PMACS_STATE_HOME` if the launching environment exports one --- a
/// hole that is invisible on a machine that does not.
///
/// A root left ambient emits no variable, so the child inherits it.
#[must_use]
pub fn child_env(&self) -> Vec<(&'static str, PathBuf)> {
let mut out = Vec::with_capacity(5);
if let Some(p) = &self.config {
out.push(("XDG_CONFIG_HOME", p.clone()));
}
if let Some(p) = &self.data {
out.push(("XDG_DATA_HOME", p.clone()));
}
if let Some(p) = &self.state {
out.push(("XDG_STATE_HOME", p.clone()));
out.push(("PMACS_STATE_HOME", p.clone()));
}
if let Some(p) = &self.cache {
out.push(("XDG_CACHE_HOME", p.clone()));
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ambient_redirects_nothing() {
let roots = BootstrapRoots::ambient();
assert!(roots.is_ambient());
assert_eq!(roots.config_dir(), None);
assert_eq!(roots.bundled_runtime_dir(), None);
assert_eq!(roots.state_dir(), None);
assert_eq!(roots.package_cache_dir(), None);
}
#[test]
fn isolated_under_mirrors_the_xdg_layout() {
let roots = BootstrapRoots::isolated_under(Path::new("/scratch"));
assert!(!roots.is_ambient());
assert_eq!(
roots.config_dir().unwrap(),
Path::new("/scratch/config/pmacs")
);
assert_eq!(
roots.bundled_runtime_dir().unwrap(),
Path::new("/scratch/data/pmacs/builtin-packages").join(bundled_leaf())
);
assert_eq!(
roots.package_install_root().unwrap(),
Path::new("/scratch/data/pmacs/packages")
);
assert_eq!(
roots.package_cache_dir().unwrap(),
Path::new("/scratch/cache/pmacs/git")
);
assert_eq!(
roots.state_dir().unwrap(),
Path::new("/scratch/state/pmacs")
);
assert_eq!(
roots.history_dir().unwrap(),
Path::new("/scratch/state/pmacs/history")
);
}
/// A builder that sets one root leaves the other three ambient ---
/// the "only that root" half of the contract.
#[test]
fn a_single_builder_leaves_the_other_roots_ambient() {
let roots = BootstrapRoots::ambient().with_config_root(PathBuf::from("/only/config"));
assert!(!roots.is_ambient());
assert_eq!(roots.config_dir().unwrap(), Path::new("/only/config/pmacs"));
assert_eq!(roots.bundled_runtime_dir(), None);
assert_eq!(roots.state_dir(), None);
assert_eq!(roots.package_cache_dir(), None);
}
/// The five-variable contract, asserted as content: naming only the
/// four XDG variables leaves `PMACS_STATE_HOME` --- which outranks
/// `XDG_STATE_HOME` --- pointing wherever the launching environment
/// left it.
#[test]
fn child_env_names_all_five_storage_variables() {
let roots = BootstrapRoots::isolated_under(Path::new("/scratch"));
let env = roots.child_env();
let names: Vec<&str> = env.iter().map(|(k, _)| *k).collect();
assert_eq!(
names,
vec![
"XDG_CONFIG_HOME",
"XDG_DATA_HOME",
"XDG_STATE_HOME",
"PMACS_STATE_HOME",
"XDG_CACHE_HOME",
]
);
let value = |name: &str| {
env.iter()
.find(|(k, _)| *k == name)
.map(|(_, v)| v.clone())
.unwrap()
};
assert_eq!(value("XDG_CONFIG_HOME"), Path::new("/scratch/config"));
assert_eq!(value("XDG_DATA_HOME"), Path::new("/scratch/data"));
assert_eq!(value("XDG_STATE_HOME"), Path::new("/scratch/state"));
assert_eq!(value("PMACS_STATE_HOME"), Path::new("/scratch/state"));
assert_eq!(value("XDG_CACHE_HOME"), Path::new("/scratch/cache"));
}
/// An ambient root emits no variable — the child inherits it. A
/// blanket five-variable emission would silently redirect roots the
/// caller deliberately left alone.
#[test]
fn child_env_emits_nothing_for_ambient_roots() {
assert!(BootstrapRoots::ambient().child_env().is_empty());
let only_config = BootstrapRoots::ambient().with_config_root(PathBuf::from("/only/config"));
assert_eq!(
only_config.child_env(),
vec![("XDG_CONFIG_HOME", PathBuf::from("/only/config"))]
);
}
/// `child_env` and the in-process resolvers must describe the same
/// tree: the spawned daemon and the in-process editor are both
/// supposed to land in the isolated roots, and a mismatch would let
/// one of them escape while the other looked fine.
#[test]
fn child_env_agrees_with_the_in_process_resolvers() {
let roots = BootstrapRoots::isolated_under(Path::new("/scratch"));
let env = roots.child_env();
let value = |name: &str| {
env.iter()
.find(|(k, _)| *k == name)
.map(|(_, v)| v.clone())
.unwrap()
};
assert_eq!(
roots.config_dir().unwrap(),
value("XDG_CONFIG_HOME").join("pmacs")
);
assert_eq!(
roots.package_install_root().unwrap(),
value("XDG_DATA_HOME").join("pmacs").join("packages")
);
assert_eq!(
roots.state_dir().unwrap(),
value("PMACS_STATE_HOME").join("pmacs")
);
assert_eq!(
roots.package_cache_dir().unwrap(),
value("XDG_CACHE_HOME").join("pmacs").join("git")
);
}
/// The isolated bundled dir must agree with the ambient resolver's
/// shape, version leaf included: a mismatch would make an isolated
/// editor materialize somewhere production never looks.
#[test]
fn bundled_leaf_matches_the_ambient_resolver() {
let roots = BootstrapRoots::isolated_under(Path::new("/scratch"));
let isolated = roots.bundled_runtime_dir().unwrap();
// `bundled_runtime_dir()` reads the live environment, so compare
// only the tail that does not depend on it.
let ambient = crate::builtin_packages::bundled_runtime_dir();
let tail = |p: &Path| {
p.components()
.rev()
.take(3)
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>()
};
assert_eq!(tail(&isolated), tail(&ambient));
}
}

View File

@ -273,6 +273,14 @@ pub struct EditorState {
/// steal or clear another's in-flight gesture, and concurrent drags
/// are perfectly legal.
window_drag: HashMap<FrontendId, WindowDragState>,
/// The bootstrap storage roots this session was constructed
/// against (`docs/test-ambient-config-isolation-framing.md`).
///
/// [`BootstrapRoots::ambient`] in production. Retained past
/// construction because [`Self::install_state_dirs`] resolves a root
/// too and runs *after* the constructor --- resolving it from the
/// environment there would reopen the hole the constructor closed.
bootstrap_roots: crate::bootstrap::BootstrapRoots,
}
#[derive(Default)]
@ -345,6 +353,24 @@ impl EditorState {
/// Panics only if Lua initialization or the builtin command/keymap
/// chunks fail to load --- both indicate broken builds.
#[must_use]
pub fn new() -> Self {
Self::new_with_roots(&crate::bootstrap::BootstrapRoots::ambient())
}
/// Construct a fresh editor against explicit bootstrap storage
/// roots (`docs/test-ambient-config-isolation-framing.md` §1.4).
///
/// [`Self::new`] is this with [`BootstrapRoots::ambient`], so
/// production behaviour is unchanged. An integration test passes
/// redirected roots instead: it links this crate without
/// `cfg(test)`, so the guard below is live for it, and without a
/// parameter it would read the developer's real `init.lua` and
/// write bundled packages into the developer's real data root.
/// `std::env::set_var` is not an alternative --- it is `unsafe` and
/// this crate is `#![forbid(unsafe_code)]`.
///
/// [`BootstrapRoots::ambient`]: crate::bootstrap::BootstrapRoots::ambient
#[must_use]
#[allow(
clippy::too_many_lines,
reason = "linear bootstrap sequence: registry → core → LuaHost → \
@ -352,7 +378,7 @@ impl EditorState {
Splitting into helpers fragments the wiring without removing \
any single decision the reader needs to follow."
)]
pub fn new() -> Self {
pub fn new_with_roots(roots: &crate::bootstrap::BootstrapRoots) -> Self {
// Build the buffer registry first so EditorCore and LuaHost
// share the same `Rc`. Both reach buffers through this handle;
// multi-window dispatch (T M2.8) requires that ids resolve to
@ -738,9 +764,34 @@ impl EditorState {
// Depends on `pmacs.buffer.add_intercept` (T M6.4 Stage 1)
// and `pmacs.ansi.parser()` (T M6.4 Stage 2), both available
// by the time `attach_editor` returns above.
let bundled_root = crate::builtin_packages::bundled_runtime_dir();
//
// `materialize_all` CREATES DIRECTORIES AND WRITES FILES, and
// it is outside every `cfg` guard — so this, not config
// loading, is the write half of the ambient-roots exposure
// (`docs/test-ambient-config-isolation-framing.md` §1.6). The
// redirected root is consulted first for exactly that reason.
let bundled_root = roots
.bundled_runtime_dir()
.unwrap_or_else(crate::builtin_packages::bundled_runtime_dir);
let bundled_packages = crate::builtin_packages::materialize_all(&bundled_root)
.expect("materialize bundled packages");
// Redirected roots also redirect where `pmacs.packages.install`
// would later fetch and install, so a caller that isolated its
// storage cannot reach the real `$XDG_CACHE_HOME/pmacs/git` or
// `$XDG_DATA_HOME/pmacs/packages` through Lua either. Installed
// before user config runs, so an `init.lua` that installs a
// package lands inside the isolated tree. Production leaves the
// slot empty (ambient roots take this branch not at all).
if !roots.is_ambient() {
let mut override_ = crate::lua_bindings::PackageInstallOverride::new();
if let Some(cache) = roots.package_cache_dir() {
override_ = override_.with_cache_dir(cache);
}
if let Some(install) = roots.package_install_root() {
override_ = override_.with_user_install_root(install);
}
lua_host.set_package_install_override(override_);
}
{
let slot = lua_host
.lua()
@ -772,17 +823,43 @@ impl EditorState {
// to exercise config loading do so explicitly via
// [`crate::config::load_user_config_at`].
//
// **`cfg(test)` covers the lib's unit tests and NOTHING ELSE.**
// An integration test in `tests/` links this crate as an
// ordinary dependency, compiled without `cfg(test)`, so this
// block is fully live for all ~96 of them: `cargo test --lib` is
// protected, `cargo test --test <name>` is not. That is what the
// `roots` parameter is for — an integration test redirects the
// config root instead of relying on a guard that does not reach
// it. (Nor is this the only ambient root: the bundled-package
// materialization above runs unconditionally and WRITES.) See
// `docs/test-ambient-config-isolation-framing.md` §1.2, §1.6.
//
// The guard is deliberately NOT widened to cover integration
// tests: production deciding it is under test is how a suite
// passes against behaviour production never runs (Q#TI1).
//
// The init-complete flip happens here too so lifecycle-gated
// Lua APIs (e.g. `pmacs.attach`, M5.6d+) become inert after
// user config returns. Tests that need post-init semantics flip
// the flag explicitly via [`crate::lua::LuaHost::set_init_complete`];
// see the option-(A) discussion in the M5.6c survey.
//
// Redirected roots change *which directory* is read, never
// *whether* the block runs: `tests/m8_2_acceptance.rs:75`
// documents its dependence on integration-test construction
// finishing init-complete, so skipping the block for isolated
// construction would leave every such test permanently in the
// init phase (framing §1.8).
#[cfg(not(test))]
{
crate::config::load_user_config(&mut lua_host);
match roots.config_dir() {
Some(dir) => crate::config::load_user_config_at(&mut lua_host, &dir),
None => crate::config::load_user_config(&mut lua_host),
}
lua_host.set_init_complete();
}
Self {
bootstrap_roots: roots.clone(),
core,
lua_host,
dispatchers: HashMap::new(),
@ -879,10 +956,24 @@ impl EditorState {
/// `~/.local/state/pmacs`. Honors the `PMACS_STATE_HOME` override
/// (see [`crate::state::user_state_dir`]).
pub fn install_state_dirs(&self) {
if let Some(dir) = crate::minibuffer::user_history_dir() {
// Redirected roots win over the environment here too. This runs
// after construction, so resolving from the environment would
// hand an isolated session the developer's real state dir --- and
// `PMACS_STATE_HOME` outranks `XDG_STATE_HOME`, so an
// environment-side fix would have to cover five variables, not
// four (framing §1.6a).
let history = self
.bootstrap_roots
.history_dir()
.or_else(crate::minibuffer::user_history_dir);
if let Some(dir) = history {
self.core.borrow_mut().minibuffer.history_dir = Some(dir);
}
if let Some(dir) = crate::state::user_state_dir() {
let state = self
.bootstrap_roots
.state_dir()
.or_else(crate::state::user_state_dir);
if let Some(dir) = state {
self.lua_host
.lua()
.set_app_data(crate::lua_bindings::StateDir(dir));
@ -1029,7 +1120,27 @@ impl EditorState {
behavioral gain"
)]
pub fn open(path: PathBuf) -> io::Result<Self> {
let mut state = Self::new();
Self::open_with_roots(path, &crate::bootstrap::BootstrapRoots::ambient())
}
/// [`Self::open`] against explicit bootstrap storage roots.
///
/// `open` calls `Self::new()` internally, so a roots parameter on
/// the constructor alone leaves every open-path test ambient
/// (`docs/test-ambient-config-isolation-framing.md` §1.7). The two
/// entry points therefore gain the parameter together, and `open`
/// stays a thin ambient caller of this — the golden-journey ratchet
/// requires that exact public entry point to have a production
/// caller shape.
#[allow(
clippy::needless_pass_by_value,
reason = "mirrors `open`'s stable signature; see the note there"
)]
pub fn open_with_roots(
path: PathBuf,
roots: &crate::bootstrap::BootstrapRoots,
) -> io::Result<Self> {
let mut state = Self::new_with_roots(roots);
let resolved = state
.core
.borrow_mut()

View File

@ -46,6 +46,7 @@ pub mod attach_dispatch;
pub mod attach_reconnect;
pub mod audit;
pub mod autosave;
pub mod bootstrap;
pub mod buffer;
pub mod buffer_registry;
pub mod builtin_packages;

View File

@ -0,0 +1,706 @@
// tests/ambient_isolation_acceptance.rs --- integration tests must not
// read or write the developer's real ambient roots.
//! Acceptance for `docs/test-ambient-config-isolation-framing.md`.
//!
//! # The defect
//!
//! `src/editor.rs` guards user-config loading with `#[cfg(not(test))]`.
//! `cfg(test)` is set only while compiling the crate's *own* unit tests;
//! an integration test in `tests/` links `pmacs` as an ordinary
//! dependency, so the guard is inactive for all of them. `cargo test
//! --lib` is protected, `cargo test --test <name>` is not. And config
//! loading is only the read half: `EditorState::new` materializes
//! bundled packages into `$XDG_DATA_HOME/pmacs` (else
//! `$HOME/.local/share`) **unconditionally**, outside every `cfg` guard,
//! creating directories and writing files.
//!
//! # The census (framing acceptance 1), read at `54a092e`
//!
//! Method: every occurrence was listed with its enclosing context and
//! read. A grep for the bare name over-counts — the framing's revision 1
//! reported 18 by grepping `Editor::new`, which does not even match the
//! real constructor `EditorState::new`.
//!
//! **In-process construction — 342 sites in 66 of 97 files.**
//!
//! * `EditorState::new()` — 334 textual occurrences, of which **330 are
//! calls**. The other 4 are prose: `persistence_acceptance.rs:6` and
//! `:76`, `m7_11_acceptance.rs:92`, `m8_2_acceptance.rs:75`. A fifth
//! file, `m5_6_acceptance.rs:94`, names the constructor only to say it
//! deliberately does **not** use it — the third place in the tree
//! documenting this same `cfg(test)` gap.
//! * `EditorState::open(` — 14 textual occurrences in 3 files, of which
//! **12 are calls** (`journey_acceptance` 7, `m4_acceptance` 4,
//! `theme_faces_acceptance` 1). The other 2 are assertion-message
//! strings in `journey_acceptance.rs:2030,2038`.
//! * Only 329 of the 330 `new()` calls are `let` bindings; the odd one
//! is `m8_1_acceptance.rs:39`, a bare tail expression in a
//! `fresh_editor()` helper. Sites, not files, are the unit.
//!
//! **Spawned `pmacs` — 14 sites in 8 files.** `grep CARGO_BIN_EXE_pmacs`
//! reports 36 sites in 26 files, but 18 of those are the *sibling*
//! binaries `CARGO_BIN_EXE_pmacs_fake_lsp` / `_fake_mcp`, which are LSP
//! and MCP stubs and not pmacs at all. Reading each occurrence:
//!
//! * `tests/common/daemon.rs:158` — the shared `--daemon` harness.
//! * `tests/common/pty.rs:111` — the shared real-PTY spawner.
//! * `m5_7_acceptance.rs` ×5, `m5_8_acceptance.rs` ×3,
//! `m5_5_acceptance.rs` ×1, `m5_perf_acceptance.rs` ×1,
//! `gpu_invocation_acceptance.rs` ×2 — direct `Command::new`.
//! * 4 further occurrences are **path derivations, not spawns**:
//! `gpu_invocation_acceptance.rs:115`, `vterm_stage3_acceptance.rs:644`
//! and `:1180`, `bottom_panel_stage2b_gpu_acceptance.rs:513` all take
//! `CARGO_BIN_EXE_pmacs`'s *parent* to locate the `pmacs-gpu` sibling.
//!
//! **Mixed files are real: 5 files are both in-process and spawned** —
//! `vterm_stage3_acceptance` constructs an editor at `:159` and reaches
//! a daemon at `:665`. A file-level partition cannot represent them,
//! which is why the ratchet below keys on sites.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use pmacs::bootstrap::BootstrapRoots;
use pmacs::editor::EditorState;
#[path = "common/iso.rs"]
mod iso;
// ---------------------------------------------------------------------------
// Isolated construction still finishes initialization (framing §1.8)
// ---------------------------------------------------------------------------
/// **N** — isolated construction skips the ambient *reads* and still
/// flips the init-complete gate.
///
/// Config loading and `set_init_complete()` share one conditional block,
/// so the tempting fix — skip the block when roots are redirected —
/// would leave every isolated test permanently in the init phase.
/// `tests/m8_2_acceptance.rs:75` documents its dependence on
/// integration-test construction being init-complete, so that is not a
/// hypothetical.
///
/// Falsified by wrapping the block in `if roots.is_ambient()`.
#[test]
fn isolated_construction_is_init_complete() {
let state = EditorState::new_with_roots(&iso::roots());
assert!(
state.lua_host.is_init_complete(),
"isolated construction must still leave the init phase, or every \
suite that reopens it (m8_2) breaks"
);
// The paired half — that the *ambient* constructor is unchanged —
// deliberately does NOT live here. Asserting it needs an ambient
// `EditorState::new()`, and an ambient construction in an ordinary
// parent test reads the developer's real `init.lua` and materializes
// packages into their real data root: the exact exposure this suite
// exists to remove, committed by the suite itself. It lives in the
// re-exec'd positive control below, where the roots are controlled
// by construction.
}
/// **N** — the init phase is genuinely closed, not merely reported
/// closed.
///
/// A flag read is one bool; this asserts the *behaviour* the flag gates,
/// so a fix that sets the flag without the surrounding block having run
/// cannot pass. `pmacs.attach` is init-only and must now refuse.
#[test]
fn isolated_construction_closes_the_init_only_lua_surface() {
let state = EditorState::new_with_roots(&iso::roots());
let err = state
.lua_host
.lua()
.load(r#"pmacs.attach { target = "local:/run/pmacs/x.sock" }"#)
.exec()
.expect_err("pmacs.attach must refuse after init");
let text = err.to_string();
assert!(
text.contains("init"),
"the refusal must name the init phase; got {text}"
);
}
// ---------------------------------------------------------------------------
// Isolated construction redirects the writes (framing §1.6)
// ---------------------------------------------------------------------------
/// **N** — the bundled-package materialization lands in the redirected
/// data root.
///
/// Asserts content produced, not an invariant preserved: the package
/// tree has to actually exist under the isolated root. A test that only
/// checked "the real root was not modified" would pass on a machine
/// where the real root already held identical bytes, because
/// `write_if_changed` is content-gated.
#[test]
fn isolated_construction_materializes_into_the_redirected_data_root() {
let base = Path::new(env!("CARGO_TARGET_TMPDIR")).join("materialize-probe");
let _ = std::fs::remove_dir_all(&base);
let roots = BootstrapRoots::isolated_under(&base);
let dir = roots.bundled_runtime_dir().expect("redirected data root");
assert!(!dir.exists(), "the probe root must start absent");
let _state = EditorState::new_with_roots(&roots);
let manifest = dir.join("repl").join("pmacs.toml");
assert!(
manifest.is_file(),
"bundled packages must materialize under the redirected data \
root; {} is missing",
manifest.display()
);
let text = std::fs::read_to_string(&manifest).expect("read materialized manifest");
assert!(
text.contains("repl"),
"the materialized manifest must be the bundled package's own; got {text:?}"
);
}
/// **N** — `install_state_dirs` honours the redirected state root.
///
/// It runs *after* the constructor, so a constructor-only parameter
/// would leave it resolving `PMACS_STATE_HOME` / `XDG_STATE_HOME` from
/// the environment and hand an isolated session the developer's real
/// state dir.
#[test]
fn install_state_dirs_honours_the_redirected_state_root() {
let base = Path::new(env!("CARGO_TARGET_TMPDIR")).join("state-probe");
let roots = BootstrapRoots::isolated_under(&base);
let state = EditorState::new_with_roots(&roots);
state.install_state_dirs();
let dir = state
.lua_host
.lua()
.app_data_ref::<pmacs::lua_bindings::StateDir>()
.expect("install_state_dirs must configure a state dir")
.0
.clone();
assert_eq!(
dir,
roots.state_dir().unwrap(),
"state dir must be redirected"
);
let history = state.core.borrow().minibuffer.history_dir.clone();
assert_eq!(
history,
roots.history_dir(),
"minibuffer history must be redirected too"
);
}
// ---------------------------------------------------------------------------
// Hostile ambient environment (framing Bet 3, acceptance 7)
// ---------------------------------------------------------------------------
/// Names the isolated base for the isolated child; its presence is the
/// signal that this process *is* that child.
const ISOLATED_CHILD_BASE: &str = "PMACS_AMBIENT_ISOLATION_CHILD_BASE";
/// Presence marks the ambient positive-control child.
const AMBIENT_CONTROL_CHILD: &str = "PMACS_AMBIENT_ISOLATION_CONTROL";
/// An `init.lua` that leaves a mark an assertion can see. The real
/// developer `init.lua` that produced this lane broke
/// `compile_mode_acceptance` by redefining a command pmacs already
/// defines; a marker global is the same exposure with a cheaper failure
/// mode, and it discriminates read-vs-not-read directly.
const HOSTILE_INIT_LUA: &str = "_G.HOSTILE_INIT_RAN = true\n";
fn hostile_root(name: &str) -> PathBuf {
let root = Path::new(env!("CARGO_TARGET_TMPDIR")).join(name);
let _ = std::fs::remove_dir_all(&root);
let config = root.join("pmacs");
std::fs::create_dir_all(&config).expect("create hostile config dir");
std::fs::write(config.join("init.lua"), HOSTILE_INIT_LUA).expect("write hostile init.lua");
// Pre-seed the data root the ambient resolver would use, so a write
// into it is visible as a *change*, not just as a new tree.
let seeded = root
.join("pmacs")
.join("builtin-packages")
.join(format!("v{}", env!("CARGO_PKG_VERSION")));
std::fs::create_dir_all(&seeded).expect("seed hostile data root");
std::fs::write(seeded.join("SEED"), b"pre-seeded\n").expect("write seed marker");
root
}
/// Flat snapshot of a tree: relative path → contents (directories map to
/// the empty vector). Sorted, so comparison is order-independent.
fn snapshot(root: &Path) -> BTreeMap<PathBuf, Vec<u8>> {
fn walk(root: &Path, dir: &Path, out: &mut BTreeMap<PathBuf, Vec<u8>>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let rel = path
.strip_prefix(root)
.expect("entry is under root")
.to_path_buf();
if path.is_dir() {
out.insert(rel, Vec::new());
walk(root, &path, out);
} else {
out.insert(rel, std::fs::read(&path).unwrap_or_default());
}
}
}
let mut out = BTreeMap::new();
walk(root, root, &mut out);
out
}
/// The five storage variables plus `HOME`, all aimed at `root`. `HOME`
/// is included here — and only here — because this is the adversary: a
/// machine that leaves an XDG variable unset falls back to it, and the
/// point of a hostile environment is to leave no path out.
fn hostile_env(root: &Path) -> Vec<(&'static str, PathBuf)> {
vec![
("HOME", root.to_path_buf()),
("XDG_CONFIG_HOME", root.to_path_buf()),
("XDG_DATA_HOME", root.to_path_buf()),
("XDG_STATE_HOME", root.to_path_buf()),
("PMACS_STATE_HOME", root.to_path_buf()),
("XDG_CACHE_HOME", root.to_path_buf()),
]
}
fn run_child(test_name: &str, env: Vec<(&'static str, PathBuf)>) -> (bool, String) {
let exe = std::env::current_exe().expect("current test binary");
let output = Command::new(exe)
.args(["--exact", test_name, "--nocapture", "--test-threads=1"])
.envs(env)
.output()
.unwrap_or_else(|e| panic!("re-exec `{test_name}`: {e}"));
let mut log = String::new();
log.push_str("--- stdout ---\n");
log.push_str(&String::from_utf8_lossy(&output.stdout));
log.push_str("--- stderr ---\n");
log.push_str(&String::from_utf8_lossy(&output.stderr));
assert!(
log.contains("1 passed") || !output.status.success(),
"child `{test_name}` ran no test — the `--exact` filter went stale\n{log}"
);
(output.status.success(), log)
}
/// **Positive control.** Under the hostile environment, the *ambient*
/// constructor really is captured by it.
///
/// Without this the isolation assertion below is unfalsifiable: an
/// `init.lua` that never loads under any circumstances would satisfy
/// "the isolated editor did not load it" while proving nothing.
///
/// **This is the suite's only ambient construction, and it runs only as
/// a re-exec'd child** (the marker gates it), where the roots are
/// controlled by construction. An ambient `EditorState::new()` in an
/// ordinary parent test would read the developer's real `init.lua` and
/// write their real data root — so the one place that legitimately needs
/// the ambient constructor is also the one place where the environment
/// has already been redirected. Every ambient claim this suite makes
/// belongs here for that reason.
#[test]
fn ambient_construction_under_a_hostile_environment_is_captured_by_it() {
if std::env::var_os(AMBIENT_CONTROL_CHILD).is_none() {
return;
}
let state = EditorState::new();
// Relocated from `isolated_construction_is_init_complete`: the
// ambient constructor is unchanged by this lane and still finishes
// initialization. Asserting it needs an ambient construction, which
// is only safe here.
assert!(
state.lua_host.is_init_complete(),
"the ambient constructor must still leave the init phase — the \
roots parameter changes which directory is read, never whether \
the block runs"
);
let ran: bool = state
.lua_host
.lua()
.load("return _G.HOSTILE_INIT_RAN == true")
.eval()
.expect("read the hostile marker");
assert!(
ran,
"the ambient constructor must load the hostile init.lua — if it \
does not, the isolation assertion proves nothing"
);
// And the write half: the ambient constructor materializes into the
// hostile data root.
let dir = pmacs::builtin_packages::bundled_runtime_dir();
assert!(
dir.join("repl").join("pmacs.toml").is_file(),
"the ambient constructor must write into the hostile data root; \
{} is empty",
dir.display()
);
}
/// The isolated child: same hostile environment, redirected roots.
#[test]
fn isolated_construction_under_a_hostile_environment_ignores_it() {
let Some(base) = std::env::var_os(ISOLATED_CHILD_BASE) else {
return;
};
let base = PathBuf::from(base);
let roots = BootstrapRoots::isolated_under(&base);
let state = EditorState::new_with_roots(&roots);
let ran: bool = state
.lua_host
.lua()
.load("return _G.HOSTILE_INIT_RAN == true")
.eval()
.expect("read the hostile marker");
assert!(!ran, "the hostile init.lua must not have been loaded");
assert!(
state.lua_host.is_init_complete(),
"and initialization must still have finished"
);
// Content produced, in the right place.
let dir = roots.bundled_runtime_dir().expect("redirected data root");
assert!(
dir.join("repl").join("pmacs.toml").is_file(),
"bundled packages must land under the isolated root; {} is empty",
dir.display()
);
}
/// **N** — the whole of Bet 3: green under a hostile environment, and
/// the hostile root byte-identical afterwards.
///
/// Two children, because the two halves need opposite environments to be
/// meaningful: the positive control must be *captured* by its hostile
/// root (and so modifies it), while the isolated child must leave its
/// own hostile root untouched.
#[test]
fn a_hostile_ambient_environment_is_neither_read_nor_written() {
// Half 1 — the control. Its hostile root is expected to change.
let control = hostile_root("hostile-control");
let before_control = snapshot(&control);
let (ok, log) = run_child(
"ambient_construction_under_a_hostile_environment_is_captured_by_it",
{
let mut env = hostile_env(&control);
env.push((AMBIENT_CONTROL_CHILD, PathBuf::from("1")));
env
},
);
assert!(ok, "the ambient positive control must be captured\n{log}");
let after_control = snapshot(&control);
assert_ne!(
before_control, after_control,
"the control's hostile root must have been written into — if it \
was not, this environment is not hostile and the isolated half \
below asserts nothing"
);
// Half 2 — the isolated child. Its hostile root must be untouched.
let hostile = hostile_root("hostile-isolated");
let isolated_base = Path::new(env!("CARGO_TARGET_TMPDIR")).join("hostile-isolated-roots");
let _ = std::fs::remove_dir_all(&isolated_base);
let before = snapshot(&hostile);
assert!(!before.is_empty(), "the hostile root must not be empty");
let (ok, log) = run_child(
"isolated_construction_under_a_hostile_environment_ignores_it",
{
let mut env = hostile_env(&hostile);
env.push((ISOLATED_CHILD_BASE, isolated_base.clone()));
env
},
);
assert!(ok, "the isolated child must stay green\n{log}");
let after = snapshot(&hostile);
assert_eq!(
before, after,
"the hostile root must be byte-identical afterwards — a green \
suite that still wrote into it has not demonstrated isolation"
);
// And the writes went somewhere: the isolated tree exists.
assert!(
isolated_base.join("data").join("pmacs").is_dir(),
"the isolated data root must have been written instead"
);
}
// ---------------------------------------------------------------------------
// Adoption ratchet (framing acceptance 12)
// ---------------------------------------------------------------------------
/// Files permitted to construct an editor through the **ambient** entry
/// points, **with the exact number of sites each is permitted**.
///
/// `journey_acceptance` is ambient on purpose: it is the golden-journey
/// ratchet, and its whole claim is that the production entry point
/// `pmacs FILE` calls has a caller. It is isolated by re-execing itself
/// with controlled roots instead (framing §1.10). This file is ambient
/// only inside the positive control above, which never runs except as a
/// deliberately re-exec'd child.
///
/// **The count is the point, not decoration.** A bare file-level
/// exemption is the weakest form of this ratchet: it licenses the named
/// file to grow *new* ambient sites forever. That is not hypothetical —
/// review round 1 of this PR found an ambient `EditorState::new()` in an
/// ordinary parent test of **this very file**, and the file-level
/// exemption is precisely what let it through a green ratchet. Every
/// exemption is now a census entry, so an added site fails even inside
/// an allowlisted file, and a removed one has to be recorded.
const AMBIENT_ALLOWLIST: &[(&str, usize)] = &[
// 19 `new()` + 7 `open(` — the golden journey, every one of them
// reached only from inside a re-exec'd child. (29 textual
// occurrences; the scanner drops 2 assertion-message mentions and
// the assembled `concat!` needle in the self-source check.)
("journey_acceptance.rs", 26),
// Exactly one: the re-exec'd positive control.
("ambient_isolation_acceptance.rs", 1),
];
/// Strip comments and string-literal *contents* from Rust source, so a
/// scan counts calls rather than mentions.
///
/// Both matter here. `m5_6_acceptance.rs:94` names `EditorState::new` in
/// a comment only to say it deliberately does not call it, and
/// `journey_acceptance.rs:2030` carries it inside an assertion message.
/// Raw strings (`r#"..."#`) are pervasive in this suite, so they are
/// handled rather than hoped about.
fn strip_comments_and_strings(src: &str) -> String {
let b: Vec<char> = src.chars().collect();
let mut out = String::with_capacity(src.len());
let mut i = 0;
while i < b.len() {
// Line comment.
if b[i] == '/' && i + 1 < b.len() && b[i + 1] == '/' {
while i < b.len() && b[i] != '\n' {
i += 1;
}
continue;
}
// Block comment (Rust's nest).
if b[i] == '/' && i + 1 < b.len() && b[i + 1] == '*' {
let mut depth = 1;
i += 2;
while i < b.len() && depth > 0 {
if b[i] == '/' && i + 1 < b.len() && b[i + 1] == '*' {
depth += 1;
i += 2;
} else if b[i] == '*' && i + 1 < b.len() && b[i + 1] == '/' {
depth -= 1;
i += 2;
} else {
i += 1;
}
}
out.push(' ');
continue;
}
// Raw string: r, then any number of #, then ".
if b[i] == 'r' {
let mut j = i + 1;
let mut hashes = 0;
while j < b.len() && b[j] == '#' {
hashes += 1;
j += 1;
}
if j < b.len() && b[j] == '"' {
j += 1;
loop {
if j >= b.len() {
break;
}
if b[j] == '"' {
let mut k = j + 1;
let mut seen = 0;
while k < b.len() && b[k] == '#' && seen < hashes {
seen += 1;
k += 1;
}
if seen == hashes {
j = k;
break;
}
}
j += 1;
}
out.push(' ');
i = j;
continue;
}
}
// Ordinary string.
if b[i] == '"' {
i += 1;
while i < b.len() {
if b[i] == '\\' {
i += 2;
continue;
}
if b[i] == '"' {
i += 1;
break;
}
i += 1;
}
out.push(' ');
continue;
}
out.push(b[i]);
i += 1;
}
out
}
fn tests_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests")
}
/// Every `.rs` file under `tests/`, including `tests/common/`.
fn test_sources() -> Vec<(String, String)> {
let mut out = Vec::new();
let dir = tests_dir();
let push_dir = |d: &Path, out: &mut Vec<(String, String)>| {
for entry in std::fs::read_dir(d).expect("read tests dir").flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "rs") {
let name = path
.file_name()
.expect("file name")
.to_string_lossy()
.into_owned();
out.push((name, std::fs::read_to_string(&path).expect("read source")));
}
}
};
push_dir(&dir, &mut out);
push_dir(&dir.join("common"), &mut out);
out
}
/// **N** — a *durable* adoption ratchet, not a one-time census.
///
/// One self-spawning hostile-environment test proves the seam works; it
/// cannot notice a raw `EditorState::new()` added to a different binary
/// next month. This can. Falsified by adding an ambient constructor to
/// any non-allowlisted suite.
#[test]
fn no_test_outside_the_allowlist_constructs_an_ambient_editor() {
let sources = test_sources();
// A broken glob must not read as a clean tree.
assert!(
sources.len() > 90,
"expected the whole tests/ corpus; found only {} files",
sources.len()
);
let needles = [
concat!("EditorState::", "new()"),
concat!("EditorState::", "open("),
];
let mut offenders: Vec<String> = Vec::new();
let mut miscounted: Vec<String> = Vec::new();
let mut seen_allowlisted: Vec<&str> = Vec::new();
for (name, src) in &sources {
let code = strip_comments_and_strings(src);
let hits: usize = needles.iter().map(|n| code.matches(n).count()).sum();
if hits == 0 {
continue;
}
match AMBIENT_ALLOWLIST.iter().find(|(f, _)| *f == name.as_str()) {
Some((file, allowed)) => {
seen_allowlisted.push(file);
// An allowlisted file is exempted for the sites it was
// reviewed with, not for any it grows later.
if hits != *allowed {
miscounted.push(format!("{name}: {hits} site(s), allowlist says {allowed}"));
}
}
None => offenders.push(format!("{name} ({hits} site(s))")),
}
}
offenders.sort();
miscounted.sort();
assert!(
offenders.is_empty(),
"these suites construct an editor through the ambient entry \
points, so they read the developer's real init.lua and write \
into their real data root: {offenders:?}\n\
Use `EditorState::new_with_roots(&crate::iso::roots())` (see \
tests/common/iso.rs), or add the file to AMBIENT_ALLOWLIST with \
a reason.",
);
assert!(
miscounted.is_empty(),
"allowlisted files whose ambient site count moved: {miscounted:?}\n\
MORE than allowed means a new ambient construction slipped into \
an exempted file the failure mode a bare file-level exemption \
cannot see. FEWER means the census is stale; update the count.",
);
// Dead allowlist entries are how a ratchet rots: an entry that no
// longer needs to be there silently licenses a future regression.
let mut missing: Vec<&str> = AMBIENT_ALLOWLIST
.iter()
.map(|(f, _)| *f)
.filter(|f| !seen_allowlisted.contains(f))
.collect();
missing.sort_unstable();
assert!(
missing.is_empty(),
"allowlisted files that no longer construct an ambient editor — \
remove them: {missing:?}"
);
}
/// **N** — the ratchet's scanner is not fooled by prose.
///
/// A grep-shaped answer is what cost this lane a review round; the scan
/// above only ratchets if it distinguishes a call from a mention. Pinned
/// with the exact shapes the corpus actually contains.
#[test]
fn the_ratchet_scanner_counts_calls_not_mentions() {
let sample = r##"
//! `EditorState::new()` in a doc comment.
// `EditorState::new()` in a line comment.
/* `EditorState::new()` in a block comment. */
fn f() {
let msg = "EditorState::open(file) must not greet";
let raw = r#"EditorState::new() inside a raw string"#;
let _ = EditorState::new();
}
"##;
let code = strip_comments_and_strings(sample);
assert_eq!(
code.matches(concat!("EditorState::", "new()")).count(),
1,
"only the call survives; got {code:?}"
);
assert_eq!(
code.matches(concat!("EditorState::", "open(")).count(),
0,
"the assertion-message mention must not count; got {code:?}"
);
}
/// **N** — the seam actually reached the corpus.
///
/// The ratchet above is an absence check, and an absence check passes on
/// a tree where nobody constructs an editor at all. This asserts the
/// positive: the isolated constructor has broad adoption.
#[test]
fn the_isolated_constructor_has_been_adopted_across_the_corpus() {
let sources = test_sources();
let adopters: Vec<&String> = sources
.iter()
.filter(|(_, src)| {
let code = strip_comments_and_strings(src);
code.contains(concat!("EditorState::new", "_with_roots("))
|| code.contains(concat!("EditorState::open", "_with_roots("))
})
.map(|(name, _)| name)
.collect();
assert!(
adopters.len() >= 60,
"expected the whole in-process population to have migrated; only \
{} files did",
adopters.len()
);
}

View File

@ -74,7 +74,7 @@ fn status(s: &EditorState) -> String {
/// Fresh editor whose active scratch buffer holds `body`, cursor at 0.
fn editor_with(body: &str) -> EditorState {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
if !body.is_empty() {
exec(&s, &format!("pmacs.window.buffer():insert(0, {body:?})"));
}
@ -503,3 +503,10 @@ fn isearch_ret_accepts_instead_of_inserting() {
"RET during isearch accepts; no newline is inserted"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -29,7 +29,7 @@ fn fresh_state_dir() -> PathBuf {
}
fn editor(state_dir: &std::path::Path) -> EditorState {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
s.lua_host.lua().remove_app_data::<StateDir>();
s.lua_host
.lua()
@ -114,7 +114,7 @@ fn status(s: &EditorState) -> String {
/// Fresh scratch-buffer editor whose buffer holds `body`, cursor at 0.
/// No state dir / no files: scratch pairing uses the `default` set.
fn editor_with(body: &str) -> EditorState {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
if !body.is_empty() {
exec(&s, &format!("pmacs.window.buffer():insert(0, {body:?})"));
}
@ -1378,3 +1378,10 @@ fn relocated_closer_first_did_change_carries_the_complete_effective_text() {
text in the first didChange never an opener-only intermediate"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -24,7 +24,7 @@ fn fresh_state_dir() -> PathBuf {
}
fn editor(state_dir: &std::path::Path) -> EditorState {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
s.lua_host.lua().remove_app_data::<StateDir>();
s.lua_host
.lua()
@ -861,3 +861,10 @@ fn before_quit_sweeps_synchronously_without_vetoing() {
);
std::fs::remove_dir_all(&dir).ok();
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -39,7 +39,7 @@ const COLS: u32 = 60;
const AREA_ROWS: u32 = ROWS - 1;
fn editor() -> EditorState {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
exec(&s, "pmacs.lsp.config = {}");
// Geometry is authoritative state, and a grid frontend's real frame
// size IS its declaration. Every test that does not render declares
@ -2571,7 +2571,7 @@ fn panel_hidden_never_describes_a_panel_that_no_longer_exists() {
#[test]
fn unknown_geometry_is_not_twenty_four_by_eighty() {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
exec(&s, "pmacs.lsp.config = {}");
let fid = FrontendId(77);
attach_frontend(&s, fid, false);
@ -2619,3 +2619,10 @@ fn cell_coord_helper_is_used() {
// Keeps the CellCoord import honest for grid assertions above.
assert_eq!(CellCoord::new(1, 2).row, 1);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -22,7 +22,7 @@ const ROWS: u32 = 24;
const COLS: u32 = 60;
fn editor() -> EditorState {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
exec(&s, "pmacs.lsp.config = {}");
s.sync_frame_geometry(FrontendId::LOCAL, CellSize::new(ROWS, COLS));
s
@ -896,3 +896,10 @@ fn a_provider_closing_the_document_split_still_clears_the_statusline() {
the phase-1 document identity, even when a callback closed that window; got {msgs:?}"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -56,7 +56,7 @@ impl Session {
/// A semantic, panel-capable frontend with one document window and
/// no geometry declared yet.
fn new() -> Self {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
exec(&state, "pmacs.lsp.config = {}");
let document = {
let mut core = state.core.borrow_mut();
@ -659,7 +659,7 @@ fn a2b1_a_rejected_declaration_reconciles_nothing() {
fn a2b1_grid_allocator_exhaustion_clears_the_declaration_and_hides() {
// The grid/LOCAL allocator, which mints its own epochs. `LOCAL` is
// panel-capable, so this is the production path for a TUI.
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
exec(&state, "pmacs.lsp.config = {}");
state.sync_frame_geometry(FrontendId::LOCAL, CellSize::new(ROWS, COLS));
exec(
@ -1340,3 +1340,10 @@ fn sweep_a_panel_wider_than_the_terminal_cap_still_presents_its_terminal() {
);
exec(&session.state, "pmacs.terminal.terminate(TERM_BUF)");
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -25,7 +25,7 @@ fn fresh_state_dir() -> PathBuf {
}
fn editor(state_dir: &std::path::Path) -> EditorState {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
s.lua_host.lua().remove_app_data::<StateDir>();
s.lua_host
.lua()
@ -366,3 +366,10 @@ fn toggle_between_kills_breaks_the_kill_chain() {
"C-k, M-;, C-k yields two ring entries (chain broken)"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -159,9 +159,31 @@ pub fn spawn_daemon_process_with_env(socket_path: &Path, env_vars: &[(&str, &str
cmd.args(["--daemon", "--socket"])
.arg(socket_path)
.env("HOME", isolated_home)
.env("XDG_CONFIG_HOME", isolated_home)
.stdout(Stdio::null())
.stderr(Stdio::from(stderr));
// All FIVE storage variables, not just `XDG_CONFIG_HOME`
// (`docs/test-ambient-config-isolation-framing.md` §1.6a).
//
// `HOME` above is only a *fallback*: it isolates a root only while
// the corresponding `XDG_*` variable is unset. On a developer
// machine that exports `XDG_DATA_HOME`, a daemon given `HOME` alone
// still materializes bundled packages into the real data root — so
// the harness's apparent adequacy was a property of one machine's
// environment, not of the harness. And `PMACS_STATE_HOME` outranks
// `XDG_STATE_HOME`, so the four XDG variables alone leave a
// higher-precedence state override live.
//
// Flat layout (every root at the socket's tempdir) so
// `spawn_with_config`'s `<tempdir>/pmacs/init.lua` keeps loading
// through the real `load_user_config` path.
let roots = pmacs::bootstrap::BootstrapRoots::ambient()
.with_config_root(isolated_home.to_path_buf())
.with_data_root(isolated_home.to_path_buf())
.with_state_root(isolated_home.to_path_buf())
.with_cache_root(isolated_home.to_path_buf());
for (key, value) in roots.child_env() {
cmd.env(key, value);
}
for (key, value) in env_vars {
cmd.env(key, value);
}

68
tests/common/iso.rs Normal file
View File

@ -0,0 +1,68 @@
// tests/common/iso.rs --- isolated bootstrap roots for integration tests.
//! The integration suite's side of `pmacs::bootstrap`.
//!
//! An integration test links `pmacs` as an ordinary dependency, so it is
//! compiled **without** `cfg(test)` and the `#[cfg(not(test))]` guard in
//! `EditorState::new` is live for it: a raw `EditorState::new()` reads
//! the developer's real `~/.config/pmacs/init.lua` and writes bundled
//! packages into the developer's real `~/.local/share/pmacs`. It cannot
//! fix that by setting an environment variable --- `std::env::set_var`
//! is `unsafe` and `pmacs` is `#![forbid(unsafe_code)]`. So it passes
//! [`roots`] to `EditorState::new_with_roots` instead.
//!
//! See `docs/test-ambient-config-isolation-framing.md`.
//!
//! Included per-file rather than through `mod common;` so a suite that
//! needs no daemon or PTY fixture does not compile them:
//!
//! ```ignore
//! #[path = "common/iso.rs"]
//! mod iso;
//! ```
//!
//! [`roots`] is a **pure function of the build environment** --- no
//! counter, no `OnceLock`. Two copies of this module in one test binary
//! (one via `mod common;`, one via `#[path]`) therefore return the same
//! value rather than racing over a shared counter.
#![allow(dead_code)] // not every including suite uses every helper
use std::path::PathBuf;
use pmacs::bootstrap::BootstrapRoots;
/// The shared isolated base, under Cargo's per-package integration-test
/// temp directory.
///
/// `CARGO_TARGET_TMPDIR` (`target/<profile>/tmp`) rather than `/tmp`:
/// nothing here is ever unlinked --- there is no libtest teardown hook
/// to unlink it from --- so the tree has to live somewhere `cargo clean`
/// owns instead of leaking into the system temp dir once per run.
///
/// Deliberately shared across tests and across test binaries.
/// `materialize_all` is content-gated and idempotent, so after the first
/// construction every later one is a no-op read; a per-test directory
/// would repeat the whole materialization ~330 times per run for no
/// isolation gain (the tree is byte-identical for every caller).
#[must_use]
pub fn base() -> PathBuf {
PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("ambient-isolation")
}
/// Isolated bootstrap roots for an in-process editor.
///
/// Pass to `EditorState::new_with_roots` / `open_with_roots`.
#[must_use]
pub fn roots() -> BootstrapRoots {
let base = base();
let roots = BootstrapRoots::isolated_under(&base);
// Create the config dir eagerly. `load_user_config_at` registers it
// on Lua's `package.path` whether or not `init.lua` exists, and a
// suite that later writes a config chunk into it should not have to
// know the layout.
if let Some(dir) = roots.config_dir() {
std::fs::create_dir_all(&dir).expect("create isolated config dir");
}
roots
}

View File

@ -14,8 +14,14 @@
//! - [`daemon`]: `pmacs --daemon` subprocess fixture. First
//! consumer M5.5 acceptance suite; second consumer M10.11
//! doubled-PTY tests.
//! - [`iso`]: isolated bootstrap storage roots, so an in-process
//! editor neither reads the developer's real `init.lua` nor writes
//! into their real data root. Most suites include it directly via
//! `#[path = "common/iso.rs"] mod iso;` rather than through this
//! module; it is re-exported here for `daemon`'s use.
#![allow(dead_code)] // not every integration-test file uses every helper
pub mod daemon;
pub mod iso;
pub mod pty;

View File

@ -112,6 +112,21 @@ pub fn spawn_pmacs_in_pty(args: &[&str], envs: &[(&str, &Path)], rows: u16, cols
for arg in args {
cmd.arg(arg);
}
// Backfill any of the five storage variables the caller did not set
// (`docs/test-ambient-config-isolation-framing.md` §1.6a). Callers
// pin the roots their assertions read (a per-test `XDG_CONFIG_HOME`,
// a `PMACS_STATE_HOME` they inspect); the rest would otherwise be
// inherited, and the child would materialize bundled packages into
// the developer's real data root. Caller entries win: this only
// fills holes.
let fallback = pmacs::bootstrap::BootstrapRoots::isolated_under(&super::iso::base());
for (key, value) in fallback.child_env() {
if envs.iter().any(|(k, _)| *k == key) {
continue;
}
std::fs::create_dir_all(&value).expect("create isolated root for PTY child");
cmd.env(key, value.as_os_str());
}
for (k, v) in envs {
let mut value = OsString::new();
value.push(v);

View File

@ -82,7 +82,7 @@ fn errors_buffer(s: &EditorState) -> String {
/// Fresh editor with LSP spawning disabled (language detection still
/// works; the after-load hook must not exec real servers).
fn editor() -> EditorState {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
exec(&s, "pmacs.lsp.config = {}");
s
}
@ -2853,3 +2853,10 @@ fn j1b1_context_reports_the_cwd_a_run_would_use() {
);
assert_eq!(kind, "rust", "the kind is detected from the given cwd");
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -52,7 +52,7 @@ fn probe(s: &EditorState) -> (String, bool, i64) {
/// the prefix is replaced by the candidate in one step.
#[test]
fn typing_opens_popup_and_tab_accepts() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "hello_world ");
let (_, visible, _) = probe(&s);
assert!(
@ -95,7 +95,7 @@ fn typing_opens_popup_and_tab_accepts() {
/// candidate wins. Uses a custom provider for a deterministic order.
#[test]
fn ctrl_n_navigates_then_ret_accepts_second_candidate() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
s.lua_host
.lua()
.load(
@ -133,7 +133,7 @@ fn ctrl_n_navigates_then_ret_accepts_second_candidate() {
/// key self-inserts normally (the shadow is partial, not modal).
#[test]
fn esc_dismisses_and_typing_falls_through() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "hello_world he");
let (_, visible, _) = probe(&s);
assert!(visible);
@ -154,7 +154,7 @@ fn esc_dismisses_and_typing_falls_through() {
/// anchor.
#[test]
fn motion_before_anchor_closes_popup() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "hello_world he");
let (_, visible, _) = probe(&s);
assert!(visible);
@ -169,7 +169,7 @@ fn motion_before_anchor_closes_popup() {
/// the Q#C9 single-char signature rejects paste-shaped deltas.
#[test]
fn yank_shaped_edit_does_not_auto_open() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "hello_world hello");
// Select the trailing word and cut it (C-w): the popup that was
// open over `hello` closes as its word dies.
@ -198,7 +198,7 @@ fn yank_shaped_edit_does_not_auto_open() {
/// auto-open prefix threshold.
#[test]
fn at_point_command_opens_below_threshold() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "hello_world h");
let (_, visible, _) = probe(&s);
assert!(!visible, "a 1-char prefix stays below the auto-open bar");
@ -223,7 +223,7 @@ fn at_point_command_opens_below_threshold() {
/// `C-g` abort) reaches the dispatcher instead of the popup shadow.
#[test]
fn pending_prefix_dismisses_popup_and_keeps_dispatcher_control() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "hello_world he");
let (_, visible, _) = probe(&s);
assert!(visible);
@ -258,7 +258,7 @@ fn pending_prefix_dismisses_popup_and_keeps_dispatcher_control() {
/// index sweep meant the server was never asked).
#[test]
fn empty_sync_sweep_still_leaves_a_pending_session() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
// A buffer whose only word is the one being typed: dabbrev is
// structurally empty, no LSP attached. The popup cannot open...
type_str(&mut s, "qz");
@ -282,7 +282,7 @@ fn empty_sync_sweep_still_leaves_a_pending_session() {
/// because the real LSP store is Rust-side.
#[test]
fn collect_scopes_lsp_candidates_to_ctx_uri() {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
let (scoped, unscoped): (u64, u64) = s
.lua_host
.lua()
@ -322,3 +322,10 @@ fn collect_scopes_lsp_candidates_to_ctx_uri() {
assert_eq!(scoped, 1, "ctx.uri reaches Lua providers (9th arg)");
assert_eq!(unscoped, 2, "no uri → all documents");
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -47,7 +47,7 @@ fn fresh_state_dir() -> PathBuf {
}
fn editor(state_dir: &std::path::Path) -> EditorState {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
s.lua_host.lua().remove_app_data::<StateDir>();
s.lua_host
.lua()
@ -507,3 +507,10 @@ fn describe_setting_shows_a_buffer_local_override_when_one_exists() {
"an existing buffer-local override must be reported, got {text:?}"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -50,7 +50,7 @@ fn probe(s: &EditorState) -> (String, bool, i64) {
#[test]
fn backspace_deletes_the_shift_selected_region() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "hello");
// Shift+Left three times: region [2, 5), cursor at 2.
@ -85,7 +85,7 @@ fn backspace_deletes_the_shift_selected_region() {
/// chorded deletion keys to this same dispatch path.
#[test]
fn ctrl_backspace_deletes_the_previous_word() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "alpha beta");
s.dispatch_key(
@ -111,7 +111,7 @@ fn ctrl_backspace_deletes_the_previous_word() {
#[test]
fn typing_replaces_the_shift_selected_region() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "hello");
// Select "llo" (region [2, 5), cursor at 2), then type 'X':
@ -149,7 +149,7 @@ fn typing_replaces_the_shift_selected_region() {
#[test]
fn type_over_is_a_single_undo_step() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "hello");
// Select "llo" (region [2, 5)) and type 'X' → "heX".
@ -177,7 +177,7 @@ fn type_over_is_a_single_undo_step() {
#[test]
fn delete_forward_deletes_the_shift_selected_region() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "world");
// Shift+Home-equivalent: extend left over the whole word.
@ -200,3 +200,10 @@ fn delete_forward_deletes_the_shift_selected_region() {
assert_eq!(text, "b", "no region ⇒ plain forward delete at cursor");
assert_eq!(cursor, 0);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -32,7 +32,7 @@ fn fresh_state_dir() -> PathBuf {
}
fn editor(state_dir: &std::path::Path) -> EditorState {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
s.lua_host.lua().remove_app_data::<StateDir>();
s.lua_host
.lua()
@ -428,3 +428,10 @@ fn startup_gate_respects_file_arg_and_arming() {
);
std::fs::remove_dir_all(&dir).ok();
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -116,7 +116,7 @@ fn eval<T: mlua::FromLuaMulti>(s: &EditorState, src: &str) -> T {
/// frame size *is* its geometry declaration, and the panel tests need
/// one before any side window can be placed).
fn editor() -> EditorState {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
exec(&s, "pmacs.lsp.config = {}");
s.sync_frame_geometry(FrontendId::LOCAL, CellSize::new(ROWS, COLS));
s
@ -1997,3 +1997,10 @@ fn dired_the_fold_refusal_names_the_read_only_lock() {
"and not the sentence that is no longer true; got {st:?}"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -114,7 +114,7 @@ fn status(s: &EditorState) -> String {
/// typing RET would route through edit.newline-and-indent and clone
/// leading whitespace), cursor at 0.
fn editor_with(text: &str) -> EditorState {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
exec(
&s,
&format!(
@ -1155,7 +1155,7 @@ fn temp_path(name: &str) -> std::path::PathBuf {
fn trim_on_save_defaults_off_and_writes_untouched_bytes() {
let path = temp_path("off.txt");
std::fs::write(&path, "x \ny\t\n").unwrap();
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
let on: bool = eval(&s, "return pmacs.editops.trim_on_save()");
assert!(!on, "default off");
exec(
@ -1175,7 +1175,7 @@ fn trim_on_save_defaults_off_and_writes_untouched_bytes() {
fn trim_on_save_trims_the_written_bytes_before_later_callbacks() {
let path = temp_path("on.txt");
std::fs::write(&path, "x \ny\t\n").unwrap();
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
exec(&s, "pmacs.editops.trim_on_save(true)");
// A callback registered AFTER editops' (load-time) hook observes
// the fan-out order saveplace sees: post-trim text.
@ -1215,7 +1215,7 @@ fn trim_on_save_trims_the_written_bytes_before_later_callbacks() {
fn trim_on_save_failure_never_vetoes_the_save() {
let path = temp_path("veto-immune.txt");
std::fs::write(&path, "x \n").unwrap();
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
exec(&s, "pmacs.editops.trim_on_save(true)");
exec(
&s,
@ -1246,7 +1246,7 @@ fn trim_on_save_failure_never_vetoes_the_save() {
fn trim_on_save_unexpected_error_reports_and_still_saves() {
let path = temp_path("unexpected.txt");
std::fs::write(&path, "x \n").unwrap();
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
exec(&s, "pmacs.editops.trim_on_save(true)");
// Capture the pmacs.error log (the m9_6 stub pattern — the
// `if pmacs.error` branch is a no-op without it).
@ -1293,7 +1293,7 @@ fn trim_on_save_unexpected_error_reports_and_still_saves() {
fn another_callbacks_veto_is_not_masked_by_trim() {
let path = temp_path("veto.txt");
std::fs::write(&path, "x \n").unwrap();
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
exec(&s, "pmacs.editops.trim_on_save(true)");
exec(
&s,
@ -1318,3 +1318,10 @@ fn another_callbacks_veto_is_not_masked_by_trim() {
);
let _ = std::fs::remove_file(&path);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -90,7 +90,7 @@ fn status(s: &EditorState) -> String {
fn editor_in(dir: &std::path::Path) -> EditorState {
let anchor = dir.join("anchor.txt");
std::fs::write(&anchor, b"anchor\n").expect("write anchor");
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
state.lua_host.reopen_init_phase_for_testing();
let anchor_str = anchor.display().to_string();
exec(
@ -279,7 +279,7 @@ fn find_file_accepting_a_directory_reports_instead_of_raising() {
/// stable, real candidate there.
#[test]
fn find_file_without_a_backing_path_roots_at_the_process_cwd() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
s.lua_host.reopen_init_phase_for_testing();
assert!(
active_path(&s).is_none(),
@ -352,7 +352,7 @@ fn find_file_expands_a_leading_tilde() {
return;
}
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
s.lua_host.reopen_init_phase_for_testing();
open_prompt(&mut s);
// Contains a '/', so the typed text reaches on_accept verbatim.
@ -371,3 +371,10 @@ fn find_file_expands_a_leading_tilde() {
"the expansion must use $HOME; got {path} with HOME={home}"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -301,7 +301,7 @@ fn insert_into(s: &EditorState, id: BufferId, text: &str) {
#[test]
fn command_path_self_insert_unfolds_at_point() {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
let id = active_id(&s);
insert_into(&s, id, "line0\nline1\nline2\nline3\n");
// Fold the interior of lines 1..2: [end of line0, end of line2].
@ -330,7 +330,7 @@ fn self_insert_at_head_line_end_does_not_unfold() {
// `(start, end]` containment: a self-insert exactly at the end of the
// head line (== range.start) is outside the fold — it must NOT unfold,
// and the translator shifts the fold right so the char lands visible.
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
let id = active_id(&s);
insert_into(&s, id, "line0\nline1\nline2\nline3\n");
let store = {
@ -389,7 +389,7 @@ fn install_rust_parse(s: &EditorState, id: BufferId) {
#[test]
fn folding_moves_point_to_head_line() {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
let id = active_id(&s);
let src = "fn foo() {\n let x = 1;\n let y = 2;\n}\n";
insert_into(&s, id, src);
@ -413,7 +413,7 @@ fn folding_moves_point_to_head_line() {
#[test]
fn data_api_validation() {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
// A plain document buffer with four lines.
exec(
&s,
@ -465,7 +465,7 @@ fn data_api_validation() {
#[test]
fn forget_drops_store_and_detaches_view() {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
let id = active_id(&s);
insert_into(&s, id, "line0\nline1\nline2\n");
let store = {
@ -500,7 +500,7 @@ fn killing_a_buffer_through_the_real_path_purges_its_fold_store() {
// assertion reads through the DEAD id on purpose: BufferIds are never
// reused, so a stale id cannot alias a later buffer. Mirrors
// config_registry's `killing_a_buffer_through_the_real_path_...`.
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
exec(
&s,
"b = pmacs.buffer.from_bytes('kill.rs', 'aaa\\nbbb\\nccc\\nddd\\n')",
@ -530,7 +530,7 @@ fn close_all_command_moves_point_to_enclosing_head() {
// Q#FD3 through the command surface: `fold.close-all` is interactive,
// so when it collapses a top-level fold around the invoking point, the
// point moves to that fold's head line (Finding 3, round 1).
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
let id = active_id(&s);
let src = "fn first() {\n a();\n b();\n}\nfn second() {\n c();\n d();\n}\n";
insert_into(&s, id, src);
@ -553,7 +553,7 @@ fn close_all_command_moves_point_to_enclosing_head() {
fn stale_parse_tree_refuses_fold() {
// Q#FD10: an edit after the parse leaves `pending_edit_count() > 0`,
// so a fold command refuses (the settled coordinates are stale).
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
let id = active_id(&s);
let src = "fn foo() {\n let x = 1;\n let y = 2;\n}\n";
insert_into(&s, id, src);
@ -571,7 +571,7 @@ fn stale_parse_tree_refuses_fold() {
fn read_only_buffer_is_rejected() {
// Q#FD11's "normal document buffer" guard: terminals are read-only, so
// a read-only buffer is not foldable.
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
exec(
&s,
"b = pmacs.buffer.from_bytes('ro.rs', 'aaa\\nbbb\\nccc\\n')",
@ -592,7 +592,7 @@ fn read_only_buffer_is_rejected() {
#[test]
fn unfold_normalizes_an_arbitrary_range_to_a_stored_fold() {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
exec(
&s,
"b = pmacs.buffer.from_bytes('doc.rs', 'aaa\\nbbb\\nccc\\nddd\\n')",
@ -608,3 +608,10 @@ fn unfold_normalizes_an_arbitrary_range_to_a_stored_fold() {
let n: i64 = eval(&s, "return #pmacs.fold.folds(b)");
assert_eq!(n, 0);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -61,7 +61,7 @@ fn end_of(line: usize) -> u64 {
}
fn editor() -> EditorState {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
exec(&s, "pmacs.lsp.config = {}");
s
}
@ -1772,3 +1772,10 @@ fn wheel_over_an_inactive_unfolded_pane_uses_that_windows_map() {
"the folded pane did not scroll"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -18,7 +18,8 @@ use pmacs::editor::EditorState;
use pmacs::protocol::{ByteRange, FrontendId, InstanceMessage};
use pmacs::semantic_render::SemanticRenderState;
#[cfg(feature = "crdt")]
// Ungated: `common::iso` (isolated bootstrap roots) is needed in every
// build, not only the CRDT one.
mod common;
// ---------------------------------------------------------------------------
@ -43,7 +44,7 @@ fn eval<T: mlua::FromLuaMulti>(s: &EditorState, src: &str) -> T {
/// Fresh editor with LSP spawning disabled.
fn editor() -> EditorState {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
exec(&s, "pmacs.lsp.config = {}");
s
}
@ -490,3 +491,11 @@ fn preference_set_before_attach_ships_on_the_first_frame() {
"the first frame ships the pre-attach preference"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root. Re-exported rather than re-declared
// with `#[path]` — this file already pulls in `common`, and loading one
// source file as two modules is `clippy::duplicate_mod`.
use common::iso;

View File

@ -38,6 +38,11 @@ fn non_crdt_root_rejects_gpu_before_socket_io_discovery_or_spawn() {
.arg("--gpu")
.env(TEST_GPU_OVERRIDE, &fake_gpu)
.env("PMACS_TEST_MARKER", &marker)
.env("XDG_CONFIG_HOME", temp.path())
.env("XDG_DATA_HOME", temp.path())
.env("XDG_STATE_HOME", temp.path())
.env("PMACS_STATE_HOME", temp.path())
.env("XDG_CACHE_HOME", temp.path())
.env("XDG_RUNTIME_DIR", &runtime)
.output()
.expect("run non-CRDT pmacs --gpu");
@ -61,6 +66,11 @@ fn non_crdt_root_rejects_gpu_before_socket_io_discovery_or_spawn() {
.arg(&occupied_socket)
.env(TEST_GPU_OVERRIDE, &fake_gpu)
.env("PMACS_TEST_MARKER", &marker)
.env("XDG_CONFIG_HOME", temp.path())
.env("XDG_DATA_HOME", temp.path())
.env("XDG_STATE_HOME", temp.path())
.env("PMACS_STATE_HOME", temp.path())
.env("XDG_CACHE_HOME", temp.path())
.output()
.expect("run non-CRDT pmacs --gpu against occupied socket");
assert!(!occupied.status.success());

View File

@ -35,7 +35,7 @@ fn pump_async<F: Fn(&EditorState) -> bool>(state: &mut EditorState, predicate: F
/// resolver would leave the Lua write-through + snapshot bridge unproven.
#[test]
fn lua_alias_override_resolves_on_async_parse() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
// Add a bespoke fence alias from Lua (write-through to the registry).
state
@ -98,7 +98,7 @@ fn lua_alias_override_resolves_on_async_parse() {
/// alias discriminates the fix: with the empty map it would not resolve.
#[test]
fn sync_parse_now_resolves_alias() {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
let src = b"# Doc\n\n```py\nx = 1\n```\n";
let buf_id = state
.lua_host
@ -136,7 +136,7 @@ fn sync_parse_now_resolves_alias() {
/// the file drops below the cap and exceeds it again.
#[test]
fn injection_cap_surfaced_once_and_rearms_via_lua() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
// Capture pmacs.error messages into a Lua global.
state
.lua_host
@ -305,3 +305,10 @@ fn many_paragraph_settle_under_budget_with_tail_covered() {
"the final paragraph still receives an inline layer (no tail loss)"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -209,6 +209,85 @@ fn launch(path: &Path) -> EditorState {
s
}
// ---------------------------------------------------------------------------
// Ambient-roots isolation
// ---------------------------------------------------------------------------
//
// This suite is the one place that must NOT take the
// `EditorState::new_with_roots` / `open_with_roots` seam
// (`docs/test-ambient-config-isolation-framing.md` §1.7). Its whole job
// is to prove the production entry point is wired: an `open` arm with no
// production caller passes every direct-call test, so the ratchet drives
// the same `EditorState::open` that `pmacs FILE` does — parameterless,
// ambient, exactly as production calls it.
//
// That leaves nothing in the process able to isolate it. Cargo launches
// each integration-test binary with the caller's environment, and a
// binary cannot re-point its own roots before its tests run:
// `std::env::set_var` is `unsafe` and pmacs is
// `#![forbid(unsafe_code)]`. "Isolated by the environment it is launched
// with" is not a mechanism — it is a hope that whoever typed `cargo
// test` wrapped it, which is the external workaround this lane exists to
// delete.
//
// So each test is a thin **parent** that re-execs this same binary for
// its own test name, with controlled roots in the child's environment.
// The child sees the marker, runs the real body, and calls the ambient
// entry point — production's call, against a controlled tree.
/// Marker the parent sets on the child. Its presence, not its value, is
/// the signal.
const ISOLATED_CHILD: &str = "PMACS_JOURNEY_ISOLATED_CHILD";
/// Re-exec this test binary for `name` under controlled bootstrap
/// storage roots.
///
/// Returns `true` in the **parent** — the child has already run and been
/// asserted, so the caller must return without doing anything itself —
/// and `false` in the **child**, whose job is to run the body.
#[must_use]
fn reexec_isolated(name: &str) -> bool {
if std::env::var_os(ISOLATED_CHILD).is_some() {
return false;
}
// Under `target/`, not `/tmp`: nothing unlinks this (libtest has no
// teardown hook), so it belongs somewhere `cargo clean` owns.
let base = Path::new(env!("CARGO_TARGET_TMPDIR"))
.join("journey-isolation")
.join(name);
let _ = std::fs::remove_dir_all(&base);
let roots = pmacs::bootstrap::BootstrapRoots::isolated_under(&base);
let child_env = roots.child_env();
for (_, dir) in &child_env {
std::fs::create_dir_all(dir).expect("create controlled root");
}
let exe = std::env::current_exe().expect("current test binary");
let output = std::process::Command::new(exe)
.args(["--exact", name, "--nocapture", "--test-threads=1"])
.env(ISOLATED_CHILD, "1")
.envs(child_env)
.output()
.unwrap_or_else(|e| panic!("re-exec `{name}`: {e}"));
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"isolated child for `{name}` failed ({})\n--- child stdout ---\n{stdout}\
\n--- child stderr ---\n{stderr}",
output.status
);
// A filter that matches nothing exits 0. Without this the parent
// would pass while running no test at all — the failure mode a
// renamed test produces, and the one that would quietly hollow out
// the whole ratchet.
assert!(
stdout.contains("1 passed"),
"isolated child for `{name}` ran no test — the `--exact` filter went \
stale against the function name\n--- child stdout ---\n{stdout}"
);
true
}
// ---------------------------------------------------------------------------
// Step 2 — launch unconfigured
// ---------------------------------------------------------------------------
@ -216,6 +295,9 @@ fn launch(path: &Path) -> EditorState {
/// **N** — the editor starts with no configuration and no arguments.
#[test]
fn journey_step2_launches_unconfigured_into_scratch() {
if reexec_isolated("journey_step2_launches_unconfigured_into_scratch") {
return;
}
let s = EditorState::new();
assert_eq!(active_name(&s), "*scratch*");
assert!(
@ -236,6 +318,9 @@ fn journey_step2_launches_unconfigured_into_scratch() {
/// `Err(EISDIR)` and `main` exited 1.
#[test]
fn journey_step3_opening_a_directory_lists_it() {
if reexec_isolated("journey_step3_opening_a_directory_lists_it") {
return;
}
let td = project();
let s = launch(td.path());
@ -259,6 +344,9 @@ fn journey_step3_opening_a_directory_lists_it() {
/// the assertion above while `pmacs .` still printed a diagnostic.
#[test]
fn journey_step3_directory_startup_reports_no_error() {
if reexec_isolated("journey_step3_directory_startup_reports_no_error") {
return;
}
let td = project();
let s = launch(td.path());
assert!(
@ -274,6 +362,9 @@ fn journey_step3_directory_startup_reports_no_error() {
#[test]
fn journey_step3_unreadable_directory_reports_without_failing_startup() {
use std::os::unix::fs::PermissionsExt;
if reexec_isolated("journey_step3_unreadable_directory_reports_without_failing_startup") {
return;
}
let td = tempfile::tempdir().expect("tempdir");
let locked = td.path().join("locked");
std::fs::create_dir(&locked).expect("mkdir");
@ -301,6 +392,9 @@ fn journey_step3_unreadable_directory_reports_without_failing_startup() {
/// because no buffer is created and `set_buffer_path` never runs.
#[test]
fn journey_directory_resolver_receives_a_canonical_path() {
if reexec_isolated("journey_directory_resolver_receives_a_canonical_path") {
return;
}
let td = project();
let mut s = EditorState::new();
exec(&s, "pmacs.lsp.config = {}");
@ -331,6 +425,9 @@ fn journey_directory_resolver_receives_a_canonical_path() {
/// fallback is a clearable slot rather than a builtin hook subscription.
#[test]
fn journey_unclaimed_directory_starts_successfully_with_a_status() {
if reexec_isolated("journey_unclaimed_directory_starts_successfully_with_a_status") {
return;
}
let td = project();
let mut s = EditorState::new();
exec(&s, "pmacs.lsp.config = {}");
@ -360,6 +457,9 @@ fn journey_unclaimed_directory_starts_successfully_with_a_status() {
/// a claim suppresses the fallback.
#[test]
fn journey_resolver_chain_is_first_claimant_wins() {
if reexec_isolated("journey_resolver_chain_is_first_claimant_wins") {
return;
}
let td = project();
let mut s = EditorState::new();
exec(&s, "pmacs.lsp.config = {}");
@ -395,6 +495,9 @@ fn journey_resolver_chain_is_first_claimant_wins() {
/// `proceed == false`.
#[test]
fn journey_a_raising_resolver_suppresses_the_fallback_and_reports() {
if reexec_isolated("journey_a_raising_resolver_suppresses_the_fallback_and_reports") {
return;
}
let td = project();
let mut s = EditorState::new();
exec(&s, "pmacs.lsp.config = {}");
@ -458,6 +561,9 @@ const COMPETITOR: FrontendId = FrontendId(7);
/// ambient display: the file then appears in the competitor's window.
#[test]
fn commit_to_delivers_to_the_requesting_frontend_not_the_ambient_one() {
if reexec_isolated("commit_to_delivers_to_the_requesting_frontend_not_the_ambient_one") {
return;
}
let td = project();
let mut s = EditorState::new();
exec(&s, "pmacs.lsp.config = {}");
@ -519,6 +625,9 @@ fn commit_to_delivers_to_the_requesting_frontend_not_the_ambient_one() {
/// `acting_frontend`, or by reordering it after the interactive origin.
#[test]
fn commit_to_outranks_an_interactive_origin() {
if reexec_isolated("commit_to_outranks_an_interactive_origin") {
return;
}
let td = project();
let mut s = EditorState::new();
exec(&s, "pmacs.lsp.config = {}");
@ -586,6 +695,9 @@ fn commit_to_outranks_an_interactive_origin() {
/// `display`, or by reading `prev` from the ambient window.
#[test]
fn a_background_open_uses_the_captured_window_not_the_selected_one() {
if reexec_isolated("a_background_open_uses_the_captured_window_not_the_selected_one") {
return;
}
let td = project();
let mut s = EditorState::new();
exec(&s, "pmacs.lsp.config = {}");
@ -652,6 +764,9 @@ fn a_background_open_uses_the_captured_window_not_the_selected_one() {
/// competitor and the assertion fails from the other direction).
#[test]
fn commit_to_scopes_and_restores_on_a_normal_return() {
if reexec_isolated("commit_to_scopes_and_restores_on_a_normal_return") {
return;
}
let td = project();
let mut s = EditorState::new();
exec(&s, "pmacs.lsp.config = {}");
@ -701,6 +816,9 @@ fn commit_to_scopes_and_restores_on_a_normal_return() {
/// through the scope, or by restoring on the success path only.
#[test]
fn commit_to_restores_when_the_callback_raises() {
if reexec_isolated("commit_to_restores_when_the_callback_raises") {
return;
}
let td = project();
let mut s = EditorState::new();
exec(&s, "pmacs.lsp.config = {}");
@ -744,6 +862,9 @@ fn commit_to_restores_when_the_callback_raises() {
/// `<no raise>`.
#[test]
fn commit_to_refuses_an_await_and_restores() {
if reexec_isolated("commit_to_refuses_an_await_and_restores") {
return;
}
let td = project();
let mut s = EditorState::new();
exec(&s, "pmacs.lsp.config = {}");
@ -795,6 +916,9 @@ fn commit_to_refuses_an_await_and_restores() {
/// userdata after invoking the callback.
#[test]
fn commit_to_refuses_a_forged_destination() {
if reexec_isolated("commit_to_refuses_a_forged_destination") {
return;
}
let td = project();
let mut s = EditorState::new();
exec(&s, "pmacs.lsp.config = {}");
@ -831,6 +955,9 @@ fn commit_to_refuses_a_forged_destination() {
/// a shared mutable table.
#[test]
fn a_declining_listener_cannot_redirect_the_destination() {
if reexec_isolated("a_declining_listener_cannot_redirect_the_destination") {
return;
}
let td = project();
let mut s = EditorState::new();
exec(&s, "pmacs.lsp.config = {}");
@ -907,6 +1034,9 @@ fn a_declining_listener_cannot_redirect_the_destination() {
/// distinguish "validates" from "validates in time".
#[test]
fn preservation_a_failed_precondition_never_reaches_the_callback() {
if reexec_isolated("preservation_a_failed_precondition_never_reaches_the_callback") {
return;
}
// (label, Lua that breaks the precondition, expected reason fragment)
let cases: [(&str, &str, &str); 4] = [
(
@ -994,6 +1124,9 @@ fn preservation_a_failed_precondition_never_reaches_the_callback() {
/// (window-only validation). The dired buffer then replaces the user's.
#[test]
fn preservation_a_stale_destination_loses_to_the_users_newer_buffer() {
if reexec_isolated("preservation_a_stale_destination_loses_to_the_users_newer_buffer") {
return;
}
let td = project();
let mut s = EditorState::new();
exec(&s, "pmacs.lsp.config = {}");
@ -1062,6 +1195,11 @@ fn preservation_a_stale_destination_loses_to_the_users_newer_buffer() {
/// `*competitor*`.
#[test]
fn preservation_dired_captures_prev_from_the_destination_not_the_ambient_frontend() {
if reexec_isolated(
"preservation_dired_captures_prev_from_the_destination_not_the_ambient_frontend",
) {
return;
}
let td = project();
let mut s = EditorState::new();
exec(&s, "pmacs.lsp.config = {}");
@ -1117,6 +1255,9 @@ fn preservation_dired_captures_prev_from_the_destination_not_the_ambient_fronten
/// the read-only contract rather than pin the journey.
#[test]
fn journey_step5_editing_a_file_reached_through_the_directory() {
if reexec_isolated("journey_step5_editing_a_file_reached_through_the_directory") {
return;
}
let td = project();
let mut s = launch(td.path());
assert!(active_name(&s).starts_with("*dired:"));
@ -1169,6 +1310,9 @@ fn journey_step5_editing_a_file_reached_through_the_directory() {
/// preserve is which window shows the file, and that is what this pins.
#[test]
fn preservation_opening_a_file_shows_it_in_the_active_window() {
if reexec_isolated("preservation_opening_a_file_shows_it_in_the_active_window") {
return;
}
let td = project();
let target = td.path().join("alpha.txt");
let s = EditorState::open(target.clone()).expect("open");
@ -1204,6 +1348,9 @@ fn preservation_opening_a_file_shows_it_in_the_active_window() {
/// failure mode is a hard error on a perfectly ordinary gesture.
#[test]
fn preservation_a_missing_path_becomes_a_new_file_buffer() {
if reexec_isolated("preservation_a_missing_path_becomes_a_new_file_buffer") {
return;
}
let td = project();
let fresh = td.path().join("not-yet.txt");
let s = EditorState::open(fresh.clone()).expect("a missing path is not an error");
@ -1223,6 +1370,9 @@ fn preservation_a_missing_path_becomes_a_new_file_buffer() {
#[test]
fn preservation_an_unreadable_file_reports_with_its_path() {
use std::os::unix::fs::PermissionsExt;
if reexec_isolated("preservation_an_unreadable_file_reports_with_its_path") {
return;
}
let td = project();
let locked = td.path().join("locked.txt");
std::fs::write(&locked, b"secret\n").expect("write");
@ -1270,6 +1420,9 @@ fn preservation_an_unreadable_file_reports_with_its_path() {
/// real accept path; this one pins the primitive and the window state.
#[test]
fn preservation_display_file_still_refuses_a_directory() {
if reexec_isolated("preservation_display_file_still_refuses_a_directory") {
return;
}
let td = project();
let mut s = EditorState::open(td.path().join("alpha.txt")).expect("open");
exec(&s, "pmacs.lsp.config = {}");
@ -1440,6 +1593,9 @@ fn walk_to_open_file(dir: &Path, name: &str) -> EditorState {
/// `COHERENCE.md` says is missing.
#[test]
fn journey_step9_the_compile_chord_opens_the_prompt() {
if reexec_isolated("journey_step9_the_compile_chord_opens_the_prompt") {
return;
}
let td = cargo_project();
let mut s = walk_to_open_file(td.path(), "Cargo.toml");
press_compile_chord(&mut s);
@ -1453,6 +1609,9 @@ fn journey_step9_the_compile_chord_opens_the_prompt() {
/// **N** — the prompt is prefilled from the detected project kind.
#[test]
fn journey_step9_the_prompt_is_prefilled_for_a_cargo_project() {
if reexec_isolated("journey_step9_the_prompt_is_prefilled_for_a_cargo_project") {
return;
}
let td = cargo_project();
let mut s = walk_to_open_file(td.path(), "Cargo.toml");
press_compile_chord(&mut s);
@ -1472,6 +1631,9 @@ fn journey_step9_the_prompt_is_prefilled_for_a_cargo_project() {
/// which is what re-resolving at accept time looks like.
#[test]
fn journey_step9_the_prompt_runs_in_the_directory_it_captured() {
if reexec_isolated("journey_step9_the_prompt_runs_in_the_directory_it_captured") {
return;
}
let a = cargo_project();
let b = project();
let mut s = walk_to_open_file(a.path(), "Cargo.toml");
@ -1521,6 +1683,9 @@ fn journey_step9_the_prompt_runs_in_the_directory_it_captured() {
/// real process.
#[test]
fn journey_step9_the_offered_command_builds_the_project() {
if reexec_isolated("journey_step9_the_offered_command_builds_the_project") {
return;
}
if !binary_available("cargo") {
assert!(
std::env::var_os("PMACS_REQUIRE_CARGO_BUILD").is_none(),
@ -1574,6 +1739,9 @@ fn journey_step9_the_offered_command_builds_the_project() {
/// from it yields `node` again and this pin stays green.
#[test]
fn journey_step9_a_nested_project_gets_its_own_kind_not_the_outer_one() {
if reexec_isolated("journey_step9_a_nested_project_gets_its_own_kind_not_the_outer_one") {
return;
}
let outer = cargo_project();
let sub = outer.path().join("sub");
std::fs::create_dir_all(&sub).expect("mkdir sub");
@ -1610,6 +1778,9 @@ fn journey_step9_a_nested_project_gets_its_own_kind_not_the_outer_one() {
/// runner's cwd and pinning it would pin the environment.
#[test]
fn journey_step9_the_compile_context_is_total_even_with_no_file_open() {
if reexec_isolated("journey_step9_the_compile_context_is_total_even_with_no_file_open") {
return;
}
let td = cargo_project();
let s = launch(td.path());
assert!(
@ -1638,6 +1809,9 @@ fn journey_step9_the_compile_context_is_total_even_with_no_file_open() {
/// is reordering the precedence chain to put `defaults[kind]` first.
#[test]
fn journey_step9_preservation_the_last_command_outranks_the_default() {
if reexec_isolated("journey_step9_preservation_the_last_command_outranks_the_default") {
return;
}
let td = cargo_project();
let mut s = walk_to_open_file(td.path(), "Cargo.toml");
exec(&s, "pmacs.compile.run('true')");
@ -1660,6 +1834,9 @@ fn journey_step9_preservation_the_last_command_outranks_the_default() {
/// over one of these sequences.
#[test]
fn journey_step9_preservation_the_existing_compile_bindings_survive() {
if reexec_isolated("journey_step9_preservation_the_existing_compile_bindings_survive") {
return;
}
let td = cargo_project();
let s = walk_to_open_file(td.path(), "Cargo.toml");
for (sequence, command) in [
@ -1705,6 +1882,9 @@ fn binary_available(name: &str) -> bool {
#[cfg(unix)]
#[test]
fn journey_step9_the_compile_directory_is_detection_canonical() {
if reexec_isolated("journey_step9_the_compile_directory_is_detection_canonical") {
return;
}
let parent = tempfile::tempdir().expect("tempdir");
let real = parent.path().join("real");
std::fs::create_dir_all(real.join("src")).expect("mkdir real");
@ -1804,6 +1984,9 @@ fn start_local() -> EditorState {
/// `prepare_startup` — the mutation every by-hand pin would survive.
#[test]
fn journey_step4_a_no_target_launch_greets_in_scratch() {
if reexec_isolated("journey_step4_a_no_target_launch_greets_in_scratch") {
return;
}
let s = start_local();
// Preconditions asserted, not assumed (framing §3.2b): a developer
@ -1835,6 +2018,9 @@ fn journey_step4_a_no_target_launch_greets_in_scratch() {
/// is two chords and nothing in the rendered text marks the boundary.
#[test]
fn journey_step4_every_advertised_key_is_bound() {
if reexec_isolated("journey_step4_every_advertised_key_is_bound") {
return;
}
let s = start_local();
let entries = welcome_entries(&s);
assert!(
@ -1858,6 +2044,9 @@ fn journey_step4_every_advertised_key_is_bound() {
/// Pin 2 alone would pass if rendering silently dropped one.
#[test]
fn journey_step4_the_rendered_welcome_contains_every_entry() {
if reexec_isolated("journey_step4_the_rendered_welcome_contains_every_entry") {
return;
}
let s = start_local();
let text = active_text(&s);
for (keys, label) in welcome_entries(&s) {
@ -1884,6 +2073,9 @@ fn journey_step4_the_rendered_welcome_contains_every_entry() {
/// so nothing about the accepted value survives afterwards.
#[test]
fn journey_step4_m_x_help_renders_the_cheat_sheet() {
if reexec_isolated("journey_step4_m_x_help_renders_the_cheat_sheet") {
return;
}
let mut s = start_local();
s.dispatch_key(
@ -1922,6 +2114,9 @@ fn journey_step4_m_x_help_renders_the_cheat_sheet() {
/// would lift read-only, discard history, and fail the insert.
#[test]
fn journey_step4_preservation_the_greeted_scratch_is_editable_and_clean() {
if reexec_isolated("journey_step4_preservation_the_greeted_scratch_is_editable_and_clean") {
return;
}
let mut s = start_local();
assert!(
eval::<bool>(
@ -1942,6 +2137,9 @@ fn journey_step4_preservation_the_greeted_scratch_is_editable_and_clean() {
/// **P** — a file target does not greet.
#[test]
fn journey_step4_preservation_a_file_target_does_not_greet() {
if reexec_isolated("journey_step4_preservation_a_file_target_does_not_greet") {
return;
}
let td = project();
let path = td.path().join("alpha.txt");
let s = match pmacs::editor::prepare_startup(Some(path.clone())).expect("startup") {
@ -1965,6 +2163,9 @@ fn journey_step4_preservation_a_file_target_does_not_greet() {
/// still exists to be wrongly greeted.
#[test]
fn journey_step4_preservation_a_directory_target_does_not_greet() {
if reexec_isolated("journey_step4_preservation_a_directory_target_does_not_greet") {
return;
}
let td = project();
let mut s =
match pmacs::editor::prepare_startup(Some(td.path().to_path_buf())).expect("startup") {
@ -1979,6 +2180,9 @@ fn journey_step4_preservation_a_directory_target_does_not_greet() {
/// **P** — a non-empty `*scratch*` is never overwritten.
#[test]
fn journey_step4_preservation_existing_scratch_content_survives() {
if reexec_isolated("journey_step4_preservation_existing_scratch_content_survives") {
return;
}
let mut s = EditorState::new();
exec(&s, "pmacs.window.buffer():insert(0, 'user content')");
s.finalize_local_launch(false);
@ -1993,6 +2197,9 @@ fn journey_step4_preservation_existing_scratch_content_survives() {
/// desktop restore having put something else in front.
#[test]
fn journey_step4_preservation_a_backgrounded_scratch_is_not_greeted() {
if reexec_isolated("journey_step4_preservation_a_backgrounded_scratch_is_not_greeted") {
return;
}
let td = project();
let mut s = EditorState::new();
exec(
@ -2019,6 +2226,9 @@ fn journey_step4_preservation_a_backgrounded_scratch_is_not_greeted() {
/// reached in production.
#[test]
fn journey_step4_preservation_constructors_never_greet() {
if reexec_isolated("journey_step4_preservation_constructors_never_greet") {
return;
}
let bare = EditorState::new();
assert_eq!(scratch_text(&bare), "", "EditorState::new must not greet");
@ -2078,6 +2288,9 @@ fn lsp_segment(s: &EditorState) -> Option<String> {
/// vacuous.
#[test]
fn journey_step6_a_missing_language_server_is_reported_not_swallowed() {
if reexec_isolated("journey_step6_a_missing_language_server_is_reported_not_swallowed") {
return;
}
let td = tempfile::tempdir().expect("tempdir");
std::fs::write(td.path().join("Cargo.toml"), b"[package]\nname=\"x\"\n").expect("write toml");
std::fs::write(td.path().join("main.rs"), b"fn main() {}\n").expect("write rs");
@ -2138,6 +2351,9 @@ fn journey_step6_a_missing_language_server_is_reported_not_swallowed() {
#[test]
fn journey_step4_the_welcome_paints_as_multiple_rows_on_the_first_frame() {
use pmacs::cell::{Cell, CellGrid, CellSize, Glyph};
if reexec_isolated("journey_step4_the_welcome_paints_as_multiple_rows_on_the_first_frame") {
return;
}
let s = start_local();
let (rows, cols) = (12u32, 100u32);
@ -2192,3 +2408,86 @@ fn journey_step4_the_welcome_paints_as_multiple_rows_on_the_first_frame() {
row_text(0)
);
}
// ---------------------------------------------------------------------------
// The isolation mechanism itself
// ---------------------------------------------------------------------------
/// **N** — the re-exec of §1.10 actually redirects the child's roots.
///
/// Every other test in this file is *protected* by the mechanism; none
/// of them fails if it silently stops working, because they assert about
/// editor behaviour and a developer's `init.lua` usually leaves that
/// alone. This one asserts the mechanism's own output: the roots the
/// child resolves, from inside the child.
///
/// Falsified by dropping `.envs(child_env)` from `reexec_isolated` — the
/// child then resolves the developer's real `~/.config/pmacs` and
/// `~/.local/share`, and all four assertions below fail.
#[test]
fn journey_isolated_child_resolves_the_controlled_roots() {
if reexec_isolated("journey_isolated_child_resolves_the_controlled_roots") {
return;
}
let base = Path::new(env!("CARGO_TARGET_TMPDIR"))
.join("journey-isolation")
.join("journey_isolated_child_resolves_the_controlled_roots");
let under = |p: Option<std::path::PathBuf>, what: &str| {
let p = p.unwrap_or_else(|| panic!("{what} must resolve in the child"));
assert!(
p.starts_with(&base),
"{what} resolved to {} — outside the controlled base {}",
p.display(),
base.display()
);
};
under(pmacs::config::user_config_dir(), "the config dir");
under(
Some(pmacs::builtin_packages::bundled_runtime_dir()),
"the bundled-package dir",
);
// `PMACS_STATE_HOME` is the fifth variable: it outranks
// `XDG_STATE_HOME`, so a child given only the four XDG ones inherits
// whatever the launching shell exported.
under(pmacs::state::user_state_dir(), "the state dir");
under(pmacs::minibuffer::user_history_dir(), "the history dir");
}
/// **N** — the ambient entry point is still what this suite drives.
///
/// The isolation seam has a `new_with_roots` / `open_with_roots` sibling
/// that every other suite in the tree now takes. Taking it *here* would
/// be a silent regression of what the ratchet exists to prove — a
/// production entry point with no caller — and would leave every
/// assertion in this file green. Asserted against this file's own source
/// rather than left as an obvious-by-inspection property.
#[test]
fn journey_drives_the_ambient_production_entry_point() {
let src = std::fs::read_to_string(
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("journey_acceptance.rs"),
)
.expect("read this suite's own source");
// Every needle is assembled rather than written whole: a literal
// spelled out here would appear in this file and match itself, which
// turns the positive check vacuous and the negative ones into
// guaranteed failures.
let ambient_open = concat!("EditorState::open(", "path.to_path_buf())");
let seam_open = concat!("EditorState::open", "_with_roots(");
let seam_new = concat!("EditorState::new", "_with_roots(");
assert!(
src.contains(ambient_open),
"journey must call the parameterless production `open`"
);
assert!(
!src.contains(seam_open),
"journey must not take the isolation seam — its isolation comes \
from the environment its child is launched with"
);
assert!(
!src.contains(seam_new),
"journey must not take the isolation seam — its isolation comes \
from the environment its child is launched with"
);
}

View File

@ -74,7 +74,7 @@ fn status(s: &EditorState) -> String {
/// Fresh editor whose scratch buffer holds `text`, cursor at 0.
fn editor_with(text: &str) -> EditorState {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, text);
exec(&s, "pmacs.editor.goto_byte(0)");
s
@ -855,3 +855,10 @@ fn frontend_detached_drops_per_frontend_state() {
);
assert!(gone, "detach dropped B's killring state");
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -90,7 +90,7 @@ impl Fixture {
/// resolver. That combination is the point: the root rule under test is
/// production code, only the command is a stand-in.
fn editor(fx: &Fixture) -> EditorState {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
exec(&state, "pmacs.lsp.config = {}");
exec(
&state,
@ -1880,3 +1880,10 @@ fn r6_no_swap_retires_only_the_failed_root() {
swap occurred"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -38,7 +38,7 @@ fn fresh_state_dir() -> PathBuf {
}
fn editor(state_dir: &std::path::Path) -> EditorState {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
s.lua_host.lua().remove_app_data::<StateDir>();
s.lua_host
.lua()
@ -243,7 +243,7 @@ fn acc10b_the_prime_suffix_does_not_pair_in_lean() {
/// WRITE-ONLY proxy (the canonical map lives Rust-side), so an
/// alias-table read would prove nothing about what the parser does.
fn markdown_layer_languages(src: &[u8]) -> Vec<String> {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
let buf_id = state
.lua_host
.registry()
@ -323,7 +323,7 @@ fn acc12_opening_lean_spawns_no_process_without_a_server_config() {
// Constructing an editor touches no process, even though the Lean
// config now exists and names `lake`.
let pristine = EditorState::new();
let pristine = EditorState::new_with_roots(&crate::iso::roots());
let at_init: i64 = eval(&pristine, "return #pmacs.process.list()");
assert_eq!(
at_init, 0,
@ -352,3 +352,10 @@ fn acc12_opening_lean_spawns_no_process_without_a_server_config() {
"with no server configured, opening a Lean buffer spawns nothing"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -72,7 +72,7 @@ fn lean_editor() -> (EditorState, PathBuf) {
let dir = fresh_dir();
let f = dir.join("a.lean");
std::fs::write(&f, "").unwrap();
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
exec(&s, "pmacs.lsp.config = {}");
let fd = f.display().to_string();
exec(&s, &format!("pmacs.buffer.find_or_open({fd:?})"));
@ -543,7 +543,7 @@ fn no_abbreviation_state_is_opened_outside_a_lean_buffer() {
let dir = fresh_dir();
let f = dir.join("a.rs");
std::fs::write(&f, "").unwrap();
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
exec(&s, "pmacs.lsp.config = {}");
let fd = f.display().to_string();
exec(&s, &format!("pmacs.buffer.find_or_open({fd:?})"));
@ -811,7 +811,7 @@ fn the_expansion_reaches_the_first_did_change() {
let f = dir.join("a.lean");
std::fs::write(&f, "").unwrap();
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
s.lua_host.lua().remove_app_data::<StateDir>();
s.lua_host.lua().set_app_data(StateDir(dir.clone()));
exec(&s, "pmacs.lsp.config = {}");
@ -1018,3 +1018,10 @@ fn detaching_a_frontend_purges_only_its_own_pending_state() {
"B's detachment purged B's entries and left A's record valid"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -212,7 +212,7 @@ fn probe(s: &EditorState) -> (String, String, i64, Option<String>) {
#[test]
fn open_seats_cursor_and_ret_visits_the_row() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
open_test_panel(&mut s);
let (name, text, line, _) = probe(&s);
assert_eq!(name, "*test-panel*");
@ -227,7 +227,7 @@ fn open_seats_cursor_and_ret_visits_the_row() {
#[test]
fn header_row_is_not_visitable() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
open_test_panel(&mut s);
press(&mut s, KeyCode::Char('p')); // up onto the header
press(&mut s, KeyCode::Enter);
@ -237,7 +237,7 @@ fn header_row_is_not_visitable() {
#[test]
fn q_restores_the_previous_buffer() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
open_test_panel(&mut s);
press(&mut s, KeyCode::Char('q'));
let (name, _, _, _) = probe(&s);
@ -246,7 +246,7 @@ fn q_restores_the_previous_buffer() {
#[test]
fn panel_rejects_typing() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
open_test_panel(&mut s);
let (_, before, _, _) = probe(&s);
press(&mut s, KeyCode::Char('z')); // unbound printable → self-insert → intercept rejects
@ -258,7 +258,7 @@ fn panel_rejects_typing() {
fn dispatch_idle_is_false_while_a_panel_is_focused() {
// Q#P6: while the panel is the active buffer, semantic frontends
// must round-trip every key (RET = visit, not an optimistic \n).
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
assert!(s.dispatch_idle(), "scratch buffer: idle");
open_test_panel(&mut s);
assert!(!s.dispatch_idle(), "panel focused: keys must round-trip");
@ -268,7 +268,7 @@ fn dispatch_idle_is_false_while_a_panel_is_focused() {
#[test]
fn refresh_reruns_the_source_and_reseats() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
open_test_panel(&mut s);
press(&mut s, KeyCode::Char('g'));
let (_, text, line, _) = probe(&s);
@ -304,7 +304,7 @@ const PANEL_TEXT: &str = "3 items RET visit q quit\nalpha\nbeta\ngamma";
/// consulting the intercept chain.
#[test]
fn s1_1_the_undo_chord_cannot_empty_a_listview_panel() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
open_test_panel(&mut s);
assert_eq!(active_text(&s), PANEL_TEXT, "precondition: rendered");
@ -327,7 +327,7 @@ fn s1_1_the_undo_chord_cannot_empty_a_listview_panel() {
/// *Bite:* same empty result on the pre-image.
#[test]
fn s1_2_m_x_buffer_undo_cannot_empty_a_listview_panel() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
open_test_panel(&mut s);
m_x(&mut s, "buffer.undo");
@ -355,7 +355,7 @@ fn s1_2_m_x_buffer_undo_cannot_empty_a_listview_panel() {
/// raising.
#[test]
fn s1_4_the_owners_refresh_still_works_after_the_lock() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
open_test_panel(&mut s);
let panel = id_of(&s, "*test-panel*");
assert!(
@ -388,7 +388,7 @@ fn s1_4_the_owners_refresh_still_works_after_the_lock() {
/// therefore passes the rope half and fails the lifted half.
#[test]
fn s1_5_the_rope_lock_and_named_intercept_refuse_in_order() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
open_test_panel(&mut s);
let panel = id_of(&s, "*test-panel*");
let before = active_text(&s);
@ -453,7 +453,7 @@ fn s1_5_the_rope_lock_and_named_intercept_refuse_in_order() {
/// pinned through `dispatch_idle_for` rather than through `read_only`.
#[test]
fn s1_6_round_trip_input_survives_the_adoption() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
open_test_panel(&mut s);
// (a) the premise.
@ -498,7 +498,7 @@ fn s1_6_round_trip_input_survives_the_adoption() {
/// the old line index live and this paint assertion bites.
#[test]
fn s1_7_a_shrinking_refresh_reaches_the_window() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
open_test_panel(&mut s);
let painted = paint_active_window(&s, 6, 24);
assert_eq!(
@ -540,7 +540,7 @@ fn s1_7_a_shrinking_refresh_reaches_the_window() {
/// builtin/runtime/listview.lua` falsifies it.
#[test]
fn s1_9_a_foreign_buffer_with_the_panels_name_is_never_adopted() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
exec(
&s,
"FOREIGN = pmacs.buffer.create('*test-panel*')\n\
@ -585,7 +585,7 @@ fn s1_9_a_foreign_buffer_with_the_panels_name_is_never_adopted() {
/// created.
#[test]
fn s1_10_the_disambiguation_limit_raises_rather_than_adopting() {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
exec(
&s,
"MINE = pmacs.buffer.create('*test-panel*')\n\
@ -624,7 +624,7 @@ fn s1_10_the_disambiguation_limit_raises_rather_than_adopting() {
/// command produced, never on "it did not raise".
#[test]
fn s1_11_a_disambiguated_panel_still_answers_ret_g_and_q() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
exec(
&s,
"FOREIGN = pmacs.buffer.create('*test-panel*')\n\
@ -678,7 +678,7 @@ fn s1_11_a_disambiguated_panel_still_answers_ret_g_and_q() {
/// the disambiguation and `q` lands back in `*test-panel*<2>`.
#[test]
fn s1_12_the_q_target_capture_is_not_inverted_across_two_panels() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
exec(
&s,
"FOREIGN = pmacs.buffer.create('*test-panel*')\n\
@ -755,3 +755,10 @@ fn s1_14_no_bypass_write_or_name_keyed_identity_remains() {
"every `panels[` subscript must be an append; found {subscripts:?}"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -36,7 +36,7 @@ fn fake_lsp_path() -> String {
/// A fresh editor with the shipped language configs cleared, so the only
/// server any test can spawn is the fake one it configures itself.
fn editor() -> EditorState {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
exec(&state, "pmacs.lsp.config = {}");
state
}
@ -722,3 +722,10 @@ fn acc34a_canonicalize_declines_a_non_utf8_resolution() {
return a U+FFFD-substituted path that exists nowhere"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -35,7 +35,7 @@ fn fake_lsp_path() -> String {
/// A fresh editor with the shipped language configs cleared, so the only
/// server any test can spawn is the fake one it configures itself.
fn editor() -> EditorState {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
exec(&state, "pmacs.lsp.config = {}");
state
}
@ -702,3 +702,10 @@ fn a_resolver_returning_nil_declines_silently() {
file_uri(&fx.dir("proj"))
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -89,7 +89,7 @@ fn open(state: &EditorState, path: &Path) {
}
fn editor_for(dir: &Path) -> EditorState {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
// Clamp detection so a stray marker above the tempdir cannot leak in.
exec(
&state,
@ -565,3 +565,10 @@ fn j1b2_preservation_a_spawnable_server_still_attaches() {
);
assert_eq!(failure_count(&state), 0);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -129,7 +129,7 @@ fn assert_disjoint_within(ranges: &[ByteRange], vp: ByteRange) {
#[test]
fn incremental_reconstruction_equals_fresh_full_projection() {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
let buffer_id = active_buffer(&state);
let vp1 = ByteRange { start: 0, end: 64 };
@ -327,3 +327,11 @@ fn daemon_routes_semantic_family_to_semantic_session_only() {
"grid session must NOT receive the semantic family"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root. Re-exported rather than re-declared
// with `#[path]` — this file already pulls in `common`, and loading one
// source file as two modules is `clippy::duplicate_mod`.
use common::iso;

View File

@ -258,7 +258,7 @@ fn m4_1_parse_tree_introspectable_via_lua() {
use pmacs::editor::EditorState;
use pmacs::lua_bindings::BufferIdLua;
let editor = EditorState::new();
let editor = EditorState::new_with_roots(&crate::iso::roots());
// Insert a buffer with known shape; bind its handle into Lua as
// `BUF` so the script can reference it by name.
let buf_id = editor
@ -359,7 +359,8 @@ fn m4_2_opening_a_rust_file_produces_a_parse_tree() {
let path = dir.path().join("hello.rs");
std::fs::write(&path, b"fn main() { let x = 1 + 2; }\n").expect("write");
let mut state = pmacs::editor::EditorState::open(path).expect("open .rs");
let mut state =
pmacs::editor::EditorState::open_with_roots(path, &crate::iso::roots()).expect("open .rs");
pump_async(&mut state, |s| current_tree_language(s).is_some());
assert_eq!(current_tree_language(&state).as_deref(), Some("rust"));
@ -389,7 +390,8 @@ fn m4_2_opening_a_lua_file_produces_a_parse_tree() {
let path = dir.path().join("hello.lua");
std::fs::write(&path, b"local x = 1\nreturn x + 2\n").expect("write");
let mut state = pmacs::editor::EditorState::open(path).expect("open .lua");
let mut state =
pmacs::editor::EditorState::open_with_roots(path, &crate::iso::roots()).expect("open .lua");
pump_async(&mut state, |s| current_tree_language(s).is_some());
assert_eq!(current_tree_language(&state).as_deref(), Some("lua"));
@ -420,7 +422,8 @@ fn m4_2_register_extension_attaches_for_a_runtime_added_extension() {
let path = dir.path().join("hello.myrust");
std::fs::write(&path, b"fn main() {}\n").expect("write");
let mut state = pmacs::editor::EditorState::open(path).expect("open .myrust");
let mut state = pmacs::editor::EditorState::open_with_roots(path, &crate::iso::roots())
.expect("open .myrust");
// The auto-attach hook fires *during* open, but at that point our
// custom extension isn't registered yet --- so the first hook
// invocation is a no-op. Register the extension and trigger the
@ -516,7 +519,8 @@ fn render_active_window_to_grid(
/// either the parse settles (so highlights can attach) or the
/// timeout deadline hits.
fn open_and_wait_for_parse(path: std::path::PathBuf) -> pmacs::editor::EditorState {
let mut state = pmacs::editor::EditorState::open(path).expect("open file");
let mut state =
pmacs::editor::EditorState::open_with_roots(path, &crate::iso::roots()).expect("open file");
pump_async(&mut state, |s| current_tree_language(s).is_some());
state
}
@ -1155,7 +1159,7 @@ fn m4_4_pty_mode_child_observes_a_tty() {
/// to Lua" spec invariant for the process case.
#[test]
fn m4_4_lua_surface_drives_lifecycle() {
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
let id_raw: i64 = state
.lua_host
.lua()
@ -1611,7 +1615,7 @@ fn m4_5_protocol_violation_surfaces_as_structured_error() {
fn m4_5_lua_surface_drives_lsp_lifecycle() {
use pmacs::editor::EditorState;
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
let lua = state.lua_host.lua();
let sid_raw: u64 = lua
@ -1846,7 +1850,7 @@ fn m4_6_diagnostic_source_field_is_preserved() {
fn m4_6_lua_surface_reads_diagnostics() {
use pmacs::editor::EditorState;
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
let lua = state.lua_host.lua();
let sid_raw: u64 = lua
@ -1958,7 +1962,7 @@ fn m4_6_lua_surface_reads_diagnostics() {
fn m4_6_diag_navigate_commands_and_bindings_are_registered() {
use pmacs::editor::EditorState;
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
let lua = state.lua_host.lua();
let commands: Vec<String> = lua
@ -2028,7 +2032,7 @@ fn m4_6_diag_navigate_commands_and_bindings_are_registered() {
fn m4_6_diag_attach_view_pushes_diagnostic_overlay() {
use pmacs::editor::EditorState;
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
let attached: bool = state
.lua_host
.lua()
@ -2260,7 +2264,7 @@ fn m4_7_signature_help_during_function_call() {
fn m4_7_lua_surface_drives_completion_hover_signature() {
use pmacs::editor::EditorState;
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
let lua = state.lua_host.lua();
let sid_raw: u64 = lua
@ -2581,7 +2585,7 @@ fn m4_8_status_buffer_text_reflects_state_and_capabilities() {
fn m4_8_lua_surface_exposes_status() {
use pmacs::editor::EditorState;
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
let lua = state.lua_host.lua();
let sid_raw: u64 = lua
@ -2761,7 +2765,7 @@ fn m4_9_project_switch_is_first_class() {
touch_file(&d1.path().join("Cargo.toml"));
touch_file(&d2.path().join(".luarc.json"));
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
let lua = state.lua_host.lua();
let d1_path = d1.path().display().to_string();
let d2_path = d2.path().display().to_string();
@ -2817,7 +2821,7 @@ fn m4_9_lsp_runs_per_project_not_per_buffer() {
touch_file(&d1.path().join("Cargo.toml"));
touch_file(&d2.path().join("Cargo.toml"));
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let d1_path = d1.path().display().to_string();
let d2_path = d2.path().display().to_string();
@ -2929,7 +2933,7 @@ fn m4_10_index_persists_across_sessions() {
// Session A: index a synthetic source file, save to disk.
{
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
let lua = state.lua_host.lua();
let saved_path: String = lua
.load(format!(
@ -2957,7 +2961,7 @@ fn m4_10_index_persists_across_sessions() {
// searches still find what session A indexed --- without ever
// calling upsert_file again.
let started = std::time::Instant::now();
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
let lua = state.lua_host.lua();
let (file_count, symbol_count, hit_count, hit_name): (u64, u64, u64, String) = lua
.load(format!(
@ -3063,7 +3067,7 @@ fn m4_10_lua_surface_drives_index() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path().display().to_string();
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
let lua = state.lua_host.lua();
let (files_before, syms_before, hit_count, after_invalidate, generation_after): (
@ -3111,7 +3115,7 @@ fn m4_10_lua_surface_ingests_lsp_workspace_symbol() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path().display().to_string();
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
let lua = state.lua_host.lua();
// The URI we feed has to be under the project root so the
// resulting absolute path is deterministic.
@ -3160,7 +3164,7 @@ fn m4_10_lua_surface_ingests_lsp_workspace_symbol() {
fn m4_11_multiple_sources_combine_without_duplicates() {
use pmacs::editor::EditorState;
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
let lua = state.lua_host.lua();
let (parse_count, parse_source, total_count): (u64, String, u64) = lua
@ -3235,7 +3239,7 @@ fn m4_11_multiple_sources_combine_without_duplicates() {
fn m4_11_source_priority_configurable() {
use pmacs::editor::EditorState;
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
let lua = state.lua_host.lua();
let (winner_first, winner_second): (String, String) = lua
@ -3289,7 +3293,7 @@ fn m4_11_source_priority_configurable() {
fn m4_11_custom_sources_from_lua() {
use pmacs::editor::EditorState;
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
let lua = state.lua_host.lua();
let (custom_count_active, custom_label, custom_source, count_disabled, count_unregistered): (
@ -3373,7 +3377,7 @@ fn m4_11_custom_sources_from_lua() {
fn m4_11_snippets_surface_through_completion() {
use pmacs::editor::EditorState;
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
let lua = state.lua_host.lua();
let (label, kind, insert_text, source): (String, String, String, String) = lua
.load(
@ -3529,7 +3533,7 @@ fn m4_12_formatting_response_lands_in_store() {
fn m4_12_lua_surface_drives_definition_and_formatting() {
use pmacs::editor::EditorState;
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
// Spawn a server through the Lua surface.
@ -3667,7 +3671,7 @@ fn m4_12_cross_file_go_to_definition_and_jump_back() {
let b_disp = b_path.display().to_string();
let b_uri = format!("file://{b_disp}");
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
// Point the default `rust` server at the fake, in `defenv` mode,
@ -3787,7 +3791,7 @@ fn m4_13_rename_applies_cross_file_workspace_edit() {
let b_disp = b_path.display().to_string();
let b_uri = format!("file://{b_disp}");
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
state
@ -3915,7 +3919,7 @@ fn m4_14_code_action_command_drives_apply_edit() {
std::fs::write(&a_path, b"abcfooxyz\n___zzz\n").expect("write a");
let a_disp = a_path.display().to_string();
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
state
@ -4028,7 +4032,7 @@ fn m4_15_workspace_edit_resource_ops_apply_in_order() {
let created = dir.path().join("created.rs");
let b2 = dir.path().join("b2.rs");
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
state
@ -4131,7 +4135,7 @@ fn m4_15_workspace_edit_resource_ops_apply_in_order() {
/// kinds, and `paddingRight`.
#[test]
fn m4_16_lua_surface_drives_inlay_hints() {
let mut s = pmacs::editor::EditorState::new();
let mut s = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
spawn_lsp_and_init(&mut s, Some("inlaybounds"));
let uri = "file:///tmp/m4_16_inlay.rs";
@ -4190,7 +4194,7 @@ fn m4_16_lua_surface_drives_inlay_hints() {
/// the `token_type` index resolves to a name.
#[test]
fn m4_17_lua_surface_drives_semantic_tokens() {
let mut s = pmacs::editor::EditorState::new();
let mut s = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
spawn_lsp_and_init(&mut s, None);
let uri = "file:///tmp/m4_17_sem.rs";
@ -4261,7 +4265,7 @@ fn m4_18_inlay_hint_refresh_repulls_via_server_request() {
std::fs::write(&a_path, b"fn a() {}\n\n").expect("write a");
let a_disp = a_path.display().to_string();
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
state
.lua_host
@ -4314,7 +4318,7 @@ fn m4_18b_inlay_hints_auto_pull_after_initialize() {
std::fs::write(&a_path, b"fn a() {}\n\n").expect("write a");
let a_disp = a_path.display().to_string();
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
state
.lua_host
@ -4365,7 +4369,7 @@ fn m4_19_semantic_tokens_refresh_repulls_via_server_request() {
std::fs::write(&a_path, b"fn a() {}\n\n").expect("write a");
let a_disp = a_path.display().to_string();
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
state
.lua_host
@ -4422,7 +4426,7 @@ fn arc1c_semantic_tokens_auto_pull_on_attach() {
std::fs::write(&a_path, b"fn a() {}\n\n").expect("write a");
let a_disp = a_path.display().to_string();
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
state
.lua_host
@ -4466,7 +4470,7 @@ fn arc1c_semantic_tokens_repull_after_edit_flush() {
std::fs::write(&a_path, b"fn a() {}\n\n").expect("write a");
let a_disp = a_path.display().to_string();
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
state
.lua_host
@ -4529,7 +4533,7 @@ fn arc1d_signature_help_auto_triggers_on_trigger_char() {
std::fs::write(&a_path, b"\n").expect("write a");
let a_disp = a_path.display().to_string();
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
state
.lua_host
@ -4589,7 +4593,7 @@ fn arc1d_signature_help_does_not_trigger_on_ordinary_typing() {
std::fs::write(&a_path, b"\n").expect("write a");
let a_disp = a_path.display().to_string();
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
state
.lua_host
@ -4648,7 +4652,7 @@ fn arc1c_range_only_server_is_served_range_requests() {
std::fs::write(&a_path, b"fn a() {}\n\n").expect("write a");
let a_disp = a_path.display().to_string();
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
state
.lua_host
@ -4703,7 +4707,7 @@ fn arc1c_range_only_utf16_server_gets_converted_bounds() {
std::fs::write(&a_path, "fn a() {}\nlet x = \u{e9}\u{e9};".as_bytes()).expect("write a");
let a_disp = a_path.display().to_string();
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
state
.lua_host
@ -4752,7 +4756,7 @@ fn arc1d_signature_help_triggers_on_non_ascii_trigger_char() {
std::fs::write(&a_path, b"\n").expect("write a");
let a_disp = a_path.display().to_string();
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
state
.lua_host
@ -4810,7 +4814,7 @@ fn arc1d_signature_help_ignores_non_typed_edits() {
std::fs::write(&a_path, b"\n").expect("write a");
let a_disp = a_path.display().to_string();
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
state
.lua_host
@ -4881,7 +4885,7 @@ fn arc1c_full_only_server_repulls_via_full_not_delta() {
std::fs::write(&a_path, b"fn a() {}\n\n").expect("write a");
let a_disp = a_path.display().to_string();
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
state
.lua_host
@ -4954,7 +4958,7 @@ fn arc1c_full_only_server_repulls_via_full_not_delta() {
/// fake returns one token.
#[test]
fn m4_20_semantic_tokens_range() {
let mut s = pmacs::editor::EditorState::new();
let mut s = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
spawn_lsp_and_init(&mut s, None);
let uri = "file:///tmp/m4_20_sem.rs";
@ -4998,7 +5002,7 @@ fn m4_20_semantic_tokens_range() {
/// `resultId` "rid-2".
#[test]
fn m4_21_semantic_tokens_full_then_delta() {
let mut s = pmacs::editor::EditorState::new();
let mut s = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
spawn_lsp_and_init(&mut s, None);
let uri = "file:///tmp/m4_21_sem.rs";
@ -5078,7 +5082,7 @@ fn m4_22_rename_prepare_gates_and_prefills() {
std::fs::write(&a_path, b"abcfooxyz\n").expect("write a");
let a_disp = a_path.display().to_string();
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
state
.lua_host
@ -5179,7 +5183,7 @@ fn m4_23_rename_prepare_refusal_aborts() {
std::fs::write(&a_path, b"abcfooxyz\n").expect("write a");
let a_disp = a_path.display().to_string();
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
state
.lua_host
@ -5272,7 +5276,7 @@ fn m4_24_workspace_did_change_watched_files() {
let received = base.join(".received");
let foo_uri = format!("file://{}", base.join("foo.txt").display());
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
state
.lua_host
@ -5351,7 +5355,7 @@ fn m4_24_workspace_did_change_watched_files() {
fn m4_25_tier1_language_server_configs_and_filetypes() {
use pmacs::editor::EditorState;
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
let probe: mlua::Table = s
.lua_host
.lua()
@ -5455,7 +5459,7 @@ fn m4_26_auto_attach_roots_server_at_opened_files_project() {
let go_file_disp = go_file.display().to_string();
let fake = fake_lsp_path();
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
// Clamp the marker walk to the tempdir so a stray ancestor marker
// (a developer's /tmp/.git, say) can't masquerade as the root.
// Point the default `go` server at the fake in `rooturi` mode.
@ -5567,7 +5571,7 @@ fn m4_27_real_gopls_analyzes_module_via_auto_attach() {
let go_file_disp = go_file.display().to_string();
let uri = format!("file://{go_file_disp}");
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
// gopls handshake + workspace load is slow on a cold cache (30s).
real_server_open_and_init(&mut state, "go", &gopls, &root_disp, &go_file_disp);
@ -5664,7 +5668,7 @@ fn m4_28_real_clangd_diagnostics_and_semantic_tokens_via_auto_attach() {
let cpp_disp = cpp.display().to_string();
let uri = format!("file://{cpp_disp}");
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
real_server_open_and_init(&mut state, "cpp", &clangd, &root_disp, &cpp_disp);
// Fire a semantic-tokens request; diagnostics flow unsolicited
@ -5767,7 +5771,7 @@ fn m4_29_real_rust_analyzer_inlay_hints_via_auto_attach() {
let file_disp = file.display().to_string();
let uri = format!("file://{file_disp}");
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
real_server_open_and_init(&mut state, "rust", &rust_analyzer, &root_disp, &file_disp);
// rust-analyzer only answers `textDocument/inlayHint` after it has
@ -5815,7 +5819,7 @@ fn m4_29_real_rust_analyzer_inlay_hints_via_auto_attach() {
fn m4_12_default_bundle_wires_commands_and_keymaps() {
use pmacs::editor::EditorState;
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
let probe: mlua::Table = s
.lua_host
.lua()
@ -5870,7 +5874,7 @@ fn m4_12_default_bundle_wires_commands_and_keymaps() {
fn m4_12_default_bundle_wires_cuda() {
use pmacs::editor::EditorState;
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
let probe: mlua::Table = s
.lua_host
.lua()
@ -5926,7 +5930,7 @@ fn m4_12_default_bundle_wires_cuda() {
fn m4_12_default_bundle_wires_bash() {
use pmacs::editor::EditorState;
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
let probe: mlua::Table = s
.lua_host
.lua()
@ -5970,7 +5974,7 @@ fn m4_12_default_bundle_wires_bash() {
#[test]
fn m4_shebang_resolver_maps_interpreters() {
use pmacs::editor::EditorState;
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
let resolve = |first_line: &str| -> Option<String> {
s.lua_host
.lua()
@ -6030,7 +6034,7 @@ fn m4_shebang_resolver_maps_interpreters() {
#[test]
fn m4_shebang_extensionless_script_resolves_bash() {
use pmacs::editor::EditorState;
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
let dir = tempfile::tempdir().expect("tempdir");
let hook = dir.path().join("pre-commit"); // no extension
std::fs::write(&hook, b"#!/bin/sh\nset -e\necho building\n").expect("write");
@ -6071,7 +6075,7 @@ fn m4_shebang_extensionless_script_resolves_bash() {
#[test]
fn m4_shebang_does_not_override_extension() {
use pmacs::editor::EditorState;
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
let dir = tempfile::tempdir().expect("tempdir");
let f = dir.path().join("tool.py");
std::fs::write(&f, b"#!/bin/sh\nprint('hi')\n").expect("write");
@ -6116,7 +6120,7 @@ fn m4_shebang_does_not_override_extension() {
#[test]
fn m4_shebang_extensionless_grammarless_language_is_silent() {
use pmacs::editor::EditorState;
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
let dir = tempfile::tempdir().expect("tempdir");
let f = dir.path().join("generate"); // no extension
// `ruby` is deliberately grammarless (and serverless) — a language the
@ -6166,7 +6170,7 @@ fn m4_shebang_extensionless_grammarless_language_is_silent() {
#[test]
fn m4_shebang_edit_keeps_pinned_grammar() {
use pmacs::editor::EditorState;
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
let dir = tempfile::tempdir().expect("tempdir");
let hook = dir.path().join("deploy"); // no extension
std::fs::write(&hook, b"#!/bin/sh\necho one\n").expect("write");
@ -6261,7 +6265,7 @@ fn m4_shebang_edit_keeps_pinned_grammar() {
#[test]
fn m4_modeline_overrides_extension_end_to_end() {
use pmacs::editor::EditorState;
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
let dir = tempfile::tempdir().expect("tempdir");
let file = dir.path().join("misleading.py");
std::fs::write(&file, b"-- -*- mode: lua -*-\nprint('ok')\n").expect("write");
@ -6299,7 +6303,7 @@ fn m4_modeline_overrides_extension_end_to_end() {
#[test]
fn m4_modeline_parser_matches_supported_emacs_and_vim_forms() {
use pmacs::editor::EditorState;
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
let resolve = |text: &str| -> Option<String> {
s.lua_host
.lua()
@ -6344,7 +6348,7 @@ fn m4_modeline_parser_matches_supported_emacs_and_vim_forms() {
#[test]
fn m4_modeline_parser_enforces_boundaries_aliases_and_conflicts() {
use pmacs::editor::EditorState;
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
let resolve = |text: &str| -> Option<String> {
s.lua_host
.lua()
@ -6428,7 +6432,7 @@ fn m4_modeline_parser_enforces_boundaries_aliases_and_conflicts() {
#[test]
fn m4_modeline_unknown_mode_is_quiet_and_parser_free() {
use pmacs::editor::EditorState;
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
let dir = tempfile::tempdir().expect("tempdir");
let file = dir.path().join("notes.txt");
std::fs::write(&file, b"# vim:ft=prose:\nhello\n").expect("write");
@ -6464,7 +6468,7 @@ fn m4_modeline_unknown_mode_is_quiet_and_parser_free() {
#[test]
fn m4_modeline_language_is_pinned_until_reopen() {
use pmacs::editor::EditorState;
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
let dir = tempfile::tempdir().expect("tempdir");
let file = dir.path().join("mutable.txt");
let other = dir.path().join("other.txt");
@ -6558,7 +6562,7 @@ fn m4_modeline_language_is_pinned_until_reopen() {
#[test]
fn m4_modeline_shared_resolver_preserves_pathless_lsp_guard() {
use pmacs::editor::EditorState;
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
let (syntax, lsp): (Option<String>, Option<String>) = s
.lua_host
.lua()
@ -6580,7 +6584,7 @@ fn m4_modeline_shared_resolver_preserves_pathless_lsp_guard() {
#[test]
fn m4_filename_map_resolves_special_files() {
use pmacs::editor::EditorState;
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
let probe: mlua::Table = s
.lua_host
.lua()
@ -6650,7 +6654,7 @@ fn m4_filename_map_resolves_special_files() {
#[test]
fn m4_filename_extensionless_dockerfile_highlights() {
use pmacs::editor::EditorState;
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
let dir = tempfile::tempdir().expect("tempdir");
let f = dir.path().join("Dockerfile"); // no extension
std::fs::write(&f, b"FROM alpine:3\nRUN apk add curl\n").expect("write");
@ -6690,7 +6694,7 @@ fn m4_filename_extensionless_dockerfile_highlights() {
#[test]
fn m4_gap_grammars_align_with_lsp_configs() {
use pmacs::editor::EditorState;
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
for (path, id) in [
("app.py", "python"),
("srv.go", "go"),
@ -6733,7 +6737,7 @@ fn m4_gap_grammars_align_with_lsp_configs() {
#[test]
fn m4_json_yaml_lsp_configs_pin_command_and_sections() {
use pmacs::editor::EditorState;
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
let lua = s.lua_host.lua();
// json: the `@t1ckbase/vscode-langservers-extracted@2.0.2` binary
@ -6809,7 +6813,7 @@ fn m4_json_yaml_lsp_configs_pin_command_and_sections() {
#[test]
fn m4_5_initial_config_pushed_via_did_change_configuration() {
use pmacs::editor::EditorState;
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
let dir = tempfile::tempdir().expect("tempdir");
let sink = dir.path().join("config.jsonl");
@ -6895,7 +6899,7 @@ fn m4_real_json_provider_receives_config_and_reports_diagnostics() {
let file_disp = file.display().to_string();
let uri = format!("file://{file_disp}");
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
state
.lua_host
.lua()
@ -6964,7 +6968,7 @@ fn m4_real_yaml_provider_pulls_config_and_reports_diagnostics() {
let file_disp = file.display().to_string();
let uri = format!("file://{file_disp}");
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
state
.lua_host
.lua()
@ -7043,7 +7047,7 @@ fn m4_real_yaml_provider_pulls_config_and_reports_diagnostics() {
fn m4_lua_bundle_debounces_did_change_per_keystroke() {
use pmacs::editor::EditorState;
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
let dir = tempfile::TempDir::new().unwrap();
let file = dir.path().join("debounce.rs");
@ -7144,12 +7148,12 @@ fn m4_lua_bundle_debounces_did_change_per_keystroke() {
fn m4_12_default_bundle_after_load_robust_to_missing_server() {
use pmacs::editor::EditorState;
let _dir = tempfile::TempDir::new().unwrap();
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
// Plain after-load with no buffer path attached — the hook should
// run, find no language for path=nil, and exit silently. If it
// throws, the *errors* buffer would record it; verify it doesn't.
s.lua_host.hooks();
let s2 = EditorState::new();
let s2 = EditorState::new_with_roots(&crate::iso::roots());
drop(s);
let saw_error: bool = s2
.lua_host
@ -7199,7 +7203,7 @@ fn project_set_search_boundary_clamps_lua_detect_call() {
let f = workspace.join("src/main.rs");
std::fs::write(&f, b"").expect("touch file");
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
let lua = state.lua_host.lua();
let workspace_str = workspace.display().to_string();
let f_str = f.display().to_string();
@ -7234,7 +7238,7 @@ fn project_search_boundary_round_trips_via_lua() {
let dir = tempfile::tempdir().expect("dir");
let dir_str = dir.path().display().to_string();
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
let lua = state.lua_host.lua();
let (initial, after_set, after_clear): (Option<String>, Option<String>, Option<String>) = lua
@ -7420,7 +7424,7 @@ fn spawn_lsp_and_init(state: &mut pmacs::editor::EditorState, mode: Option<&str>
#[test]
fn m4_5_await_completion_returns_result_and_populates_store() {
use pmacs::editor::EditorState;
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
spawn_lsp_and_init(&mut state, None);
state
.lua_host
@ -7456,7 +7460,7 @@ fn m4_5_await_completion_returns_result_and_populates_store() {
#[test]
fn m4_5_await_server_error_raises_failed() {
use pmacs::editor::EditorState;
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
spawn_lsp_and_init(&mut state, Some("error"));
state
.lua_host
@ -7496,7 +7500,7 @@ fn m4_5_await_server_error_raises_failed() {
#[test]
fn m4_5_await_cancelled_when_server_stops_mid_request() {
use pmacs::editor::EditorState;
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
spawn_lsp_and_init(&mut state, Some("silent"));
state
.lua_host
@ -7534,7 +7538,7 @@ fn m4_5_await_cancelled_when_server_stops_mid_request() {
#[test]
fn m4_5_await_times_out_against_silent_server() {
use pmacs::editor::EditorState;
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
spawn_lsp_and_init(&mut state, Some("silent"));
state
.lua_host
@ -7576,7 +7580,7 @@ fn m4_5_await_times_out_against_silent_server() {
#[test]
fn m4_5_await_superseded_request_is_cancelled() {
use pmacs::editor::EditorState;
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
spawn_lsp_and_init(&mut state, Some("silent"));
state
.lua_host
@ -7622,7 +7626,7 @@ fn m4_5_await_superseded_request_is_cancelled() {
#[test]
fn m4_5_await_resolves_same_frame_as_response_absorbed() {
use pmacs::editor::EditorState;
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
spawn_lsp_and_init(&mut state, None);
state
.lua_host
@ -7710,7 +7714,7 @@ fn m4_5_await_resolves_same_frame_as_response_absorbed() {
#[test]
fn m4_5_position_encoding_utf16_round_trips_non_ascii() {
use pmacs::editor::EditorState;
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
spawn_lsp_and_init(&mut state, Some("posecho"));
state
.lua_host
@ -7766,7 +7770,7 @@ fn m4_5_position_encoding_utf16_round_trips_non_ascii() {
fn m4_5_utf16_rename_and_prepare_rename_convert_positions() {
use pmacs::editor::EditorState;
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
spawn_lsp_and_init(&mut state, Some("posecho"));
let uri = "file:///tmp/m4_5_rename_utf16.rs";
state
@ -7806,7 +7810,7 @@ fn m4_5_utf16_rename_and_prepare_rename_convert_positions() {
#[test]
fn m4_5_workspace_configuration_answered_from_settings() {
use pmacs::editor::EditorState;
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
state
.lua_host
@ -7863,7 +7867,7 @@ fn m4_5_workspace_configuration_answered_from_settings() {
#[test]
fn m4_5_location_nav_requests_route_by_kind() {
use pmacs::editor::EditorState;
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
spawn_lsp_and_init(&mut state, None);
state
.lua_host
@ -7913,7 +7917,7 @@ fn m4_5_location_nav_requests_route_by_kind() {
#[test]
fn m4_5_symbols_and_highlight_round_trip() {
use pmacs::editor::EditorState;
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
spawn_lsp_and_init(&mut state, None);
state
.lua_host
@ -7977,7 +7981,7 @@ fn m4_5_symbols_and_highlight_round_trip() {
/// Open `path` against the fake server and wait for initialization
/// (shared bootstrap for the panel tests).
fn open_against_fake(path: &std::path::Path) -> pmacs::editor::EditorState {
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_lsp_path();
state
.lua_host
@ -8223,7 +8227,7 @@ fn rd1_delete_refuses_when_a_bound_buffer_is_modified() {
let f = dir.path().join("a.rs");
std::fs::write(&f, b"original\n").expect("write");
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
rd_open(&mut state, "B", &f);
state
.lua_host
@ -8265,7 +8269,7 @@ fn rd2_delete_still_succeeds_for_a_clean_open_buffer() {
let f = dir.path().join("clean.rs");
std::fs::write(&f, b"untouched\n").expect("write");
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
rd_open(&mut state, "B", &f);
let (ok, err) = rd_delete(&mut state, &f, "");
@ -8310,7 +8314,7 @@ fn rd3_filesystem_failure_leaves_the_clean_buffer_intact() {
let target = dir.path().join("target");
std::fs::write(&target, b"was a file\n").expect("write");
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
rd_open(&mut state, "B", &target);
// The path changes type behind pmacs's back — the same class of
@ -8354,7 +8358,7 @@ fn rd4_on_removed_observes_the_path_already_gone() {
std::fs::write(&f, b"bye\n").expect("write");
let p = f.display().to_string();
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
rd_open(&mut state, "B", &f);
state
.lua_host
@ -8399,7 +8403,7 @@ fn rd5_delete_from_inside_the_targets_own_intercept_refuses_before_disk() {
std::fs::write(&f, b"busy\n").expect("write");
let p = f.display().to_string();
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
rd_open(&mut state, "B", &f);
state
.lua_host
@ -8455,7 +8459,7 @@ fn rd6_a_clean_first_match_cannot_hide_a_modified_duplicate() {
std::fs::write(&f, b"shared\n").expect("write");
let p = f.display().to_string();
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
state
.lua_host
.lua()
@ -8493,7 +8497,7 @@ fn rd7_a_sibling_directory_sharing_a_name_prefix_does_not_block() {
let outside = sibling.join("out.rs");
std::fs::write(&outside, b"out\n").expect("write out");
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
rd_open(&mut state, "B", &outside);
state
.lua_host
@ -8527,7 +8531,7 @@ fn rd8_recursive_delete_refuses_for_a_modified_descendant() {
let inner = nested.join("deep.rs");
std::fs::write(&inner, b"deep\n").expect("write");
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
rd_open(&mut state, "B", &inner);
state
.lua_host
@ -8575,7 +8579,7 @@ fn rd9_clean_recursive_delete_reconciles_descendants_through_both_phases() {
let inner = tree.join("kept.rs");
std::fs::write(&inner, b"kept\n").expect("write");
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
rd_open(&mut state, "B", &inner);
// Display it, so the phase-1 window redirect has something to do.
state
@ -8629,7 +8633,7 @@ fn rd10_absent_plus_ignore_does_not_destroy_a_modified_buffer() {
let f = dir.path().join("vanished.rs");
std::fs::write(&f, b"content\n").expect("write");
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
rd_open(&mut state, "B", &f);
state
.lua_host
@ -8684,7 +8688,7 @@ fn rd14_clean_duplicates_all_reconcile() {
std::fs::write(&f, b"twin\n").expect("write");
let p = f.display().to_string();
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
state
.lua_host
.lua()
@ -8940,7 +8944,7 @@ fn rd11_absent_plus_ignore_succeeds_through_the_server_pump() {
let victim = dir.path().join("victim.rs");
std::fs::write(&victim, b"content\n").expect("write");
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
let plan = serde_json::json!({
"documentChanges": [
{ "kind": "delete", "uri": rd_uri(&victim),
@ -8991,7 +8995,7 @@ fn rd11a_present_plus_ignore_with_a_modified_buffer_is_refused() {
let victim = dir.path().join("victim.rs");
std::fs::write(&victim, b"content\n").expect("write");
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
let plan = serde_json::json!({
"documentChanges": [
{ "kind": "delete", "uri": rd_uri(&victim),
@ -9049,7 +9053,7 @@ fn rd11b_a_dangling_symlink_counts_as_present() {
std::fs::write(&real, b"content\n").expect("write");
std::os::unix::fs::symlink(&real, &link).expect("symlink");
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
let plan = serde_json::json!({
"documentChanges": [
{ "kind": "delete", "uri": rd_uri(&link),
@ -9113,7 +9117,7 @@ fn rd11c_absent_without_ignore_refuses_before_the_earlier_op_runs() {
let missing = dir.path().join("never-existed.rs");
std::fs::write(&a, b"abcfooxyz\n").expect("write a");
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
let plan = serde_json::json!({
"documentChanges": [
rd_edit_op(&a, 0, 3, 6, "EDITED"),
@ -9167,7 +9171,7 @@ fn rd11d_an_unanswerable_stat_fails_closed_in_the_plan() {
std::fs::write(&not_a_dir, b"I am a file\n").expect("write the would-be parent");
let target = not_a_dir.join("child.rs");
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
let plan = serde_json::json!({
"documentChanges": [
rd_edit_op(&a, 0, 3, 6, "EDITED"),
@ -9223,7 +9227,7 @@ fn rd12a_edit_then_delete_answers_the_server_and_reports_partial_work() {
let a = dir.path().join("a.rs");
std::fs::write(&a, b"abcfooxyz\n").expect("write a");
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
let plan = serde_json::json!({
"documentChanges": [
rd_edit_op(&a, 0, 3, 6, "EDITED"),
@ -9276,7 +9280,7 @@ fn rd12b_rename_into_delete_answers_the_server_and_reports_partial_work() {
let inside = tree.join("m.rs");
std::fs::write(&outside, b"content\n").expect("write");
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
let plan = serde_json::json!({
"documentChanges": [
{ "kind": "rename", "oldUri": rd_uri(&outside), "newUri": rd_uri(&inside) },
@ -9341,7 +9345,7 @@ fn rd13_the_refusal_answers_the_server_and_leaves_a_durable_trace() {
let victim = dir.path().join("victim.rs");
std::fs::write(&victim, b"content\n").expect("write");
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
let plan = serde_json::json!({
"documentChanges": [ { "kind": "delete", "uri": rd_uri(&victim) } ]
});
@ -9411,7 +9415,7 @@ fn rd15_defensive_a_parse_failure_still_attempts_a_response() {
let a = dir.path().join("a.rs");
std::fs::write(&a, b"abcfooxyz\n").expect("write a");
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
// A plan that would otherwise succeed, so a response saying
// `applied = false` can only have come from the parse stub.
let plan = serde_json::json!({
@ -9476,7 +9480,7 @@ fn rd18_non_recursive_delete_is_not_blocked_by_an_orphan_beneath_it() {
let gone = tree.join("gone.rs");
std::fs::write(&gone, b"content\n").expect("write");
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
rd_open(&mut state, "B", &gone);
state
.lua_host
@ -9521,7 +9525,7 @@ fn rd19a_create_then_delete_is_not_refused_by_the_plan_time_preflight() {
let transient = dir.path().join("transient.rs");
let witness = dir.path().join("witness.rs");
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
let plan = serde_json::json!({
"documentChanges": [
{ "kind": "create", "uri": rd_uri(&transient) },
@ -9563,7 +9567,7 @@ fn rd19b_rename_then_delete_is_not_refused_by_the_plan_time_preflight() {
let destination = dir.path().join("destination.rs");
std::fs::write(&source, b"moving\n").expect("write source");
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
let plan = serde_json::json!({
"documentChanges": [
{ "kind": "rename", "oldUri": rd_uri(&source), "newUri": rd_uri(&destination) },
@ -9607,7 +9611,7 @@ fn rd19c_deferring_the_check_does_not_skip_it() {
std::fs::write(&a, b"abcfooxyz\n").expect("write a");
let fresh = dir.path().join("fresh.rs");
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
let plan = serde_json::json!({
"documentChanges": [
{ "kind": "create", "uri": rd_uri(&fresh) },
@ -9671,7 +9675,7 @@ fn rd20_the_user_facing_message_reports_partial_application() {
let a = dir.path().join("a.rs");
std::fs::write(&a, b"abcfooxyz\n").expect("write a");
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let plan = serde_json::json!({
"documentChanges": [
rd_edit_op(&a, 0, 3, 6, "EDITED"),
@ -9753,7 +9757,7 @@ fn rd21_equivalent_dot_path_create_then_delete_is_not_preflight_refused() {
"fixture: the URI spellings must differ"
);
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
let plan = serde_json::json!({
"documentChanges": [
{ "kind": "create", "uri": dot_uri },
@ -9792,7 +9796,7 @@ fn rd22a_partial_edits_inside_one_item_are_reported_conservatively() {
let target = dir.path().join("target.rs");
std::fs::write(&target, b"abcdef\n").expect("write target");
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
let plan = serde_json::json!({
"documentChanges": [{
"textDocument": { "uri": rd_uri(&target), "version": 1 },
@ -9877,7 +9881,7 @@ fn rd22b_a_failing_resource_item_can_leave_filesystem_state() {
"fixture: destination parent must start absent"
);
let mut state = pmacs::editor::EditorState::new();
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
let plan = serde_json::json!({
"documentChanges": [{
"kind": "rename",
@ -9907,3 +9911,10 @@ fn rd22b_a_failing_resource_item_can_leave_filesystem_state() {
"the response must not deny the directory left on disk: {reason:?}"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -253,6 +253,10 @@ fn second_daemon_same_socket_fails_clearly() {
.arg(daemon_a.socket_path())
.env("HOME", isolated_home)
.env("XDG_CONFIG_HOME", isolated_home)
.env("XDG_DATA_HOME", isolated_home)
.env("XDG_STATE_HOME", isolated_home)
.env("PMACS_STATE_HOME", isolated_home)
.env("XDG_CACHE_HOME", isolated_home)
.stdout(Stdio::null())
.stderr(Stdio::piped())
.output()

View File

@ -139,6 +139,10 @@ fn spawn_pmacs_daemon(socket_path: &Path) -> Child {
.arg(socket_path)
.env("HOME", isolated_home)
.env("XDG_CONFIG_HOME", isolated_home)
.env("XDG_DATA_HOME", isolated_home)
.env("XDG_STATE_HOME", isolated_home)
.env("PMACS_STATE_HOME", isolated_home)
.env("XDG_CACHE_HOME", isolated_home)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
@ -270,6 +274,10 @@ fn cli_attach_invokes_test_ssh_with_expected_argv() {
.env(PMACS_TEST_SSH_BIN, &fake_ssh)
.env("HOME", isolated_home)
.env("XDG_CONFIG_HOME", isolated_home)
.env("XDG_DATA_HOME", isolated_home)
.env("XDG_STATE_HOME", isolated_home)
.env("PMACS_STATE_HOME", isolated_home)
.env("XDG_CACHE_HOME", isolated_home)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
@ -323,6 +331,10 @@ fn cli_attach_with_user_and_instance_name_passes_through_dash_l_and_dash_dash_so
.env(PMACS_TEST_SSH_BIN, &fake_ssh)
.env("HOME", isolated_home)
.env("XDG_CONFIG_HOME", isolated_home)
.env("XDG_DATA_HOME", isolated_home)
.env("XDG_STATE_HOME", isolated_home)
.env("PMACS_STATE_HOME", isolated_home)
.env("XDG_CACHE_HOME", isolated_home)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
@ -361,6 +373,10 @@ fn spawn_bridge(socket: &Path, isolated_home: &Path) -> (Child, ChildStdin, Chil
.arg(socket)
.env("HOME", isolated_home)
.env("XDG_CONFIG_HOME", isolated_home)
.env("XDG_DATA_HOME", isolated_home)
.env("XDG_STATE_HOME", isolated_home)
.env("PMACS_STATE_HOME", isolated_home)
.env("XDG_CACHE_HOME", isolated_home)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
@ -466,6 +482,10 @@ fn cli_attach_with_missing_ssh_binary_surfaces_spawn_failure() {
.env(PMACS_TEST_SSH_BIN, &missing_ssh)
.env("HOME", isolated_home)
.env("XDG_CONFIG_HOME", isolated_home)
.env("XDG_DATA_HOME", isolated_home)
.env("XDG_STATE_HOME", isolated_home)
.env("PMACS_STATE_HOME", isolated_home)
.env("XDG_CACHE_HOME", isolated_home)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())

View File

@ -151,6 +151,10 @@ fn handshake_retry_cap_fires_after_three_failed_handshakes() {
.env(PMACS_TEST_BACKOFF_SCALE_MS, "1")
.env("HOME", isolated_home)
.env("XDG_CONFIG_HOME", isolated_home)
.env("XDG_DATA_HOME", isolated_home)
.env("XDG_STATE_HOME", isolated_home)
.env("PMACS_STATE_HOME", isolated_home)
.env("XDG_CACHE_HOME", isolated_home)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
@ -198,6 +202,10 @@ fn backoff_scaling_observable_in_wall_clock_runtime() {
.env(PMACS_TEST_BACKOFF_SCALE_MS, scale_ms)
.env("HOME", isolated_home)
.env("XDG_CONFIG_HOME", isolated_home)
.env("XDG_DATA_HOME", isolated_home)
.env("XDG_STATE_HOME", isolated_home)
.env("PMACS_STATE_HOME", isolated_home)
.env("XDG_CACHE_HOME", isolated_home)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
@ -278,6 +286,10 @@ fn ssh_stderr_from_handshake_attempts_reaches_user() {
.env(PMACS_TEST_BACKOFF_SCALE_MS, "1")
.env("HOME", isolated_home)
.env("XDG_CONFIG_HOME", isolated_home)
.env("XDG_DATA_HOME", isolated_home)
.env("XDG_STATE_HOME", isolated_home)
.env("PMACS_STATE_HOME", isolated_home)
.env("XDG_CACHE_HOME", isolated_home)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())

View File

@ -98,6 +98,10 @@ impl TestDaemon {
.arg(&socket_path)
.env("HOME", tempdir.path())
.env("XDG_CONFIG_HOME", tempdir.path())
.env("XDG_DATA_HOME", tempdir.path())
.env("XDG_STATE_HOME", tempdir.path())
.env("PMACS_STATE_HOME", tempdir.path())
.env("XDG_CACHE_HOME", tempdir.path())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()

View File

@ -43,7 +43,7 @@ use pmacs::lua_bindings::BufferIdLua;
/// translate them into typed Rust errors because the chunk's
/// assertions are the test contract.
fn run(chunk: &str) {
let mut editor = EditorState::new();
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
editor
.lua_host
.eval(Some("@m6_4_test"), chunk)
@ -53,7 +53,7 @@ fn run(chunk: &str) {
/// Construct a fresh editor and return a captured value (for tests
/// that want to do final assertions on the Rust side).
fn run_returning<T: mlua::FromLuaMulti>(chunk: &str) -> T {
let mut editor = EditorState::new();
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
editor
.lua_host
.eval(Some("@m6_4_test"), chunk)
@ -438,7 +438,7 @@ fn m6_4_buffer_id_is_a_real_buffer_handle() {
// The handle's `buffer_id()` returns a real BufferIdLua that
// resolves through the registry. This locks in that the package
// is built atop genuinely-public surface; no shadow APIs.
let mut editor = EditorState::new();
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
let returned: mlua::Value = editor
.lua_host
.eval(
@ -457,3 +457,10 @@ fn m6_4_buffer_id_is_a_real_buffer_handle() {
let buf = r.get(id_lua.id()).expect("registered");
assert_eq!(buf.name(), "*handle*");
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -80,7 +80,7 @@ fn locate_shell(name: &str) -> Option<PathBuf> {
/// Construct a fresh editor and run the given Lua chunk against it.
fn run(chunk: &str) {
let _guard = pump_test_guard();
let mut editor = EditorState::new();
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
editor
.lua_host
.eval(Some("@m6_5_test"), chunk)
@ -95,7 +95,7 @@ fn run(chunk: &str) {
/// after-tick contract is exercised end-to-end.
fn run_with_pump(setup_chunk: &str, predicate_chunk: &str, timeout_ms: u64) {
let _guard = pump_test_guard();
let mut editor = EditorState::new();
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
editor
.lua_host
.eval(Some("@m6_5_setup"), setup_chunk)
@ -528,3 +528,10 @@ fn m6_5_close_terminates_child_and_unregisters() {
pmacs.hook.run("process.after-tick")
"#);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -31,7 +31,7 @@ use pmacs::editor::EditorState;
// ---------------------------------------------------------------------------
fn run(chunk: &str) {
let mut editor = EditorState::new();
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
editor
.lua_host
.eval(Some("@m6_7_test"), chunk)
@ -341,3 +341,10 @@ fn m6_7_50_truncation_events_preserve_block_boundaries() {
pmacs.repl.config.scrollback_bytes = 16 * 1024 * 1024
"#);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -180,7 +180,7 @@ fn m6_8_three_repls_render_independently() {
let Some(lua) = locate_lua() else {
return;
};
let mut editor = EditorState::new();
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
spawn_three_and_wait_running(&mut editor, &lua);
// Write a unique marker through each REPL: `io.write("MARK_<n>\n")`
@ -246,7 +246,7 @@ fn m6_8_three_repls_respond_independently() {
let Some(lua) = locate_lua() else {
return;
};
let mut editor = EditorState::new();
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
spawn_three_and_wait_running(&mut editor, &lua);
// Type into h1, switch to h2, type into h2, etc. Each typed marker
@ -308,7 +308,7 @@ fn m6_8_close_one_does_not_affect_others() {
let Some(lua) = locate_lua() else {
return;
};
let mut editor = EditorState::new();
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
spawn_three_and_wait_running(&mut editor, &lua);
// Capture h2's proc_id before close; Handle:close clears the
@ -375,7 +375,7 @@ fn m6_8_supervisor_reaps_all_children_across_cycles() {
let Some(lua) = locate_lua() else {
return;
};
let mut editor = EditorState::new();
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
// Baseline: list size before any spawning. The post-cycle list
// size must equal this — no REPL processes left behind.
@ -480,7 +480,7 @@ fn m6_8_supervisor_reaps_all_children_across_cycles() {
/// `pmacs.repl.create` (which the spawn path also calls).
#[test]
fn m6_8_repls_have_independent_parser_state() {
let mut editor = EditorState::new();
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
editor
.lua_host
.eval(
@ -505,7 +505,7 @@ fn m6_8_repls_have_independent_parser_state() {
/// accidentally shared.
#[test]
fn m6_8_repls_have_independent_scrollback_state() {
let mut editor = EditorState::new();
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
editor
.lua_host
.eval(
@ -535,7 +535,7 @@ fn m6_8_buffer_scoped_bindings_route_to_active_buffer() {
let Some(lua) = locate_lua() else {
return;
};
let mut editor = EditorState::new();
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
let setup = format!(
r#"
_G.h1 = pmacs.repl.spawn {{ argv = {{ "{lua}", "-i" }} }}
@ -618,7 +618,7 @@ fn m6_8_after_tick_hook_drains_all_handles_per_tick() {
let Some(lua) = locate_lua() else {
return;
};
let mut editor = EditorState::new();
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
spawn_three_and_wait_running(&mut editor, &lua);
// Capture pre-tick history_end on each handle.
@ -692,3 +692,10 @@ fn m6_8_after_tick_hook_drains_all_handles_per_tick() {
.exec()
.expect("teardown");
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -278,7 +278,7 @@ fn m6_6_sustained_ingest_rate_meets_100mbps_gate() {
)
.max(1);
let mut editor = EditorState::new();
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
// 100 chars + newline per line. The exact value is unimportant;
// what matters is that `yes` blasts at a higher rate than pmacs's
// ingest path, so pmacs is the bottleneck under measurement.
@ -389,7 +389,7 @@ fn m6_6_buffer_memory_stays_under_200mb_during_run() {
baseline_rss as f64 / (1024.0 * 1024.0)
);
let mut editor = EditorState::new();
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
// 100-char line; the line content is unimportant for the ceiling
// gate. Total target = 150 MB of bytes-into-history.
let line = "a".repeat(100);
@ -526,7 +526,7 @@ fn m6_6_cancel_response_p99_under_100ms() {
let delay_ms = MIN_DELAY_MS + (r % (max_delay_ms - MIN_DELAY_MS + 1));
let delay = Duration::from_millis(delay_ms);
let mut editor = EditorState::new();
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
let _ = spawn_repl(&mut editor, &["yes"]);
wait_until_running(&mut editor);
@ -670,7 +670,7 @@ fn m6_7_scrollback_navigation_p99_under_16ms() {
const TRIALS: usize = 1000;
const P99_THRESHOLD: Duration = Duration::from_millis(16);
let mut editor = EditorState::new();
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
populate_scrollback(&mut editor, LINES);
seek_cursor_to_middle(&mut editor, LINES);
@ -749,7 +749,7 @@ fn m6_7_scrollback_search_p99_under_100ms() {
const TRIALS: usize = 1000;
const P99_THRESHOLD: Duration = Duration::from_millis(100);
let mut editor = EditorState::new();
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
populate_scrollback(&mut editor, LINES);
// Warmup: 100 searches at varied positions. Same trace-compilation
@ -815,3 +815,10 @@ fn m6_7_scrollback_search_p99_under_100ms() {
p50={p50:?}, p90={p90:?}, max={max:?}"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -89,14 +89,14 @@ fn repl_manifest_declares_exports_and_pmacs_required() {
// Bullet 2b: bootstrap loads through the package system
// ---------------------------------------------------------------------------
//
// EditorState::new() runs the M7.11 bootstrap that materializes
// `EditorState::new()` runs the M7.11 bootstrap that materializes
// the bundled REPL and pushes its InstalledPackage record into
// the roster. After that, `require("repl")` from Lua resolves
// through the M7.7 searcher (not the legacy direct eval).
#[test]
fn editor_init_makes_repl_loadable_via_require() {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
let result: bool = state
.lua_host
.lua()
@ -202,3 +202,10 @@ fn materialize_all_produces_one_record_per_bundled_package() {
assert!(repl.entry_path().exists());
let _ = std::fs::remove_dir_all(&tmp);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -45,7 +45,7 @@ fn outline_package_path() -> PathBuf {
fn editor_with_outline() -> (EditorState, TempDir, TempDir) {
let cache = tempfile::tempdir().expect("cache tempdir");
let user_root = tempfile::tempdir().expect("user-root tempdir");
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
state.lua_host.reopen_init_phase_for_testing();
state.lua_host.set_package_install_override(
PackageInstallOverride::new()
@ -1262,3 +1262,10 @@ fn outline_aggregate_empty_sources_rejected() {
"error must mention non-empty; got: {msg}"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -36,7 +36,7 @@ fn pump_until<F: Fn(&EditorState) -> bool>(state: &mut EditorState, predicate: F
/// Spin up a fresh editor with no file open. The caller drives the
/// fs API entirely from Lua chunks via `lua_host.eval`.
fn fresh_editor() -> EditorState {
EditorState::new()
EditorState::new_with_roots(&crate::iso::roots())
}
/// Run a Lua chunk to completion (no `:await`); returns the chunk's
@ -609,3 +609,10 @@ fn fs_watch_reports_file_change_and_can_cancel() {
.expect("cancelled");
assert!(cancelled, "watch handle must report cancellation");
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -80,7 +80,7 @@ fn pump_until<F: Fn(&EditorState) -> bool>(state: &mut EditorState, predicate: F
fn editor_with_dired() -> (EditorState, TempDir, TempDir) {
let cache = tempfile::tempdir().expect("cache tempdir");
let user_root = tempfile::tempdir().expect("user-root tempdir");
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
state.lua_host.reopen_init_phase_for_testing();
state.lua_host.set_package_install_override(
PackageInstallOverride::new()
@ -1251,3 +1251,10 @@ fn dired_source_size_under_audit_ceiling() {
"M8.2 spec: dired source under 1500 lines; got {lines}"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -44,7 +44,7 @@ fn pump_until<F: Fn(&EditorState) -> bool>(state: &mut EditorState, predicate: F
fn editor_with_dired() -> (EditorState, TempDir, TempDir) {
let cache = tempfile::tempdir().expect("cache tempdir");
let user_root = tempfile::tempdir().expect("user-root tempdir");
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
state.lua_host.reopen_init_phase_for_testing();
state.lua_host.set_package_install_override(
PackageInstallOverride::new()
@ -2101,3 +2101,10 @@ fn dired_active_handle_is_cleared_when_active_buffer_removed() {
"active_handle must not return a stale handle after its buffer is removed"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -52,7 +52,7 @@ fn pump_until<F: Fn(&EditorState) -> bool>(state: &mut EditorState, predicate: F
fn editor_with_magit() -> (EditorState, TempDir, TempDir) {
let cache = tempfile::tempdir().expect("cache tempdir");
let user_root = tempfile::tempdir().expect("user-root tempdir");
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
state.lua_host.reopen_init_phase_for_testing();
state.lua_host.set_package_install_override(
PackageInstallOverride::new()
@ -913,3 +913,10 @@ fn magit_cursor_on_hidden_descendant_reseats_to_visible_ancestor() {
"cursor on hidden A1 must reseat to A's header (line 0)"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -64,7 +64,7 @@ fn pump_until_with_deadline<F: Fn(&EditorState) -> bool>(
fn editor_with_magit() -> (EditorState, TempDir, TempDir) {
let cache = tempfile::tempdir().expect("cache tempdir");
let user_root = tempfile::tempdir().expect("user-root tempdir");
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
state.lua_host.reopen_init_phase_for_testing();
state.lua_host.set_package_install_override(
PackageInstallOverride::new()
@ -653,3 +653,10 @@ fn magit_build_spec_section_ids_are_stable_canonical_set() {
.expect("build_spec ids");
assert_eq!(ids, "working,staged,log,branches,stashes");
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -62,7 +62,7 @@ fn pump_until_with_deadline<F: Fn(&EditorState) -> bool>(
fn editor_with_magit() -> (EditorState, TempDir, TempDir) {
let cache = tempfile::tempdir().expect("cache tempdir");
let user_root = tempfile::tempdir().expect("user-root tempdir");
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
state.lua_host.reopen_init_phase_for_testing();
state.lua_host.set_package_install_override(
PackageInstallOverride::new()
@ -1011,3 +1011,10 @@ fn magit_b_c_keybinding_dispatches_to_branch_create_prompt() {
.eval(Some("cancel"), "pmacs.minibuffer.cancel()")
.expect("cancel");
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -45,7 +45,7 @@ fn outline_package_path() -> PathBuf {
fn editor_with_outline() -> (EditorState, TempDir, TempDir) {
let cache = tempfile::tempdir().expect("cache tempdir");
let user_root = tempfile::tempdir().expect("user-root tempdir");
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
state.lua_host.reopen_init_phase_for_testing();
state.lua_host.set_package_install_override(
PackageInstallOverride::new()
@ -1090,3 +1090,10 @@ fn outline_parser_unit_parses_org_subset() {
.expect("tags");
assert_eq!(n_tags, 2);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -725,7 +725,7 @@ fn m9_1_oncrash_policy_does_not_restart_on_clean_exit() {
fn m9_1_lua_surface_drives_mcp_lifecycle() {
use pmacs::editor::EditorState;
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_mcp_path();
let lua = state.lua_host.lua();
let sid_raw: u64 = lua
@ -815,7 +815,7 @@ fn m9_1_lua_surface_drives_mcp_lifecycle() {
fn m9_1_lua_send_request_returns_awaitable_handle() {
use pmacs::editor::EditorState;
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_mcp_path();
let lua = state.lua_host.lua();
@ -924,3 +924,10 @@ fn m9_1_lua_send_request_returns_awaitable_handle() {
.load("pmacs.mcp.stop(_G._mcp_test_server)")
.exec();
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -587,7 +587,7 @@ fn m9_2_cancelled_sibling_wins_over_queued_response() {
fn m9_2_lua_read_resource_returns_awaitable_handle() {
use pmacs::editor::EditorState;
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_mcp_path();
state
@ -738,3 +738,10 @@ fn m9_2_lua_read_resource_returns_awaitable_handle() {
.load("pmacs.mcp.stop(_G._mcp_test_server)")
.exec();
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -332,7 +332,7 @@ fn m9_3_cancellation_reaches_server() {
fn m9_3_lua_invoke_tool_returns_awaitable_handle() {
use pmacs::editor::EditorState;
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_mcp_path();
state
@ -496,3 +496,10 @@ fn m9_3_lua_invoke_tool_returns_awaitable_handle() {
.load("pmacs.mcp.stop(_G._mcp_test_server)")
.exec();
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -317,7 +317,7 @@ fn m9_4_no_args_wire_shape_is_empty_object() {
fn m9_4_lua_get_prompt_returns_awaitable_handle() {
use pmacs::editor::EditorState;
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = fake_mcp_path();
state
@ -494,3 +494,10 @@ fn m9_4_lua_get_prompt_returns_awaitable_handle() {
.load("pmacs.mcp.stop(_G._mcp_test_server)")
.exec();
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -51,7 +51,7 @@ fn resources_package_path() -> PathBuf {
fn editor_with_resources() -> (EditorState, TempDir, TempDir) {
let cache = tempfile::tempdir().expect("cache tempdir");
let user_root = tempfile::tempdir().expect("user-root tempdir");
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
state.lua_host.reopen_init_phase_for_testing();
state.lua_host.set_package_install_override(
PackageInstallOverride::new()
@ -1275,3 +1275,10 @@ fn m9_5_stale_buffer_recovers_on_reopen_after_restart() {
"after re-open, the buffer must have fresh content; got {reopen_body:?}"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -48,7 +48,7 @@ fn tools_package_path() -> PathBuf {
fn editor_with_tools() -> (EditorState, TempDir, TempDir) {
let cache = tempfile::tempdir().expect("cache tempdir");
let user_root = tempfile::tempdir().expect("user-root tempdir");
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
state.lua_host.reopen_init_phase_for_testing();
state.lua_host.set_package_install_override(
PackageInstallOverride::new()
@ -1532,3 +1532,10 @@ fn m9_6_editor_describe_command_unknown_name_status_only() {
"*help* must not be created for an unknown-name describe-command call"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -74,7 +74,7 @@ fn prompts_package_path() -> PathBuf {
fn editor_with_prompts() -> (EditorState, TempDir, TempDir) {
let cache = tempfile::tempdir().expect("cache tempdir");
let user_root = tempfile::tempdir().expect("user-root tempdir");
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
state.lua_host.reopen_init_phase_for_testing();
state.lua_host.set_package_install_override(
PackageInstallOverride::new()
@ -1386,3 +1386,10 @@ fn m9_7_prompt_hash_includes_required_argument_order() {
"reordering required args must change the prompt hash so reconcile re-registers"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -56,7 +56,7 @@ fn prompts_package_path() -> PathBuf {
fn editor_with_ai() -> (EditorState, TempDir, TempDir) {
let cache = tempfile::tempdir().expect("cache tempdir");
let user_root = tempfile::tempdir().expect("user-root tempdir");
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
state.lua_host.reopen_init_phase_for_testing();
state.lua_host.set_package_install_override(
PackageInstallOverride::new()
@ -1122,3 +1122,10 @@ fn m9_8_configured_prompt_missing_on_server_surfaces_error() {
state.core.borrow().status
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -41,7 +41,7 @@ fn count_of(kinds: &[String], kind: &str) -> usize {
#[test]
fn switch_away_and_back_reattaches_syntax_overlay_exactly_once() {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
let path = open_probe_file(&s);
s.lua_host
.lua()
@ -86,7 +86,7 @@ fn switch_away_and_back_reattaches_syntax_overlay_exactly_once() {
#[test]
fn panel_quit_restores_overlays_on_the_source_buffer() {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
let path = open_probe_file(&s);
s.lua_host
.lua()
@ -121,3 +121,10 @@ fn panel_quit_restores_overlays_on_the_source_buffer() {
"leaving a panel restores the source buffer's styling (got {after:?})"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -26,7 +26,7 @@ fn editor_with_state_dir() -> (EditorState, PathBuf) {
SEQ.fetch_add(1, Ordering::Relaxed)
));
std::fs::create_dir_all(&dir).expect("mk state tempdir");
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
// Override whatever startup configured with our tempdir.
s.lua_host.lua().remove_app_data::<StateDir>();
s.lua_host.lua().set_app_data(StateDir(dir.clone()));
@ -73,12 +73,12 @@ fn state_round_trips_and_rejects_escapes() {
#[test]
fn state_is_inert_when_unconfigured() {
// A plain EditorState::new() must NOT configure a state dir — that
// A plain `EditorState::new()` must NOT configure a state dir — that
// is what keeps the whole integration-test suite (which links the
// lib without cfg(test)) from writing to a developer's real
// ~/.local/state/pmacs. Only the real entry points call
// install_state_dirs(); tests construct EditorState directly.
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
assert!(
s.lua_host.lua().app_data_ref::<StateDir>().is_none(),
"new() must leave the state dir unconfigured"
@ -182,3 +182,10 @@ fn saveplace_can_be_disabled() {
assert_eq!(cursor, 0, "disabled saveplace leaves the cursor at the top");
std::fs::remove_dir_all(&dir).ok();
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -85,7 +85,7 @@ fn start_query_replace(s: &mut EditorState, from: &str, to: &str, regex: bool) {
#[test]
fn replace_skip_and_quit_is_selective() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "x x x x");
// Cursor to buffer start so all four are ahead of point.
goto_start(&s);
@ -107,7 +107,7 @@ fn replace_skip_and_quit_is_selective() {
#[test]
fn bang_replaces_all_remaining() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "a a a a");
goto_start(&s);
start_query_replace(&mut s, "a", "b", false);
@ -119,7 +119,7 @@ fn bang_replaces_all_remaining() {
#[test]
fn dot_replaces_current_then_quits() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "a a a");
goto_start(&s);
start_query_replace(&mut s, "a", "z", false);
@ -133,7 +133,7 @@ fn dot_replaces_current_then_quits() {
fn growing_replacement_does_not_loop() {
// a → aa must not re-match the inserted text (offset-shift + the
// search-forward-past-replacement rule).
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "a a a");
goto_start(&s);
start_query_replace(&mut s, "a", "aa", false);
@ -144,7 +144,7 @@ fn growing_replacement_does_not_loop() {
#[test]
fn empty_to_deletes() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "a-b-c");
goto_start(&s);
start_query_replace(&mut s, "-", "", false);
@ -155,7 +155,7 @@ fn empty_to_deletes() {
#[test]
fn regex_query_replace_via_binding() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "a1 b2 c3");
goto_start(&s);
start_query_replace(&mut s, "[0-9]", "#", true);
@ -167,7 +167,7 @@ fn regex_query_replace_via_binding() {
#[test]
fn m_percent_binding_starts_query_replace() {
// The literal chord: M-% (Alt + Shift+5 → Char('%') with ALT).
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "cat cat");
goto_start(&s);
s.dispatch_key(
@ -196,7 +196,7 @@ fn m_percent_binding_starts_query_replace() {
fn c_m_percent_binding_starts_regexp_query_replace() {
// Control-meta-shifted punctuation — the chord most likely to parse
// differently across key paths (the C-c H lesson).
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "x1 x2");
goto_start(&s);
s.dispatch_key(
@ -224,7 +224,7 @@ fn c_m_percent_binding_starts_regexp_query_replace() {
#[test]
fn nothing_matched_leaves_buffer_untouched() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "hello");
start_query_replace(&mut s, "zzz", "q", false);
let (text, active) = probe(&s);
@ -236,7 +236,7 @@ fn nothing_matched_leaves_buffer_untouched() {
fn query_replace_flips_dispatch_idle_so_gpu_round_trips() {
// While the interactive phase runs, dispatch_idle must be false so a
// semantic frontend round-trips y/n/etc. instead of self-inserting.
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "a a");
goto_start(&s);
start_query_replace(&mut s, "a", "b", false);
@ -251,7 +251,7 @@ fn query_replace_flips_dispatch_idle_so_gpu_round_trips() {
#[test]
fn replace_fires_after_edit_hook() {
// The Q#QR1 hook: an LSP/syntax observer must see replaced text.
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
s.lua_host
.lua()
.load(
@ -289,7 +289,7 @@ fn bang_fires_after_edit_hook_once_for_the_batch() {
// Q#QR1: `!` applies many replacements under one keypress, but the
// debounced didChange wants a single after-edit — the shadow
// compares revision once across the whole handler.
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
s.lua_host
.lua()
.load(
@ -322,7 +322,7 @@ fn bang_fires_after_edit_hook_once_for_the_batch() {
fn quit_via_ret_and_esc_keeps_replacements() {
// Q#QR10: RET and Esc both quit (keeping replacements), not just q.
for quit in [KeyCode::Enter, KeyCode::Esc] {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "a a a");
goto_start(&s);
start_query_replace(&mut s, "a", "b", false);
@ -337,7 +337,7 @@ fn quit_via_ret_and_esc_keeps_replacements() {
#[test]
fn ctrl_g_quits_keeping_replacements() {
// Q#QR10: C-g exits and KEEPS replacements (unlike isearch's C-g).
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "a a a");
goto_start(&s);
start_query_replace(&mut s, "a", "b", false);
@ -353,7 +353,7 @@ fn ctrl_g_quits_keeping_replacements() {
#[test]
fn del_key_skips_like_n() {
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "a a a");
goto_start(&s);
start_query_replace(&mut s, "a", "b", false);
@ -373,7 +373,7 @@ fn focus_drift_mid_session_aborts_without_touching_either_buffer() {
// (simulated by switch_buffer, which the pointer path also uses)
// while query-replace is active. The next y must abort, not apply
// the origin-buffer match to the now-active unrelated buffer.
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
type_str(&mut s, "foo foo");
goto_start(&s);
start_query_replace(&mut s, "foo", "bar", false);
@ -416,3 +416,10 @@ fn focus_drift_mid_session_aborts_without_touching_either_buffer() {
"origin buffer untouched by the aborted replace"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -113,7 +113,7 @@ impl Fixture {
}
fn editor() -> EditorState {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
// No language server may spawn from these fixtures. The LSP rows
// that DO want one configure it explicitly.
exec(&state, "pmacs.lsp.config = {}");
@ -2043,3 +2043,10 @@ fn a_subscriber_reconciliation_failure_is_reported_and_the_rest_still_reconcile(
unreconciled the raise has to come after the loop, not inside it"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -57,7 +57,7 @@ fn save_refuses_to_clobber_a_file_changed_on_disk() {
write(&f, "original\n");
let fs = f.display().to_string();
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
open_and_dirty(&s, &fs);
// Another writer lands between our read and our save.
@ -88,7 +88,7 @@ fn the_save_command_does_not_fire_after_save_when_it_refuses() {
write(&f, "original\n");
let fs = f.display().to_string();
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
open_and_dirty(&s, &fs);
exec(
&s,
@ -110,7 +110,7 @@ fn save_anyway_overwrites_deliberately_and_resyncs_meta() {
write(&f, "original\n");
let fs = f.display().to_string();
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
open_and_dirty(&s, &fs);
write(&f, "theirs\n");
assert!(!eval::<bool>(&s, "return pmacs.editor.save()"));
@ -137,7 +137,7 @@ fn an_unchanged_file_saves_normally_and_repeatedly() {
write(&f, "original\n");
let fs = f.display().to_string();
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
open_and_dirty(&s, &fs);
assert!(eval::<bool>(&s, "return pmacs.editor.save()"));
assert_eq!(read(&f), "mine original\n");
@ -157,7 +157,7 @@ fn a_deleted_file_is_recreated_not_refused() {
write(&f, "original\n");
let fs = f.display().to_string();
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
open_and_dirty(&s, &fs);
// Nothing on disk to clobber, so recreating it is not data loss.
std::fs::remove_file(&f).unwrap();
@ -174,7 +174,7 @@ fn a_new_file_buffer_refuses_once_someone_else_creates_the_file() {
let dir = tempdir();
let missing = dir.join("draft.txt");
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
// The argv `[new file]` shape: a path with nothing on disk, no meta.
exec(
&s,
@ -200,3 +200,10 @@ fn a_new_file_buffer_refuses_once_someone_else_creates_the_file() {
assert_eq!(read(&missing), "my draft");
std::fs::remove_dir_all(&dir).ok();
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -29,7 +29,8 @@ use pmacs::statusline::{
StatuslineEvaluationOutcome, StatuslineEvaluationTarget, evaluate_statusline,
};
#[cfg(feature = "crdt")]
// Ungated: `common::iso` (isolated bootstrap roots) is needed in every
// build, not only the CRDT one.
mod common;
fn exec(state: &EditorState, source: &str) {
@ -41,7 +42,7 @@ fn eval<T: mlua::FromLuaMulti>(state: &EditorState, source: &str) -> T {
}
fn editor() -> EditorState {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
exec(&state, "pmacs.lsp.config = {}");
state
}
@ -689,7 +690,7 @@ fn a09_11_full_tui_frame_composes_unicode_clips_and_preserves_echo() {
// map and polls the real tracker without a buffer edit.
#[test]
fn a12_builtin_lsp_provider_tracks_real_attachment_and_unknown_label() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let fake = env!("CARGO_BIN_EXE_pmacs_fake_lsp");
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("statusline.rs");
@ -1048,3 +1049,11 @@ fn a16_26_real_daemon_v17_gate_v18_first_frame_and_late_join() {
"late join sees established state"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root. Re-exported rather than re-declared
// with `#[path]` — this file already pulls in `common`, and loading one
// source file as two modules is `clippy::duplicate_mod`.
use common::iso;

View File

@ -187,7 +187,7 @@ fn escape_was_armed(state: &mut EditorState, buffer: pmacs::buffer::BufferId, pr
/// Acceptance 1: a profile spec is strict, and rejects before anything spawns.
#[test]
fn acc1_profile_specs_are_strict_and_reject_before_spawning() {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
let before = state.core.borrow().registry.borrow().ids().len();
exec(
@ -218,7 +218,7 @@ fn acc1_profile_specs_are_strict_and_reject_before_spawning() {
/// Acceptance 2: an unknown profile names the known ones and creates nothing.
#[test]
fn acc2_unknown_profile_lists_known_names_and_creates_nothing() {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
exec(&state, CAT_PROFILE);
exec(
&state,
@ -262,7 +262,7 @@ fn acc2_unknown_profile_lists_known_names_and_creates_nothing() {
/// unknown-profile path, replacing the exact error being asked for.
#[test]
fn acc2_malformed_profile_keys_do_not_mask_the_unknown_profile_error() {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
exec(&state, CAT_PROFILE);
exec(
&state,
@ -297,7 +297,7 @@ fn acc2_malformed_profile_keys_do_not_mask_the_unknown_profile_error() {
/// `env` MERGES rather than replacing.
#[test]
fn acc3_field_resolution_order_and_env_merge() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
exec(
&state,
r#"
@ -323,7 +323,7 @@ fn acc3_field_resolution_order_and_env_merge() {
/// Acceptance 3 (explicit command wins) and 4 (`""` means no profile).
#[test]
fn acc3_acc4_explicit_command_wins_and_empty_default_means_no_profile() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
exec(&state, CAT_PROFILE);
exec(
&state,
@ -373,7 +373,7 @@ pmacs.terminal.profiles.fill = {
/// fallback) deleted outright.
#[test]
fn acc5_scrollback_setting_reaches_retained_history() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
exec(&state, FILL_PROFILE);
// Arm 1: `0` is legal, and means the early rows are GONE.
@ -423,7 +423,7 @@ fn acc5_scrollback_setting_reaches_retained_history() {
/// values, and `0` is inside it rather than a disabled sentinel.
#[test]
fn acc5_scrollback_bounds() {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
exec(&state, r#"pmacs.config.set("terminal.scrollback-rows", 0)"#);
assert_eq!(
state
@ -458,7 +458,7 @@ fn acc5_scrollback_bounds() {
/// THAT chord to the child, and an ordinary `C-c` still reaches the child.
#[test]
fn acc6_acc9_configured_escape_chord_and_literal_repeat() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
exec(&state, CAT_PROFILE);
let buffer = open_cat_terminal(&state, r#"profile = "echo""#);
assert!(tick_until(&mut state, "READY", buffer));
@ -499,7 +499,7 @@ fn acc6_acc9_configured_escape_chord_and_literal_repeat() {
/// count that does not grow, and a cache that dies with its terminal.
#[test]
fn acc7_acc8_acc8a_per_terminal_escape_cache_identity_and_lifecycle() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
exec(&state, CAT_PROFILE);
let a = open_cat_terminal(&state, r#"profile = "echo""#);
exec(&state, "TERM_A = TERM_BUF");
@ -633,7 +633,7 @@ fn acc7_acc8_acc8a_per_terminal_escape_cache_identity_and_lifecycle() {
/// the status line, and reports once per terminal per effective bad value.
#[test]
fn acc10_acc10a_invalid_escape_falls_back_and_reports_once() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
exec(&state, CAT_PROFILE);
let buffer = open_cat_terminal(&state, r#"profile = "echo""#);
assert!(tick_until(&mut state, "READY", buffer));
@ -699,7 +699,7 @@ fn acc10_acc10a_invalid_escape_falls_back_and_reports_once() {
/// proves the second half).
#[test]
fn acc11_terminal_opening_binding_is_bound_and_shadowed_nothing() {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
let command: Option<String> = state
.lua_host
.lua()
@ -717,7 +717,7 @@ fn acc11_terminal_opening_binding_is_bound_and_shadowed_nothing() {
/// defaults reproduce the pre-arc behavior.
#[test]
fn acc12_defaults_reproduce_prior_behavior() {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
let lua = state.lua_host.lua();
assert_eq!(
lua.load(r#"return pmacs.config.get("terminal.default-profile")"#)
@ -744,3 +744,10 @@ fn acc12_defaults_reproduce_prior_behavior() {
"no profiles are registered by default"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -223,7 +223,7 @@ fn buffer_count(state: &EditorState) -> usize {
/// `copy_selection_bytes` itself.
#[test]
fn acc13_snapshot_is_the_whole_retained_range_through_the_shared_serializer() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let terminal = open_fill_terminal(&mut state);
focus_terminal(&state, terminal);
@ -250,7 +250,7 @@ fn acc13_snapshot_is_the_whole_retained_range_through_the_shared_serializer() {
/// buffer-shaped consumer work and what removes the transport arm.
#[test]
fn acc14_the_snapshot_is_an_ordinary_non_terminal_buffer() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let terminal = open_fill_terminal(&mut state);
focus_terminal(&state, terminal);
exec(&state, "pmacs.terminal.copy_mode(TERM_BUF)");
@ -272,7 +272,7 @@ fn acc14_the_snapshot_is_an_ordinary_non_terminal_buffer() {
/// with no change to `src/search.rs` (B1).
#[test]
fn acc15_isearch_finds_content_only_in_scrollback() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let terminal = open_fill_terminal(&mut state);
focus_terminal(&state, terminal);
@ -319,7 +319,7 @@ fn acc15_isearch_finds_content_only_in_scrollback() {
/// agreement; what stops the mutation is this.
#[test]
fn acc16_dispatch_idle_is_false_while_the_snapshot_is_focused() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let terminal = open_fill_terminal(&mut state);
focus_terminal(&state, terminal);
exec(&state, "pmacs.terminal.copy_mode(TERM_BUF)");
@ -344,7 +344,7 @@ fn acc16_dispatch_idle_is_false_while_the_snapshot_is_focused() {
/// protection does not depend on which key or command was used.
#[test]
fn acc16b_the_snapshot_is_immutable_at_the_rope_not_merely_intercepted() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let terminal = open_fill_terminal(&mut state);
focus_terminal(&state, terminal);
exec(&state, "pmacs.terminal.copy_mode(TERM_BUF)");
@ -390,7 +390,7 @@ fn acc16b_the_snapshot_is_immutable_at_the_rope_not_merely_intercepted() {
/// leaves the buffer emptiable. Only rope-level `read_only` closes both.
#[test]
fn acc16c_undo_cannot_empty_the_snapshot_by_chord_or_by_command() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let terminal = open_fill_terminal(&mut state);
focus_terminal(&state, terminal);
exec(&state, "pmacs.terminal.copy_mode(TERM_BUF)");
@ -523,7 +523,7 @@ fn grid_row(cells: &[pmacs::cell::Cell], row: u32, cols: u32) -> String {
/// any other owner that adopts the primitive later.
#[test]
fn acc16d_a_generated_write_notifies_the_window_that_displays_it() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
exec(
&state,
r"
@ -567,7 +567,7 @@ fn acc16d_a_generated_write_notifies_the_window_that_displays_it() {
#[cfg(feature = "crdt")]
#[test]
fn acc16e_a_refresh_queues_the_owners_write_for_replica_mirrors() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let terminal = open_fill_terminal(&mut state);
focus_terminal(&state, terminal);
exec(&state, "pmacs.terminal.copy_mode(TERM_BUF)");
@ -606,7 +606,7 @@ fn acc16e_a_refresh_queues_the_owners_write_for_replica_mirrors() {
/// both directions.
#[test]
fn acc18_reinvoke_refreshes_in_place_and_lifecycle_runs_both_ways() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let terminal = open_fill_terminal(&mut state);
focus_terminal(&state, terminal);
@ -681,7 +681,7 @@ fn acc18_reinvoke_refreshes_in_place_and_lifecycle_runs_both_ways() {
/// `q` returns to the source terminal.
#[test]
fn acc19_escape_c_t_enters_copy_mode_and_g_and_q_work() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let terminal = open_fill_terminal(&mut state);
focus_terminal(&state, terminal);
let terminal_name = active_buffer_name(&state);
@ -749,7 +749,7 @@ fn acc19_escape_c_t_enters_copy_mode_and_g_and_q_work() {
/// tail.
#[test]
fn acc20_live_terminal_keys_are_unchanged_while_a_snapshot_exists() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let terminal = open_fill_terminal(&mut state);
let key = focus_terminal(&state, terminal);
exec(&state, "pmacs.terminal.copy_mode(TERM_BUF)");
@ -806,7 +806,7 @@ fn acc20_live_terminal_keys_are_unchanged_while_a_snapshot_exists() {
/// (or nothing) while the keys behaved differently.
#[test]
fn acc21_describe_key_reports_the_truth_for_the_snapshot_bindings() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let terminal = open_fill_terminal(&mut state);
focus_terminal(&state, terminal);
exec(&state, "pmacs.terminal.copy_mode(TERM_BUF)");
@ -847,7 +847,7 @@ fn acc21_describe_key_reports_the_truth_for_the_snapshot_bindings() {
/// (dired's F7 rule); a taken name gets a `<2>` variant instead.
#[test]
fn acc18a_a_foreign_same_named_buffer_is_never_adopted_or_clobbered() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let terminal = open_fill_terminal(&mut state);
focus_terminal(&state, terminal);
@ -898,7 +898,7 @@ fn acc18a_a_foreign_same_named_buffer_is_never_adopted_or_clobbered() {
/// terminal, and killing either one removes the shared snapshot.
#[test]
fn acc18b_two_same_named_terminals_get_two_independent_snapshots() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
exec(&state, FILL_PROFILE);
let before = terminal_buffers(&state);
@ -985,7 +985,7 @@ fn acc18b_two_same_named_terminals_get_two_independent_snapshots() {
/// snapshot of nothing.
#[test]
fn copy_mode_refuses_a_non_terminal_buffer() {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
let err = eval_err(&state, "return pmacs.terminal.copy_mode()");
assert!(
err.contains("not a terminal"),
@ -1010,7 +1010,7 @@ fn copy_mode_refuses_a_non_terminal_buffer() {
/// state. Falsify by deleting the `win.cursor > len` clamp.
#[test]
fn acc16f_a_shrinking_generated_write_clamps_the_window_cursor() {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
exec(
&state,
r"
@ -1066,7 +1066,7 @@ fn acc16f_a_shrinking_generated_write_clamps_the_window_cursor() {
/// the measurement.
#[test]
fn acc16g_a_line_collapsing_generated_write_clamps_view_top() {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
exec(
&state,
r"
@ -1129,7 +1129,7 @@ fn acc16g_a_line_collapsing_generated_write_clamps_view_top() {
/// `notify_buffer_edit`; the first copy reaches the stale-anchor panic.
#[test]
fn acc16h_a_shrinking_generated_write_clamps_or_clears_the_selection() {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
exec(
&state,
r"
@ -1224,7 +1224,7 @@ fn acc16h_a_shrinking_generated_write_clamps_or_clears_the_selection() {
/// `acc16h` stays green.
#[test]
fn acc16i_a_shrinking_view_rebuild_clamps_or_clears_the_selection() {
let state = EditorState::new();
let state = EditorState::new_with_roots(&crate::iso::roots());
// 286 bytes, then 154: a real shrink through the help renderer.
exec(
&state,
@ -1300,3 +1300,10 @@ fn acc16i_a_shrinking_view_rebuild_clamps_or_clears_the_selection() {
"the collapsed region is not retained as active-but-empty"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -20,7 +20,8 @@ use pmacs::protocol::{ByteRange, FrontendId, InstanceMessage, ThemeFace};
use pmacs::semantic_render::SemanticRenderState;
use std::time::{Duration, Instant};
#[cfg(feature = "crdt")]
// Ungated: `common::iso` (isolated bootstrap roots) is needed in every
// build, not only the CRDT one.
mod common;
// ---------------------------------------------------------------------------
@ -78,7 +79,7 @@ fn eval<T: mlua::FromLuaMulti>(s: &EditorState, src: &str) -> T {
/// Fresh editor with LSP spawning disabled.
fn editor() -> EditorState {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
exec(&s, "pmacs.lsp.config = {}");
s
}
@ -184,7 +185,7 @@ fn current_tree_language(state: &EditorState) -> Option<String> {
/// Open `path` and pump until its parse settles (highlights attached).
fn open_and_wait_for_parse(path: std::path::PathBuf) -> EditorState {
let mut state = EditorState::open(path).expect("open file");
let mut state = EditorState::open_with_roots(path, &crate::iso::roots()).expect("open file");
exec(&state, "pmacs.lsp.config = {}");
pump_async(&mut state, |s| current_tree_language(s).is_some());
state
@ -1511,3 +1512,11 @@ fn minibuffer_and_candidate_faces_apply_through_m_x() {
panic!("no candidate suffix rendered: {text:?}");
}
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root. Re-exported rather than re-declared
// with `#[path]` — this file already pulls in `common`, and loading one
// source file as two modules is `clippy::duplicate_mod`.
use common::iso;

View File

@ -78,7 +78,7 @@ fn status(s: &EditorState) -> String {
/// Fresh scratch-buffer editor, cursor at 0. Scratch pairing uses the
/// `default` set, so `(` pairs — which is what 46c reads.
fn editor_with(body: &str) -> EditorState {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
if !body.is_empty() {
exec(&s, &format!("pmacs.window.buffer():insert(0, {body:?})"));
}
@ -656,7 +656,7 @@ fn a_chain_consumers_edit_reaches_the_first_did_change() {
let sink_disp = sink.display().to_string();
let fake = fake_lsp_path();
let mut s = EditorState::new();
let mut s = EditorState::new_with_roots(&crate::iso::roots());
s.lua_host.lua().remove_app_data::<StateDir>();
s.lua_host.lua().set_app_data(StateDir(dir.clone()));
exec(&s, "pmacs.lsp.config = {}");
@ -733,3 +733,10 @@ fn a_chain_consumers_edit_reaches_the_first_did_change() {
before lsp.lua's synchronous flush (Q#AP7)"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -60,7 +60,7 @@ fn terminal_cells_reject_child_control_characters() {
#[test]
fn spawn_failure_is_transactional() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let buffers_before = state.core.borrow().registry.borrow().len();
let processes_before = state.process_supervisor.borrow().ids().count();
state.process_supervisor.borrow_mut().shutdown();
@ -77,7 +77,7 @@ fn spawn_failure_is_transactional() {
#[test]
fn strict_owned_spec_rejects_before_spawn_and_is_mutation_independent() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let buffers_before = state.core.borrow().registry.borrow().len();
let mut invalid = TerminalSpec::new("/bin/sh");
invalid.rows = 0;
@ -193,7 +193,7 @@ fn read_only_empty_crdt_bootstrap_is_immutable_against_remote_content() {
#[test]
fn final_output_precedes_exact_nonzero_annotation_and_buffer_is_retained() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let mut spec = TerminalSpec::new("/bin/sh");
spec.args = vec![
"-c".into(),
@ -306,7 +306,7 @@ fn normal_and_signal_annotations_use_exact_pid_and_outcome() {
"exited abnormally with signal SIGTERM",
),
] {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let mut spec = TerminalSpec::new("/bin/sh");
spec.args = vec!["-c".into(), script.into()];
spec.rows = 6;
@ -334,7 +334,7 @@ fn normal_and_signal_annotations_use_exact_pid_and_outcome() {
#[test]
fn killing_terminal_buffer_prunes_session_and_reaps_owned_process() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let mut spec = TerminalSpec::new("/bin/sh");
spec.args = vec!["-c".into(), "sleep 30".into()];
let buffer_id = state.open_terminal(spec).expect("open terminal");
@ -364,7 +364,7 @@ fn killing_terminal_buffer_prunes_session_and_reaps_owned_process() {
#[test]
fn editor_shutdown_kills_term_ignoring_terminal_child() {
let pid = {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
state
.process_supervisor
.borrow_mut()
@ -397,7 +397,7 @@ fn editor_shutdown_kills_term_ignoring_terminal_child() {
#[test]
fn terminal_tick_does_not_take_non_terminal_process_events() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let mut process = pmacs::process::ProcessSpec::new("ordinary", "/bin/sh");
process.args = vec!["-c".into(), "printf ordinary".into()];
let ordinary_id = state
@ -424,3 +424,10 @@ fn terminal_tick_does_not_take_non_terminal_process_events() {
"TerminalManager must not steal ordinary process output"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;

View File

@ -60,7 +60,7 @@ fn snapshot_text(snapshot: &pmacs::terminal::TerminalSnapshot) -> String {
reason = "cross-surface Lua transaction scenario"
)]
fn lua_surface_is_strict_fresh_transactional_and_context_safe() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let command_lua = lua_string("/bin/sh");
let baseline_buffer_id = state.core.borrow().active_buffer_id();
@ -364,7 +364,7 @@ fn lua_surface_is_strict_fresh_transactional_and_context_safe() {
#[test]
#[allow(clippy::too_many_lines, reason = "shared view and controller scenario")]
fn shared_screen_keeps_view_scroll_selection_and_controller_independent() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let mut spec = TerminalSpec::new("/bin/sh");
spec.args = vec![
"-c".into(),
@ -508,7 +508,7 @@ fn terminal_escape_gates_local_bindings_and_double_escape_sends_interrupt() {
ready_path.to_str().expect("UTF-8 ready path"),
input_path.to_str().expect("UTF-8 input path")
);
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
state
.lua_host
.lua()
@ -986,3 +986,11 @@ fn longest_rendered_prefix_separates_absent_child_text_from_a_split_render() {
needle.len()
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root. Re-exported rather than re-declared
// with `#[path]` — this file already pulls in `common`, and loading one
// source file as two modules is `clippy::duplicate_mod`.
use common::iso;

View File

@ -156,7 +156,7 @@ fn one_frame(messages: &[InstanceMessage]) -> TerminalFrame {
reason = "one producer-baseline lifecycle scenario"
)]
fn a30_first_frame_is_authoritative_then_only_real_changes_emit() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let frontend_id = FrontendId(31);
let terminal_buffer = open_terminal(
&mut state,
@ -314,7 +314,7 @@ fn a30_first_frame_is_authoritative_then_only_real_changes_emit() {
reason = "one shared-session two-frontend scenario"
)]
fn a31_two_semantic_frontends_share_one_session_with_independent_views() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let first_id = FrontendId(41);
let second_id = FrontendId(42);
let terminal_buffer = open_terminal(
@ -459,7 +459,7 @@ fn a31_two_semantic_frontends_share_one_session_with_independent_views() {
reason = "one case per rejected identity or bound"
)]
fn a32_forged_stale_and_out_of_bounds_terminal_events_change_nothing() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let owner = FrontendId(51);
let attacker = FrontendId(52);
let terminal_buffer = open_terminal(&mut state, "sleep 30", 6, 20);
@ -978,7 +978,7 @@ fn terminal_mode_keeps_reporting_presence_so_peers_drop_the_stale_caret() {
/// gesture still claims; only motion does not.
#[test]
fn hover_does_not_steal_terminal_control_from_the_active_frontend() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let owner = FrontendId(71);
let bystander = FrontendId(72);
let terminal_buffer = open_terminal(&mut state, "sleep 30", 6, 20);
@ -1075,7 +1075,7 @@ fn hover_does_not_steal_terminal_control_from_the_active_frontend() {
/// the empty identity buffer.
#[test]
fn a28_a30_a_v18_semantic_peer_has_no_terminal_surface() {
let mut state = EditorState::new();
let mut state = EditorState::new_with_roots(&crate::iso::roots());
let frontend_id = FrontendId(61);
let terminal_buffer = open_terminal(&mut state, "sleep 30", 4, 20);
tick_until(&mut state, Duration::from_secs(5), |state| {
@ -1341,3 +1341,11 @@ fn gpu_terminal_input_reaches_the_child_and_returns_in_a_frame() {
report()
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root. Re-exported rather than re-declared
// with `#[path]` — this file already pulls in `common`, and loading one
// source file as two modules is `clippy::duplicate_mod`.
use common::iso;

View File

@ -30,7 +30,7 @@ fn live_threads() -> usize {
fn editor_state_drop_releases_workers_and_shutdown_is_idempotent() {
let baseline = live_threads();
for _ in 0..3 {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
drop(s);
}
// Signal-only shutdown: parked workers exit within their 100ms
@ -45,7 +45,7 @@ fn editor_state_drop_releases_workers_and_shutdown_is_idempotent() {
// Idempotence: explicit shutdown twice, then drop runs it a
// third time --- none may hang or panic.
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
s.async_runtime.shutdown_workers();
s.async_runtime.shutdown_workers();
drop(s);
@ -55,8 +55,15 @@ fn editor_state_drop_releases_workers_and_shutdown_is_idempotent() {
#[cfg(not(target_os = "linux"))]
#[test]
fn explicit_shutdown_is_idempotent() {
let s = EditorState::new();
let s = EditorState::new_with_roots(&crate::iso::roots());
s.async_runtime.shutdown_workers();
s.async_runtime.shutdown_workers(); // second call must not hang or panic
drop(s); // drop runs shutdown a third time
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;