diff --git a/crates/colibri-glasspane-tui/src/main.rs b/crates/colibri-glasspane-tui/src/main.rs index f20edb0..e28ad95 100644 --- a/crates/colibri-glasspane-tui/src/main.rs +++ b/crates/colibri-glasspane-tui/src/main.rs @@ -724,4 +724,139 @@ mod tests { assert_eq!(app.sessions, vec!["s1", "s2"]); assert_eq!(app.session_filter.as_deref(), Some("s1")); } + + // ── render tests (TestBackend) ── + + /// Render into a TestBackend buffer and return all visible text as a + /// single string — useful for content assertions without pinning exact + /// cell positions. + fn render_text(app: &mut App, width: u16, height: u16) -> String { + use ratatui::backend::TestBackend; + let backend = TestBackend::new(width, height); + let mut terminal = Terminal::new(backend).expect("terminal creation"); + terminal + .draw(|f| app.render(f)) + .expect("render should not panic"); + let buf = terminal.backend().buffer(); + let mut lines: Vec = Vec::new(); + for y in 0..buf.area.height { + let mut line = String::new(); + for x in 0..buf.area.width { + let cell = buf.cell((x, y)).expect("cell in bounds"); + line.push_str(cell.symbol()); + } + let trimmed = line.trim_end().to_string(); + if !trimmed.is_empty() { + lines.push(trimmed); + } + } + lines.join("\n") + } + + #[test] + fn render_connecting_state_shows_connecting_text() { + let mut app = App::new(PathBuf::from("/tmp/nonexistent.sock")); + let text = render_text(&mut app, 80, 24); + assert!( + text.contains("connecting…"), + "expected 'connecting…' in: {text}" + ); + assert!( + text.contains("colibri-harness"), + "expected 'colibri-harness' title in: {text}" + ); + } + + #[test] + fn render_with_snapshot_shows_panes_and_agent() { + let snap = GlasspaneSnapshot::new( + "osa", + "2026-06-24T12:00:00Z", + vec![colibri_glasspane::Pane { + id: "pane-1".into(), + agent: "zot".into(), + state: AgentState::Working, + session_id: Some("s1".into()), + last_event_at: Some("2026-06-24T12:00:01Z".into()), + cwd: Some("/home/clawdie".into()), + stalled: false, + }], + ); + let mut app = App::new(PathBuf::from("/tmp/nonexistent.sock")); + app.snapshot = Some(snap); + app.rebuild_session_list(); + let text = render_text(&mut app, 80, 24); + + assert!( + text.contains("colibri-harness"), + "expected title in: {text}" + ); + assert!(text.contains("host: osa"), "expected host in: {text}"); + assert!(text.contains("pane-1"), "expected pane id in: {text}"); + assert!(text.contains("zot"), "expected agent name in: {text}"); + assert!( + text.contains("Working"), + "expected state 'Working' in: {text}" + ); + // State icon for Working is ● + assert!(text.contains("●"), "expected working icon in: {text}"); + } + + #[test] + fn render_does_not_panic_on_empty_snapshot() { + let mut app = App::new(PathBuf::from("/tmp/nonexistent.sock")); + // After refresh, snapshot is still None → "No data" path + let text = render_text(&mut app, 80, 24); + // Must not have panicked; content is fine either way + let _ = text; + } + + #[test] + fn render_stalled_pane_shows_warning_icon() { + // Stalled is a distinct render branch (state_icon → "⚠", magenta). + let snap = GlasspaneSnapshot::new( + "osa", + "2026-06-24T12:00:00Z", + vec![colibri_glasspane::Pane { + id: "pane-stalled".into(), + agent: "zot".into(), + state: AgentState::Working, + session_id: Some("s1".into()), + last_event_at: Some("2026-06-24T12:00:01Z".into()), + cwd: Some("/home/clawdie".into()), + stalled: true, + }], + ); + let mut app = App::new(PathBuf::from("/tmp/nonexistent.sock")); + app.snapshot = Some(snap); + app.rebuild_session_list(); + let text = render_text(&mut app, 80, 24); + + assert!(text.contains("pane-stalled"), "expected pane id in: {text}"); + // Stalled icon is ⚠ (not the ● of a healthy Working pane). + assert!(text.contains("⚠"), "expected stalled icon in: {text}"); + } + + #[test] + fn render_does_not_panic_on_tiny_terminal() { + // Cramped layouts are a classic ratatui panic source — the draw must + // still succeed (render_text's .expect would fail if it panicked). + let snap = GlasspaneSnapshot::new( + "osa", + "2026-06-24T12:00:00Z", + vec![colibri_glasspane::Pane { + id: "p".into(), + agent: "zot".into(), + state: AgentState::Working, + session_id: Some("s1".into()), + last_event_at: None, + cwd: None, + stalled: false, + }], + ); + let mut app = App::new(PathBuf::from("/tmp/nonexistent.sock")); + app.snapshot = Some(snap); + app.rebuild_session_list(); + let _ = render_text(&mut app, 20, 5); + } }