Viewing files at commit bd39a563b7f5Commit detailsView latest

Browse files
tests/test_claim_eligibility.py 267 lines · 14.7 KB · python
1
"""Adversarial saved-baseline comparisons and claim eligibility controls."""
2
from dataclasses import replace
3
import contextlib
4
import io
5
import json
6
from pathlib import Path
7
import subprocess
8
import sys
9
import tempfile
10
import unittest
11
from unittest.mock import patch
12
13
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
14
import benchmark_stats
15
from oakbench.baseline_book import Target, book_json, cells_from_rows, load_baseline_book
16
from oakbench.populations import comparison_identity
17
from oakbench.scorecard import evaluate_targets
18
19
20
def rows(subject="git", **overrides):
21
    result = []
22
    for index in range(30):
23
        row = dict(subject=subject, subject_kind="git" if subject == "git" else "oak",
24
                   bench_id="campaign", run=index, lane="core", profile="standard",
25
                   scenario="tiny", operation="status.clean", returncode=0,
26
                   elapsed_ms=10.0 if subject == "git" else 1.0,
27
                   benchmark_track="core-equivalent", command_semantics_version=1,
28
                   fixture_version=1, scenario_version=1, runner_class="mac-arm",
29
                   cache_state="warm", env_isolation_version="1", load_tier="idle",
30
                   binary_sha256="git-sha" if subject == "git" else "oak-sha")
31
        row.update(overrides)
32
        result.append(row)
33
    return result
34
35
36
TARGET = Target("t", "core", "standard", "tiny", "status.clean", "oak_local", "git",
37
                "elapsed_ms", "median", "ratio_at_most", .8, {})
38
39
40
def aa_rows():
41
    """Interleaved Git A/A control (same Git, second subject name) for per-cell floors."""
42
    return [dict(row, subject="git_aa", subject_kind="git", elapsed_ms=10.0 + (0.05 if row["run"] % 2 else -0.05))
43
            for row in rows()]
44
45
46
def baseline(source=None):
47
    return cells_from_rows((rows() + aa_rows()) if source is None else source, lane="core", metrics=("elapsed_ms",),
48
                           statistics_names=("median",), noise_floor_pct=1.0, bootstrap_samples=20)[0]
49
50
51
def evaluate(candidate=None, cell=None, mode="claim"):
52
    cell = baseline() if cell is None else cell
53
    return evaluate_targets(rows("oak_local") if candidate is None else candidate,
54
                            [TARGET], {cell.key: cell}, mode=mode, bootstrap_samples=20)[0]
55
56
57
class ClaimEligibilityTest(unittest.TestCase):
58
    def test_comparable_independent_trials_allow_different_vcs_binary_hashes(self):
59
        result = evaluate()
60
        self.assertEqual(result["verdict"], "GREEN")
61
        self.assertTrue(result["claim_eligible"])
62
63
    def test_saved_baseline_mismatch_is_invalid_even_in_diagnostic_mode(self):
64
        for field, value in [("runner_class", "linux"), ("fixture_version", 2),
65
                             ("benchmark_track", "agent-default"), ("cache_state", "cold"),
66
                             ("remote_transport", "network"), ("load_tier", "cpu50"),
67
                             ("output_oracle_version", "different")]:
68
            with self.subTest(field=field):
69
                for mode in ("claim", "diagnostic"):
70
                    result = evaluate(rows("oak_local", **{field: value}), mode=mode)
71
                    self.assertEqual(result["verdict"], "INVALID")
72
                    self.assertFalse(result["claim_eligible"])
73
74
    def test_missing_and_duplicate_trials_cannot_manufacture_precision(self):
75
        candidates = [rows("oak_local")[:1], rows("oak_local", run=0),
76
                      rows("oak_local") + [rows("oak_local")[0]] * 1000]
77
        missing = rows("oak_local")
78
        del missing[0]["bench_id"]
79
        candidates.append(missing)
80
        for candidate in candidates:
81
            with self.subTest(n=len(candidate)):
82
                result = evaluate(candidate)
83
                self.assertEqual(result["verdict"], "AMBER")
84
                self.assertFalse(result["claim_eligible"])
