Add desktop reset history checks and native wiring fixtures

This commit is contained in:
2026-09-24 19:18:31 -07:00
parent 6c44a2d6a1
commit 9eb4642e50
2 changed files with 421 additions and 0 deletions
@@ -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>) -> 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<Position>, 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<u8> {
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<u8> {
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<dyn Error>> {
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(())
}
+158
View File
@@ -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, "-");
}