Viewing files at commit f0c34b306c79Commit detailsView latest

Browse files
tests/test_campaign_report.py 275 lines · 16.0 KB · python
1
import copy
2
from pathlib import Path
3
import sys
4
import unittest
5
6
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
7
from oakbench.campaign_report import report_campaign, render_markdown
8
9
10
def plan(repetitions=2):
11
    return {"schema_version": 1, "campaign_id": "example", "plan_sha256": "digest", "manifest": {},
12
            "schedule": [dict(trial_id=f"{rep}-{subject}", block_id=f"block-{rep}", ordinal=rep * 2 + index,
13
                              repetition=rep, workflow="wide_config_refactor", instruction_level="zero-shot",
14
                              agent="codex", model="pinned-model", subject=subject)
15
                         for rep in range(repetitions) for index, subject in enumerate(("git", "oak"))]}
16
17
18
def event(trial_id, kind, **extra):
19
    return dict(event=kind, trial_id=trial_id, plan_sha256="digest", timestamp_utc="2026-09-10T00:00:00Z", **extra)
20
21
22
def outcome(trial_id, name="pass", tokens=10):
23
    return [event(trial_id, "started"), event(trial_id, "finished", row={
24
        "outcome": name, "success": name == "pass", "wall_clock_metrics": {"agent_active_ms": 20, "total_ms": 30},
25
        "token_metrics": {"total_tokens_reported": tokens, "cost_weighted_total": 40},
26
        "turn_metrics": {"assistant_turns_total": 2}, "tool_call_metrics": {"tool_calls_total": 3},
27
    })]
28
29
30
class CampaignReportTest(unittest.TestCase):
31
    def report(self, configuration, events):
32
        return report_campaign(configuration, events, bootstrap_samples=100, seed=7)
33
34
    def test_schedule_and_started_denominators_survive_interruption(self):
35
        summary = self.report(plan(), outcome("0-git") + outcome("0-oak", "fail") + [event("1-git", "started")])
36
        self.assertEqual(summary["counts"], dict(scheduled=4, started=3, finished=2, budget_truncated=0, success=1, failure=1,
37
                         timeout=0, infrastructure_failure=0, interrupted=1, skipped=0, unstarted=1))
38
        subjects = {item["subject"]: item for item in summary["groups"][0]["subjects"]}
39
        self.assertEqual(subjects["git"]["verified_success_fraction_of_started"], .5)
40
        self.assertIsNone(subjects["git"]["success_wilson_interval"])
41
        self.assertIsNone(subjects["git"]["resources"]["reported_tokens"]["complete_total"])
42
        self.assertEqual(summary["groups"][0]["paired_comparisons"][0]["incomplete_pairs"], 1)
43
        self.assertFalse(summary["claim_eligible"])
44
45
    def test_legacy_shortened_timeout_is_not_a_complete_pair(self):
46
        configuration = plan(1)
47
        configuration["manifest"]["trial_timeout_seconds"] = 60
48
        events = outcome("0-git") + outcome("0-oak", "timeout")
49
        events[0]["timeout_seconds"] = 60
50
        events[2]["timeout_seconds"] = .15
51
        summary = self.report(configuration, events)
52
        self.assertEqual(summary["counts"]["budget_truncated"], 1)
53
        self.assertEqual(summary["counts"]["interrupted"], 1)
54
        pair = summary["groups"][0]["paired_comparisons"][0]
55
        self.assertEqual(pair["complete_pairs"], 0)
56
        self.assertIsNone(pair["success_delta_percentage_points"])
57
58
    def test_missing_instrument_is_unmeasured_not_a_paired_failure(self):
59
        events = outcome("0-git", "error") + outcome("0-oak")
60
        events[1]["row"].update(returncode=77, skip_reason="required oracle unavailable")
61
        summary = self.report(plan(1), events)
62
        self.assertEqual(summary["counts"]["skipped"], 1)
63
        self.assertEqual(summary["counts"]["started"], 2)
64
        pair = summary["groups"][0]["paired_comparisons"][0]
65
        self.assertEqual(pair["complete_pairs"], 0)
66
        self.assertIsNone(pair["success_delta_percentage_points"])
67
        git = next(subject for subject in summary["groups"][0]["subjects"] if subject["subject"] == "git")
68
        self.assertIsNone(git["success_wilson_interval"])
69
70
    def test_fixture_invalidated_trial_is_not_a_complete_paired_failure(self):
