diff --git a/tests/auto_indent_acceptance.rs b/tests/auto_indent_acceptance.rs index 5d40590..17c8403 100644 --- a/tests/auto_indent_acceptance.rs +++ b/tests/auto_indent_acceptance.rs @@ -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; diff --git a/tests/auto_pair_acceptance.rs b/tests/auto_pair_acceptance.rs index 29291b3..7a20b06 100644 --- a/tests/auto_pair_acceptance.rs +++ b/tests/auto_pair_acceptance.rs @@ -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::(); 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; diff --git a/tests/autosave_acceptance.rs b/tests/autosave_acceptance.rs index 4567f96..8433f28 100644 --- a/tests/autosave_acceptance.rs +++ b/tests/autosave_acceptance.rs @@ -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::(); 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; diff --git a/tests/bottom_panel_stage1_acceptance.rs b/tests/bottom_panel_stage1_acceptance.rs index fb3a4ad..400754a 100644 --- a/tests/bottom_panel_stage1_acceptance.rs +++ b/tests/bottom_panel_stage1_acceptance.rs @@ -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; diff --git a/tests/bottom_panel_stage2a_acceptance.rs b/tests/bottom_panel_stage2a_acceptance.rs index d39d804..1e5ed05 100644 --- a/tests/bottom_panel_stage2a_acceptance.rs +++ b/tests/bottom_panel_stage2a_acceptance.rs @@ -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; diff --git a/tests/bottom_panel_stage2b_daemon_acceptance.rs b/tests/bottom_panel_stage2b_daemon_acceptance.rs index 0bbf8d0..cc80422 100644 --- a/tests/bottom_panel_stage2b_daemon_acceptance.rs +++ b/tests/bottom_panel_stage2b_daemon_acceptance.rs @@ -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; diff --git a/tests/comment_toggle_acceptance.rs b/tests/comment_toggle_acceptance.rs index 0e00f21..caa06e1 100644 --- a/tests/comment_toggle_acceptance.rs +++ b/tests/comment_toggle_acceptance.rs @@ -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::(); 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; diff --git a/tests/common/daemon.rs b/tests/common/daemon.rs index 1c8bc0d..1e67642 100644 --- a/tests/common/daemon.rs +++ b/tests/common/daemon.rs @@ -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 `/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); } diff --git a/tests/common/pty.rs b/tests/common/pty.rs index a1f16c8..e8573e1 100644 --- a/tests/common/pty.rs +++ b/tests/common/pty.rs @@ -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); diff --git a/tests/compile_mode_acceptance.rs b/tests/compile_mode_acceptance.rs index e7f21e0..a223406 100644 --- a/tests/compile_mode_acceptance.rs +++ b/tests/compile_mode_acceptance.rs @@ -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; diff --git a/tests/completion_popup_acceptance.rs b/tests/completion_popup_acceptance.rs index f15a705..1867fc2 100644 --- a/tests/completion_popup_acceptance.rs +++ b/tests/completion_popup_acceptance.rs @@ -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; diff --git a/tests/config_registry_acceptance.rs b/tests/config_registry_acceptance.rs index 41749fa..e194003 100644 --- a/tests/config_registry_acceptance.rs +++ b/tests/config_registry_acceptance.rs @@ -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::(); 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; diff --git a/tests/cua_region_acceptance.rs b/tests/cua_region_acceptance.rs index c4c16d7..89f22a8 100644 --- a/tests/cua_region_acceptance.rs +++ b/tests/cua_region_acceptance.rs @@ -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; diff --git a/tests/desktop_acceptance.rs b/tests/desktop_acceptance.rs index 61cab86..dffe516 100644 --- a/tests/desktop_acceptance.rs +++ b/tests/desktop_acceptance.rs @@ -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::(); 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; diff --git a/tests/dired_acceptance.rs b/tests/dired_acceptance.rs index bff9134..cacfdea 100644 --- a/tests/dired_acceptance.rs +++ b/tests/dired_acceptance.rs @@ -116,7 +116,7 @@ fn eval(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; diff --git a/tests/editops_acceptance.rs b/tests/editops_acceptance.rs index 0a30f95..f58f53a 100644 --- a/tests/editops_acceptance.rs +++ b/tests/editops_acceptance.rs @@ -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; diff --git a/tests/find_file_acceptance.rs b/tests/find_file_acceptance.rs index fb793e3..f290a3a 100644 --- a/tests/find_file_acceptance.rs +++ b/tests/find_file_acceptance.rs @@ -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; diff --git a/tests/folding_acceptance.rs b/tests/folding_acceptance.rs index 6df069a..01de6a5 100644 --- a/tests/folding_acceptance.rs +++ b/tests/folding_acceptance.rs @@ -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; diff --git a/tests/folding_stage2_acceptance.rs b/tests/folding_stage2_acceptance.rs index 79b41c8..473be53 100644 --- a/tests/folding_stage2_acceptance.rs +++ b/tests/folding_stage2_acceptance.rs @@ -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; diff --git a/tests/gpu_font_acceptance.rs b/tests/gpu_font_acceptance.rs index 264f25d..fe773ed 100644 --- a/tests/gpu_font_acceptance.rs +++ b/tests/gpu_font_acceptance.rs @@ -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(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; diff --git a/tests/gpu_invocation_acceptance.rs b/tests/gpu_invocation_acceptance.rs index 153ed6f..5a3bb88 100644 --- a/tests/gpu_invocation_acceptance.rs +++ b/tests/gpu_invocation_acceptance.rs @@ -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()); diff --git a/tests/injection_acceptance.rs b/tests/injection_acceptance.rs index 88a314f..ac113e9 100644 --- a/tests/injection_acceptance.rs +++ b/tests/injection_acceptance.rs @@ -35,7 +35,7 @@ fn pump_async 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; diff --git a/tests/journey_acceptance.rs b/tests/journey_acceptance.rs index db88942..a9529ac 100644 --- a/tests/journey_acceptance.rs +++ b/tests/journey_acceptance.rs @@ -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() { /// ``. #[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::( @@ -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 { /// 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, 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" + ); +} diff --git a/tests/kill_ring_acceptance.rs b/tests/kill_ring_acceptance.rs index 1a375ad..11cb3e3 100644 --- a/tests/kill_ring_acceptance.rs +++ b/tests/kill_ring_acceptance.rs @@ -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; diff --git a/tests/lean4_server_acceptance.rs b/tests/lean4_server_acceptance.rs index 86be1d5..113f886 100644 --- a/tests/lean4_server_acceptance.rs +++ b/tests/lean4_server_acceptance.rs @@ -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; diff --git a/tests/lean4_stage1_acceptance.rs b/tests/lean4_stage1_acceptance.rs index 9aafcab..6a9c469 100644 --- a/tests/lean4_stage1_acceptance.rs +++ b/tests/lean4_stage1_acceptance.rs @@ -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::(); 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 { - 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; diff --git a/tests/lean_input_acceptance.rs b/tests/lean_input_acceptance.rs index 9b6a3b8..825184d 100644 --- a/tests/lean_input_acceptance.rs +++ b/tests/lean_input_acceptance.rs @@ -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::(); 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; diff --git a/tests/listview_acceptance.rs b/tests/listview_acceptance.rs index 8c15629..d546f00 100644 --- a/tests/listview_acceptance.rs +++ b/tests/listview_acceptance.rs @@ -212,7 +212,7 @@ fn probe(s: &EditorState) -> (String, String, i64, Option) { #[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; diff --git a/tests/lsp_dispatch_seams_acceptance.rs b/tests/lsp_dispatch_seams_acceptance.rs index f644367..531c4f3 100644 --- a/tests/lsp_dispatch_seams_acceptance.rs +++ b/tests/lsp_dispatch_seams_acceptance.rs @@ -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; diff --git a/tests/lsp_multi_root_acceptance.rs b/tests/lsp_multi_root_acceptance.rs index 39ac68f..7a4c570 100644 --- a/tests/lsp_multi_root_acceptance.rs +++ b/tests/lsp_multi_root_acceptance.rs @@ -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; diff --git a/tests/lsp_spawn_guidance_acceptance.rs b/tests/lsp_spawn_guidance_acceptance.rs index fd48ace..6ac5b3c 100644 --- a/tests/lsp_spawn_guidance_acceptance.rs +++ b/tests/lsp_spawn_guidance_acceptance.rs @@ -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; diff --git a/tests/m11_5_semantic_acceptance.rs b/tests/m11_5_semantic_acceptance.rs index 09a4e7e..761a0ba 100644 --- a/tests/m11_5_semantic_acceptance.rs +++ b/tests/m11_5_semantic_acceptance.rs @@ -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; diff --git a/tests/m4_acceptance.rs b/tests/m4_acceptance.rs index 64323e0..32511ff 100644 --- a/tests/m4_acceptance.rs +++ b/tests/m4_acceptance.rs @@ -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 = 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 { 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 { 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 { 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, Option) = 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, Option, Option) = 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(¬_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; diff --git a/tests/m5_5_acceptance.rs b/tests/m5_5_acceptance.rs index 031e86b..47adb6b 100644 --- a/tests/m5_5_acceptance.rs +++ b/tests/m5_5_acceptance.rs @@ -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() diff --git a/tests/m5_7_acceptance.rs b/tests/m5_7_acceptance.rs index ce85591..24829c3 100644 --- a/tests/m5_7_acceptance.rs +++ b/tests/m5_7_acceptance.rs @@ -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()) diff --git a/tests/m5_8_acceptance.rs b/tests/m5_8_acceptance.rs index f58c609..8bf2a43 100644 --- a/tests/m5_8_acceptance.rs +++ b/tests/m5_8_acceptance.rs @@ -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()) diff --git a/tests/m5_perf_acceptance.rs b/tests/m5_perf_acceptance.rs index 12139b3..6d261d7 100644 --- a/tests/m5_perf_acceptance.rs +++ b/tests/m5_perf_acceptance.rs @@ -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() diff --git a/tests/m6_4_repl_acceptance.rs b/tests/m6_4_repl_acceptance.rs index 200dea0..9dbfce0 100644 --- a/tests/m6_4_repl_acceptance.rs +++ b/tests/m6_4_repl_acceptance.rs @@ -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(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; diff --git a/tests/m6_5_repl_acceptance.rs b/tests/m6_5_repl_acceptance.rs index 44d6b95..99135b3 100644 --- a/tests/m6_5_repl_acceptance.rs +++ b/tests/m6_5_repl_acceptance.rs @@ -80,7 +80,7 @@ fn locate_shell(name: &str) -> Option { /// 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; diff --git a/tests/m6_7_scrollback_acceptance.rs b/tests/m6_7_scrollback_acceptance.rs index 2cc4dec..7c97f66 100644 --- a/tests/m6_7_scrollback_acceptance.rs +++ b/tests/m6_7_scrollback_acceptance.rs @@ -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; diff --git a/tests/m6_8_multi_repl_acceptance.rs b/tests/m6_8_multi_repl_acceptance.rs index 4bc482f..fbc9afc 100644 --- a/tests/m6_8_multi_repl_acceptance.rs +++ b/tests/m6_8_multi_repl_acceptance.rs @@ -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")` @@ -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; diff --git a/tests/m6_perf_acceptance.rs b/tests/m6_perf_acceptance.rs index 5ca857b..094808f 100644 --- a/tests/m6_perf_acceptance.rs +++ b/tests/m6_perf_acceptance.rs @@ -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; diff --git a/tests/m7_11_acceptance.rs b/tests/m7_11_acceptance.rs index 5b574ba..03c904c 100644 --- a/tests/m7_11_acceptance.rs +++ b/tests/m7_11_acceptance.rs @@ -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; diff --git a/tests/m8_10_acceptance.rs b/tests/m8_10_acceptance.rs index 74cbc20..24f106d 100644 --- a/tests/m8_10_acceptance.rs +++ b/tests/m8_10_acceptance.rs @@ -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; diff --git a/tests/m8_1_acceptance.rs b/tests/m8_1_acceptance.rs index 5e5b899..3d33f20 100644 --- a/tests/m8_1_acceptance.rs +++ b/tests/m8_1_acceptance.rs @@ -36,7 +36,7 @@ fn pump_until 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; diff --git a/tests/m8_2_acceptance.rs b/tests/m8_2_acceptance.rs index c61abdb..a7e7e63 100644 --- a/tests/m8_2_acceptance.rs +++ b/tests/m8_2_acceptance.rs @@ -80,7 +80,7 @@ fn pump_until 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; diff --git a/tests/m8_3_acceptance.rs b/tests/m8_3_acceptance.rs index 7706efd..742cec1 100644 --- a/tests/m8_3_acceptance.rs +++ b/tests/m8_3_acceptance.rs @@ -44,7 +44,7 @@ fn pump_until 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; diff --git a/tests/m8_5_acceptance.rs b/tests/m8_5_acceptance.rs index ecba842..2fac268 100644 --- a/tests/m8_5_acceptance.rs +++ b/tests/m8_5_acceptance.rs @@ -52,7 +52,7 @@ fn pump_until 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; diff --git a/tests/m8_6_acceptance.rs b/tests/m8_6_acceptance.rs index 4ddcffe..e9d8baf 100644 --- a/tests/m8_6_acceptance.rs +++ b/tests/m8_6_acceptance.rs @@ -64,7 +64,7 @@ fn pump_until_with_deadline 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; diff --git a/tests/m8_7_acceptance.rs b/tests/m8_7_acceptance.rs index fb3cd88..6990557 100644 --- a/tests/m8_7_acceptance.rs +++ b/tests/m8_7_acceptance.rs @@ -62,7 +62,7 @@ fn pump_until_with_deadline 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; diff --git a/tests/m8_9_acceptance.rs b/tests/m8_9_acceptance.rs index 11834a6..2877b23 100644 --- a/tests/m8_9_acceptance.rs +++ b/tests/m8_9_acceptance.rs @@ -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; diff --git a/tests/m9_1_acceptance.rs b/tests/m9_1_acceptance.rs index 47c8a59..a7eb45d 100644 --- a/tests/m9_1_acceptance.rs +++ b/tests/m9_1_acceptance.rs @@ -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; diff --git a/tests/m9_2_acceptance.rs b/tests/m9_2_acceptance.rs index 06338c9..e0f5437 100644 --- a/tests/m9_2_acceptance.rs +++ b/tests/m9_2_acceptance.rs @@ -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; diff --git a/tests/m9_3_acceptance.rs b/tests/m9_3_acceptance.rs index aadf606..cf020ec 100644 --- a/tests/m9_3_acceptance.rs +++ b/tests/m9_3_acceptance.rs @@ -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; diff --git a/tests/m9_4_acceptance.rs b/tests/m9_4_acceptance.rs index 8474db1..43e2de3 100644 --- a/tests/m9_4_acceptance.rs +++ b/tests/m9_4_acceptance.rs @@ -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; diff --git a/tests/m9_5_acceptance.rs b/tests/m9_5_acceptance.rs index 8831935..c0e44cc 100644 --- a/tests/m9_5_acceptance.rs +++ b/tests/m9_5_acceptance.rs @@ -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; diff --git a/tests/m9_6_acceptance.rs b/tests/m9_6_acceptance.rs index 4b5166d..ee3eac9 100644 --- a/tests/m9_6_acceptance.rs +++ b/tests/m9_6_acceptance.rs @@ -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; diff --git a/tests/m9_7_acceptance.rs b/tests/m9_7_acceptance.rs index 456ad97..63d88eb 100644 --- a/tests/m9_7_acceptance.rs +++ b/tests/m9_7_acceptance.rs @@ -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; diff --git a/tests/m9_8_acceptance.rs b/tests/m9_8_acceptance.rs index 06ed3a3..0b418e8 100644 --- a/tests/m9_8_acceptance.rs +++ b/tests/m9_8_acceptance.rs @@ -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; diff --git a/tests/overlay_reattach_acceptance.rs b/tests/overlay_reattach_acceptance.rs index 48eafbb..efd57f8 100644 --- a/tests/overlay_reattach_acceptance.rs +++ b/tests/overlay_reattach_acceptance.rs @@ -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; diff --git a/tests/persistence_acceptance.rs b/tests/persistence_acceptance.rs index c3a6c9d..3045709 100644 --- a/tests/persistence_acceptance.rs +++ b/tests/persistence_acceptance.rs @@ -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::(); 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::().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; diff --git a/tests/query_replace_acceptance.rs b/tests/query_replace_acceptance.rs index 4fa69b3..a36ba56 100644 --- a/tests/query_replace_acceptance.rs +++ b/tests/query_replace_acceptance.rs @@ -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; diff --git a/tests/resource_reconciliation_acceptance.rs b/tests/resource_reconciliation_acceptance.rs index c9beba4..6a00681 100644 --- a/tests/resource_reconciliation_acceptance.rs +++ b/tests/resource_reconciliation_acceptance.rs @@ -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; diff --git a/tests/save_clobber_guard_acceptance.rs b/tests/save_clobber_guard_acceptance.rs index f1700c7..da177b7 100644 --- a/tests/save_clobber_guard_acceptance.rs +++ b/tests/save_clobber_guard_acceptance.rs @@ -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::(&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::(&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; diff --git a/tests/statusline_segments_acceptance.rs b/tests/statusline_segments_acceptance.rs index 2f5cfd9..2df0161 100644 --- a/tests/statusline_segments_acceptance.rs +++ b/tests/statusline_segments_acceptance.rs @@ -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(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; diff --git a/tests/terminal_config_acceptance.rs b/tests/terminal_config_acceptance.rs index ceeb8fe..9a52bc8 100644 --- a/tests/terminal_config_acceptance.rs +++ b/tests/terminal_config_acceptance.rs @@ -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 = 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; diff --git a/tests/terminal_copy_mode_acceptance.rs b/tests/terminal_copy_mode_acceptance.rs index aeba1ce..ecb6e69 100644 --- a/tests/terminal_copy_mode_acceptance.rs +++ b/tests/terminal_copy_mode_acceptance.rs @@ -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; diff --git a/tests/theme_faces_acceptance.rs b/tests/theme_faces_acceptance.rs index c81ace7..c9e5fb2 100644 --- a/tests/theme_faces_acceptance.rs +++ b/tests/theme_faces_acceptance.rs @@ -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(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 { /// 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; diff --git a/tests/typed_edit_chain_acceptance.rs b/tests/typed_edit_chain_acceptance.rs index 8ff9c13..2a65cb7 100644 --- a/tests/typed_edit_chain_acceptance.rs +++ b/tests/typed_edit_chain_acceptance.rs @@ -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::(); 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; diff --git a/tests/vterm_stage1_acceptance.rs b/tests/vterm_stage1_acceptance.rs index d1a4a63..489ca9a 100644 --- a/tests/vterm_stage1_acceptance.rs +++ b/tests/vterm_stage1_acceptance.rs @@ -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; diff --git a/tests/vterm_stage2_acceptance.rs b/tests/vterm_stage2_acceptance.rs index 852ed1f..d5b86e3 100644 --- a/tests/vterm_stage2_acceptance.rs +++ b/tests/vterm_stage2_acceptance.rs @@ -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; diff --git a/tests/vterm_stage3_acceptance.rs b/tests/vterm_stage3_acceptance.rs index e4eb3c1..c415abf 100644 --- a/tests/vterm_stage3_acceptance.rs +++ b/tests/vterm_stage3_acceptance.rs @@ -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; diff --git a/tests/worker_shutdown_acceptance.rs b/tests/worker_shutdown_acceptance.rs index 3893a20..a9cf831 100644 --- a/tests/worker_shutdown_acceptance.rs +++ b/tests/worker_shutdown_acceptance.rs @@ -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;