use super::*;
type Split = (Vec<(usize, Record)>, Vec<crate::parse::SpineRow>);
fn spine_row(line: usize, raw: &str) -> crate::parse::SpineRow {
crate::parse::spine_record(line, raw.as_bytes()).expect("a spine row")
}
fn split_fixture() -> Split {
let records = vec![
(
1,
parse(
r#"{"type":"user","uuid":"u0","parentUuid":null,"timestamp":"2026-06-07T05:00:00.000Z","message":{"role":"user","content":"chart the lagoon"}}"#,
),
),
(
4,
parse(
r#"{"type":"user","uuid":"u1","parentUuid":"x0","timestamp":"2026-06-07T05:01:00.000Z","message":{"role":"user","content":"dredge the northern channel"}}"#,
),
),
(
6,
parse(
r#"{"type":"user","uuid":"u2","parentUuid":"x0","timestamp":"2026-06-07T05:02:00.000Z","message":{"role":"user","content":"survey the southern shoal"}}"#,
),
),
];
let spine = vec![
spine_row(
2,
r#"{"type":"assistant","uuid":"a0","parentUuid":"u0","timestamp":"2026-06-07T05:00:05.000Z"}"#,
),
spine_row(
3,
r#"{"type":"attachment","uuid":"x0","parentUuid":"a0","timestamp":"2026-06-07T05:00:50.000Z"}"#,
),
spine_row(
5,
r#"{"type":"assistant","uuid":"a1","parentUuid":"u1","timestamp":"2026-06-07T05:01:05.000Z"}"#,
),
spine_row(
7,
r#"{"type":"assistant","uuid":"a2","parentUuid":"u2","timestamp":"2026-06-07T05:02:05.000Z"}"#,
),
];
(records, spine)
}
#[test]
fn the_spine_is_merged_by_line_number_and_answers_land_in_surface_indices() {
let (records, spine) = split_fixture();
let view = ChainView::build(&records, &spine);
assert_eq!(view.survival(0), Survival::Live, "the first opener");
assert!(
matches!(view.survival(1), Survival::Abandoned { .. }),
"the rewound opener: {:?}",
view.survival(1)
);
assert_eq!(view.survival(2), Survival::Live, "the resend");
assert_eq!(view.stamp(0), TurnStamp::Live(0));
assert_eq!(view.stamp(1), TurnStamp::Abandoned { root_line: 4 });
assert_eq!(view.stamp(2), TurnStamp::Live(1));
assert_eq!(view.abandoned_openers(), &[4]);
}
#[test]
fn an_abandoned_record_still_carries_the_live_turn_it_follows() {
let (records, spine) = split_fixture();
let view = ChainView::build(&records, &spine);
assert_eq!(view.order_turn(0), 0);
assert_eq!(view.order_turn(1), 0, "the abandoned opener follows turn 0");
assert_eq!(view.order_turn(2), 1);
}
#[test]
fn the_groups_are_the_live_turns_then_one_per_abandoned_branch() {
let (records, spine) = split_fixture();
let view = ChainView::build(&records, &spine);
assert_eq!(view.turn_count(), 2, "two live turns survive the rewind");
assert_eq!(view.turns(), &[vec![0], vec![2]]);
let groups = view.groups();
assert_eq!(groups.len(), 3, "two live turns + one abandoned branch");
assert_eq!(groups[0].0, TurnStamp::Live(0));
assert_eq!(groups[1].0, TurnStamp::Live(1));
assert_eq!(groups[2].0, TurnStamp::Abandoned { root_line: 4 });
assert_eq!(groups[2].1, &[1], "the branch's only surface member");
}
#[test]
fn a_turn_opened_by_a_real_record_keeps_that_record_in_surface_space() {
let (records, spine) = split_fixture();
let view = ChainView::build(&records, &spine);
for (i, group) in view.turns().iter().enumerate() {
assert!(
!group.is_empty(),
"live turn {i} lost its opener in the back-map"
);
}
assert_eq!(view.survival(records.len()), Survival::Live);
assert_eq!(view.stamp(records.len()), TurnStamp::Live(0));
}
#[test]
fn an_all_spine_lead_group_is_kept_empty_rather_than_renumbered() {
let (_, spine) = split_fixture();
let view = ChainView::build(&[], &spine);
assert_eq!(
view.turns(),
&[Vec::<usize>::new()],
"one group, anchored on a spine row, with no surface member"
);
assert_eq!(view.turn_count(), 1);
}
#[test]
fn a_surface_holding_only_an_abandoned_opener_still_numbers_the_live_turns() {
let (records, spine) = split_fixture();
let only_abandoned = vec![records[1].clone()];
let mut full_spine = spine;
full_spine.push(spine_row(
1,
r#"{"type":"user","uuid":"u0","parentUuid":null,"timestamp":"2026-06-07T05:00:00.000Z"}"#,
));
full_spine.push(spine_row(
6,
r#"{"type":"user","uuid":"u2","parentUuid":"x0","timestamp":"2026-06-07T05:02:00.000Z"}"#,
));
full_spine.sort_by_key(crate::parse::SpineRow::line);
let view = ChainView::build(&only_abandoned, &full_spine);
assert!(
matches!(view.survival(0), Survival::Abandoned { .. }),
"the one kept record is still off the chain: {:?}",
view.survival(0)
);
assert_eq!(view.stamp(0), TurnStamp::Abandoned { root_line: 4 });
assert!(
view.turns().iter().all(Vec::is_empty),
"no live turn has a surface member here: {:?}",
view.turns()
);
assert!(
view.turn_count() >= 1,
"the live turns are still counted: {:?}",
view.turns()
);
}
#[test]
fn an_empty_spine_is_the_degenerate_case_and_still_answers() {
let (records, _) = split_fixture();
let view = ChainView::build(&records, &[]);
assert_eq!(view.turn_count(), view.turns().len());
assert!(view.abandoned_openers().len() <= 1);
}