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::>() }; let lines = |s: &Scene| { s.lines .iter() .map(|l| { ( l.id, l.points.clone(), l.label_at, l.label.clone(), l.dashed, ) }) .collect::>() }; let slots = |s: &Scene| { s.slots .iter() .map(|slot| { ( slot.slot.parent, slot.slot.before, slot.y, slot.label.clone(), ) }) .collect::>() }; 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, ¤t.title, ¤t.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, ¤t_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 { // 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![(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::>() }; 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![(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![(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![(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![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)); }