From 0746197ddf6a678d5b5772c3a226f1bd9d5e4710 Mon Sep 17 00:00:00 2001 From: Sasa Junuzovic <44276455+microsasa@users.noreply.github.com> Date: Sat, 18 Apr 2026 22:30:51 -0700 Subject: [PATCH 1/2] fix: convert VS Code log timestamps to aware UTC datetimes (#993) _parse_vscode_log_from_offset now calls .astimezone(UTC) on parsed timestamps so VSCodeRequest.timestamp, VSCodeLogSummary.first_timestamp, and VSCodeLogSummary.last_timestamp are timezone-aware, consistent with the rest of the codebase. Add tests verifying parsed timestamps are aware and comparable with session-level aware datetimes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/copilot_usage/vscode_parser.py | 4 +- tests/copilot_usage/test_vscode_parser.py | 45 ++++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/copilot_usage/vscode_parser.py b/src/copilot_usage/vscode_parser.py index cc1c3321..153319a3 100644 --- a/src/copilot_usage/vscode_parser.py +++ b/src/copilot_usage/vscode_parser.py @@ -8,7 +8,7 @@ from collections import OrderedDict, defaultdict from collections.abc import Mapping, Sequence from dataclasses import dataclass, field -from datetime import datetime +from datetime import UTC, datetime from pathlib import Path from typing import Final, Literal @@ -372,7 +372,7 @@ def _parse_vscode_log_from_offset( continue ts_str, req_id, model, duration_str, category = m.groups() try: - ts = datetime.fromisoformat(ts_str) + ts = datetime.fromisoformat(ts_str).astimezone(UTC) except ValueError: continue requests.append( diff --git a/tests/copilot_usage/test_vscode_parser.py b/tests/copilot_usage/test_vscode_parser.py index f3133cfa..1ae0c9f1 100644 --- a/tests/copilot_usage/test_vscode_parser.py +++ b/tests/copilot_usage/test_vscode_parser.py @@ -5,7 +5,7 @@ import dataclasses import os import re -from datetime import date, datetime +from datetime import UTC, date, datetime from pathlib import Path from unittest.mock import patch @@ -3023,3 +3023,46 @@ def test_internal_fields_rejected_by_init(self) -> None: """Passing internal counters to the constructor must raise.""" with pytest.raises(TypeError): _SummaryAccumulator(total_requests=1) # type: ignore[call-arg] + + +# --------------------------------------------------------------------------- +# Aware-datetime contract for parsed VS Code timestamps +# --------------------------------------------------------------------------- + + +class TestParsedTimestampsAreAware: + """Parsed VS Code log timestamps must be timezone-aware (UTC).""" + + def test_parsed_request_timestamp_is_aware(self, tmp_path: Path) -> None: + """parse_vscode_log produces requests with aware (UTC) timestamps.""" + log_file = tmp_path / "test.log" + log_file.write_text(_LOG_OPUS, encoding="utf-8") + requests = parse_vscode_log(log_file) + assert len(requests) == 1 + assert requests[0].timestamp.tzinfo is not None + + def test_vscode_summary_timestamps_are_aware(self, tmp_path: Path) -> None: + """build_vscode_summary fed parsed requests yields aware timestamps.""" + log_file = tmp_path / "test.log" + log_file.write_text(_LOG_OPUS, encoding="utf-8") + requests = parse_vscode_log(log_file) + summary = build_vscode_summary(requests) + assert summary.first_timestamp is not None + assert summary.first_timestamp.tzinfo is not None + assert summary.last_timestamp is not None + assert summary.last_timestamp.tzinfo is not None + + def test_vscode_timestamps_comparable_with_session_timestamps( + self, tmp_path: Path + ) -> None: + """Parsed VS Code timestamps can be compared with aware datetimes.""" + log_file = tmp_path / "test.log" + log_file.write_text(_LOG_OPUS, encoding="utf-8") + requests = parse_vscode_log(log_file) + summary = build_vscode_summary(requests) + aware_dt = datetime(2026, 1, 1, tzinfo=UTC) + # Must not raise TypeError: can't compare offset-naive and offset-aware datetimes + assert summary.first_timestamp is not None + assert summary.first_timestamp > aware_dt + assert summary.last_timestamp is not None + assert summary.last_timestamp > aware_dt From e812a28dfd18771a8ad40b1906b54fcd6b002f24 Mon Sep 17 00:00:00 2001 From: Sasa Junuzovic <44276455+microsasa@users.noreply.github.com> Date: Sat, 18 Apr 2026 22:38:45 -0700 Subject: [PATCH 2/2] fix: address review comments Strengthen timezone assertions in TestParsedTimestampsAreAware from `tzinfo is not None` to `tzinfo == UTC` to lock in the UTC contract and catch regressions. Updated docstring to say 'aware UTC timestamps'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/copilot_usage/test_vscode_parser.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/copilot_usage/test_vscode_parser.py b/tests/copilot_usage/test_vscode_parser.py index 1ae0c9f1..85cdfdee 100644 --- a/tests/copilot_usage/test_vscode_parser.py +++ b/tests/copilot_usage/test_vscode_parser.py @@ -3039,18 +3039,18 @@ def test_parsed_request_timestamp_is_aware(self, tmp_path: Path) -> None: log_file.write_text(_LOG_OPUS, encoding="utf-8") requests = parse_vscode_log(log_file) assert len(requests) == 1 - assert requests[0].timestamp.tzinfo is not None + assert requests[0].timestamp.tzinfo == UTC def test_vscode_summary_timestamps_are_aware(self, tmp_path: Path) -> None: - """build_vscode_summary fed parsed requests yields aware timestamps.""" + """build_vscode_summary fed parsed requests yields aware UTC timestamps.""" log_file = tmp_path / "test.log" log_file.write_text(_LOG_OPUS, encoding="utf-8") requests = parse_vscode_log(log_file) summary = build_vscode_summary(requests) assert summary.first_timestamp is not None - assert summary.first_timestamp.tzinfo is not None + assert summary.first_timestamp.tzinfo == UTC assert summary.last_timestamp is not None - assert summary.last_timestamp.tzinfo is not None + assert summary.last_timestamp.tzinfo == UTC def test_vscode_timestamps_comparable_with_session_timestamps( self, tmp_path: Path