use crate::interactive::tui::transcript::MAX_BLOCKS;
use crate::interactive::tui::transcript::{Block, BlockKind, Transcript, chapters};
const W: usize = 80;
const H: usize = 5;
fn view_texts(t: &Transcript, height: usize) -> Vec<String> {
t.view(W, height)
.iter()
.map(|row| row.text.clone())
.collect()
}
fn write_request(path: &str) -> serde_json::Value {
serde_json::json!({"path": path})
}
fn flush(t: &mut Transcript) {
t.flush_tool_buffer(
|name, args| vec![format!("→ {name} {args}")],
|name, summary| format!("✓ {name}: {summary}"),
);
}
fn scrolled_history() -> Transcript {
let mut t = Transcript::new();
for i in 0..20 {
t.push(BlockKind::Assistant, format!("history line {i}"));
}
t.scroll_up(15, W, H);
assert!(
!t.is_following_tail(),
"precondition: the reader scrolled up"
);
assert_eq!(
view_texts(&t, H),
["SAYA", "history line 10", "SAYA", "history line 11", "SAYA",],
"precondition: the window sits on painted rows 20…24"
);
t
}
fn scrolled_history_with_run() -> Transcript {
let mut t = scrolled_history();
t.buffer_tool_request("workspace_write".into(), write_request("north.md"), None);
t.buffer_tool_request("workspace_write".into(), write_request("south.md"), None);
assert!(t.buffer_tool_completion("workspace_write", "north.md written"));
assert!(t.buffer_tool_completion("workspace_write", "south.md written"));
assert!(
t.blocks()
.last()
.is_some_and(|b| b.text.contains("✓ workspace_write")),
"precondition: the completed run sits at the tail, below the window"
);
t
}
#[test]
fn a_group_collapsing_below_the_viewport_does_not_move_the_reader() {
let mut t = scrolled_history_with_run();
let before = view_texts(&t, H);
flush(&mut t);
let last = t.blocks().last().expect("blocks remain");
assert!(
last.is_collapsible() && last.text.contains("2 tool calls"),
"precondition: the run folded into one summary below the window: {}",
last.text
);
assert_eq!(
view_texts(&t, H),
before,
"the collapse below the window must not move the visible rows"
);
}
#[test]
fn a_discarded_tool_buffer_below_the_viewport_does_not_move_the_reader() {
let mut t = scrolled_history_with_run();
let before = view_texts(&t, H);
t.discard_tool_buffer();
assert!(
t.blocks()
.last()
.is_some_and(|b| b.text.contains("history line 19")),
"precondition: the run's live rows were discarded from the tail"
);
assert_eq!(
view_texts(&t, H),
before,
"the discard below the window must not move the visible rows"
);
}
#[test]
fn prefix_eviction_does_not_double_adjust() {
let mut t = Transcript::new();
for i in 0..MAX_BLOCKS {
t.push(BlockKind::User, format!("m{i}"));
}
let _ = view_texts(&t, H);
t.scroll_up(15, W, H);
assert!(
!t.is_following_tail(),
"precondition: the reader scrolled up"
);
let before = view_texts(&t, H);
t.push(BlockKind::User, "one more");
assert_eq!(
t.blocks().first().map(|b| b.text.as_str()),
Some("m1"),
"precondition: the bound evicted rows off the front"
);
assert_eq!(
view_texts(&t, H),
before,
"front eviction must land the window on the same logical rows"
);
}
#[test]
fn a_shrink_and_an_append_in_one_mutation_net_correctly() {
let mut t = scrolled_history();
let before = view_texts(&t, H);
let anchor = t.scroll_up;
t.mutate(|t| {
t.blocks.pop();
t.blocks.pop();
t.blocks.pop();
let chapter = chapters::chapter_for(&t.blocks, BlockKind::Tool);
t.blocks.push(Block {
kind: BlockKind::Tool,
text: "tail work landed".to_string(),
chapter,
group: None,
});
});
assert_eq!(
t.blocks().last().map(|b| b.text.as_str()),
Some("tail work landed"),
"precondition: the mutation both removed and added below the window"
);
assert_eq!(
view_texts(&t, H),
before,
"the net shrink must not move the visible rows"
);
assert_eq!(
t.scroll_up,
anchor - 4,
"one net delta: six rows out, two rows in"
);
}
#[test]
fn tail_follow_is_unchanged_by_a_collapse() {
let mut t = Transcript::new();
for i in 0..20 {
t.push(BlockKind::Assistant, format!("history line {i}"));
}
t.buffer_tool_request("workspace_write".into(), write_request("north.md"), None);
t.buffer_tool_request("workspace_write".into(), write_request("south.md"), None);
assert!(t.buffer_tool_completion("workspace_write", "north.md written"));
assert!(t.buffer_tool_completion("workspace_write", "south.md written"));
let _ = view_texts(&t, H);
assert!(
t.is_following_tail(),
"precondition: watching the live tail"
);
flush(&mut t);
assert!(t.is_following_tail(), "the tail anchor must stay at zero");
assert_eq!(t.unseen_new_rows(), 0, "nothing is counted at the tail");
let shown = view_texts(&t, H);
assert!(
shown.iter().any(|l| l.contains("2 tool calls")),
"the folded summary must surface on the very next render: {shown:?}"
);
}
#[test]
fn unseen_rows_does_not_rise_on_a_collapse() {
let mut t = scrolled_history_with_run();
let counted = t.unseen_new_rows();
assert_eq!(
counted, 8,
"precondition: the run streamed below the scrolled reader, four \
blocks of two rows each"
);
flush(&mut t);
let after = t.unseen_new_rows();
assert!(
after <= counted,
"a collapsing group is not new activity: {after} > {counted}"
);
assert_eq!(
after, 2,
"the fold removed six of the eight counted rows (the run's live rows \
folded into one two-row block), so the count falls with them"
);
let (total, start) = t.scroll_metrics(W, H);
assert!(
after <= total - start - H,
"the count must never exceed the rows actually below the window: \
{after} > {}",
total - start - H
);
}