Support atomic Allocation group assignment and Arrange gestures

This commit is contained in:
2026-09-23 20:25:26 -07:00
parent 16b30a6760
commit 495464e84b
14 changed files with 2928 additions and 220 deletions
@@ -0,0 +1,767 @@
//! Deterministic native fixture and literal complete-Project Allocation oracle.
//! No desktop planner, inferred output delta, or GUI input is used by this oracle.
use oa_core::{source_project::*, *};
use std::{collections::BTreeMap, fs, io::Write, path::Path};
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
const MODES: [&str; 6] = [
"unchanged",
"oa-assigned",
"sa-assigned",
"oa-arranged",
"sa-arranged",
"oa-performers",
];
fn id(alias: &str) -> Id {
if alias == "oa_assign" {
return Id::from_u128(0x292609230001);
}
Id::new_v5(
&Id::from_u128(0xc1192026_0923_5000_8000_000000002931),
alias.as_bytes(),
)
}
fn xy(x: f32, y: f32) -> Position {
Position { x, y }
}
fn named(scope: &str, suffix: &str) -> String {
format!("{scope}_{suffix}")
}
fn need(ok: bool, message: impl Into<String>) -> Result<()> {
if ok {
Ok(())
} else {
Err(message.into().into())
}
}
fn element(p: &mut Project, alias: &str, name: &str, kind: Kind, layer: Layer) {
p.elements.insert(
id(alias),
Element {
id: id(alias),
alias: alias.into(),
name: name.into(),
description: format!("Preserve engineering record {alias}."),
parent: None,
body: ElementBody::new(kind, layer).unwrap(),
},
);
}
fn diagram(p: &mut Project, alias: &str, name: &str, layer: Layer, projection: ProjectionKind) {
p.diagrams.insert(
id(alias),
Diagram {
id: id(alias),
alias: alias.into(),
name: name.into(),
description: "Native group fixture; preserve unrelated records exactly.".into(),
layer,
projection,
view_options: Default::default(),
nodes: Default::default(),
routes: vec![],
},
);
}
fn place(p: &mut Project, scope: &str, suffix: &str, position: Position) {
let alias = named(scope, &format!("node_{suffix}"));
p.diagrams.get_mut(&id(scope)).unwrap().nodes.insert(
id(&alias),
NodePlacement {
id: id(&alias),
alias,
element: id(&named(scope, suffix)),
position,
size: Size {
width: 224.,
height: 94.,
},
collapsed: false,
port_anchors: vec![],
},
);
}
fn relation(
p: &mut Project,
scope: &str,
suffix: &str,
kind: RelationKind,
from: &str,
to: &str,
) -> Result<()> {
let alias = named(scope, suffix);
let old = p.relate(
kind,
&format!("{scope} {suffix}"),
id(&named(scope, from)),
id(&named(scope, to)),
);
reidentify(p, old, id(&alias))?;
let r = p.relations.get_mut(&id(&alias)).unwrap();
r.alias = alias.clone();
r.description = format!("Reviewed identity and provenance {alias} must survive.");
// The actual constructor creates proper SA function contracts. Normalize
// every generated identity/alias; no random UUID can leak into the fixture.
let ports = match r.body {
RelationBody::FunctionalExchange {
source_port,
target_port,
..
} => Some([source_port, target_port]),
_ => None,
};
if let Some(ports) = ports {
for (old_port, suffix) in ports.into_iter().zip(["output", "input"]) {
let alias = format!("{alias}_{suffix}");
reidentify(p, old_port, id(&alias))?;
let port = p.elements.get_mut(&id(&alias)).unwrap();
port.alias = alias.clone();
port.name = format!("{scope} {suffix}");
port.description = format!("Preserved typed endpoint {alias}.");
}
}
Ok(())
}
fn route(scope: &str, pins: [Position; 2]) -> EdgeRoute {
EdgeRoute {
edge: id(&named(scope, "exchange")),
waypoints: pins.to_vec(),
source_attachment: None,
target_attachment: None,
label: Some(EdgeLabel {
fraction: 0.45,
offset: xy(7., -19.),
}),
}
}
fn fixture() -> Result<Project> {
let mut p = Project::empty();
p.id = id("project");
p.alias = "allocation_multiselect_native".into();
p.name = "Allocation group native".into();
p.description = "Four independent compact OA/SA assignment and Arrange trials; ordinary performers use fresh OA assign baseline.".into();
for (layer, prefix, work, performer) in [
(Layer::Oa, "oa", Kind::Activity, Kind::Entity),
(Layer::Sa, "sa", Kind::Function, Kind::SystemActor),
] {
for arrange in [false, true] {
let scope = format!("{prefix}_{}", if arrange { "arrange" } else { "assign" });
let number = match (prefix, arrange) {
("oa", false) => "01",
("sa", false) => "02",
("oa", true) => "03",
_ => "04",
};
diagram(
&mut p,
&scope,
&format!(
"{number} {} {}",
prefix.to_uppercase(),
if arrange { "arrange" } else { "assign" }
),
layer,
ProjectionKind::Allocation,
);
let records: Vec<_> = if arrange {
vec![
("p", "P - team", performer, xy(40., 40.)),
("u", "U - contained work", work, xy(80., 140.)),
("v", "V - independent work", work, xy(440., 140.)),
]
} else {
vec![
("a", "A - origin team", performer, xy(43., 47.)),
// Leave room for SA Y's automatic top port stub below
// B's 64-unit header plus 12-unit routing clearance.
("b", "B - other team", performer, xy(43., 337.)),
("c", "C - destination", performer, xy(500., 40.)),
("x", "X - receive", work, xy(80., 140.)),
("y", "Y - assess", work, xy(80., 440.)),
]
};
for (suffix, label, kind, pos) in records {
// SA admits one System for the entire project, not one per view.
let kind = if layer == Layer::Sa && !arrange && suffix == "a" {
Kind::System
} else {
kind
};
element(&mut p, &named(&scope, suffix), label, kind, layer);
place(&mut p, &scope, suffix, pos);
}
let (from, to, pins) = if arrange {
relation(&mut p, &scope, "pu", RelationKind::Allocation, "p", "u")?;
("u", "v", [xy(360., 180.), xy(360., 230.)])
} else {
p.diagrams
.get_mut(&id(&scope))
.unwrap()
.nodes
.get_mut(&id(&named(&scope, "node_c")))
.unwrap()
.size = Size {
width: 360.,
height: 600.,
};
relation(&mut p, &scope, "ax", RelationKind::Allocation, "a", "x")?;
relation(&mut p, &scope, "by", RelationKind::Allocation, "b", "y")?;
("x", "y", [xy(340., 260.), xy(340., 400.)])
};
relation(
&mut p,
&scope,
"exchange",
if layer == Layer::Oa {
RelationKind::Interaction
} else {
RelationKind::FunctionalExchange
},
from,
to,
)?;
p.diagrams
.get_mut(&id(&scope))
.unwrap()
.routes
.push(route(&scope, pins));
}
}
diagram(
&mut p,
"other_view",
"05 Other saved view - preserve",
Layer::Oa,
ProjectionKind::Overview,
);
for (suffix, pos) in [("x", xy(40., 60.)), ("y", xy(410., 180.))] {
let alias = named("other_view", suffix);
p.diagrams.get_mut(&id("other_view")).unwrap().nodes.insert(
id(&alias),
NodePlacement {
id: id(&alias),
alias,
element: id(&named("oa_assign", suffix)),
position: pos,
size: Size {
width: 224.,
height: 94.,
},
collapsed: false,
port_anchors: vec![],
},
);
}
validate_candidate(&p)?;
Ok(p)
}
fn node_mut<'a>(p: &'a mut Project, scope: &str, suffix: &str) -> &'a mut NodePlacement {
p.diagrams
.get_mut(&id(scope))
.unwrap()
.nodes
.get_mut(&id(&named(scope, &format!("node_{suffix}"))))
.unwrap()
}
fn expected(before: &Project, mode: &str) -> Result<Project> {
let mut p = before.clone();
match mode {
"unchanged" => {}
"oa-assigned" | "sa-assigned" => {
let scope = if mode == "oa-assigned" {
"oa_assign"
} else {
"sa_assign"
};
for (rel, work) in [("ax", "x"), ("by", "y")] {
p.relations.get_mut(&id(&named(scope, rel))).unwrap().body =
RelationBody::Allocation {
source: id(&named(scope, "c")),
target: id(&named(scope, work)),
};
}
node_mut(&mut p, scope, "x").position = xy(560., 140.);
node_mut(&mut p, scope, "y").position = xy(560., 440.);
p.diagrams.get_mut(&id(scope)).unwrap().routes[0].waypoints =
vec![xy(820., 260.), xy(820., 400.)];
}
"oa-arranged" | "sa-arranged" => {
let scope = if mode == "oa-arranged" {
"oa_arrange"
} else {
"sa_arrange"
};
for (suffix, pos) in [
("p", xy(40., 280.)),
("u", xy(80., 380.)),
("v", xy(440., 380.)),
] {
node_mut(&mut p, scope, suffix).position = pos;
}
p.diagrams.get_mut(&id(scope)).unwrap().routes[0].waypoints =
vec![xy(360., 420.), xy(360., 470.)];
}
"oa-performers" => {
for (suffix, pos) in [
("a", xy(120., 90.)),
("b", xy(120., 380.)),
("x", xy(157., 183.)),
("y", xy(157., 483.)),
] {
node_mut(&mut p, "oa_assign", suffix).position = pos;
}
p.diagrams.get_mut(&id("oa_assign")).unwrap().routes[0].waypoints =
vec![xy(417., 303.), xy(417., 443.)];
}
_ => return Err(format!("Unknown mode {mode}; expected {MODES:?}").into()),
}
Ok(p)
}
fn ancestor(view: &projection::ProjectedView, parent: Id, child: Id) -> bool {
let mut next = view
.nodes
.iter()
.find(|n| n.id == child)
.and_then(|n| n.visual_parent);
for _ in 0..view.nodes.len() {
let Some(current) = next else {
return false;
};
if current == parent {
return true;
}
next = view
.nodes
.iter()
.find(|n| n.id == current)
.and_then(|n| n.visual_parent);
}
false
}
/// Fixture-usability preflight only: never calculates expected mutations.
fn preflight(p: &Project) -> Result<serde_json::Value> {
validate_candidate(p)?;
let mut scenes = Vec::new();
for scope in ["oa_assign", "sa_assign", "oa_arrange", "sa_arrange"] {
let d = &p.diagrams[&id(scope)];
let view = projection::project_view(p, d.id);
need(
view.nodes.len() == d.nodes.len(),
format!("{scope}: hidden/missing fixture placement"),
)?;
for n in &view.nodes {
need(
n.position == d.nodes[&n.id].position,
format!("{scope}: unexpectedly shifted projected origin {}", n.id),
)?;
need(
n.position.x >= 0.
&& n.position.y >= 0.
&& n.position.x + n.size.width <= 900.
&& n.position.y + n.size.height <= 750.,
format!("{scope}: fixture exceeds declared compact world envelope: {n:?}"),
)?;
if p.elements[&n.element].kind().is_work() {
let height = if d.layer == Layer::Sa { 132. } else { 94. };
need(
n.size
== Size {
width: 224.,
height,
},
format!("{scope}: unexpected work/port extent {n:?}"),
)?;
}
}
for (i, a) in view.nodes.iter().enumerate() {
for b in &view.nodes[i + 1..] {
if ancestor(&view, a.id, b.id) || ancestor(&view, b.id, a.id) {
continue;
}
let width = (a.position.x + a.size.width).min(b.position.x + b.size.width)
- a.position.x.max(b.position.x);
let height = (a.position.y + a.size.height).min(b.position.y + b.size.height)
- a.position.y.max(b.position.y);
need(
width <= 0. || height <= 0.,
format!("{scope}: unrelated projected overlap {} {}", a.id, b.id),
)?;
}
}
let routed = routing::route_diagram(p, d, &view);
let edge = id(&named(scope, "exchange"));
let r = routed
.edges
.get(&edge)
.ok_or("Authored exchange route is missing")?;
need(
!r.custom_suspended && r.pins == d.routes[0].waypoints,
format!("{scope}: authored pins are not active"),
)?;
need(
r.warnings.is_empty(),
format!(
"{scope}: route warnings require fixture review: {:?}; ports={:?}; points={:?}; nodes={:?}",
r.warnings, view.ports, r.points, view.nodes
),
)?;
need(
view.ports.len() == if d.layer == Layer::Sa { 2 } else { 0 },
format!("{scope}: unexpected visible typed ports"),
)?;
scenes.push(serde_json::json!({"diagram":scope,"nodes":view.nodes,"ports":view.ports,"active_pins":r.pins,"route_warnings":r.warnings.iter().map(ToString::to_string).collect::<Vec<_>>()}));
}
Ok(serde_json::json!(scenes))
}
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())?;
need(
!disk
.diagnostics
.iter()
.any(|d| d.diagnostic.severity == validation::Severity::Error),
format!("Source discovery errors: {:?}", disk.diagnostics),
)?;
let compiled = compile_bundle(&disk.reachable_bundle(), ProjectLimits::default())
.map_err(|e| format!("Source compile: {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 mut bundle = format_bundle(p);
for (path, text) in &mut bundle.files {
*text = format!("// Native preservation sentinel: {path}\n{text}");
}
let parsed = compile_bundle(&bundle, ProjectLimits::default())
.map_err(|e| format!("Source roundtrip: {e:?}"))?;
equal(p, &parsed.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<()> {
need(!out.exists(), "Output exists; choose a fresh directory")?;
let p = fixture()?;
let bundle = roundtrip(&p)?;
let mut geometry = BTreeMap::new();
// All literal candidates must satisfy the same compact-fixture prerequisites
// before any bytes are published for a native run.
for mode in MODES {
let candidate = expected(&p, mode)?;
roundtrip(&candidate)?;
geometry.insert(mode, preflight(&candidate)?);
}
let mut ids = BTreeMap::from([(p.alias.clone(), p.id)]);
ids.extend(p.elements.values().map(|v| (v.alias.clone(), v.id)));
ids.extend(p.relations.values().map(|v| (v.alias.clone(), v.id)));
ids.extend(p.diagrams.values().map(|v| (v.alias.clone(), v.id)));
ids.extend(
p.diagrams
.values()
.flat_map(|d| d.nodes.values().map(|v| (v.alias.clone(), v.id))),
);
write_new(&out.join("baseline.json"), &serde_json::to_vec_pretty(&p)?)?;
for (path, text) in &bundle.files {
write_new(&out.join("source").join(path), text.as_bytes())?;
}
let paths: BTreeMap<_, _> = ids
.iter()
.map(|(alias, id)| {
let needle = id.to_string();
let files: Vec<_> = bundle
.files
.iter()
.filter(|(_, text)| text.contains(&needle))
.map(|(path, _)| path.clone())
.collect();
(alias.clone(), files)
})
.collect();
write_new(
&out.join("manifest.json"),
&serde_json::to_vec_pretty(&serde_json::json!({
"fixture":"allocation-multiselect-native-20260923","source_entry":bundle.entry,"ids":ids,
"source_files":bundle.files.keys().collect::<Vec<_>>(),"id_text_occurrence_files":paths,
"modes":MODES,"geometry_preflight":geometry,
"literal_deltas":{"oa-assigned":[480,0],"sa-assigned":[480,0],"oa-arranged":[0,240],"sa-arranged":[0,240],"oa-performers":[77,43]},
"selection":{"assigned":["X","Y"],"arranged":["P","V"],"ordinary_performers":["A","B"]},
"ordinary_anchor_target":[120,90],
"evaluation":"Literal complete Project oracle, never inferred from saved geometry; baseline pinned to deterministic original",
"limits":"Geometry preflight is not GUI/planner admission. Source comments/history bytes, focus, exact selection, pointer delivery and one Undo/Redo require separate observation/checks. Each mode is one operation from baseline."
}))?,
)?;
equal(&p, &load(&out.join("baseline.json"))?)?;
equal(&p, &load(&out.join("source").join(&bundle.entry))?)?;
println!("PASS generated and reloaded fixture: {}", out.display());
Ok(())
}
fn check(mode: &str, baseline: &Path, saved: &Path) -> Result<()> {
let before = load(baseline)?;
equal(&fixture()?, &before).map_err(|e| format!("Baseline is not immutable original: {e}"))?;
let actual = load(saved)?;
validate_candidate(&actual)?;
equal(&expected(&before, mode)?, &actual)?;
roundtrip(&actual)?;
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"result":"PASS","mode":mode,"baseline":baseline,"saved":saved,
"comparison":"complete Project equality; literal fixed record edits",
"limits":"No pointer, selection, source-byte preservation, focus, Undo/Redo, or native 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(format!("Usage: allocation_multiselect_native generate DIRECTORY | check MODE BASELINE SAVED; modes={MODES:?}").into()),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn through_json(mode: &str, before: &Project, actual: &Project) -> Result<()> {
validate_candidate(before).expect("baseline valid");
validate_candidate(actual).expect("falsifier must remain core-valid");
let dir = tempfile::tempdir()?;
let baseline = dir.path().join("baseline.json");
let saved = dir.path().join("saved.json");
persistence::save(&baseline, before)?;
persistence::save(&saved, actual)?;
check(mode, &baseline, &saved)
}
fn mismatch(mode: &str, before: &Project, actual: &Project) {
let error = through_json(mode, before, actual).unwrap_err().to_string();
assert!(
error.contains("Whole Project mismatch"),
"wrong failure cause: {error}"
);
}
#[test]
fn deterministic_create_only_generation_and_all_literal_modes() {
let p = fixture().unwrap();
assert_eq!(p, fixture().unwrap());
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("fixture");
generate(&out).unwrap();
let before = fs::read(out.join("baseline.json")).unwrap();
assert!(generate(&out).is_err());
assert_eq!(before, fs::read(out.join("baseline.json")).unwrap());
for mode in MODES {
let candidate = expected(&p, mode).unwrap();
preflight(&candidate).unwrap();
through_json(mode, &p, &candidate).unwrap();
if mode != "unchanged" {
mismatch(mode, &p, &p);
}
}
assert!(expected(&p, "invented").is_err());
let changed = expected(&p, "oa-assigned").unwrap();
assert!(
through_json("unchanged", &changed, &changed)
.unwrap_err()
.to_string()
.contains("immutable original")
);
}
#[test]
fn actual_core_assignment_batch_matches_literal_oracle_and_one_step_history() {
let before = fixture().unwrap();
for (mode, scope) in [("oa-assigned", "oa_assign"), ("sa-assigned", "sa_assign")] {
let mut commands = Vec::new();
for (alias, work) in [("ax", "x"), ("by", "y")] {
let relation = id(&named(scope, alias));
let mut value = before.relations[&relation].clone();
value.body = RelationBody::Allocation {
source: id(&named(scope, "c")),
target: id(&named(scope, work)),
};
commands.push(Command::EditRelation(relation, value));
}
for (suffix, position) in [("x", xy(560., 140.)), ("y", xy(560., 440.))] {
let original = id(&named(scope, &format!("node_{suffix}")));
let mut node = before.diagrams[&id(scope)].nodes[&original].clone();
node.position = position;
commands.push(Command::EditNode {
diagram: id(scope),
original,
node,
});
}
let mut route = before.diagrams[&id(scope)].routes[0].clone();
route.waypoints = vec![xy(820., 260.), xy(820., 400.)];
commands.push(Command::SetEdgeRoute {
diagram: id(scope),
route,
});
let mut session = Session::new(before.clone());
session.execute(Command::Batch(commands)).unwrap();
through_json(mode, &before, session.project()).unwrap();
let after = session.project().clone();
session.undo();
assert_eq!(session.project(), &before);
assert!(!session.can_undo());
session.redo();
assert_eq!(session.project(), &after);
assert!(!session.can_redo());
}
}
#[test]
fn assignment_rejects_singleton_semantic_identity_and_route_drift() {
let before = fixture().unwrap();
for mode in ["oa-assigned", "sa-assigned"] {
let scope = if mode == "oa-assigned" {
"oa_assign"
} else {
"sa_assign"
};
let good = expected(&before, mode).unwrap();
let mut bad = good.clone();
node_mut(&mut bad, scope, "y").position = xy(80., 440.);
mismatch(mode, &before, &bad);
let mut bad = good.clone();
bad.relations.insert(
id(&named(scope, "by")),
before.relations[&id(&named(scope, "by"))].clone(),
);
mismatch(mode, &before, &bad);
let mut bad = good.clone();
reidentify(
&mut bad,
id(&named(scope, "ax")),
id("replacement_allocation"),
)
.unwrap();
mismatch(mode, &before, &bad);
let mut bad = good.clone();
bad.relations
.get_mut(&id(&named(scope, "ax")))
.unwrap()
.description
.push_str(" drift");
mismatch(mode, &before, &bad);
for pins in [
vec![xy(340., 260.), xy(340., 400.)],
vec![xy(1300., 260.), xy(1300., 400.)],
] {
let mut bad = good.clone();
bad.diagrams.get_mut(&id(scope)).unwrap().routes[0].waypoints = pins;
mismatch(mode, &before, &bad);
}
}
}
#[test]
fn arrange_and_ordinary_closure_falsifiers_remain_core_valid() {
let before = fixture().unwrap();
for mode in ["oa-arranged", "sa-arranged"] {
let scope = if mode == "oa-arranged" {
"oa_arrange"
} else {
"sa_arrange"
};
let good = expected(&before, mode).unwrap();
for (suffix, pos) in [("u", xy(80., 140.)), ("v", xy(440., 140.))] {
let mut bad = good.clone();
node_mut(&mut bad, scope, suffix).position = pos;
mismatch(mode, &before, &bad);
}
let mut bad = good.clone();
bad.relations.remove(&id(&named(scope, "pu")));
mismatch(mode, &before, &bad);
}
let good = expected(&before, "oa-performers").unwrap();
let mut bad = good.clone();
node_mut(&mut bad, "oa_assign", "x").position = xy(80., 140.);
mismatch("oa-performers", &before, &bad);
let mut bad = good;
bad.relations.get_mut(&id("oa_assign_ax")).unwrap().body = RelationBody::Allocation {
source: id("oa_assign_c"),
target: id("oa_assign_x"),
};
mismatch("oa-performers", &before, &bad);
}
#[test]
fn whole_project_sentinels_and_real_saved_source_are_checked() {
let before = fixture().unwrap();
let dir = tempfile::tempdir().unwrap();
let baseline = dir.path().join("baseline.json");
persistence::save(&baseline, &before).unwrap();
for mode in MODES {
let good = expected(&before, mode).unwrap();
for corrupt in [false, true] {
let mut actual = good.clone();
if corrupt {
actual
.elements
.get_mut(&id("sa_arrange_v"))
.unwrap()
.description
.push_str(" drift");
}
let bundle = roundtrip(&actual).unwrap();
let folder = dir.path().join(format!("{mode}-{corrupt}"));
for (path, text) in &bundle.files {
write_new(&folder.join(path), text.as_bytes()).unwrap();
}
let result = check(mode, &baseline, &folder.join(&bundle.entry));
if corrupt {
assert!(
result
.unwrap_err()
.to_string()
.contains("Whole Project mismatch")
);
} else {
result.unwrap();
}
}
let mut bad = good;
bad.diagrams
.get_mut(&id("other_view"))
.unwrap()
.nodes
.get_mut(&id("other_view_x"))
.unwrap()
.position
.x += 10.;
mismatch(mode, &before, &bad);
}
let invalid = dir.path().join("invalid.arcadia");
write_new(&invalid, b"project {").unwrap();
assert!(load(&invalid).is_err());
}
}