use std::collections::HashSet;
use crate::git_tree::Framing;
use crate::steps_view::render::{StepTab, render};
use crate::steps_view::{Doc, Orphan, StepDetail, StepSummary, StepsView, ToolIo, UNPARSED, Wound};
pub(super) fn painted(
view: &StepsView,
selected: Option<usize>,
detail: Option<&StepDetail>,
tab: StepTab,
) -> String {
let mut collapsed = HashSet::new();
crate::paint_probe::paint(|ui| {
render(ui, view, selected, detail, tab, &mut collapsed, false);
})
}
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: crate::login::auth::AuthFailure::No,
wound: Wound::None,
}
}
#[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 {
orphan: Orphan::default(),
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: crate::login::auth::AuthFailure::No,
wound: Wound::None,
},
],
};
let text = painted(&view, Some(0), None, StepTab::Meta);
assert!(text.contains("✔ complete"), "got:\n{text}");
assert!(text.contains("✖ failed"), "got:\n{text}");
assert!(text.contains("■ no clean end"), "got:\n{text}");
assert!(text.contains("▶ 001"));
assert!(text.contains('2'));
assert!(text.contains('7'));
assert!(text.contains("abcdef1"));
assert!(text.contains("t-start"));
assert!(text.contains("t-end"));
}
#[test]
fn every_column_is_headed_and_explained() {
use crate::steps_view::columns::COLUMNS;
let view = StepsView {
steps: vec![summary("001", Framing::Complete)],
orphan: Orphan::default(),
};
let text = painted(&view, Some(0), None, StepTab::Meta);
let mut headers = std::collections::HashSet::new();
for column in COLUMNS {
assert!(
text.contains(column.header),
"unlabelled column {:?}:\n{text}",
column.header
);
assert!(
headers.insert(column.header),
"duplicate {:?}",
column.header
);
assert!(!column.hint.is_empty(), "unexplained {:?}", column.header);
}
let empty = painted(&StepsView::default(), None, None, StepTab::Meta);
assert!(
!empty.contains("Attempts"),
"headers over nothing:\n{empty}"
);
}
#[test]
fn every_framing_says_itself_in_distinct_words() {
use crate::steps_view::render::{framing_badge, summary_badge};
let mut phrases = std::collections::HashSet::new();
for framing in [Framing::Complete, Framing::Failed, Framing::Killed] {
let (glyph, _, phrase) = framing_badge(framing);
assert!(!phrase.is_empty(), "unsaid framing {framing:?}");
assert!(
phrases.insert(phrase),
"duplicate phrase {phrase:?} for {framing:?}"
);
assert!(!glyph.is_empty(), "badgeless framing {framing:?}");
}
let wound = summary_badge(&StepSummary {
wound: Wound::Mute,
..summary("001", Framing::Killed)
});
let failed = framing_badge(Framing::Failed);
assert_eq!((wound.0, wound.1), (failed.0, failed.1));
assert!(phrases.insert(wound.2), "the wound speaks for itself");
}
fn detail_fixture() -> StepDetail {
StepDetail {
seq: "001".into(),
meta: Doc::of_bytes(br#"{"commit": "c0ffee"}"#.to_vec()),
request: Doc::of_bytes(br#"{"model": "opus"}"#.to_vec()),
staging: Doc::Unparsed(b"raw-staging".to_vec()),
response: vec![
Doc::of_bytes(br#"{"type": "end"}"#.to_vec()),
Doc::Unparsed(b"bad-line".to_vec()),
],
tools: vec![
ToolIo {
tool_id: "toolu_ok".into(),
input: Doc::of_bytes(br#"{"name": "Read"}"#.to_vec()),
output: Doc::of_bytes(br#"{"exit_code": 0}"#.to_vec()),
is_error: false,
},
ToolIo {
tool_id: "toolu_err".into(),
input: Doc::of_bytes(br#"{"name": "Bash"}"#.to_vec()),
output: Doc::Absent,
is_error: true,
},
],
stderr: None,
driver: None,
}
}
#[test]
fn detail_tabs_render_their_records() {
let view = StepsView {
steps: vec![summary("001", Framing::Complete)],
orphan: Orphan::default(),
};
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("Records:"), "unlabelled picker:\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"));
assert!(staging.contains(UNPARSED), "error row missing:\n{staging}");
let response = painted(&view, Some(0), Some(&d), StepTab::Response);
assert!(response.contains("type:"));
assert!(response.contains("bad-line"));
assert!(response.contains(UNPARSED));
let tools = painted(&view, Some(0), Some(&d), StepTab::Tools);
assert!(tools.contains("call"), "unlabelled tool id:\n{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('✖'));
let (_, _, ok) = crate::theme::tool_result_badge(false);
let (_, _, err) = crate::theme::tool_result_badge(true);
assert!(tools.contains(ok), "unsaid ok outcome:\n{tools}");
assert!(tools.contains(err), "unsaid error outcome:\n{tools}");
}
#[test]
fn empty_drill_ins_and_absent_docs_show_their_placeholders() {
let view = StepsView {
steps: vec![summary("001", Framing::Complete)],
orphan: Orphan::default(),
};
let empty = StepDetail {
seq: "001".into(),
meta: Doc::Absent,
request: Doc::Absent,
staging: Doc::Absent,
response: Vec::new(),
tools: Vec::new(),
stderr: None,
driver: None,
};
let meta = painted(&view, Some(0), Some(&empty), StepTab::Meta);
assert!(meta.contains("(absent)"), "got:\n{meta}");
assert!(!meta.contains(UNPARSED), "absent ≠ unparseable:\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)"));
}