Viewing files at commit 63ed254f5df1Commit detailsView latest

Browse files
tests/test_devloop.py 381 lines · 15.3 KB · python
1
"""Focused regression tests for devloop verdict gates."""
2
3
from __future__ import annotations
4
5
import importlib.util
6
import sys
7
import tempfile
8
import unittest
9
from pathlib import Path
10
from unittest import mock
11
12
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
13
14
15
def load_devloop():
16
    path = Path(__file__).resolve().parents[1] / "scripts" / "devloop.py"
17
    spec = importlib.util.spec_from_file_location("devloop", path)
18
    module = importlib.util.module_from_spec(spec)
19
    sys.modules["devloop"] = module
20
    spec.loader.exec_module(module)
21
    return module
22
23
24
def row(subject: str, operation: str, *, returncode: int = 0, elapsed_ms: float = 10.0) -> dict:
25
    return {
26
        "subject": subject,
27
        "scenario": "tiny_text",
28
        "operation": operation,
29
        "run": 0,
30
        "returncode": returncode,
31
        "elapsed_ms": elapsed_ms,
32
        "vcs_tool_call_count": 1,
33
        "tool_call_count": 1,
34
        "raw_output_bytes": 10,
35
        "estimated_tokens_total": 3,
36
    }
37
38
39
class DevloopVerdictTest(unittest.TestCase):
40
    def setUp(self) -> None:
41
        self.devloop = load_devloop()
42
43
    def compare(self, rows: list[dict]) -> list[str]:
44
        regressions, _ = self.devloop.compare_subjects(
45
            rows,
46
            "oak_local",
47
            "oak_installed",
48
            "Oak baseline",
49
            latency_threshold_pct=10.0,
50
            min_delta_ms=5.0,
51
            efficiency_gates=True,
52
            output_bytes_threshold_pct=25.0,
53
        )
54
        return regressions
55
56
    def test_all_failing_target_is_regression_even_without_target_latency_median(self) -> None:
57
        rows = [
58
            row("oak_installed", "status.dirty", returncode=0, elapsed_ms=10.0),
59
            row("oak_installed", "status.dirty", returncode=0, elapsed_ms=11.0),
60
            row("oak_local", "status.dirty", returncode=1, elapsed_ms=100.0),
61
            row("oak_local", "status.dirty", returncode=1, elapsed_ms=101.0),
62
        ]
63
64
        regressions = self.compare(rows)
65
66
        self.assertTrue(any("NO SUCCESS" in item and "status.dirty" in item for item in regressions))
67
68
    def test_baseline_flake_does_not_hide_higher_target_failure_rate(self) -> None:
69
        rows = [
70
            row("oak_installed", "status.dirty", returncode=0, elapsed_ms=10.0),
71
            row("oak_installed", "status.dirty", returncode=0, elapsed_ms=11.0),
72
            row("oak_installed", "status.dirty", returncode=1, elapsed_ms=12.0),
73
            row("oak_local", "status.dirty", returncode=0, elapsed_ms=10.0),
74
            row("oak_local", "status.dirty", returncode=1, elapsed_ms=12.0),
75
            row("oak_local", "status.dirty", returncode=1, elapsed_ms=13.0),
76
        ]
77
78
        regressions = self.compare(rows)
79
80
        self.assertTrue(any("FAIL-RATE" in item and "2/3" in item for item in regressions))
81
82
    def test_embedded_null_excludes_probe_and_failed_or_skipped_groups(self) -> None:
