From abe808f7af21176c3176b430ba66af2cd366b547 Mon Sep 17 00:00:00 2001 From: Sasa Junuzovic <44276455+microsasa@users.noreply.github.com> Date: Thu, 23 Apr 2026 01:28:13 -0700 Subject: [PATCH 1/3] test: cover untested branches in render_detail.py (#1058) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tests for three untested code paths: - TestBuildEventDetailsSessionShutdown: SESSION_SHUTDOWN arm of _build_event_details with non-empty type, empty type, and malformed data sub-cases - TestRenderShutdownCyclesNoneTimestamp: ts=None path in _render_shutdown_cycles renders '—' in Date column - test_truncate_non_positive_max_len_returns_empty: parametrized test for _truncate max_len <= 0 guard with values {0, -1, -100} Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/copilot_usage/test_render_detail.py | 82 +++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/tests/copilot_usage/test_render_detail.py b/tests/copilot_usage/test_render_detail.py index b28a5282..6e48872a 100644 --- a/tests/copilot_usage/test_render_detail.py +++ b/tests/copilot_usage/test_render_detail.py @@ -33,6 +33,7 @@ _render_recent_events, _render_shutdown_cycles, _safe_event_data, + _truncate, render_session_detail, ) @@ -957,3 +958,84 @@ def test_build_event_details_returns_empty_for_unrecognized_type() -> None: """Wildcard case must return '' for event types without explicit handling.""" ev = SessionEvent(type=EventType.SESSION_RESUME, data={}) assert _build_event_details(ev) == "" + + +# --------------------------------------------------------------------------- +# _build_event_details — SESSION_SHUTDOWN arm (issue #1058) +# --------------------------------------------------------------------------- + + +class TestBuildEventDetailsSessionShutdown: + """Tests for the SESSION_SHUTDOWN branch of _build_event_details.""" + + def test_non_empty_shutdown_type(self) -> None: + """Non-empty shutdownType must render as 'type='.""" + ev = SessionEvent( + type=EventType.SESSION_SHUTDOWN, + data={ + "shutdownType": "routine", + "totalPremiumRequests": 0, + "totalApiDurationMs": 0, + "modelMetrics": {}, + }, + ) + assert _build_event_details(ev) == "type=routine" + + def test_empty_shutdown_type(self) -> None: + """Empty shutdownType must render as ''.""" + ev = SessionEvent( + type=EventType.SESSION_SHUTDOWN, + data={ + "shutdownType": "", + "totalPremiumRequests": 0, + "totalApiDurationMs": 0, + "modelMetrics": {}, + }, + ) + assert _build_event_details(ev) == "" + + def test_malformed_data_returns_empty(self) -> None: + """Malformed data (int shutdownType) triggers ValidationError → ''.""" + ev = SessionEvent( + type=EventType.SESSION_SHUTDOWN, + data={"shutdownType": 99}, # int triggers ValidationError + ) + assert _build_event_details(ev) == "" + + +# --------------------------------------------------------------------------- +# _render_shutdown_cycles — None timestamp path (issue #1058) +# --------------------------------------------------------------------------- + + +class TestRenderShutdownCyclesNoneTimestamp: + """A shutdown cycle with ts=None must display '—' in the Date column.""" + + def test_none_timestamp_renders_dash(self) -> None: + """A shutdown cycle with ts=None must display '—' in the Date column.""" + sd = SessionShutdownData( + shutdownType="routine", + totalPremiumRequests=1, + totalApiDurationMs=500, + modelMetrics={}, + ) + summary = SessionSummary( + session_id="no-ts", + shutdown_cycles=[(None, sd)], + ) + buf = io.StringIO() + console = Console(file=buf, force_terminal=True, width=120) + _render_shutdown_cycles(summary, target_console=console) + output = _strip_ansi(buf.getvalue()) + assert "—" in output + + +# --------------------------------------------------------------------------- +# _truncate — max_len ≤ 0 guard (issue #1058) +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize("max_len", [0, -1, -100]) +def test_truncate_non_positive_max_len_returns_empty(max_len: int) -> None: + """_truncate must return '' for any max_len ≤ 0.""" + assert _truncate("hello", max_len) == "" From c2d652e1dbb8d0058d0f4a4046df7ecdb7418da3 Mon Sep 17 00:00:00 2001 From: Sasa Junuzovic <44276455+microsasa@users.noreply.github.com> Date: Thu, 23 Apr 2026 01:38:44 -0700 Subject: [PATCH 2/3] fix: address review comments Replace inline (StringIO, Console) setup with existing _buf_console() helper in TestRenderShutdownCyclesNoneTimestamp to reduce duplication and keep console configuration consistent across tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/copilot_usage/test_render_detail.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/copilot_usage/test_render_detail.py b/tests/copilot_usage/test_render_detail.py index 6e48872a..9bced1a1 100644 --- a/tests/copilot_usage/test_render_detail.py +++ b/tests/copilot_usage/test_render_detail.py @@ -1023,8 +1023,7 @@ def test_none_timestamp_renders_dash(self) -> None: session_id="no-ts", shutdown_cycles=[(None, sd)], ) - buf = io.StringIO() - console = Console(file=buf, force_terminal=True, width=120) + buf, console = _buf_console() _render_shutdown_cycles(summary, target_console=console) output = _strip_ansi(buf.getvalue()) assert "—" in output From e1e955dc7180e48daae8da3ae53fc3cb8f0ee0d1 Mon Sep 17 00:00:00 2001 From: Sasa Junuzovic <44276455+microsasa@users.noreply.github.com> Date: Thu, 23 Apr 2026 01:46:00 -0700 Subject: [PATCH 3/3] fix: address review comments Replace inline StringIO/Console setup in TestRenderCodeChanges with the existing _buf_console() helper to keep console configuration consistent across tests and reduce duplication. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/copilot_usage/test_render_detail.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/tests/copilot_usage/test_render_detail.py b/tests/copilot_usage/test_render_detail.py index 9bced1a1..8434d0f4 100644 --- a/tests/copilot_usage/test_render_detail.py +++ b/tests/copilot_usage/test_render_detail.py @@ -87,23 +87,20 @@ class TestRenderCodeChanges: def test_none_produces_no_output(self) -> None: """code_changes=None → returns immediately without printing.""" - buf = io.StringIO() - console = Console(file=buf, force_terminal=True) + buf, console = _buf_console() _render_code_changes(None, target_console=console) assert buf.getvalue() == "" def test_all_zero_produces_no_output(self) -> None: """All fields zero/empty → returns without printing.""" - buf = io.StringIO() - console = Console(file=buf, force_terminal=True) + buf, console = _buf_console() changes = CodeChanges(linesAdded=0, linesRemoved=0, filesModified=[]) _render_code_changes(changes, target_console=console) assert buf.getvalue() == "" def test_with_data_shows_table(self) -> None: """Non-zero code changes → renders a table with stats.""" - buf = io.StringIO() - console = Console(file=buf, force_terminal=True) + buf, console = _buf_console() changes = CodeChanges(linesAdded=10, linesRemoved=2, filesModified=["a.py"]) _render_code_changes(changes, target_console=console) output = buf.getvalue() @@ -113,8 +110,7 @@ def test_with_data_shows_table(self) -> None: def test_files_present_zero_line_counts_renders_table(self) -> None: """filesModified non-empty with zero line deltas → table IS rendered.""" - buf = io.StringIO() - console = Console(file=buf, force_terminal=True) + buf, console = _buf_console() changes = CodeChanges(linesAdded=0, linesRemoved=0, filesModified=["a.py"]) _render_code_changes(changes, target_console=console) output = _strip_ansi(buf.getvalue()) @@ -126,8 +122,7 @@ def test_files_present_zero_line_counts_renders_table(self) -> None: def test_zero_files_positive_line_counts_renders_table(self) -> None: """Empty filesModified with positive line counts → table IS rendered.""" - buf = io.StringIO() - console = Console(file=buf, force_terminal=True) + buf, console = _buf_console() changes = CodeChanges(linesAdded=5, linesRemoved=2, filesModified=[]) _render_code_changes(changes, target_console=console) output = buf.getvalue()