Files
ARCADIA-Studio/harness/experiments/navigation-validation-20261002/objects/b4f7d5bc92f11e91cff140c55e98ac8d5baaa05d3814b5ef0ead6e0bcdfe6fc5
T

274 lines
13 KiB
Plaintext

--- a/crates/oa-desktop/src/app.rs
+++ b/crates/oa-desktop/src/app.rs
@@ -6,6 +6,8 @@
mod behavior_removal_tests;
#[cfg(test)]
mod canvas_selection_tests;
+#[cfg(test)]
+mod connection_begin_regressions;
mod checkpoints;
#[cfg(test)]
mod diagram_focus_tests;
--- /dev/null
+++ b/crates/oa-desktop/src/app/connection_begin_regressions.rs
@@ -0,0 +1,259 @@
+// Test-only proposal pinned to peer 6520a89dc042dd433454f6cc63cb23b0e6df7911.
+// Independent App test module; real popup messages retain menu tokens.
+use super::*;
+use crate::model_menu::{Event as MenuEvent, Request};
+
+struct BeginFixture {
+ app: App,
+ diagram: Id,
+ surface_layer: Layer,
+ source: Id,
+ target: Id,
+ wrong_layer: Id,
+ hidden: Id,
+ tool: crate::connection_tool::Tool,
+}
+
+fn begin_fixture(source_layer: Layer, allocation: bool, mixed: bool) -> BeginFixture {
+ use crate::connection_tool::Tool;
+ let mut p = Project::empty();
+ let (work, performer, exchange) = match source_layer {
+ Layer::Oa => (Kind::Activity, Kind::Entity, Tool::Interaction),
+ Layer::Sa => (Kind::Function, Kind::System, Tool::FunctionalExchange),
+ };
+ let opposite = if source_layer == Layer::Oa { Layer::Sa } else { Layer::Oa };
+ let surface_layer = if mixed { opposite } else { source_layer };
+ let source = p.add_element(if allocation { performer } else { work }, source_layer, "Source", None).unwrap();
+ let target = p.add_element(work, source_layer, "Target", None).unwrap();
+ let hidden = p.add_element(work, source_layer, "Hidden target", None).unwrap();
+ let other_work = if opposite == Layer::Oa { Kind::Activity } else { Kind::Function };
+ let wrong_layer = p.add_element(other_work, opposite, "Wrong layer target", None).unwrap();
+ p.ensure_diagram(source_layer);
+ let diagram = p.ensure_diagram(surface_layer);
+ p.diagrams.get_mut(&diagram).unwrap().projection = if mixed { ProjectionKind::Traceability } else { ProjectionKind::Capabilities };
+ p.place(diagram, source, Position { x: 0., y: 0. });
+ p.place(diagram, target, Position { x: 400., y: 0. });
+ if mixed {
+ p.place(diagram, wrong_layer, Position { x: 800., y: 0. });
+ }
+ let mut app = App::with_project(p);
+ app.page = Page::Diagram;
+ app.layer = surface_layer;
+ app.active_diagram = Some(diagram);
+ app.selected = diagram;
+ app.refresh_draft();
+ app.reconcile_multiselect();
+ assert!(!app.document.can_undo());
+ BeginFixture { app, diagram, surface_layer, source, target, wrong_layer, hidden, tool: if allocation { Tool::Allocation } else { exchange } }
+}
+
+fn begin_surface(f: &BeginFixture) {
+ assert_eq!(f.app.page, Page::Diagram);
+ assert_eq!(f.app.layer, f.surface_layer);
+ assert_eq!(f.app.active_diagram, Some(f.diagram));
+}
+
+fn begin_popup_action(f: &mut BeginFixture) -> Option<Message> {
+ begin_popup_action_from(f, true)
+}
+
+fn begin_popup_action_from(f: &mut BeginFixture, canvas: bool) -> Option<Message> {
+ let mut request = Request::object(f.source, iced::Point::ORIGIN);
+ if canvas {
+ request.canvas = f.app.route_context();
+ request.placement = f.app.project().diagrams[&f.diagram].nodes.values()
+ .find(|node| node.element == f.source).map(|node| node.id);
+ }
+ let _ = f.app.update(Message::Menu(MenuEvent::Open(request)));
+ let popup = crate::model_menu::popup(&f.app).expect("real model popup");
+ let expected = format!("Draw {}", f.tool.label().to_lowercase());
+ for entry in popup.entries {
+ if let crate::context_menu::Entry::Action { label, message, .. } = entry
+ && label == expected
+ {
+ if let Some(Message::Menu(MenuEvent::Invoke(token, _))) = &message {
+ assert_eq!(*token, popup.token);
+ } else {
+ assert!(message.is_none(), "Draw must use the menu token envelope");
+ }
+ return message;
+ }
+ }
+ None // Omission is also a valid unavailable-action policy.
+}
+
+fn begin_from_popup(f: &mut BeginFixture) -> Message {
+ begin_from_popup_origin(f, true)
+}
+
+fn begin_from_popup_origin(f: &mut BeginFixture, canvas: bool) -> Message {
+ let before = f.app.document.snapshot();
+ let revision = f.app.document.revision();
+ let action = begin_popup_action_from(f, canvas).expect("visible semantic source offers enabled Draw");
+ let _ = f.app.update(action.clone());
+ begin_surface(f);
+ assert_eq!(f.app.project(), &before.decoded.project);
+ assert_eq!(f.app.document.bundle(), &before.bundle);
+ assert_eq!(f.app.document.revision(), revision);
+ assert!(!f.app.document.can_undo());
+ let session = f.app.connection.as_ref().expect("menu Draw must arm its tool");
+ assert_eq!(session.tool, f.tool);
+ assert_eq!(session.source, Some(f.source));
+ assert_eq!(session.context, f.app.route_context().unwrap());
+ assert!(session.candidates.contains(&f.target));
+ assert!(!session.candidates.contains(&f.wrong_layer));
+ assert!(!session.candidates.contains(&f.hidden));
+ action
+}
+
+fn begin_complete_and_check_history(f: &mut BeginFixture) {
+ let before = f.app.document.snapshot();
+ let revision = f.app.document.revision();
+ let context = f.app.route_context().unwrap();
+ let _ = f.app.update(Message::Connection(crate::connection_edit::Event::Endpoint(context, f.target)));
+ begin_surface(f);
+ assert!(f.app.connection.is_none());
+ assert_eq!(f.app.document.revision(), revision + 1);
+ let relations: Vec<_> = f.app.project().relations.values()
+ .filter(|r| !before.decoded.project.relations.contains_key(&r.id)).collect();
+ assert_eq!(relations.len(), 1);
+ let expected = match f.tool {
+ crate::connection_tool::Tool::Interaction => RelationKind::Interaction,
+ crate::connection_tool::Tool::FunctionalExchange => RelationKind::FunctionalExchange,
+ crate::connection_tool::Tool::Allocation => RelationKind::Allocation,
+ _ => unreachable!(),
+ };
+ assert_eq!(relations[0].kind(), expected);
+ assert_eq!((relations[0].source(), relations[0].target()), (f.source, f.target));
+ // Random identities are accepted only for the precise command delta.
+ // Everything else, including traces, metadata and view options, stays exact.
+ let mut expected_project = before.decoded.project.clone();
+ let created = relations[0];
+ assert!(expected_project.relations.insert(created.id, created.clone()).is_none());
+ if let RelationBody::FunctionalExchange { source_port, target_port, items, .. } = &created.body {
+ assert_eq!(f.tool, crate::connection_tool::Tool::FunctionalExchange);
+ assert!(items.is_empty());
+ assert_ne!(source_port, target_port);
+ let output = &f.app.project().elements[source_port];
+ let input = &f.app.project().elements[target_port];
+ assert_eq!((output.kind(), output.layer(), output.parent), (Kind::Port, Layer::Sa, Some(f.source)));
+ assert_eq!((input.kind(), input.layer(), input.parent), (Kind::Port, Layer::Sa, Some(f.target)));
+ assert!(matches!(port_contract(output), Some(PortContract::FunctionOutput { outgoing_items }) if outgoing_items.is_empty()));
+ assert!(matches!(port_contract(input), Some(PortContract::FunctionInput { incoming_items }) if incoming_items.is_empty()));
+ assert!(!before.decoded.project.elements.contains_key(source_port));
+ assert!(!before.decoded.project.elements.contains_key(target_port));
+ assert_eq!(output.id, *source_port);
+ assert_eq!(input.id, *target_port);
+ expected_project.elements.insert(*source_port, output.clone());
+ expected_project.elements.insert(*target_port, input.clone());
+ assert_eq!(f.app.project().elements.len(), before.decoded.project.elements.len() + 2);
+ } else {
+ assert_ne!(f.tool, crate::connection_tool::Tool::FunctionalExchange);
+ assert_eq!(f.app.project().elements, before.decoded.project.elements);
+ }
+ assert_eq!(f.app.project(), &expected_project);
+ assert_eq!(f.app.project().diagrams, before.decoded.project.diagrams);
+ for (id, element) in &before.decoded.project.elements {
+ assert_eq!(&f.app.project().elements[id], element);
+ }
+ begin_assert_source_pair(&f.app);
+ let after = f.app.document.snapshot();
+ let _ = f.app.update(Message::Undo);
+ assert_eq!(f.app.project(), &before.decoded.project);
+ assert_eq!(f.app.document.bundle(), &before.bundle);
+ assert!(!f.app.document.can_undo(), "creation is a single history step");
+ let _ = f.app.update(Message::Redo);
+ assert_eq!(f.app.project(), &after.decoded.project);
+ assert_eq!(f.app.document.bundle(), &after.bundle);
+ begin_assert_source_pair(&f.app);
+}
+
+#[test]
+fn contextual_begin_exchange_uses_actual_menu_in_both_mixed_directions() {
+ for layer in [Layer::Oa, Layer::Sa] {
+ let mut f = begin_fixture(layer, false, true);
+ begin_from_popup(&mut f);
+ begin_complete_and_check_history(&mut f);
+ }
+}
+
+#[test]
+fn contextual_begin_cancel_restart_and_stale_menu_preserve_session_ownership() {
+ for layer in [Layer::Oa, Layer::Sa] {
+ let mut f = begin_fixture(layer, false, true);
+ let original = f.app.document.snapshot();
+ let stale_menu = begin_from_popup(&mut f);
+ let first = f.app.route_context().unwrap();
+ let _ = f.app.update(Message::Connection(crate::connection_edit::Event::Restart(first)));
+ begin_surface(&f);
+ let restarted = f.app.connection.as_ref().expect("restart retains contextual tool");
+ assert_eq!(restarted.tool, f.tool);
+ assert_eq!(restarted.source, None);
+ assert!(restarted.candidates.contains(&f.source));
+ let current = restarted.context;
+ assert_ne!(current, first);
+ let _ = f.app.update(stale_menu);
+ let _ = f.app.update(Message::Connection(crate::connection_edit::Event::Endpoint(first, f.target)));
+ assert_eq!(f.app.connection.as_ref().unwrap().context, current);
+ assert_eq!(f.app.connection.as_ref().unwrap().source, None);
+ let _ = f.app.update(Message::Connection(crate::connection_edit::Event::Endpoint(current, f.source)));
+ assert_eq!(f.app.connection.as_ref().unwrap().source, Some(f.source));
+ let _ = f.app.update(Message::Escape);
+ let _ = f.app.update(Message::Connection(crate::connection_edit::Event::Endpoint(current, f.target)));
+ assert!(f.app.connection.is_none());
+ begin_surface(&f);
+ assert_eq!(f.app.project(), &original.decoded.project);
+ assert_eq!(f.app.document.bundle(), &original.bundle);
+ assert!(!f.app.document.can_undo());
+ }
+}
+
+#[test]
+fn contextual_begin_allocation_uses_actual_menu_in_both_mixed_directions() {
+ for layer in [Layer::Oa, Layer::Sa] {
+ let mut f = begin_fixture(layer, true, true);
+ begin_from_popup(&mut f);
+ begin_complete_and_check_history(&mut f);
+ }
+}
+
+#[test]
+fn contextual_begin_allocation_is_not_enabled_when_projection_hides_work() {
+ for layer in [Layer::Oa, Layer::Sa] {
+ let mut f = begin_fixture(layer, true, false);
+ let view = project_view(f.app.project(), f.diagram);
+ assert!(view.nodes.iter().any(|n| n.element == f.source));
+ assert!(!view.nodes.iter().any(|n| n.element == f.target));
+ assert!(!f.tool.available_in(layer, ProjectionKind::Capabilities));
+ let before = f.app.document.snapshot();
+ assert!(begin_popup_action(&mut f).is_none(), "Draw allocation must be disabled or omitted when work is not projected");
+ let _ = f.app.update(Message::Menu(MenuEvent::Close));
+ // Defensive handler refusal complements actual-menu enabledness above.
+ let _ = f.app.update(Message::Connection(crate::connection_edit::Event::Begin(f.tool, f.source)));
+ assert!(f.app.connection.is_none());
+ begin_surface(&f);
+ assert_eq!(f.app.project(), &before.decoded.project);
+ assert_eq!(f.app.document.bundle(), &before.bundle);
+ assert!(!f.app.document.can_undo());
+ }
+}
+
+fn begin_assert_source_pair(app: &App) {
+ let compiled = oa_core::source_project::compile_bundle(
+ app.document.bundle(),
+ oa_core::source_project::ProjectLimits::default(),
+ ).unwrap();
+ assert_eq!(&compiled.decoded.project, app.project());
+ assert_eq!(&compiled.bundle, app.document.bundle());
+}
+
+#[test]
+fn contextual_begin_explorer_draw_retains_visible_mixed_diagram_in_both_directions() {
+ for layer in [Layer::Oa, Layer::Sa] {
+ let mut f = begin_fixture(layer, false, true);
+ // A real Explorer Request::object has no canvas/placement envelope,
+ // but its generated Draw action still targets the visible current view.
+ begin_from_popup_origin(&mut f, false);
+ begin_complete_and_check_history(&mut f);
+ }
+}