From 9eb4642e50564165aa0db38d8e91f4c14b818bdc Mon Sep 17 00:00:00 2001 From: Codex-ytr83r Date: Thu, 24 Sep 2026 19:18:31 -0700 Subject: [PATCH] Add desktop reset history checks and native wiring fixtures --- .../examples/arrange_reset_native_probe.rs | 263 ++++++++++++++++++ crates/oa-desktop/src/tests.rs | 158 +++++++++++ 2 files changed, 421 insertions(+) create mode 100644 crates/oa-core/examples/arrange_reset_native_probe.rs diff --git a/crates/oa-core/examples/arrange_reset_native_probe.rs b/crates/oa-core/examples/arrange_reset_native_probe.rs new file mode 100644 index 00000000..6fbe895b --- /dev/null +++ b/crates/oa-core/examples/arrange_reset_native_probe.rs @@ -0,0 +1,263 @@ +//! Native UI wiring fixture/oracle. Expected output comes from candidate Layout, +//! not from an independent geometry algorithm. Run only after parent source freeze. +use oa_core::*; +use serde_json::{Value, json}; +use std::{collections::BTreeMap, error::Error, fs, path::PathBuf}; + +fn id(n: u128) -> Id { + Id::from_u128(700_000 + n) +} +fn at(x: f32, y: f32) -> Position { + Position { x, y } +} + +fn function(p: &mut Project, n: u128, name: &str, parent: Option) -> Id { + let old = p + .add_element(Kind::Function, Layer::Sa, name, parent) + .unwrap(); + reidentify(p, old, id(n)).unwrap(); + id(n) +} + +fn place(p: &mut Project, d: Id, n: u128, element: Id, position: Position, size: Size) { + let old = p.place(d, element, position); + reidentify(p, old, id(n)).unwrap(); + let node = p + .diagrams + .get_mut(&d) + .unwrap() + .nodes + .get_mut(&id(n)) + .unwrap(); + node.alias = format!("placement_{n}"); + node.size = size; +} + +fn exchange(p: &mut Project, n: u128, name: &str, source: Id, target: Id) -> Id { + let request = FunctionalExchangeRequest { + id: id(n), + alias: format!("exchange_{n}"), + name: name.into(), + description: String::new(), + source, + target, + source_port: PortChoice::Create, + target_port: PortChoice::Create, + items: vec![], + }; + let commands = ports::plan_functional_exchange(p, &request).unwrap(); + *p = execute_batch(p, &commands).unwrap(); + id(n) +} + +fn route(edge: Id, waypoints: Vec, fraction: f32, offset: Position) -> EdgeRoute { + EdgeRoute { + edge, + source_attachment: None, + target_attachment: None, + waypoints, + label: Some(EdgeLabel { fraction, offset }), + } +} + +fn original() -> (Project, Id, Id, Value) { + let mut p = Project::empty(); + p.id = id(0); + p.alias = "arrange_native_fixture".into(); + p.name = "Arrange route reset trial".into(); + let old = p.ensure_diagram(Layer::Sa); + let main = id(1); + reidentify(&mut p, old, main).unwrap(); + { + let d = p.diagrams.get_mut(&main).unwrap(); + d.alias = "main_dispatch".into(); + d.name = "Main dispatch".into(); + d.projection = ProjectionKind::FunctionalDataflow; + } + let group = function(&mut p, 10, "Prepare report", None); + let send = function(&mut p, 11, "Send report", Some(group)); + let check = function(&mut p, 12, "Check report", Some(group)); + let monitor = function(&mut p, 13, "Monitor report", None); + let archive = function(&mut p, 14, "Archive report", None); + let card = Size { + width: 224., + height: 94., + }; + place( + &mut p, + main, + 20, + group, + at(40., 80.), + Size { + width: 400., + height: 420., + }, + ); + for (n, element, x, y) in [ + (21, send, 80., 160.), + (22, check, 80., 330.), + (23, monitor, 740., 80.), + (24, archive, 740., 380.), + ] { + place(&mut p, main, n, element, at(x, y), card); + } + let authored = exchange(&mut p, 100, "Verified report", send, monitor); + exchange(&mut p, 101, "Report for checking", send, check); + exchange(&mut p, 102, "Checked report archive", check, archive); + let hidden = exchange( + &mut p, + 103, + "Filtered archive acknowledgement", + monitor, + archive, + ); + let RelationBody::FunctionalExchange { + source_port, + target_port, + .. + } = p.relations[&authored].body + else { + unreachable!() + }; + for (node, port, side, offset) in [ + (id(21), source_port, PortAnchorSide::Right, 0.25), + (id(23), target_port, PortAnchorSide::Left, 0.75), + ] { + p = prepare_command( + &p, + &Command::SetPortAnchor { + diagram: main, + node, + anchor: PortAnchor { port, side, offset }, + }, + ) + .unwrap() + .project; + } + let mut visible = route( + authored, + vec![at(610., 210.), at(610., 420.)], + 0.5, + at(0., -32.), + ); + // Real semantic ports remain authoritative; these stored preferences are dormant. + visible.source_attachment = Some(EdgeAttachment { + side: PortAnchorSide::Bottom, + offset: 0.5, + }); + visible.target_attachment = Some(EdgeAttachment { + side: PortAnchorSide::Top, + offset: 0.25, + }); + let filtered = route(hidden, vec![at(1020., 270.)], 0.75, at(16., -16.)); + p.diagrams.get_mut(&main).unwrap().routes = vec![visible, filtered]; + p.diagrams.get_mut(&main).unwrap().view_options.hidden_edges = vec![hidden]; + let second = id(2); + p.diagrams.insert( + second, + Diagram { + id: second, + alias: "review_sentinel".into(), + name: "Review sentinel".into(), + description: + "Independent other-view route; Arrange Main dispatch must retain this entire view" + .into(), + layer: Layer::Sa, + projection: ProjectionKind::FunctionalDataflow, + view_options: Default::default(), + nodes: BTreeMap::new(), + layout: None, + routes: vec![route(authored, vec![at(420., 40.)], 0.25, at(8., -8.))], + }, + ); + place(&mut p, second, 30, send, at(40., 120.), card); + place(&mut p, second, 31, monitor, at(660., 120.), card); + validate_candidate(&p).unwrap(); + ( + p, + main, + second, + json!({"authored_edge":authored,"authored_edge_name":"Verified report", + "filtered_edge":hidden,"container_placement":id(20),"source_port":source_port,"target_port":target_port, + "main_diagram":main,"second_diagram":second,"authored_pin_count":2, + "source_anchor":{"side":"Right","offset":0.25},"target_anchor":{"side":"Left","offset":0.75}, + "dormant_source_attachment":{"side":"Bottom","offset":0.5}, + "dormant_target_attachment":{"side":"Top","offset":0.25}}), + ) +} + +fn json_bytes(p: &Project) -> Vec { + let mut bytes = serde_json::to_vec_pretty(p).unwrap(); + if !bytes.ends_with(b"\n") { + bytes.push(b'\n'); + } + assert_eq!(persistence::decode(&bytes).unwrap(), *p); + bytes +} + +fn source_bytes(p: &Project) -> Vec { + let source = language::format(p); + assert_eq!(language::parse(&source).unwrap(), *p); + let mut bytes = source.into_bytes(); + if !bytes.ends_with(b"\n") { + bytes.push(b'\n'); + } + bytes +} + +fn main() -> Result<(), Box> { + let mut args = std::env::args_os().skip(1); + let output = PathBuf::from( + args.next() + .ok_or("Usage: arrange_reset_native_probe NEW_DIRECTORY")?, + ); + if args.next().is_some() || output.exists() { + return Err("Require exactly one output directory that does not already exist".into()); + } + let (before, main, second, identities) = original(); + let mut folded = before.clone(); + folded + .diagrams + .get_mut(&main) + .unwrap() + .nodes + .get_mut(&id(20)) + .unwrap() + .collapsed = true; + let mut files = Vec::new(); + let mut cases = Vec::new(); + for (name, project) in [("expanded", before), ("folded", folded)] { + validate_candidate(&project)?; + // Explicitly candidate-derived UI oracle: this is not an independent layout proof. + let expected = execute_batch(&project, &[Command::Layout(main)])?; + assert_eq!(project.elements, expected.elements); + assert_eq!(project.relations, expected.relations); + assert_eq!(project.diagrams[&second], expected.diagrams[&second]); + for (stage, p) in [("original", &project), ("expected-after", &expected)] { + files.push((format!("{name}-{stage}.json"), json_bytes(p))); + files.push((format!("{name}-{stage}.arcadia"), source_bytes(p))); + } + cases.push(json!({"name":name,"diagram":main, + "original_json":format!("{name}-original.json"), + "expected_json":format!("{name}-expected-after.json"), + "original_source":format!("{name}-original.arcadia"), + "expected_source":format!("{name}-expected-after.arcadia"), + "original_routes":project.diagrams[&main].routes,"expected_routes":expected.diagrams[&main].routes, + "expected_origin":"Candidate Command::Layout; UI-wiring oracle only"})); + } + let manifest = json!({"schema":1,"cases":cases,"identities":identities, + "definition":"Source-authored five-Function/two-view fixture; original geometry never generated by Layout.", + "comparison":"Decode saved JSON with production persistence and compare complete Project to immutable original/expected file. Prefer direct serializer bytes for byte checks; do not reserialize Project through generic JSON Value as a float oracle.", + "performed":"Generation and core checks only when this program is actually run; no native UI results recorded here."}); + files.push(( + "manifest.json".into(), + serde_json::to_vec_pretty(&manifest)?, + )); + fs::create_dir(&output)?; + for (name, bytes) in files { + fs::write(output.join(name), bytes)?; + } + println!("{}", output.join("manifest.json").display()); + Ok(()) +} diff --git a/crates/oa-desktop/src/tests.rs b/crates/oa-desktop/src/tests.rs index e2bf9b8a..261512b7 100644 --- a/crates/oa-desktop/src/tests.rs +++ b/crates/oa-desktop/src/tests.rs @@ -1692,3 +1692,161 @@ fn adding_bend_between_close_ports_stays_outside_owner_clearance() { }] ); } + +fn arrange_reset_fixture() -> (Project, Id, Id) { + let mut project = Project::empty(); + let source = project.add(Kind::Activity, "Send report", 20., 70.); + let target = project.add(Kind::Activity, "Receive report", 650., 200.); + let edge = project.relate(RelationKind::Interaction, "Report", source, target); + let diagram = project.ensure_diagram(Layer::Oa); + project + .diagrams + .get_mut(&diagram) + .unwrap() + .routes + .push(EdgeRoute { + edge, + source_attachment: Some(EdgeAttachment { + side: PortAnchorSide::Bottom, + offset: 0.25, + }), + target_attachment: Some(EdgeAttachment { + side: PortAnchorSide::Top, + offset: 0.75, + }), + waypoints: vec![Position { x: 400., y: 500. }], + label: Some(EdgeLabel { + fraction: 0.25, + offset: Position { x: 11., y: 7. }, + }), + }); + (project, diagram, source) +} + +#[test] +fn arrange_reset_document_preserves_multifile_comments_and_exact_undo_redo_reopen() { + use crate::document::Document; + use oa_core::source_project::{self, ProjectLimits}; + let (project, diagram, _) = arrange_reset_fixture(); + let mut bundle = source_project::format_bundle(&project); + assert!( + bundle.files.len() > 1, + "This must exercise a real include closure" + ); + for (path, text) in &mut bundle.files { + *text = format!("// Keep exact authored module: {path}\r\n{text}"); + } + let mut document = Document::from_bundle(bundle, ProjectLimits::default()).unwrap(); + document.mark_saved(document.snapshot()); + let before = document.snapshot(); + let original_route = before.decoded.project.diagrams[&diagram].routes[0].clone(); + document.execute(Command::Layout(diagram)).unwrap(); + let after = document.snapshot(); + let routes = &after.decoded.project.diagrams[&diagram].routes; + let mut expected_route = original_route; + expected_route.waypoints.clear(); + expected_route.label = None; + assert_eq!(routes, &vec![expected_route]); + let mut outside = after.decoded.project.clone(); + outside + .diagrams + .insert(diagram, before.decoded.project.diagrams[&diagram].clone()); + assert_eq!(outside, before.decoded.project); + let affected: std::collections::BTreeSet<_> = std::iter::once(diagram) + .chain( + before.decoded.project.diagrams[&diagram] + .nodes + .keys() + .copied(), + ) + .map(|id| document.compiled().index.declarations[&id].path.clone()) + .collect(); + for (path, text) in &before.bundle.files { + assert!( + after.bundle.files[path].contains(&format!("// Keep exact authored module: {path}")) + ); + if !affected.contains(path) { + assert_eq!( + &after.bundle.files[path], text, + "Unrelated accepted bytes: {path}" + ); + } + } + assert!(document.undo()); + assert_eq!(document.bundle(), &before.bundle); + assert_eq!(document.project(), &before.decoded.project); + assert!(!document.dirty()); + assert!(document.redo()); + assert_eq!(document.bundle(), &after.bundle); + assert_eq!(document.project(), &after.decoded.project); + let reopened = Document::from_bundle(after.bundle.clone(), ProjectLimits::default()).unwrap(); + assert_eq!(reopened.bundle(), &after.bundle); + assert_eq!(reopened.project(), &after.decoded.project); +} + +#[test] +fn arrange_reset_pending_valid_property_has_separate_apply_and_layout_history() { + let (project, diagram, source) = arrange_reset_fixture(); + let mut control = App::with_project(project.clone()); + send(&mut control, Message::Select(source)); + send( + &mut control, + Message::Field("/name".into(), "Revised sender".into()), + ); + send(&mut control, Message::Apply); + let applied_project = control.project().clone(); + let applied_bundle = control.document.bundle().clone(); + assert_eq!(applied_project.elements[&source].name, "Revised sender"); + assert_eq!( + applied_project.diagrams[&diagram].routes, + project.diagrams[&diagram].routes + ); + let mut app = App::with_project(project); + let before_project = app.project().clone(); + let before_bundle = app.document.bundle().clone(); + send(&mut app, Message::Select(source)); + send( + &mut app, + Message::Field("/name".into(), "Revised sender".into()), + ); + send(&mut app, Message::Command(Command::Layout(diagram))); + assert_eq!(app.project().elements[&source].name, "Revised sender"); + assert!( + app.project().diagrams[&diagram].routes[0] + .waypoints + .is_empty() + ); + assert!(app.project().diagrams[&diagram].routes[0].label.is_none()); + let arranged_project = app.project().clone(); + let arranged_bundle = app.document.bundle().clone(); + send(&mut app, Message::Undo); + assert_eq!(app.project(), &applied_project); + assert_eq!(app.document.bundle(), &applied_bundle); + send(&mut app, Message::Undo); + assert_eq!(app.project(), &before_project); + assert_eq!(app.document.bundle(), &before_bundle); + send(&mut app, Message::Redo); + assert_eq!(app.project(), &applied_project); + assert_eq!(app.document.bundle(), &applied_bundle); + send(&mut app, Message::Redo); + assert_eq!(app.project(), &arranged_project); + assert_eq!(app.document.bundle(), &arranged_bundle); +} + +#[test] +fn arrange_reset_invalid_property_blocks_layout_without_losing_draft_or_accepted_bytes() { + let (project, diagram, _) = arrange_reset_fixture(); + let node = *project.diagrams[&diagram].nodes.keys().next().unwrap(); + let mut app = App::with_project(project); + send(&mut app, Message::Select(node)); + let before_project = app.project().clone(); + let before_bundle = app.document.bundle().clone(); + let revision = app.document.revision(); + send(&mut app, Message::Field("/position/x".into(), "-".into())); + send(&mut app, Message::Command(Command::Layout(diagram))); + assert_eq!(app.project(), &before_project); + assert_eq!(app.document.bundle(), &before_bundle); + assert_eq!(app.document.revision(), revision); + assert!(!app.document.can_undo()); + assert_eq!(app.draft.as_ref().unwrap().inputs["/position/x"].raw, "-"); +}