71
        events = outcome("0-git") + [event("0-oak", "started"), event("0-oak", "finished",
72
            outcome="error", measurement_invalidated=True, invalidation_reason="fixture content drift",
73
            rejected_row={"outcome": "pass", "success": True})]
74
        summary = self.report(plan(1), events)
75
        self.assertEqual(summary["counts"]["skipped"], 1)
76
        self.assertEqual(summary["invalidated_count"], 1)
77
        self.assertEqual(summary["invalidated_trials"][0]["reason"], "fixture content drift")
78
        self.assertTrue(summary["invalidated_trials"][0]["rejected_row_retained"])
79
        self.assertIn("0-oak: fixture content drift", render_markdown(summary))
80
        self.assertEqual(summary["counts"]["infrastructure_failure"], 0)
81
        pair = summary["groups"][0]["paired_comparisons"][0]
82
        self.assertEqual(pair["complete_pairs"], 0)
83
        self.assertIsNone(pair["success_delta_percentage_points"])
84
85
    def test_infrastructure_attempts_stay_resources_not_success_pairs(self):
86
        events = outcome("0-git") + outcome("1-git")
87
        for trial in ("0-oak", "1-oak"):
88
            events.extend([event(trial, "started"), event(trial, "finished", outcome="error", error="harness setup failed")])
89
        summary = self.report(plan(2), events)
90
        pair = summary["groups"][0]["paired_comparisons"][0]
91
        self.assertEqual(pair["complete_pairs"], 0)
92
        self.assertIsNone(pair["success_delta_percentage_points"])
93
        self.assertIn("schedule_or_attempt_accounting_incomplete", summary["claim_ineligibility_reasons"])
94
        oak = next(item for item in summary["groups"][0]["subjects"] if item["subject"] == "oak")
95
        self.assertIsNone(oak["success_wilson_interval"])
96
        self.assertEqual(oak["resources"]["reported_tokens"]["attempted_n"], 2)
97
98
    def test_measured_precommit_failure_keeps_pair_but_success_proof_classes_must_match(self):
99
        events = outcome("0-git") + outcome("0-oak", "fail")
100
        events[1]["row"].update(workflow_integrity_passed=True, workflow_integrity_evidence_class="independent_committed_tree")
101
        events[3]["row"].update(workflow_integrity_passed=False, workflow_integrity_evidence_class="trusted_worktree_payload", agent_launched=True, outcome_attribution="agent")
102
        summary = self.report(plan(1), events)
103
        pair = summary["groups"][0]["paired_comparisons"][0]
104
        self.assertEqual(pair["complete_pairs"], 1)
105
        self.assertEqual(pair["success_delta_percentage_points"], -100)
106
        events[3]["row"].update(outcome="pass", success=True, workflow_integrity_passed=True)
107
        with self.assertRaisesRegex(ValueError, "workflow_integrity_evidence_class"):
108
            self.report(plan(1), events)
109
110
    def test_every_outcome_remains_visible(self):
111
        configuration = plan(4)
112
        events = (outcome("0-git") + outcome("0-oak", "fail") + outcome("1-git", "timeout") +
113
                  [event("1-oak", "started"), event("1-oak", "finished", outcome="error", error="crash"),
114
                   event("2-git", "started"), event("2-oak", "finished", outcome="skipped")])
115
        counts = self.report(configuration, events)["counts"]
116
        for field in ("success", "failure", "timeout", "infrastructure_failure", "interrupted", "skipped"):
117
            self.assertEqual(counts[field], 1)
118
        self.assertEqual(counts["unstarted"], 2)
119
        self.assertEqual(sum(counts[name] for name in ("success", "failure", "timeout", "infrastructure_failure", "interrupted", "skipped", "unstarted")), counts["scheduled"])
120
121
    def test_paired_bootstrap_preserves_block_pairs_and_is_seeded(self):
122
        events = outcome("0-git") + outcome("0-oak", "fail") + outcome("1-git", "fail") + outcome("1-oak")
123
        first = self.report(plan(), events)
124
        second = self.report(plan(), events)
125
        self.assertEqual(first, second)
126
        pair = first["groups"][0]["paired_comparisons"][0]
127
        self.assertEqual(pair["success_delta_percentage_points"], 0)
128
        self.assertEqual(pair["confidence_interval"], [-100, 100])
129
        self.assertEqual(pair["complete_pairs"], 2)
130
        self.assertFalse(pair["multiplicity_adjusted"])
