bigpowers 2.89.1 → 2.90.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,309 @@
1
+ #!/usr/bin/env python3
2
+ # story: e58s01
3
+ """tests/test_ab_eval.py — A/B eval runner (vanilla vs bigpowers) with a fake adapter.
4
+
5
+ Run: python3 tests/test_ab_eval.py
6
+ No agent is called: every adapter here is a small shell command.
7
+ """
8
+ import importlib.util
9
+ import io
10
+ import json
11
+ import shutil
12
+ import subprocess
13
+ import sys
14
+ import tempfile
15
+ import unittest
16
+ from contextlib import redirect_stdout
17
+ from pathlib import Path
18
+
19
+ import yaml
20
+
21
+ REPO_ROOT = Path(__file__).resolve().parents[1]
22
+ LIB = REPO_ROOT / "scripts" / "lib"
23
+
24
+
25
+ def load_module(name: str, filename: str):
26
+ spec = importlib.util.spec_from_file_location(name, LIB / filename)
27
+ assert spec is not None and spec.loader is not None
28
+ mod = importlib.util.module_from_spec(spec)
29
+ sys.modules[name] = mod
30
+ spec.loader.exec_module(mod)
31
+ return mod
32
+
33
+
34
+ ab = load_module("ab_eval", "ab-eval.py")
35
+ rep = load_module("ab_report", "ab_report.py")
36
+ met = load_module("ab_metrics", "ab_metrics.py")
37
+ bench = load_module("run_benchmark", "run-benchmark.py")
38
+
39
+ SOLVE_IF_SKILLS = 'if [ -d .claude/skills ]; then echo 42 > answer.txt; fi'
40
+ CLAUDE_JSON = json.dumps({
41
+ "type": "result", "subtype": "success", "is_error": False, "num_turns": 7,
42
+ "total_cost_usd": 0.1234, "usage": {"input_tokens": 1500, "output_tokens": 300},
43
+ "modelUsage": {"claude-opus-5-5": {"costUSD": 0.1}, "claude-haiku-5-5": {"costUSD": 0.0234}},
44
+ })
45
+
46
+
47
+ class Sandbox(unittest.TestCase):
48
+ """Builds a throwaway config + one task, so tests never touch specs/benchmarks/ab/."""
49
+
50
+ def setUp(self):
51
+ self.tmp = Path(tempfile.mkdtemp(prefix="ab-test-"))
52
+ self.tasks_dir = self.tmp / "tasks"
53
+ task = self.tasks_dir / "t-01"
54
+ (task / "fixture" / "test").mkdir(parents=True)
55
+ (task / "fixture" / "test" / "a.test").write_text("keep me\n")
56
+ (task / "hidden").mkdir()
57
+ (task / "hidden" / "secret.check").write_text('test "$(cat answer.txt 2>/dev/null)" = 42\n')
58
+ (task / "task.yaml").write_text(yaml.safe_dump({
59
+ "id": "t-01", "title": "answer", "kind": "feature",
60
+ "prompt": "Write 42 to answer.txt",
61
+ "protected": ["test/a.test"],
62
+ "grade": "sh secret.check",
63
+ "timeout_s": 30,
64
+ }))
65
+ self.config = self.tmp / "config.yaml"
66
+ self.config.write_text(yaml.safe_dump({
67
+ "k": 1,
68
+ "arms": {"vanilla": {"setup": "true"}, "bigpowers": {"setup": "mkdir -p .claude/skills"}},
69
+ "adapters": {"claude": {"command": "false", "parser": "claude-json"}},
70
+ }))
71
+ self.out = self.tmp / "out"
72
+
73
+ def tearDown(self):
74
+ shutil.rmtree(self.tmp, ignore_errors=True)
75
+
76
+ def run_main(self, *extra):
77
+ argv = ["--config", str(self.config), "--tasks-dir", str(self.tasks_dir),
78
+ "--out", str(self.out), *extra]
79
+ buf = io.StringIO()
80
+ with redirect_stdout(buf):
81
+ code = ab.main(argv)
82
+ return code, buf.getvalue()
83
+
84
+ def records(self):
85
+ lines = next(self.out.glob("ab-runs-*.jsonl")).read_text().splitlines()
86
+ return [json.loads(line) for line in lines]
87
+
88
+
89
+ class TestFairComparison(Sandbox):
90
+ def test_skill_dependent_agent_shows_full_delta(self):
91
+ code, _ = self.run_main("--adapter-cmd", SOLVE_IF_SKILLS, "--k", "2")
92
+ self.assertEqual(code, 0)
93
+ report = yaml.safe_load(next(self.out.glob("AB-*.yaml")).read_text())
94
+ self.assertEqual(report["arms_summary"]["bigpowers"]["pass_rate"], 1.0)
95
+ self.assertEqual(report["arms_summary"]["vanilla"]["pass_rate"], 0.0)
96
+ self.assertEqual(report["delta"]["pass_rate"], 1.0)
97
+ self.assertEqual(report["per_task"]["t-01"]["bigpowers"], 1.0)
98
+
99
+ def test_hidden_tests_are_not_visible_to_agent(self):
100
+ peek = "find . -name 'secret*' | grep -q . && exit 1; echo 42 > answer.txt"
101
+ self.run_main("--adapter-cmd", peek, "--arms", "vanilla")
102
+ self.assertTrue(all(r["pass"] for r in self.records()))
103
+
104
+ def test_workdir_lives_outside_repo(self):
105
+ where = f"pwd > {self.tmp}/where.txt"
106
+ self.run_main("--adapter-cmd", where, "--arms", "vanilla")
107
+ workdir = Path((self.tmp / "where.txt").read_text().strip()).resolve()
108
+ self.assertNotIn(str(REPO_ROOT.resolve()), str(workdir))
109
+
110
+
111
+ class TestRunRecord(Sandbox):
112
+ def test_deleting_protected_test_is_tampering(self):
113
+ self.run_main("--adapter-cmd", "rm test/a.test; echo 42 > answer.txt", "--arms", "vanilla")
114
+ rec = self.records()[0]
115
+ self.assertTrue(rec["tampered"])
116
+ self.assertTrue(rec["pass"])
117
+
118
+ def test_untouched_protected_test_is_not_tampering(self):
119
+ self.run_main("--adapter-cmd", "echo 42 > answer.txt", "--arms", "vanilla")
120
+ self.assertFalse(self.records()[0]["tampered"])
121
+
122
+ def test_appending_tests_is_not_tampering(self): # TDD arms add tests to visible files
123
+ self.run_main("--adapter-cmd", "echo more >> test/a.test; echo 42 > answer.txt", "--arms", "vanilla")
124
+ rec = self.records()[0]
125
+ self.assertFalse(rec["tampered"])
126
+ self.assertEqual(rec["protected_lines_added"], 1)
127
+
128
+ def test_editing_a_protected_line_is_tampering(self):
129
+ self.run_main("--adapter-cmd", "echo weak > test/a.test; echo 42 > answer.txt", "--arms", "vanilla")
130
+ self.assertTrue(self.records()[0]["tampered"])
131
+
132
+ def test_diff_lines_count_agent_changes_only(self):
133
+ self.run_main("--adapter-cmd", "printf '42\\nx\\n' > answer.txt", "--arms", "bigpowers")
134
+ self.assertEqual(self.records()[0]["diff_lines"], 2)
135
+
136
+ def test_agent_failure_is_recorded_not_fatal(self):
137
+ code, _ = self.run_main("--adapter-cmd", "exit 1", "--arms", "vanilla")
138
+ self.assertEqual(code, 0)
139
+ rec = self.records()[0]
140
+ self.assertTrue(rec["agent_error"])
141
+ self.assertFalse(rec["pass"])
142
+
143
+ def test_prompt_reaches_adapter(self):
144
+ self.run_main("--adapter-cmd", 'printf "%s" "$AB_PROMPT" > got.txt; cp got.txt ' + str(self.tmp),
145
+ "--arms", "vanilla")
146
+ self.assertEqual((self.tmp / "got.txt").read_text(), "Write 42 to answer.txt")
147
+
148
+
149
+ class TestMetrics(unittest.TestCase):
150
+ def test_claude_json_fields_are_parsed(self):
151
+ m = met.parse_metrics("claude-json", "noise line\n" + CLAUDE_JSON + "\n")
152
+ self.assertEqual(m["cost_usd"], 0.1234)
153
+ self.assertEqual(m["input_tokens"], 1500)
154
+ self.assertEqual(m["output_tokens"], 300)
155
+ self.assertEqual(m["turns"], 7)
156
+ self.assertEqual(m["models"], ["claude-haiku-5-5", "claude-opus-5-5"])
157
+ self.assertFalse(m["agent_reported_error"])
158
+
159
+ def test_unparseable_output_gives_nulls(self):
160
+ m = met.parse_metrics("claude-json", "not json")
161
+ self.assertIsNone(m["cost_usd"])
162
+
163
+ def test_bootstrap_is_deterministic_and_brackets_estimate(self):
164
+ a = [True, True, False, True, True, False]
165
+ b = [False, True, False, False, True, False]
166
+ ci1 = rep.bootstrap_delta_ci(a, b)
167
+ ci2 = rep.bootstrap_delta_ci(a, b)
168
+ self.assertEqual(ci1, ci2)
169
+ self.assertLessEqual(ci1[0], 2 / 6)
170
+ self.assertGreaterEqual(ci1[1], 2 / 6)
171
+
172
+
173
+ OPENCODE_JSONL = "\n".join(json.dumps(e) for e in [
174
+ {"type": "step_start", "part": {"type": "step-start"}},
175
+ {"type": "step_finish", "part": {"type": "step-finish", "cost": 0.01,
176
+ "tokens": {"input": 100, "output": 20, "reasoning": 5,
177
+ "cache": {"read": 300, "write": 50}}}},
178
+ {"type": "text", "part": {"type": "text", "text": "done"}},
179
+ {"type": "step_finish", "part": {"type": "step-finish", "cost": 0.02,
180
+ "tokens": {"input": 10, "output": 2, "reasoning": 0,
181
+ "cache": {"read": 400, "write": 0}}}},
182
+ ])
183
+
184
+
185
+ class TestOpencodeParser(unittest.TestCase):
186
+ def test_opencode_jsonl_sums_steps(self):
187
+ m = met.parse_metrics("opencode-jsonl", "log noise\n" + OPENCODE_JSONL + "\n")
188
+ self.assertAlmostEqual(m["cost_usd"], 0.03)
189
+ self.assertEqual(m["input_tokens"], 100 + 300 + 50 + 10 + 400)
190
+ self.assertEqual(m["output_tokens"], 20 + 5 + 2)
191
+ self.assertEqual(m["turns"], 2)
192
+ self.assertFalse(m["agent_reported_error"])
193
+
194
+ def test_opencode_error_event_is_agent_error(self):
195
+ out = OPENCODE_JSONL + "\n" + json.dumps({"type": "error", "error": {"name": "APIError"}})
196
+ self.assertTrue(met.parse_metrics("opencode-jsonl", out)["agent_reported_error"])
197
+
198
+ def test_opencode_without_steps_gives_nulls(self):
199
+ self.assertIsNone(met.parse_metrics("opencode-jsonl", "")["cost_usd"])
200
+
201
+
202
+ class TestRunEnvironment(Sandbox):
203
+ def test_each_run_gets_fresh_home_removed_afterwards(self):
204
+ cmd = (f'ls -A "$AB_HOME" | wc -l | tr -d " " > {self.tmp}/n.txt; '
205
+ f'echo "$AB_HOME" > {self.tmp}/home.txt; echo 42 > answer.txt')
206
+ self.run_main("--adapter-cmd", cmd, "--arms", "vanilla")
207
+ self.assertEqual((self.tmp / "n.txt").read_text().strip(), "0")
208
+ home = Path((self.tmp / "home.txt").read_text().strip())
209
+ self.assertTrue(str(home))
210
+ self.assertFalse(home.exists())
211
+
212
+ def test_adapter_stdin_is_closed(self):
213
+ self.run_main("--adapter-cmd", "cat > /dev/null; echo 42 > answer.txt", "--arms", "vanilla")
214
+ self.assertTrue(self.records()[0]["pass"])
215
+
216
+ def test_adapter_default_model_is_used_and_recorded(self):
217
+ cfg = yaml.safe_load(self.config.read_text())
218
+ cfg["adapters"]["fake"] = {
219
+ "command": f'printf "%s" "$AB_MODEL" > {self.tmp}/model.txt; echo 42 > answer.txt',
220
+ "parser": "none", "default_model": "prov/model-x"}
221
+ self.config.write_text(yaml.safe_dump(cfg))
222
+ self.run_main("--adapter", "fake", "--arms", "vanilla")
223
+ self.assertEqual((self.tmp / "model.txt").read_text(), "prov/model-x")
224
+ report = yaml.safe_load(next(self.out.glob("AB-*.yaml")).read_text())
225
+ self.assertEqual(report["model"], "prov/model-x")
226
+
227
+
228
+ class TestCli(Sandbox):
229
+ def test_unknown_arm_exits_2(self):
230
+ code, out = self.run_main("--arms", "nope")
231
+ self.assertEqual(code, 2)
232
+ self.assertIn("vanilla", out)
233
+
234
+ def test_unknown_task_exits_2(self):
235
+ code, _ = self.run_main("--tasks", "missing")
236
+ self.assertEqual(code, 2)
237
+
238
+ def test_dry_run_spends_nothing(self):
239
+ marker = self.tmp / "ran.txt"
240
+ code, out = self.run_main("--adapter-cmd", f"touch {marker}", "--dry-run")
241
+ self.assertEqual(code, 0)
242
+ self.assertFalse(marker.exists())
243
+ self.assertIn("t-01", out)
244
+ self.assertFalse(self.out.exists())
245
+
246
+ def test_single_arm_has_no_delta(self):
247
+ self.run_main("--adapter-cmd", "true", "--arms", "vanilla")
248
+ report = yaml.safe_load(next(self.out.glob("AB-*.yaml")).read_text())
249
+ self.assertIsNone(report["delta"])
250
+
251
+
252
+ @unittest.skipUnless(shutil.which("node"), "node is needed for the starter tasks")
253
+ class TestStarterTasks(unittest.TestCase):
254
+ """Each shipped task must fail as given and pass with its reference solution."""
255
+
256
+ TASKS = REPO_ROOT / "specs" / "benchmarks" / "ab" / "tasks"
257
+
258
+ def grade_copy(self, task_dir: Path, with_solution: bool) -> bool:
259
+ task = yaml.safe_load((task_dir / "task.yaml").read_text())
260
+ work = Path(tempfile.mkdtemp(prefix="ab-starter-"))
261
+ try:
262
+ shutil.copytree(task_dir / "fixture", work, dirs_exist_ok=True)
263
+ if with_solution:
264
+ shutil.copytree(task_dir / "solution", work, dirs_exist_ok=True)
265
+ shutil.copytree(task_dir / "hidden", work, dirs_exist_ok=True)
266
+ res = subprocess.run(["bash", "-c", task["grade"]], cwd=work, capture_output=True)
267
+ return res.returncode == 0
268
+ finally:
269
+ shutil.rmtree(work, ignore_errors=True)
270
+
271
+ def test_eight_tasks_ship(self):
272
+ self.assertGreaterEqual(len(list(self.TASKS.glob("*/task.yaml"))), 8)
273
+
274
+ def test_fixture_fails_and_solution_passes(self):
275
+ for task_yaml in sorted(self.TASKS.glob("*/task.yaml")):
276
+ with self.subTest(task=task_yaml.parent.name):
277
+ self.assertFalse(self.grade_copy(task_yaml.parent, with_solution=False))
278
+ self.assertTrue(self.grade_copy(task_yaml.parent, with_solution=True))
279
+
280
+ def test_prompts_never_name_bigpowers(self):
281
+ for task_yaml in self.TASKS.glob("*/task.yaml"):
282
+ self.assertNotIn("bigpowers", yaml.safe_load(task_yaml.read_text())["prompt"].lower())
283
+
284
+ def test_grade_commands_never_pass_a_bare_directory(self):
285
+ """`node --test test/` exits 1 on node 24 (it resolves the directory as a module path), so
286
+ such a task can never pass however good the solution is, while a hidden-only `score`
287
+ command still looks healthy (BUG found in e58s07: ab-11 and ab-13 read pass=0 forever)."""
288
+ for task_yaml in self.TASKS.glob("*/task.yaml"):
289
+ with self.subTest(task=task_yaml.parent.name):
290
+ for token in yaml.safe_load(task_yaml.read_text())["grade"].split():
291
+ self.assertNotIn(token.rstrip("/"), ("test", "test-hidden"),
292
+ f"{task_yaml.parent.name} grades a directory, not a glob")
293
+
294
+
295
+ class TestRunBenchmarkHonesty(unittest.TestCase):
296
+ def test_without_skill_arm_is_unmeasured_not_zero(self):
297
+ sc = {"id": "s1", "grader": {"type": "code", "command": "true"}}
298
+ _, status = bench.scenario_pass_rate(sc, runs=1, with_skill=False)
299
+ self.assertEqual(status, "unmeasured")
300
+ report = bench.build_split_report([sc], runs=1)
301
+ self.assertIsNone(report["without_skill"])
302
+ self.assertIsNone(report["delta"])
303
+
304
+ def test_summary_says_unmeasured(self):
305
+ self.assertIn("UNMEASURED", bench.delta_summary(None))
306
+
307
+
308
+ if __name__ == "__main__":
309
+ unittest.main(verbosity=2)
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env python3
2
+ # story: e58s06
3
+ """tests/test_ab_pi.py — pi adapter: pi-json metrics parser and the config default adapter.
4
+
5
+ Run: python3 tests/test_ab_pi.py
6
+ """
7
+ import json
8
+ import sys
9
+ import unittest
10
+ from pathlib import Path
11
+
12
+ import yaml
13
+
14
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
15
+ from test_ab_eval import Sandbox, met # noqa: E402
16
+
17
+
18
+ def assistant_end(inp, out, cache_read, cost, stop="toolUse"):
19
+ usage = {"input": inp, "output": out, "cacheRead": cache_read, "cacheWrite": 0, "cost": {"total": cost}}
20
+ return {"type": "message_end", "message": {"role": "assistant", "stopReason": stop, "usage": usage}}
21
+
22
+
23
+ def stream(*events):
24
+ return "\n".join(json.dumps(e) for e in events)
25
+
26
+
27
+ class TestPiParser(unittest.TestCase):
28
+ def test_sums_assistant_usage(self):
29
+ out = stream({"type": "session", "id": "x"},
30
+ {"type": "message_end", "message": {"role": "user", "content": "hi"}},
31
+ assistant_end(1390, 56, 256, 0.000243),
32
+ assistant_end(55, 3, 1664, 0.000015, stop="stop"),
33
+ {"type": "agent_settled", "aborted": False})
34
+ m = met.parse_metrics("pi-json", out)
35
+ self.assertEqual((m["input_tokens"], m["output_tokens"], m["turns"]), (1390 + 256 + 55 + 1664, 59, 2))
36
+ self.assertAlmostEqual(m["cost_usd"], 0.000258)
37
+ self.assertFalse(m["agent_reported_error"])
38
+
39
+ def test_provider_error_is_agent_error(self):
40
+ out = stream(assistant_end(10, 0, 0, 0.0, stop="error"), {"type": "agent_settled", "aborted": False})
41
+ self.assertTrue(met.parse_metrics("pi-json", out)["agent_reported_error"])
42
+
43
+ def test_aborted_run_is_agent_error(self):
44
+ out = stream(assistant_end(10, 1, 0, 0.0, stop="stop"), {"type": "agent_settled", "aborted": True})
45
+ self.assertTrue(met.parse_metrics("pi-json", out)["agent_reported_error"])
46
+
47
+ def test_no_events_is_unknown(self):
48
+ self.assertIsNone(met.parse_metrics("pi-json", "boom")["agent_reported_error"])
49
+
50
+
51
+ class TestDefaultAdapter(Sandbox):
52
+ def test_config_default_adapter_is_used(self):
53
+ config = yaml.safe_load(self.config.read_text())
54
+ config["adapters"]["fakepi"] = {"command": "echo 42 > answer.txt", "parser": "none"}
55
+ config["default_adapter"] = "fakepi"
56
+ self.config.write_text(yaml.safe_dump(config))
57
+ _, out = self.run_main("--arms", "vanilla")
58
+ self.assertIn("adapter 'fakepi'", out)
59
+ self.assertTrue(self.records()[0]["pass"])
60
+
61
+
62
+ if __name__ == "__main__":
63
+ unittest.main(verbosity=1)
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env python3
2
+ # story: e58s05
3
+ """tests/test_ab_progress.py — live records, interleaved runs, saved transcripts, ab_progress.
4
+
5
+ Run: python3 tests/test_ab_progress.py
6
+ """
7
+ import gzip
8
+ import io
9
+ import json
10
+ import sys
11
+ import unittest
12
+ from contextlib import redirect_stdout
13
+ from pathlib import Path
14
+
15
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
16
+ from test_ab_eval import Sandbox, load_module # noqa: E402
17
+
18
+ prog = load_module("ab_progress", "ab_progress.py")
19
+
20
+
21
+ class TestLiveRun(Sandbox):
22
+ def live(self):
23
+ return [json.loads(line) for line in (self.out / "ab-live.jsonl").read_text().splitlines()]
24
+
25
+ def test_records_stream_with_sha_and_model(self):
26
+ self.run_main("--adapter-cmd", "echo 42 > answer.txt", "--k", "2", "--model", "m-1")
27
+ live = self.live()
28
+ self.assertEqual(len(live), 4)
29
+ self.assertTrue(all(r["bigpowers_sha"] and r["model"] == "m-1" for r in live))
30
+
31
+ def test_runs_are_interleaved(self):
32
+ self.run_main("--adapter-cmd", "echo 42 > answer.txt", "--k", "2")
33
+ order = [(r["run"], r["arm"]) for r in self.live()]
34
+ self.assertEqual(order, [(1, "vanilla"), (1, "bigpowers"), (2, "vanilla"), (2, "bigpowers")])
35
+
36
+ def test_transcripts_are_saved(self):
37
+ self.run_main("--adapter-cmd", "echo transcript-line; echo 42 > answer.txt", "--arms", "vanilla")
38
+ traces = sorted((self.out / "traces").glob("*.jsonl.gz"))
39
+ self.assertEqual([t.name for t in traces], ["t-01-vanilla-1.jsonl.gz"])
40
+ self.assertIn("transcript-line", gzip.decompress(traces[0].read_bytes()).decode())
41
+
42
+
43
+ class TestProgress(unittest.TestCase):
44
+ def shard(self, tmp: Path, name: str, records: list[dict]) -> Path:
45
+ folder = tmp / name
46
+ folder.mkdir()
47
+ (folder / "ab-live.jsonl").write_text("".join(json.dumps(r) + "\n" for r in records))
48
+ return folder
49
+
50
+ def rec(self, arm, run, passed, score):
51
+ return {"task": "ab-11", "arm": arm, "run": run, "pass": passed, "score": score, "tampered": False,
52
+ "agent_error": False, "cost_usd": 0.01, "input_tokens": None, "output_tokens": None,
53
+ "turns": 5, "duration_s": 1.0, "diff_lines": 3, "bigpowers_sha": "abc", "model": "m"}
54
+
55
+ def test_reads_shards_and_prints_evolution(self):
56
+ import tempfile
57
+ tmp = Path(tempfile.mkdtemp(prefix="ab-prog-"))
58
+ a = self.shard(tmp, "s1", [self.rec("vanilla", i, i % 2 == 0, 0.5) for i in range(1, 11)]
59
+ + [self.rec("bigpowers", i, True, 1.0) for i in range(1, 11)])
60
+ b = self.shard(tmp, "s2", [self.rec("vanilla", i, False, 0.4) for i in range(1, 6)]
61
+ + [self.rec("bigpowers", i, True, 1.0) for i in range(1, 6)])
62
+ buf = io.StringIO()
63
+ with redirect_stdout(buf):
64
+ code = prog.main([str(a), str(b)])
65
+ text = buf.getvalue()
66
+ self.assertEqual(code, 0)
67
+ self.assertIn("30 runs", text)
68
+ self.assertRegex(text, r"ab-11\s+vanilla\s+15")
69
+ self.assertRegex(text, r"ab-11\s+bigpowers\s+15")
70
+ self.assertIn("bigpowers - vanilla", text)
71
+ self.assertIn("evolution", text)
72
+
73
+ def test_no_live_file_exits_2(self):
74
+ import tempfile
75
+ buf = io.StringIO()
76
+ with redirect_stdout(buf):
77
+ self.assertEqual(prog.main([tempfile.mkdtemp()]), 2)
78
+
79
+
80
+ if __name__ == "__main__":
81
+ unittest.main(verbosity=1)
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env python3
2
+ # story: e58s07
3
+ """tests/test_ab_sandbox.py — Docker sandbox: grade_wrapper, AB_TIMEOUT, container isolation.
4
+
5
+ Run: python3 tests/test_ab_sandbox.py
6
+ The isolation tests need Docker and `bash scripts/ab-sandbox.sh up`; they skip otherwise.
7
+ """
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ import unittest
12
+ from pathlib import Path
13
+
14
+ import yaml
15
+
16
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
17
+ from test_ab_eval import REPO_ROOT, Sandbox # noqa: E402
18
+
19
+ AGENT_IMAGE = "bp-ab-agent:pi-1.5.0"
20
+
21
+
22
+ def sandbox_up() -> bool:
23
+ if not shutil.which("docker"):
24
+ return False
25
+ res = subprocess.run(["bash", str(REPO_ROOT / "scripts" / "ab-sandbox.sh"), "status"], capture_output=True)
26
+ return res.returncode == 0
27
+
28
+
29
+ class TestGradeWrapper(Sandbox):
30
+ def add_adapter(self, **extra):
31
+ config = yaml.safe_load(self.config.read_text())
32
+ config["adapters"]["boxed"] = {"command": "echo $AB_TIMEOUT > timeout.txt; cp timeout.txt "
33
+ + str(self.tmp) + "/; echo 42 > answer.txt", "parser": "none", **extra}
34
+ self.config.write_text(yaml.safe_dump(config))
35
+
36
+ def test_grade_and_score_go_through_the_wrapper(self):
37
+ log = self.tmp / "wrapped.log"
38
+ self.add_adapter(grade_wrapper=f'echo "$AB_CMD" >> {log}; bash -c "$AB_CMD"')
39
+ task_yaml = self.tasks_dir / "t-01" / "task.yaml"
40
+ task = yaml.safe_load(task_yaml.read_text())
41
+ task["score"] = "printf '# pass 1\\n# fail 0\\n'"
42
+ task_yaml.write_text(yaml.safe_dump(task))
43
+ self.run_main("--adapter", "boxed", "--arms", "vanilla")
44
+ rec = self.records()[0]
45
+ self.assertTrue(rec["pass"])
46
+ self.assertEqual(rec["score"], 1.0)
47
+ self.assertEqual(len(log.read_text().splitlines()), 2)
48
+
49
+ def test_timeout_is_exported_with_margin(self):
50
+ self.add_adapter()
51
+ self.run_main("--adapter", "boxed", "--arms", "vanilla")
52
+ self.assertEqual((self.tmp / "timeout.txt").read_text().strip(), "10")
53
+
54
+
55
+ class TestSandboxScript(unittest.TestCase):
56
+ def test_sandbox_script_syntax_and_usage(self):
57
+ script = REPO_ROOT / "scripts" / "ab-sandbox.sh"
58
+ self.assertTrue(script.exists())
59
+ self.assertTrue(bool(script.stat().st_mode & 0o111), "ab-sandbox.sh must be executable")
60
+ res = subprocess.run(["bash", "-n", str(script)], capture_output=True, text=True)
61
+ self.assertEqual(res.returncode, 0, res.stderr)
62
+ res = subprocess.run(["bash", str(script), "unknown-cmd"], capture_output=True, text=True)
63
+ self.assertEqual(res.returncode, 1)
64
+ self.assertIn("usage: ab-sandbox.sh up|down|status", res.stderr)
65
+
66
+
67
+ @unittest.skipUnless(sandbox_up(), "Docker sandbox not up (bash scripts/ab-sandbox.sh up)")
68
+ class TestIsolation(unittest.TestCase):
69
+ def agent(self, script: str, network: str = "bp-ab-internal") -> subprocess.CompletedProcess:
70
+ return subprocess.run(
71
+ ["docker", "run", "--rm", "--network", network,
72
+ "-e", "HTTPS_PROXY=http://bp-ab-proxy:8888", "-e", "HTTP_PROXY=http://bp-ab-proxy:8888",
73
+ AGENT_IMAGE, "bash", "-c", script], capture_output=True, text=True, timeout=120)
74
+
75
+ def test_host_paths_are_invisible(self):
76
+ res = self.agent(f'test ! -e /Users && test ! -e "{REPO_ROOT}" && echo hidden')
77
+ self.assertEqual(res.stdout.strip(), "hidden", res.stderr)
78
+
79
+ def test_runs_as_non_root(self):
80
+ self.assertNotEqual(self.agent("id -u").stdout.strip(), "0")
81
+
82
+ def test_only_the_model_provider_is_reachable(self):
83
+ probe = "node -e \"fetch(process.argv[1]).then(r=>console.log('ok',r.status)).catch(e=>console.log('blocked'))\""
84
+ self.assertIn("blocked", self.agent(f"{probe} https://example.com").stdout)
85
+ self.assertIn("ok", self.agent(f"{probe} https://opencode.ai").stdout)
86
+
87
+ def test_grading_container_has_no_network(self):
88
+ res = self.agent("node -e \"fetch('https://opencode.ai').then(()=>console.log('open'))"
89
+ ".catch(()=>console.log('none'))\"", network="none")
90
+ self.assertIn("none", res.stdout)
91
+
92
+
93
+ if __name__ == "__main__":
94
+ unittest.main(verbosity=1)
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env python3
2
+ # story: e58s03
3
+ """tests/test_ab_score.py — partial-credit scoring and the hard tasks (ab-11, ab-13).
4
+
5
+ Run: python3 tests/test_ab_score.py
6
+ Split from test_ab_eval.py (300-line cap); reuses its Sandbox and module loaders.
7
+ """
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ import tempfile
12
+ import unittest
13
+ from pathlib import Path
14
+
15
+ import yaml
16
+
17
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
18
+ from test_ab_eval import REPO_ROOT, Sandbox, met, rep # noqa: E402
19
+
20
+ TASKS = REPO_ROOT / "specs" / "benchmarks" / "ab" / "tasks"
21
+ HARD_TASKS = ["ab-11-ledger-rounding", "ab-13-orders-split"]
22
+
23
+
24
+ class TestTapScore(unittest.TestCase):
25
+ def test_share_of_passing_tests(self):
26
+ self.assertEqual(met.tap_score("# tests 4\n# pass 3\n# fail 1\n"), 0.75)
27
+
28
+ def test_several_summaries_are_summed(self):
29
+ self.assertEqual(met.tap_score("# pass 2\n# fail 0\nnoise\n# pass 1\n# fail 1\n"), 0.75)
30
+
31
+ def test_no_summary_is_none(self):
32
+ self.assertIsNone(met.tap_score("SyntaxError: boom\n"))
33
+
34
+
35
+ class TestScoreInRuns(Sandbox):
36
+ def set_score(self, command):
37
+ task_yaml = self.tasks_dir / "t-01" / "task.yaml"
38
+ task = yaml.safe_load(task_yaml.read_text())
39
+ task["score"] = command
40
+ task_yaml.write_text(yaml.safe_dump(task))
41
+
42
+ def test_score_command_sets_score(self):
43
+ self.set_score("printf '# pass 3\\n# fail 1\\n'; exit 1")
44
+ self.run_main("--adapter-cmd", "echo 42 > answer.txt", "--arms", "vanilla")
45
+ self.assertEqual(self.records()[0]["score"], 0.75)
46
+
47
+ def test_no_score_key_means_null(self):
48
+ self.run_main("--adapter-cmd", "echo 42 > answer.txt")
49
+ self.assertIsNone(self.records()[0]["score"])
50
+ report = yaml.safe_load(next(self.out.glob("AB-*.yaml")).read_text())
51
+ self.assertIsNone(report["delta"].get("score"))
52
+
53
+
54
+ class TestScoreInReport(unittest.TestCase):
55
+ def rec(self, arm, task, score):
56
+ return {"arm": arm, "task": task, "pass": score == 1.0, "tampered": False, "agent_error": False,
57
+ "cost_usd": None, "input_tokens": None, "output_tokens": None, "turns": None,
58
+ "duration_s": 1.0, "diff_lines": 1, "score": score}
59
+
60
+ def test_mean_score_and_delta(self):
61
+ records = [self.rec("vanilla", "t", 0.5), self.rec("vanilla", "t", 0.7),
62
+ self.rec("bigpowers", "t", 0.9), self.rec("bigpowers", "t", 1.0)]
63
+ result = rep.aggregate(records, ["vanilla", "bigpowers"], ["t"])
64
+ self.assertEqual(result["arms_summary"]["vanilla"]["mean_score"], 0.6)
65
+ self.assertEqual(result["per_task_score"]["t"]["bigpowers"], 0.95)
66
+ self.assertEqual(result["delta"]["score"], 0.35)
67
+ lo, hi = result["delta"]["score_ci95"]
68
+ self.assertLessEqual(lo, 0.35)
69
+ self.assertGreaterEqual(hi, 0.35)
70
+
71
+
72
+ @unittest.skipUnless(shutil.which("node"), "node is needed for the hard tasks")
73
+ class TestHardTasks(unittest.TestCase):
74
+ """Each hard task must fail as given, pass with its solution, and give partial credit."""
75
+
76
+ def grade(self, name: str, with_solution: bool) -> tuple[bool, float | None]:
77
+ task_dir = TASKS / name
78
+ task = yaml.safe_load((task_dir / "task.yaml").read_text())
79
+ work = Path(tempfile.mkdtemp(prefix="ab-hard-"))
80
+ try:
81
+ shutil.copytree(task_dir / "fixture", work, dirs_exist_ok=True)
82
+ if with_solution:
83
+ shutil.copytree(task_dir / "solution", work, dirs_exist_ok=True)
84
+ shutil.copytree(task_dir / "hidden", work, dirs_exist_ok=True)
85
+ run = lambda cmd: subprocess.run(["bash", "-c", cmd], cwd=work, capture_output=True, text=True)
86
+ passed = run(task["grade"]).returncode == 0
87
+ return passed, met.tap_score(run(task["score"]).stdout)
88
+ finally:
89
+ shutil.rmtree(work, ignore_errors=True)
90
+
91
+ def test_fixture_fails_with_partial_score(self):
92
+ for name in HARD_TASKS:
93
+ with self.subTest(task=name):
94
+ passed, score = self.grade(name, with_solution=False)
95
+ self.assertFalse(passed)
96
+ assert score is not None
97
+ self.assertLess(score, 1.0)
98
+
99
+ def test_solution_passes_with_full_score(self):
100
+ for name in HARD_TASKS:
101
+ with self.subTest(task=name):
102
+ self.assertEqual(self.grade(name, with_solution=True), (True, 1.0))
103
+
104
+ def test_score_runs_hidden_tests_only(self):
105
+ for name in HARD_TASKS:
106
+ with self.subTest(task=name):
107
+ score_cmd = yaml.safe_load((TASKS / name / "task.yaml").read_text())["score"]
108
+ self.assertIn("test-hidden", score_cmd)
109
+ self.assertNotRegex(score_cmd, r"(^|\s)test/")
110
+
111
+
112
+ if __name__ == "__main__":
113
+ unittest.main(verbosity=1)