use crate::transcript::{AutoExpand, Block, Entry, EntryKind, Transcript, Usage};
use std::collections::HashSet;
pub(super) use crate::paint_probe::screen as input;
pub(super) fn rendered_text(t: &Transcript, raw: bool) -> String {
painted_with(t, raw, AutoExpand::default(), &mut HashSet::new())
}
pub(super) fn painted_with(
t: &Transcript,
raw: bool,
auto: AutoExpand,
folds: &mut HashSet<String>,
) -> String {
crate::paint_probe::paint(|ui| super::plain(ui, t, raw, auto, folds))
}
fn repaint_delay(t: &Transcript) -> std::time::Duration {
let ctx = egui::Context::default();
let auto = AutoExpand::default();
let mut folds = HashSet::new();
for _ in 0..2 {
let _ = ctx.run(input(), |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
super::plain(ui, t, false, auto, &mut folds);
});
});
}
ctx.run(input(), |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
super::plain(ui, t, false, auto, &mut folds);
});
})
.viewport_output
.get(&egui::ViewportId::ROOT)
.expect("root viewport output present")
.repaint_delay
}
pub(super) fn entry(kind: EntryKind) -> Entry {
Entry {
name: "001-x.json".into(),
raw: b"RAWBYTES".to_vec(),
kind,
}
}
pub(super) fn tx(entries: Vec<Entry>) -> Transcript {
Transcript { entries }
}
#[test]
fn empty_transcript_shows_placeholder() {
assert!(rendered_text(&Transcript::default(), false).contains("(no messages yet)"));
}
#[test]
fn delivered_renders_sender_and_body() {
let t = tx(vec![entry(EntryKind::Delivered {
sender: "alice".into(),
epitaph: None,
body: "hello world".into(),
})]);
let painted = rendered_text(&t, false);
assert!(painted.contains("alice:"), "got:\n{painted}");
assert!(painted.contains("hello world"));
}
#[test]
fn model_text_and_thinking_render() {
let t = tx(vec![entry(EntryKind::Model {
model_id: "opus".into(),
usage: Usage::default(),
blocks: vec![
Block::Text("visible answer".into()),
Block::Thinking("private reasoning".into()),
],
})]);
let painted = rendered_text(&t, false);
assert!(
painted.contains(&format!("{}:", super::rows::SPEAKER)),
"got:\n{painted}"
);
assert!(!painted.contains("opus:"), "got:\n{painted}");
assert!(painted.contains("visible answer"));
assert!(painted.contains("thinking:"), "got:\n{painted}");
assert!(painted.contains("private reasoning"));
}
fn tool_use_entry() -> Entry {
entry(EntryKind::Model {
model_id: "opus".into(),
usage: Usage::default(),
blocks: vec![Block::ToolUse {
id: "toolu_9".into(),
name: "Read".into(),
input_summary: r#"{"path":"/x"}"#.into(),
}],
})
}
#[test]
fn in_progress_tool_chip_pulses_and_paints() {
let t = tx(vec![tool_use_entry()]);
assert!(
repaint_delay(&t) < std::time::Duration::from_secs(1),
"in-progress tool must schedule a near-term repaint"
);
let painted = rendered_text(&t, false);
assert!(painted.contains("⚙ Read — running"), "got:\n{painted}");
assert!(painted.contains(r#"{"path":"/x"}"#));
}
#[test]
fn resolved_tool_chip_is_static() {
let t = tx(vec![
tool_use_entry(),
entry(EntryKind::ToolResult {
tool_use_id: "toolu_9".into(),
content: "file body".into(),
is_error: false,
}),
]);
assert_eq!(
repaint_delay(&t),
std::time::Duration::MAX,
"resolved tools must not pull repaints"
);
let painted = rendered_text(&t, false);
assert!(painted.contains("⚙ Read"), "got:\n{painted}");
assert!(!painted.contains("running"));
}
#[test]
fn tool_result_renders_content_and_error_glyph() {
let ok = tx(vec![entry(EntryKind::ToolResult {
tool_use_id: "t".into(),
content: "all good".into(),
is_error: false,
})]);
let ok_painted = rendered_text(&ok, false);
assert!(ok_painted.contains("all good"));
assert!(
ok_painted.contains("✔ tool result — ok"),
"got:\n{ok_painted}"
);
let err = tx(vec![entry(EntryKind::ToolResult {
tool_use_id: "t".into(),
content: "boom".into(),
is_error: true,
})]);
let err_painted = rendered_text(&err, false);
assert!(err_painted.contains("boom"));
assert!(
err_painted.contains("✖ tool result — error"),
"got:\n{err_painted}"
);
}
#[test]
fn streaming_tail_renders_live_marker() {
let t = tx(vec![entry(EntryKind::Streaming {
thinking: String::new(),
text: "partial output".into(),
})]);
let painted = rendered_text(&t, false);
assert!(painted.contains("live"));
assert!(painted.contains("partial output"));
}
#[test]
fn each_role_paints_its_stripe_and_machinery_paints_none() {
use crate::theme::{BRAZEN, BRAZEN_DIM, GATE, SPECTRE};
let delivered = |sender: &str, epitaph| EntryKind::Delivered {
sender: sender.into(),
epitaph,
body: "words".into(),
};
let t = tx(vec![
entry(delivered("user", None)),
entry(EntryKind::Model {
model_id: "opus".into(),
usage: Usage::default(),
blocks: vec![Block::Text("reply".into())],
}),
entry(delivered("peer", None)),
entry(delivered("kid", Some(crate::inboxview::Epitaph::Stopped))),
]);
let fills = crate::paint_probe::paint_fills(|ui| {
super::plain(ui, &t, false, AutoExpand::default(), &mut HashSet::new());
});
for hue in [GATE, SPECTRE, BRAZEN, BRAZEN_DIM] {
assert!(fills.contains(&hue), "a role stripe must paint {hue:?}");
}
let machinery = tx(vec![entry(EntryKind::ToolResult {
tool_use_id: "t".into(),
content: "bytes".into(),
is_error: false,
})]);
let fills = crate::paint_probe::paint_fills(|ui| {
super::plain(
ui,
&machinery,
false,
AutoExpand::default(),
&mut HashSet::new(),
);
});
for hue in [GATE, SPECTRE, BRAZEN, BRAZEN_DIM] {
assert!(!fills.contains(&hue), "machinery wears the empty seat");
}
}
#[test]
fn raw_kind_renders_bytes_in_parsed_mode() {
let painted = rendered_text(&tx(vec![entry(EntryKind::Raw)]), false);
assert!(painted.contains("RAWBYTES"), "got:\n{painted}");
assert!(painted.contains("001-x.json"));
}
#[test]
fn raw_toggle_shows_verbatim_bytes_over_parsed() {
let t = tx(vec![entry(EntryKind::Model {
model_id: "opus".into(),
usage: Usage::default(),
blocks: vec![Block::Text("parsed answer".into())],
})]);
let parsed = rendered_text(&t, false);
assert!(parsed.contains("parsed answer"));
assert!(!parsed.contains("RAWBYTES"));
let raw = rendered_text(&t, true);
assert!(raw.contains("RAWBYTES"), "got:\n{raw}");
assert!(raw.contains("001-x.json"));
assert!(!raw.contains("parsed answer"));
}