From e3f004b9ec6967631a823c3cbcdf11518ff69e47 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 20 Mar 2026 08:07:07 +0000 Subject: [PATCH 1/6] fix: populate active_* fields for pure active sessions (#154) Pure active sessions (no shutdown) were not populating active_model_calls, active_user_messages, and active_output_tokens, causing the interactive home view and cost view to show zero activity. Set active_model_calls=total_turn_starts, active_user_messages=user_message_count, and active_output_tokens=total_output_tokens in the active (no shutdown) code path of build_session_summary. Added unit tests in test_parser.py and test_report.py, plus an e2e fixture and test class to verify non-zero activity is displayed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/copilot_usage/parser.py | 3 ++ tests/copilot_usage/test_parser.py | 21 ++++++++ tests/copilot_usage/test_report.py | 27 ++++++++++ .../fixtures/pure-active-session/events.jsonl | 10 ++++ tests/e2e/test_e2e.py | 49 +++++++++++++++++-- 5 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 tests/e2e/fixtures/pure-active-session/events.jsonl diff --git a/src/copilot_usage/parser.py b/src/copilot_usage/parser.py index 7cd7072..bd2f551 100644 --- a/src/copilot_usage/parser.py +++ b/src/copilot_usage/parser.py @@ -334,6 +334,9 @@ def build_session_summary( model_calls=total_turn_starts, user_messages=user_message_count, is_active=True, + active_model_calls=total_turn_starts, + active_user_messages=user_message_count, + active_output_tokens=total_output_tokens, ) diff --git a/tests/copilot_usage/test_parser.py b/tests/copilot_usage/test_parser.py index 4578718..795950f 100644 --- a/tests/copilot_usage/test_parser.py +++ b/tests/copilot_usage/test_parser.py @@ -515,6 +515,27 @@ def test_last_resume_time_none_for_active(self, tmp_path: Path) -> None: summary = build_session_summary(events, session_dir=sdir) assert summary.last_resume_time is None + def test_active_fields_populated(self, tmp_path: Path) -> None: + """Pure active session populates active_model_calls/user_messages/output_tokens.""" + p = tmp_path / "s" / "events.jsonl" + _write_events( + p, + _START_EVENT, + _USER_MSG, + _TURN_START_1, + _ASSISTANT_MSG, + _USER_MSG, # second user message (same fixture reused) + _TURN_START_2, + _ASSISTANT_MSG_2, + _TOOL_EXEC, + ) + events = parse_events(p) + summary = build_session_summary(events, session_dir=p.parent) + assert summary.is_active is True + assert summary.active_model_calls == 2 + assert summary.active_user_messages == 2 + assert summary.active_output_tokens == 350 # 150 + 200 + # --------------------------------------------------------------------------- # build_session_summary — resumed session (shutdown followed by more events) diff --git a/tests/copilot_usage/test_report.py b/tests/copilot_usage/test_report.py index 6615e18..af79b63 100644 --- a/tests/copilot_usage/test_report.py +++ b/tests/copilot_usage/test_report.py @@ -1262,6 +1262,33 @@ def test_active_section_uses_last_resume_time(self) -> None: # Should show minutes (from last_resume_time), NOT days (from start_time) assert "3d" not in output and "72h" not in output + def test_active_section_shows_nonzero_activity(self) -> None: + """Active section renders the actual active_* field values, not zero.""" + now = datetime.now(tz=UTC) + session = SessionSummary( + session_id="pure-active-abcdef", + name="Pure Active", + model="claude-sonnet-4", + start_time=now - timedelta(minutes=10), + is_active=True, + user_messages=4, + model_calls=3, + active_model_calls=3, + active_user_messages=4, + active_output_tokens=1500, + model_metrics={ + "claude-sonnet-4": ModelMetrics( + usage=TokenUsage(outputTokens=1500), + ) + }, + ) + output = _capture_full_summary([session]) + assert "Active Sessions" in output + # The active section should display the non-zero counts + assert "3" in output # active_model_calls + assert "4" in output # active_user_messages + assert "1.5K" in output # active_output_tokens (format_tokens) + # --------------------------------------------------------------------------- # render_cost_view capture helper diff --git a/tests/e2e/fixtures/pure-active-session/events.jsonl b/tests/e2e/fixtures/pure-active-session/events.jsonl new file mode 100644 index 0000000..8ab6b56 --- /dev/null +++ b/tests/e2e/fixtures/pure-active-session/events.jsonl @@ -0,0 +1,10 @@ +{"type":"session.start","data":{"sessionId":"pure-active-0000-0000-000000000001","version":1,"producer":"copilot-agent","copilotVersion":"1.0.2","startTime":"2026-03-07T09:00:00.000Z","context":{"cwd":"/Users/testuser/active-project"}},"id":"pa-start","timestamp":"2026-03-07T09:00:00.000Z","parentId":null} +{"type":"user.message","data":{"content":"First user message.","transformedContent":"First user message.","attachments":[],"interactionId":"pa-int-1"},"id":"pa-user1","timestamp":"2026-03-07T09:01:00.000Z","parentId":"pa-start"} +{"type":"assistant.turn_start","data":{"turnId":"0","interactionId":"pa-int-1"},"id":"pa-turn-start-1","timestamp":"2026-03-07T09:01:01.000Z","parentId":"pa-user1"} +{"type":"assistant.message","data":{"messageId":"pa-msg-1","content":"First assistant reply.","toolRequests":[],"interactionId":"pa-int-1","outputTokens":300},"id":"pa-asst1","timestamp":"2026-03-07T09:01:10.000Z","parentId":"pa-turn-start-1"} +{"type":"assistant.turn_end","data":{"turnId":"0"},"id":"pa-turn-end-1","timestamp":"2026-03-07T09:01:11.000Z","parentId":"pa-asst1"} +{"type":"user.message","data":{"content":"Second user message.","transformedContent":"Second user message.","attachments":[],"interactionId":"pa-int-2"},"id":"pa-user2","timestamp":"2026-03-07T09:02:00.000Z","parentId":"pa-turn-end-1"} +{"type":"assistant.turn_start","data":{"turnId":"1","interactionId":"pa-int-2"},"id":"pa-turn-start-2","timestamp":"2026-03-07T09:02:01.000Z","parentId":"pa-user2"} +{"type":"assistant.message","data":{"messageId":"pa-msg-2","content":"Second assistant reply.","toolRequests":[],"interactionId":"pa-int-2","outputTokens":400},"id":"pa-asst2","timestamp":"2026-03-07T09:02:10.000Z","parentId":"pa-turn-start-2"} +{"type":"tool.execution_start","data":{"toolCallId":"pa-tc-1","toolName":"bash","arguments":{"command":"echo hello"}},"id":"pa-tool-start","timestamp":"2026-03-07T09:02:11.000Z","parentId":"pa-asst2"} +{"type":"tool.execution_complete","data":{"toolCallId":"pa-tc-1","model":"claude-sonnet-4","interactionId":"pa-int-2","success":true},"id":"pa-tool-end","timestamp":"2026-03-07T09:02:12.000Z","parentId":"pa-tool-start"} diff --git a/tests/e2e/test_e2e.py b/tests/e2e/test_e2e.py index 3cf6e1a..c693432 100644 --- a/tests/e2e/test_e2e.py +++ b/tests/e2e/test_e2e.py @@ -30,7 +30,7 @@ class TestSummaryE2E: def test_finds_eight_sessions(self) -> None: result = CliRunner().invoke(main, ["summary", "--path", str(FIXTURES)]) assert result.exit_code == 0 - assert "8 sessions" in result.output + assert "9 sessions" in result.output def test_total_premium_requests(self) -> None: result = CliRunner().invoke(main, ["summary", "--path", str(FIXTURES)]) @@ -62,12 +62,12 @@ def test_date_filtering_excludes_sessions(self) -> None: def test_model_calls_shown(self) -> None: result = CliRunner().invoke(main, ["summary", "--path", str(FIXTURES)]) assert result.exit_code == 0 - assert "20 model calls" in result.output + assert "22 model calls" in result.output def test_user_messages_shown(self) -> None: result = CliRunner().invoke(main, ["summary", "--path", str(FIXTURES)]) assert result.exit_code == 0 - assert "14 user messages" in result.output + assert "16 user messages" in result.output # --------------------------------------------------------------------------- @@ -183,7 +183,7 @@ def test_summary_survives_corrupt_lines(self) -> None: result = CliRunner().invoke(main, ["summary", "--path", str(FIXTURES)]) assert result.exit_code == 0 # 8 sessions total - assert "8 sessions" in result.output + assert "9 sessions" in result.output def test_corrupt_session_appears_in_summary(self) -> None: """The corrupt session is parsed (valid lines kept) and shown.""" @@ -245,7 +245,7 @@ def test_summary_date_filter_includes_all(self) -> None: ["summary", "--path", str(FIXTURES), "--since", "2020-01-01"], ) assert result.exit_code == 0 - assert "8 sessions" in result.output + assert "9 sessions" in result.output # --------------------------------------------------------------------------- @@ -536,6 +536,45 @@ def test_has_recent_events(self) -> None: assert "Recent Events" in result.output +# --------------------------------------------------------------------------- +# pure active session with activity (regression for #154) +# --------------------------------------------------------------------------- + + +class TestPureActiveSessionActivityE2E: + """Regression: pure active session must show non-zero activity counts.""" + + def test_live_shows_nonzero_model_calls(self) -> None: + """Pure active session with turn_starts shows model calls in live view.""" + result = CliRunner().invoke(main, ["live", "--path", str(FIXTURES)]) + assert result.exit_code == 0 + assert "pure-act" in result.output + + def test_session_detail_shows_active(self) -> None: + """Pure active session detail shows active status.""" + result = CliRunner().invoke( + main, ["session", "pure-active", "--path", str(FIXTURES)] + ) + assert result.exit_code == 0 + assert "active" in result.output.lower() + + def test_session_detail_shows_user_messages(self) -> None: + """Pure active session detail shows 2 user messages.""" + result = CliRunner().invoke( + main, ["session", "pure-active", "--path", str(FIXTURES)] + ) + assert result.exit_code == 0 + assert "2 user messages" in result.output + + def test_session_detail_shows_model_calls(self) -> None: + """Pure active session detail shows 2 model calls.""" + result = CliRunner().invoke( + main, ["session", "pure-active", "--path", str(FIXTURES)] + ) + assert result.exit_code == 0 + assert "2 model calls" in result.output + + # --------------------------------------------------------------------------- # shutdown aggregation regression # --------------------------------------------------------------------------- From f81b13fc86d859632b9b8c5c53dfb5ef444d008c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 20 Mar 2026 16:13:37 +0000 Subject: [PATCH 2/6] fix: address review comments - Make active session assertions specific to the Pure Active line - Rename test_finds_eight_sessions to test_finds_all_sessions - Update stale '8 sessions total' comment to 9 - Rename and strengthen test_live_shows_nonzero_model_calls Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/copilot_usage/test_report.py | 10 ++++++---- tests/e2e/test_e2e.py | 12 +++++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/copilot_usage/test_report.py b/tests/copilot_usage/test_report.py index af79b63..e38386e 100644 --- a/tests/copilot_usage/test_report.py +++ b/tests/copilot_usage/test_report.py @@ -1284,10 +1284,12 @@ def test_active_section_shows_nonzero_activity(self) -> None: ) output = _capture_full_summary([session]) assert "Active Sessions" in output - # The active section should display the non-zero counts - assert "3" in output # active_model_calls - assert "4" in output # active_user_messages - assert "1.5K" in output # active_output_tokens (format_tokens) + # The active section should display the non-zero counts for this session + lines = output.splitlines() + pure_active_line = next(l for l in lines if "Pure Active" in l) + assert "3" in pure_active_line # active_model_calls + assert "4" in pure_active_line # active_user_messages + assert "1.5K" in pure_active_line # active_output_tokens (format_tokens) # --------------------------------------------------------------------------- diff --git a/tests/e2e/test_e2e.py b/tests/e2e/test_e2e.py index c693432..5610b7e 100644 --- a/tests/e2e/test_e2e.py +++ b/tests/e2e/test_e2e.py @@ -27,7 +27,7 @@ def _wide_terminal(monkeypatch: pytest.MonkeyPatch) -> None: class TestSummaryE2E: """Tests for the ``summary`` command.""" - def test_finds_eight_sessions(self) -> None: + def test_finds_all_sessions(self) -> None: result = CliRunner().invoke(main, ["summary", "--path", str(FIXTURES)]) assert result.exit_code == 0 assert "9 sessions" in result.output @@ -182,7 +182,7 @@ def test_summary_survives_corrupt_lines(self) -> None: """Summary still works when events.jsonl has malformed JSON lines.""" result = CliRunner().invoke(main, ["summary", "--path", str(FIXTURES)]) assert result.exit_code == 0 - # 8 sessions total + # 9 sessions total assert "9 sessions" in result.output def test_corrupt_session_appears_in_summary(self) -> None: @@ -544,11 +544,13 @@ def test_has_recent_events(self) -> None: class TestPureActiveSessionActivityE2E: """Regression: pure active session must show non-zero activity counts.""" - def test_live_shows_nonzero_model_calls(self) -> None: - """Pure active session with turn_starts shows model calls in live view.""" + def test_live_shows_pure_active_session(self) -> None: + """Pure active session appears in the live view.""" result = CliRunner().invoke(main, ["live", "--path", str(FIXTURES)]) assert result.exit_code == 0 - assert "pure-act" in result.output + lines = result.output.splitlines() + active_line = next(l for l in lines if "pure-act" in l) + assert "2" in active_line # user_messages shown in Messages column def test_session_detail_shows_active(self) -> None: """Pure active session detail shows active status.""" From 215c7e1b192f794c7a2777bb8b2541ef1665d8ff Mon Sep 17 00:00:00 2001 From: Sasa Junuzovic Date: Fri, 20 Mar 2026 09:28:34 -0700 Subject: [PATCH 3/6] fix: rename ambiguous variable 'l' to 'line' (ruff E741) The responder's commit used 'l' as a loop variable in generator expressions, which ruff flags as E741 (ambiguous variable name). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/copilot_usage/test_report.py | 2 +- tests/e2e/test_e2e.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/copilot_usage/test_report.py b/tests/copilot_usage/test_report.py index e38386e..75a9919 100644 --- a/tests/copilot_usage/test_report.py +++ b/tests/copilot_usage/test_report.py @@ -1286,7 +1286,7 @@ def test_active_section_shows_nonzero_activity(self) -> None: assert "Active Sessions" in output # The active section should display the non-zero counts for this session lines = output.splitlines() - pure_active_line = next(l for l in lines if "Pure Active" in l) + pure_active_line = next(line for line in lines if "Pure Active" in line) assert "3" in pure_active_line # active_model_calls assert "4" in pure_active_line # active_user_messages assert "1.5K" in pure_active_line # active_output_tokens (format_tokens) diff --git a/tests/e2e/test_e2e.py b/tests/e2e/test_e2e.py index 5610b7e..e1955cc 100644 --- a/tests/e2e/test_e2e.py +++ b/tests/e2e/test_e2e.py @@ -549,7 +549,7 @@ def test_live_shows_pure_active_session(self) -> None: result = CliRunner().invoke(main, ["live", "--path", str(FIXTURES)]) assert result.exit_code == 0 lines = result.output.splitlines() - active_line = next(l for l in lines if "pure-act" in l) + active_line = next(line for line in lines if "pure-act" in line) assert "2" in active_line # user_messages shown in Messages column def test_session_detail_shows_active(self) -> None: From f47ade81d0f11e5096e00259bf021a12a4ec21bf Mon Sep 17 00:00:00 2001 From: Sasa Junuzovic Date: Fri, 20 Mar 2026 09:30:46 -0700 Subject: [PATCH 4/6] Revert "fix: rename ambiguous variable 'l' to 'line' (ruff E741)" This reverts commit 6e49aca6bf0557a9e16c0337eb578864f2605d2e. --- tests/copilot_usage/test_report.py | 2 +- tests/e2e/test_e2e.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/copilot_usage/test_report.py b/tests/copilot_usage/test_report.py index 75a9919..e38386e 100644 --- a/tests/copilot_usage/test_report.py +++ b/tests/copilot_usage/test_report.py @@ -1286,7 +1286,7 @@ def test_active_section_shows_nonzero_activity(self) -> None: assert "Active Sessions" in output # The active section should display the non-zero counts for this session lines = output.splitlines() - pure_active_line = next(line for line in lines if "Pure Active" in line) + pure_active_line = next(l for l in lines if "Pure Active" in l) assert "3" in pure_active_line # active_model_calls assert "4" in pure_active_line # active_user_messages assert "1.5K" in pure_active_line # active_output_tokens (format_tokens) diff --git a/tests/e2e/test_e2e.py b/tests/e2e/test_e2e.py index e1955cc..5610b7e 100644 --- a/tests/e2e/test_e2e.py +++ b/tests/e2e/test_e2e.py @@ -549,7 +549,7 @@ def test_live_shows_pure_active_session(self) -> None: result = CliRunner().invoke(main, ["live", "--path", str(FIXTURES)]) assert result.exit_code == 0 lines = result.output.splitlines() - active_line = next(line for line in lines if "pure-act" in line) + active_line = next(l for l in lines if "pure-act" in l) assert "2" in active_line # user_messages shown in Messages column def test_session_detail_shows_active(self) -> None: From 3669b190b84be4e7c7e73dd4beac25eeff125230 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 20 Mar 2026 18:04:19 +0000 Subject: [PATCH 5/6] fix: resolve CI failures (ruff E741 ambiguous variable name) Rename loop variable 'l' to 'line' in two generator expressions to fix ruff E741 (ambiguous variable name) lint errors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/copilot_usage/test_report.py | 2 +- tests/e2e/test_e2e.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/copilot_usage/test_report.py b/tests/copilot_usage/test_report.py index e38386e..75a9919 100644 --- a/tests/copilot_usage/test_report.py +++ b/tests/copilot_usage/test_report.py @@ -1286,7 +1286,7 @@ def test_active_section_shows_nonzero_activity(self) -> None: assert "Active Sessions" in output # The active section should display the non-zero counts for this session lines = output.splitlines() - pure_active_line = next(l for l in lines if "Pure Active" in l) + pure_active_line = next(line for line in lines if "Pure Active" in line) assert "3" in pure_active_line # active_model_calls assert "4" in pure_active_line # active_user_messages assert "1.5K" in pure_active_line # active_output_tokens (format_tokens) diff --git a/tests/e2e/test_e2e.py b/tests/e2e/test_e2e.py index 5610b7e..e1955cc 100644 --- a/tests/e2e/test_e2e.py +++ b/tests/e2e/test_e2e.py @@ -549,7 +549,7 @@ def test_live_shows_pure_active_session(self) -> None: result = CliRunner().invoke(main, ["live", "--path", str(FIXTURES)]) assert result.exit_code == 0 lines = result.output.splitlines() - active_line = next(l for l in lines if "pure-act" in l) + active_line = next(line for line in lines if "pure-act" in line) assert "2" in active_line # user_messages shown in Messages column def test_session_detail_shows_active(self) -> None: From 951e4b0db15a029b4c87dd679656969b59b2bf88 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 21 Mar 2026 01:29:55 +0000 Subject: [PATCH 6/6] fix: address review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improve test assertions to be column-aware by splitting on │ separators instead of checking for substring matches that could match unintended columns (e.g., model name containing '4', running time containing '2'). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/copilot_usage/test_report.py | 17 ++++++++++++----- tests/e2e/test_e2e.py | 9 +++++++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/tests/copilot_usage/test_report.py b/tests/copilot_usage/test_report.py index 75a9919..30e8e2a 100644 --- a/tests/copilot_usage/test_report.py +++ b/tests/copilot_usage/test_report.py @@ -1282,14 +1282,21 @@ def test_active_section_shows_nonzero_activity(self) -> None: ) }, ) + import re + output = _capture_full_summary([session]) assert "Active Sessions" in output - # The active section should display the non-zero counts for this session - lines = output.splitlines() + # Strip ANSI codes and split on │ to validate the correct columns + clean = re.sub(r"\x1b\[[0-9;]*m", "", output) + lines = clean.splitlines() pure_active_line = next(line for line in lines if "Pure Active" in line) - assert "3" in pure_active_line # active_model_calls - assert "4" in pure_active_line # active_user_messages - assert "1.5K" in pure_active_line # active_output_tokens (format_tokens) + # Active Sessions columns: Name | Model | Model Calls | User Msgs | Output Tokens | Running Time + cols = [c.strip() for c in pure_active_line.split("│")] + assert cols[3] == "3", f"Model Calls column: expected '3', got '{cols[3]}'" + assert cols[4] == "4", f"User Msgs column: expected '4', got '{cols[4]}'" + assert cols[5] == "1.5K", ( + f"Output Tokens column: expected '1.5K', got '{cols[5]}'" + ) # --------------------------------------------------------------------------- diff --git a/tests/e2e/test_e2e.py b/tests/e2e/test_e2e.py index e1955cc..1f560bd 100644 --- a/tests/e2e/test_e2e.py +++ b/tests/e2e/test_e2e.py @@ -546,11 +546,16 @@ class TestPureActiveSessionActivityE2E: def test_live_shows_pure_active_session(self) -> None: """Pure active session appears in the live view.""" + import re + result = CliRunner().invoke(main, ["live", "--path", str(FIXTURES)]) assert result.exit_code == 0 - lines = result.output.splitlines() + clean = re.sub(r"\x1b\[[0-9;]*m", "", result.output) + lines = clean.splitlines() active_line = next(line for line in lines if "pure-act" in line) - assert "2" in active_line # user_messages shown in Messages column + # Live table columns: Session ID | Name | Model | Running | Messages | Output Tokens | CWD + cols = [c.strip() for c in active_line.split("│")] + assert cols[5] == "2", f"Messages column: expected '2', got '{cols[5]}'" def test_session_detail_shows_active(self) -> None: """Pure active session detail shows active status."""