diff --git a/pmacs-protocol/src/message.rs b/pmacs-protocol/src/message.rs index 517fe34..75340d4 100644 --- a/pmacs-protocol/src/message.rs +++ b/pmacs-protocol/src/message.rs @@ -1904,15 +1904,22 @@ pub enum ResourceBody { /// `PanelPointer`, the final v23 `FrontendEvent` variant, so no /// existing discriminant moves. /// -/// **This is the first FRONTEND→INSTANCE extension to need a gate in -/// both directions**, and the reason is that the direction of travel is -/// reversed: for an instance→frontend variant the daemon simply -/// withholds, but here the *frontend* must withhold below -/// [`TEXT_INPUT_MIN_VERSION`] **and** the daemon must refuse what a peer -/// below it nonetheless sends. A client built from this crate can encode -/// the variant whatever it negotiated, so the producer gate alone would -/// leave a v6–v23 session able to drive an edit through a variant its -/// own session never declared. +/// **The gate is producer-side AND receiver-side**, and for an inbound +/// variant the receiving half is the load-bearing one. An +/// instance→frontend variant is gated by the daemon simply not sending +/// it, which is entirely within the daemon's control; an inbound variant +/// cannot be, because the withholding would be the *peer's* job and a +/// client built from this same crate can encode the discriminant +/// whatever it negotiated. So the daemon refuses `TextInput` from a +/// session below [`TEXT_INPUT_MIN_VERSION`] rather than trusting the +/// producer to withhold — otherwise a v6–v23 session could drive an edit +/// through a variant its own session never declared. +/// +/// This is not a new shape: the v19 terminal and v21 panel families +/// already gate their own inbound events on the authenticated session's +/// negotiated version. What is unusual here is only that the extension +/// is **inbound-only** — there is no outbound counterpart to withhold, +/// so the receiver check is the whole of the daemon's half. pub const PROTOCOL_VERSION: u32 = 24; /// Protocol version placed in the daemon's server-first [`Hello`]. @@ -2096,10 +2103,9 @@ pub fn negotiated_session_version(frontend_offer: u32) -> u32 { /// both. [`ADVERTISED_PROTOCOL_VERSION`] does not move. /// /// GUI arc Stage 1a: extended to `[6, ..., 24]` for -/// [`FrontendEvent::TextInput`]. Additive, and gated in **both** -/// directions rather than only daemon-side — see [`PROTOCOL_VERSION`] -/// for why an inbound frontend→instance variant needs the receiving -/// check too. [`ADVERTISED_PROTOCOL_VERSION`] does not move: a v23 +/// [`FrontendEvent::TextInput`]. Additive, and gated producer-side AND +/// receiver-side — see [`PROTOCOL_VERSION`] for why an inbound variant +/// cannot rely on the producer withholding. [`ADVERTISED_PROTOCOL_VERSION`] does not move: a v23 /// frontend negotiates v23, never sends the variant, and keeps today's /// first-scalar behaviour. pub const SUPPORTED_PROTOCOL_VERSIONS: &[u32] = &[ diff --git a/tests/gui_stage1a_acceptance.rs b/tests/gui_stage1a_acceptance.rs index 369e653..e322694 100644 --- a/tests/gui_stage1a_acceptance.rs +++ b/tests/gui_stage1a_acceptance.rs @@ -127,10 +127,38 @@ fn multi_scalar_text_input_creates_no_typed_provenance() { no_record, "a multi-scalar commit must not forge a typed-edit record" ); - let this_command: String = eval(&s, "return pmacs.editor.this_command() or ''"); - assert_ne!( - this_command, "buffer.self-insert", - "a multi-scalar commit is not a self-insert" +} + +/// §5 — a MULTI-scalar commit **breaks the command chain**, as a paste +/// does. +/// +/// **The chain is PRIMED first, and that is what makes the row +/// discriminating.** Starting from a fresh editor the chain is already +/// empty, so an assertion that it is empty afterwards passes whether or +/// not `break_command_chain` is called — the first version of this row +/// did exactly that and would have survived deleting the call. +#[test] +fn multi_scalar_text_input_breaks_a_live_command_chain() { + let mut s = editor_with(""); + + // Prime it with 1a's OWN single-scalar path, which rotates to + // `buffer.self-insert`. A programmatic + // `pmacs.command.invoke('buffer.self-insert')` cannot prime it: + // rotation belongs to the dispatcher, and invoking the command + // directly deliberately never rotates or arms. + s.dispatch_text_input(FID, "x"); + let primed: String = eval(&s, "return pmacs.editor.this_command() or ''"); + assert_eq!( + primed, "buffer.self-insert", + "precondition: the chain is live before the commit" + ); + + s.dispatch_text_input(FID, "e\u{301}"); + + let after: Option = eval(&s, "return pmacs.editor.this_command()"); + assert_eq!( + after, None, + "a multi-scalar commit is not a command and must clear the chain" ); }