Skip to content

Commit 71b0a1c

Browse files
PabloLIONclaude
andcommitted
fix: Complete type error resolution and broken test fixes
- Fixed type errors across all test files using cast() for test data compatibility - Added proper TypedDict imports and casting patterns with explanatory comments - Fixed SerializedBlock structure with missing required fields (burnRate, projection, limitMessages) - Resolved test_monitoring_loop_periodic_updates timing issue by: - Restoring original update_interval = 0.1 seconds in test - Updating MonitoringOrchestrator to accept float update_interval for fractional timing - Applied consistent patterns for Mock object access with pyright ignore comments - All 516 tests now pass with proper type safety 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 2a579aa commit 71b0a1c

16 files changed

+373
-231
lines changed

src/claude_monitor/monitoring/orchestrator.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@
1717
class MonitoringOrchestrator:
1818
"""Orchestrates monitoring components following SRP."""
1919

20-
def __init__(self, update_interval: int = 10, data_path: str | None = None) -> None:
20+
def __init__(self, update_interval: float = 10, data_path: str | None = None) -> None:
2121
"""Initialize orchestrator with components.
2222
2323
Args:
24-
update_interval: Seconds between updates
24+
update_interval: Seconds between updates (can be fractional)
2525
data_path: Optional path to Claude data directory
2626
"""
27-
self.update_interval: int = update_interval
27+
self.update_interval: float = update_interval
2828

2929
self.data_manager: DataManager = DataManager(cache_ttl=5, data_path=data_path)
3030
self.session_monitor: SessionMonitor = SessionMonitor()

