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..85cdfdee 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 == UTC + + def test_vscode_summary_timestamps_are_aware(self, tmp_path: Path) -> None: + """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 == UTC + assert summary.last_timestamp is not None + assert summary.last_timestamp.tzinfo == UTC + + 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