85
86
    def test_unknown_baseline_evidence_cannot_be_public_green(self):
87
        for changes in ({"noise_floor_pct": None}, {"noise_floor_pct": float("nan")},
88
                        {"noise_floor_pct": -1}, {"comparison_identity": None},
89
                        {"comparison_identity": {}}, {"independent_n": 1},
90
                        {"attempted_n": None}, {"failure_n": 1}, {"ci_low": None}):
91
            with self.subTest(changes=changes):
92
                result = evaluate(cell=replace(baseline(), **changes))
93
                self.assertNotEqual(result["verdict"], "GREEN")
94
                self.assertFalse(result["claim_eligible"])
95
96
    def test_low_or_unknown_confidence_cannot_qualify_as_claim_evidence(self):
97
        for confidence in (None, 0.5, float("nan"), 1.0):
98
            with self.subTest(baseline_confidence=confidence):
99
                self.assertFalse(evaluate(cell=replace(baseline(), confidence=confidence))["claim_eligible"])
100
        cell = cells_from_rows(rows(), lane="core", metrics=("elapsed_ms",), statistics_names=("median",),
101
                               noise_floor_pct=1.0, bootstrap_samples=20, confidence=.5)[0]
102
        self.assertFalse(evaluate(cell=cell)["claim_eligible"])
103
        cell = baseline()
104
        result = evaluate_targets(rows("oak_local"), [TARGET], {cell.key: cell}, mode="claim",
105
                                  confidence=.5, bootstrap_samples=20)[0]
106
        self.assertEqual(result["verdict"], "AMBER")
107
        self.assertEqual(result["confidence"], .5)
108
        self.assertEqual(result["bootstrap_samples"], 20)
109
110
    def test_invalid_interval_arguments_rejected_before_scoring_or_building(self):
111
        for kwargs in ({"bootstrap_samples": 0}, {"bootstrap_samples": -1}, {"bootstrap_samples": True},
112
                       {"bootstrap_samples": 1.5}, {"confidence": 0}, {"confidence": 1},
113
                       {"confidence": float("nan")}, {"confidence": float("inf")}):
114
            with self.subTest(kwargs=kwargs):
115
                with self.assertRaises(ValueError):
116
                    evaluate_targets([], [], {}, **kwargs)
117
                with self.assertRaises(ValueError):
118
                    cells_from_rows([], lane="core", **kwargs)
119
120
    def test_uniformly_unknown_cache_cannot_be_claim_evidence(self):
121
        result = evaluate(rows("oak_local", cache_state="unknown"), baseline(rows(cache_state="unknown")))
122
        self.assertEqual(result["verdict"], "AMBER")
123
        self.assertIn("cache_state", " ".join(result["messages"]))
124
125
    def test_legacy_diagnostic_is_explicitly_not_claim_eligible(self):
126
        cell = replace(baseline(), comparison_identity=None, noise_floor_pct=None)
127
        result = evaluate(rows("oak_local")[:1], cell, mode="diagnostic")
128
        self.assertEqual(result["verdict"], "GREEN")
129
        self.assertFalse(result["claim_eligible"])
130
        self.assertIn("diagnostic only", " ".join(result["messages"]))
131
132
    def test_baseline_builder_preserves_failures_and_unmeasured_denominators(self):
133
        source = rows() + [dict(rows()[0], returncode=1), dict(rows()[0], elapsed_ms=None)]
134
        cell = baseline(source)
135
        self.assertEqual(cell.attempted_n, 32)
136
        self.assertEqual(cell.failure_n, 1)
137
        self.assertFalse(evaluate(cell=cell)["claim_eligible"])
138
139
    def test_global_operator_floor_is_not_claim_eligible_for_latency(self):
140
        # ADR-0011 / QA P1-5: no single floor for every cell on the claim path.
141
        cell = baseline(rows())
142
        self.assertEqual(cell.noise_floor_source, "operator_global")
143
        result = evaluate(cell=cell)
144
        self.assertFalse(result["claim_eligible"])
145
        self.assertTrue(any("per-cell A/A noise floor required" in m for m in result["claim_ineligibility_reasons"]))