83
        rows = [
84
            {**row("oak_installed", "read.file", elapsed_ms=100.0), "vcs_tool_call_count": 0},
85
            {**row("oak_local", "read.file", elapsed_ms=100.0), "vcs_tool_call_count": 0},
86
            {**row("oak_installed", "diff.dirty.inforecall", elapsed_ms=100.0), "vcs_tool_call_count": 0},
87
            {**row("oak_local", "diff.dirty.inforecall", elapsed_ms=900.0), "vcs_tool_call_count": 0},
88
            {**row("oak_installed", "test.step", elapsed_ms=100.0), "vcs_tool_call_count": 0},
89
            {**row("oak_local", "test.step", elapsed_ms=900.0), "vcs_tool_call_count": 0},
90
            {**row("oak_local", "test.step", returncode=1, elapsed_ms=901.0), "vcs_tool_call_count": 0},
91
            {**row("oak_installed", "skipped.step", elapsed_ms=100.0), "vcs_tool_call_count": 0},
92
            {**row("oak_local", "skipped.step", elapsed_ms=900.0), "vcs_tool_call_count": 0},
93
            {
94
                **row("oak_local", "skipped.step", returncode=self.devloop.SKIP_RETURNCODE, elapsed_ms=0.0),
95
                "vcs_tool_call_count": 0,
96
                "skipped": True,
97
            },
98
        ]
99
100
        null_pct, null_ops = self.devloop.embedded_null_p95(rows, "oak_local", "oak_installed")
101
102
        self.assertEqual(null_pct, 0.0)
103
        self.assertEqual(null_ops, 1)
104
105
    def test_noise_floor_command_skips_remote_operations(self) -> None:
106
        captured: list[list[str]] = []
107
108
        def fake_run_lane(command: list[str], label: str) -> None:
109
            captured.append(command)
110
111
        with (
112
            mock.patch.object(self.devloop, "run_lane", side_effect=fake_run_lane),
113
            mock.patch.object(self.devloop, "load_jsonl", return_value=[]),
114
        ):
115
            self.devloop.measure_noise_floor(Path("/tmp/aa"), Path("/bin/oak"), 3)
116
117
        self.assertIn("--skip-remote", captured[0])
118
119
    def test_contention_preflight_includes_workspace_and_clone_push_gate(self) -> None:
120
        requirements = self.devloop.remote_preflight_requirements(["contention"])
121
122
        self.assertEqual(len(requirements), 1)
123
        operations = set(requirements[0].operations)
124
        self.assertIn("contention_workspace_per_task_w2/parallel.total", operations)
125
        self.assertIn("contention_workspace_per_task_w8/parallel.total", operations)
126
        self.assertIn("contention_clone_push_storm_w2/parallel.total", operations)
127
        self.assertIn("contention_clone_push_storm_w8/parallel.total", operations)
128
129
    def test_contention_preflight_uses_custom_launch_worker_tiers(self) -> None:
130
        requirements = self.devloop.remote_preflight_requirements(
131
            ["contention"],
132
            contention_modes=["clone_push_storm"],
133
            contention_workers=["16", "128"],
134
        )
135
136
        operations = set(requirements[0].operations)
137
        self.assertEqual(
138
            operations,
139
            {
140
                "contention_clone_push_storm_w16/parallel.total",
141
                "contention_clone_push_storm_w128/parallel.total",
142
            },
143
        )
144
145
    def test_contention_command_runs_clone_push_storm_by_default(self) -> None:
146
        import tempfile
147
148
        captured: list[list[str]] = []
149
150
        def fake_run_lane(command: list[str], label: str) -> None:
151
            captured.append(command)
152
153
        with tempfile.TemporaryDirectory() as tmp:
154
            argv = [
155
                "devloop.py",
156
                "--lanes",
157
                "contention",
158
                "--skip-noise-floor",
159
                "--skip-build",
160
                "--oak-baseline-bin",
161
                "/bin/echo",
162
                "--oak-local-bin",
163
                "/bin/echo",
164
                "--results",
165
                tmp,
166
            ]
167
            with (
168
                mock.patch.object(sys, "argv", argv),
169
                mock.patch.object(self.devloop, "run_lane", side_effect=fake_run_lane),
170
                mock.patch.object(self.devloop, "load_jsonl", return_value=[]),
171
                mock.patch.object(self.devloop.oakbench_remotes, "print_remote_preflight_warnings"),
172
            ):
173
                rc = self.devloop.main()
174
175
        self.assertEqual(rc, 0)
