diff --git a/docs/auto-pairing-framing.md b/docs/auto-pairing-framing.md index 78375e2..4c89fe2 100644 --- a/docs/auto-pairing-framing.md +++ b/docs/auto-pairing-framing.md @@ -47,6 +47,13 @@ context-switching command no longer attributes them to the destination buffer); and non-table set containers degrade language→default→empty instead of throwing from the callback. +Revision 6: PR #110 round 3 — coverage pins only, no code changes: +the predicate's raw-byte posture is pinned from the buffer side +(`(` typed before a lone `0xFF` inserts no closer; a regression to +nil-on-malformed would read junk as end-of-buffer), and the +top-level non-table `pmacs.pair.sets` container is pinned alongside +the per-entry cases (pairs nothing, clean `*errors*`). + ## Ground truth (as of `7e127ab`) - **Dispatch is keymap-first for printables** — `Char('(')` resolves @@ -488,9 +495,11 @@ facility is the only way a consumed record outlives its fan-out (R4). and skips at byte-correct cursors. Ill-formed UTF-8 closers (R5) — truncated `"(\xC2x"`, overlong `"(\xC0\xAF"`, surrogate `"(\xED\xA0\x80"`, beyond-U+10FFFF `"(\xF5\x80\x80\x80"` — all - pair nothing. Non-table containers (R5): a string `default` pairs - nothing without erroring; a junk language entry falls back to the - default set. + pair nothing. Non-table containers (R5/R6): a string `default` and + a non-table `pmacs.pair.sets` itself pair nothing without erroring; + a junk language entry falls back to the default set. Malformed + BUFFER bytes (R6): `(` typed immediately before a lone `0xFF` + stays unpaired — junk is word-like, not EOL-like. - Source-buffer relevance (R5): `'` typed in Rust with a context-switching command landing in Python stays silent; the inverse Python→Rust route still reports "source context changed". diff --git a/tests/auto_pair_acceptance.rs b/tests/auto_pair_acceptance.rs index 09fee39..29291b3 100644 --- a/tests/auto_pair_acceptance.rs +++ b/tests/auto_pair_acceptance.rs @@ -322,6 +322,52 @@ fn multibyte_pair_entries_pair_and_skip() { assert_eq!(cursor(&s), 4); } +#[test] +fn opener_before_malformed_buffer_bytes_does_not_pair() { + // PR #110 round 3, finding 1: the predicate's raw-byte posture, + // pinned from the BUFFER side. A byte that begins no well-formed + // sequence (0xFF) sits after the cursor; `char_at` must surface + // it as a raw byte the predicate treats as word-like — a + // regression back to nil would read it as end-of-buffer and pair + // before the junk. + let mut s = editor_with(""); + exec(&s, "pmacs.window.buffer():insert(0, string.char(0xFF))"); + exec(&s, "pmacs.editor.goto_byte(0)"); + type_str(&mut s, "("); + let len: i64 = eval(&s, "return pmacs.window.buffer():len()"); + assert_eq!(len, 2, "opener + junk byte only — a closer would make 3"); + let shape_ok: bool = eval( + &s, + "local b = pmacs.window.buffer() \ + return b:slice(0, 1) == \"(\" and b:slice(1, 2):byte(1) == 0xFF", + ); + assert!( + shape_ok, + "the opener stands alone before the malformed byte" + ); + assert!( + !status(&s).contains("auto-pair"), + "declining the predicate is silent; got: {:?}", + status(&s) + ); +} + +#[test] +fn non_table_sets_container_fails_closed_without_erroring() { + // PR #110 round 3, finding 2: the TOP-LEVEL container guard, + // pinned. `set_for` degrades a non-table `pmacs.pair.sets` to the + // empty set; the callback must not throw. + let mut s = editor_with(""); + exec(&s, "pmacs.pair.sets = 42"); + type_str(&mut s, "("); + assert_eq!(buffer_text(&s), "(", "a non-table container pairs nothing"); + let log = s.lua_host.errors_buffer_text(); + assert!( + !log.contains("pair"), + "the pairing callback must not error over a config typo; *errors*:\n{log}" + ); +} + #[test] fn non_table_default_set_fails_closed_without_erroring() { // PR #110 round 2, finding 3: a config typo assigning a STRING