131
132
    def test_singleton_pair_does_not_get_zero_width_interval(self):
133
        pair = self.report(plan(1), outcome("0-git") + outcome("0-oak"))["groups"][0]["paired_comparisons"][0]
134
        self.assertEqual(pair["success_delta_percentage_points"], 0)
135
        self.assertIsNone(pair["confidence_interval"])
136
137
    def test_missing_usage_does_not_become_zero_or_complete_total(self):
138
        summary = self.report(plan(), outcome("0-git", tokens=None) + outcome("1-git", tokens=0))
139
        values = summary["groups"][0]["subjects"][0]["resources"]["reported_tokens"]
140
        self.assertEqual(values["measured_n"], 1)
141
        self.assertEqual(values["missing_n"], 1)
142
        self.assertEqual(values["observed_sum"], 0)
143
        self.assertIsNone(values["complete_total"])
144
        self.assertIsNone(values["observed_cost_per_success"])
145
        self.assertEqual(values["success_conditioned"]["median"], 0)
146
147
    def test_partial_capture_counters_are_not_complete_usage(self):
148
        for changes in ({"outcome": "timeout", "success": False}, {"output_truncated": True},
149
                        {"source": {"agent_execution": {"capture_complete": False}}},
150
                        {"source": {"agent_execution": {"usage_complete": False}}},
151
                        {"source": {"agent_execution": {"counters_coverage": "observed_lower_bounds"}}},
152
                        {"source": {"agent_execution": {"output_limited": True}}}):
153
            events = outcome("0-git")
154
            events[-1]["row"].update(changes)
155
            resources = self.report(plan(1), events)["groups"][0]["subjects"][0]["resources"]
156
            for metric in ("reported_tokens", "assistant_turns", "tool_calls"):
157
                self.assertIsNone(resources[metric]["complete_total"])
158
                self.assertEqual(resources[metric]["missing_n"], 1)
159
            self.assertEqual(resources["agent_active_ms"]["complete_total"], 20)
160
161
    def test_capture_error_row_keeps_infrastructure_denominator_and_time(self):
162
        events = outcome("0-git")
163
        events[-1]["outcome"] = "error"
164
        events[-1]["row"].update(outcome="error", success=False, output_truncated=True)
165
        summary = self.report(plan(1), events)
166
        self.assertEqual(summary["counts"]["infrastructure_failure"], 1)
167
        resources = summary["groups"][0]["subjects"][0]["resources"]
168
        self.assertEqual(resources["agent_active_ms"]["complete_total"], 20)
169
        self.assertIsNone(resources["tool_calls"]["complete_total"])
170
171
    def test_failures_contribute_resource_cost_but_not_success_latency(self):
172
        events = outcome("0-git", tokens=10) + outcome("1-git", "fail", tokens=50)
173
        subject = self.report(plan(), events)["groups"][0]["subjects"][0]
174
        values = subject["resources"]["reported_tokens"]
175
        self.assertEqual(values["complete_total"], 60)
176
        self.assertEqual(values["observed_cost_per_success"], 60)
177
        self.assertEqual(values["success_conditioned"]["median"], 10)
178
        self.assertAlmostEqual(subject["success_wilson_interval"][0], .094531, places=5)
179
        self.assertAlmostEqual(subject["success_wilson_interval"][1], .905469, places=5)
180
181
    def test_zero_success_or_no_attempts_keeps_undefined_metrics_null(self):
182
        summary = self.report(plan(1), outcome("0-git", "fail"))
183
        git, oak = summary["groups"][0]["subjects"]
184
        self.assertIsNone(git["resources"]["reported_tokens"]["observed_cost_per_success"])
185
        self.assertIsNone(oak["verified_success_fraction_of_started"])
186
        self.assertIsNone(oak["resources"]["reported_tokens"]["observed_sum"])
187
        self.assertIn("unmeasured", render_markdown(summary))
188
189
    def test_instruction_and_model_conditions_never_pool(self):
190
        configuration = plan()
191
        configuration["schedule"][2]["instruction_level"] = "full-docs"
192
        configuration["schedule"][3]["model"] = "other-model"
193
        summary = self.report(configuration, [])
194
        self.assertEqual(len(summary["groups"]), 3)
195
196
    def test_duplicate_or_foreign_journal_evidence_rejected(self):
197
        for events in ([event("0-git", "started")] * 2,
198
                       outcome("0-git") + [outcome("0-git")[1]],
199
                       [event("unknown", "started")],
200
                       [dict(event("0-git", "started"), plan_sha256="wrong")],
201
                       [outcome("0-git")[1]],
202
                       [event("0-git", "other")]):
