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
|| (!matches!(event, behavior::scenario::CanvasEvent::Restored(..))
&& 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();
}
// 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
&& *lifetime != self.camera_epoch
{
@@ -86,7 +86,7 @@ fn exercise(via_guide: bool) {
}
assert_destination(&app, target);
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(
held,
crate::behavior::scenario::Shelf::Selection,
@@ -50,6 +50,7 @@ fn stale_text_and_scenario_messages_do_not_retire_a_new_saved_spacing_gesture()
context,
Size::new(900., 600.),
)));
let context = app.scenario_context().unwrap();
let (event, start, _) = begin(&mut app, Target::Lane(lane));
move_to(&mut app, event, start + Vector::new(24., 0.));
let accepted = app.document.snapshot();
@@ -314,6 +315,63 @@ fn stale_finish_cannot_remove_new_same_view_deferred_reveal() {
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]
fn modal_open_cancels_preview_and_captured_begin_stays_retired_after_close() {
let (mut app, lane, _) = setup();