diff --git a/src/copilot_usage/models.py b/src/copilot_usage/models.py index 2e14ee8..a8d84ed 100644 --- a/src/copilot_usage/models.py +++ b/src/copilot_usage/models.py @@ -124,7 +124,7 @@ class AssistantMessageData(BaseModel): reasoningText: str | None = None reasoningOpaque: str | None = None toolRequests: list[dict[str, object]] = Field( - default_factory=lambda: list[dict[str, object]]() + default_factory=list[dict[str, object]] ) diff --git a/src/copilot_usage/report.py b/src/copilot_usage/report.py index 0d55787..29c2eef 100644 --- a/src/copilot_usage/report.py +++ b/src/copilot_usage/report.py @@ -111,6 +111,16 @@ def _estimated_output_tokens(session: SessionSummary) -> int: return sum(m.usage.outputTokens for m in session.model_metrics.values()) +def _format_session_running_time(session: SessionSummary) -> str: + """Return a human-readable running time for *session*. + + Returns ``"—"`` when the session has no ``start_time``. + """ + if not session.start_time: + return "—" + return _format_elapsed_since(session.last_resume_time or session.start_time) + + def render_live_sessions(sessions: list[SessionSummary]) -> None: """Render overview of active sessions only. @@ -144,11 +154,7 @@ def render_live_sessions(sessions: list[SessionSummary]) -> None: short_id = s.session_id[:8] if s.session_id else "—" name = s.name or "—" model = s.model or "—" - running = ( - _format_elapsed_since(s.last_resume_time or s.start_time) - if s.start_time - else "—" - ) + running = _format_session_running_time(s) messages = str(s.user_messages) tokens = f"{_estimated_output_tokens(s):,}" cwd = s.cwd or "—" @@ -580,10 +586,9 @@ def _render_totals(console: Console, sessions: list[SessionSummary]) -> None: total_duration = sum(s.total_api_duration_ms for s in sessions) total_sessions = len(sessions) - total_output = 0 - for s in sessions: - for mm in s.model_metrics.values(): - total_output += mm.usage.outputTokens + total_output = sum( + mm.usage.outputTokens for s in sessions for mm in s.model_metrics.values() + ) pr_label = "premium request" if total_premium == 1 else "premium requests" session_label = "session" if total_sessions == 1 else "sessions" @@ -788,11 +793,7 @@ def _render_active_section( for s in active: name = s.name or s.session_id[:12] model = s.model or "—" - running = ( - _format_elapsed_since(s.last_resume_time or s.start_time) - if s.start_time - else "—" - ) + running = _format_session_running_time(s) table.add_row( name, @@ -847,9 +848,9 @@ def render_cost_view( for premium and the active model calls / output tokens. """ console = target_console or Console() - sessions = _filter_sessions(sessions, since, until) + filtered = _filter_sessions(sessions, since, until) - if not sessions: + if not filtered: console.print("[yellow]No sessions found.[/yellow]") return @@ -866,7 +867,7 @@ def render_cost_view( grand_model_calls = 0 grand_output = 0 - for s in sessions: + for s in filtered: name = s.name or s.session_id[:12] model_calls_display = str(s.model_calls) diff --git a/tests/copilot_usage/test_report.py b/tests/copilot_usage/test_report.py index 4f12f30..0b16ca5 100644 --- a/tests/copilot_usage/test_report.py +++ b/tests/copilot_usage/test_report.py @@ -27,6 +27,7 @@ _filter_sessions, _format_detail_duration, _format_relative_time, + _format_session_running_time, format_duration, format_tokens, render_cost_view, @@ -96,6 +97,43 @@ def _capture_print(*args: object, **kwargs: object) -> None: # --------------------------------------------------------------------------- +class TestFormatSessionRunningTime: + """Tests for _format_session_running_time helper.""" + + def test_returns_dash_when_start_time_is_none(self) -> None: + session = _make_session(start_time=None) + assert _format_session_running_time(session) == "—" + + def test_uses_start_time_when_no_last_resume_time(self) -> None: + now = datetime.now(tz=UTC) + session = _make_session(start_time=now - timedelta(minutes=5)) + session.last_resume_time = None + result = _format_session_running_time(session) + assert "5m" in result + + def test_uses_last_resume_time_when_present(self) -> None: + now = datetime.now(tz=UTC) + session = _make_session(start_time=now - timedelta(hours=2)) + session.last_resume_time = now - timedelta(minutes=3) + result = _format_session_running_time(session) + assert "3m" in result + assert "h" not in result + + def test_delegates_to_format_elapsed_since(self) -> None: + now = datetime.now(tz=UTC) + start = now - timedelta(minutes=7) + session = _make_session(start_time=start) + session.last_resume_time = None + sentinel = "7m 00s" + with patch( + "copilot_usage.report._format_elapsed_since", + return_value=sentinel, + ) as mock_fmt: + result = _format_session_running_time(session) + mock_fmt.assert_called_once_with(start) + assert result == sentinel + + class TestRenderLiveSessions: """Tests for render_live_sessions."""