176
        self.assertEqual(len(captured), 1)
177
        command = captured[0]
178
        self.assertIn("--modes", command)
179
        self.assertEqual(
180
            command[command.index("--modes") + 1],
181
            self.devloop.DEFAULT_CONTENTION_MODES,
182
        )
183
        self.assertIn("clone_push_storm", command[command.index("--modes") + 1])
184
185
    def test_diagnostic_probe_failure_does_not_trip_regression_gate(self) -> None:
186
        rows = [
187
            row("oak_installed", "status.dirty.determinism", returncode=0, elapsed_ms=10.0),
188
            row("oak_local", "status.dirty.determinism", returncode=1, elapsed_ms=11.0),
189
            row("oak_installed", "status.dirty", returncode=0, elapsed_ms=10.0),
190
            row("oak_local", "status.dirty", returncode=0, elapsed_ms=11.0),
191
        ]
192
193
        regressions = self.compare(rows)
194
195
        self.assertFalse(any("status.dirty.determinism" in item for item in regressions))
196
197
    def test_load_jsonl_reports_controlled_input_errors(self) -> None:
198
        with tempfile.TemporaryDirectory() as tmp:
199
            root = Path(tmp)
200
            with self.assertRaises(SystemExit) as missing:
201
                self.devloop.load_jsonl(root / "missing.jsonl")
202
            self.assertIn("result JSONL missing", str(missing.exception))
203
204
            empty = root / "empty.jsonl"
205
            empty.write_text("\n")
206
            with self.assertRaises(SystemExit) as empty_error:
207
                self.devloop.load_jsonl(empty)
208
            self.assertIn("contains no rows", str(empty_error.exception))
209
210
            malformed = root / "malformed.jsonl"
211
            malformed.write_text('{"ok": true}\nnot json\n')
212
            with self.assertRaises(SystemExit) as malformed_error:
213
                self.devloop.load_jsonl(malformed)
214
            self.assertIn("malformed result JSONL", str(malformed_error.exception))
215
            self.assertIn(":2:", str(malformed_error.exception))
216
217
            not_object = root / "not-object.jsonl"
218
            not_object.write_text("[1, 2, 3]\n")
219
            with self.assertRaises(SystemExit) as not_object_error:
220
                self.devloop.load_jsonl(not_object)
221
            self.assertIn("row is not an object", str(not_object_error.exception))
222
223
            valid = root / "valid.jsonl"
224
            valid.write_text('{"ok": true}\n\n')
225
            self.assertEqual(self.devloop.load_jsonl(valid), [{"ok": True}])
226
227
228
class DevloopScorecardTest(unittest.TestCase):
229
    def setUp(self) -> None:
230
        self.devloop = load_devloop()
231
232
    def test_scorecard_lines_explain_missing_book_and_targets(self) -> None:
233
        import json
234
        import tempfile
235
236
        with tempfile.TemporaryDirectory() as tmp:
237
            root = Path(tmp)
238
            self.assertIn("config/targets.json missing", self.devloop.scorecard_lines([], root)[0])
239
240
            (root / "config").mkdir()
241
            (root / "config" / "targets.json").write_text(json.dumps({"schema_version": 1, "targets": []}))
242
            self.assertIn("baselines/git/CURRENT missing", self.devloop.scorecard_lines([], root)[0])
243
244
    def test_scorecard_lines_render_verdicts_from_current_book(self) -> None:
245
        import json
246
        import tempfile
247
248
        with tempfile.TemporaryDirectory() as tmp:
249
            root = Path(tmp)
250
            (root / "config").mkdir()
251
            (root / "config" / "targets.json").write_text(
252
                json.dumps(
253
                    {
254
                        "schema_version": 1,
255
                        "targets": [
256
                            {
257
                                "id": "t",
258
                                "lane": "core",
259
                                "profile": "smoke",
260
                                "scenario": "tiny_text",
261
                                "operation": "status.clean",
262
                                "subject": "oak_local",
263
                                "comparator": "git",
264
                                "metric": "elapsed_ms",
265
                                "goal": {"type": "ratio_at_most", "value": 0.8},
266
                                "guards": {},
267
                            }
268
                        ],
269
                    }
270
                )
271
            )
