main
py 167 lines 5.73 KB
Raw
1 """Tests for baseline telemetry collection and reporting.
2
3 Verifies that the baseline telemetry module correctly:
4 - Loads observability ledger entries
5 - Computes p50/p95 statistics per pipeline stage
6 - Enforces the minimum-runs requirement
7 - Evaluates trigger thresholds
8 """
9
10 from __future__ import annotations
11
12 import json
13 from pathlib import Path
14 from typing import Any
15
16 from scripts.baseline_telemetry import (
17 build_baseline_report,
18 check_trigger_thresholds,
19 compute_stage_baseline,
20 load_ledger_entries,
21 percentile,
22 )
23
24
25 def _sample_ledger(
26 *,
27 github_duration: float = 45.0,
28 rss_duration: float = 1.2,
29 analysis_duration: float = 120.0,
30 timestamp: str = "2026-06-10T12:00:00Z",
31 ) -> dict[str, Any]:
32 return {
33 "schema_version": "observability_v1",
34 "run_id": "test",
35 "week": "2026-W24",
36 "timestamp": timestamp,
37 "crawl_metrics": [
38 {
39 "duration_seconds": github_duration,
40 "api_calls": 100,
41 "cache_hits": 40,
42 "cache_misses": 60,
43 "stale_cache_hits": 2,
44 "rate_limit_events": 0,
45 "secondary_rate_limit_hit": False,
46 "source_type": "github",
47 },
48 {
49 "duration_seconds": rss_duration,
50 "api_calls": 5,
51 "cache_hits": 0,
52 "cache_misses": 5,
53 "stale_cache_hits": 0,
54 "rate_limit_events": 0,
55 "secondary_rate_limit_hit": False,
56 "source_type": "rss",
57 },
58 ],
59 "analysis_metrics": {
60 "duration_seconds": analysis_duration,
61 "token_ledger": {"input_tokens": 1000, "output_tokens": 500, "total_tokens": 1500},
62 "map_stages": [],
63 },
64 "environment": {},
65 }
66
67
68 class TestPercentile:
69 def test_p50_odd(self):
70 assert percentile([1.0, 2.0, 3.0, 4.0, 5.0], 50) == 3.0
71
72 def test_p95_small_sample(self):
73 assert percentile([10.0, 20.0, 30.0, 40.0, 50.0], 95) == 50.0
74
75 def test_empty(self):
76 assert percentile([], 50) == 0.0
77
78
79 class TestComputeStageBaseline:
80 def test_valid_durations(self):
81 durations = [10.0, 20.0, 30.0, 40.0, 50.0]
82 baseline = compute_stage_baseline("test", durations)
83 assert baseline.stage == "test"
84 assert baseline.sample_count == 5
85 assert baseline.min_seconds == 10.0
86 assert baseline.max_seconds == 50.0
87 assert baseline.p50_seconds == 30.0
88
89 def test_empty_durations(self):
90 baseline = compute_stage_baseline("test", [])
91 assert baseline.sample_count == 0
92 assert baseline.p50_seconds == 0.0
93
94
95 class TestLoadLedgerEntries:
96 def test_loads_json_files(self, tmp_path: Path):
97 ledger = _sample_ledger()
98 (tmp_path / "run1.json").write_text(json.dumps(ledger))
99 (tmp_path / "run2.json").write_text(json.dumps(ledger))
100 entries = load_ledger_entries(tmp_path)
101 assert len(entries) == 2
102
103 def test_skips_invalid_json(self, tmp_path: Path):
104 (tmp_path / "bad.json").write_text("not json")
105 (tmp_path / "good.json").write_text(json.dumps(_sample_ledger()))
106 entries = load_ledger_entries(tmp_path)
107 assert len(entries) == 1
108
109 def test_missing_dir_returns_empty(self, tmp_path: Path):
110 entries = load_ledger_entries(tmp_path / "nonexistent")
111 assert entries == []
112
113
114 class TestBuildBaselineReport:
115 def test_sufficient_runs(self):
116 entries = [_sample_ledger(timestamp=f"2026-06-{10 + i}T12:00:00Z") for i in range(5)]
117 report = build_baseline_report(entries, min_runs=5)
118 assert report.sufficient
119 assert report.total_runs == 5
120 assert len(report.stages) == 4
121
122 def test_insufficient_runs(self):
123 entries = [_sample_ledger() for _ in range(3)]
124 report = build_baseline_report(entries, min_runs=5)
125 assert not report.sufficient
126 assert report.total_runs == 3
127
128 def test_stage_durations_collected(self):
129 entries = [
130 _sample_ledger(github_duration=40.0),
131 _sample_ledger(github_duration=50.0),
132 _sample_ledger(github_duration=60.0),
133 _sample_ledger(github_duration=70.0),
134 _sample_ledger(github_duration=80.0),
135 ]
136 report = build_baseline_report(entries)
137 github_stage = next(s for s in report.stages if s.stage == "github_crawl")
138 assert github_stage.sample_count == 5
139 assert github_stage.min_seconds == 40.0
140 assert github_stage.max_seconds == 80.0
141
142
143 class TestTriggerThresholds:
144 def test_rss_not_triggered_below_threshold(self):
145 entries = [_sample_ledger(rss_duration=1.0) for _ in range(5)]
146 report = build_baseline_report(entries)
147 thresholds = check_trigger_thresholds(report)
148 assert not thresholds["rss_matrix_triggers"]["triggered"]
149
150 def test_rss_triggered_above_threshold(self):
151 entries = [_sample_ledger(rss_duration=65.0) for _ in range(5)]
152 report = build_baseline_report(entries)
153 thresholds = check_trigger_thresholds(report)
154 assert thresholds["rss_matrix_triggers"]["triggered"]
155
156 def test_github_shard_not_auto_triggered(self):
157 entries = [_sample_ledger() for _ in range(5)]
158 report = build_baseline_report(entries)
159 thresholds = check_trigger_thresholds(report)
160 # GitHub shard requires experiment comparison, never auto-triggers
161 assert not thresholds["github_shard_triggers"]["triggered"]
162
163 def test_baseline_sufficient_flag(self):
164 entries = [_sample_ledger() for _ in range(5)]
165 report = build_baseline_report(entries)
166 thresholds = check_trigger_thresholds(report)
167 assert thresholds["baseline_sufficient"]