From 4a475d88a7ea0a99e8713bf710d791fa738b108d Mon Sep 17 00:00:00 2001 From: Sam & Claude Date: Wed, 24 Jun 2026 14:26:47 +0200 Subject: [PATCH 1/3] =?UTF-8?q?test(tui):=20add=20TestBackend=20render=20t?= =?UTF-8?q?ests=20=E2=80=94=20connecting,=20snapshot,=20no-panic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the 'compiles but never verified to draw' gap: - render_connecting_state_shows_connecting_text — asserts 'connecting…' and 'colibri-harness' title render before daemon connects - render_with_snapshot_shows_panes_and_agent — asserts pane id, agent name, state label, and state icon appear in rendered buffer - render_does_not_panic_on_empty_snapshot — smoke test for the snapshot=None path All three use ratatui::TestBackend (no terminal needed, CI-friendly). --- crates/colibri-glasspane-tui/src/main.rs | 86 ++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/crates/colibri-glasspane-tui/src/main.rs b/crates/colibri-glasspane-tui/src/main.rs index f20edb0..86b8c46 100644 --- a/crates/colibri-glasspane-tui/src/main.rs +++ b/crates/colibri-glasspane-tui/src/main.rs @@ -724,4 +724,90 @@ 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; + } } -- 2.45.3 From ac0a77c82c134991c46690535732787ea130d518 Mon Sep 17 00:00:00 2001 From: Sam & Claude Date: Wed, 24 Jun 2026 14:29:18 +0200 Subject: [PATCH 2/3] force hook run -- 2.45.3 From 57e3f30f9c89c69910977b559dfb2f66b09fd998 Mon Sep 17 00:00:00 2001 From: Sam & Claude Date: Wed, 24 Jun 2026 14:45:30 +0200 Subject: [PATCH 3/3] test(tui): add stalled-pane + tiny-terminal render tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two more TestBackend render tests on top of the connecting/populated/empty set: - render_stalled_pane_shows_warning_icon — covers the distinct stalled branch (state_icon → ⚠), which the healthy-Working test (●) didn't exercise. - render_does_not_panic_on_tiny_terminal — renders at 20x5 to guard against cramped-layout panics (a classic ratatui footgun). 10 tests pass; fmt clean. --- crates/colibri-glasspane-tui/src/main.rs | 49 ++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/crates/colibri-glasspane-tui/src/main.rs b/crates/colibri-glasspane-tui/src/main.rs index 86b8c46..e28ad95 100644 --- a/crates/colibri-glasspane-tui/src/main.rs +++ b/crates/colibri-glasspane-tui/src/main.rs @@ -810,4 +810,53 @@ mod tests { // 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); + } } -- 2.45.3