//! Isolated test-only adapter. See README.md and wiring.patch before running. //! It drives the actual parent Scene; it contains no replacement geometry model. use super::*; use oa_core::{language, validation}; use std::cell::RefCell; mod lifetime; type Layout = BTreeMap; thread_local! { static ANCHORS: RefCell> = const { RefCell::new(None) }; } struct AnchorScope(Option); impl AnchorScope { fn install(layout: &Layout) -> Self { Self(ANCHORS.with(|slot| slot.replace(Some(layout.clone())))) } } impl Drop for AnchorScope { fn drop(&mut self) { ANCHORS.with(|slot| { slot.replace(self.0.take()); }); } } /// Called only by the proposed cfg(test) row hook, before Scene records rows. /// Retains the CURRENT renderer's next-row floor; this is not general free y. pub(super) fn resolve_row_anchor(id: Id, y: &mut f32) { ANCHORS.with(|slot| { if let Some(desired) = slot.borrow().as_ref().and_then(|map| map.get(&id)) { assert!(desired.is_finite()); *y = (*y).max(*desired); } }); } fn render(project: &Project, owner: Id, layout: &Layout) -> Scene { let _scope = AnchorScope::install(layout); Scene::sequence(project, owner, owner) } /// Pointer intent differs from explicit SetDesiredY: zero displacement preserves /// the complete original authored candidate even when desired != displayed. fn drag(original: &Layout, frame: &Scene, target: Id, delta_y: f32) -> Layout { assert!(delta_y.is_finite()); if delta_y == 0. { return original.clone(); } let mut next = original.clone(); next.insert(target, frame.rows[&target].1 + delta_y); next } fn id(p: &Project, alias: &str) -> Id { p.identities() .into_iter() .find(|(_, name)| *name == alias) .unwrap() .0 } fn fixture() -> (Project, Id) { let p = language::parse(include_str!("fixture.arcadia")).expect("real Arcadia fixture parses"); let errors: Vec<_> = validation::validate(&p) .into_iter() .filter(|issue| issue.severity == validation::Severity::Error) .collect(); assert!(errors.is_empty(), "fixture semantic errors: {errors:?}"); let owner = id(&p, "conversation"); (p, owner) } 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_geometry(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 lines = |s: &Scene| { s.lines .iter() .map(|l| { ( l.id, l.points.clone(), l.label_at, l.label.clone(), l.dashed, ) }) .collect::>() }; let boxes = |s: &Scene| { s.boxes .iter() .map(|b| (b.id, b.rect, b.title.clone(), b.detail.clone(), b.marker)) .collect::>() }; assert_eq!(lines(a), lines(b)); assert_eq!(boxes(a), boxes(b)); let slots = |s: &Scene| { s.slots .iter() .map(|s| (s.slot.parent, s.slot.before, s.y, s.label.clone())) .collect::>() }; assert_eq!(slots(a), slots(b)); } #[test] fn real_parser_roundtrip_and_baseline_scene_have_independent_expected_anchors() { let (p, owner) = fixture(); let before = p.clone(); let source = language::format(&p); let reparsed = language::parse(&source).unwrap(); assert_eq!(reparsed, p); let baseline = Scene::sequence(&p, owner, owner); same_geometry(&baseline, &render(&p, owner, &Layout::new())); same_geometry(&baseline, &Scene::sequence(&reparsed, owner, owner)); let first = id(&p, "first"); let second = id(&p, "second"); assert_ne!(first, second); for occurrence in [first, second] { assert!(matches!(p.elements[&occurrence].shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { exchange: Some(edge), .. }, .. }) if *edge == id(&p, "channel"))); } // Hand-derived current metrics: start148, each message92, fragment55, // operand90, action95, fragment trailing64. These are regression oracles, // NOT a claim that those fixed spacings should remain product constraints. for (alias, expected) in [ ("first", 148.), ("second", 240.), ("fragment", 332.), ("nested", 477.), ("self_call", 636.), ("self_reply", 728.), ] { assert_eq!(row(&baseline, id(&p, alias)), expected, "{alias}"); } assert_eq!(baseline.rows[&id(&p, "nested")].0, id(&p, "operand")); assert_eq!(named_box(&baseline, id(&p, "nested")).rect.y, 457.); let frame = named_box(&baseline, id(&p, "fragment")).rect; assert_eq!((frame.y, frame.height), (332., 240.)); let left = baseline.lanes[&id(&p, "left")]; let right = baseline.lanes[&id(&p, "right")]; assert_eq!( line(&baseline, first).points, vec![Point::new(left, 148.), Point::new(right, 148.)] ); assert_eq!( line(&baseline, second).points, vec![Point::new(left, 240.), Point::new(right, 240.)] ); let local = line(&baseline, id(&p, "self_call")); assert_eq!( local.points, vec![ Point::new(right, 636.), Point::new(right + 75., 636.), Point::new(right + 75., 660.), Point::new(right, 660.) ] ); assert_eq!(p, before); assert_eq!(language::format(&p), source); } #[test] fn moved_repeat_drives_actual_scene_followers_frames_and_self_message_without_semantic_edits() { let (p, owner) = fixture(); let before = p.clone(); let source = language::format(&p); let baseline = Scene::sequence(&p, owner, owner); let second = id(&p, "second"); let candidate = drag(&Layout::new(), &baseline, second, 40.); assert_eq!(candidate, Layout::from([(second, 280.)])); let moved = render(&p, owner, &candidate); assert_eq!(row(&moved, id(&p, "first")), 148.); assert_eq!(row(&moved, second), 280.); assert_eq!(row(&moved, id(&p, "nested")), 517.); assert_eq!(row(&moved, id(&p, "self_call")), 676.); assert_eq!(row(&moved, id(&p, "self_reply")), 768.); assert_eq!( line(&moved, id(&p, "first")).points, line(&baseline, id(&p, "first")).points ); for alias in ["second", "self_call", "self_reply"] { assert_eq!( line(&moved, id(&p, alias)).points.len(), line(&baseline, id(&p, alias)).points.len() ); for (old, new) in line(&baseline, id(&p, alias)) .points .iter() .zip(&line(&moved, id(&p, alias)).points) { assert_eq!(new.x, old.x); assert_eq!(new.y - old.y, 40.); } } let frame = named_box(&moved, id(&p, "fragment")).rect; assert_eq!((frame.y, frame.height), (372., 240.)); assert_eq!(named_box(&moved, id(&p, "nested")).rect.y, 497.); same_geometry(&moved, &render(&p, owner, &candidate)); same_geometry( &moved, &render(&language::parse(&source).unwrap(), owner, &candidate), ); assert_eq!(p, before); // Entire Project, not only the steps vector. assert_eq!(language::format(&p), source); } #[test] fn nested_anchor_grows_actual_fragment_and_self_call_keeps_its_receive_offset() { let (p, owner) = fixture(); let before = p.clone(); let baseline = Scene::sequence(&p, owner, owner); let nested = id(&p, "nested"); let candidate = drag(&Layout::new(), &baseline, nested, 30.); let moved = render(&p, owner, &candidate); let frame = named_box(&moved, id(&p, "fragment")).rect; assert_eq!((frame.y, frame.height), (332., 270.)); assert_eq!(named_box(&moved, nested).rect.y, 487.); assert_eq!(row(&moved, id(&p, "self_call")), 666.); let call = id(&p, "self_call"); let next = drag(&candidate, &moved, call, 18.); let local = render(&p, owner, &next); assert_eq!(row(&local, call), 684.); assert_eq!(line(&local, call).points.first().unwrap().y, 684.); assert_eq!(line(&local, call).points.last().unwrap().y, 708.); assert_eq!(row(&local, id(&p, "self_reply")), 776.); assert_eq!( named_box(&local, nested).rect, named_box(&moved, nested).rect ); assert_eq!(p, before); } #[test] fn zero_pointer_delta_preserves_latent_authored_intent_but_explicit_assignment_can_change_it() { let (p, owner) = fixture(); let baseline = Scene::sequence(&p, owner, owner); let first = id(&p, "first"); let second = id(&p, "second"); let original = Layout::from([(first, 148.), (second, 240.)]); let displaced = drag(&original, &baseline, first, 200.); let frame = render(&p, owner, &displaced); assert_eq!(row(&frame, second), 440.); assert_eq!(displaced[&second], 240.); let untouched = drag(&displaced, &frame, second, 0.); assert_eq!(untouched, displaced); let back = drag(&untouched, &frame, first, -200.); assert_eq!(back, original); assert_eq!(row(&render(&p, owner, &back), second), 240.); let mut explicit = displaced.clone(); explicit.insert(second, 440.); // Explicit SetDesiredY, not a pointer gesture. let explicit_back = drag(&explicit, &frame, first, -200.); assert_eq!(row(&render(&p, owner, &explicit_back), second), 440.); } #[test] fn source_reorder_keeps_override_bound_to_occurrence_identity_and_does_not_write_back_projection() { let (p, owner) = fixture(); let input = include_str!("fixture.arcadia"); let reordered_source = input.replace("[@first, @second, @fragment", "[@second, @first, @fragment"); assert_ne!(input, reordered_source); let reordered = language::parse(&reordered_source).unwrap(); let before = reordered.clone(); let second = id(&p, "second"); let first = id(&p, "first"); assert_eq!(id(&reordered, "second"), second); let scene = render(&reordered, owner, &Layout::from([(second, 280.)])); assert_eq!(row(&scene, second), 280.); assert_eq!(row(&scene, first), 372.); assert_eq!( &reordered.elements[&owner].steps().unwrap()[..2], &[second, first] ); assert_eq!(reordered, before); assert_eq!( language::parse(&language::format(&reordered)).unwrap(), before ); } #[test] fn current_floor_is_an_explicit_counterexample_to_general_free_positioning() { let (p, owner) = fixture(); let baseline = Scene::sequence(&p, owner, owner); let second = id(&p, "second"); let requested = drag(&Layout::new(), &baseline, second, -40.); assert_eq!(requested[&second], 200.); assert_eq!(row(&render(&p, owner, &requested), second), 240.); // This expected mismatch records a limitation; it is not acceptance of // silently clamped product gestures or proof that y200 is semantically bad. } #[test] fn scoped_hook_is_inactive_by_default_thread_local_and_restored_after_panic() { let (p, owner) = fixture(); let first = id(&p, "first"); let baseline = Scene::sequence(&p, owner, owner); let caught = std::panic::catch_unwind(|| { let _scope = AnchorScope::install(&Layout::from([(first, 900.)])); assert_eq!(row(&Scene::sequence(&p, owner, owner), first), 900.); // Another test thread has no access to this thread's temporary anchors. std::thread::spawn(|| { let (p, owner) = fixture(); assert_eq!( row(&Scene::sequence(&p, owner, owner), id(&p, "first")), 148. ); }) .join() .unwrap(); panic!("exercise scoped restoration"); }); let payload = caught.expect_err("the deliberate scope-restoration panic must occur"); assert_eq!( payload.downcast_ref::<&str>().copied(), Some("exercise scoped restoration"), "an earlier failed assertion must not masquerade as the expected panic" ); same_geometry(&baseline, &Scene::sequence(&p, owner, owner)); }