Reject stale Scenario actions before shared reveal boundaries

This commit is contained in:
2026-09-23 00:37:07 -07:00
parent 6f4dfc67b3
commit 0b61771d07
4 changed files with 78 additions and 1 deletions
+11
View File
@@ -1386,9 +1386,20 @@ impl App {
|| current.visit != captured.visit || current.visit != captured.visit
|| (!matches!(event, behavior::scenario::CanvasEvent::Restored(..)) || (!matches!(event, behavior::scenario::CanvasEvent::Restored(..))
&& current.revision != captured.revision) && current.revision != captured.revision)
|| (!matches!(
event,
behavior::scenario::CanvasEvent::FormShown(..)
| behavior::scenario::CanvasEvent::Viewport(..)
| behavior::scenario::CanvasEvent::Restored(..)
| behavior::scenario::CanvasEvent::Scrolled(..)
| behavior::scenario::CanvasEvent::Camera(..)
) && current.epoch != captured.epoch)
}) { }) {
return Task::none(); return Task::none();
} }
// Passive layout/form reports retain their existing epoch rules;
// camera input uses its separate lifetime below. All other actions
// must match the exact context before shared reveal reconciliation.
if let behavior::scenario::CanvasEvent::Camera(_, lifetime, _) = event if let behavior::scenario::CanvasEvent::Camera(_, lifetime, _) = event
&& *lifetime != self.camera_epoch && *lifetime != self.camera_epoch
{ {
@@ -86,7 +86,7 @@ fn exercise(via_guide: bool) {
} }
assert_destination(&app, target); assert_destination(&app, target);
let camera = app.camera_boundary(); let camera = app.camera_boundary();
// The prior saved canvas cannot regain focus after either owner route. // The prior saved canvas cannot open its outline after either owner route.
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Shelf( let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Shelf(
held, held,
crate::behavior::scenario::Shelf::Selection, crate::behavior::scenario::Shelf::Selection,
@@ -50,6 +50,7 @@ fn stale_text_and_scenario_messages_do_not_retire_a_new_saved_spacing_gesture()
context, context,
Size::new(900., 600.), Size::new(900., 600.),
))); )));
let context = app.scenario_context().unwrap();
let (event, start, _) = begin(&mut app, Target::Lane(lane)); let (event, start, _) = begin(&mut app, Target::Lane(lane));
move_to(&mut app, event, start + Vector::new(24., 0.)); move_to(&mut app, event, start + Vector::new(24., 0.));
let accepted = app.document.snapshot(); let accepted = app.document.snapshot();
@@ -314,6 +315,63 @@ fn stale_finish_cannot_remove_new_same_view_deferred_reveal() {
assert!(!app.document.can_undo()); assert!(!app.document.can_undo());
} }
#[test]
fn stale_active_actions_cannot_remove_new_same_visit_deferred_reveal() {
let (mut app, lane, message) = setup();
let old = app.scenario_context().unwrap();
let behavior::scenario_layout::Representation::Saved { diagram } = old.representation else {
unreachable!()
};
let accepted = app.document.snapshot();
let source = app.source_workspace.bundle();
let _ = app.update(Message::RevealInDiagram(diagram, message));
let current = app.scenario_context().unwrap();
assert_eq!(current.visit, old.visit);
assert_eq!(current.representation, old.representation);
assert_eq!(current.revision, old.revision);
assert_ne!(current.epoch, old.epoch);
let pending = app.pending_diagram_reveal.clone();
assert!(pending.is_some());
let focus = app.diagram_navigation_focus.as_ref().unwrap();
let focus = (focus.context, focus.usage.clone(), focus.selected);
let camera = app.camera_boundary();
for event in [
CanvasEvent::Representation(old, old.representation),
CanvasEvent::Shelf(old, behavior::scenario::Shelf::Selection),
CanvasEvent::Select(old, lane),
CanvasEvent::Zoom(old, Some(1.)),
] {
let _ = app.update(Message::ScenarioCanvas(event));
assert_eq!(app.scenario_context(), Some(current));
assert_eq!(app.pending_diagram_reveal, pending);
assert_eq!(app.pending_scenario_reveal_visit, Some(current.visit));
let actual = app.diagram_navigation_focus.as_ref().unwrap();
assert_eq!(
(actual.context, actual.usage.clone(), actual.selected),
focus
);
assert!(app.camera_boundary() == camera);
assert_eq!(app.selected, message);
assert_eq!(app.document.bundle(), &accepted.bundle);
assert_eq!(app.project(), &accepted.decoded.project);
assert_eq!(app.source_workspace.bundle(), source);
assert!(!app.document.can_undo() && !app.document.can_redo());
}
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Viewport(
current,
Size::new(800., 500.),
)));
assert!(app.pending_diagram_reveal.is_none());
assert!(app.pending_scenario_reveal_visit.is_none());
assert!(
app.diagram_focus(crate::diagram_usage::Surface::Saved(diagram))
.is_some()
);
assert_eq!(app.selected, message);
assert_eq!(app.document.bundle(), &accepted.bundle);
assert!(!app.document.can_undo() && !app.document.can_redo());
}
#[test] #[test]
fn modal_open_cancels_preview_and_captured_begin_stays_retired_after_close() { fn modal_open_cancels_preview_and_captured_begin_stays_retired_after_close() {
let (mut app, lane, _) = setup(); let (mut app, lane, _) = setup();
+8
View File
@@ -29,6 +29,14 @@ representation/visit checks also run before those effects. Scope input-owner
equality now includes Scenario representation and visit. Current source Tab and equality now includes Scenario representation and visit. Current source Tab and
selection behavior are retained, as are passive WorkDrag reveal exceptions. selection behavior are retained, as are passive WorkDrag reveal exceptions.
Independent review found a same-visit, newer-epoch stale action could retire a
newer deferred reveal before its late rejection. Active Scenario actions now
require the exact context before shared boundaries. FormShown, Viewport,
Restored and Scrolled retain their existing passive admission, while Camera
retains its separate lifetime rule. A regression captures arrangement/outline/
selection/zoom actions, starts a same-view reveal, verifies all stale actions
preserve its full receipt/focus/source/history, and completes the live viewport.
Root intentionally removed the obsolete panel-focus mode and Canvas button. Root intentionally removed the obsolete panel-focus mode and Canvas button.
This integration retains that always-active canvas policy. Saved Scenario This integration retains that always-active canvas policy. Saved Scenario
Details opens the current inspector and dismisses its outline without replacing Details opens the current inspector and dismisses its outline without replacing