Files
ARCADIA-Studio/crates/oa-desktop/src/allocation_drag/tests.rs
T

1661 lines
59 KiB
Rust

use super::*;
use crate::document::Document;
use oa_core::source_project::{ProjectLimits, compile_bundle, format_bundle};
struct Fixture {
p: Project,
d: Id,
second: Id,
a: Id,
b: Id,
w: Id,
n: Id,
}
fn xy(x: f32, y: f32) -> Position {
Position { x, y }
}
fn fixture(layer: Layer) -> Fixture {
let mut p = Project::empty();
let (a_kind, b_kind, work) = match layer {
Layer::Oa => (Kind::Entity, Kind::Actor, Kind::Activity),
Layer::Sa => (Kind::System, Kind::SystemActor, Kind::Function),
};
let a = p.add_element(a_kind, layer, "A", None).unwrap();
let b = p.add_element(b_kind, layer, "B", None).unwrap();
let w = p.add_element(work, layer, "Work", None).unwrap();
let d = p.ensure_diagram(layer);
p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Allocation;
p.place(d, a, xy(100., 100.));
p.place(d, b, xy(900., 100.));
let n = p.place(d, w, xy(100., 500.));
let second = p.ensure_diagram(layer);
p.place(second, a, xy(20., 30.));
p.place(second, w, xy(500., 900.));
validate_candidate(&p).unwrap();
Fixture {
p,
d,
second,
a,
b,
w,
n,
}
}
fn scope(f: &Fixture) -> Scope {
capture(&f.p, f.d, f.w, f.n).unwrap()
}
fn presentation(p: &Project, d: Id, target: Option<Id>) -> PresentationContext {
let target = target.map(|element| {
let view = projection::project_view(p, d);
let node = view.nodes.iter().find(|n| n.element == element).unwrap();
TargetPresentation { placement: node.id }
});
PresentationContext {
viewport: Rect {
position: xy(-100., -100.),
size: Size {
width: 3000.,
height: 2400.,
},
},
zoom: 1.,
target,
}
}
fn allocation(f: &mut Fixture, performer: Id) -> Id {
f.p.relate(
RelationKind::Allocation,
"Original responsibility",
performer,
f.w,
)
}
fn semantic(f: &Fixture, destination: Destination) -> SemanticPlan {
plan_semantics(
&f.p,
&scope(f),
destination,
&BTreeMap::from([(f.w, Id::new_v4())]),
)
.unwrap()
}
fn command(f: &Fixture, destination: Destination, requested: Position) -> Command {
let plan = semantic(f, destination);
let target = match destination {
Destination::Performer(id) => Some(id),
_ => None,
};
let placed = place(&plan, requested, presentation(&f.p, f.d, target)).unwrap();
finish(&plan, &f.p, &placed).unwrap().unwrap()
}
fn roundtrip(p: &Project, command: Command) -> Project {
let mut bundle = format_bundle(p);
bundle
.files
.get_mut(&bundle.entry.clone())
.unwrap()
.insert_str(
0,
"// Allocation review rationale: preserve this source exactly.\n",
);
let mut document = Document::from_bundle(bundle, ProjectLimits::default()).unwrap();
let before_source = document.bundle().clone();
let expected = prepare_command(p, &command).unwrap().project;
document.execute(command).unwrap();
assert_eq!(document.project(), &expected);
assert_eq!(document.revision(), 1);
let after_source = document.bundle().clone();
assert!(after_source.files[&after_source.entry].starts_with("// Allocation review rationale:"));
assert_eq!(
compile_bundle(&after_source, ProjectLimits::default())
.unwrap()
.decoded
.project,
expected
);
let reopened = Document::from_bundle(after_source.clone(), ProjectLimits::default()).unwrap();
assert_eq!(reopened.project(), &expected);
assert!(document.undo());
assert_eq!(document.project(), p);
assert_eq!(document.bundle(), &before_source);
assert!(!document.can_undo(), "one drag is exactly one history step");
assert!(document.redo());
assert_eq!(document.project(), &expected);
assert_eq!(document.bundle(), &after_source);
assert!(!document.can_redo());
expected
}
#[test]
fn oa_sa_incoming_between_outgoing_preserve_identity_source_history_and_other_views() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = fixture(layer);
let elements = f.p.elements.clone();
let other = f.p.diagrams[&f.second].clone();
let stable = Id::new_v4();
let plan = plan_semantics(
&f.p,
&scope(&f),
Destination::Performer(f.a),
&BTreeMap::from([(f.w, stable)]),
)
.unwrap();
let same_target_again = plan_semantics(
&f.p,
&scope(&f),
Destination::Performer(f.a),
&BTreeMap::from([(f.w, stable)]),
)
.unwrap();
assert_eq!(plan.project(), same_target_again.project());
let placed = place(&plan, xy(151., 203.), presentation(&f.p, f.d, Some(f.a))).unwrap();
assert_eq!(placed.delta, xy(50., -300.));
let batch = finish(&plan, &f.p, &placed).unwrap().unwrap();
f.p = roundtrip(&f.p, batch);
assert_eq!(
f.p.relations[&stable].body,
RelationBody::Allocation {
source: f.a,
target: f.w
}
);
let record = f.p.relations.get_mut(&stable).unwrap();
record.name = "Reviewed responsibility".into();
record.description = "Retain engineering rationale and relation identity".into();
let original = record.clone();
let batch = command(&f, Destination::Performer(f.b), xy(950., 200.));
f.p = roundtrip(&f.p, batch);
let mut expected = original;
expected.body = RelationBody::Allocation {
source: f.b,
target: f.w,
};
assert_eq!(f.p.relations[&stable], expected);
let batch = command(&f, Destination::OutsidePerformers, xy(100., 500.));
f.p = roundtrip(&f.p, batch);
assert!(!f.p.relations.contains_key(&stable));
assert_eq!(f.p.elements, elements);
assert_eq!(f.p.diagrams[&f.second], other);
assert_eq!(f.p.diagrams[&f.d].nodes[&f.n].position, xy(100., 500.));
}
}
#[test]
fn snapped_noop_returns_no_command_and_keeps_source_revision_and_redo() {
let f = fixture(Layer::Oa);
let plan = semantic(&f, Destination::OutsidePerformers);
let placed = place(&plan, xy(103., 498.), presentation(&f.p, f.d, None)).unwrap();
assert_eq!(placed.delta, xy(0., 0.));
assert!(finish(&plan, &f.p, &placed).unwrap().is_none());
let mut document = Document::from_project(f.p.clone(), ProjectLimits::default()).unwrap();
document
.execute(Command::Rename(f.w, "Changed work".into()))
.unwrap();
document.undo();
let source = document.bundle().clone();
let revision = document.revision();
if let Some(command) = finish(&plan, document.project(), &placed).unwrap() {
document.execute(command).unwrap();
}
assert_eq!(document.bundle(), &source);
assert_eq!(document.revision(), revision);
assert!(document.can_redo());
}
fn role(f: &mut Fixture, performer: Option<Id>) -> (Id, Id) {
let role =
f.p.add_element(Kind::Role, Layer::Oa, "Operational responsibility", None)
.unwrap();
let assignment =
f.p.relate(RelationKind::RoleAssignment, "Role assignment", role, f.w);
if let Some(performer) = performer {
f.p.relate(
RelationKind::RoleRealization,
"Role realization",
performer,
role,
);
}
(role, assignment)
}
#[test]
fn role_only_same_effective_performer_is_semantic_noop_and_outgoing_refuses() {
let mut f = fixture(Layer::Oa);
let a = f.a;
let (role, _) = role(&mut f, Some(a));
f.p.diagrams
.get_mut(&f.d)
.unwrap()
.nodes
.get_mut(&f.n)
.unwrap()
.position = xy(150., 200.);
let s = scope(&f);
assert_eq!(s.uniform, Uniform::Assigned(a));
assert!(s.leaves[&f.w].direct.is_none());
let plan = plan_semantics(&f.p, &s, Destination::Performer(a), &BTreeMap::new()).unwrap();
assert_eq!(plan.project(), &f.p);
assert!(plan.commands.is_empty());
let placed = place(&plan, xy(160., 210.), presentation(&f.p, f.d, Some(a))).unwrap();
let after = roundtrip(&f.p, finish(&plan, &f.p, &placed).unwrap().unwrap());
assert_eq!(after.relations, f.p.relations);
assert!(
!after
.relations
.values()
.any(|r| r.kind() == RelationKind::Allocation)
);
let before = f.p.clone();
let Refusal::RoleExitRequired {
roles,
performers,
message,
..
} = plan_semantics(&f.p, &s, Destination::OutsidePerformers, &BTreeMap::new()).unwrap_err()
else {
panic!("role exit must refuse")
};
assert_eq!(roles, vec![role]);
assert_eq!(performers, vec![a]);
assert!(message.contains("Operational responsibility"));
assert_eq!(f.p, before);
}
#[test]
fn direct_exit_revealing_another_role_performer_has_truthful_refusal() {
let mut f = fixture(Layer::Oa);
let (a, b) = (f.a, f.b);
allocation(&mut f, a);
role(&mut f, Some(b));
let Refusal::RoleExitRequired {
message,
performers,
..
} = plan_semantics(
&f.p,
&scope(&f),
Destination::OutsidePerformers,
&BTreeMap::new(),
)
.unwrap_err()
else {
panic!()
};
assert_eq!(performers, vec![b]);
assert!(message.contains("would place Work in B through its role"));
}
#[test]
fn unresolved_role_leaf_can_arrange_or_accept_explicit_target_without_guessing() {
let mut f = fixture(Layer::Oa);
let (role_id, assignment) = role(&mut f, None);
let s = scope(&f);
assert_eq!(s.uniform, Uniform::MixedOrUnresolved);
let arranged = command(&f, Destination::LayoutOnly, xy(110., 500.));
assert_eq!(roundtrip(&f.p, arranged).relations, f.p.relations);
let plan = semantic(&f, Destination::Performer(f.b));
assert!(
plan.label
.contains("explicit assignment; role responsibility retained")
);
assert_eq!(
plan.project().relations[&assignment],
f.p.relations[&assignment]
);
assert_eq!(
validation::effective_performer(plan.project(), f.w),
Some(f.b)
);
// Ambiguous realization is also a valid warning-only baseline, and B is an
// explicit user destination even when the responsibility names A and B.
f.p.relate(RelationKind::RoleRealization, "A role", f.a, role_id);
f.p.relate(RelationKind::RoleRealization, "B role", f.b, role_id);
assert_eq!(scope(&f).uniform, Uniform::MixedOrUnresolved);
assert_eq!(
validation::effective_performer(semantic(&f, Destination::Performer(f.b)).project(), f.w),
Some(f.b)
);
assert!(matches!(
plan_semantics(
&f.p,
&scope(&f),
Destination::OutsidePerformers,
&BTreeMap::new()
),
Err(Refusal::RoleExitRequired { .. })
));
}
#[test]
fn explicit_different_performer_retains_advisory_role_warning() {
let mut f = fixture(Layer::Oa);
let a = f.a;
role(&mut f, Some(a));
let plan = semantic(&f, Destination::Performer(f.b));
assert!(
plan.warnings
.iter()
.any(|i| i.code == "role_allocation_conflict" && i.severity == Severity::Warning)
);
let placed = place(&plan, xy(950., 200.), presentation(&f.p, f.d, Some(f.b))).unwrap();
roundtrip(&f.p, finish(&plan, &f.p, &placed).unwrap().unwrap());
}
fn subtree(layer: Layer) -> (Fixture, Vec<Id>, Id, Id) {
let mut f = fixture(layer);
let kind = f.p.elements[&f.w].kind();
let x =
f.p.add_element(kind, layer, "First child", Some(f.w))
.unwrap();
let grand = f.p.add_element(kind, layer, "Grandchild", Some(x)).unwrap();
let y =
f.p.add_element(kind, layer, "Second child", Some(f.w))
.unwrap();
let unplaced =
f.p.add_element(kind, layer, "Unplaced leaf", Some(f.w))
.unwrap();
f.p.diagrams
.get_mut(&f.d)
.unwrap()
.nodes
.get_mut(&f.n)
.unwrap()
.position = xy(140., 200.);
let xn = f.p.place(f.d, x, xy(180., 300.));
f.p.place(f.d, grand, xy(220., 400.));
f.p.place(f.d, y, xy(180., 550.));
for work in [grand, y, unplaced] {
f.p.relate(RelationKind::Allocation, "Leaf responsibility", f.a, work);
}
f.p.diagrams
.get_mut(&f.d)
.unwrap()
.nodes
.get_mut(&f.n)
.unwrap()
.collapsed = true;
f.p.diagrams
.get_mut(&f.d)
.unwrap()
.nodes
.get_mut(&xn)
.unwrap()
.collapsed = true;
(f, vec![grand, y, unplaced], x, unplaced)
}
#[test]
fn uniform_folded_grandchild_and_unplaced_scope_moves_exactly_q_and_all_l_oa_sa() {
for layer in [Layer::Oa, Layer::Sa] {
let (mut f, leaves, _x, unplaced) = subtree(layer);
let edge_kind = if layer == Layer::Oa {
RelationKind::Interaction
} else {
RelationKind::FunctionalExchange
};
let internal = f.p.relate(edge_kind, "Internal flow", leaves[0], leaves[1]);
if let RelationBody::FunctionalExchange { source_port, .. } = f.p.relations[&internal].body
{
let node =
f.p.diagrams
.get_mut(&f.d)
.unwrap()
.nodes
.values_mut()
.find(|n| n.element == leaves[0])
.unwrap();
node.port_anchors.push(PortAnchor {
port: source_port,
side: PortAnchorSide::Bottom,
offset: 0.4,
});
}
let external =
f.p.add_element(f.p.elements[&f.w].kind(), layer, "External work", None)
.unwrap();
f.p.place(f.d, external, xy(1800., 800.));
let one_ended = f.p.relate(edge_kind, "External flow", leaves[1], external);
for diagram in [f.d, f.second] {
f.p.diagrams.get_mut(&diagram).unwrap().routes =
vec![route(internal), route(one_ended)];
}
let s = scope(&f);
assert_eq!(s.work_records.len(), 5);
assert_eq!(
s.leaves.keys().copied().collect::<BTreeSet<_>>(),
leaves.iter().copied().collect()
);
assert_eq!(s.placements.len(), 4);
assert!(!s.placements.values().any(|n| n.element == unplaced));
assert_eq!(s.uniform, Uniform::Assigned(f.a));
let plan = plan_semantics(&f.p, &s, Destination::Performer(f.b), &BTreeMap::new()).unwrap();
let placed = place(&plan, xy(940., 200.), presentation(&f.p, f.d, Some(f.b))).unwrap();
assert_eq!(placed.delta, xy(800., 0.));
for (id, before) in &s.placements {
let mut expected = before.clone();
expected.position.x += 800.;
assert_eq!(placed.diagram.nodes[id], expected);
}
assert!(placed.view.nodes.iter().all(|n| n.element != unplaced));
assert!(
placed
.view
.nodes
.iter()
.all(|n| !leaves.contains(&n.element)),
"fold state retained"
);
let mut translated = route(internal);
for p in &mut translated.waypoints {
p.x += 800.;
}
assert_eq!(placed.diagram.routes, vec![translated, route(one_ended)]);
let after = roundtrip(&f.p, finish(&plan, &f.p, &placed).unwrap().unwrap());
assert_eq!(after.diagrams[&f.second], f.p.diagrams[&f.second]);
assert_eq!(
after.elements, f.p.elements,
"decomposition, ports and all element metadata remain exact"
);
assert_eq!(after.relations.len(), f.p.relations.len());
for relation in f.p.relations.values() {
let mut expected = relation.clone();
if relation.kind() == RelationKind::Allocation && leaves.contains(&relation.target()) {
expected.body = RelationBody::Allocation {
source: f.b,
target: relation.target(),
};
}
assert_eq!(after.relations[&relation.id], expected);
}
}
}
#[test]
fn uniform_role_only_and_mixed_direct_role_composites_same_target_do_not_allocate() {
for mixed_direct in [false, true] {
let (mut f, leaves, _, _) = subtree(Layer::Oa);
f.p.relations.clear();
let role =
f.p.add_element(Kind::Role, Layer::Oa, "Team responsibility", None)
.unwrap();
f.p.relate(RelationKind::RoleRealization, "A realizes role", f.a, role);
for (index, work) in leaves.into_iter().enumerate() {
f.p.relate(RelationKind::RoleAssignment, "Leaf role", role, work);
if mixed_direct && index == 0 {
f.p.relate(RelationKind::Allocation, "Explicit allocation", f.a, work);
}
}
let s = scope(&f);
assert_eq!(s.uniform, Uniform::Assigned(f.a));
let plan = plan_semantics(&f.p, &s, Destination::Performer(f.a), &BTreeMap::new()).unwrap();
assert_eq!(plan.project(), &f.p);
let placed = place(&plan, xy(150., 210.), presentation(&f.p, f.d, Some(f.a))).unwrap();
let after = roundtrip(&f.p, finish(&plan, &f.p, &placed).unwrap().unwrap());
assert_eq!(after.relations, f.p.relations);
let ids: BTreeMap<_, _> = s.leaves.keys().map(|id| (*id, Id::new_v4())).collect();
let reassign = plan_semantics(&f.p, &s, Destination::Performer(f.b), &ids).unwrap();
for (work, leaf) in &s.leaves {
let id = leaf.direct.as_ref().map_or(ids[work], |r| r.id);
assert_eq!(
reassign.project().relations[&id].body,
RelationBody::Allocation {
source: f.b,
target: *work
}
);
if let Some(original) = &leaf.direct {
let mut expected = original.clone();
expected.body = RelationBody::Allocation {
source: f.b,
target: *work,
};
assert_eq!(reassign.project().relations[&id], expected);
}
}
for original in
f.p.relations
.values()
.filter(|r| r.kind() != RelationKind::Allocation)
{
assert_eq!(reassign.project().relations[&original.id], *original);
}
assert!(
!reassign
.project()
.relations
.values()
.any(|r| r.kind() == RelationKind::Allocation && r.target() == f.w)
);
let placed = place(
&reassign,
xy(940., 200.),
presentation(&f.p, f.d, Some(f.b)),
)
.unwrap();
roundtrip(&f.p, finish(&reassign, &f.p, &placed).unwrap().unwrap());
}
}
#[test]
fn uniformly_unassigned_composite_creates_only_stable_leaf_allocations_including_unplaced() {
for layer in [Layer::Oa, Layer::Sa] {
let (mut f, leaves, _, _) = subtree(layer);
f.p.relations.clear();
let s = scope(&f);
assert_eq!(s.uniform, Uniform::Unassigned);
let ids: BTreeMap<_, _> = leaves.iter().map(|id| (*id, Id::new_v4())).collect();
let plan = plan_semantics(&f.p, &s, Destination::Performer(f.b), &ids).unwrap();
assert_eq!(plan.project().relations.len(), leaves.len());
for work in &leaves {
assert_eq!(
plan.project().relations[&ids[work]].body,
RelationBody::Allocation {
source: f.b,
target: *work
}
);
}
assert_eq!(plan.project().elements, f.p.elements);
assert_eq!(
plan.project().diagrams,
f.p.diagrams,
"semantics planning invents no placements"
);
let placed = place(&plan, xy(940., 200.), presentation(&f.p, f.d, Some(f.b))).unwrap();
roundtrip(&f.p, finish(&plan, &f.p, &placed).unwrap().unwrap());
}
}
#[test]
fn source_budget_refusal_keeps_project_bundle_history_and_retry_identity_exact() {
let f = fixture(Layer::Oa);
let stable = Id::new_v4();
let plan = plan_semantics(
&f.p,
&scope(&f),
Destination::Performer(f.a),
&BTreeMap::from([(f.w, stable)]),
)
.unwrap();
let placed = place(&plan, xy(150., 200.), presentation(&f.p, f.d, Some(f.a))).unwrap();
let command = finish(&plan, &f.p, &placed).unwrap().unwrap();
let bundle = format_bundle(&f.p);
let size: usize = bundle.files.values().map(String::len).sum();
let mut limits = ProjectLimits::default();
limits.model.max_source_bytes = size;
let mut document = Document::from_bundle(bundle, limits).unwrap();
let source = document.bundle().clone();
assert!(matches!(
document.execute(command.clone()),
Err(crate::document::DocumentError::Source(_))
));
assert_eq!(document.project(), &f.p);
assert_eq!(document.bundle(), &source);
assert_eq!(document.revision(), 0);
assert!(!document.can_undo());
assert!(!document.can_redo());
// A retry through a document with sufficient admission budget uses the same
// stable relation identity; no incomplete semantic/layout result escaped.
let after = roundtrip(&f.p, command);
assert_eq!(after.relations[&stable].target(), f.w);
}
#[test]
fn mixed_p_x_y_refuses_semantic_and_arrange_instead_of_stretching_destination() {
for layer in [Layer::Oa, Layer::Sa] {
let (mut f, leaves, _, _) = subtree(layer);
let allocation =
f.p.relations
.values_mut()
.find(|r| r.kind() == RelationKind::Allocation && r.target() == leaves[1])
.unwrap();
allocation.body = RelationBody::Allocation {
source: f.b,
target: leaves[1],
};
let s = scope(&f);
assert_eq!(s.uniform, Uniform::MixedOrUnresolved);
for destination in [
Destination::Performer(f.a),
Destination::Performer(f.b),
Destination::OutsidePerformers,
Destination::LayoutOnly,
] {
assert!(matches!(
plan_semantics(&f.p, &s, destination, &BTreeMap::new()),
Err(Refusal::MixedLayoutRequired)
));
}
}
}
fn collateral(f: &Fixture) -> Vec<ChangedRecord> {
match plan_semantics(
&f.p,
&scope(f),
Destination::OutsidePerformers,
&BTreeMap::new(),
) {
Err(Refusal::CollateralChangeRequired(records)) => records,
other => panic!("expected exact diff refusal, got {other:?}"),
}
}
#[test]
fn surviving_constraint_mutation_is_refused_even_when_core_candidate_is_valid() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = fixture(layer);
let a = f.a;
let allocation = allocation(&mut f, a);
let constraint =
f.p.add_element(Kind::Constraint, layer, "Keep reviewed assignment", None)
.unwrap();
*f.p.elements
.get_mut(&constraint)
.unwrap()
.shared_mut()
.unwrap() = SharedBody::Constraint {
expression: "reviewed assignment".into(),
constrained: vec![allocation, f.w],
};
let actual = prepare_command(&f.p, &Command::Delete(allocation))
.unwrap()
.project;
validate_candidate(&actual).unwrap();
assert_eq!(
actual.elements.len(),
f.p.elements.len(),
"the counterexample loses no element identity"
);
assert_ne!(actual.elements[&constraint], f.p.elements[&constraint]);
let changes = collateral(&f);
assert!(changes.iter().any(|r| r.id == constraint
&& r.category == "element"
&& r.name == "Keep reviewed assignment"));
}
}
#[test]
fn allocation_artifact_and_manual_trace_cascades_are_named_and_refused() {
let mut f = fixture(Layer::Oa);
let a = f.a;
let allocation = allocation(&mut f, a);
let artifact = Id::new_v4();
f.p.artifact_links.insert(
artifact,
ArtifactLink {
id: artifact,
alias: "assignment_evidence".into(),
name: "Assignment evidence".into(),
description: "Reviewed".into(),
owner: allocation,
relation: ArtifactRelation::Verifies,
uri: "evidence/assignment.pdf".into(),
revision: Some("r1".into()),
fragment: Some("section-2".into()),
},
);
let sa =
f.p.add_element(Kind::Function, Layer::Sa, "Counterpart", None)
.unwrap();
let trace = Id::new_v4();
f.p.traces.insert(
trace,
Trace {
id: trace,
alias: "assignment_trace".into(),
name: "Assignment trace".into(),
description: "Reviewed trace".into(),
oa: allocation,
sa,
kind: TraceKind::Manual,
baseline: Some(TraceBaseline {
oa: f.p.properties(allocation).unwrap(),
sa: f.p.properties(sa).unwrap(),
semantic: None,
}),
},
);
validate_candidate(&f.p).unwrap();
let changes = collateral(&f);
assert!(
changes
.iter()
.any(|r| r.id == artifact && r.category == "artifact")
);
assert!(
changes
.iter()
.any(|r| r.id == trace && r.category == "trace")
);
// Source retargeting retains the reviewed relation identity, so the same
// evidence remains valid and byte-identical rather than being cascade-deleted.
let plan = semantic(&f, Destination::Performer(f.b));
assert_eq!(plan.project().artifact_links, f.p.artifact_links);
assert_eq!(plan.project().traces, f.p.traces);
}
fn route(edge: Id) -> EdgeRoute {
EdgeRoute {
edge,
waypoints: vec![xy(420., 400.), xy(420., 550.)],
label: Some(EdgeLabel {
fraction: 0.4,
offset: xy(3., -12.),
}),
}
}
#[test]
fn own_deleted_allocation_routes_may_disappear_in_every_diagram_only() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = fixture(layer);
let a = f.a;
let allocation = allocation(&mut f, a);
for d in [f.d, f.second] {
f.p.diagrams
.get_mut(&d)
.unwrap()
.routes
.push(route(allocation));
}
let cmd = command(&f, Destination::OutsidePerformers, xy(110., 500.));
let after = roundtrip(&f.p, cmd);
for d in [f.d, f.second] {
let mut expected = f.p.diagrams[&d].clone();
expected.routes.clear();
if d == f.d {
expected.nodes.get_mut(&f.n).unwrap().position = xy(110., 500.);
}
assert_eq!(after.diagrams[&d], expected);
}
}
}
#[test]
fn unrequested_view_option_cleanup_is_not_whitelisted() {
let mut f = fixture(Layer::Oa);
let a = f.a;
let allocation = allocation(&mut f, a);
f.p.diagrams
.get_mut(&f.second)
.unwrap()
.view_options
.hidden_edges
.push(allocation);
let changes = collateral(&f);
assert!(
changes
.iter()
.any(|r| r.id == f.second && r.category == "diagram")
);
}
#[test]
fn sa_port_and_deferred_exchange_allocation_cascade_refuses_and_retarget_is_invalid() {
let mut f = fixture(Layer::Sa);
let a = f.a;
let work_allocation = allocation(&mut f, a);
let peer =
f.p.add_element(Kind::Function, Layer::Sa, "Receive", None)
.unwrap();
f.p.relate(RelationKind::Allocation, "Receive allocation", f.b, peer);
let exchange =
f.p.relate(RelationKind::FunctionalExchange, "Flow", f.w, peer);
let (fp1, fp2) = match f.p.relations[&exchange].body {
RelationBody::FunctionalExchange {
source_port,
target_port,
..
} => (source_port, target_port),
_ => panic!(),
};
let component = |p: &mut Project, owner: Id, orientation: ComponentPortOrientation| {
let id = Id::new_v4();
let port = make_port(
p,
id,
owner,
"Boundary",
PortContract::Component {
kind: ComponentPortKind::Flow,
orientation,
},
)
.unwrap();
p.elements.insert(id, port);
id
};
let out = component(&mut f.p, f.a, ComponentPortOrientation::Out);
let input = component(&mut f.p, f.b, ComponentPortOrientation::In);
let boundary =
f.p.relate(RelationKind::BoundaryExchange, "Boundary flow", out, input);
let port_allocation =
f.p.relate(RelationKind::PortAllocation, "Output mapping", out, fp1);
f.p.relate(RelationKind::PortAllocation, "Input mapping", input, fp2);
let carried = f.p.relate(
RelationKind::ExchangeAllocation,
"Carried flow",
boundary,
exchange,
);
validate_candidate(&f.p).unwrap();
let actual = prepare_command(&f.p, &Command::Delete(work_allocation))
.unwrap()
.project;
assert!(!actual.relations.contains_key(&port_allocation));
assert!(!actual.relations.contains_key(&carried));
let changes = collateral(&f);
assert!(changes.iter().any(|r| r.id == port_allocation));
assert!(changes.iter().any(|r| r.id == carried));
assert!(matches!(
plan_semantics(
&f.p,
&scope(&f),
Destination::Performer(f.b),
&BTreeMap::new()
),
Err(Refusal::InvalidCandidate(_))
));
}
#[test]
fn oa_communication_incompleteness_remains_advisory_without_collateral_loss() {
let mut f = fixture(Layer::Oa);
let a = f.a;
allocation(&mut f, a);
let peer =
f.p.add_element(Kind::Activity, Layer::Oa, "Peer work", None)
.unwrap();
f.p.relate(RelationKind::Allocation, "Peer allocation", f.b, peer);
let work =
f.p.relate(RelationKind::Interaction, "Work flow", f.w, peer);
let mean =
f.p.relate(RelationKind::CommunicationMean, "Radio", f.a, f.b);
let carried = f.p.relate(
RelationKind::ExchangeAllocation,
"Radio carries work",
mean,
work,
);
let plan = semantic(&f, Destination::OutsidePerformers);
assert_eq!(plan.project().relations[&carried], f.p.relations[&carried]);
assert!(
plan.warnings
.iter()
.any(|i| i.code == "incomplete_exchange_allocation" && i.severity == Severity::Warning)
);
let placed = place(&plan, xy(110., 500.), presentation(&f.p, f.d, None)).unwrap();
roundtrip(&f.p, finish(&plan, &f.p, &placed).unwrap().unwrap());
}
#[test]
fn scope_detects_first_child_allocation_and_role_transfer_and_finish_rejects_live_edits() {
let mut f = fixture(Layer::Oa);
let a = f.a;
let direct = allocation(&mut f, a);
let (role, assignment) = role(&mut f, Some(a));
let original = scope(&f);
let plan = semantic(&f, Destination::LayoutOnly);
let placement = place(&plan, xy(110., 500.), presentation(&f.p, f.d, None)).unwrap();
let child =
f.p.add_element(Kind::Activity, Layer::Oa, "New first child", Some(f.w))
.unwrap();
f.p.relations.get_mut(&direct).unwrap().body = RelationBody::Allocation {
source: a,
target: child,
};
f.p.relations.get_mut(&assignment).unwrap().body = RelationBody::RoleAssignment {
source: role,
target: child,
};
validate_candidate(&f.p).unwrap();
assert!(matches!(
plan_semantics(&f.p, &original, Destination::LayoutOnly, &BTreeMap::new()),
Err(Refusal::StaleScope)
));
assert!(matches!(
finish(&plan, &f.p, &placement),
Err(Refusal::StaleScope)
));
// Equal model values after undo are deliberately not an input capability;
// the App must still reject a retired token using its document/input epoch.
}
#[test]
fn cache_plan_uses_candidate_index_and_same_pixel_different_target_changes_preview() {
let f = fixture(Layer::Oa);
let stable = BTreeMap::from([(f.w, Id::new_v4())]);
let a = plan_semantics(&f.p, &scope(&f), Destination::Performer(f.a), &stable).unwrap();
let b = plan_semantics(&f.p, &scope(&f), Destination::Performer(f.b), &stable).unwrap();
// Same pointer coordinate, different externally resolved target: semantics
// and grouping must be recomputed even though the snapped delta is equal.
let first = place(&a, xy(500., 500.), presentation(&f.p, f.d, Some(f.a))).unwrap();
let second = place(&b, xy(500., 500.), presentation(&f.p, f.d, Some(f.b))).unwrap();
assert_eq!(first.delta, second.delta);
assert_ne!(
first
.view
.nodes
.iter()
.find(|n| n.id == f.n)
.unwrap()
.visual_parent,
second
.view
.nodes
.iter()
.find(|n| n.id == f.n)
.unwrap()
.visual_parent
);
assert_eq!(
first.view,
projection::project_diagram_with_index(a.project(), &first.diagram, a.index())
);
assert_eq!(
second.view,
projection::project_diagram_with_index(b.project(), &second.diagram, b.index())
);
let index = b.index() as *const ViewIndex;
let repeated = place(&b, xy(503., 498.), presentation(&f.p, f.d, Some(f.b))).unwrap();
assert_eq!(second.diagram, repeated.diagram);
assert_eq!(index, b.index() as *const ViewIndex);
}
#[test]
fn collapsed_target_unfold_is_explicit_and_no_other_fold_changes() {
let mut f = fixture(Layer::Oa);
let an = f.p.diagrams[&f.d]
.nodes
.values()
.find(|n| n.element == f.a)
.unwrap()
.id;
f.p.diagrams
.get_mut(&f.d)
.unwrap()
.nodes
.get_mut(&an)
.unwrap()
.collapsed = true;
let plan = semantic(&f, Destination::Performer(f.a));
let placed = place(&plan, xy(150., 200.), presentation(&f.p, f.d, Some(f.a))).unwrap();
assert_eq!(placed.unfolded, Some(an));
assert!(placed.view.nodes.iter().any(|n| n.id == f.n));
let after = roundtrip(&f.p, finish(&plan, &f.p, &placed).unwrap().unwrap());
assert!(!after.diagrams[&f.d].nodes[&an].collapsed);
assert_eq!(after.diagrams[&f.second], f.p.diagrams[&f.second]);
}
#[test]
fn target_presentation_requires_accepted_identity_and_selected_header_visibility() {
let f = fixture(Layer::Oa);
let plan = semantic(&f, Destination::Performer(f.a));
assert!(matches!(
place(&plan, xy(150., 200.), presentation(&f.p, f.d, Some(f.b))),
Err(Refusal::LayoutRequired(_))
));
let mut context = presentation(&f.p, f.d, Some(f.a));
context.target.as_mut().unwrap().placement = f.n;
assert!(matches!(
place(&plan, xy(150., 200.), context),
Err(Refusal::LayoutRequired(_))
));
let mut clipped = presentation(&f.p, f.d, Some(f.a));
clipped.viewport.position.y = 220.;
assert!(matches!(
place(&plan, xy(150., 200.), clipped),
Err(Refusal::LayoutRequired(_))
));
}
#[test]
fn toolbar_identity_allows_exact_incoming_placement_at_old_label_overlap() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = fixture(layer);
f.p.elements.get_mut(&f.a).unwrap().name = "Receiver".into();
let plan = semantic(&f, Destination::Performer(f.a));
let context = presentation(&f.p, f.d, Some(f.a));
let before = plan.project().clone();
let placed = place(&plan, xy(120., 105.), context).unwrap();
assert_eq!(placed.diagram.nodes[&f.n].position, xy(120., 110.));
assert_eq!(plan.project(), &before);
let after = roundtrip(&f.p, finish(&plan, &f.p, &placed).unwrap().unwrap());
assert_eq!(validation::effective_performer(&after, f.w), Some(f.a));
assert_eq!(after.diagrams[&f.d].nodes[&f.n].position, xy(120., 110.));
assert_eq!(after.elements, f.p.elements);
assert_eq!(after.diagrams[&f.second], f.p.diagrams[&f.second]);
}
}
#[test]
fn selected_header_visibility_allows_viewport_edge_but_refuses_clipping() {
let f = fixture(Layer::Oa);
let plan = semantic(&f, Destination::Performer(f.a));
let mut context = presentation(&f.p, f.d, Some(f.a));
context.viewport.position.y = 100.;
for (position, allowed) in [
(xy(120., 90.), false),
(xy(120., 100.), true),
(xy(120., 110.), true),
] {
let result = place(&plan, position, context);
assert_eq!(result.is_ok(), allowed, "{position:?}: {result:?}");
if let Ok(placed) = result {
assert_eq!(placed.diagram.nodes[&f.n].position, position);
}
}
}
#[test]
fn same_performer_readability_preserves_snapped_noop_and_allocation_identity() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = fixture(layer);
let a = f.a;
allocation(&mut f, a);
f.p.diagrams
.get_mut(&f.d)
.unwrap()
.nodes
.get_mut(&f.n)
.unwrap()
.position = xy(150., 200.);
let plan = semantic(&f, Destination::Performer(f.a));
let context = presentation(&f.p, f.d, Some(f.a));
assert_eq!(plan.project(), &f.p);
let noop = place(&plan, xy(154., 200.), context).unwrap();
assert!(finish(&plan, &f.p, &noop).unwrap().is_none());
let moved = place(&plan, xy(160., 210.), context).unwrap();
let after = roundtrip(&f.p, finish(&plan, &f.p, &moved).unwrap().unwrap());
assert_eq!(after.relations, f.p.relations);
}
}
#[test]
fn geometry_refuses_new_overlap_bounds_and_invisible_headers_and_preserves_existing_overlap() {
let mut f = fixture(Layer::Oa);
let obstacle =
f.p.add_element(Kind::Activity, Layer::Oa, "Other work", None)
.unwrap();
f.p.place(f.d, obstacle, xy(450., 500.));
let plan = semantic(&f, Destination::LayoutOnly);
assert!(matches!(
place(&plan, xy(400., 500.), presentation(&f.p, f.d, None)),
Err(Refusal::LayoutRequired(_))
));
assert!(matches!(
place(
&plan,
xy(MAX_COORDINATE + 10., 500.),
presentation(&f.p, f.d, None)
),
Err(Refusal::InvalidCandidate(_))
));
let mut clipped = presentation(&f.p, f.d, None);
clipped.viewport.size.width = 200.;
assert!(matches!(
place(&plan, xy(110., 500.), clipped),
Err(Refusal::LayoutRequired(_))
));
// Preexisting overlap is tolerated if its positive area is not increased.
f.p.diagrams
.get_mut(&f.d)
.unwrap()
.nodes
.get_mut(&f.n)
.unwrap()
.position = xy(400., 500.);
let plan = semantic(&f, Destination::LayoutOnly);
place(&plan, xy(390., 500.), presentation(&f.p, f.d, None)).unwrap();
}
#[test]
fn folded_subtree_cannot_hide_new_collision() {
let (mut f, _, _, _) = subtree(Layer::Oa);
let other =
f.p.add_element(Kind::Activity, Layer::Oa, "Hidden collision", None)
.unwrap();
// Expanded moved root will collide with this card, while its folded visible
// 94-unit card remains far above it. Use Arrange only to isolate geometry.
f.p.place(f.d, other, xy(550., 530.));
let plan = semantic(&f, Destination::LayoutOnly);
assert!(matches!(
place(&plan, xy(440., 200.), presentation(&f.p, f.d, None)),
Err(Refusal::LayoutRequired(_))
));
}
#[test]
fn malformed_scope_wrong_projection_duplicate_placement_and_stable_id_collision_refuse() {
let mut f = fixture(Layer::Oa);
assert!(matches!(
capture(&f.p, f.second, f.w, f.n),
Err(Refusal::Ineligible(_))
));
assert!(
matches!(plan_semantics(&f.p, &scope(&f), Destination::Performer(f.a), &BTreeMap::new()), Err(Refusal::MissingAllocationId(id)) if id == f.w)
);
assert!(matches!(
plan_semantics(
&f.p,
&scope(&f),
Destination::Performer(f.a),
&BTreeMap::from([(f.w, f.a)])
),
Err(Refusal::InvalidCandidate(_))
));
let mut duplicate = f.p.diagrams[&f.d].nodes[&f.n].clone();
duplicate.id = Id::new_v4();
duplicate.alias = "duplicate_placement".into();
f.p.diagrams
.get_mut(&f.d)
.unwrap()
.nodes
.insert(duplicate.id, duplicate);
assert!(matches!(
capture(&f.p, f.d, f.w, f.n),
Err(Refusal::InvalidCandidate(_))
));
}
struct DecomposedPair {
f: Fixture,
x: Id,
y: Id,
xn: Id,
yn: Id,
allocation_x: Id,
}
fn decomposed_pair(layer: Layer, parent_position: Position) -> DecomposedPair {
let mut f = fixture(layer);
let kind = f.p.elements[&f.w].kind();
f.p.diagrams
.get_mut(&f.d)
.unwrap()
.nodes
.get_mut(&f.n)
.unwrap()
.position = parent_position;
let x = f.p.add_element(kind, layer, "x", Some(f.w)).unwrap();
let y = f.p.add_element(kind, layer, "y", Some(f.w)).unwrap();
let xn = f.p.place(f.d, x, xy(180., 400.));
let yn = f.p.place(f.d, y, xy(180., 550.));
f.p.place(f.second, x, xy(1100., 800.));
f.p.place(f.second, y, xy(1500., 800.));
let allocation_x =
f.p.relate(RelationKind::Allocation, "Reviewed x allocation", f.a, x);
f.p.relations.get_mut(&allocation_x).unwrap().description =
"Preserve the reviewed allocation identity and rationale".into();
f.p.relate(RelationKind::Allocation, "Reviewed y allocation", f.a, y);
let edge_kind = if layer == Layer::Oa {
RelationKind::Interaction
} else {
RelationKind::FunctionalExchange
};
let edge = f.p.relate(edge_kind, "x to y", x, y);
for d in [f.d, f.second] {
f.p.diagrams.get_mut(&d).unwrap().routes = vec![route(edge)];
}
validate_candidate(&f.p).unwrap();
DecomposedPair {
f,
x,
y,
xn,
yn,
allocation_x,
}
}
fn projected_node(view: &ProjectedView, id: Id) -> &ProjectedNode {
view.nodes.iter().find(|node| node.id == id).unwrap()
}
#[test]
fn oa_sa_one_leaf_reassignment_and_outward_move_regroup_ancestors_without_moving_sibling() {
for layer in [Layer::Oa, Layer::Sa] {
// Authored parent placement has room to return to its independent card
// when the summary stops being uniformly A. Initially projection encloses
// x/y under P and P under A, even though P's authored position is distant.
let pair = decomposed_pair(layer, xy(500., 1000.));
let DecomposedPair {
f,
x,
y,
xn,
yn,
allocation_x,
} = pair;
let before = projection::project_view(&f.p, f.d);
let an = before
.nodes
.iter()
.find(|node| node.element == f.a)
.unwrap()
.id;
let bn = before
.nodes
.iter()
.find(|node| node.element == f.b)
.unwrap()
.id;
assert_eq!(validation::allocation_summary(&f.p, f.w), Some(f.a));
assert_eq!(projected_node(&before, f.n).visual_parent, Some(an));
assert_eq!(projected_node(&before, xn).visual_parent, Some(f.n));
assert_eq!(projected_node(&before, yn).visual_parent, Some(f.n));
let original_sibling = f.p.diagrams[&f.d].nodes[&yn].clone();
let original_routes = f.p.diagrams[&f.d].routes.clone();
let other_diagram = f.p.diagrams[&f.second].clone();
for (destination, requested, visual_parent) in [
(Destination::Performer(f.b), xy(950., 200.), bn),
// An unassigned semantic child returns to its work parent; this is
// outside performers, not removal from its semantic decomposition.
(Destination::OutsidePerformers, xy(540., 1100.), f.n),
] {
let captured = capture(&f.p, f.d, x, xn).unwrap();
assert_eq!(
captured.work_records.keys().copied().collect::<Vec<_>>(),
vec![x]
);
assert_eq!(
captured.placements.keys().copied().collect::<Vec<_>>(),
vec![xn]
);
let plan = plan_semantics(&f.p, &captured, destination, &BTreeMap::new()).unwrap();
let mut expected = f.p.clone();
match destination {
Destination::Performer(performer) => {
expected.relations.get_mut(&allocation_x).unwrap().body =
RelationBody::Allocation {
source: performer,
target: x,
};
}
Destination::OutsidePerformers => {
expected.relations.remove(&allocation_x);
}
Destination::LayoutOnly => unreachable!(),
}
assert_eq!(
plan.project(),
&expected,
"the complete intended semantic diff is one leaf Allocation record"
);
let target = match destination {
Destination::Performer(id) => Some(id),
_ => None,
};
let placed = place(&plan, requested, presentation(&f.p, f.d, target)).unwrap();
expected
.diagrams
.get_mut(&f.d)
.unwrap()
.nodes
.get_mut(&xn)
.unwrap()
.position = requested;
assert_eq!(placed.diagram, expected.diagrams[&f.d]);
assert_eq!(projected_node(&placed.view, f.n).visual_parent, None);
assert_eq!(
projected_node(&placed.view, xn).visual_parent,
Some(visual_parent)
);
assert_eq!(projected_node(&placed.view, yn).visual_parent, Some(an));
assert_eq!(validation::allocation_summary(plan.project(), f.w), None);
let result = roundtrip(&f.p, finish(&plan, &f.p, &placed).unwrap().unwrap());
assert_eq!(result, expected);
assert_eq!(result.elements[&x].parent, Some(f.w));
assert_eq!(result.elements[&y].parent, Some(f.w));
assert_eq!(result.diagrams[&f.d].nodes[&yn], original_sibling);
assert_eq!(
result.diagrams[&f.d].routes, original_routes,
"one-ended movement retains authored pins and labels"
);
assert_eq!(result.diagrams[&f.second], other_diagram);
}
}
}
#[test]
fn oa_sa_one_leaf_move_refuses_when_ancestor_regrouping_introduces_unrelated_overlap() {
for layer in [Layer::Oa, Layer::Sa] {
let DecomposedPair { f, x, xn, .. } = decomposed_pair(layer, xy(140., 200.));
let original = f.p.clone();
let captured = capture(&f.p, f.d, x, xn).unwrap();
for (destination, requested) in [
(Destination::Performer(f.b), xy(950., 200.)),
(Destination::OutsidePerformers, xy(540., 1100.)),
] {
let plan = plan_semantics(&f.p, &captured, destination, &BTreeMap::new()).unwrap();
validate_candidate(plan.project()).unwrap();
let mut diagram = plan.project().diagrams[&f.d].clone();
diagram.nodes.get_mut(&xn).unwrap().position = requested;
let actual_projection =
projection::project_diagram_with_index(plan.project(), &diagram, plan.index());
let parent = projected_node(&actual_projection, f.n);
let a = actual_projection
.nodes
.iter()
.find(|n| n.element == f.a)
.unwrap();
assert_eq!(parent.visual_parent, None);
assert!(
parent.position.x < a.position.x + a.size.width
&& a.position.x < parent.position.x + parent.size.width
);
assert!(
parent.position.y < a.position.y + a.size.height
&& a.position.y < parent.position.y + parent.size.height
);
let target = match destination {
Destination::Performer(id) => Some(id),
_ => None,
};
assert!(matches!(
place(&plan, requested, presentation(&f.p, f.d, target)),
Err(Refusal::LayoutRequired(_))
));
assert_eq!(
f.p, original,
"a valid semantic split still needs readable ancestor geometry"
);
}
}
}
struct PopulatedTarget {
f: Fixture,
target: Id,
group: Id,
nested: Id,
peer: Id,
}
fn populated_target(layer: Layer, hidden_overlap: bool) -> PopulatedTarget {
let mut f = fixture(layer);
let kind = f.p.elements[&f.w].kind();
let group =
f.p.add_element(kind, layer, "Existing collapsed work", None)
.unwrap();
let nested =
f.p.add_element(kind, layer, "Existing hidden child", Some(group))
.unwrap();
let peer =
f.p.add_element(kind, layer, "Existing peer work", None)
.unwrap();
f.p.relate(
RelationKind::Allocation,
"Nested responsibility",
f.b,
nested,
);
f.p.relate(RelationKind::Allocation, "Peer responsibility", f.b, peer);
let group = f.p.place(f.d, group, xy(950., 300.));
let nested = f.p.place(f.d, nested, xy(990., 450.));
let peer = f.p.place(
f.d,
peer,
xy(if hidden_overlap { 1050. } else { 1300. }, 300.),
);
let target = f.p.diagrams[&f.d]
.nodes
.values()
.find(|n| n.element == f.b)
.unwrap()
.id;
for id in [target, group] {
f.p.diagrams
.get_mut(&f.d)
.unwrap()
.nodes
.get_mut(&id)
.unwrap()
.collapsed = true;
}
validate_candidate(&f.p).unwrap();
PopulatedTarget {
f,
target,
group,
nested,
peer,
}
}
#[test]
fn oa_sa_populated_collapsed_target_accepts_room_and_refuses_collision_without_relocating_contents()
{
for layer in [Layer::Oa, Layer::Sa] {
let PopulatedTarget {
f,
target,
group,
nested,
peer,
} = populated_target(layer, false);
let original = f.p.clone();
let plan = semantic(&f, Destination::Performer(f.b));
let frozen = presentation(&f.p, f.d, Some(f.b));
let placed = place(&plan, xy(950., 200.), frozen).unwrap();
assert_eq!(placed.unfolded, Some(target));
for id in [group, nested, peer] {
assert_eq!(
placed.diagram.nodes[&id],
original.diagrams[&f.d].nodes[&id]
);
}
assert!(
projected_node(&placed.view, group).collapsed,
"opening the target does not open nested collapsed work"
);
assert!(placed.view.nodes.iter().all(|n| n.id != nested));
assert_eq!(
projected_node(&placed.view, f.n).visual_parent,
Some(target)
);
let mut expected = plan.project().clone();
let diagram = expected.diagrams.get_mut(&f.d).unwrap();
diagram.nodes.get_mut(&f.n).unwrap().position = xy(950., 200.);
diagram.nodes.get_mut(&target).unwrap().collapsed = false;
let after = roundtrip(&f.p, finish(&plan, &f.p, &placed).unwrap().unwrap());
assert_eq!(after, expected);
assert_eq!(after.diagrams[&f.second], original.diagrams[&f.second]);
// Same destination and baseline, but the moving card covers existing
// content. The planner cannot quietly pack or relocate that content.
assert!(matches!(
place(&plan, xy(950., 300.), frozen),
Err(Refusal::LayoutRequired(_))
));
assert_eq!(f.p, original);
}
}
#[test]
fn oa_sa_unfolding_prior_hidden_overlap_refuses_even_when_insertion_does_not_collide() {
for layer in [Layer::Oa, Layer::Sa] {
let PopulatedTarget {
f,
target,
group,
nested,
peer,
} = populated_target(layer, true);
let original = f.p.clone();
let before = projection::project_view(&f.p, f.d);
assert!(
before
.nodes
.iter()
.all(|n| ![group, nested, peer].contains(&n.id))
);
// This confusing pair exists in saved geometry already. Merely opening
// B exposes it: neither existing placement has been relocated by a drag.
let mut unfolded = f.p.diagrams[&f.d].clone();
unfolded.nodes.get_mut(&target).unwrap().collapsed = false;
let opening_only = projection::project_diagram(&f.p, &unfolded);
let existing_group = projected_node(&opening_only, group);
let existing_peer = projected_node(&opening_only, peer);
assert!(existing_group.position.x + existing_group.size.width > existing_peer.position.x);
assert_eq!(existing_group.position.y, existing_peer.position.y);
assert!(
opening_only.nodes.iter().all(|n| n.id != nested),
"nested fold remains closed"
);
let plan = semantic(&f, Destination::Performer(f.b));
unfolded.nodes.get_mut(&f.n).unwrap().position = xy(950., 200.);
let insertion =
projection::project_diagram_with_index(plan.project(), &unfolded, plan.index());
assert_eq!(projected_node(&insertion, group), existing_group);
assert_eq!(projected_node(&insertion, peer), existing_peer);
let moving = projected_node(&insertion, f.n);
assert!(
moving.position.y + moving.size.height <= existing_group.position.y,
"the incoming work itself has room"
);
assert!(matches!(
place(&plan, xy(950., 200.), presentation(&f.p, f.d, Some(f.b))),
Err(Refusal::LayoutRequired(_))
));
assert_eq!(
f.p, original,
"first-slice policy refuses newly exposed confusion, not only relocation collisions"
);
}
}
#[test]
fn oa_sa_state_owned_action_and_availability_reject_reassignment_atomically() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = fixture(layer);
let a = f.a;
let direct = allocation(&mut f, a);
let machine =
f.p.add_element(Kind::StateMachine, layer, "A lifecycle", Some(a))
.unwrap();
let region =
f.p.add_element(Kind::Region, layer, "Main region", Some(machine))
.unwrap();
let initial =
f.p.add_element(Kind::Initial, layer, "Initial", Some(region))
.unwrap();
let ready =
f.p.add_element(Kind::State, layer, "Ready", Some(region))
.unwrap();
let final_state =
f.p.add_element(Kind::Final, layer, "Finished", Some(region))
.unwrap();
f.p.relate(RelationKind::StateTransition, "Enter ready", initial, ready);
f.p.relate(RelationKind::StateTransition, "Finish", ready, final_state);
let action =
f.p.relate(RelationKind::StateAction, "A entry action", ready, f.w);
let availability =
f.p.relate(RelationKind::Availability, "A availability", f.w, ready);
validate_candidate(&f.p).unwrap();
let mut bundle = format_bundle(&f.p);
bundle
.files
.get_mut(&bundle.entry.clone())
.unwrap()
.insert_str(
0,
"// Keep state responsibility and accepted source intact.\n",
);
let mut document = Document::from_bundle(bundle, ProjectLimits::default()).unwrap();
document
.execute(Command::Describe(
f.w,
"Accepted engineering rationale".into(),
))
.unwrap();
document
.execute(Command::Rename(f.w, "Future reviewed work".into()))
.unwrap();
assert!(document.undo());
assert!(document.can_undo() && document.can_redo());
let accepted = document.project().clone();
let source = document.bundle().clone();
let revision = document.revision();
let mut changed = accepted.relations[&direct].clone();
changed.body = RelationBody::Allocation {
source: f.b,
target: f.w,
};
let mut intended = accepted.clone();
intended.relations.insert(direct, changed.clone());
let errors: Vec<_> = validation::validate(&intended)
.into_iter()
.filter(|i| i.severity == Severity::Error)
.collect();
for context in [action, availability] {
assert!(
errors
.iter()
.any(|i| i.element == Some(context) && i.code == "state_owner_context"),
"actual core validator must reject the cross-performer behavior: {errors:?}"
);
}
let captured = capture(&accepted, f.d, f.w, f.n).unwrap();
assert!(
matches!(plan_semantics(&accepted, &captured, Destination::Performer(f.b), &BTreeMap::new()), Err(Refusal::InvalidCandidate(reason)) if reason.contains("state_owner_context"))
);
// Exercise actual Document admission too: its model preparation rejects
// the whole semantic-plus-layout batch before source or history changes.
let mut moved = accepted.diagrams[&f.d].nodes[&f.n].clone();
moved.position = xy(950., 200.);
let error = document
.execute(Command::Batch(vec![
Command::EditRelation(direct, changed),
Command::EditNode {
diagram: f.d,
original: f.n,
node: moved,
},
]))
.unwrap_err();
assert!(
matches!(error, crate::document::DocumentError::Model(ref reason) if reason.to_string().contains("state_owner_context"))
);
assert_eq!(document.project(), &accepted);
assert_eq!(document.bundle(), &source);
assert_eq!(document.revision(), revision);
assert!(document.can_undo() && document.can_redo());
assert!(document.redo());
assert_eq!(
document.project().elements[&f.w].name,
"Future reviewed work"
);
assert!(document.undo());
assert_eq!(document.project(), &accepted);
assert_eq!(document.bundle(), &source);
// Core intentionally permits missing allocation here. Do not turn the
// A-to-B context error into an invented prohibition of outward removal.
let outward = plan_semantics(
&accepted,
&captured,
Destination::OutsidePerformers,
&BTreeMap::new(),
)
.unwrap();
validate_candidate(outward.project()).unwrap();
assert_eq!(
outward.project().relations[&action],
accepted.relations[&action]
);
assert_eq!(
outward.project().relations[&availability],
accepted.relations[&availability]
);
}
}