src/tests/conftest.py

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Shared pytest fixtures for Claude Monitor tests."""
22

33
from datetime import datetime, timezone
4+
from typing import cast
45
from unittest.mock import Mock
56

67
import pytest
@@ -87,7 +88,7 @@ def sample_assistant_data() -> RawJSONEntry:
8788
@pytest.fixture
8889
def sample_user_data() -> RawJSONEntry:
8990
"""Sample user-type data for testing."""
90-
return {
91+
return cast(RawJSONEntry, {
9192
"timestamp": "2024-01-01T12:00:00Z",
9293
"type": "user",
9394
"usage": {
@@ -99,33 +100,33 @@ def sample_user_data() -> RawJSONEntry:
99100
"model": "claude-3-haiku",
100101
"message_id": "msg_123",
101102
"request_id": "req_456",
102-
}
103+
}) # Test data with simplified structure
103104

104105

105106
@pytest.fixture
106107
def sample_malformed_data() -> RawJSONEntry:
107108
"""Sample malformed data for testing error handling."""
108-
return {
109+
return cast(RawJSONEntry, {
109110
"timestamp": "invalid_timestamp",
110111
"message": "not_a_dict",
111112
"usage": {"input_tokens": "not_a_number", "output_tokens": None},
112-
}
113+
}) # Test data with invalid types for error testing
113114

114115

115116
@pytest.fixture
116117
def sample_minimal_data() -> RawJSONEntry:
117118
"""Sample minimal valid data for testing."""
118-
return {
119+
return cast(RawJSONEntry, {
119120
"timestamp": "2024-01-01T12:00:00Z",
120121
"usage": {"input_tokens": 100, "output_tokens": 50},
121122
"request_id": "req_456",
122-
}
123+
}) # Minimal test data structure
123124

124125

125126
@pytest.fixture
126127
def sample_empty_tokens_data() -> RawJSONEntry:
127128
"""Sample data with empty/zero tokens for testing."""
128-
return {
129+
return cast(RawJSONEntry, {
129130
"timestamp": "2024-01-01T12:00:00Z",
130131
"usage": {
131132
"input_tokens": 0,
@@ -134,13 +135,13 @@ def sample_empty_tokens_data() -> RawJSONEntry:
134135
"cache_read_input_tokens": 0,
135136
},
136137
"request_id": "req_456",
137-
}
138+
}) # Test data with zero token values
138139

139140

140141
@pytest.fixture
141142
def sample_duplicate_data() -> list[RawJSONEntry]:
142143
"""Sample data for testing duplicate detection."""
143-
return [
144+
return cast(list[RawJSONEntry], [
144145
{
145146
"timestamp": "2024-01-01T12:00:00Z",
146147
"message_id": "msg_1",
@@ -159,7 +160,7 @@ def sample_duplicate_data() -> list[RawJSONEntry]:
159160
"request_id": "req_2",
160161
"usage": {"input_tokens": 200, "output_tokens": 75},
161162
},
162-
]
163+
]) # Test data with duplicate message IDs
163164

164165

165166
@pytest.fixture
@@ -302,36 +303,70 @@ def mock_session_monitor() -> Mock:
302303
@pytest.fixture
303304
def sample_monitoring_data() -> AnalysisResult:
304305
"""Sample monitoring data structure for testing."""
305-
return {
306+
return cast(AnalysisResult, {
306307
"blocks": [
307308
{
308309
"id": "session_1",
309310
"isActive": True,
311+
"isGap": False,
312+
"startTime": "2024-01-01T12:00:00Z",
313+
"endTime": "2024-01-01T17:00:00Z",
314+
"actualEndTime": "2024-01-01T17:00:00Z",
315+
"tokenCounts": {"inputTokens": 800, "outputTokens": 200, "cacheCreationInputTokens": 0, "cacheReadInputTokens": 0},
310316
"totalTokens": 1000,
311317
"costUSD": 0.05,
312-
"startTime": "2024-01-01T12:00:00Z",
318+
"models": ["claude-3-haiku"],
319+
"perModelStats": {},
320+
"sentMessagesCount": 5,
321+
"durationMinutes": 300.0,
322+
"entries": [],
323+
"entries_count": 5,
313324
},
314325
{
315326
"id": "session_2",
316327
"isActive": False,
328+
"isGap": False,
329+
"startTime": "2024-01-01T11:00:00Z",
330+
"endTime": "2024-01-01T12:00:00Z",
331+
"actualEndTime": "2024-01-01T12:00:00Z",
332+
"tokenCounts": {"inputTokens": 400, "outputTokens": 100, "cacheCreationInputTokens": 0, "cacheReadInputTokens": 0},
317333
"totalTokens": 500,
318334
"costUSD": 0.025,
319-
"startTime": "2024-01-01T11:00:00Z",
335+
"models": ["claude-3-haiku"],
336+
"perModelStats": {},
337+
"sentMessagesCount": 3,
338+
"durationMinutes": 60.0,
339+
"entries": [],
340+
"entries_count": 3,
320341
},
321-
]
322-
}
342+
],
343+
"metadata": {
344+
"generated_at": "2024-01-01T12:00:00Z",
345+
"hours_analyzed": 24,
346+
"entries_processed": 8,
347+
"blocks_created": 2,
348+
"limits_detected": 0,
349+
"load_time_seconds": 0.1,
350+
"transform_time_seconds": 0.05,
351+
"cache_used": False,
352+
"quick_start": False,
353+
},
354+
"entries_count": 8,
355+
"total_tokens": 1500,
356+
"total_cost": 0.075,
357+
}) # Complete test monitoring data
323358

324359

325360
@pytest.fixture
326361
def sample_session_data() -> RawJSONEntry:
327362
"""Sample session data for testing."""
328-
return {
363+
return cast(RawJSONEntry, {
329364
"id": "session_1",
330365
"isActive": True,
331366
"totalTokens": 1000,
332367
"costUSD": 0.05,
333368
"startTime": "2024-01-01T12:00:00Z",
334-
}
369+
}) # Session test data with simplified structure
335370

336371

337372
@pytest.fixture

src/tests/test_aggregator.py

Lines changed: 58 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Tests for data aggregator module."""
22

33
from datetime import datetime, timezone
4+
from pathlib import Path
45

56
import pytest
67

@@ -10,6 +11,19 @@
1011
AggregatedStatsData,
1112
UsageAggregator,
1213
)
14+
from claude_monitor.types import CompleteAggregatedUsage
15+
16+
17+
def get_daily_result_date(result: CompleteAggregatedUsage) -> str:
18+
"""Get date from daily aggregation result, which should always have date set."""
19+
assert "date" in result, "Daily aggregation result should have date field"
20+
return result["date"] # type: ignore[return-value,no-any-return] # Daily aggregation always sets date
21+
22+
23+
def get_monthly_result_month(result: CompleteAggregatedUsage) -> str:
24+
"""Get month from monthly aggregation result, which should always have month set."""
25+
assert "month" in result, "Monthly aggregation result should have month field"
26+
return result["month"] # type: ignore[return-value,no-any-return] # Monthly aggregation always sets month
1327

