718 lines
24 KiB
Rust
718 lines
24 KiB
Rust
//! Native evidence fixture/checker; install as a temporary oa-core example.
|
|
//! This invariant evaluator derives delta from saved A, NOT from pointer input.
|
|
use oa_core::{source_project::*, *};
|
|
use std::{fs, io::Write, path::Path};
|
|
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
|
|
|
fn id(alias: &str) -> Id {
|
|
// Make the intended OA view first for default UUID-ordered view selection.
|
|
if alias == "oa_view" {
|
|
return Id::from_u128(0x29_0001);
|
|
}
|
|
Id::new_v5(
|
|
&Id::from_u128(0xc1192026_0923_5000_8000_000000000029),
|
|
alias.as_bytes(),
|
|
)
|
|
}
|
|
fn point(x: f32, y: f32) -> Position {
|
|
Position { x, y }
|
|
}
|
|
fn element(p: &mut Project, alias: &str, name: &str, kind: Kind, layer: Layer, parent: Option<Id>) {
|
|
p.elements.insert(
|
|
id(alias),
|
|
Element {
|
|
id: id(alias),
|
|
alias: alias.into(),
|
|
name: name.into(),
|
|
description: format!("Preserve engineering record {alias}."),
|
|
parent,
|
|
body: ElementBody::new(kind, layer).unwrap(),
|
|
},
|
|
);
|
|
}
|
|
fn diagram(p: &mut Project, alias: &str, name: &str, layer: Layer) {
|
|
p.diagrams.insert(
|
|
id(alias),
|
|
Diagram {
|
|
id: id(alias),
|
|
alias: alias.into(),
|
|
name: name.into(),
|
|
description: "Selection changes placement geometry only.".into(),
|
|
layer,
|
|
projection: ProjectionKind::Overview,
|
|
view_options: Default::default(),
|
|
layout: None,
|
|
nodes: Default::default(),
|
|
routes: vec![],
|
|
},
|
|
);
|
|
}
|
|
fn place(p: &mut Project, view: &str, alias: &str, object: &str, position: Position, size: Size) {
|
|
p.diagrams.get_mut(&id(view)).unwrap().nodes.insert(
|
|
id(alias),
|
|
NodePlacement {
|
|
id: id(alias),
|
|
alias: alias.into(),
|
|
element: id(object),
|
|
position,
|
|
size,
|
|
collapsed: false,
|
|
port_anchors: vec![],
|
|
},
|
|
);
|
|
}
|
|
fn route(edge: &str, pins: Vec<Position>) -> EdgeRoute {
|
|
EdgeRoute {
|
|
edge: id(edge),
|
|
waypoints: pins,
|
|
source_attachment: Some(EdgeAttachment {
|
|
side: PortAnchorSide::Right,
|
|
offset: 0.25,
|
|
}),
|
|
target_attachment: Some(EdgeAttachment {
|
|
side: PortAnchorSide::Left,
|
|
offset: 0.7,
|
|
}),
|
|
label: Some(EdgeLabel {
|
|
fraction: 0.45,
|
|
offset: point(7., -19.),
|
|
}),
|
|
}
|
|
}
|
|
fn fixture() -> Result<Project> {
|
|
let mut p = Project::empty();
|
|
p.id = id("project");
|
|
p.alias = "multiselect_native".into();
|
|
p.name = "Multiselect native".into();
|
|
p.description =
|
|
"Shift-select A and B; move together, retain C and engineering semantics.".into();
|
|
for (alias, name, parent) in [
|
|
("activity_a", "A - receive request", None),
|
|
("activity_b", "B - assess request", None),
|
|
("activity_c", "C - dispatch response", None),
|
|
(
|
|
"activity_child",
|
|
"A.1 - classify request",
|
|
Some(id("activity_a")),
|
|
),
|
|
] {
|
|
element(&mut p, alias, name, Kind::Activity, Layer::Oa, parent);
|
|
}
|
|
element(
|
|
&mut p,
|
|
"sa_function",
|
|
"Technical request processing",
|
|
Kind::Function,
|
|
Layer::Sa,
|
|
None,
|
|
);
|
|
for (alias, name, source, target) in [
|
|
("internal", "Request assessment", "activity_a", "activity_b"),
|
|
("external", "Response decision", "activity_b", "activity_c"),
|
|
] {
|
|
let old = p.relate(RelationKind::Interaction, name, id(source), id(target));
|
|
reidentify(&mut p, old, id(alias))?;
|
|
p.relations.get_mut(&id(alias)).unwrap().alias = alias.into();
|
|
}
|
|
diagram(
|
|
&mut p,
|
|
"oa_view",
|
|
"Multiselect native - OA overview",
|
|
Layer::Oa,
|
|
);
|
|
diagram(&mut p, "oa_other", "Other OA view - preserve", Layer::Oa);
|
|
diagram(&mut p, "sa_view", "SA engineering - preserve", Layer::Sa);
|
|
let size = Size {
|
|
width: 224.,
|
|
height: 94.,
|
|
};
|
|
place(
|
|
&mut p,
|
|
"oa_view",
|
|
"node_a",
|
|
"activity_a",
|
|
point(80., 80.),
|
|
Size {
|
|
width: 310.,
|
|
height: 260.,
|
|
},
|
|
);
|
|
place(
|
|
&mut p,
|
|
"oa_view",
|
|
"node_b",
|
|
"activity_b",
|
|
point(470., 120.),
|
|
size,
|
|
);
|
|
place(
|
|
&mut p,
|
|
"oa_view",
|
|
"node_c",
|
|
"activity_c",
|
|
point(780., 270.),
|
|
size,
|
|
);
|
|
place(
|
|
&mut p,
|
|
"oa_view",
|
|
"node_child",
|
|
"activity_child",
|
|
point(120., 210.),
|
|
size,
|
|
);
|
|
place(
|
|
&mut p,
|
|
"oa_other",
|
|
"other_a",
|
|
"activity_a",
|
|
point(40., 60.),
|
|
size,
|
|
);
|
|
place(
|
|
&mut p,
|
|
"sa_view",
|
|
"sa_node",
|
|
"sa_function",
|
|
point(100., 110.),
|
|
size,
|
|
);
|
|
p.diagrams.get_mut(&id("oa_view")).unwrap().routes = vec![
|
|
route("internal", vec![point(420., 145.), point(420., 186.)]),
|
|
route("external", vec![point(725., 150.), point(725., 336.)]),
|
|
];
|
|
p.traces.insert(
|
|
id("trace"),
|
|
Trace {
|
|
id: id("trace"),
|
|
alias: "preserved_refinement".into(),
|
|
name: "Reviewed request refinement".into(),
|
|
description: "Group geometry and Hide preserve this trace and both endpoints.".into(),
|
|
oa: id("activity_a"),
|
|
sa: id("sa_function"),
|
|
kind: TraceKind::Refinement,
|
|
baseline: Some(TraceBaseline {
|
|
oa: p.properties(id("activity_a")).unwrap(),
|
|
sa: p.properties(id("sa_function")).unwrap(),
|
|
semantic: None,
|
|
}),
|
|
},
|
|
);
|
|
validate_candidate(&p)?;
|
|
Ok(p)
|
|
}
|
|
fn load(path: &Path) -> Result<Project> {
|
|
if path
|
|
.extension()
|
|
.is_some_and(|ext| ext.eq_ignore_ascii_case("arcadia"))
|
|
{
|
|
let disk = source_files::load_project(path, ProjectLimits::default())?;
|
|
// This deliberately partial engineering fixture has completeness
|
|
// warnings (allocation/behavior context). They are not source errors.
|
|
// Compilation, candidate validation and whole-Project equality still
|
|
// reject malformed source or any unexpected model change.
|
|
if disk
|
|
.diagnostics
|
|
.iter()
|
|
.any(|d| d.diagnostic.severity == validation::Severity::Error)
|
|
{
|
|
return Err(format!("Source discovery diagnostics: {:?}", disk.diagnostics).into());
|
|
}
|
|
let compiled = compile_bundle(&disk.reachable_bundle(), ProjectLimits::default())
|
|
.map_err(|e| format!("Source compile diagnostics: {e:?}"))?;
|
|
Ok(compiled.decoded.project)
|
|
} else {
|
|
Ok(persistence::load(path)?)
|
|
}
|
|
}
|
|
fn equal(expected: &Project, actual: &Project) -> Result<()> {
|
|
if expected == actual {
|
|
return Ok(());
|
|
}
|
|
let e = serde_json::to_value(expected)?;
|
|
let a = serde_json::to_value(actual)?;
|
|
let fields: Vec<_> = e
|
|
.as_object()
|
|
.unwrap()
|
|
.keys()
|
|
.filter(|key| e[*key] != a[*key])
|
|
.cloned()
|
|
.collect();
|
|
Err(format!("Whole Project mismatch in {fields:?}; no partial PASS").into())
|
|
}
|
|
fn roundtrip(p: &Project) -> Result<SourceBundle> {
|
|
validate_candidate(p)?;
|
|
equal(p, &persistence::decode(&serde_json::to_vec(p)?)?)?;
|
|
let bundle = format_bundle(p);
|
|
let compiled = compile_bundle(&bundle, ProjectLimits::default())
|
|
.map_err(|e| format!("Roundtrip: {e:?}"))?;
|
|
equal(p, &compiled.decoded.project)?;
|
|
Ok(bundle)
|
|
}
|
|
fn write_new(path: &Path, bytes: &[u8]) -> Result<()> {
|
|
fs::create_dir_all(path.parent().ok_or("Missing parent directory")?)?;
|
|
fs::OpenOptions::new()
|
|
.write(true)
|
|
.create_new(true)
|
|
.open(path)?
|
|
.write_all(bytes)?;
|
|
Ok(())
|
|
}
|
|
fn generate(out: &Path) -> Result<()> {
|
|
if out.exists() {
|
|
return Err("Output directory already exists; choose a fresh path".into());
|
|
}
|
|
let p = fixture()?;
|
|
let bundle = roundtrip(&p)?;
|
|
write_new(&out.join("baseline.json"), &serde_json::to_vec_pretty(&p)?)?;
|
|
for (path, source) in &bundle.files {
|
|
write_new(&out.join("source").join(path), source.as_bytes())?;
|
|
}
|
|
let names = [
|
|
"project",
|
|
"oa_view",
|
|
"oa_other",
|
|
"sa_view",
|
|
"activity_a",
|
|
"activity_b",
|
|
"activity_c",
|
|
"activity_child",
|
|
"node_a",
|
|
"node_b",
|
|
"node_c",
|
|
"node_child",
|
|
"internal",
|
|
"external",
|
|
"trace",
|
|
"sa_function",
|
|
"sa_node",
|
|
"other_a",
|
|
];
|
|
let ids: std::collections::BTreeMap<_, _> =
|
|
names.into_iter().map(|name| (name, id(name))).collect();
|
|
write_new(
|
|
&out.join("manifest.json"),
|
|
&serde_json::to_vec_pretty(&serde_json::json!({
|
|
"fixture": "multiselect-native-20260923", "ids": ids, "source_entry": bundle.entry,
|
|
"move_members": ["node_a", "node_b", "node_child"], "fixed_member": "node_c",
|
|
"evaluation": "Whole Project invariant comparison; saved A supplies bounded shared delta, not an exact pointer oracle",
|
|
"hide": "Remove node_a/node_b only; retain child, stored routes, all model records and other views",
|
|
}))?,
|
|
)?;
|
|
equal(&p, &load(&out.join("baseline.json"))?)?;
|
|
equal(&p, &load(&out.join("source/project.arcadia"))?)?;
|
|
println!("PASS generated JSON and source bundle: {}", out.display());
|
|
Ok(())
|
|
}
|
|
fn check(mode: &str, baseline: &Path, saved: &Path) -> Result<()> {
|
|
let before = load(baseline)?;
|
|
let actual = load(saved)?;
|
|
validate_candidate(&before)?;
|
|
validate_candidate(&actual)?;
|
|
if before.id != id("project") {
|
|
return Err("Not this fixture's baseline project".into());
|
|
}
|
|
let mut expected = before.clone();
|
|
let mut delta = None;
|
|
match mode {
|
|
"unchanged" => {}
|
|
"hide" => {
|
|
let view = expected
|
|
.diagrams
|
|
.get_mut(&id("oa_view"))
|
|
.ok_or("Missing OA view")?;
|
|
for alias in ["node_a", "node_b"] {
|
|
view.nodes
|
|
.remove(&id(alias))
|
|
.ok_or("Baseline selected placement missing")?;
|
|
}
|
|
// references::remove_objects removes routes only when their semantic
|
|
// edge is deleted. Hide deletes placements; both routes MUST survive.
|
|
}
|
|
"moved" => {
|
|
let old = before
|
|
.diagrams
|
|
.get(&id("oa_view"))
|
|
.ok_or("Missing baseline OA view")?;
|
|
let new = actual
|
|
.diagrams
|
|
.get(&id("oa_view"))
|
|
.ok_or("Missing saved OA view")?;
|
|
let a = old
|
|
.nodes
|
|
.get(&id("node_a"))
|
|
.ok_or("Missing baseline A")?
|
|
.position;
|
|
let moved_a = new
|
|
.nodes
|
|
.get(&id("node_a"))
|
|
.ok_or("Missing saved A")?
|
|
.position;
|
|
let d = point(moved_a.x - a.x, moved_a.y - a.y);
|
|
if !d.x.is_finite()
|
|
|| !d.y.is_finite()
|
|
|| d.x.abs().max(d.y.abs()) > 500.
|
|
|| d.x.hypot(d.y) < 10.
|
|
{
|
|
return Err(format!(
|
|
"Move delta must have length >=10 and each axis <=500 world units: {d:?}"
|
|
)
|
|
.into());
|
|
}
|
|
if moved_a.x % 10. != 0. || moved_a.y % 10. != 0. {
|
|
return Err("Saved anchor is not on 10-unit grid".into());
|
|
}
|
|
let view = expected.diagrams.get_mut(&id("oa_view")).unwrap();
|
|
// Independent explicit fixture enumeration: no group/descendant/
|
|
// routing planner is called to predict the result.
|
|
for alias in ["node_a", "node_b", "node_child"] {
|
|
let node = view
|
|
.nodes
|
|
.get_mut(&id(alias))
|
|
.ok_or("Missing expected moved placement")?;
|
|
node.position.x += d.x;
|
|
node.position.y += d.y;
|
|
}
|
|
let internal = view
|
|
.routes
|
|
.iter_mut()
|
|
.find(|route| route.edge == id("internal"))
|
|
.ok_or("Missing internal pins")?;
|
|
for pin in &mut internal.waypoints {
|
|
pin.x += d.x;
|
|
pin.y += d.y;
|
|
}
|
|
delta = Some(d);
|
|
}
|
|
_ => return Err("Mode must be moved, hide, or unchanged".into()),
|
|
}
|
|
equal(&expected, &actual)?;
|
|
roundtrip(&actual)?;
|
|
println!(
|
|
"{}",
|
|
serde_json::to_string_pretty(&serde_json::json!({
|
|
"result": "PASS", "mode": mode, "baseline": baseline, "saved": saved,
|
|
"comparison": "complete Project equality", "inferred_common_delta": delta,
|
|
"limits": "Invariant evaluator only; no pointer/action/focus/Undo/source-byte acceptance claim",
|
|
}))?
|
|
);
|
|
Ok(())
|
|
}
|
|
fn main() -> Result<()> {
|
|
let args: Vec<_> = std::env::args().skip(1).collect();
|
|
match args.as_slice() {
|
|
[command, out] if command == "generate" => generate(Path::new(out)),
|
|
[command, mode, baseline, saved] if command == "check" => check(mode, Path::new(baseline), Path::new(saved)),
|
|
_ => Err("Usage: multiselect-native-producer generate DIRECTORY | check moved|hide|unchanged BASELINE SAVED".into()),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::collections::BTreeSet;
|
|
|
|
// Produce evidence through real validated commands; the checker itself uses
|
|
// explicit fixture enumeration and never calls this movement implementation.
|
|
fn moved(before: &Project, delta: Position) -> Project {
|
|
let command = diagram_edit::translate_placements(
|
|
before,
|
|
id("oa_view"),
|
|
&BTreeSet::from([id("node_a"), id("node_b")]),
|
|
delta,
|
|
)
|
|
.unwrap();
|
|
let mut session = Session::new(before.clone());
|
|
session.execute(command).unwrap();
|
|
session.project().clone()
|
|
}
|
|
fn hidden(before: &Project) -> Project {
|
|
let mut session = Session::new(before.clone());
|
|
session
|
|
.execute(Command::Batch(vec![
|
|
Command::Delete(id("node_a")),
|
|
Command::Delete(id("node_b")),
|
|
]))
|
|
.unwrap();
|
|
session.project().clone()
|
|
}
|
|
fn through_saved_json(mode: &str, baseline: &Project, actual: &Project) -> Result<()> {
|
|
let dir = tempfile::tempdir()?;
|
|
let before = dir.path().join("before.json");
|
|
let saved = dir.path().join("saved.json");
|
|
// These assertions distinguish meaningful invariant corruptions from
|
|
// malformed projects rejected before the evaluator even sees them.
|
|
validate_candidate(baseline).expect("test baseline must be valid");
|
|
validate_candidate(actual).expect("corruption must remain a valid model");
|
|
persistence::save(&before, baseline)?;
|
|
persistence::save(&saved, actual)?;
|
|
check(mode, &before, &saved)
|
|
}
|
|
fn mismatch(mode: &str, before: &Project, actual: &Project, case: &str) {
|
|
let error = through_saved_json(mode, before, actual)
|
|
.expect_err(case)
|
|
.to_string();
|
|
assert!(
|
|
error.contains("Whole Project mismatch"),
|
|
"{case}: unexpected rejection: {error}"
|
|
);
|
|
}
|
|
fn edge<'a>(p: &'a mut Project, alias: &str) -> &'a mut EdgeRoute {
|
|
p.diagrams
|
|
.get_mut(&id("oa_view"))
|
|
.unwrap()
|
|
.routes
|
|
.iter_mut()
|
|
.find(|route| route.edge == id(alias))
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn generated_json_and_real_source_bundle_are_accepted_unchanged() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let out = dir.path().join("fixture");
|
|
generate(&out).unwrap();
|
|
let baseline = out.join("baseline.json");
|
|
let saved = out.join("saved.json");
|
|
persistence::save(&saved, &fixture().unwrap()).unwrap();
|
|
check("unchanged", &baseline, &saved).unwrap();
|
|
check("unchanged", &baseline, &out.join("source/project.arcadia")).unwrap();
|
|
let bytes = fs::read(&baseline).unwrap();
|
|
assert!(
|
|
generate(&out).is_err(),
|
|
"generation cannot overwrite evidence"
|
|
);
|
|
assert_eq!(fs::read(&baseline).unwrap(), bytes);
|
|
let invalid = dir.path().join("invalid.arcadia");
|
|
fs::write(&invalid, "project {").unwrap();
|
|
assert!(load(&invalid).is_err(), "source errors must still reject");
|
|
}
|
|
|
|
#[test]
|
|
fn real_group_command_outputs_pass_and_are_not_unchanged() {
|
|
let before = fixture().unwrap();
|
|
for delta in [point(40., 60.), point(-20., 30.)] {
|
|
let actual = moved(&before, delta);
|
|
through_saved_json("moved", &before, &actual).unwrap();
|
|
mismatch(
|
|
"unchanged",
|
|
&before,
|
|
&actual,
|
|
"real movement cannot count as unchanged",
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn real_hide_command_outputs_pass_before_and_after_movement() {
|
|
let initial = fixture().unwrap();
|
|
for before in [initial.clone(), moved(&initial, point(40., 60.))] {
|
|
let actual = hidden(&before);
|
|
through_saved_json("hide", &before, &actual).unwrap();
|
|
mismatch(
|
|
"unchanged",
|
|
&before,
|
|
&actual,
|
|
"real Hide cannot count as unchanged",
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn omitted_and_double_moved_child_are_rejected() {
|
|
let before = fixture().unwrap();
|
|
let delta = point(40., 60.);
|
|
let correct = moved(&before, delta);
|
|
let initial = before.diagrams[&id("oa_view")].nodes[&id("node_child")].position;
|
|
for position in [
|
|
initial,
|
|
point(initial.x + 2. * delta.x, initial.y + 2. * delta.y),
|
|
] {
|
|
let mut bad = correct.clone();
|
|
bad.diagrams
|
|
.get_mut(&id("oa_view"))
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&id("node_child"))
|
|
.unwrap()
|
|
.position = position;
|
|
mismatch("moved", &before, &bad, "child did not move exactly once");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn internal_pins_left_behind_and_external_pins_moved_are_rejected() {
|
|
let mut before = fixture().unwrap();
|
|
let original_pins = edge(&mut before, "internal").waypoints.clone();
|
|
let correct = moved(&before, point(40., 60.));
|
|
let mut bad = correct.clone();
|
|
edge(&mut bad, "internal").waypoints = original_pins;
|
|
mismatch("moved", &before, &bad, "internal pins left behind");
|
|
let mut bad = correct;
|
|
for pin in &mut edge(&mut bad, "external").waypoints {
|
|
pin.x += 40.;
|
|
pin.y += 60.;
|
|
}
|
|
mismatch("moved", &before, &bad, "one-ended pins moved");
|
|
}
|
|
|
|
#[test]
|
|
fn manual_attachments_and_label_data_must_remain_exact() {
|
|
let before = fixture().unwrap();
|
|
let correct = moved(&before, point(40., 60.));
|
|
let mutations: [fn(&mut EdgeRoute); 4] = [
|
|
|r| r.source_attachment.as_mut().unwrap().offset = 0.65,
|
|
|r| r.target_attachment.as_mut().unwrap().side = PortAnchorSide::Top,
|
|
|r| r.label.as_mut().unwrap().offset.x += 40.,
|
|
|r| r.label.as_mut().unwrap().fraction = 0.75,
|
|
];
|
|
for mutate in mutations {
|
|
let mut bad = correct.clone();
|
|
mutate(edge(&mut bad, "internal"));
|
|
mismatch("moved", &before, &bad, "relative route metadata changed");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn semantic_other_view_fixed_node_and_trace_drift_are_rejected() {
|
|
let before = fixture().unwrap();
|
|
let correct = moved(&before, point(40., 60.));
|
|
let mutations: [fn(&mut Project); 5] = [
|
|
|p| {
|
|
p.elements
|
|
.get_mut(&id("activity_a"))
|
|
.unwrap()
|
|
.description
|
|
.push_str(" drift")
|
|
},
|
|
|p| {
|
|
p.diagrams
|
|
.get_mut(&id("oa_other"))
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&id("other_a"))
|
|
.unwrap()
|
|
.position
|
|
.x += 40.
|
|
},
|
|
|p| {
|
|
p.diagrams
|
|
.get_mut(&id("sa_view"))
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&id("sa_node"))
|
|
.unwrap()
|
|
.position
|
|
.y += 60.
|
|
},
|
|
|p| {
|
|
p.diagrams
|
|
.get_mut(&id("oa_view"))
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&id("node_c"))
|
|
.unwrap()
|
|
.position
|
|
.x += 40.
|
|
},
|
|
|p| {
|
|
p.traces
|
|
.get_mut(&id("trace"))
|
|
.unwrap()
|
|
.description
|
|
.push_str(" drift")
|
|
},
|
|
];
|
|
for mutate in mutations {
|
|
let mut bad = correct.clone();
|
|
mutate(&mut bad);
|
|
mismatch(
|
|
"moved",
|
|
&before,
|
|
&bad,
|
|
"record outside permitted movement changed",
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn zero_small_excessive_and_offgrid_moves_cannot_satisfy_move_evidence() {
|
|
let before = fixture().unwrap();
|
|
for (delta, reason) in [
|
|
(point(0., 0.), "length >=10"),
|
|
(point(3., 0.), "length >=10"),
|
|
(point(510., 0.), "each axis <=500"),
|
|
(point(20.5, 0.), "10-unit grid"),
|
|
] {
|
|
let actual = moved(&before, delta);
|
|
let error = through_saved_json("moved", &before, &actual)
|
|
.unwrap_err()
|
|
.to_string();
|
|
assert!(error.contains(reason), "{delta:?}: {error}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn hide_must_preserve_child_routes_and_semantic_objects() {
|
|
let before = fixture().unwrap();
|
|
let correct = hidden(&before);
|
|
let mut missing_child = correct.clone();
|
|
missing_child
|
|
.diagrams
|
|
.get_mut(&id("oa_view"))
|
|
.unwrap()
|
|
.nodes
|
|
.remove(&id("node_child"));
|
|
mismatch(
|
|
"hide",
|
|
&before,
|
|
&missing_child,
|
|
"Hide removed unselected child placement",
|
|
);
|
|
let mut missing_routes = correct;
|
|
missing_routes
|
|
.diagrams
|
|
.get_mut(&id("oa_view"))
|
|
.unwrap()
|
|
.routes
|
|
.clear();
|
|
mismatch(
|
|
"hide",
|
|
&before,
|
|
&missing_routes,
|
|
"Hide removed persistent route records",
|
|
);
|
|
let mut semantic_delete = Session::new(before.clone());
|
|
semantic_delete
|
|
.execute(Command::Batch(vec![
|
|
Command::Delete(id("activity_a")),
|
|
Command::Delete(id("activity_b")),
|
|
]))
|
|
.unwrap();
|
|
mismatch(
|
|
"hide",
|
|
&before,
|
|
semantic_delete.project(),
|
|
"Hide became semantic deletion",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unchanged_rejects_valid_semantic_drift_without_geometry_changes() {
|
|
let before = fixture().unwrap();
|
|
let mut actual = before.clone();
|
|
actual
|
|
.elements
|
|
.get_mut(&id("activity_b"))
|
|
.unwrap()
|
|
.name
|
|
.push_str(" edited");
|
|
mismatch(
|
|
"unchanged",
|
|
&before,
|
|
&actual,
|
|
"geometry equality is not complete Project equality",
|
|
);
|
|
}
|
|
}
|