Files
ARCADIA-Studio/harness/experiments/behavior-anchor-plan-20260912/tests.rs
T

728 lines
25 KiB
Rust

use super::*;
use crate::document::Document;
use oa_core::{
language,
source_project::{ProjectLimits, SourceBundle},
validation,
};
fn id(p: &Project, alias: &str) -> Id {
p.identities()
.into_iter()
.find(|(_, name)| *name == alias)
.unwrap()
.0
}
fn fixture(lifetime: bool) -> (Document, Id) {
let (name, input, owner) = if lifetime {
(
"lifetime.arcadia",
include_str!("../behavior-layout-scene-20260912/lifetime.arcadia"),
"lifetime_conversation",
)
} else {
(
"fixture.arcadia",
include_str!("../behavior-layout-scene-20260912/fixture.arcadia"),
"conversation",
)
};
let p = language::parse(input).expect("existing real Arcadia fixture");
let errors: Vec<_> = validation::validate(&p)
.into_iter()
.filter(|issue| issue.severity == validation::Severity::Error)
.collect();
assert!(errors.is_empty(), "{errors:?}");
let bundle = SourceBundle {
entry: name.into(),
files: BTreeMap::from([(name.into(), input.into())]),
};
let document = Document::from_bundle(bundle, ProjectLimits::default()).unwrap();
assert_eq!(document.project(), &p);
assert_eq!(language::parse(&language::format(&p)).unwrap(), p);
let owner = id(&p, owner);
(document, owner)
}
fn passive(document: &Document, before: &Project, source: &SourceBundle) {
assert_eq!(document.project(), before);
assert_eq!(document.bundle(), source);
assert!(!document.dirty());
assert!(!document.can_undo() && !document.can_redo());
}
fn row(scene: &Scene, id: Id) -> f32 {
scene.rows[&id].1
}
fn line(scene: &Scene, id: Id) -> &Line {
assert_eq!(scene.lines.iter().filter(|line| line.id == id).count(), 1);
scene.lines.iter().find(|line| line.id == id).unwrap()
}
fn named_box(scene: &Scene, id: Id) -> &BoxItem {
scene
.boxes
.iter()
.find(|item| item.id == id && !item.title.is_empty())
.unwrap()
}
fn same_scene(a: &Scene, b: &Scene) {
assert_eq!(a.rows, b.rows);
assert_eq!(a.sequence_bounds, b.sequence_bounds);
assert_eq!(a.size, b.size);
assert_eq!(a.lanes, b.lanes);
assert_eq!(a.lifecycle, b.lifecycle);
assert_eq!(a.open_calls, b.open_calls);
let boxes = |s: &Scene| {
s.boxes
.iter()
.map(|b| (b.id, b.rect, b.title.clone(), b.detail.clone(), b.marker))
.collect::<Vec<_>>()
};
let lines = |s: &Scene| {
s.lines
.iter()
.map(|l| {
(
l.id,
l.points.clone(),
l.label_at,
l.label.clone(),
l.dashed,
)
})
.collect::<Vec<_>>()
};
let slots = |s: &Scene| {
s.slots
.iter()
.map(|slot| {
(
slot.slot.parent,
slot.slot.before,
slot.y,
slot.label.clone(),
)
})
.collect::<Vec<_>>()
};
assert_eq!(boxes(a), boxes(b));
assert_eq!(lines(a), lines(b));
assert_eq!(slots(a), slots(b));
}
fn others_fixed(before: &Scene, after: &Scene, changed: Id) {
assert_eq!(before.rows.len(), after.rows.len());
for (&id, original) in &before.rows {
if id != changed {
assert_eq!(&after.rows[&id], original, "unexpected movement of {id}");
}
}
}
fn fixed_frame_metadata(before: &Scene, after: &Scene, frame: Id) {
let original = named_box(before, frame);
let current = named_box(after, frame);
assert_eq!(
(
current.id,
current.rect,
&current.title,
&current.detail,
current.marker
),
(
original.id,
original.rect,
&original.title,
&original.detail,
original.marker
)
);
assert_eq!(before.rows[&frame], after.rows[&frame]);
assert_eq!(row(after, frame), 332.);
// Scene::hit uses the first38px as the frame's active painted header.
let header = Rectangle::new(
Point::new(original.rect.x, original.rect.y),
Size::new(original.rect.width, 38.),
);
assert_eq!((header.y, header.y + header.height), (332., 370.));
let probe = Point::new(header.x + 8., header.y + 8.);
assert_eq!(before.hit(probe), Some(frame));
assert_eq!(after.hit(probe), Some(frame));
let original_slot = before
.slots
.iter()
.find(|slot| slot.slot.before == Some(frame))
.unwrap();
let current_slot = after
.slots
.iter()
.find(|slot| slot.slot.before == Some(frame))
.unwrap();
assert_eq!(
(
current_slot.slot.parent,
current_slot.slot.before,
current_slot.y,
&current_slot.label
),
(
original_slot.slot.parent,
original_slot.slot.before,
original_slot.y,
&original_slot.label
)
);
assert_eq!(current_slot.y, 290.);
}
fn actual_lifetime_projection(
p: &Project,
owner: Id,
scene: &Scene,
) -> Vec<scenario_lifecycle::LaneLifetime> {
// Same actual rows/scope inputs as Rendering::new, including its root span
// and empty-operand allowance; call the real projector for diagnostics.
let mut scopes = scene.sequence_bounds.clone();
for (&scope, (start, end)) in &mut scopes {
if scope != owner && *start == *end {
*end = *start + 26.;
}
}
scopes.insert(owner, (85., scene.size.height - 20.));
scenario_lifecycle::project_lifetimes(
p,
owner,
&scenario_lifecycle::Geometry {
rows: &scene.rows,
scopes: &scopes,
},
)
}
#[test]
fn complete_seed_plan_preserves_both_actual_scene_and_document_baselines() {
for lifetime in [false, true] {
let (document, owner) = fixture(lifetime);
let p = document.project();
let before = p.clone();
let source = document.bundle().clone();
let baseline = Scene::sequence(p, owner, owner);
let plan = Plan::build(p, owner, &baseline, &Candidate::new()).unwrap();
assert!(plan.diagnostics.is_empty());
for (&id, (_, y)) in &baseline.rows {
assert_eq!(plan.anchors[&id].y, *y);
}
same_scene(&baseline, &render(p, owner, &plan));
let reparsed = language::parse(&language::format(p)).unwrap();
same_scene(&baseline, &render(&reparsed, owner, &plan));
passive(&document, &before, &source);
}
}
#[test]
fn upward_second_delivery_reaches_200_and_all_other_occurrence_anchors_stay_fixed() {
let (document, owner) = fixture(false);
let p = document.project();
let before = p.clone();
let source = document.bundle().clone();
let baseline = Scene::sequence(p, owner, owner);
let first = id(p, "first");
let second = id(p, "second");
assert_ne!(first, second);
for occurrence_id in [first, second] {
assert!(
matches!(occurrence(p,occurrence_id),Some(OccurrenceBody::Exchange { exchange:Some(edge),.. }) if *edge==id(p,"channel"))
);
}
assert_eq!(
(row(&baseline, first), row(&baseline, second)),
(148., 240.)
);
let candidate = drag(&Candidate::new(), &baseline, second, -40.);
assert_eq!(candidate, Candidate::from([(second, 200.)]));
let plan = Plan::build(p, owner, &baseline, &candidate).unwrap();
let moved = render(p, owner, &plan);
assert_eq!((row(&moved, first), row(&moved, second)), (148., 200.));
others_fixed(&baseline, &moved, second);
assert_eq!(line(&moved, first).points, line(&baseline, first).points);
let left = moved.lanes[&id(p, "left")];
let right = moved.lanes[&id(p, "right")];
assert_eq!(
line(&moved, second).points,
vec![Point::new(left, 200.), Point::new(right, 200.)]
);
assert_eq!(
plan.messages[&second],
MessageAnchors {
send_y: 200.,
receive_y: 200.
}
);
assert!(plan.diagnostics.is_empty());
same_scene(&moved, &render(p, owner, &plan));
passive(&document, &before, &source);
}
#[test]
fn nested_upward_anchor_reaches_457_without_moving_parent_or_followers() {
let (document, owner) = fixture(false);
let p = document.project();
let before = p.clone();
let source = document.bundle().clone();
let baseline = Scene::sequence(p, owner, owner);
let nested = id(p, "nested");
let fragment = id(p, "fragment");
assert_eq!(row(&baseline, nested), 477.);
let candidate = drag(&Candidate::new(), &baseline, nested, -20.);
let plan = Plan::build(p, owner, &baseline, &candidate).unwrap();
let moved = render(p, owner, &plan);
assert_eq!(row(&moved, nested), 457.);
assert_eq!(named_box(&moved, nested).rect.y, 437.);
assert_eq!(named_box(&moved, nested).rect.height, 70.);
assert_eq!(
named_box(&moved, fragment).rect,
named_box(&baseline, fragment).rect
);
assert_eq!(
(
named_box(&moved, fragment).rect.y,
named_box(&moved, fragment).rect.height
),
(332., 240.)
);
fixed_frame_metadata(&baseline, &moved, fragment);
assert_eq!(
moved.sequence_bounds[&id(p, "operand")],
baseline.sequence_bounds[&id(p, "operand")]
);
others_fixed(&baseline, &moved, nested);
assert!(plan.diagnostics.is_empty());
passive(&document, &before, &source);
}
#[test]
fn nested_move_that_would_relocate_fixed_header_is_rejected_but_mere_overlap_is_not_banned() {
let (document, owner) = fixture(false);
let p = document.project();
let before = p.clone();
let source = document.bundle().clone();
let baseline = Scene::sequence(p, owner, owner);
let nested = id(p, "nested");
let fragment = id(p, "fragment");
let requested = Candidate::from([(nested, 300.)]);
// Actual70px action box has row-to-top offset-20: proposed footprint280..350.
// The old envelope union would move frame/header top332 to260 while leaving
// FrameStart332 and before-slot290. Reject before any such Scene is exposed.
let original = named_box(&baseline, nested).rect;
assert_eq!(
(row(&baseline, nested), original.y, original.height),
(477., 457., 70.)
);
let proposed_top = original.y + (300. - row(&baseline, nested));
assert_eq!((proposed_top, proposed_top + original.height), (280., 350.));
assert_eq!(
Plan::build(p, owner, &baseline, &requested).unwrap_err(),
PlanError::FixedFrameHeaderWouldMove { frame: fragment }
);
fixed_frame_metadata(&baseline, &Scene::sequence(p, owner, owner), fragment);
// Overlap inside the existing frame does not itself move its top. This
// remains admitted mechanical geometry, not a claim of usable overlap UX.
let overlap = Plan::build(p, owner, &baseline, &Candidate::from([(nested, 380.)])).unwrap();
let overlapping = render(p, owner, &overlap);
assert_eq!(named_box(&overlapping, nested).rect.y, 360.);
assert_eq!(overlapping.sequence_bounds[&id(p, "operand")], (338., 530.));
fixed_frame_metadata(&baseline, &overlapping, fragment);
let original_lifetimes = actual_lifetime_projection(p, owner, &baseline);
let moved_lifetimes = actual_lifetime_projection(p, owner, &overlapping);
assert_eq!(original_lifetimes.len(), 2);
assert_eq!(moved_lifetimes.len(), 2);
assert!(
original_lifetimes
.iter()
.all(|lane| lane.uncertainties.is_empty())
);
assert!(
moved_lifetimes
.iter()
.all(|lane| lane.uncertainties.is_empty()),
"{moved_lifetimes:?}"
);
let receiver = id(p, "right");
for scene in [&baseline, &overlapping] {
assert_eq!(
scene
.lifecycle
.alive_spans(receiver, owner, 530., scene.size.height - 20.)
.iter()
.map(|span| (span.start, span.end))
.collect::<Vec<_>>(),
vec![(530., 845.)]
);
}
let call = id(p, "self_call");
let bars = |scene: &Scene| {
scene
.boxes
.iter()
.filter(|item| item.id == call && item.title.is_empty())
.map(|item| (item.rect, item.marker))
.collect::<Vec<_>>()
};
assert!(!bars(&baseline).is_empty());
assert_eq!(bars(&overlapping), bars(&baseline));
passive(&document, &before, &source);
}
#[test]
fn nested353_scope_escape_is_named_and_rejected_before_missing_lifecycle_geometry_is_exposed() {
let (document, owner) = fixture(false);
let p = document.project();
let before = p.clone();
let source = document.bundle().clone();
let baseline = Scene::sequence(p, owner, owner);
let nested = id(p, "nested");
let operand = id(p, "operand");
let fragment = id(p, "fragment");
let original = named_box(&baseline, nested).rect;
assert_eq!(
(original.y, baseline.sequence_bounds[&operand].0),
(457., 435.)
);
let proposed_top = original.y + (353. - row(&baseline, nested));
let proposed_scope_start = proposed_top - (original.y - baseline.sequence_bounds[&operand].0);
assert_eq!(
(proposed_top, proposed_scope_start, row(&baseline, fragment)),
(333., 311., 332.)
);
assert_eq!(
Plan::build(p, owner, &baseline, &Candidate::from([(nested, 353.)])).unwrap_err(),
PlanError::OperandScopeOutsideEnclosingInterval { operand, fragment }
);
same_scene(&baseline, &Scene::sequence(p, owner, owner));
passive(&document, &before, &source);
}
#[test]
fn lifecycle_anchor_below_actual_root_render_scope_is_rejected_before_scene_construction() {
let (document, owner) = fixture(true);
let p = document.project();
let before = p.clone();
let source = document.bundle().clone();
let baseline = Scene::sequence(p, owner, owner);
let create = id(p, "create_receiver");
let provider = id(p, "provider");
let requester = id(p, "requester");
let root_life = baseline
.lifecycle
.alive_spans(requester, owner, 0., baseline.size.height);
assert_eq!(
root_life
.iter()
.map(|span| (span.start, span.end))
.collect::<Vec<_>>(),
vec![(85., 544.)]
);
for requested in [50., 84.] {
assert_eq!(
Plan::build(p, owner, &baseline, &Candidate::from([(create, requested)])).unwrap_err(),
PlanError::LifecycleAnchorOutsideRenderScope {
occurrence: create,
scope: owner
}
);
}
// At the renderer's actual85 boundary the marker/lifetime remains present.
// Source-order crossing is still diagnosed separately, not silently banned.
let boundary = Plan::build(p, owner, &baseline, &Candidate::from([(create, 85.)])).unwrap();
let moved = render(p, owner, &boundary);
let x = moved.lanes[&provider];
assert_eq!(row(&moved, create), 85.);
assert_eq!(moved.lifecycle.marker_at(Point::new(x, 85.)), Some(create));
let alive = moved
.lifecycle
.alive_spans(provider, owner, 0., moved.size.height);
assert_eq!(
alive
.iter()
.map(|span| (span.start, span.end))
.collect::<Vec<_>>(),
vec![(85., 427.)]
);
passive(&document, &before, &source);
}
#[test]
fn earlier_death_reaches_380_and_actual_lifetime_and_activation_clip_there() {
let (document, owner) = fixture(true);
let p = document.project();
let before = p.clone();
let source = document.bundle().clone();
let baseline = Scene::sequence(p, owner, owner);
let create = id(p, "create_receiver");
let call = id(p, "service_call");
let delete = id(p, "delete_receiver");
let provider = id(p, "provider");
assert_eq!(
(
row(&baseline, create),
row(&baseline, call),
row(&baseline, delete)
),
(243., 335., 427.)
);
let candidate = drag(&Candidate::new(), &baseline, delete, -47.);
let plan = Plan::build(p, owner, &baseline, &candidate).unwrap();
assert_eq!(plan.anchors[&delete].role, AnchorRole::MessageReceive);
assert_eq!(
plan.messages[&delete],
MessageAnchors {
send_y: 380.,
receive_y: 380.
}
);
let moved = render(p, owner, &plan);
assert_eq!(row(&moved, delete), 380.);
others_fixed(&baseline, &moved, delete);
let spans = moved
.lifecycle
.alive_spans(provider, owner, 85., moved.size.height - 20.);
assert_eq!(
spans.iter().map(|s| (s.start, s.end)).collect::<Vec<_>>(),
vec![(243., 380.)]
);
assert!(
moved
.lifecycle
.alive_spans(provider, owner, 380., moved.size.height - 20.)
.is_empty()
);
let x = moved.lanes[&provider];
assert_eq!(moved.lifecycle.marker_at(Point::new(x, 380.)), Some(delete));
assert_eq!(moved.lifecycle.marker_at(Point::new(x, 427.)), None);
assert_eq!(moved.hit(Point::new(x, 380.)), Some(delete));
let bars: Vec<_> = moved
.boxes
.iter()
.filter(|b| b.id == call && b.title.is_empty())
.collect();
assert_eq!(bars.len(), 1);
assert_eq!(
bars[0].rect,
Rectangle::new(Point::new(x - 4., 335.), Size::new(8., 45.))
);
assert_eq!(moved.open_calls, vec![Point::new(x + 12., 364.)]);
assert!(plan.diagnostics.is_empty());
passive(&document, &before, &source);
}
#[test]
fn self_call_has_explicit_send610_receive634_and_reply_stays728() {
let (document, owner) = fixture(false);
let p = document.project();
let before = p.clone();
let source = document.bundle().clone();
let baseline = Scene::sequence(p, owner, owner);
let call = id(p, "self_call");
let reply = id(p, "self_reply");
let candidate = drag(&Candidate::new(), &baseline, call, -26.);
let plan = Plan::build(p, owner, &baseline, &candidate).unwrap();
assert_eq!(
plan.anchors[&call],
Anchor {
y: 610.,
role: AnchorRole::MessageSend
}
);
assert_eq!(
plan.messages[&call],
MessageAnchors {
send_y: 610.,
receive_y: 634.
}
);
let moved = render(p, owner, &plan);
assert_eq!(row(&moved, call), 610.);
assert_eq!(row(&moved, reply), 728.);
assert_eq!(
line(&moved, call)
.points
.iter()
.map(|p| p.y)
.collect::<Vec<_>>(),
vec![610., 610., 634., 634.]
);
assert_eq!(line(&moved, reply).points, line(&baseline, reply).points);
others_fixed(&baseline, &moved, call);
// Peer #21 owns the activation start/column contract. This assertion covers
// actual message anchors, not the old row-based activation start defect.
passive(&document, &before, &source);
}
#[test]
fn signed_zero_keeps_sparse_candidate_and_restoring_original_candidate_restores_actual_scene() {
let (document, owner) = fixture(false);
let p = document.project();
let before = p.clone();
let source = document.bundle().clone();
let baseline = Scene::sequence(p, owner, owner);
let second = id(p, "second");
let original = Candidate::new();
for zero in [0., -0.] {
assert_eq!(drag(&original, &baseline, second, zero), original);
}
let changed = drag(&original, &baseline, second, -40.);
let plan = Plan::build(p, owner, &baseline, &changed).unwrap();
let frame = render(p, owner, &plan);
for zero in [0., -0.] {
let untouched = drag(&changed, &frame, second, zero);
assert_eq!(untouched, changed);
same_scene(
&frame,
&render(
p,
owner,
&Plan::build(p, owner, &baseline, &untouched).unwrap(),
),
);
}
// This restores the exact sidecar candidate; it is not a production Undo
// command or evidence of a layout source/REST persistence implementation.
same_scene(
&baseline,
&render(
p,
owner,
&Plan::build(p, owner, &baseline, &original).unwrap(),
),
);
passive(&document, &before, &source);
}
#[test]
fn crossing_is_rendered_exactly_and_diagnosed_with_extents_covering_the_earlier_far_event() {
let (document, owner) = fixture(false);
let p = document.project();
let before = p.clone();
let source = document.bundle().clone();
let baseline = Scene::sequence(p, owner, owner);
let first = id(p, "first");
let second = id(p, "second");
let plan = Plan::build(p, owner, &baseline, &Candidate::from([(first, 1000.)])).unwrap();
assert!(plan.diagnostics.contains(&Diagnostic::SourceOrderCrossing {
scope: owner,
before: first,
after: second
}));
let moved = render(p, owner, &plan);
assert_eq!((row(&moved, first), row(&moved, second)), (1000., 240.));
assert_eq!(row(&moved, id(p, "self_reply")), 728.);
others_fixed(&baseline, &moved, first);
assert_eq!(moved.sequence_bounds[&owner].1, 1026.);
assert_eq!(moved.size.height, 1113.);
assert!(
line(&moved, first)
.points
.iter()
.all(|point| point.y < moved.size.height)
);
assert_eq!(&p.elements[&owner].steps().unwrap()[..2], &[first, second]);
// This is a policy falsifier: mechanically exact coordinates do not prove
// that out-of-order vertical notation is understandable or acceptable.
passive(&document, &before, &source);
}
#[test]
fn nested_outlier_expands_enclosing_geometry_without_using_final_traversal_row_as_extent() {
let (document, owner) = fixture(false);
let p = document.project();
let baseline = Scene::sequence(p, owner, owner);
let nested = id(p, "nested");
let fragment = id(p, "fragment");
let following = id(p, "self_call");
let plan = Plan::build(p, owner, &baseline, &Candidate::from([(nested, 1000.)])).unwrap();
assert!(
plan.diagnostics
.contains(&Diagnostic::FrameExtendsPastFollowingAnchor {
frame: fragment,
following
})
);
let moved = render(p, owner, &plan);
assert_eq!(row(&moved, nested), 1000.);
assert_eq!(named_box(&moved, nested).rect.y, 980.);
let enclosing = named_box(&moved, fragment).rect;
assert_eq!(enclosing.y + enclosing.height, 1070.);
assert!(moved.sequence_bounds[&owner].1 >= 1070.);
assert!(moved.size.height > 1070.);
assert_eq!(row(&moved, following), 636.);
others_fixed(&baseline, &moved, nested);
}
#[test]
fn unsupported_targets_and_lifetime_inversion_fail_explicitly_instead_of_silent_clamping() {
let (document, owner) = fixture(true);
let p = document.project();
let baseline = Scene::sequence(p, owner, owner);
let create = id(p, "create_receiver");
let delete = id(p, "delete_receiver");
assert_eq!(
Plan::build(p, owner, &baseline, &Candidate::from([(delete, 200.)])).unwrap_err(),
PlanError::LifetimeInversionNotSupported { create, delete }
);
assert_eq!(
Plan::build(p, owner, &baseline, &Candidate::from([(delete, f32::NAN)])).unwrap_err(),
PlanError::NonFinite(delete)
);
let edge = id(p, "service");
assert_eq!(
Plan::build(p, owner, &baseline, &Candidate::from([(edge, 200.)])).unwrap_err(),
PlanError::UnknownAnchor(edge)
);
let (other, other_owner) = fixture(false);
let fragment = id(other.project(), "fragment");
let other_scene = Scene::sequence(other.project(), other_owner, other_owner);
assert_eq!(
Plan::build(
other.project(),
other_owner,
&other_scene,
&Candidate::from([(fragment, 300.)])
)
.unwrap_err(),
PlanError::CompoundMoveNotImplemented(fragment)
);
}
#[test]
fn complete_plan_scope_is_inactive_in_other_threads_and_restored_after_deliberate_panic() {
let (document, owner) = fixture(false);
let p = document.project();
let baseline = Scene::sequence(p, owner, owner);
let second = id(p, "second");
let plan = Plan::build(p, owner, &baseline, &Candidate::from([(second, 200.)])).unwrap();
let caught = std::panic::catch_unwind(|| {
let _scope = Scope::install(&plan);
assert_eq!(row(&Scene::sequence(p, owner, owner), second), 200.);
std::thread::spawn(|| {
let (document, owner) = fixture(false);
let p = document.project();
assert_eq!(
row(&Scene::sequence(p, owner, owner), id(p, "second")),
240.
);
})
.join()
.unwrap();
panic!("deliberate complete-anchor scope unwind");
});
let payload = caught.expect_err("deliberate panic is required");
assert_eq!(
payload.downcast_ref::<&str>().copied(),
Some("deliberate complete-anchor scope unwind")
);
same_scene(&baseline, &Scene::sequence(p, owner, owner));
}