272
            book_dir = root / "baselines" / "git" / "test-book"
273
            book_dir.mkdir(parents=True)
274
            (root / "baselines" / "git" / "CURRENT").write_text("test-book\n")
275
            (book_dir / "book.json").write_text(
276
                json.dumps(
277
                    {
278
                        "schema_version": 1,
279
                        "cells": [
280
                            {
281
                                "lane": "core",
282
                                "profile": "smoke",
283
                                "scenario": "tiny_text",
284
                                "operation": "status.clean",
285
                                "subject": "git",
286
                                "metric": "elapsed_ms",
287
                                "statistic": "median",
288
                                "value": 10.0,
289
                                "n": 50,
290
                            }
291
                        ],
292
                    }
293
                )
294
            )
295
            core_row = {
296
                "profile": "smoke",
297
                "scenario": "tiny_text",
298
                "operation": "status.clean",
299
                "subject": "oak_local",
300
                "subject_kind": "oak",
301
                "returncode": 0,
302
                "elapsed_ms": 5.0,
303
                "command": ["oak", "status"],
304
            }
305
            lines = self.devloop.scorecard_lines([core_row, core_row], root)
306
            self.assertEqual(len(lines), 1)
307
            self.assertIn("GREEN", lines[0])
308
309
    def test_scorecard_lines_applies_live_noise_floor_to_loaded_book(self) -> None:
310
        import json
311
        import tempfile
312
313
        with tempfile.TemporaryDirectory() as tmp:
314
            root = Path(tmp)
315
            (root / "config").mkdir()
316
            (root / "config" / "targets.json").write_text(
317
                json.dumps(
318
                    {
319
                        "schema_version": 1,
320
                        "targets": [
321
                            {
322
                                "id": "t",
323
                                "lane": "core",
324
                                "profile": "smoke",
325
                                "scenario": "tiny_text",
326
                                "operation": "status.clean",
327
                                "subject": "oak_local",
328
                                "comparator": "git",
329
                                "metric": "elapsed_ms",
330
                                "goal": {"type": "ratio_at_most", "value": 0.99},
331
                                "guards": {},
332
                            }
333
                        ],
334
                    }
335
                )
336
            )
337
            book_dir = root / "baselines" / "git" / "test-book"
338
            book_dir.mkdir(parents=True)
339
            (root / "baselines" / "git" / "CURRENT").write_text("test-book\n")
340
            (book_dir / "book.json").write_text(
341
                json.dumps(
342
                    {
343
                        "schema_version": 1,
344
                        "cells": [
345
                            {
346
                                "lane": "core",
347
                                "profile": "smoke",
348
                                "scenario": "tiny_text",
349
                                "operation": "status.clean",
350
                                "subject": "git",
351
                                "metric": "elapsed_ms",
352
                                "statistic": "median",
353
                                "value": 10.0,
354
                                "n": 50,
355
                                "noise_floor_pct": None,
356
                            }
357
                        ],
358
                    }
359
                )
360
            )
361
            core_row = {
362
                "profile": "smoke",
363
                "scenario": "tiny_text",
364
                "operation": "status.clean",
365
                "subject": "oak_local",
366
                "subject_kind": "oak",
367
                "returncode": 0,
368
                "elapsed_ms": 9.7,
369
                "command": ["oak", "status"],
370
            }
371
            no_noise = self.devloop.scorecard_lines([core_row, core_row], root, noise_floor_pct=0.0)
372
            self.assertIn("GREEN", no_noise[0])
373
            self.assertIn("`t`", no_noise[0])
374
            self.assertIn(
375
                "UNDECIDABLE",
376
                self.devloop.scorecard_lines([core_row, core_row], root, noise_floor_pct=5.0)[0],
377
            )
378
379
380
if __name__ == "__main__":
381
    unittest.main()