Merge Process and Allocation group authoring into the prerelease candidate
This commit is contained in:
@@ -0,0 +1,768 @@
|
||||
//! 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,
|
||||
layout: None,
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
//! Deterministic Process native fixture and independent complete-Project oracle.
|
||||
//! Expected order is literal; no desktop reorder/layout planner is used here.
|
||||
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 {
|
||||
Id::new_v5(
|
||||
&Id::from_u128(0xc1192026_0923_5000_8000_000000002930),
|
||||
alias.as_bytes(),
|
||||
)
|
||||
}
|
||||
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 occurrence(
|
||||
p: &mut Project,
|
||||
alias: &str,
|
||||
name: &str,
|
||||
layer: Layer,
|
||||
owner: &str,
|
||||
value: OccurrenceBody,
|
||||
) {
|
||||
element(p, alias, name, Kind::Occurrence, layer, Some(id(owner)));
|
||||
let Some(SharedBody::Occurrence { body, .. }) =
|
||||
p.elements.get_mut(&id(alias)).unwrap().shared_mut()
|
||||
else {
|
||||
panic!("Occurrence body");
|
||||
};
|
||||
*body = value;
|
||||
}
|
||||
fn set_steps(p: &mut Project, owner: &str, aliases: &[&str]) {
|
||||
*p.elements.get_mut(&id(owner)).unwrap().steps_mut().unwrap() =
|
||||
aliases.iter().map(|alias| id(alias)).collect();
|
||||
}
|
||||
fn sequence_link(
|
||||
p: &mut Project,
|
||||
alias: &str,
|
||||
name: &str,
|
||||
source: &str,
|
||||
target: &str,
|
||||
) -> Result<()> {
|
||||
let old = p.relate(RelationKind::SequenceLink, name, id(source), id(target));
|
||||
reidentify(p, old, id(alias))?;
|
||||
let relation = p.relations.get_mut(&id(alias)).unwrap();
|
||||
relation.alias = alias.into();
|
||||
relation.description = format!("Preserve link {alias} exactly.");
|
||||
if let RelationBody::SequenceLink { guard, .. } = &mut relation.body {
|
||||
*guard = "ready".into();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn fixture() -> Result<Project> {
|
||||
let mut p = Project::empty();
|
||||
p.id = id("project");
|
||||
p.alias = "process_multiselect_native".into();
|
||||
p.name = "Process group native".into();
|
||||
p.description = "Select Card C then Shift-select Card A; drop after Card B. Other columns and reusable engineering records remain exact.".into();
|
||||
for (layer, prefix, owner, name, owner_kind, work_kind) in [
|
||||
(
|
||||
Layer::Oa,
|
||||
"oa",
|
||||
"oa_process",
|
||||
"01 OA group order",
|
||||
Kind::Process,
|
||||
Kind::Activity,
|
||||
),
|
||||
(
|
||||
Layer::Sa,
|
||||
"sa",
|
||||
"sa_chain",
|
||||
"SA chain - preserve",
|
||||
Kind::FunctionalChain,
|
||||
Kind::Function,
|
||||
),
|
||||
] {
|
||||
element(&mut p, owner, name, owner_kind, layer, None);
|
||||
let shared = format!("{prefix}_shared_work");
|
||||
let downstream = format!("{prefix}_downstream_work");
|
||||
element(
|
||||
&mut p,
|
||||
&shared,
|
||||
"Shared request work",
|
||||
work_kind,
|
||||
layer,
|
||||
None,
|
||||
);
|
||||
element(
|
||||
&mut p,
|
||||
&downstream,
|
||||
"Downstream verification",
|
||||
work_kind,
|
||||
layer,
|
||||
None,
|
||||
);
|
||||
for (suffix, label) in [
|
||||
("a", "Card A"),
|
||||
("u", "Downstream U"),
|
||||
("b", "Card B"),
|
||||
("v", "Downstream V"),
|
||||
("c", "Card C"),
|
||||
] {
|
||||
let body = if suffix == "b" {
|
||||
OccurrenceBody::Control {
|
||||
kind: ControlKind::Choice,
|
||||
}
|
||||
} else {
|
||||
OccurrenceBody::Action {
|
||||
action: Some(id(if suffix == "a" || suffix == "c" {
|
||||
&shared
|
||||
} else {
|
||||
&downstream
|
||||
})),
|
||||
lifeline: None,
|
||||
}
|
||||
};
|
||||
occurrence(
|
||||
&mut p,
|
||||
&format!("{prefix}_{suffix}"),
|
||||
label,
|
||||
layer,
|
||||
owner,
|
||||
body,
|
||||
);
|
||||
}
|
||||
let order: Vec<_> = ["a", "u", "b", "v", "c"]
|
||||
.map(|suffix| format!("{prefix}_{suffix}"))
|
||||
.into_iter()
|
||||
.collect();
|
||||
set_steps(
|
||||
&mut p,
|
||||
owner,
|
||||
&order.iter().map(String::as_str).collect::<Vec<_>>(),
|
||||
);
|
||||
sequence_link(
|
||||
&mut p,
|
||||
&format!("{prefix}_a_u"),
|
||||
"A to U",
|
||||
&format!("{prefix}_a"),
|
||||
&format!("{prefix}_u"),
|
||||
)?;
|
||||
sequence_link(
|
||||
&mut p,
|
||||
&format!("{prefix}_b_v"),
|
||||
"B to V",
|
||||
&format!("{prefix}_b"),
|
||||
&format!("{prefix}_v"),
|
||||
)?;
|
||||
}
|
||||
element(
|
||||
&mut p,
|
||||
"reusable",
|
||||
"Reusable request detail",
|
||||
Kind::Process,
|
||||
Layer::Oa,
|
||||
None,
|
||||
);
|
||||
occurrence(
|
||||
&mut p,
|
||||
"reused_leaf",
|
||||
"Shared detail",
|
||||
Layer::Oa,
|
||||
"reusable",
|
||||
OccurrenceBody::Action {
|
||||
action: Some(id("oa_shared_work")),
|
||||
lifeline: None,
|
||||
},
|
||||
);
|
||||
set_steps(&mut p, "reusable", &["reused_leaf"]);
|
||||
element(
|
||||
&mut p,
|
||||
"reuse_trial",
|
||||
"02 Read-only preview trial",
|
||||
Kind::Process,
|
||||
Layer::Oa,
|
||||
None,
|
||||
);
|
||||
for (alias, name) in [
|
||||
("reference_one", "First use"),
|
||||
("reference_two", "Second use"),
|
||||
] {
|
||||
occurrence(
|
||||
&mut p,
|
||||
alias,
|
||||
name,
|
||||
Layer::Oa,
|
||||
"reuse_trial",
|
||||
OccurrenceBody::SequenceReference {
|
||||
sequence: Some(id("reusable")),
|
||||
},
|
||||
);
|
||||
}
|
||||
set_steps(&mut p, "reuse_trial", &["reference_one", "reference_two"]);
|
||||
p.traces.insert(
|
||||
id("trace"),
|
||||
Trace {
|
||||
id: id("trace"),
|
||||
alias: "preserved_work_refinement".into(),
|
||||
name: "Shared request refinement".into(),
|
||||
description: "Process arrangement must preserve cross-layer correspondence.".into(),
|
||||
oa: id("oa_shared_work"),
|
||||
sa: id("sa_shared_work"),
|
||||
kind: TraceKind::Refinement,
|
||||
baseline: Some(TraceBaseline {
|
||||
oa: p.properties(id("oa_shared_work")).unwrap(),
|
||||
sa: p.properties(id("sa_shared_work")).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())?;
|
||||
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 expected = serde_json::to_value(expected)?;
|
||||
let actual = serde_json::to_value(actual)?;
|
||||
let fields: Vec<_> = expected
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.keys()
|
||||
.filter(|key| expected[*key] != actual[*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 ids: std::collections::BTreeMap<_, _> = [
|
||||
"project",
|
||||
"oa_process",
|
||||
"oa_a",
|
||||
"oa_u",
|
||||
"oa_b",
|
||||
"oa_v",
|
||||
"oa_c",
|
||||
"oa_a_u",
|
||||
"oa_b_v",
|
||||
"oa_shared_work",
|
||||
"oa_downstream_work",
|
||||
"sa_chain",
|
||||
"sa_shared_work",
|
||||
"sa_downstream_work",
|
||||
"sa_a",
|
||||
"sa_u",
|
||||
"sa_b",
|
||||
"sa_v",
|
||||
"sa_c",
|
||||
"sa_a_u",
|
||||
"sa_b_v",
|
||||
"trace",
|
||||
"reusable",
|
||||
"reused_leaf",
|
||||
"reuse_trial",
|
||||
"reference_one",
|
||||
"reference_two",
|
||||
]
|
||||
.into_iter()
|
||||
.map(|name| (name, id(name)))
|
||||
.collect();
|
||||
write_new(
|
||||
&out.join("manifest.json"),
|
||||
&serde_json::to_vec_pretty(&serde_json::json!({
|
||||
"fixture": "process-multiselect-native-20260923", "ids": ids, "source_entry": bundle.entry,
|
||||
"original_oa_steps": ["oa_a", "oa_u", "oa_b", "oa_v", "oa_c"],
|
||||
"reordered_oa_steps": ["oa_b", "oa_u", "oa_a", "oa_v", "oa_c"],
|
||||
"gesture": "Select Card C then add Card A; Inspect Card C without changing membership, then drag Card C to After Card B",
|
||||
"fixed_slots": {"1": "oa_u", "3": "oa_v"},
|
||||
"read_only_trial": "02 Read-only preview trial has two qualified tiles of reused_leaf through distinct references",
|
||||
"receipt": "Pin baseline.json, manifest.json and generation command externally before interaction. Never replace baseline with a later saved snapshot.",
|
||||
"evaluation": "Literal expected OA steps and complete Project equality; all SA/reuse/trace records exact",
|
||||
"limits": "No pointer, focus, selection-paint, history or source-byte acceptance claim",
|
||||
}))?,
|
||||
)?;
|
||||
equal(&p, &load(&out.join("baseline.json"))?)?;
|
||||
equal(&p, &load(&out.join("source").join(&bundle.entry))?)?;
|
||||
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)?;
|
||||
// Pin the exact deterministic original, not merely a reusable project UUID.
|
||||
equal(&fixture()?, &before)
|
||||
.map_err(|e| format!("Baseline is not the immutable original: {e}"))?;
|
||||
let mut expected = before;
|
||||
match mode {
|
||||
"unchanged" => {}
|
||||
"reordered" => set_steps(
|
||||
&mut expected,
|
||||
"oa_process",
|
||||
&["oa_b", "oa_u", "oa_a", "oa_v", "oa_c"],
|
||||
),
|
||||
_ => return Err("Mode must be reordered 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; literal expected order",
|
||||
"limits": "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: process_multiselect_native generate DIRECTORY | check reordered|unchanged BASELINE SAVED".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
fn reordered(before: &Project) -> Project {
|
||||
let mut session = Session::new(before.clone());
|
||||
session
|
||||
.execute(Command::SetSteps(
|
||||
id("oa_process"),
|
||||
["oa_b", "oa_u", "oa_a", "oa_v", "oa_c"].map(id).to_vec(),
|
||||
))
|
||||
.unwrap();
|
||||
session.project().clone()
|
||||
}
|
||||
fn through_json(mode: &str, baseline: &Project, actual: &Project) -> Result<()> {
|
||||
validate_candidate(baseline).expect("Baseline must be valid");
|
||||
validate_candidate(actual).expect("Corruption must remain semantically valid");
|
||||
let dir = tempfile::tempdir()?;
|
||||
let before = dir.path().join("before.json");
|
||||
let saved = dir.path().join("saved.json");
|
||||
persistence::save(&before, baseline)?;
|
||||
persistence::save(&saved, actual)?;
|
||||
check(mode, &before, &saved)
|
||||
}
|
||||
fn mismatch(before: &Project, actual: &Project) {
|
||||
let error = through_json("reordered", before, actual)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(
|
||||
error.contains("Whole Project mismatch"),
|
||||
"Unexpected rejection: {error}"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn generate_is_deterministic_create_only_and_checks_real_source() {
|
||||
equal(&fixture().unwrap(), &fixture().unwrap()).unwrap();
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let out = dir.path().join("fixture");
|
||||
generate(&out).unwrap();
|
||||
let baseline = out.join("baseline.json");
|
||||
let bytes = fs::read(&baseline).unwrap();
|
||||
check("unchanged", &baseline, &out.join("source/project.arcadia")).unwrap();
|
||||
assert!(generate(&out).is_err());
|
||||
assert_eq!(fs::read(&baseline).unwrap(), bytes);
|
||||
let invalid = dir.path().join("invalid.arcadia");
|
||||
fs::write(&invalid, "project {").unwrap();
|
||||
assert!(load(&invalid).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn real_set_steps_passes_but_unchanged_and_mutated_baseline_do_not() {
|
||||
let before = fixture().unwrap();
|
||||
let actual = reordered(&before);
|
||||
through_json("reordered", &before, &actual).unwrap();
|
||||
through_json("unchanged", &before, &before).unwrap();
|
||||
mismatch(&before, &before);
|
||||
assert!(through_json("unchanged", &before, &actual).is_err());
|
||||
let error = through_json("unchanged", &actual, &actual)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(error.contains("immutable original"), "{error}");
|
||||
}
|
||||
#[test]
|
||||
fn global_splice_reversed_block_and_singleton_c_results_are_rejected() {
|
||||
let before = fixture().unwrap();
|
||||
// Native primary/pressed C is already after B, so a singleton C move
|
||||
// is unchanged. Group {C,A} instead puts B before both selected cards.
|
||||
// If A were primary, its singleton result could equal the group result.
|
||||
mismatch(&before, &before);
|
||||
for order in [
|
||||
["oa_u", "oa_b", "oa_a", "oa_c", "oa_v"],
|
||||
["oa_b", "oa_u", "oa_c", "oa_v", "oa_a"],
|
||||
["oa_a", "oa_u", "oa_b", "oa_c", "oa_v"],
|
||||
] {
|
||||
let mut actual = before.clone();
|
||||
set_steps(&mut actual, "oa_process", &order);
|
||||
mismatch(&before, &actual);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn semantic_link_work_sa_reuse_and_trace_changes_are_rejected() {
|
||||
let before = fixture().unwrap();
|
||||
let correct = reordered(&before);
|
||||
let mutations: [fn(&mut Project); 7] = [
|
||||
|p| {
|
||||
if let RelationBody::SequenceLink { guard, .. } =
|
||||
&mut p.relations.get_mut(&id("oa_a_u")).unwrap().body
|
||||
{
|
||||
*guard = "changed".into();
|
||||
}
|
||||
},
|
||||
|p| {
|
||||
if let RelationBody::SequenceLink { target, .. } =
|
||||
&mut p.relations.get_mut(&id("oa_a_u")).unwrap().body
|
||||
{
|
||||
*target = id("oa_v");
|
||||
}
|
||||
},
|
||||
|p| {
|
||||
p.elements
|
||||
.get_mut(&id("oa_shared_work"))
|
||||
.unwrap()
|
||||
.name
|
||||
.push_str(" drift");
|
||||
},
|
||||
|p| {
|
||||
p.elements
|
||||
.get_mut(&id("sa_shared_work"))
|
||||
.unwrap()
|
||||
.description
|
||||
.push_str(" drift");
|
||||
},
|
||||
|p| {
|
||||
set_steps(p, "sa_chain", &["sa_b", "sa_u", "sa_a", "sa_v", "sa_c"]);
|
||||
},
|
||||
|p| {
|
||||
p.elements
|
||||
.get_mut(&id("reused_leaf"))
|
||||
.unwrap()
|
||||
.description
|
||||
.push_str(" drift");
|
||||
},
|
||||
|p| {
|
||||
p.traces.get_mut(&id("trace")).unwrap().baseline = None;
|
||||
},
|
||||
];
|
||||
for mutate in mutations {
|
||||
let mut bad = correct.clone();
|
||||
mutate(&mut bad);
|
||||
mismatch(&before, &bad);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn changed_source_roundtrip_preserves_both_layers_and_checker_rejects_source_drift() {
|
||||
let before = fixture().unwrap();
|
||||
let actual = reordered(&before);
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let baseline = dir.path().join("baseline.json");
|
||||
persistence::save(&baseline, &before).unwrap();
|
||||
for corrupt in [false, true] {
|
||||
let mut saved = actual.clone();
|
||||
if corrupt {
|
||||
saved
|
||||
.elements
|
||||
.get_mut(&id("sa_c"))
|
||||
.unwrap()
|
||||
.name
|
||||
.push_str(" drift");
|
||||
}
|
||||
let bundle = roundtrip(&saved).unwrap();
|
||||
let folder = dir.path().join(if corrupt { "corrupt" } else { "saved" });
|
||||
for (path, text) in &bundle.files {
|
||||
write_new(&folder.join(path), text.as_bytes()).unwrap();
|
||||
}
|
||||
let parsed = load(&folder.join(&bundle.entry)).unwrap();
|
||||
assert_eq!(parsed.elements[&id("oa_process")].kind(), Kind::Process);
|
||||
assert_eq!(
|
||||
parsed.elements[&id("sa_chain")].kind(),
|
||||
Kind::FunctionalChain
|
||||
);
|
||||
let result = check("reordered", &baseline, &folder.join(&bundle.entry));
|
||||
if corrupt {
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Whole Project mismatch")
|
||||
);
|
||||
} else {
|
||||
result.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user