use std::collections::HashSet;
use serde_json::json;
use crate::git_tree::Framing;
use crate::steps_view::render::{StepTab, render};
use crate::steps_view::{Doc, StepDetail, StepSummary, StepsView, ToolIo};
fn screen() -> egui::RawInput {
egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::vec2(1024.0, 4096.0),
)),
..Default::default()
}
}
fn collect_text(shape: &egui::Shape, out: &mut String) {
match shape {
egui::Shape::Text(t) => {
out.push_str(t.galley.text());
out.push('\n');
}
egui::Shape::Vec(shapes) => shapes.iter().for_each(|s| collect_text(s, out)),
_ => {}
}
}
pub(super) fn painted(
view: &StepsView,
selected: Option<usize>,
detail: Option<&StepDetail>,
tab: StepTab,
) -> String {
let ctx = egui::Context::default();
let mut collapsed = HashSet::new();
let out = ctx.run(screen(), |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
render(ui, view, selected, detail, tab, &mut collapsed);
});
});
let mut text = String::new();
for clipped in out.shapes {
collect_text(&clipped.shape, &mut text);
}
text
}
fn summary(seq: &str, framing: Framing) -> StepSummary {
StepSummary {
seq: seq.into(),
framing,
attempts: 2,
tokens: crate::budgets::spend_from_bytes(
br#"{"type":"usage","input_tokens":7,"output_tokens":0}"#,
),
commit: Some("abcdef1234567890".into()),
started_at: Some("t-start".into()),
ended_at: Some("t-end".into()),
auth_failed: false,
no_response: false,
}
}
#[test]
fn collect_text_descends_shape_vec_and_ignores_non_text() {
use egui::{Color32, FontId, Pos2, Stroke};
let ctx = egui::Context::default();
let mut nested: Option<egui::Shape> = None;
let _ = ctx.run(egui::RawInput::default(), |ctx| {
let galley =
ctx.fonts(|f| f.layout_no_wrap("nested".into(), FontId::default(), Color32::WHITE));
nested = Some(egui::Shape::Vec(vec![egui::Shape::Text(
egui::epaint::TextShape {
pos: Pos2::ZERO,
galley,
underline: Stroke::NONE,
fallback_color: Color32::WHITE,
override_text_color: None,
opacity_factor: 1.0,
angle: 0.0,
},
)]));
});
let mut out = String::new();
collect_text(nested.as_ref().unwrap(), &mut out);
collect_text(&egui::Shape::Noop, &mut out);
assert!(out.contains("nested"));
}
#[test]
fn empty_view_shows_placeholder() {
let text = painted(&StepsView::default(), None, None, StepTab::Meta);
assert!(text.contains("(no steps yet)"), "got:\n{text}");
}
#[test]
fn list_paints_badges_selection_and_figures() {
let view = StepsView {
steps: vec![
summary("001", Framing::Complete),
summary("002", Framing::Failed),
StepSummary {
seq: "003".into(),
framing: Framing::Killed,
attempts: 0,
tokens: crate::budgets::BudgetSpend::default(),
commit: None,
started_at: None,
ended_at: None,
auth_failed: false,
no_response: false,
},
],
};
let text = painted(&view, Some(0), None, StepTab::Meta);
assert!(text.contains('✓'), "got:\n{text}");
assert!(text.contains('✗'));
assert!(text.contains('■'));
assert!(text.contains("▶ 001"));
assert!(text.contains("2 attempts"));
assert!(text.contains("7 tok"));
assert!(text.contains("abcdef1"));
assert!(text.contains("t-start"));
assert!(text.contains("→ t-end"));
}
fn detail_fixture() -> StepDetail {
StepDetail {
seq: "001".into(),
meta: Doc::Json(json!({"commit": "c0ffee"})),
request: Doc::Json(json!({"model": "opus"})),
staging: Doc::Raw(b"raw-staging".to_vec()),
response: vec![
Doc::Json(json!({"type": "end"})),
Doc::Raw(b"bad-line".to_vec()),
],
tools: vec![
ToolIo {
tool_id: "toolu_ok".into(),
input: Doc::Json(json!({"name": "Read"})),
output: Doc::Json(json!({"exit_code": 0})),
is_error: false,
},
ToolIo {
tool_id: "toolu_err".into(),
input: Doc::Json(json!({"name": "Bash"})),
output: Doc::Raw(Vec::new()),
is_error: true,
},
],
}
}
#[test]
fn detail_tabs_render_their_records() {
let view = StepsView {
steps: vec![summary("001", Framing::Complete)],
};
let d = detail_fixture();
let meta = painted(&view, Some(0), Some(&d), StepTab::Meta);
for header in ["meta", "request", "staging", "response", "tools"] {
assert!(meta.contains(header), "missing tab {header}:\n{meta}");
}
assert!(meta.contains("commit:"), "meta tree:\n{meta}");
let request = painted(&view, Some(0), Some(&d), StepTab::Request);
assert!(request.contains("model:"));
let staging = painted(&view, Some(0), Some(&d), StepTab::Staging);
assert!(staging.contains("raw-staging"));
let response = painted(&view, Some(0), Some(&d), StepTab::Response);
assert!(response.contains("type:"));
assert!(response.contains("bad-line"));
let tools = painted(&view, Some(0), Some(&d), StepTab::Tools);
assert!(tools.contains("toolu_ok"));
assert!(tools.contains("toolu_err"));
assert!(tools.contains("input"));
assert!(tools.contains("output"));
assert!(tools.contains('✓'));
assert!(tools.contains('✗'));
}
#[test]
fn empty_drill_ins_and_absent_docs_show_their_placeholders() {
let view = StepsView {
steps: vec![summary("001", Framing::Complete)],
};
let empty = StepDetail {
seq: "001".into(),
meta: Doc::Raw(Vec::new()),
request: Doc::Raw(Vec::new()),
staging: Doc::Raw(Vec::new()),
response: Vec::new(),
tools: Vec::new(),
};
let meta = painted(&view, Some(0), Some(&empty), StepTab::Meta);
assert!(meta.contains("(absent)"), "got:\n{meta}");
let response = painted(&view, Some(0), Some(&empty), StepTab::Response);
assert!(response.contains("(no events)"));
let tools = painted(&view, Some(0), Some(&empty), StepTab::Tools);
assert!(tools.contains("(no tool calls)"));
}