1428

1529
class TestAggregatedStats:
@@ -206,7 +220,7 @@ def test_add_entry_with_unknown_model(self) -> None:
206220
cache_creation_tokens=0,
207221
cache_read_tokens=0,
208222
cost_usd=0.001,
209-
model=None,
223+
model="unknown",
210224
message_id="msg_1",
211225
request_id="req_1",
212226
)
@@ -247,7 +261,7 @@ def test_to_dict_daily(self) -> None:
247261

248262
result = period.to_dict("date")
249263

250-
assert result["date"] == "2024-01-01"
264+
assert get_daily_result_date(result) == "2024-01-01"
251265
assert result["input_tokens"] == 1000
252266
assert result["output_tokens"] == 500
253267
assert result["cache_creation_tokens"] == 100
@@ -273,7 +287,7 @@ def test_to_dict_monthly(self) -> None:
273287

274288
result = period.to_dict("month")
275289

276-
assert result["month"] == "2024-01"
290+
assert get_monthly_result_month(result) == "2024-01"
277291
assert result["input_tokens"] == 10000
278292
assert result["total_cost"] == 0.5
279293

@@ -282,7 +296,7 @@ class TestUsageAggregator:
282296
"""Test cases for UsageAggregator class."""
283297

284298
@pytest.fixture
285-
def aggregator(self, tmp_path) -> UsageAggregator:
299+
def aggregator(self, tmp_path: Path) -> UsageAggregator:
286300
"""Create a UsageAggregator instance."""
287301
return UsageAggregator(data_path=str(tmp_path))
288302