203
            with self.subTest(events=events), self.assertRaises(ValueError):
204
                self.report(plan(), events)
205
206
    def test_contradictory_result_does_not_count_as_success(self):
207
        events = outcome("0-git")
208
        events[1]["row"]["outcome"] = "fail"
209
        with self.assertRaises(ValueError):
210
            self.report(plan(), events)
211
        events = outcome("0-git")
212
        events[1]["row"]["agent"] = {"model": "unplanned"}
213
        with self.assertRaises(ValueError):
214
            self.report(plan(), events)
215
216
    def test_mock_plan_never_becomes_claim_eligible(self):
217
        configuration = plan(1)
218
        for trial in configuration["schedule"]:
219
            trial.update(agent="mock", model=None)
220
        summary = self.report(configuration, outcome("0-git") + outcome("0-oak"))
221
        self.assertFalse(summary["claim_eligible"])
222
        self.assertIn("mock_agent_measurements_are_instrument_checks", summary["claim_ineligibility_reasons"])
223
224
    def test_duplicate_scheduled_subject_block_is_rejected(self):
225
        configuration = plan()
226
        duplicate = copy.deepcopy(configuration["schedule"][0])
227
        duplicate["trial_id"] = "new-id-same-block-subject"
228
        configuration["schedule"].append(duplicate)
229
        with self.assertRaises(ValueError):
230
            self.report(configuration, [])
231
232
    def test_runtime_environment_and_version_drift_cannot_pool_or_pair(self):
233
        events = outcome("0-git") + outcome("1-git")
234
        events[-1]["row"]["agent"] = {"environment": "local-default"}
235
        with self.assertRaisesRegex(ValueError, "incompatible campaign population"):
236
            self.report(plan(), events)
237
238
    def test_row_cannot_be_reassigned_to_another_trial(self):
239
        for field, value in (("task_id", "other"), ("bench_id", "other"), ("run", 99)):
240
            events = outcome("0-git")
241
            events[-1]["row"][field] = value
242
            with self.subTest(field=field), self.assertRaisesRegex(ValueError, "row trial identity"):
243
                self.report(plan(), events)
244
        events = outcome("0-git")
245
        events[-1]["outcome"] = "fail"
246
        with self.assertRaisesRegex(ValueError, "event outcome contradicts"):
247
            self.report(plan(), events)
248
249
    def test_efficiency_is_paired_and_only_joint_success_with_same_sources(self):
250
        events = outcome("0-git", tokens=10) + outcome("0-oak", tokens=20) + outcome("1-git", tokens=100) + outcome("1-oak", tokens=110)
251
        for entry in events:
252
            if "row" in entry:
253
                entry["row"]["token_metrics"]["token_source"] = "provider_reported"
254
                entry["row"]["wall_clock_metrics"]["measurement_source"] = "outer_cli"
255
        pair = self.report(plan(), events)["groups"][0]["paired_comparisons"][0]
256
        tokens = pair["success_conditioned_efficiency"]["reported_tokens"]
257
        self.assertEqual(tokens["candidate_minus_reference_mean_delta"], 10)
258
        self.assertEqual(tokens["confidence_interval"], [10, 10])
259
        self.assertEqual(tokens["measured_pairs"], 2)
260
        events[-1]["row"]["token_metrics"]["token_source"] = "other_convention"
261
        tokens = self.report(plan(), events)["groups"][0]["paired_comparisons"][0]["success_conditioned_efficiency"]["reported_tokens"]
262
        self.assertEqual(tokens["measured_pairs"], 1)
263
        self.assertEqual(tokens["incompatible_or_unknown_source_pairs"], 1)
264
        self.assertIsNone(tokens["confidence_interval"])
265
        events[-1]["row"].update(success=False, outcome="fail")
266
        tokens = self.report(plan(), events)["groups"][0]["paired_comparisons"][0]["success_conditioned_efficiency"]["reported_tokens"]
267
        self.assertEqual(tokens["jointly_successful_pairs"], 1)
268
        events = outcome("0-git") + outcome("0-oak")
269
        events[-1]["row"]["oracle_version"] = "different"
270
        with self.assertRaisesRegex(ValueError, "incompatible paired evidence"):
271
            self.report(plan(), events)
272
273
274
if __name__ == "__main__":
275
    unittest.main()