146
        self.assertEqual(baseline().noise_floor_source, "per_cell_aa")
147
148
    def test_claim_verdicts_are_fdr_controlled_across_targets(self):
149
        from oakbench.scorecard import apply_claim_fdr
150
        results = [{"verdict": "GREEN", "p_value": 0.001, "claim_eligible": True},
151
                   {"verdict": "GREEN", "p_value": 0.2, "claim_eligible": True},
152
                   {"verdict": "AMBER", "p_value": None}]
153
        apply_claim_fdr(results, fdr=0.05)
154
        self.assertEqual([r["verdict"] for r in results], ["GREEN", "AMBER", "AMBER"])
155
        self.assertFalse(results[1]["claim_eligible"])
156
        self.assertEqual(results[0]["fdr_family_size"], 2)
157
158
    def test_baseline_builder_rejects_mixed_populations(self):
159
        with self.assertRaisesRegex(ValueError, "incompatible baseline population"):
160
            baseline(rows() + rows(runner_class="linux"))
161
162
    def test_book_v2_roundtrip_and_duplicate_keys_rejected(self):
163
        book = book_json([baseline()])
164
        self.assertEqual(book["schema_version"], 2)
165
        with tempfile.TemporaryDirectory() as tmp:
166
            path = Path(tmp) / "book.json"
167
            path.write_text(json.dumps(book))
168
            cell = next(iter(load_baseline_book(path).values()))
169
            self.assertTrue(evaluate(cell=cell)["claim_eligible"])
170
            book["cells"].append(book["cells"][0])
171
            path.write_text(json.dumps(book))
172
            with self.assertRaisesRegex(ValueError, "duplicate baseline cell"):
173
                load_baseline_book(path)
174
175
    def test_unknown_identity_version_rejected(self):
176
        book = book_json([baseline()])
177
        book["cells"][0]["comparison_identity"]["schema_version"] = 99
178
        with tempfile.TemporaryDirectory() as tmp:
179
            path = Path(tmp) / "book.json"
180
            path.write_text(json.dumps(book))
181
            with self.assertRaisesRegex(ValueError, "unsupported baseline comparison_identity"):
182
                load_baseline_book(path)
183
184
    def test_versioned_patch_guard_requires_actual_success(self):
185
        target = replace(TARGET, guards={"patch_apply_ok": True})
186
        cell = baseline(rows(patch_oracle_version="apply-tree-v1") + [dict(r, patch_oracle_version="apply-tree-v1") for r in aa_rows()])
187
        for evidence in (None, False, True):
188
            candidate = rows("oak_local", patch_oracle_version="apply-tree-v1", patch_apply_ok=evidence)
189
            result = evaluate_targets(candidate, [target], {cell.key: cell}, mode="claim", bootstrap_samples=20)[0]
190
            self.assertEqual(result["verdict"], "GREEN" if evidence is True else "INVALID")
191
192
    def test_failure_only_recall_guard_fails_without_crashing(self):
193
        target = replace(TARGET, guards={"information_recall_at_least": 1.0})
194
        cell = baseline()
195
        result = evaluate_targets(rows("oak_local", returncode=1), [target], {cell.key: cell})[0]
196
        self.assertEqual(result["verdict"], "INVALID")
197
198
    def test_nested_fixture_version_normalizes_but_subject_prompt_does_not_compare(self):
199
        row = rows("oak_local")[0]
200
        del row["fixture_version"]
201
        row["fixture"] = {"fixture_version": 1}
202
        row["task_prompt_id"] = "oak-specific-label"
203
        self.assertEqual(comparison_identity(row), comparison_identity(rows()[0]))
204
205
    def test_cli_defaults_to_claim_and_refuses_legacy_evidence(self):
206
        target = {"schema_version": 1, "targets": [dict(id="t", lane="core", profile="standard",
207
                  scenario="tiny", operation="status.clean", subject="oak_local", comparator="git",
208
                  metric="elapsed_ms", goal=dict(type="ratio_at_most", value=.8))]}
209
        with tempfile.TemporaryDirectory() as tmp:
210
            root = Path(tmp)
