diff --git a/docs/active-work.md b/docs/active-work.md index a0e5a9d..9fb49bb 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -433,6 +433,19 @@ from #171 and #215. **1.146× is the smallest margin this budget has ever failed by** (U6 1.297×, U10 1.343×, U9 1.613×), and the "realistic" figure was **negative** (−25.8%) in the same run. Isolated rerun green. + - **`setsid_escapee_is_not_reaped_and_teardown_reclaims_readers`, + once** (log `20260815T184808Z`, step `08-sweep`). Fragment: `live + runtime probe` at `src/process.rs:5155` — `active_reader_probe` + returned `None` for a process that had just reported `Started`. A + live-process race under sweep load. Isolated rerun green. **Not in + the registry under any id**, so it is a new signature, owed like + the two above. + - **Three consecutive full-gate runs, three DIFFERENT unrelated + failures** (composition budget in `04-lib-crdt`, then composition + again in `10-sweep-crdt`, then this in `08-sweep`), against a diff + that touches panels and the wire. Recorded as a rate observation + only: no mechanism is claimed, and the standing leaked-daemon + confound is uncontrolled as always. - **Cost, stated plainly:** four `--protocol` gate runs on one commit, three of them lost to these two signatures. U9's synthetic-load control remains unrun and is the cheapest thing that would either diff --git a/pmacs-gpu/src/main.rs b/pmacs-gpu/src/main.rs index 94299cc..b278823 100644 --- a/pmacs-gpu/src/main.rs +++ b/pmacs-gpu/src/main.rs @@ -860,37 +860,56 @@ fn run_headless_probe(socket: &Path, report: &Path) -> i32 { // the retained old frame supplied the rendering. The // probe would report the band showing something it does // not show. + // The payload KIND is kept only to tell a real `Absent` + // apart from a refusal; every fact below comes from the + // accepted state. + let panel_absent_payload = matches!( + msg.as_ref(), + InstanceMessage::PanelFrame(pmacs_protocol::panel::PanelFramePayload::Absent) + ); let panel_message = matches!(msg.as_ref(), InstanceMessage::PanelFrame(_)); - let panel_before = state + // The COMPLETE accepted authority, both halves. An + // epoch/size triple is not enough: ordinary content, + // focus, cursor and mapping-generation updates all leave + // it unchanged, so accepted frames would go uncounted — + // including the identical-frame/higher-generation case + // this slice requires — and a fixture waiting for two + // frames would wait forever. + let authority_before = state .panel .presented() - .map(|frame| (frame.panel_epoch, frame.geometry_epoch, frame.size)); + .cloned() + .map(|frame| (frame, state.panel.mapping_generation)); state.apply_attach_message(*msg); if panel_message { - match state.panel.presented() { - Some(frame) => { - let identity = (frame.panel_epoch, frame.geometry_epoch, frame.size); - // Counted only when the retained frame - // actually moved: a duplicate or a refusal - // leaves the band exactly as it was, and - // counting either would say the daemon is - // painting when it is not. - if panel_before != Some(identity) { - facts.panel_frames += 1; - } - facts.panel_rows = frame.size.rows; - facts.panel_cols = frame.size.cols; - facts.panel_focused = frame.focused; - facts.panel_frame_text = grid_probe_text(&frame.cells); - if let Some(expected) = expected_panel_text.as_deref() - && facts.panel_frame_text.contains(expected) - { - facts.panel_text_observed = true; - } + let authority_after = state + .panel + .presented() + .cloned() + .map(|frame| (frame, state.panel.mapping_generation)); + if authority_before != authority_after { + facts.panel_frames += 1; + } + if let Some((frame, _)) = authority_after.as_ref() { + facts.panel_rows = frame.size.rows; + facts.panel_cols = frame.size.cols; + facts.panel_focused = frame.focused; + facts.panel_frame_text = grid_probe_text(&frame.cells); + if let Some(expected) = expected_panel_text.as_deref() + && facts.panel_frame_text.contains(expected) + { + facts.panel_text_observed = true; } - None => facts.panel_absent_observed = true, + } + // Absence is only ever reported for an actual + // `Absent`. Inferring it from `presented() == None` + // turns a REFUSAL with nothing retained into "the + // daemon says there is no band", which is a + // different fact entirely. + if panel_absent_payload { + facts.panel_absent_observed = true; } } if is_snapshot { @@ -19983,6 +20002,139 @@ mod tests { } } + /// §5b G8b — a MAPPED frontend refuses the legacy family, and the + /// refusal is ATOMIC. + /// + /// The baseline is accepted in the CORRECT family first, so there is + /// real authority to preserve: a row that installs a legacy frame + /// and then switches to mapped has `mapping_generation == None` + /// throughout, and asserting it stayed `None` proves nothing. The + /// pointer latches are primed for the same reason — untouched state + /// that was never set is not evidence of atomicity. + #[test] + fn g8b_a_mapped_frontend_refuses_legacy_atomically() { + let Some(mut state) = headless_or_skip(800, 600, "alpha") else { + return; + }; + let frame = present_panel(&mut state, 4); + state.set_panel_wire(pmacs_protocol::PANEL_MAPPING_MIN_VERSION); + + // A correct-family baseline, so the retained authority is real. + let mut baseline = frame.clone(); + baseline.panel_epoch = frame.panel_epoch + 1; + assert!( + state.apply_panel_payload(PanelFramePayload::PresentMapped { + frame: baseline.clone(), + mapping_generation: 5, + }), + "fixture: the mapped baseline must install" + ); + state.set_panel_pointer_held(true); + state.panel.last_pointer_cell = Some(pmacs_protocol::CellCoord::new(1, 1)); + + let mut intruder = frame.clone(); + intruder.panel_epoch = frame.panel_epoch + 2; + assert!( + !state.apply_panel_payload(PanelFramePayload::Present(intruder)), + "a v25 frontend must not paint a band it cannot safely \ + hit-test — the frame carries no mapping identity" + ); + + assert_eq!( + state.panel.frame.as_ref().map(|f| f.panel_epoch), + Some(baseline.panel_epoch), + "the retained frame survives the refusal" + ); + assert_eq!( + state.panel.mapping_generation, + Some(5), + "and so does its authority — this is the half a switched \ + baseline could not have shown" + ); + assert!(state.panel.pointer_held, "a refusal ends no live gesture"); + assert_eq!( + state.panel.last_pointer_cell, + Some(pmacs_protocol::CellCoord::new(1, 1)), + "nor discards its dedupe baseline" + ); + } + + /// §5b G8d — a LEGACY frontend refuses the mapped family, atomically. + /// + /// An independent state, not a continuation of G8b: sharing one + /// would let the second direction inherit whatever the first left + /// behind. + #[test] + fn g8d_a_legacy_frontend_refuses_mapped_atomically() { + let Some(mut state) = headless_or_skip(800, 600, "alpha") else { + return; + }; + let frame = present_panel(&mut state, 4); + state.set_panel_wire(PANEL_MIN_VERSION); + + let mut baseline = frame.clone(); + baseline.panel_epoch = frame.panel_epoch + 1; + assert!( + state.apply_panel_payload(PanelFramePayload::Present(baseline.clone())), + "fixture: the legacy baseline must install" + ); + state.set_panel_pointer_held(true); + state.panel.last_pointer_cell = Some(pmacs_protocol::CellCoord::new(2, 2)); + + let mut intruder = frame.clone(); + intruder.panel_epoch = frame.panel_epoch + 2; + assert!( + !state.apply_panel_payload(PanelFramePayload::PresentMapped { + frame: intruder, + mapping_generation: 9, + }), + "a v24 frontend must reject a family it never negotiated" + ); + + assert_eq!( + state.panel.frame.as_ref().map(|f| f.panel_epoch), + Some(baseline.panel_epoch), + "the retained frame survives" + ); + assert_eq!( + state.panel.mapping_generation, None, + "and no authority is acquired from a refused frame" + ); + assert!(state.panel.pointer_held, "a refusal ends no live gesture"); + assert_eq!( + state.panel.last_pointer_cell, + Some(pmacs_protocol::CellCoord::new(2, 2)) + ); + } + + /// §5b — an UNSUPPORTED session accepts neither family. + /// + /// The case a `!= Mapped` gate let through: `Unsupported` is neither + /// family, so a negative test admitted a band the session never + /// negotiated. + #[test] + fn an_unsupported_session_accepts_neither_panel_family() { + let Some(mut state) = headless_or_skip(800, 600, "alpha") else { + return; + }; + let frame = present_panel(&mut state, 4); + state.set_panel_wire(PANEL_MIN_VERSION - 1); + + let mut orphan = frame.clone(); + orphan.panel_epoch = frame.panel_epoch + 1; + assert!( + !state.apply_panel_payload(PanelFramePayload::Present(orphan.clone())), + "a sub-panel session has no band at all" + ); + assert!( + !state.apply_panel_payload(PanelFramePayload::PresentMapped { + frame: orphan, + mapping_generation: 9, + }), + "in either family" + ); + } + /// F1 — a held left button makes motion a `Drag(Left)`, and the dedupe /// re-arms on every press and release. #[test] diff --git a/src/daemon.rs b/src/daemon.rs index 8d5db9a..0ab1bce 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -6074,6 +6074,16 @@ mod tests { /// /// The session is registered because the dispatcher drops any event /// from an uninstalled session before it reaches a handler. + /// §5b G8e — the frontend a forged payload claims to be. Registered + /// in every dispatch fixture at the MAPPED version, so "the claimed + /// id speaks the other family" is a real condition rather than an + /// absent session. + const FORGERY_TARGET_FID: FrontendId = FrontendId(767); + + /// §5b G8e, the other direction: a frontend that speaks the LEGACY + /// family, for a mapped session to try to borrow. + const FORGERY_TARGET_LEGACY_FID: FrontendId = FrontendId(768); + /// §5b — a session that negotiated the LEGACY panel family. /// /// These rows drive `FrontendEvent::PanelPointer`, which a `>= v25` @@ -6102,6 +6112,23 @@ mod tests { let mut bells = HashMap::new(); let mut registry = SessionRegistry::new(); registry.register_session(fid, session(version, !render_states.contains_key(&fid))); + // §5b G8e — a SECOND session that speaks the mapped family, so a + // payload claiming its id has something real to borrow. Without + // it, a payload-keyed lookup would fail for want of a session + // rather than for want of authority, and the mutation that + // swaps `source` for the payload's id would be invisible. + if fid != FORGERY_TARGET_FID { + registry.register_session( + FORGERY_TARGET_FID, + session(pmacs_protocol::PANEL_MAPPING_MIN_VERSION, true), + ); + } + if fid != FORGERY_TARGET_LEGACY_FID { + registry.register_session( + FORGERY_TARGET_LEGACY_FID, + session(LEGACY_PANEL_VERSION, true), + ); + } handle_dispatcher_event( DispatcherEvent::FrontendEvent { source: fid, event }, editor, @@ -6305,6 +6332,344 @@ mod tests { (frame.geometry_epoch, frame.panel_epoch) } + // ----------------------------------------------------------------- + // §5b G6–G8 — the family gate, both directions, all four quadrants. + // + // Every row negotiates ONE version for both the session registry and + // the retained producer: a control that negotiates two proves + // nothing about either. + // ----------------------------------------------------------------- + + /// A panel session at one negotiated version: the editor, its + /// producer, its render states, the side window, and the epochs a + /// gesture must echo. + type PanelSessionFixture = ( + crate::editor::EditorState, + HashMap, + HashMap, + crate::window::WindowId, + crate::window::WindowId, + (u64, u64), + ); + + /// Build a panel session at one negotiated version and ship its + /// declaration, returning the epochs a gesture must echo. + fn panel_session_at(version: u32, fid: FrontendId) -> PanelSessionFixture { + let editor = crate::editor::EditorState::new(); + let (document, panel) = semantic_panel_view(&editor, fid, true); + let panel = panel.expect("panel window"); + let mut semantic_states = HashMap::new(); + semantic_states.insert( + fid, + crate::semantic_render::SemanticRenderState::for_peer(fid, version), + ); + editor.accept_semantic_frame_geometry(fid, 1, CellSize::new(24, 80)); + let epochs = shipped_declaration(&editor, fid, &mut semantic_states); + ( + editor, + semantic_states, + HashMap::new(), + document, + panel, + epochs, + ) + } + + /// The mapping generation this session's producer most recently + /// stamped, which a mapped gesture must echo. + fn stamped_generation( + semantic_states: &HashMap, + fid: FrontendId, + ) -> u64 { + semantic_states + .get(&fid) + .expect("semantic projection") + .panel_mapping_generation_peek() + .expect("a stamped mapping generation") + } + + fn legacy_pointer( + fid: FrontendId, + epochs: (u64, u64), + buffer_id: crate::buffer::BufferId, + ) -> FrontendEvent { + FrontendEvent::PanelPointer { + frontend_id: fid, + geometry_epoch: epochs.0, + panel_epoch: epochs.1, + buffer_id, + coord: pmacs_protocol::CellCoord::new(0, 0), + kind: pmacs_protocol::MouseKind::Down(pmacs_protocol::MouseButton::Left), + mods: pmacs_protocol::Modifiers::default(), + } + } + + fn mapped_pointer( + fid: FrontendId, + epochs: (u64, u64), + buffer_id: crate::buffer::BufferId, + mapping_generation: u64, + ) -> FrontendEvent { + FrontendEvent::PanelPointerMapped { + frontend_id: fid, + geometry_epoch: epochs.0, + panel_epoch: epochs.1, + buffer_id, + coord: pmacs_protocol::CellCoord::new(0, 0), + kind: pmacs_protocol::MouseKind::Down(pmacs_protocol::MouseButton::Left), + mods: pmacs_protocol::Modifiers::default(), + mapping_generation, + } + } + + /// §5b G6a — **legacy OUTBOUND**: a v24 peer receives exactly + /// `Present`, never the mapped family. + #[test] + fn g6a_a_legacy_peer_receives_the_legacy_present_family() { + let fid = FrontendId(760); + let (_editor, semantic_states, _render, _document, _panel, _epochs) = + panel_session_at(LEGACY_PANEL_VERSION, fid); + let declaration = semantic_states + .get(&fid) + .expect("projection") + .last_panel_payload_for_test(); + assert!( + matches!( + declaration, + Some(pmacs_protocol::panel::PanelFramePayload::Present(_)) + ), + "a v24 session must receive the legacy family and only it; \ + got {declaration:?}" + ); + } + + /// §5b G7a — **mapped OUTBOUND**: a v25 peer receives + /// `PresentMapped` carrying a live, nonzero generation. + #[test] + fn g7a_a_mapped_peer_receives_present_mapped_with_a_live_generation() { + let fid = FrontendId(761); + let (_editor, semantic_states, _render, _document, _panel, _epochs) = + panel_session_at(PROTOCOL_VERSION, fid); + let declaration = semantic_states + .get(&fid) + .expect("projection") + .last_panel_payload_for_test(); + match declaration { + Some(pmacs_protocol::panel::PanelFramePayload::PresentMapped { + mapping_generation, + .. + }) => assert!( + mapping_generation >= 1, + "zero is the wire's uninitialised value and is refused on \ + sight, so a stamped frame must never carry it" + ), + other => panic!("a v25 session must receive the mapped family; got {other:?}"), + } + } + + /// §5b G6b — **legacy INBOUND routing**: a current legacy gesture + /// reaches the dispatcher and performs the landed focus activation. + #[test] + fn g6b_a_legacy_gesture_routes_and_activates() { + let fid = FrontendId(762); + let (mut editor, mut semantic_states, mut render, document, panel, epochs) = + panel_session_at(LEGACY_PANEL_VERSION, fid); + let buffer_id = editor.core.borrow().windows[&panel].buffer_id; + // THIS view's active window, asserted directly. The ambient + // `active_window_id()` tracks `active_frontend` too, so a row + // reading it can be satisfied by a frontend switch that never + // routed anything to the panel. + assert_eq!( + editor.core.borrow().views[&fid].active, + document, + "precondition: this frontend's active window is the document" + ); + + dispatch_panel_event( + &mut editor, + fid, + LEGACY_PANEL_VERSION, + &mut semantic_states, + &mut render, + legacy_pointer(fid, epochs, buffer_id), + ); + assert_eq!( + editor.core.borrow().views[&fid].active, + panel, + "a legacy gesture from a legacy session must still route and \ + activate — the gate must not break the family it kept" + ); + } + + /// §5b G7b — **mapped INBOUND routing**: a current mapped gesture + /// reaches the dispatcher and activates. + #[test] + fn g7b_a_mapped_gesture_routes_and_activates() { + let fid = FrontendId(763); + let (mut editor, mut semantic_states, mut render, document, panel, epochs) = + panel_session_at(PROTOCOL_VERSION, fid); + let buffer_id = editor.core.borrow().windows[&panel].buffer_id; + let generation = stamped_generation(&semantic_states, fid); + assert_eq!( + editor.core.borrow().views[&fid].active, + document, + "precondition: this frontend's active window is the document" + ); + + dispatch_panel_event( + &mut editor, + fid, + PROTOCOL_VERSION, + &mut semantic_states, + &mut render, + mapped_pointer(fid, epochs, buffer_id, generation), + ); + assert_eq!( + editor.core.borrow().views[&fid].active, + panel, + "a mapped gesture echoing the current generation must route" + ); + } + + /// §5b G8a — a **bare `PanelPointer` from a v25 session is + /// REFUSED**, not handled under legacy semantics. + #[test] + fn g8a_a_legacy_gesture_from_a_mapped_session_is_refused() { + let fid = FrontendId(764); + let (mut editor, mut semantic_states, mut render, document, panel, epochs) = + panel_session_at(PROTOCOL_VERSION, fid); + let buffer_id = editor.core.borrow().windows[&panel].buffer_id; + assert_eq!( + editor.core.borrow().views[&fid].active, + document, + "precondition: this frontend's active window is the document" + ); + + dispatch_panel_event( + &mut editor, + fid, + PROTOCOL_VERSION, + &mut semantic_states, + &mut render, + legacy_pointer(fid, epochs, buffer_id), + ); + assert_eq!( + editor.core.borrow().views[&fid].active, + document, + "handling it under legacy semantics would leave the mapping \ + hole reachable by choosing a discriminant" + ); + } + + /// §5b G8c — a **`PanelPointerMapped` from a v24 session is + /// REFUSED**, even though a peer built from this crate can encode + /// the discriminant. Negotiation is a gate, not a convention. + #[test] + fn g8c_a_mapped_gesture_from_a_legacy_session_is_refused() { + let fid = FrontendId(765); + let (mut editor, mut semantic_states, mut render, document, panel, epochs) = + panel_session_at(LEGACY_PANEL_VERSION, fid); + let buffer_id = editor.core.borrow().windows[&panel].buffer_id; + assert_eq!( + editor.core.borrow().views[&fid].active, + document, + "precondition: this frontend's active window is the document" + ); + + dispatch_panel_event( + &mut editor, + fid, + LEGACY_PANEL_VERSION, + &mut semantic_states, + &mut render, + // Any generation at all: the family gate refuses before the + // generation is even looked at. + mapped_pointer(fid, epochs, buffer_id, 1), + ); + assert_eq!( + editor.core.borrow().views[&fid].active, + document, + "inbound negotiation is a gate, not a sender convention" + ); + } + + /// §5b G8e — **authenticated-session authority**: claiming another + /// frontend whose session negotiated the desired family still + /// refuses. + /// + /// The mutation this row exists for is one line: look the + /// negotiation up by the payload's `frontend_id` instead of the + /// authenticated `source`, and the forged cross-session claim + /// succeeds. + #[test] + fn g8e_a_forged_frontend_id_cannot_borrow_another_sessions_family() { + let legacy_fid = FrontendId(766); + let mapped_fid = FORGERY_TARGET_FID; + let (mut editor, mut semantic_states, mut render, document, panel, epochs) = + panel_session_at(LEGACY_PANEL_VERSION, legacy_fid); + let buffer_id = editor.core.borrow().windows[&panel].buffer_id; + assert_eq!( + editor.core.borrow().views[&legacy_fid].active, + document, + "precondition: this frontend's active window is the document" + ); + + // The AUTHENTICATED source is the legacy session; the payload + // claims a frontend that speaks the mapped family. + dispatch_panel_event( + &mut editor, + legacy_fid, + LEGACY_PANEL_VERSION, + &mut semantic_states, + &mut render, + mapped_pointer(mapped_fid, epochs, buffer_id, 1), + ); + assert_eq!( + editor.core.borrow().views[&legacy_fid].active, + document, + "the family comes from the AUTHENTICATED session; a payload \ + id is untrusted and must not borrow another's negotiation" + ); + } + + /// §5b G8e, the OTHER direction — a mapped session cannot borrow a + /// legacy identity to smuggle the bare variant through. + /// + /// Both directions, because an authority check that only holds one + /// way is one a peer walks around by choosing which identity to + /// forge. The claimed frontend has a REAL legacy session, so a + /// payload-keyed lookup succeeds far enough to expose the defect + /// rather than failing for want of a session. + #[test] + fn g8e_a_mapped_session_cannot_forge_a_legacy_identity() { + let fid = FrontendId(769); + let (mut editor, mut semantic_states, mut render, document, panel, epochs) = + panel_session_at(PROTOCOL_VERSION, fid); + let buffer_id = editor.core.borrow().windows[&panel].buffer_id; + assert_eq!( + editor.core.borrow().views[&fid].active, + document, + "precondition: this frontend's active window is the document" + ); + + dispatch_panel_event( + &mut editor, + fid, + PROTOCOL_VERSION, + &mut semantic_states, + &mut render, + // Authenticated as the MAPPED session; the payload claims a + // frontend whose session speaks legacy. + legacy_pointer(FORGERY_TARGET_LEGACY_FID, epochs, buffer_id), + ); + assert_eq!( + editor.core.borrow().views[&fid].active, + document, + "a mapped session may not send the bare variant, and cannot \ + acquire permission by naming someone who may" + ); + } + /// Criterion 50: a gesture from a source whose latest declaration is /// not a visible `Present` is dropped. #[test] diff --git a/src/semantic_render.rs b/src/semantic_render.rs index 9fd8f57..d91bd71 100644 --- a/src/semantic_render.rs +++ b/src/semantic_render.rs @@ -637,6 +637,13 @@ impl SemanticRenderState { } } + /// The panel payload most recently shipped, for the family rows. + #[doc(hidden)] + #[must_use] + pub fn last_panel_payload_for_test(&self) -> Option { + self.last_panel_payload.clone() + } + /// Advance-if-changed, then read: the authoritative mapping key. /// /// **The single seam §5b requires.** Projection stamps the frame diff --git a/tests/bottom_panel_stage2b_gpu_acceptance.rs b/tests/bottom_panel_stage2b_gpu_acceptance.rs index b24629c..6f158eb 100644 --- a/tests/bottom_panel_stage2b_gpu_acceptance.rs +++ b/tests/bottom_panel_stage2b_gpu_acceptance.rs @@ -182,8 +182,13 @@ fn press_and_await_panel(session: &mut Session) -> bool { }), ) .expect("write panel-open key"); + // §5b — whichever Present family this session negotiated. These + // rows are about the band ARRIVING; which wrapper carries it is + // pinned by the G6/G7/G8 rows, not incidentally here. drain_until(&mut session.stream, "panel", |message| match message { - InstanceMessage::PanelFrame(PanelFramePayload::Present(frame)) => Some(frame.size), + InstanceMessage::PanelFrame( + PanelFramePayload::Present(frame) | PanelFramePayload::PresentMapped { frame, .. }, + ) => Some(frame.size), _ => None, }) .is_some() @@ -251,8 +256,8 @@ fn one_daemon_serves_a_v21_panel_session_and_a_shipped_v20_client() { ); assert!( press_and_await_panel(&mut current), - "a v21-negotiated semantic session must be panel-capable and receive \ - a Present panel frame" + "a current-wire semantic session must be panel-capable and \ + receive a Present-family panel frame" ); // Half 3 — a semantic session that echoed the baseline is NOT