@@ -335,7 +349,7 @@ def test_aggregate_daily_basic(
335349

336350
# Check first day (Jan 1 - 4 entries: 2 at 10AM, 2 at 2PM)
337351
jan1 = result[0]
338-
assert jan1["date"] == "2024-01-01"
352+
assert get_daily_result_date(jan1) == "2024-01-01"
339353
assert jan1["input_tokens"] == 400 # 4 entries * 100
340354
assert jan1["output_tokens"] == 200 # 4 entries * 50
341355
assert jan1["total_cost"] == 0.004 # 4 entries * 0.001
@@ -355,8 +369,8 @@ def test_aggregate_daily_with_date_filter(
355369

356370
# Should have Jan 15 and Jan 31 (entries on those days are within the filter)
357371
assert len(result) == 2
358-
assert result[0]["date"] == "2024-01-15"
359-
assert result[1]["date"] == "2024-01-31"
372+
assert get_daily_result_date(result[0]) == "2024-01-15"
373+
assert get_daily_result_date(result[1]) == "2024-01-31"
360374

361375
def test_aggregate_monthly_basic(
362376
self, aggregator: UsageAggregator, sample_entries: list[UsageEntry]
@@ -369,7 +383,7 @@ def test_aggregate_monthly_basic(
369383

370384
# Check January
371385
jan = result[0]
372-
assert jan["month"] == "2024-01"
386+
assert get_monthly_result_month(jan) == "2024-01"
373387
assert jan["input_tokens"] == 1400 # 14 entries * 100
374388
assert jan["output_tokens"] == 700 # 14 entries * 50
375389
assert (
@@ -380,7 +394,7 @@ def test_aggregate_monthly_basic(
380394

381395
# Check February
382396
feb = result[1]
383-
assert feb["month"] == "2024-02"
397+
assert get_monthly_result_month(feb) == "2024-02"
384398
assert feb["input_tokens"] == 600 # 3 entries * 200
385399
assert feb["output_tokens"] == 300 # 3 entries * 100
386400
assert feb["total_cost"] == 0.006 # 3 entries * 0.002
@@ -397,7 +411,7 @@ def test_aggregate_monthly_with_date_filter(
397411

398412
# Should only have February
399413
assert len(result) == 1
400-
assert result[0]["month"] == "2024-02"
414+
assert get_monthly_result_month(result[0]) == "2024-02"
401415

402416
def test_aggregate_from_blocks_daily(
403417
self, aggregator: UsageAggregator, sample_entries: list[UsageEntry]
@@ -435,7 +449,7 @@ def test_aggregate_from_blocks_daily(
435449
result = aggregator.aggregate_from_blocks(blocks, "daily")
436450

437451
assert len(result) >= 2 # At least 2 days of data
438-
assert result[0]["date"] == "2024-01-01"
452+
assert get_daily_result_date(result[0]) == "2024-01-01"
439453

440454
def test_aggregate_from_blocks_monthly(
441455
self, aggregator: UsageAggregator, sample_entries: list[UsageEntry]
@@ -454,8 +468,8 @@ def test_aggregate_from_blocks_monthly(
454468
result = aggregator.aggregate_from_blocks([block], "monthly")
455469

456470
assert len(result) == 2 # Jan and Feb
457-
assert result[0]["month"] == "2024-01"
458-
assert result[1]["month"] == "2024-02"
471+
assert get_monthly_result_month(result[0]) == "2024-01"
472+
assert get_monthly_result_month(result[1]) == "2024-02"
459473

460474
def test_aggregate_from_blocks_invalid_view_type(
461475
self, aggregator: UsageAggregator
@@ -488,25 +502,31 @@ def test_calculate_totals_empty(self, aggregator: UsageAggregator) -> None:
488502

489503
def test_calculate_totals_with_data(self, aggregator: UsageAggregator) -> None:
490504
"""Test calculating totals with aggregated data."""
491-
aggregated_data = [
492-
{
493-
"date": "2024-01-01",
494-
"input_tokens": 1000,
495-
"output_tokens": 500,
496-
"cache_creation_tokens": 100,
497-
"cache_read_tokens": 50,
498-
"total_cost": 0.05,
499-
"entries_count": 10,
500-
},
501-
{
502-
"date": "2024-01-02",
503-
"input_tokens": 2000,
504-
"output_tokens": 1000,
505-
"cache_creation_tokens": 200,
506-
"cache_read_tokens": 100,
507-
"total_cost": 0.10,
508-
"entries_count": 20,
509-
},
505+
from claude_monitor.types import CompleteAggregatedUsage
506+
507+
aggregated_data: list[CompleteAggregatedUsage] = [
508+
CompleteAggregatedUsage(
509+
date="2024-01-01",
510+
input_tokens=1000,
511+
output_tokens=500,
512+
cache_creation_tokens=100,
513+
cache_read_tokens=50,
514+
total_cost=0.05,
515+
entries_count=10,
516+
models_used=[],
517+
model_breakdowns={},
518+
),
519+
CompleteAggregatedUsage(
520+
date="2024-01-02",
521+
input_tokens=2000,
522+
output_tokens=1000,
523+
cache_creation_tokens=200,
524+
cache_read_tokens=100,
525+
total_cost=0.10,
526+
entries_count=20,
527+
models_used=[],
528+
model_breakdowns={},
529+
),
510530
]
511531

512532
result = aggregator.calculate_totals(aggregated_data)
@@ -573,9 +593,9 @@ def test_period_sorting(self, aggregator: UsageAggregator) -> None:
573593
# Test daily sorting
574594
daily_result = aggregator.aggregate_daily(entries)
575595
assert len(daily_result) == 3
576-
assert daily_result[0]["date"] == "2024-01-01"
577-
assert daily_result[1]["date"] == "2024-01-10"
578-
assert daily_result[2]["date"] == "2024-01-15"
596+
assert get_daily_result_date(daily_result[0]) == "2024-01-01"
597+
assert get_daily_result_date(daily_result[1]) == "2024-01-10"
598+
assert get_daily_result_date(daily_result[2]) == "2024-01-15"
579599

580600
# Test monthly sorting
581601
monthly_entries = [
@@ -616,6 +636,6 @@ def test_period_sorting(self, aggregator: UsageAggregator) -> None:
616636

617637
monthly_result = aggregator.aggregate_monthly(monthly_entries)
618638
assert len(monthly_result) == 3
619-
assert monthly_result[0]["month"] == "2024-01"
620-
assert monthly_result[1]["month"] == "2024-02"
621-
assert monthly_result[2]["month"] == "2024-03"
639+
assert get_monthly_result_month(monthly_result[0]) == "2024-01"
640+
assert get_monthly_result_month(monthly_result[1]) == "2024-02"
641+
assert get_monthly_result_month(monthly_result[2]) == "2024-03"

0 commit comments

Comments
 (0)