Save unverified allocation and repair work at user-requested pause

This commit is contained in:
2026-09-12 04:01:44 -07:00
parent bc8bc4f5a1
commit 6b9cc77ece
10 changed files with 558 additions and 29 deletions
@@ -132,7 +132,7 @@ fn mounted_oa_sa_ordered_incoming_between_outgoing_are_exact_single_history_step
let mut f = fixture(layer);
let original = f.app.project().clone();
let original_source = f.app.document.bundle().clone();
drag(&mut f, screen(xy(160., 140.)));
drag(&mut f, screen(xy(160., 190.)));
assert_eq!(f.app.document.revision(), 1, "{}", f.app.status);
let relation = f
.app
@@ -150,7 +150,7 @@ fn mounted_oa_sa_ordered_incoming_between_outgoing_are_exact_single_history_step
assert!(!f.app.document.can_undo());
let _ = f.app.update(Message::Redo);
assert_eq!(f.app.document.bundle(), &after_in);
drag(&mut f, screen(xy(960., 140.)));
drag(&mut f, screen(xy(960., 190.)));
let reassigned = &f.app.project().relations[&relation.id];
assert_eq!(reassigned.source(), f.b, "{}", f.app.status);
assert_eq!(reassigned.name, relation.name);
@@ -208,7 +208,7 @@ fn mounted_dirty_property_press_does_not_select_apply_or_destroy_draft() {
let input = &f.app.draft.as_ref().unwrap().inputs["/name"];
let raw = input.raw.clone();
let cursor = input.editor.cursor();
let events = batch(&f.app, start(&f), screen(xy(160., 140.)));
let events = batch(&f.app, start(&f), screen(xy(160., 190.)));
assert!(matches!(
events.first(),
Some(Message::WorkDrag(Event::Refused(..)))
@@ -256,7 +256,7 @@ fn source_and_modal_admission_reject_prebuilt_clean_press_without_selecting() {
let _ = f.app.update(Message::WorkDrag(Event::Release(
p.stamp,
p.token,
Some(screen(xy(160., 140.))),
Some(screen(xy(160., 190.))),
)));
assert_eq!(f.app.selected, selection);
assert_eq!(f.app.document.bundle(), &source);
@@ -302,7 +302,7 @@ fn modal_roundtrip_equal_fit_undo_resize_and_focus_retire_press_before_replay()
let _ = f.app.update(Message::WorkDrag(Event::Release(
p.stamp,
p.token,
Some(screen(xy(160., 140.))),
Some(screen(xy(160., 190.))),
)));
assert_eq!(f.app.document.bundle(), &before);
assert!(!f.app.document.can_undo());
@@ -321,7 +321,7 @@ fn stale_packets_do_not_cancel_a_newer_token_and_each_release_is_consumed_once()
let _ = f.app.update(Message::WorkDrag(Event::Preview(
old.stamp,
old.token,
Some(screen(xy(160., 140.))),
Some(screen(xy(160., 190.))),
)));
assert_eq!(
f.app.allocation_drag.active.as_ref().unwrap().press.token,
@@ -330,7 +330,7 @@ fn stale_packets_do_not_cancel_a_newer_token_and_each_release_is_consumed_once()
let release = Message::WorkDrag(Event::Release(
new.stamp,
new.token,
Some(screen(xy(160., 140.))),
Some(screen(xy(160., 190.))),
));
let _ = f.app.update(release.clone());
let revision = f.app.document.revision();
@@ -370,7 +370,7 @@ fn arrange_is_one_gesture_and_snapped_noop_preserves_redo_and_source() {
#[test]
fn outside_release_is_cancel_and_does_not_remove_an_assignment() {
let mut f = fixture(Layer::Oa);
drag(&mut f, screen(xy(160., 140.)));
drag(&mut f, screen(xy(160., 190.)));
let before = f.app.document.bundle().clone();
let revision = f.app.document.revision();
drag(
@@ -386,8 +386,8 @@ fn candidate_semantics_are_cached_per_destination_with_stable_ids_and_own_index(
let mut f = fixture(Layer::Oa);
let p = press(&f);
let _ = f.app.update(Message::WorkDrag(Event::Press(p.clone())));
let a = screen(xy(160., 140.));
let b = screen(xy(960., 140.));
let a = screen(xy(160., 190.));
let b = screen(xy(960., 190.));
let _ = f
.app
.update(Message::WorkDrag(Event::Preview(p.stamp, p.token, Some(a))));
@@ -508,8 +508,8 @@ fn frozen_targets_use_half_open_semantic_priority_and_refuse_incomparable_overla
fn release_uses_ordered_native_position_before_later_motion_in_same_batch() {
let mut f = fixture(Layer::Oa);
let from = start(&f);
let a = screen(xy(160., 140.));
let b = screen(xy(960., 140.));
let a = screen(xy(160., 190.));
let b = screen(xy(960., 190.));
let mut state = CanvasState::default();
let canvas = canvas(&f.app);
let events: Vec<_> = [
@@ -603,7 +603,7 @@ fn source_refusal_is_atomic_and_a_new_gesture_can_retry_against_live_revision()
f.app.document = crate::document::Document::from_bundle(before.clone(), limits).unwrap();
f.app.sync();
let snapshot = f.app.document.snapshot();
drag(&mut f, screen(xy(160., 140.)));
drag(&mut f, screen(xy(160., 190.)));
assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot()));
assert_eq!(f.app.document.bundle(), &before);
assert!(!f.app.document.can_undo());
@@ -611,7 +611,7 @@ fn source_refusal_is_atomic_and_a_new_gesture_can_retry_against_live_revision()
assert!(f.app.allocation_drag.active.is_none());
f.app.document = crate::document::Document::from_bundle(before, Default::default()).unwrap();
f.app.sync();
drag(&mut f, screen(xy(160., 140.)));
drag(&mut f, screen(xy(160., 190.)));
assert_eq!(
validation::effective_performer(f.app.project(), f.work),
Some(f.a),
@@ -656,7 +656,7 @@ fn mounted_role_only_outgoing_refusal_retains_placement_and_same_performer_creat
.find(|t| t.element == f.a)
.unwrap()
.region;
drag(&mut f, screen(xy(target.x + 45., target.y + 40.)));
drag(&mut f, screen(xy(target.x + 45., target.y + 100.)));
assert_eq!(f.app.document.revision(), 1, "{}", f.app.status);
assert_eq!(f.app.project().relations, relations);
assert!(
@@ -686,7 +686,7 @@ fn mounted_uniform_parent_moves_hidden_placements_and_assigns_only_leaves() {
.collapsed = true;
replace(&mut f, project);
let before = f.app.project().diagrams[&f.diagram].nodes.clone();
drag(&mut f, screen(xy(960., 140.)));
drag(&mut f, screen(xy(960., 190.)));
assert_eq!(f.app.document.revision(), 1, "{}", f.app.status);
let after = &f.app.project().diagrams[&f.diagram].nodes;
let dx = after[&f.node].position.x - before[&f.node].position.x;
@@ -805,7 +805,7 @@ fn pending_behavior_draft_refuses_actual_work_press_and_equal_document_replaceme
crate::behavior::Event::Creation(crate::behavior::creation::Event::Open(Layer::Oa)),
);
assert!(f.app.behavior.has_pending());
let events = batch(&f.app, start(&f), screen(xy(160., 140.)));
let events = batch(&f.app, start(&f), screen(xy(160., 190.)));
assert!(matches!(
events.first(),
Some(Message::WorkDrag(Event::Refused(..)))
@@ -823,7 +823,7 @@ fn pending_behavior_draft_refuses_actual_work_press_and_equal_document_replaceme
let _ = f.app.update(Message::WorkDrag(Event::Release(
p.stamp,
p.token,
Some(screen(xy(160., 140.))),
Some(screen(xy(160., 190.))),
)));
assert_eq!(f.app.document.bundle(), &source);
assert!(!f.app.document.can_undo());
@@ -854,7 +854,7 @@ fn actual_canvas_origin_change_cancels_held_work_before_any_legacy_release() {
&mut state,
&canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
moved_bounds,
mouse::Cursor::Available(screen(xy(160., 140.))),
mouse::Cursor::Available(screen(xy(160., 190.))),
)
.and_then(|a| a.into_inner().0)
.unwrap();
@@ -953,3 +953,92 @@ fn toolbar_status_leaves_transformed_top_edge_frozen_target_label_readable() {
);
}
}
#[test]
fn oa_sa_label_overlap_refuses_preview_release_and_retries_without_partial_source_or_history() {
for layer in [Layer::Oa, Layer::Sa] {
for (zoom, pan) in [
(1., Vector::ZERO),
(0.75, Vector::new(37., -15.)),
(1.25, Vector::new(-30., 23.)),
] {
let mut f = fixture(layer);
let mut project = f.app.project().clone();
project.elements.get_mut(&f.a).unwrap().name = "Receiver".into();
replace(&mut f, project);
// Refusals must also preserve an existing redo branch.
let _ = f.app.update(Message::Command(Command::Rename(f.work, "Temporary".into())));
let _ = f.app.update(Message::Undo);
f.app.zoom = zoom;
f.app.pan = pan;
f.app.fit_to_view = false;
let screen = |world: Position| Point::new(
bounds().x + world.x * zoom + pan.x,
bounds().y + world.y * zoom + pan.y,
);
let from = screen(xy(140., 535.));
let overlap = screen(xy(160., 140.));
let clear = screen(xy(160., 190.));
let accepted = f.app.project().clone();
let source = f.app.document.bundle().clone();
let snapshot = f.app.document.snapshot();
let revision = f.app.document.revision();
let mut events = batch(&f.app, from, overlap);
let release = events.pop().unwrap();
send(&mut f.app, events);
assert!(f.app.allocation_drag.preview.is_none());
let feedback = f.app.allocation_drag.feedback.as_ref().unwrap();
assert!(feedback.refused, "{}", feedback.text);
assert!(feedback.text.contains("Aim below or beside"));
assert_eq!(feedback.press.frozen.targets[feedback.targets[0]].element, f.a);
assert_eq!(f.app.project(), &accepted);
assert_eq!(f.app.document.bundle(), &source);
assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot()));
assert!(!f.app.document.can_undo());
assert!(f.app.document.can_redo());
let _ = f.app.update(release);
assert!(f.app.allocation_drag.active.is_none());
assert!(f.app.allocation_drag.preview.is_none());
assert_eq!(f.app.project(), &accepted);
assert_eq!(f.app.document.bundle(), &source);
assert_eq!(f.app.document.revision(), revision);
assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot()));
assert!(!f.app.document.can_undo());
assert!(f.app.document.can_redo());
// In a new gesture, moving out of the collision reuses its semantic
// candidate and stable allocation identity; only geometry changes.
let mut events = batch(&f.app, from, overlap);
events.pop();
send(&mut f.app, events);
let active = f.app.allocation_drag.active.as_ref().unwrap();
let p = active.press.clone();
let semantic = active.cache[0].1.as_ref().unwrap().clone();
let _ = f.app.update(Message::WorkDrag(Event::Preview(p.stamp, p.token, Some(clear))));
let preview = f.app.allocation_drag.preview.as_ref().expect(&f.app.status);
assert!(Arc::ptr_eq(&semantic, &preview.semantic));
assert_eq!(preview.placement.diagram.nodes[&f.node].position, xy(120., 160.));
assert_eq!(preview.placement.view, projection::project_diagram_with_index(
semantic.project(), &preview.placement.diagram, semantic.index(),
));
let mut expected = semantic.project().clone();
expected.diagrams.insert(f.diagram, preview.placement.diagram.clone());
assert_eq!(f.app.project(), &accepted);
assert_eq!(f.app.document.bundle(), &source);
let _ = f.app.update(Message::WorkDrag(Event::Release(p.stamp, p.token, Some(clear))));
assert_eq!(f.app.project(), &expected, "{}", f.app.status);
assert_eq!(f.app.document.revision(), revision + 1);
assert!(f.app.document.can_undo());
assert!(!f.app.document.can_redo());
let after = f.app.document.bundle().clone();
assert_eq!(oa_core::source_project::compile_bundle(&after, Default::default()).unwrap().decoded.project, expected);
let _ = f.app.update(Message::Undo);
assert_eq!(f.app.project(), &accepted);
assert_eq!(f.app.document.bundle(), &source);
assert!(!f.app.document.can_undo());
let _ = f.app.update(Message::Redo);
assert_eq!(f.app.project(), &expected);
assert_eq!(f.app.document.bundle(), &after);
}
}
}