test(destination): pin the capture, the profile, and both matrix columns
`docs/destination-capture-framing.md` §7, eight tests. The one that decides the lane is `the_preflight_matrix_holds_in_both _profiles`. Every cell Q#DC-2 marks "not applicable" for the panel profile is asserted as NOT refusing, not merely left untested: a check deliberately omitted and a check someone forgot look identical from the outside, and the next reader restores the second one. The document column re-asserts all four refusals in the same table, so a mutation that collapses the two profiles fails one column or the other. `a_bad_profile_is_refused_by_one_message_that_names_the_accepted_values` is the guard on the argument's TYPE, not only on its behaviour. It asserts the number, table and boolean cases produce the same message as an unrecognized string --- which stops being true the moment the argument is retyped to `Option<String>`, because mlua then rejects the value during argument conversion and the pointed message is never reached. `a_captured_destination_survives_a_frontend_switch` runs under both profiles. The panel profile drops three of the four preflight checks, and a plausible way to implement that is to drop the frontend scope with them --- which would leave a panel continuation resolving its target from ambient state, the exact defect the lane removes. `a_two_argument_commit_takes_the_document_profile` witnesses the default through a check the panel profile omits (a stale buffer), because asserting merely that a legacy call does not error would pass on one silently downgraded to the panel profile. ONE FINDING, RECORDED IN THE TEST RATHER THAN WORKED AROUND. Q#DC-4's "a frontend with no document window" reads as a frontend showing only a bottom panel, and that state is asserted impossible: Q#BP6 says a layout always retains at least one non-side window, and `non_side_target` carries a `debug_assert!` that fires under `cargo test` if one ever does. So with Q#BP6 held, a registered frontend in a healthy editor always has a live document window, and the absent document pair is a DEFENSIVE branch rather than a routine one. The decision still stands --- capture stays total, and an adopter with nowhere to land gets a refusal naming that rather than permission to guess --- and the two Q#DC-4 pins drive the reachable spelling of the same condition: a layout whose document window has gone while the view remains. The helper says so at its definition. `tests/journey_acceptance.rs` (47) and `tests/dired_acceptance.rs` (31) pass UNCHANGED, which is §7's stop signal and the reason the profile default is the document one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
parent
9fee5618ee
commit
96c7e466f1
|
|
@ -0,0 +1,635 @@
|
|||
// tests/destination_capture_acceptance.rs --- the Lua-reachable capture.
|
||||
|
||||
//! Acceptance for `docs/destination-capture-framing.md` §7: a
|
||||
//! destination any asynchronous continuation can capture, and the
|
||||
//! profile that says which of `commit_to`'s preconditions it depends on
|
||||
//! (Q#DC-1 … Q#DC-5).
|
||||
//!
|
||||
//! **What this suite does NOT prove**, deliberately: that git — or any
|
||||
//! other adopter — surfaces in the right frontend. This lane ships the
|
||||
//! mechanism and the tests for the mechanism; adoption is #227's, after
|
||||
//! it lands (§8). Every test here therefore drives the Lua surface
|
||||
//! directly rather than through a consumer.
|
||||
//!
|
||||
//! Two disciplines it keeps:
|
||||
//!
|
||||
//! * **Every "not applicable" cell in Q#DC-2's preflight matrix is
|
||||
//! asserted as NOT refusing**, not merely left untested. A check
|
||||
//! deliberately omitted and a check someone forgot look identical from
|
||||
//! the outside, and the next reader restores the second one.
|
||||
//! * **A refusal is asserted on its reason**, never on the mere fact
|
||||
//! that something failed. `commit_to` has five distinct refusals and a
|
||||
//! raise; "it errored" would pass on any of the wrong ones.
|
||||
//!
|
||||
//! `tests/journey_acceptance.rs` and `tests/dired_acceptance.rs` are the
|
||||
//! preservation half of the same §7 and are run alongside this suite:
|
||||
//! they hold the Stage 1a contract this lane generalizes, and if either
|
||||
//! needed editing the generalization changed Journey semantics rather
|
||||
//! than extending them.
|
||||
|
||||
use pmacs::buffer::BufferId;
|
||||
use pmacs::editor::EditorState;
|
||||
use pmacs::protocol::FrontendId;
|
||||
use pmacs::window::{FrontendView, Layout, Window, WindowId};
|
||||
use tempfile::TempDir;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Harness
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
/// A fresh editor with LSP disabled: no test here asserts anything about
|
||||
/// a language server, so the wipe cannot make an assertion vacuous.
|
||||
fn editor() -> EditorState {
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
s
|
||||
}
|
||||
|
||||
/// A directory with a file worth displaying.
|
||||
fn project() -> TempDir {
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
std::fs::write(td.path().join("alpha.txt"), b"alpha\n").expect("write alpha");
|
||||
td
|
||||
}
|
||||
|
||||
fn active_name(s: &EditorState) -> String {
|
||||
eval(s, "return pmacs.window.buffer():name()")
|
||||
}
|
||||
|
||||
fn buffer_in(s: &EditorState, window: WindowId) -> Option<BufferId> {
|
||||
s.core.borrow().windows.get(&window).map(|w| w.buffer_id)
|
||||
}
|
||||
|
||||
fn local_window(s: &EditorState) -> WindowId {
|
||||
s.core
|
||||
.borrow()
|
||||
.views
|
||||
.get(&FrontendId::LOCAL)
|
||||
.expect("LOCAL view")
|
||||
.active
|
||||
}
|
||||
|
||||
/// The frontend that competes for ambient authority.
|
||||
const COMPETITOR: FrontendId = FrontendId(7);
|
||||
|
||||
/// A frontend that has a layout but no live document window (Q#DC-4).
|
||||
const DOCUMENTLESS: FrontendId = FrontendId(9);
|
||||
|
||||
/// Register a second frontend with its own single-window layout,
|
||||
/// mirroring `build_fresh_frontend_view` — the same helper shape
|
||||
/// `journey_acceptance` and `bottom_panel_stage1_acceptance` use.
|
||||
fn attach_frontend(s: &EditorState, fid: FrontendId) -> WindowId {
|
||||
let win = WindowId::next();
|
||||
let mut core = s.core.borrow_mut();
|
||||
let buffer_id = core.active_buffer_id();
|
||||
let text_view = {
|
||||
let reg = core.registry.borrow();
|
||||
pmacs::text_view::TextView::new(reg.get(buffer_id).expect("buffer"))
|
||||
};
|
||||
core.windows
|
||||
.insert(win, Window::new(win, buffer_id, text_view));
|
||||
core.register_frontend_view(fid, view_over(win));
|
||||
win
|
||||
}
|
||||
|
||||
/// Register a frontend whose layout names a window that is **not live**,
|
||||
/// so `primary_document_window` finds nothing to hand back.
|
||||
///
|
||||
/// **Why this shape and not a side-window-only layout.** The obvious
|
||||
/// reading of "a frontend with no document window" is a frontend showing
|
||||
/// only a bottom panel — but that state is asserted impossible: Q#BP6
|
||||
/// says a layout always retains at least one non-side window, and
|
||||
/// `EditorCore::non_side_target` carries a `debug_assert!` that fires
|
||||
/// under `cargo test` if one ever does. So the reachable spelling of the
|
||||
/// same condition is a layout whose document window has gone while the
|
||||
/// view remains, which is what this builds.
|
||||
///
|
||||
/// **Recorded honestly, because the framing implies more than the tree
|
||||
/// does** (`docs/destination-capture-framing.md` Q#DC-4): with Q#BP6
|
||||
/// held, a *registered* frontend in a healthy editor always has a live
|
||||
/// document window, so the absent document pair is a **defensive**
|
||||
/// branch rather than a routine one. It is still the right decision —
|
||||
/// capture stays total, and an adopter with nowhere to land gets a
|
||||
/// refusal naming that rather than permission to fall back to ambient
|
||||
/// state — and it is still worth pinning, because the alternative to
|
||||
/// pinning it is a branch nothing ever executes.
|
||||
fn attach_documentless_frontend(s: &EditorState, fid: FrontendId) {
|
||||
let mut core = s.core.borrow_mut();
|
||||
core.register_frontend_view(fid, view_over(WindowId::next()));
|
||||
}
|
||||
|
||||
fn view_over(win: WindowId) -> FrontendView {
|
||||
FrontendView {
|
||||
layout: Layout::single(win),
|
||||
active: win,
|
||||
fold_projection: true,
|
||||
panel_capable: true,
|
||||
frame_geometry: None,
|
||||
panel_hidden: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Capture through the **production** Lua entry point and leave the
|
||||
/// userdata in the global `dest`.
|
||||
///
|
||||
/// Nothing in this suite can construct one by any other route — that is
|
||||
/// what `a_forged_destination_is_still_refused` is about — so every test
|
||||
/// below runs against a destination the editor minted.
|
||||
fn capture(s: &EditorState) {
|
||||
exec(s, "dest = pmacs.window.capture_destination()");
|
||||
assert!(
|
||||
eval::<bool>(s, "return dest ~= nil"),
|
||||
"the capture must always yield a destination while a frontend exists"
|
||||
);
|
||||
}
|
||||
|
||||
/// Run `body` under `profile` and report `(ok, reason)`.
|
||||
///
|
||||
/// `profile` is spliced as a Lua expression, so a caller can pass
|
||||
/// `"nil"`, `"'panel'"`, `"42"` — the argument-shape distinctions
|
||||
/// Q#DC-5 turns on are exactly what this suite has to vary.
|
||||
fn commit(s: &EditorState, profile: Option<&str>) {
|
||||
let call = match profile {
|
||||
Some(profile) => format!("pmacs.window.commit_to(dest, body, {profile})"),
|
||||
None => "pmacs.window.commit_to(dest, body)".to_string(),
|
||||
};
|
||||
exec(
|
||||
s,
|
||||
&format!(
|
||||
"ran = false
|
||||
local body = function() ran = true end
|
||||
raised = nil
|
||||
local caught, a, b = pcall(function() return {call} end)
|
||||
if caught then ok, reason = a, b
|
||||
else ok, reason, raised = false, nil, tostring(a) end"
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
fn ok(s: &EditorState) -> bool {
|
||||
eval(s, "return ok == true")
|
||||
}
|
||||
|
||||
fn ran(s: &EditorState) -> bool {
|
||||
eval(s, "return ran")
|
||||
}
|
||||
|
||||
fn reason(s: &EditorState) -> String {
|
||||
eval(s, "return tostring(reason)")
|
||||
}
|
||||
|
||||
/// The message a raise (as opposed to a `(false, reason)` refusal)
|
||||
/// carried, or `None` if nothing was raised.
|
||||
fn raised(s: &EditorState) -> Option<String> {
|
||||
eval::<Option<String>>(s, "return raised")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// §7 — a captured destination survives a frontend switch
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// **N** — the failure the lane exists for: the result lands in the
|
||||
/// frontend that *asked*, not in whichever one is ambient when the work
|
||||
/// settles.
|
||||
///
|
||||
/// Asserted for **both** profiles. The panel profile drops three of the
|
||||
/// four preflight checks, and a plausible way to implement that is to
|
||||
/// drop the scope with them — which would leave a panel continuation
|
||||
/// resolving its target from ambient state, the exact P1a defect. So the
|
||||
/// scope is pinned per profile rather than once.
|
||||
///
|
||||
/// Falsified by making the commit display ambiently: the file then
|
||||
/// appears in the competitor's window. Asserting merely that
|
||||
/// `capture_destination()` returns userdata would pass on a capture that
|
||||
/// does nothing.
|
||||
#[test]
|
||||
fn a_captured_destination_survives_a_frontend_switch() {
|
||||
for profile in [None, Some("'panel'")] {
|
||||
let td = project();
|
||||
let s = editor();
|
||||
capture(&s);
|
||||
|
||||
let local_win = local_window(&s);
|
||||
let other_win = attach_frontend(&s, COMPETITOR);
|
||||
let other_before = buffer_in(&s, other_win);
|
||||
|
||||
// The competitor becomes the dispatching frontend while the work
|
||||
// is "in flight" — the state a worker completion returns to.
|
||||
s.core.borrow_mut().active_frontend = COMPETITOR;
|
||||
|
||||
let alpha = td.path().join("alpha.txt").display().to_string();
|
||||
exec(
|
||||
&s,
|
||||
&format!(
|
||||
"committed = pmacs.window.commit_to(dest, function()
|
||||
pmacs.window.display_file({alpha:?})
|
||||
end{})",
|
||||
profile.map_or(String::new(), |p| format!(", {p}"))
|
||||
),
|
||||
);
|
||||
|
||||
assert!(
|
||||
eval::<bool>(&s, "return committed"),
|
||||
"{profile:?}: the commit must be accepted"
|
||||
);
|
||||
assert_eq!(
|
||||
buffer_in(&s, other_win),
|
||||
other_before,
|
||||
"{profile:?}: the competing frontend's window must be untouched"
|
||||
);
|
||||
s.core.borrow_mut().active_frontend = FrontendId::LOCAL;
|
||||
assert_eq!(
|
||||
active_name(&s),
|
||||
alpha,
|
||||
"{profile:?}: the commit must land in the capturing frontend's window"
|
||||
);
|
||||
assert_eq!(
|
||||
local_window(&s),
|
||||
local_win,
|
||||
"{profile:?}: and in that window, not a new one"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// §7 — the forged destination stays refused
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// **P (Q#JR14d)** — generalizing the capture does not widen what
|
||||
/// extension code can fabricate.
|
||||
///
|
||||
/// A plausible `{frontend, window, buffer}` table is what any Lua could
|
||||
/// build, and the capture now hands out the *same* userdata type through
|
||||
/// a public entry point — so the type check is re-asserted after the
|
||||
/// rename rather than assumed to have survived it.
|
||||
///
|
||||
/// *Mutation:* accept `mlua::Value::Table` in the borrow arm. This
|
||||
/// fails; nothing in `journey_acceptance` covers the new entry point.
|
||||
#[test]
|
||||
fn a_forged_destination_is_still_refused() {
|
||||
let s = editor();
|
||||
capture(&s);
|
||||
let win = eval::<i64>(&s, "return dest:window()");
|
||||
|
||||
exec(
|
||||
&s,
|
||||
&format!(
|
||||
"ran = false
|
||||
local caught, err = pcall(pmacs.window.commit_to,
|
||||
{{ frontend = 0, window = {win}, buffer = 0 }},
|
||||
function() ran = true end)
|
||||
rejected = (not caught) and tostring(err) or '<accepted>'"
|
||||
),
|
||||
);
|
||||
|
||||
let rejected: String = eval(&s, "return rejected");
|
||||
assert!(
|
||||
rejected.contains("cannot be constructed from Lua"),
|
||||
"a forged table must be rejected by type, not merely fail later; got {rejected:?}"
|
||||
);
|
||||
assert!(
|
||||
!ran(&s),
|
||||
"a rejected destination must not reach the callback"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// §7 — the preflight matrix, in BOTH profiles (Q#DC-2)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// **N** — each of the four preconditions refuses under the document
|
||||
/// profile, and each of the three the panel profile omits does **not**
|
||||
/// refuse under it.
|
||||
///
|
||||
/// This is the substance of Q#DC-2. The matrix:
|
||||
///
|
||||
/// | # | precondition | document | panel |
|
||||
/// |---|--------------|----------|-------|
|
||||
/// | 1 | frontend has a layout | required | **required** |
|
||||
/// | 2 | window still live | required | not applicable |
|
||||
/// | 3 | window still shows the captured buffer | required | not applicable |
|
||||
/// | 4 | window is not dedicated | required | not applicable |
|
||||
///
|
||||
/// The panel column is the half that could not be written before this
|
||||
/// lane, and the half most at risk of being "fixed" later by someone who
|
||||
/// reads an omission as an oversight — a panel result does not occupy
|
||||
/// the captured document window, does not replace its buffer, and does
|
||||
/// not need it to exist, so each of checks 2–4 would refuse `git.status`
|
||||
/// for a document-window change unrelated to where the panel goes.
|
||||
///
|
||||
/// Table-driven so the failure message names *which* cell regressed,
|
||||
/// which eight near-identical tests would give up in exchange for
|
||||
/// nothing.
|
||||
///
|
||||
/// *Mutation:* apply all four checks in both profiles — the three panel
|
||||
/// rows fail. *Second mutation:* apply only check 1 in both profiles —
|
||||
/// the three document rows fail.
|
||||
#[test]
|
||||
fn the_preflight_matrix_holds_in_both_profiles() {
|
||||
// (label, Lua that breaks the precondition, reason fragment,
|
||||
// whether the PANEL profile refuses too)
|
||||
let cases: [(&str, &str, &str, bool); 4] = [
|
||||
(
|
||||
"frontend gone",
|
||||
// Handled in Rust below: unregistering a view has no Lua surface.
|
||||
"",
|
||||
"requesting frontend is gone",
|
||||
true,
|
||||
),
|
||||
(
|
||||
"window gone",
|
||||
"local doomed = dest:window()
|
||||
pmacs.window.split_horizontal()
|
||||
while pmacs.window.current() == doomed do pmacs.window.focus_next() end
|
||||
pmacs.window.close_others()",
|
||||
"is gone",
|
||||
false,
|
||||
),
|
||||
(
|
||||
"stale buffer",
|
||||
"pmacs.window.switch_buffer(pmacs.buffer.create('*usurper*'))",
|
||||
"now shows another buffer",
|
||||
false,
|
||||
),
|
||||
(
|
||||
"dedicated",
|
||||
"pmacs.window.set_params(dest:window(), { dedicated = true })",
|
||||
"is dedicated",
|
||||
false,
|
||||
),
|
||||
];
|
||||
|
||||
for (label, break_it, expected, panel_refuses) in cases {
|
||||
for profile in [None, Some("'panel'")] {
|
||||
let s = editor();
|
||||
capture(&s);
|
||||
|
||||
if label == "frontend gone" {
|
||||
s.core
|
||||
.borrow_mut()
|
||||
.unregister_frontend_view(FrontendId::LOCAL);
|
||||
} else {
|
||||
exec(&s, break_it);
|
||||
}
|
||||
|
||||
commit(&s, profile);
|
||||
assert_eq!(
|
||||
raised(&s),
|
||||
None,
|
||||
"{label}/{profile:?}: a precondition is a refusal, not a raise"
|
||||
);
|
||||
|
||||
let refuses = profile.is_none() || panel_refuses;
|
||||
if refuses {
|
||||
assert!(!ok(&s), "{label}/{profile:?}: commit_to must refuse");
|
||||
assert!(
|
||||
reason(&s).contains(expected),
|
||||
"{label}/{profile:?}: reason must say why; wanted {expected:?}, got {:?}",
|
||||
reason(&s)
|
||||
);
|
||||
assert!(
|
||||
!ran(&s),
|
||||
"{label}/{profile:?}: the callback must not run at all -- validating \
|
||||
after it is four mutations too late"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
ok(&s),
|
||||
"{label}/panel: this check is DELIBERATELY omitted for a panel \
|
||||
result, which touches no document window; got refusal {:?}",
|
||||
reason(&s)
|
||||
);
|
||||
assert!(ran(&s), "{label}/panel: the callback must run");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// §7 — the profile argument (Q#DC-5)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// **P** — a two-argument `commit_to(dest, body)` takes the **document**
|
||||
/// profile, so every caller written before the profile existed keeps all
|
||||
/// four checks.
|
||||
///
|
||||
/// Witnessed by a check the panel profile omits — a stale buffer.
|
||||
/// Asserting merely that the call does not error would pass on a legacy
|
||||
/// call silently downgraded to the panel profile, which is the
|
||||
/// regression that would quietly void Journey Stage 1a's guarantees for
|
||||
/// dired and every future two-argument caller.
|
||||
///
|
||||
/// *Mutation:* default the profile to `Panel`. This fails;
|
||||
/// `journey_acceptance` also fails, which is the point — the default is
|
||||
/// what makes that suite's untouched pass a consequence of the signature
|
||||
/// rather than of care.
|
||||
#[test]
|
||||
fn a_two_argument_commit_takes_the_document_profile() {
|
||||
let s = editor();
|
||||
capture(&s);
|
||||
exec(
|
||||
&s,
|
||||
"pmacs.window.switch_buffer(pmacs.buffer.create('*usurper*'))",
|
||||
);
|
||||
|
||||
commit(&s, None);
|
||||
|
||||
assert!(
|
||||
!ok(&s),
|
||||
"a two-argument commit must keep the stale-intent check"
|
||||
);
|
||||
assert!(
|
||||
reason(&s).contains("now shows another buffer"),
|
||||
"and refuse for that reason; got {:?}",
|
||||
reason(&s)
|
||||
);
|
||||
assert!(!ran(&s), "the callback must not run");
|
||||
}
|
||||
|
||||
/// **N** — an explicit `nil` profile is the document profile, exactly as
|
||||
/// omitting it is.
|
||||
///
|
||||
/// Witnessed separately from the two-argument case rather than assumed
|
||||
/// equivalent: a Lua caller threading an optional variable produces
|
||||
/// `commit_to(dest, body, nil)`, and a third behaviour there would stay
|
||||
/// invisible until someone hit it in production.
|
||||
///
|
||||
/// *Mutation:* treat `Value::Nil` as an unrecognized profile. This
|
||||
/// fails; the two-argument test above does not, because mlua supplies
|
||||
/// `Nil` for a missing argument either way only if the binding asks for
|
||||
/// a `Value` — which is the type this suite also pins below.
|
||||
#[test]
|
||||
fn an_explicit_nil_profile_is_the_document_profile() {
|
||||
let s = editor();
|
||||
capture(&s);
|
||||
exec(
|
||||
&s,
|
||||
"pmacs.window.switch_buffer(pmacs.buffer.create('*usurper*'))",
|
||||
);
|
||||
|
||||
commit(&s, Some("nil"));
|
||||
|
||||
assert_eq!(
|
||||
raised(&s),
|
||||
None,
|
||||
"an explicit nil must not be treated as a bad profile"
|
||||
);
|
||||
assert!(!ok(&s), "an explicit nil must keep the stale-intent check");
|
||||
assert!(
|
||||
reason(&s).contains("now shows another buffer"),
|
||||
"and refuse for that reason; got {:?}",
|
||||
reason(&s)
|
||||
);
|
||||
}
|
||||
|
||||
/// **N** — an unrecognized profile is an ERROR naming the accepted
|
||||
/// values, and a non-string profile is refused by the **same** message.
|
||||
///
|
||||
/// Two claims, one test, because their whole content is that they agree:
|
||||
///
|
||||
/// * a fallback to `"document"` would hand a caller different checks
|
||||
/// than it asked for — the failure the parameterization exists to
|
||||
/// prevent — so an unknown string raises;
|
||||
/// * **this is the guard on the argument's type.** With
|
||||
/// `profile: Option<String>` mlua rejects `42` and `{}` during
|
||||
/// argument *conversion*, before the closure body runs, and the
|
||||
/// message below becomes unreachable — the caller gets a generic
|
||||
/// conversion error naming neither the rule nor the vocabulary. So the
|
||||
/// number and table cases are asserted on the message's *content* and
|
||||
/// against the string case's message, not merely on "an error
|
||||
/// occurred".
|
||||
///
|
||||
/// *Mutation:* retype the argument to `Option<String>`. The number and
|
||||
/// table rows fail.
|
||||
#[test]
|
||||
fn a_bad_profile_is_refused_by_one_message_that_names_the_accepted_values() {
|
||||
let mut messages = Vec::new();
|
||||
for (label, profile) in [
|
||||
("unknown string", "'documents'"),
|
||||
("number", "42"),
|
||||
("table", "{}"),
|
||||
("boolean", "true"),
|
||||
] {
|
||||
let s = editor();
|
||||
capture(&s);
|
||||
commit(&s, Some(profile));
|
||||
|
||||
let raised = raised(&s).unwrap_or_else(|| panic!("{label}: a bad profile must raise"));
|
||||
assert!(
|
||||
raised.contains("\"document\"") && raised.contains("\"panel\""),
|
||||
"{label}: the message must name both accepted values; got {raised:?}"
|
||||
);
|
||||
assert!(
|
||||
raised.contains("must be the string"),
|
||||
"{label}: and say a string was expected; got {raised:?}"
|
||||
);
|
||||
assert!(
|
||||
!ran(&s),
|
||||
"{label}: a bad profile must not reach the callback"
|
||||
);
|
||||
messages.push((label, raised));
|
||||
}
|
||||
|
||||
let (_, first) = &messages[0];
|
||||
for (label, message) in &messages[1..] {
|
||||
assert_eq!(
|
||||
message, first,
|
||||
"{label}: a non-string profile must be refused by the SAME message as an \
|
||||
unrecognized one -- a different message means mlua rejected the value \
|
||||
during argument conversion, which is what `Option<String>` would do"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// §7 — no document window (Q#DC-4)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// **N** — a frontend with no live document window still captures, and
|
||||
/// the destination reports the absence.
|
||||
///
|
||||
/// Asserted as a *successful* capture rather than as `nil`: returning
|
||||
/// `nil` here would push the adopter back onto ambient behaviour, which
|
||||
/// is the P1a bug this lane removes. An adopter with nowhere to land
|
||||
/// gets a refusal it can report; it does not get permission to guess.
|
||||
///
|
||||
/// The `window()` accessor reporting **nil** is the other half: the pair
|
||||
/// is set or cleared together, so no consumer ever sees a window id
|
||||
/// without the buffer that was captured with it.
|
||||
///
|
||||
/// *Mutation:* return `None` from `capture_view_destination` when
|
||||
/// `primary_document_window` finds nothing. This fails on the capture
|
||||
/// assertion inside the helper. *Second mutation:* keep `window` while
|
||||
/// clearing `buffer`. This fails here.
|
||||
#[test]
|
||||
fn capture_succeeds_with_no_document_window() {
|
||||
let s = editor();
|
||||
attach_documentless_frontend(&s, DOCUMENTLESS);
|
||||
s.core.borrow_mut().active_frontend = DOCUMENTLESS;
|
||||
|
||||
capture(&s);
|
||||
|
||||
assert!(
|
||||
eval::<bool>(&s, "return dest:window() == nil"),
|
||||
"the document pair must be reported as ABSENT, not invented"
|
||||
);
|
||||
}
|
||||
|
||||
/// **N** — on that destination a panel commit **succeeds** and a
|
||||
/// document commit is **refused**, naming the missing window.
|
||||
///
|
||||
/// Both halves, because asserting only the refusal would pass on a
|
||||
/// capture that refuses everything, and asserting only the success would
|
||||
/// pass on one that checks nothing. Together they are Q#DC-4's decision:
|
||||
/// the document pair is optional, and the profile is what decides
|
||||
/// whether its absence matters.
|
||||
///
|
||||
/// The refusal is a `(false, reason)` like the other four rather than a
|
||||
/// raise, so an adopter handles all five the same way.
|
||||
///
|
||||
/// *Mutation:* drop the `dest.window == None` arm. The document half
|
||||
/// then commits against no window at all.
|
||||
#[test]
|
||||
fn a_panel_commit_succeeds_where_a_document_commit_is_refused() {
|
||||
let s = editor();
|
||||
attach_documentless_frontend(&s, DOCUMENTLESS);
|
||||
s.core.borrow_mut().active_frontend = DOCUMENTLESS;
|
||||
capture(&s);
|
||||
|
||||
commit(&s, Some("'panel'"));
|
||||
assert!(
|
||||
ok(&s),
|
||||
"a panel result needs only a live frontend; got refusal {:?}",
|
||||
reason(&s)
|
||||
);
|
||||
assert!(ran(&s), "and its callback must run");
|
||||
|
||||
commit(&s, None);
|
||||
assert_eq!(
|
||||
raised(&s),
|
||||
None,
|
||||
"the missing document window joins the preflight refusals rather than raising"
|
||||
);
|
||||
assert!(!ok(&s), "a document commit has nowhere to land");
|
||||
assert!(
|
||||
reason(&s).contains("no document window"),
|
||||
"and must say so; got {:?}",
|
||||
reason(&s)
|
||||
);
|
||||
assert!(!ran(&s), "and must not reach the callback");
|
||||
}
|
||||
|
||||
// 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;
|
||||
Loading…
Reference in New Issue