pmacs/tests/listview_acceptance.rs

765 lines
27 KiB
Rust

//! List-panel acceptance (Arc 1b phase 1) --- the `pmacs.listview`
//! module end-to-end through `dispatch_key`: open/navigate/visit,
//! `q` restore, the Q#P3 read-only intercept, the Q#P6 round-trip
//! gate (`dispatch_idle` false while a panel is focused), and
//! refresh. The references panel itself needs a live LSP and is
//! validated manually / via the m4 harness; these tests drive the
//! substrate hermetically.
//!
//! Framing: docs/lsp-panels-framing.md.
//!
//! Generated-buffer immutability Stage 1
//! (docs/generated-buffer-immutability-framing.md §6) adds the
//! criteria below `refresh_reruns_the_source_and_reseats`: the undo
//! paths the Q#P3 intercept never guarded, the Q#GB13 ownership rule,
//! and the Q#GB18 identity routing that ownership makes load-bearing.
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
use pmacs::buffer::BufferId;
use pmacs::editor::EditorState;
use pmacs::protocol::FrontendId;
fn key(code: KeyCode, mods: KeyModifiers) -> KeyEvent {
KeyEvent {
code,
modifiers: mods,
kind: KeyEventKind::Press,
state: KeyEventState::empty(),
}
}
fn press(s: &mut EditorState, code: KeyCode) {
s.dispatch_key(FrontendId::LOCAL, key(code, KeyModifiers::NONE));
}
fn ctrl(s: &mut EditorState, c: char) {
s.dispatch_key(
FrontendId::LOCAL,
key(KeyCode::Char(c), KeyModifiers::CONTROL),
);
}
fn alt(s: &mut EditorState, c: char) {
s.dispatch_key(FrontendId::LOCAL, key(KeyCode::Char(c), KeyModifiers::ALT));
}
fn type_str(s: &mut EditorState, text: &str) {
for ch in text.chars() {
s.dispatch_key(
FrontendId::LOCAL,
key(KeyCode::Char(ch), KeyModifiers::NONE),
);
}
}
/// `M-x <name> RET` through the **real** minibuffer, not
/// `pmacs.command.invoke`: `buffer.undo` is reachable that way on every
/// buffer in the tree and no buffer-local rebinding can remove it, which
/// is the whole reason the intercept idiom did not close this hole.
fn m_x(s: &mut EditorState, name: &str) {
alt(s, 'x');
type_str(s, name);
press(s, KeyCode::Enter);
}
fn exec(s: &EditorState, src: &str) {
s.lua_host.lua().load(src.to_string()).exec().unwrap();
}
fn eval<T: mlua::FromLuaMulti>(s: &EditorState, src: &str) -> T {
s.lua_host.lua().load(src.to_string()).eval().unwrap()
}
fn status(s: &EditorState) -> String {
s.core.borrow().status.clone()
}
fn buffer_names(s: &EditorState) -> Vec<String> {
eval(
s,
"local out = {}\n\
for _, id in ipairs(pmacs.buffer.list()) do\n\
out[#out + 1] = pmacs.describe.buffer(id).name\n\
end\n\
return out",
)
}
fn active_name(s: &EditorState) -> String {
eval(
s,
"return pmacs.describe.buffer(pmacs.window.buffer()).name",
)
}
fn active_text(s: &EditorState) -> String {
eval(
s,
"local b = pmacs.window.buffer()\nreturn b:slice(0, b:len())",
)
}
fn id_of(s: &EditorState, name: &str) -> BufferId {
let core = s.core.borrow();
let reg = core.registry.borrow();
reg.find_by_name(name)
.unwrap_or_else(|| panic!("no buffer named {name:?} in {:?}", buffer_names(s)))
}
/// Q#GB14: the rope lock is not observable from Lua --- `describe.buffer`
/// carries `name`, `length`, `modified`, `view_count` and nothing else ---
/// so every "is it locked" assertion goes through Rust.
fn is_read_only(s: &EditorState, id: BufferId) -> bool {
let core = s.core.borrow();
let reg = core.registry.borrow();
reg.get(id).expect("buffer in registry").is_read_only()
}
fn set_read_only(s: &EditorState, id: BufferId, value: bool) {
let core = s.core.borrow();
let mut reg = core.registry.borrow_mut();
reg.get_mut(id)
.expect("buffer in registry")
.set_read_only(value);
}
/// Render the active window's text view into a cell grid. Criterion 7 is
/// pinned by PAINTING, because that is where a rope/window disagreement
/// bites: the rope is right and the screen is not.
fn paint_active_window(s: &EditorState, rows: u32, cols: u32) -> Vec<pmacs::cell::Cell> {
use pmacs::cell::{Cell, CellGrid, CellSize};
use pmacs::view::{View, Viewport};
use pmacs::window::Rect;
let mut core = s.core.borrow_mut();
let active = core.active_window_id();
let registry = core.registry.clone();
let win = core.windows.get_mut(&active).expect("active window");
let rect = Rect::new(0, 0, rows, cols);
let mut backing = vec![Cell::default(); (rows * cols) as usize];
let reg = registry.borrow();
let buf = reg.get(win.buffer_id).expect("buffer in registry");
let viewport = Viewport {
buffer_start: 0,
buffer_end: buf.len(),
cell_origin: rect.origin,
cell_size: CellSize::new(rows, cols),
gutter_w: 0,
folds: None,
};
let mut grid = CellGrid {
cells: &mut backing,
stride: cols,
size: CellSize::new(rows, cols),
};
win.text_view.render(buf, viewport, &mut grid);
backing
}
fn grid_row(cells: &[pmacs::cell::Cell], row: u32, cols: u32) -> String {
use pmacs::cell::Glyph;
(0..cols)
.map(|c| match cells[(row * cols + c) as usize].glyph {
Glyph::Char(ch) => ch,
_ => ' ',
})
.collect::<String>()
.trim_end()
.to_owned()
}
/// Open a three-row test panel whose visits record into `_G.VISITED`.
fn open_test_panel(s: &mut EditorState) {
s.lua_host
.lua()
.load(
r#"
_G.VISITED = nil
pmacs.listview.open {
name = "*test-panel*",
header = "3 items RET visit q quit",
rows = {
{ text = "alpha", item = "A" },
{ text = "beta", item = "B" },
{ text = "gamma", item = "C" },
},
on_visit = function(item) _G.VISITED = item end,
on_refresh = function()
return { { text = "delta", item = "D" } }
end,
}
"#,
)
.exec()
.expect("open test panel");
}
/// `(active buffer name, buffer text, cursor line, visited)` probed
/// through the Lua surface.
fn probe(s: &EditorState) -> (String, String, i64, Option<String>) {
s.lua_host
.lua()
.load(
r"
local b = pmacs.window.buffer()
local d = pmacs.describe.buffer(b)
return d.name, b:slice(0, b:len()), pmacs.editor.cursor_line(), _G.VISITED
",
)
.eval()
.expect("probe panel state")
}
#[test]
fn open_seats_cursor_and_ret_visits_the_row() {
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*");
assert!(text.starts_with("3 items"), "header renders first");
assert_eq!(line, 1, "the cursor opens on the first data row");
press(&mut s, KeyCode::Char('n')); // buffer-local: cursor.down
press(&mut s, KeyCode::Enter);
let (_, _, _, visited) = probe(&s);
assert_eq!(visited.as_deref(), Some("B"), "RET visits the second row");
}
#[test]
fn header_row_is_not_visitable() {
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);
let (_, _, _, visited) = probe(&s);
assert_eq!(visited, None, "the header maps to no item");
}
#[test]
fn q_restores_the_previous_buffer() {
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);
assert_eq!(name, "*scratch*", "q returns to the buffer we came from");
}
#[test]
fn panel_rejects_typing() {
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
let (_, after, _, _) = probe(&s);
assert_eq!(before, after, "the read-only intercept rejects self-insert");
}
#[test]
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_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");
press(&mut s, KeyCode::Char('q'));
assert!(s.dispatch_idle(), "restored buffer: idle again");
}
#[test]
fn refresh_reruns_the_source_and_reseats() {
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);
assert!(text.contains("delta"), "g re-renders from on_refresh");
assert!(!text.contains("alpha"), "old rows are gone");
assert_eq!(line, 1, "cursor re-seats on a data row after refresh");
press(&mut s, KeyCode::Enter);
let (_, _, _, visited) = probe(&s);
assert_eq!(visited.as_deref(), Some("D"), "the refreshed row visits");
}
// ---------------------------------------------------------------------------
// Generated-buffer immutability, Stage 1
// (docs/generated-buffer-immutability-framing.md §6, Stage 1)
// ---------------------------------------------------------------------------
/// The exact bytes `open_test_panel` renders. Asserted by value, not by
/// `is_empty()`: "the panel is not empty" is the assertion shape the
/// framing's §0.1 shows passing with the bug live on other families.
const PANEL_TEXT: &str = "3 items RET visit q quit\nalpha\nbeta\ngamma";
/// Criterion 1 [`main`] --- `C-/` cannot empty a listview panel.
///
/// Driven by a real chord through `dispatch_key`, because listview
/// rebinds **no** undo chord (`grep -n 'C-/\|C-_\|C-x u\|undo'
/// builtin/runtime/listview.lua` is empty), so this is the whole
/// distance from a keystroke to an empty panel.
///
/// *Bite:* measured on the pre-image --- `"H\nrow-one\nrow-two"` -> `""`.
/// `scripts/bite githubsucks/main builtin/runtime/listview.lua` falsifies
/// it: the panel's own render pushes a poppable undo entry, and
/// `Buffer::undo` reaches the rope through `ensure_writable` without ever
/// consulting the intercept chain.
#[test]
fn s1_1_the_undo_chord_cannot_empty_a_listview_panel() {
let mut s = EditorState::new_with_roots(&crate::iso::roots());
open_test_panel(&mut s);
assert_eq!(active_text(&s), PANEL_TEXT, "precondition: rendered");
ctrl(&mut s, '/');
assert_eq!(
active_text(&s),
PANEL_TEXT,
"C-/ must leave the panel's content intact"
);
}
/// Criterion 2 [`main`] --- `M-x buffer.undo` cannot empty a listview
/// panel, driven through the **real** minibuffer.
///
/// Separate from criterion 1 on purpose: a fix that only rebound the
/// chords would pass 1 and fail this. `compile.lua`'s own comment already
/// concedes the point ("command/menu undo stays dispatchable").
///
/// *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_with_roots(&crate::iso::roots());
open_test_panel(&mut s);
m_x(&mut s, "buffer.undo");
assert_eq!(
active_name(&s),
"*test-panel*",
"the minibuffer round trip must land back in the panel"
);
assert_eq!(
active_text(&s),
PANEL_TEXT,
"M-x buffer.undo must leave the panel's content intact"
);
}
/// Criterion 4 [fix-shape] --- the owner's own refresh still works after
/// the lock, and the buffer is still locked afterwards.
///
/// *Bite:* a naive `set_read_only(true)` at panel creation passes
/// criteria 1-3 and fails here, because it refuses the refresh the panel
/// exists for. That is the failure mode `Buffer::set_generated_contents`
/// exists to prevent, and it is why the **pairing** is the primitive.
/// The assertion is on the content `g` produced, not on the call not
/// raising.
#[test]
fn s1_4_the_owners_refresh_still_works_after_the_lock() {
let mut s = EditorState::new_with_roots(&crate::iso::roots());
open_test_panel(&mut s);
let panel = id_of(&s, "*test-panel*");
assert!(
is_read_only(&s, panel),
"precondition: the first render locked the rope"
);
press(&mut s, KeyCode::Char('g'));
let text = active_text(&s);
assert!(text.contains("delta"), "g must re-render: {text:?}");
assert!(
!text.contains("alpha"),
"and replace the old rows: {text:?}"
);
assert!(
is_read_only(&s, panel),
"and the panel must still be locked afterwards"
);
}
/// Stage 1 criterion 5 [`main`, and also fix-shape] — the rope lock
/// refuses an ordinary edit first, and the named intercept survives
/// behind it.
///
/// Both halves are required. The first asserts the exact
/// `BufferError::ReadOnly` rendering and byte identity. The second lifts
/// the lock Rust-side and distinguishes the intercept by its
/// `intercept rejected the edit` message. Deleting `add_intercept`
/// 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_with_roots(&crate::iso::roots());
open_test_panel(&mut s);
let panel = id_of(&s, "*test-panel*");
let before = active_text(&s);
press(&mut s, KeyCode::Char('z'));
assert_eq!(
status(&s),
format!("insert failed: buffer `*test-panel*` (id {panel:?}) is read-only"),
"with the lock on, the rope must provide the exact refusal"
);
assert_eq!(
active_text(&s),
before,
"the rope refusal leaves every byte unchanged"
);
set_read_only(&s, panel, false);
press(&mut s, KeyCode::Char('z'));
set_read_only(&s, panel, true);
assert_eq!(
active_text(&s),
PANEL_TEXT,
"the intercept must refuse the edit even with the rope writable"
);
let st = status(&s);
assert!(
st.starts_with("insert failed: intercept rejected the edit:")
&& st.contains("listview.lua")
&& st.contains("*test-panel* is read-only"),
"the lifted path must carry the named intercept refusal; got {st:?}"
);
assert!(
!st.contains(&format!(
"buffer `*test-panel*` (id {panel:?}) is read-only"
)),
"the lifted path must not masquerade as the rope refusal: {st:?}"
);
}
/// Criterion 6 [fix-shape] --- `set_round_trip_input` survives adoption,
/// asserted so that only the round-trip mark can make it pass.
///
/// `dispatch_idle_for` (`src/editor.rs:1126-1155`) returns `false` for
/// **six** independent reasons, so `!dispatch_idle_for(..)` alone is
/// satisfied by any of them. All three halves are required:
///
/// * **(a)** the document-window premise (`!window.is_side()`), so a
/// fixture that later displays the panel in a side window fails loudly
/// rather than passing vacuously --- `tests/dired_acceptance.rs:975`'s
/// shape;
/// * **(b)** the gate itself while the panel is focused;
/// * **(c)** the positive control --- switching the same window to a
/// plain buffer must flip the gate back to `true`. A stuck minibuffer,
/// a pending chord, an open menu or a live search would keep it `false`
/// across the switch, so (c) failing is the signal that (b) passed for
/// the wrong reason.
///
/// *Bite:* delete the `set_round_trip_input` call in `listview.lua` and
/// criteria 1-5 all still pass; only (b) fails. A daemon-side rope
/// refusal does nothing for a replica's own mirror, which is why this is
/// 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_with_roots(&crate::iso::roots());
open_test_panel(&mut s);
// (a) the premise.
{
let core = s.core.borrow();
let active = core.active_window_id();
assert!(
!core.windows.get(&active).expect("live window").is_side(),
"fixture premise: the panel is in a document window here"
);
}
// (b) the gate.
assert!(
!s.dispatch_idle_for(FrontendId::LOCAL),
"a round-trip buffer must turn optimistic apply OFF"
);
// (c) the positive control.
exec(
&s,
"pmacs.window.switch_buffer(pmacs.buffer.create('*plain*'))",
);
assert!(
s.dispatch_idle_for(FrontendId::LOCAL),
"and back ON for a plain buffer --- otherwise (b) passed for one \
of the other five reasons"
);
}
/// **Stage 1 criterion 7 [mutation]**, the listview half: *a refresh
/// reaches the window, not just the rope --- pinned by painting a
/// shrinking render (many rows -> one) and asserting row 1 is empty, for
/// each adopter.* Bite: *delete the `notify_buffer_edit_to_windows` call
/// in the `set_generated_contents` binding
/// (`src/lua_bindings/mod.rs:3092`).*
///
/// The criterion says **for each adopter**, so both halves exist; the
/// dired half is `dired_acceptance::dired_a_shrinking_repaint_reaches_the_window`.
///
/// `listview.refresh` re-seats through the already-notified `TextView`;
/// it deliberately does not rebuild the view by switching to the buffer
/// it already shows. Deleting the notification fan-out therefore leaves
/// 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_with_roots(&crate::iso::roots());
open_test_panel(&mut s);
let painted = paint_active_window(&s, 6, 24);
assert_eq!(
grid_row(&painted, 1, 24),
"alpha",
"precondition: three data rows paint"
);
assert_eq!(grid_row(&painted, 3, 24), "gamma");
// `g` re-renders from three rows to one.
press(&mut s, KeyCode::Char('g'));
let painted = paint_active_window(&s, 6, 24);
assert_eq!(
grid_row(&painted, 1, 24),
"delta",
"the refreshed row must paint"
);
assert_eq!(
grid_row(&painted, 2, 24),
"",
"and nothing of the rows it replaced"
);
}
/// Criterion 9 [`main`] --- a foreign buffer that happens to share the
/// panel's name is never adopted (Q#GB13).
///
/// Both halves, because the second is what fails if adoption is merely
/// made "safe" by skipping the render: the user's bytes survive **and**
/// an ordinary edit to the user's buffer still lands. Adoption installed
/// an erroring intercept whose handle it discarded, so the pre-image left
/// the clobbered buffer permanently un-editable --- and this arc removes
/// the `M-x buffer.undo` that was the only way back.
///
/// *Bite:* measured on the pre-image --- `"my precious notes"` ->
/// `"H\nr1"`, one buffer where there should be two, and the user's buffer
/// left un-editable. `scripts/bite githubsucks/main
/// 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_with_roots(&crate::iso::roots());
exec(
&s,
"FOREIGN = pmacs.buffer.create('*test-panel*')\n\
FOREIGN:insert(0, 'my precious notes')",
);
open_test_panel(&mut s);
let foreign: String = eval(&s, "return FOREIGN:slice(0, FOREIGN:len())");
assert_eq!(
foreign, "my precious notes",
"the user's bytes must survive the panel opening"
);
let landed: String = eval(
&s,
"FOREIGN:insert(0, 'still mine: ')\n\
return FOREIGN:slice(0, FOREIGN:len())",
);
assert_eq!(
landed, "still mine: my precious notes",
"and an ordinary edit to it must still land"
);
assert_eq!(
active_name(&s),
"*test-panel*<2>",
"the panel opens under a disambiguated name"
);
let names = buffer_names(&s);
assert!(
names.iter().any(|n| n == "*test-panel*") && names.iter().any(|n| n == "*test-panel*<2>"),
"two buffers, not one: {names:?}"
);
}
/// Criterion 10 [fix-shape] --- exhausting the disambiguation limit
/// raises rather than falling back to adoption, matching
/// `dired.lua:493-503` and `terminal.lua:309-315`.
///
/// *Bite:* an implementation that adopts once `<99>` is taken passes
/// criterion 9 and fails here --- and it fails in the worst direction,
/// because the buffer it would adopt is by construction one a user
/// created.
#[test]
fn s1_10_the_disambiguation_limit_raises_rather_than_adopting() {
let s = EditorState::new_with_roots(&crate::iso::roots());
exec(
&s,
"MINE = pmacs.buffer.create('*test-panel*')\n\
MINE:insert(0, 'mine')\n\
for i = 2, 99 do pmacs.buffer.create(string.format('*test-panel*<%d>', i)) end",
);
let (ok, err): (bool, String) = eval(
&s,
"local ok, err = pcall(pmacs.listview.open, { name = '*test-panel*', rows = {} })\n\
return ok, tostring(err)",
);
assert!(!ok, "the open must raise, not adopt");
assert!(
err.contains("no free variant remains"),
"and say why; got {err:?}"
);
let mine: String = eval(&s, "return MINE:slice(0, MINE:len())");
assert_eq!(mine, "mine", "and touch nothing");
}
/// Stage 1 criterion 11 [`main`] — a **disambiguated** panel still answers
/// `RET`, `g` and `q` (Q#GB18). The framing labels it `[main]` and names
/// its bite as *Q#GB13 landed without Q#GB18*.
///
/// On `main` the test first fails at the disambiguation premise because
/// ownership is absent. The framing's narrower pre-image is also pinned:
/// keep disambiguation but restore a name-keyed `panel_for_buffer`, and
/// the test reaches the consumer checks and fails at `g`.
///
/// Disambiguation alone leaves the old lookup reading
/// `panels["*test-panel*<2>"]` for a record stored under
/// `"*test-panel*"`, so all three commands return early. Every one of
/// them fails **silently**, so the assertion is on the content each
/// 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_with_roots(&crate::iso::roots());
exec(
&s,
"FOREIGN = pmacs.buffer.create('*test-panel*')\n\
ORIGIN = pmacs.buffer.create('*origin*')\n\
pmacs.window.switch_buffer(ORIGIN)",
);
open_test_panel(&mut s);
assert_eq!(active_name(&s), "*test-panel*<2>", "premise: disambiguated");
// g --- re-render from the data source.
press(&mut s, KeyCode::Char('g'));
let text = active_text(&s);
assert!(text.contains("delta"), "g must re-render: {text:?}");
// RET --- fire on_visit for the row under the cursor.
press(&mut s, KeyCode::Enter);
let visited: Option<String> = eval(&s, "return _G.VISITED");
assert_eq!(
visited.as_deref(),
Some("D"),
"RET must visit the refreshed row"
);
// q --- restore the buffer the panel was opened from.
press(&mut s, KeyCode::Char('q'));
assert_eq!(
active_name(&s),
"*origin*",
"q must restore the previous buffer"
);
}
/// Stage 1 criterion 12 [`main`] — the `q`-target capture is not inverted
/// (Q#GB18), which needs its own criterion because it fails **open**
/// rather than closed. The framing labels it `[main]`.
///
/// On `main` the ownership premise fails first. With disambiguation kept
/// and only `panel_for_buffer` restored to name-keyed lookup, the test
/// reaches and fails the `q`-target assertion the criterion exists for.
///
/// `listview.open`'s guard reads "capture the current buffer as the `q`
/// target, but never another panel (chained panels would trap `q` in a
/// loop)". When the lookup cannot recognise a disambiguated panel it
/// returns nil, the guard reads "not a panel", and the panel is captured
/// as the next panel's `q` target --- producing exactly the loop the
/// guard exists to prevent. Criterion 11 passes with that bug live,
/// because each command works in isolation; only the two-panel sequence
/// shows it.
///
/// *Bite:* restore the name-keyed `panels[d.name]` lookup while keeping
/// 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_with_roots(&crate::iso::roots());
exec(
&s,
"FOREIGN = pmacs.buffer.create('*test-panel*')\n\
ORIGIN = pmacs.buffer.create('*origin*')\n\
pmacs.window.switch_buffer(ORIGIN)",
);
open_test_panel(&mut s);
assert_eq!(active_name(&s), "*test-panel*<2>", "premise: disambiguated");
exec(
&s,
"pmacs.listview.open { name = '*other-panel*', header = 'O', \
rows = { { text = 'x', item = 'X' } } }",
);
assert_eq!(active_name(&s), "*other-panel*", "premise: second panel");
press(&mut s, KeyCode::Char('q'));
assert_ne!(
active_name(&s),
"*test-panel*<2>",
"q must never return into another panel --- the chained-panel loop"
);
assert_eq!(
active_name(&s),
"*scratch*",
"with no capturable previous buffer, q falls back to *scratch*"
);
}
/// Criterion 14 [structural] --- rides **alongside** 1-13, never instead:
/// a structural comparison of two authorities does not catch a misrouted
/// consumer, which is why 11 and 12 assert through `dispatch_key`.
///
/// Three claims, each keyed to a decision: no `bypass_intercept` write
/// survives in either Stage 1 adopter (§1.1's arithmetic is the
/// reference, so the check is per non-comment line rather than a
/// substring sweep that the explanatory comments would trip);
/// `ensure_panel` contains no find-by-name adoption (Q#GB13); and every
/// `panels[` subscript is an append, so none can be keyed by a name
/// derived from `describe.buffer` (Q#GB18).
#[test]
fn s1_14_no_bypass_write_or_name_keyed_identity_remains() {
const LISTVIEW: &str = include_str!("../builtin/runtime/listview.lua");
const DIRED: &str = include_str!("../builtin/runtime/dired.lua");
for (file, src) in [("listview.lua", LISTVIEW), ("dired.lua", DIRED)] {
let writes: Vec<&str> = src
.lines()
.filter(|l| !l.trim_start().starts_with("--") && l.contains("bypass_intercept"))
.collect();
assert!(
writes.is_empty(),
"{file} must contain no bypass_intercept write; found {writes:?}"
);
assert!(
src.contains("set_generated_contents"),
"{file} must write through the authorized primitive"
);
}
assert!(
!LISTVIEW.contains("find_buffer_by_name(name) or pmacs.buffer.create"),
"ensure_panel must not adopt a same-named foreign buffer"
);
let subscripts: Vec<&str> = LISTVIEW
.lines()
.map(str::trim)
.filter(|line| !line.starts_with("--") && line.contains("panels["))
.filter(|line| !line.starts_with("panels[#panels + 1]"))
.collect();
assert!(
subscripts.is_empty(),
"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;