Files
ARCADIA-Studio/harness/experiments/behavior-layout-scene-20260912/mod.rs
T

346 lines
12 KiB
Rust

//! 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<Id, f32>;
thread_local! {
static ANCHORS: RefCell<Option<Layout>> = const { RefCell::new(None) };
}
struct AnchorScope(Option<Layout>);
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::<Vec<_>>()
};
let boxes = |s: &Scene| {
s.boxes
.iter()
.map(|b| (b.id, b.rect, b.title.clone(), b.detail.clone(), b.marker))
.collect::<Vec<_>>()
};
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::<Vec<_>>()
};
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));
}