211
            (root / "rows.jsonl").write_text("\n".join(json.dumps(row) for row in rows("oak_local")))
212
            (root / "book.json").write_text(json.dumps(book_json([replace(baseline(), comparison_identity=None)])))
213
            (root / "targets.json").write_text(json.dumps(target))
214
            command = [sys.executable, "scripts/scorecard.py", str(root / "rows.jsonl"),
215
                       "--baseline-book", str(root / "book.json"), "--targets", str(root / "targets.json"), "--json"]
216
            proc = subprocess.run(command, capture_output=True, text=True)
217
            self.assertEqual(proc.returncode, 1)
218
            self.assertEqual(json.loads(proc.stdout)[0]["mode"], "claim")
219
220
221
class OutcomeReportingTest(unittest.TestCase):
222
    def test_failure_only_cells_and_unknown_outcomes_stay_visible(self):
223
        source = [dict(rows()[0], returncode=1, elapsed_ms=999),
224
                  dict(rows()[0], returncode=None), dict(rows()[0], returncode=77)]
225
        with tempfile.TemporaryDirectory() as tmp:
226
            path = Path(tmp) / "rows.jsonl"
227
            path.write_text("\n".join(json.dumps(row) for row in source))
228
            output = io.StringIO()
229
            with patch.object(sys, "argv", ["benchmark_stats.py", str(path)]), contextlib.redirect_stdout(output):
230
                self.assertEqual(benchmark_stats.main(), 0)
231
            self.assertIn("| `git` | `tiny/status.clean` | 2 | 0 | 1 | 1 | 1 | 0 |", output.getvalue())
232
            self.assertIn("| 0 | unmeasured |", output.getvalue())
233
            self.assertNotIn("999", output.getvalue())
234
235
236
237
class AgentProducerIdentityTest(unittest.TestCase):
238
    def test_producer_records_runner_and_explicit_remaining_claim_blockers(self):
239
        from types import SimpleNamespace
240
        import agent_workflow as aw
241
        import workflow_ab
242
        from oakbench.populations import missing_claim_identity
243
        with tempfile.TemporaryDirectory() as tmp:
244
            root = Path(tmp)
245
            stdout, stderr, prompt = [root / name for name in ("stdout", "stderr", "prompt")]
246
            for path in (stdout, stderr, prompt): path.write_text("")
247
            execution = aw.AgentExecution(0, False, 1, stdout, stderr, prompt)
248
            oracle = SimpleNamespace(elapsed_ms=1, vcs_ms=1, test_ms=1, success=True, checks={}, failure_reason=None,
249
                changed_files=[], test_returncode=0, test_output_bytes=0, dirty_files_at_end=0, commit_delta=1,
250
                bytes_written=0, artifact_paths=[])
251
            with patch.object(aw, "runner_fields", return_value={"runner_class": "dedicated-test", "runner_id": "observed", "runner_profile": {}}):
252
                row = aw.make_row("synthetic", "2026-09-11T00:00:00Z", "test",
253
                    aw.AgentConfig("test", "codex_exec_json", "provider", Path(sys.executable), "version", "model"),
254
                    workflow_ab.Subject("git", "git", "Git", Path("/git")), workflow_ab.WORKFLOWS["bugfix_test_loop"],
255
                    "agent-default", 0, root, root, 0, 0, execution, oracle, "task", {}, {}, {}, "minimal", "full-docs")
256
            self.assertEqual(aw.validate_row(row), [])
257
            identity = comparison_identity(row)
258
            self.assertEqual(identity["runner_class"], "dedicated-test")
259
            self.assertEqual(identity["oracle_version"], aw.ORACLE_VERSION)
260
            self.assertEqual(row["cache_state_reason"], "cache_state_unset")
261
            self.assertIsNone(row["calibration_version"])
262
            self.assertTrue(row["calibration_unmeasured_reason"])
263
            self.assertEqual(missing_claim_identity(identity, "agent", "elapsed_ms", "agent.workflow"), ["cache_state"])
264
            self.assertEqual(missing_claim_identity(identity, "agent", "tokens", "agent.workflow"), ["calibration_version"])
265
266
if __name__ == "__main__":
267
    unittest.main()