@tangle-network/agent-bench 0.1.0 → 0.3.5
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.
- package/CHANGELOG.md +17 -0
- package/HARNESS.md +302 -0
- package/README.md +26 -1
- package/fixtures/aec-bench.json +18 -0
- package/fixtures/agentbench-dbbench.json +22 -0
- package/fixtures/bfcl.json +45 -0
- package/fixtures/commit0.json +72 -0
- package/fixtures/crag.json +10 -0
- package/fixtures/dabstep.json +22 -0
- package/fixtures/enterpriseops-gym.json +103 -0
- package/fixtures/finresearchbench.json +21 -0
- package/fixtures/finsearchcomp.json +66 -0
- package/fixtures/frames.json +26 -0
- package/fixtures/hotpotqa.json +182 -0
- package/fixtures/nomiracl.json +26 -0
- package/fixtures/open-rag-bench.json +16 -0
- package/fixtures/pier-agent/no-model-task/environment/Dockerfile +16 -0
- package/fixtures/pier-agent/no-model-task/environment/seed/src/status.txt +1 -0
- package/fixtures/pier-agent/no-model-task/instruction.md +6 -0
- package/fixtures/pier-agent/no-model-task/pre_artifacts.sh +6 -0
- package/fixtures/pier-agent/no-model-task/task.toml +35 -0
- package/fixtures/pier-agent/no-model-task/tests/Dockerfile +17 -0
- package/fixtures/pier-agent/no-model-task/tests/seed/src/status.txt +1 -0
- package/fixtures/pier-agent/no-model-task/tests/test.sh +19 -0
- package/fixtures/programbench.json +17 -0
- package/fixtures/ragbench.json +21 -0
- package/fixtures/simpleqa.json +121 -0
- package/fixtures/t2-ragbench.json +13 -0
- package/fixtures/tau2-bench.json +16 -0
- package/fixtures/tau3-banking.json +16 -0
- package/fixtures/toollm.json +28 -0
- package/fixtures/webarena-verified.json +20 -0
- package/package.json +39 -15
- package/pier_agents/__init__.py +18 -0
- package/pier_agents/candidate_contract.py +755 -0
- package/pier_agents/process_boundary.py +321 -0
- package/pier_agents/tangle_candidate.py +907 -0
- package/pier_agents/workspace_boundary.py +368 -0
- package/scripts/appworld_driver.py +359 -0
- package/scripts/cadbench_prepare.py +22 -0
- package/scripts/cadgenbench_hard_parts.py +48 -0
- package/scripts/clbench_codebase_judge.py +73 -0
- package/scripts/commit0_judge.py +170 -0
- package/scripts/dabstep_judge.py +42 -0
- package/scripts/enterpriseops_gym_judge.py +281 -0
- package/scripts/programbench_judge.py +120 -0
- package/scripts/render-gate-chart.mjs +176 -0
- package/scripts/run-package-tests.mjs +56 -0
- package/scripts/terminate-pier-trial.mts +66 -0
- package/scripts/trata-hedge/README.md +56 -0
- package/scripts/trata-hedge/run.sh +60 -0
- package/scripts/trata-hedge/solve.py +83 -0
- package/scripts/verify-packed-consumer.mjs +224 -0
- package/scripts/verify-pier-agent.mts +715 -0
- package/scripts/verify-pier-pair.mts +74 -0
- package/scripts/verify-pier-recovery.mts +139 -0
- package/src/adapters.ts +26 -0
- package/src/benchmarks/_harness.test.mts +178 -0
- package/src/benchmarks/_harness.ts +239 -16
- package/src/benchmarks/agentbench.ts +163 -0
- package/src/benchmarks/appworld.test.mts +15 -9
- package/src/benchmarks/bfcl.ts +346 -0
- package/src/benchmarks/crag.ts +137 -0
- package/src/benchmarks/dabstep.test.mts +70 -0
- package/src/benchmarks/dabstep.ts +212 -0
- package/src/benchmarks/external-adapters.test.mts +150 -0
- package/src/benchmarks/finresearchbench.ts +269 -0
- package/src/benchmarks/humaneval.ts +20 -8
- package/src/benchmarks/nomiracl.ts +180 -0
- package/src/benchmarks/open-rag-bench.ts +153 -0
- package/src/benchmarks/rag-benchmarks.test.mts +138 -0
- package/src/benchmarks/rag-shared.ts +327 -0
- package/src/benchmarks/ragbench.ts +171 -0
- package/src/benchmarks/swe-bench.test.mts +61 -0
- package/src/benchmarks/swe-bench.ts +201 -19
- package/src/benchmarks/t2-ragbench.ts +166 -0
- package/src/benchmarks/tau-bench-shared.ts +214 -0
- package/src/benchmarks/tau2-bench.ts +30 -0
- package/src/benchmarks/tau3-banking.ts +29 -0
- package/src/benchmarks/terminal-bench.test.mts +33 -0
- package/src/benchmarks/terminal-bench.ts +23 -8
- package/src/benchmarks/toollm.ts +254 -0
- package/src/benchmarks/types.ts +42 -0
- package/src/benchmarks/webarena-verified.ts +200 -0
- package/src/commit0-prereqs.sh +0 -0
- package/src/coordination-mcp-container-reach.mts +181 -0
- package/src/decoder-live.mts +1 -1
- package/src/examples/README.md +103 -39
- package/src/examples/benchmark-matrix.mts +101 -0
- package/src/examples/lean-proof-gate.README.md +77 -0
- package/src/examples/lean-proof-gate.mts +162 -0
- package/src/examples/lean-verify.ts +95 -0
- package/src/examples/lean.Dockerfile +12 -0
- package/src/examples/math-demo.mts +9 -7
- package/src/examples/strategy-demo.mts +10 -12
- package/src/gate.ts +3 -2
- package/src/hev-eval.mts +69 -0
- package/src/hev-improve.mts +169 -0
- package/src/hev-structural.mts +688 -0
- package/src/index.ts +73 -0
- package/src/mbpp-structural.mts +662 -0
- package/src/pier-agent.test-fixtures.mts +19 -0
- package/src/pier-agent.test.mts +363 -0
- package/src/pier-agent.ts +657 -0
- package/src/pier-result-grader.mjs +30 -0
- package/src/pier-result-grader.test.mts +62 -0
- package/src/pier-result-grader.ts +108 -0
- package/src/pier-task-outcome.test.mts +117 -0
- package/src/pier-task-outcome.ts +240 -0
- package/src/pier-trial-controller.test.mts +412 -0
- package/src/pier-trial-controller.ts +858 -0
- package/src/pier-trial-supervisor.mjs +352 -0
- package/src/resolve-client.ts +25 -2
- package/src/run-benchmarks-cli.mts +72 -0
- package/src/run-benchmarks-report.ts +66 -0
- package/src/run-benchmarks.test.mts +231 -0
- package/src/run-benchmarks.ts +589 -0
- package/src/smoke-structural-rollout.mts +393 -0
- package/src/swe-bench-env.test.ts +207 -0
- package/src/swe-bench-env.ts +554 -0
- package/src/swe-jail.ts +293 -0
- package/src/swe-self-improve.mts +84 -0
- package/src/swe-structural-judge-policy.test.ts +117 -0
- package/src/swe-structural-judge-policy.ts +133 -0
- package/src/swe-structural-policy.test.ts +124 -0
- package/src/swe-structural-policy.ts +132 -0
- package/src/swe-structural-provenance.test.ts +93 -0
- package/src/swe-structural-provenance.ts +138 -0
- package/src/swe-structural.mts +1260 -0
- package/src/swe-temp.ts +14 -0
- package/src/tb-container-executor.mts +234 -0
- package/src/tb-container-executor.test.mts +99 -0
- package/src/tb-supervisor-sidecar.mts +222 -0
- package/src/trata-gepa.mts +1 -1
- package/steerers/eops-itsm-population.json +1 -0
- package/tb_agents/opencode_refine_agent.py +117 -0
- package/tb_agents/opencode_router_agent.py +406 -0
- package/tb_agents/opencode_supervisor_agent.py +239 -0
- package/tb_agents/script_agent.py +66 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""DABStep judge bridge.
|
|
3
|
+
|
|
4
|
+
Reads {"prediction": str, "golds": list} from stdin and delegates scoring to
|
|
5
|
+
the official DABStep grade.py module. This script owns no grading semantics.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import importlib.util
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_grade(grade_file: Path):
|
|
16
|
+
spec = importlib.util.spec_from_file_location("dabstep_grade", grade_file)
|
|
17
|
+
if spec is None or spec.loader is None:
|
|
18
|
+
raise RuntimeError(f"could not import DABStep grade file: {grade_file}")
|
|
19
|
+
module = importlib.util.module_from_spec(spec)
|
|
20
|
+
spec.loader.exec_module(module)
|
|
21
|
+
return module.grade
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> int:
|
|
25
|
+
parser = argparse.ArgumentParser(description="Score one DABStep answer")
|
|
26
|
+
parser.add_argument("--grade-file", required=True)
|
|
27
|
+
args = parser.parse_args()
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
payload = json.loads(sys.stdin.read())
|
|
31
|
+
prediction = payload["prediction"]
|
|
32
|
+
golds = payload["golds"]
|
|
33
|
+
correct = bool(load_grade(Path(args.grade_file))(prediction, golds))
|
|
34
|
+
print(json.dumps({"correct": correct, "score": 1.0 if correct else 0.0}))
|
|
35
|
+
return 0
|
|
36
|
+
except Exception as exc:
|
|
37
|
+
print(json.dumps({"error": str(exc)}))
|
|
38
|
+
return 1
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# EnterpriseOps-Gym judge driver.
|
|
2
|
+
#
|
|
3
|
+
# Scores a worker's tool-call transcript against a task's verifiers, deterministically,
|
|
4
|
+
# by REPLAYING the transcript into a FRESHLY-SEEDED gym database and then reading the
|
|
5
|
+
# verifier SQL back. The full deployable-checker loop (matches the official benchmark/
|
|
6
|
+
# executor.py protocol):
|
|
7
|
+
#
|
|
8
|
+
# 1. SEED per gym: create an isolated database (its own database_id) from the task's
|
|
9
|
+
# seed_database_file SQL snapshot via POST /api/seed-database. Every later call
|
|
10
|
+
# targets that database via the `x-database-id` header, so concurrent judges on
|
|
11
|
+
# one container never collide.
|
|
12
|
+
# 2. REPLAY each transcript call as an MCP JSON-RPC `tools/call` to /mcp (NOT a bare
|
|
13
|
+
# {tool,arguments} POST, which the server accepts as a no-op 204 and never
|
|
14
|
+
# executes). A tool-call HTTP error is the AGENT's mistake (wrong args / policy
|
|
15
|
+
# violation) — counted + surfaced, never fatal; the verifiers score the result.
|
|
16
|
+
# A transport failure (server down) IS fatal (fail loud, never a fake score).
|
|
17
|
+
# 3. VERIFY each database_state verifier's SQL via /api/sql-runner (parsing the server's
|
|
18
|
+
# {"data":[{...}]} shape), compared to expected_value under comparison_type.
|
|
19
|
+
# 4. CLEAN delete each seeded database (best-effort).
|
|
20
|
+
#
|
|
21
|
+
# Seed snapshots are resolved against EOPS_GYM_DBS_DIR (the unzipped gym_dbs.zip). A task
|
|
22
|
+
# whose server has no seed_database_file, or an unset EOPS_GYM_DBS_DIR, fails loud — an
|
|
23
|
+
# unseeded run would score every solve against the same default state (no signal).
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import re
|
|
29
|
+
import sys
|
|
30
|
+
import urllib.error
|
|
31
|
+
import urllib.request
|
|
32
|
+
import uuid
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def fail(msg: str) -> None:
|
|
36
|
+
print(json.dumps({"error": msg}))
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def request(url: str, payload: dict, headers: dict, method: str = "POST", timeout: float = 60.0) -> str:
|
|
41
|
+
body = json.dumps(payload).encode("utf-8")
|
|
42
|
+
req = urllib.request.Request(url, data=body, method=method)
|
|
43
|
+
req.add_header("Content-Type", "application/json")
|
|
44
|
+
req.add_header("Accept", "application/json, text/event-stream")
|
|
45
|
+
for k, v in (headers or {}).items():
|
|
46
|
+
req.add_header(k, str(v))
|
|
47
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
48
|
+
return resp.read().decode("utf-8")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_body(raw: str) -> object:
|
|
52
|
+
"""Parse a JSON body OR an SSE `data: {...}` stream (the /mcp endpoint streams SSE)."""
|
|
53
|
+
raw = raw.strip()
|
|
54
|
+
if not raw:
|
|
55
|
+
return {}
|
|
56
|
+
if raw.startswith("event:") or raw.startswith("data:") or "\ndata:" in raw:
|
|
57
|
+
matches = re.findall(r"data:\s*(\{.*\})", raw)
|
|
58
|
+
if matches:
|
|
59
|
+
return json.loads(matches[-1])
|
|
60
|
+
return json.loads(raw)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def auth_headers(server: dict) -> dict:
|
|
64
|
+
"""Context auth headers (e.g. x-itsm-user-token) plus the per-run x-database-id."""
|
|
65
|
+
headers = dict(server.get("context") or {})
|
|
66
|
+
db_id = server.get("_database_id")
|
|
67
|
+
if db_id:
|
|
68
|
+
headers["x-database-id"] = db_id
|
|
69
|
+
return headers
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def server_for_gym(servers: list, gym_name: str) -> dict:
|
|
73
|
+
by_name = {s.get("mcp_server_name"): s for s in servers}
|
|
74
|
+
if gym_name in by_name:
|
|
75
|
+
return by_name[gym_name]
|
|
76
|
+
if len(servers) == 1:
|
|
77
|
+
return servers[0]
|
|
78
|
+
fail(f"verifier gym_name {gym_name!r} not in gym_servers_config ({sorted(by_name)})")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def seed_databases(servers: list, gym_dbs_dir: str) -> None:
|
|
82
|
+
for s in servers:
|
|
83
|
+
seed_file = s.get("seed_database_file")
|
|
84
|
+
if not seed_file:
|
|
85
|
+
fail(
|
|
86
|
+
f"server {s.get('mcp_server_name')!r} has no seed_database_file — cannot seed a "
|
|
87
|
+
"deterministic state (an unseeded run scores every solve against the same default DB)"
|
|
88
|
+
)
|
|
89
|
+
if not gym_dbs_dir:
|
|
90
|
+
fail(
|
|
91
|
+
"EOPS_GYM_DBS_DIR is unset — cannot resolve seed_database_file. Unzip gym_dbs.zip "
|
|
92
|
+
"(from ServiceNow/EnterpriseOps-Gym) and set EOPS_GYM_DBS_DIR to its directory."
|
|
93
|
+
)
|
|
94
|
+
path = seed_file if os.path.isabs(seed_file) else os.path.join(gym_dbs_dir, seed_file)
|
|
95
|
+
try:
|
|
96
|
+
sql = open(path, encoding="utf-8").read()
|
|
97
|
+
except Exception as e: # noqa: BLE001
|
|
98
|
+
fail(f"reading seed file {path} failed: {e}")
|
|
99
|
+
db_id = "gate_" + uuid.uuid4().hex[:12]
|
|
100
|
+
url = s["mcp_server_url"].rstrip("/") + "/api/seed-database"
|
|
101
|
+
payload = {
|
|
102
|
+
"database_id": db_id,
|
|
103
|
+
"name": f"gate_{db_id}",
|
|
104
|
+
"description": f"gate seed from {os.path.basename(seed_file)}",
|
|
105
|
+
"sql_content": sql,
|
|
106
|
+
}
|
|
107
|
+
try:
|
|
108
|
+
request(url, payload, {}, timeout=max(300.0, len(sql) / 1024))
|
|
109
|
+
except Exception as e: # noqa: BLE001
|
|
110
|
+
fail(f"seed-database POST to {url} failed: {e}")
|
|
111
|
+
s["_database_id"] = db_id
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def delete_databases(servers: list) -> None:
|
|
115
|
+
for s in servers:
|
|
116
|
+
db_id = s.get("_database_id")
|
|
117
|
+
if not db_id:
|
|
118
|
+
continue
|
|
119
|
+
url = s["mcp_server_url"].rstrip("/") + "/api/delete-database"
|
|
120
|
+
try:
|
|
121
|
+
request(url, {"database_id": db_id}, {}, method="DELETE", timeout=30)
|
|
122
|
+
except Exception: # noqa: BLE001
|
|
123
|
+
pass # best-effort cleanup; a leaked db never corrupts a future run (unique id)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def replay_transcript(servers: list, transcript: list) -> int:
|
|
127
|
+
"""Replay each call as an MCP tools/call. Returns the count of agent-side call errors
|
|
128
|
+
(surfaced, never fatal). A transport failure fails loud."""
|
|
129
|
+
errors = 0
|
|
130
|
+
for i, call in enumerate(transcript):
|
|
131
|
+
tool = call.get("tool")
|
|
132
|
+
if not isinstance(tool, str) or not tool:
|
|
133
|
+
errors += 1
|
|
134
|
+
continue
|
|
135
|
+
gym_name = call.get("gym_name")
|
|
136
|
+
server = server_for_gym(servers, gym_name) if gym_name else servers[0]
|
|
137
|
+
url = server["mcp_server_url"].rstrip("/") + "/mcp"
|
|
138
|
+
payload = {
|
|
139
|
+
"jsonrpc": "2.0",
|
|
140
|
+
"id": i + 1,
|
|
141
|
+
"method": "tools/call",
|
|
142
|
+
"params": {"name": tool, "arguments": call.get("arguments") or {}},
|
|
143
|
+
}
|
|
144
|
+
try:
|
|
145
|
+
raw = request(url, payload, auth_headers(server), timeout=60)
|
|
146
|
+
result = parse_body(raw)
|
|
147
|
+
# A JSON-RPC error or an isError tool result is the agent's mistake, not a judge
|
|
148
|
+
# failure — count it so a wholesale replay failure is visible, but keep going.
|
|
149
|
+
if isinstance(result, dict) and (result.get("error") or (result.get("result") or {}).get("isError")):
|
|
150
|
+
errors += 1
|
|
151
|
+
except urllib.error.HTTPError:
|
|
152
|
+
errors += 1 # the agent issued a bad call (wrong args / policy) — verifiers score it
|
|
153
|
+
except urllib.error.URLError as e:
|
|
154
|
+
fail(f"gym server unreachable at {url} replaying tool {tool!r}: {e}")
|
|
155
|
+
except Exception as e: # noqa: BLE001
|
|
156
|
+
fail(f"tool call {tool!r} against {url} failed unexpectedly: {e}")
|
|
157
|
+
return errors
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def run_sql(server: dict, query: str) -> object:
|
|
161
|
+
url = server["mcp_server_url"].rstrip("/") + "/api/sql-runner"
|
|
162
|
+
payload = {"query": query, "database_id": server.get("_database_id")}
|
|
163
|
+
try:
|
|
164
|
+
out = parse_body(request(url, payload, auth_headers(server), timeout=60))
|
|
165
|
+
except urllib.error.URLError as e:
|
|
166
|
+
fail(f"gym server unreachable at {url}: {e}")
|
|
167
|
+
except Exception as e: # noqa: BLE001
|
|
168
|
+
fail(f"sql-runner POST to {url} failed: {e}")
|
|
169
|
+
if isinstance(out, dict):
|
|
170
|
+
if out.get("error"):
|
|
171
|
+
fail(f"sql-runner error for query {query!r}: {out['error']}")
|
|
172
|
+
# The gym server returns {"data":[{col:val}], ...}; older/other shapes use result/rows.
|
|
173
|
+
for key in ("data", "rows"):
|
|
174
|
+
rows = out.get(key)
|
|
175
|
+
if rows:
|
|
176
|
+
first = rows[0]
|
|
177
|
+
if isinstance(first, dict) and first:
|
|
178
|
+
return next(iter(first.values()))
|
|
179
|
+
if isinstance(first, list) and first:
|
|
180
|
+
return first[0]
|
|
181
|
+
return first
|
|
182
|
+
if "result" in out:
|
|
183
|
+
return out["result"]
|
|
184
|
+
return out
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def compare(actual: object, expected: object, comparison_type: str) -> bool:
|
|
188
|
+
if comparison_type == "equals":
|
|
189
|
+
try:
|
|
190
|
+
return float(actual) == float(expected)
|
|
191
|
+
except (TypeError, ValueError):
|
|
192
|
+
return str(actual) == str(expected)
|
|
193
|
+
if comparison_type == "greater_than":
|
|
194
|
+
return float(actual) > float(expected)
|
|
195
|
+
if comparison_type == "less_than":
|
|
196
|
+
return float(actual) < float(expected)
|
|
197
|
+
if comparison_type == "contains":
|
|
198
|
+
return str(expected) in str(actual)
|
|
199
|
+
fail(f"unsupported comparison_type {comparison_type!r}")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def cmd_judge(args) -> None:
|
|
203
|
+
raw_transcript = sys.stdin.read()
|
|
204
|
+
try:
|
|
205
|
+
task = json.load(open(args.task_json, encoding="utf-8"))
|
|
206
|
+
except Exception as e: # noqa: BLE001
|
|
207
|
+
fail(f"reading task json {args.task_json} failed: {e}")
|
|
208
|
+
|
|
209
|
+
servers = task.get("gym_servers_config")
|
|
210
|
+
if isinstance(servers, str):
|
|
211
|
+
servers = json.loads(servers)
|
|
212
|
+
if not isinstance(servers, list) or not servers:
|
|
213
|
+
fail("task has no gym_servers_config list")
|
|
214
|
+
|
|
215
|
+
verifiers = task.get("verifiers")
|
|
216
|
+
if isinstance(verifiers, str):
|
|
217
|
+
verifiers = json.loads(verifiers)
|
|
218
|
+
if not isinstance(verifiers, list) or not verifiers:
|
|
219
|
+
fail("task has no verifiers list")
|
|
220
|
+
|
|
221
|
+
transcript: list = []
|
|
222
|
+
if raw_transcript.strip():
|
|
223
|
+
try:
|
|
224
|
+
parsed = json.loads(raw_transcript)
|
|
225
|
+
except Exception as e: # noqa: BLE001
|
|
226
|
+
fail(f"transcript is not valid JSON: {e}")
|
|
227
|
+
transcript = parsed.get("calls", []) if isinstance(parsed, dict) else parsed
|
|
228
|
+
if not isinstance(transcript, list):
|
|
229
|
+
fail('transcript must be a JSON list of tool calls (or {"calls":[...]})')
|
|
230
|
+
|
|
231
|
+
gym_dbs_dir = os.environ.get("EOPS_GYM_DBS_DIR", "")
|
|
232
|
+
seed_databases(servers, gym_dbs_dir)
|
|
233
|
+
try:
|
|
234
|
+
replay_errors = replay_transcript(servers, transcript)
|
|
235
|
+
|
|
236
|
+
results = []
|
|
237
|
+
passes = 0
|
|
238
|
+
for v in verifiers:
|
|
239
|
+
vtype = v.get("verifier_type")
|
|
240
|
+
if vtype != "database_state":
|
|
241
|
+
fail(f"verifier {v.get('name')!r} has non-deterministic verifier_type {vtype!r}")
|
|
242
|
+
cfg = v.get("validation_config") or {}
|
|
243
|
+
query = cfg.get("query")
|
|
244
|
+
if not isinstance(query, str) or not query.strip():
|
|
245
|
+
fail(f"verifier {v.get('name')!r} has no SQL query")
|
|
246
|
+
server = server_for_gym(servers, v.get("gym_name"))
|
|
247
|
+
actual = run_sql(server, query)
|
|
248
|
+
ok = compare(actual, cfg.get("expected_value"), cfg.get("comparison_type", "equals"))
|
|
249
|
+
if ok:
|
|
250
|
+
passes += 1
|
|
251
|
+
results.append({"name": v.get("name"), "passed": bool(ok)})
|
|
252
|
+
|
|
253
|
+
total = len(verifiers)
|
|
254
|
+
print(
|
|
255
|
+
json.dumps(
|
|
256
|
+
{
|
|
257
|
+
"success": passes == total,
|
|
258
|
+
"passes": passes,
|
|
259
|
+
"total": total,
|
|
260
|
+
"replay_calls": len(transcript),
|
|
261
|
+
"replay_errors": replay_errors,
|
|
262
|
+
"verifiers": results,
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
finally:
|
|
267
|
+
delete_databases(servers)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def main() -> None:
|
|
271
|
+
ap = argparse.ArgumentParser(description="EnterpriseOps-Gym judge driver")
|
|
272
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
273
|
+
p = sub.add_parser("judge")
|
|
274
|
+
p.add_argument("--task-json", required=True)
|
|
275
|
+
args = ap.parse_args()
|
|
276
|
+
if args.cmd == "judge":
|
|
277
|
+
cmd_judge(args)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
if __name__ == "__main__":
|
|
281
|
+
main()
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# ProgramBench judge driver: materialize the worker's file-manifest submission
|
|
2
|
+
# (read from stdin, ===FILE:<path>=== envelopes) into a run-dir
|
|
3
|
+
# (<run>/<instance>/submission.tar.gz + <instance>.traj.json), pull the hidden
|
|
4
|
+
# test blobs with `programbench blob sync`, run `programbench eval`, then parse
|
|
5
|
+
# <instance>.eval.json applying the tests.json ignore mask. Emit
|
|
6
|
+
# {"passed","total","resolved"} as the LAST stdout line. Fail loud: any harness or
|
|
7
|
+
# Docker error prints {"error": "..."} and exits nonzero — never a fabricated score.
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import io
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import tarfile
|
|
16
|
+
import tempfile
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def fail(msg: str) -> None:
|
|
21
|
+
print(json.dumps({"error": msg}))
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def parse_manifest(text: str) -> dict[str, str]:
|
|
26
|
+
"""Split the ===FILE:<path>=== envelope stream into {path: contents}."""
|
|
27
|
+
files: dict[str, str] = {}
|
|
28
|
+
parts = text.split("===FILE:")
|
|
29
|
+
for part in parts[1:]:
|
|
30
|
+
header, _, body = part.partition("\n")
|
|
31
|
+
path = header.replace("===", "").strip()
|
|
32
|
+
if path:
|
|
33
|
+
files[path] = body
|
|
34
|
+
return files
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run(cmd: list[str], cwd: str | None = None) -> subprocess.CompletedProcess:
|
|
38
|
+
return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main() -> None:
|
|
42
|
+
ap = argparse.ArgumentParser(description="programbench single-instance judge")
|
|
43
|
+
ap.add_argument("--instance", required=True)
|
|
44
|
+
args = ap.parse_args()
|
|
45
|
+
|
|
46
|
+
submission_text = sys.stdin.read()
|
|
47
|
+
files = parse_manifest(submission_text)
|
|
48
|
+
# A submission with no compile.sh cannot build; the harness scores it 0, which
|
|
49
|
+
# is the honest fail-closed outcome — we still stage it so `total` is real.
|
|
50
|
+
|
|
51
|
+
pb = os.environ.get("PROGRAMBENCH_BIN", "programbench")
|
|
52
|
+
|
|
53
|
+
run_dir = tempfile.mkdtemp(prefix="programbench-run-")
|
|
54
|
+
try:
|
|
55
|
+
inst_dir = Path(run_dir) / args.instance
|
|
56
|
+
inst_dir.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
|
|
58
|
+
# submission.tar.gz = gzipped tar of the agent's workspace files.
|
|
59
|
+
tar_path = inst_dir / "submission.tar.gz"
|
|
60
|
+
with tarfile.open(tar_path, "w:gz") as tar:
|
|
61
|
+
for path, content in files.items():
|
|
62
|
+
data = content.encode("utf-8")
|
|
63
|
+
info = tarfile.TarInfo(name=path)
|
|
64
|
+
info.size = len(data)
|
|
65
|
+
# compile.sh must be executable for the harness to run it.
|
|
66
|
+
info.mode = 0o755 if path.endswith(".sh") else 0o644
|
|
67
|
+
tar.addfile(info, io.BytesIO(data))
|
|
68
|
+
|
|
69
|
+
# Minimal trajectory file the harness expects alongside the submission.
|
|
70
|
+
(inst_dir / f"{args.instance}.traj.json").write_text(json.dumps({"instance_id": args.instance, "steps": []}))
|
|
71
|
+
|
|
72
|
+
# Pull the hidden behavioral test blobs for this instance.
|
|
73
|
+
sync = run([pb, "blob", "sync", args.instance], cwd=run_dir)
|
|
74
|
+
if sync.returncode != 0:
|
|
75
|
+
fail(f"programbench blob sync failed: {sync.stderr.strip()[:600]}")
|
|
76
|
+
|
|
77
|
+
# Run the cleanroom Docker image, build via compile.sh, run hidden tests.
|
|
78
|
+
ev = run([pb, "eval", run_dir], cwd=run_dir)
|
|
79
|
+
if ev.returncode != 0:
|
|
80
|
+
fail(f"programbench eval failed: {ev.stderr.strip()[:800]}")
|
|
81
|
+
|
|
82
|
+
eval_path = inst_dir / f"{args.instance}.eval.json"
|
|
83
|
+
if not eval_path.exists():
|
|
84
|
+
fail(f"programbench wrote no eval.json at {eval_path}: {ev.stdout.strip()[:400]}")
|
|
85
|
+
|
|
86
|
+
report = json.loads(eval_path.read_text())
|
|
87
|
+
if report.get("error_code"):
|
|
88
|
+
fail(f"programbench eval error_code={report['error_code']}: {report.get('error_details')}")
|
|
89
|
+
|
|
90
|
+
results = report.get("test_results", [])
|
|
91
|
+
|
|
92
|
+
# Apply the tests.json ignore mask (the same logic `programbench info`
|
|
93
|
+
# applies). The mask ships in the package data; load it if present.
|
|
94
|
+
ignored: set[str] = set()
|
|
95
|
+
try:
|
|
96
|
+
import programbench # noqa: F401
|
|
97
|
+
from importlib import resources
|
|
98
|
+
|
|
99
|
+
data = resources.files("programbench").joinpath("data/tests.json")
|
|
100
|
+
if data.is_file():
|
|
101
|
+
mask = json.loads(data.read_text())
|
|
102
|
+
entry = mask.get(args.instance, {})
|
|
103
|
+
ignored = set(entry.get("ignored_tests", []))
|
|
104
|
+
except Exception: # noqa: BLE001
|
|
105
|
+
ignored = set()
|
|
106
|
+
|
|
107
|
+
scored = [r for r in results if r.get("name") not in ignored]
|
|
108
|
+
passed = sum(1 for r in scored if r.get("status") == "passed")
|
|
109
|
+
total = len(scored)
|
|
110
|
+
resolved = total > 0 and passed == total
|
|
111
|
+
|
|
112
|
+
print(json.dumps({"passed": passed, "total": total, "resolved": resolved}))
|
|
113
|
+
finally:
|
|
114
|
+
import shutil
|
|
115
|
+
|
|
116
|
+
shutil.rmtree(run_dir, ignore_errors=True)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
main()
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render the gate results as clean, dependency-free SVG bar charts.
|
|
3
|
+
*
|
|
4
|
+
* One honest chart per benchmark: bars for each arm (baseline greys + the
|
|
5
|
+
* verifier-grounded method in the accent), an optional dashed oracle-ceiling line,
|
|
6
|
+
* and a single annotated delta. No chartjunk — value labels, one accent, lots of
|
|
7
|
+
* whitespace. Data is passed in (no hidden numbers) so the charts regenerate from
|
|
8
|
+
* the real run outputs.
|
|
9
|
+
*
|
|
10
|
+
* node bench/scripts/render-gate-chart.mjs # writes docs/assets/*.svg
|
|
11
|
+
* (then: cairosvg docs/assets/<f>.svg -o /tmp/<f>.png to preview)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
15
|
+
import { dirname, join } from 'node:path'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
|
|
18
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
19
|
+
const assetsDir = join(here, '..', '..', 'docs', 'assets')
|
|
20
|
+
|
|
21
|
+
const palette = {
|
|
22
|
+
bg: '#ffffff',
|
|
23
|
+
ink: '#0f172a', // slate-900 — titles, values
|
|
24
|
+
muted: '#64748b', // slate-500 — subtitles, labels
|
|
25
|
+
faint: '#e2e8f0', // slate-200 — baseline / axis
|
|
26
|
+
baseline: '#cbd5e1', // slate-300 — "blind" / weaker baselines
|
|
27
|
+
baseline2: '#94a3b8', // slate-400 — the competing selector
|
|
28
|
+
method: '#4f46e5', // indigo-600 — the verifier-grounded method (the hero)
|
|
29
|
+
methodSoft: '#eef2ff', // indigo-50 — method bar wash
|
|
30
|
+
ceiling: '#475569', // slate-600 — oracle ceiling line
|
|
31
|
+
good: '#059669', // emerald-600 — positive delta
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
35
|
+
const f = 'system-ui, -apple-system, "Segoe UI", Inter, Roboto, sans-serif'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {{
|
|
39
|
+
* title: string, subtitle: string, footnote?: string,
|
|
40
|
+
* bars: {label: string, value: number, kind: 'blind'|'rival'|'method'}[],
|
|
41
|
+
* ceiling?: {value: number, label: string},
|
|
42
|
+
* delta?: {fromIdx: number, toIdx: number, label: string},
|
|
43
|
+
* yMax?: number, unit?: string,
|
|
44
|
+
* }} cfg
|
|
45
|
+
*/
|
|
46
|
+
function chart(cfg) {
|
|
47
|
+
const W = 820
|
|
48
|
+
const H = 524
|
|
49
|
+
const m = { top: 152, right: 136, bottom: 120, left: 66 }
|
|
50
|
+
const plotW = W - m.left - m.right
|
|
51
|
+
const plotH = H - m.top - m.bottom
|
|
52
|
+
const yMax = cfg.yMax ?? 100
|
|
53
|
+
const unit = cfg.unit ?? '%'
|
|
54
|
+
const y = (v) => m.top + plotH * (1 - v / yMax)
|
|
55
|
+
|
|
56
|
+
const n = cfg.bars.length
|
|
57
|
+
const bandW = plotW / n
|
|
58
|
+
const barW = Math.min(112, bandW * 0.56)
|
|
59
|
+
const cx = (i) => m.left + bandW * (i + 0.5)
|
|
60
|
+
const fill = { blind: palette.baseline, rival: palette.baseline2, method: palette.method }
|
|
61
|
+
|
|
62
|
+
const parts = []
|
|
63
|
+
parts.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" font-family='${f}'>`)
|
|
64
|
+
parts.push(`<rect width="${W}" height="${H}" fill="${palette.bg}"/>`)
|
|
65
|
+
|
|
66
|
+
// title + subtitle
|
|
67
|
+
parts.push(`<text x="${m.left}" y="46" font-size="22" font-weight="700" fill="${palette.ink}">${esc(cfg.title)}</text>`)
|
|
68
|
+
parts.push(`<text x="${m.left}" y="72" font-size="13.5" fill="${palette.muted}">${esc(cfg.subtitle)}</text>`)
|
|
69
|
+
|
|
70
|
+
// baseline axis
|
|
71
|
+
parts.push(`<line x1="${m.left}" y1="${y(0)}" x2="${m.left + plotW}" y2="${y(0)}" stroke="${palette.faint}" stroke-width="1.5"/>`)
|
|
72
|
+
// light gridlines at 25/50/75/100
|
|
73
|
+
for (const g of [25, 50, 75, 100]) {
|
|
74
|
+
if (g > yMax) continue
|
|
75
|
+
parts.push(`<line x1="${m.left}" y1="${y(g)}" x2="${m.left + plotW}" y2="${y(g)}" stroke="${palette.faint}" stroke-width="1" stroke-dasharray="2 5"/>`)
|
|
76
|
+
parts.push(`<text x="${m.left - 12}" y="${y(g) + 4}" font-size="11" text-anchor="end" fill="${palette.muted}">${g}${unit}</text>`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// oracle ceiling line
|
|
80
|
+
if (cfg.ceiling) {
|
|
81
|
+
const yc = y(cfg.ceiling.value)
|
|
82
|
+
parts.push(`<line x1="${m.left}" y1="${yc}" x2="${m.left + plotW}" y2="${yc}" stroke="${palette.ceiling}" stroke-width="1.5" stroke-dasharray="6 4"/>`)
|
|
83
|
+
parts.push(`<text x="${m.left + plotW + 8}" y="${yc - 5}" font-size="12" font-weight="600" fill="${palette.ceiling}">${esc(cfg.ceiling.label)}</text>`)
|
|
84
|
+
parts.push(`<text x="${m.left + plotW + 8}" y="${yc + 12}" font-size="12" fill="${palette.ceiling}">${cfg.ceiling.value}${unit}</text>`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// bars
|
|
88
|
+
cfg.bars.forEach((b, i) => {
|
|
89
|
+
const x = cx(i) - barW / 2
|
|
90
|
+
const top = y(b.value)
|
|
91
|
+
const h = y(0) - top
|
|
92
|
+
const isMethod = b.kind === 'method'
|
|
93
|
+
if (isMethod) parts.push(`<rect x="${x - 3}" y="${top - 3}" width="${barW + 6}" height="${h + 3}" rx="7" fill="${palette.methodSoft}"/>`)
|
|
94
|
+
parts.push(`<rect x="${x}" y="${top}" width="${barW}" height="${h}" rx="5" fill="${fill[b.kind]}"/>`)
|
|
95
|
+
// value label
|
|
96
|
+
parts.push(`<text x="${cx(i)}" y="${top - 12}" font-size="16" font-weight="700" text-anchor="middle" fill="${isMethod ? palette.method : palette.ink}">${b.value}${unit}</text>`)
|
|
97
|
+
// x label (two lines if it contains a newline)
|
|
98
|
+
const lines = b.label.split('\n')
|
|
99
|
+
lines.forEach((ln, li) => {
|
|
100
|
+
parts.push(`<text x="${cx(i)}" y="${y(0) + 24 + li * 16}" font-size="12.5" font-weight="${isMethod ? 600 : 400}" text-anchor="middle" fill="${isMethod ? palette.method : palette.muted}">${esc(ln)}</text>`)
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// delta annotation: a bracket from one bar's top to another's, labelled
|
|
105
|
+
if (cfg.delta) {
|
|
106
|
+
const a = cfg.delta.fromIdx
|
|
107
|
+
const b = cfg.delta.toIdx
|
|
108
|
+
const xa = cx(a)
|
|
109
|
+
const xb = cx(b)
|
|
110
|
+
const ya = y(cfg.bars[a].value)
|
|
111
|
+
const yb = y(cfg.bars[b].value)
|
|
112
|
+
const yBar = Math.min(ya, yb) - 34
|
|
113
|
+
const midx = (xa + xb) / 2
|
|
114
|
+
parts.push(`<line x1="${xa}" y1="${ya - 26}" x2="${xa}" y2="${yBar}" stroke="${palette.good}" stroke-width="1.25"/>`)
|
|
115
|
+
parts.push(`<line x1="${xb}" y1="${yb - 26}" x2="${xb}" y2="${yBar}" stroke="${palette.good}" stroke-width="1.25"/>`)
|
|
116
|
+
parts.push(`<line x1="${xa}" y1="${yBar}" x2="${xb}" y2="${yBar}" stroke="${palette.good}" stroke-width="1.25"/>`)
|
|
117
|
+
const lw = cfg.delta.label.length * 7.2 + 16
|
|
118
|
+
parts.push(`<rect x="${midx - lw / 2}" y="${yBar - 22}" width="${lw}" height="20" rx="10" fill="${palette.good}"/>`)
|
|
119
|
+
parts.push(`<text x="${midx}" y="${yBar - 8}" font-size="12.5" font-weight="700" text-anchor="middle" fill="#ffffff">${esc(cfg.delta.label)}</text>`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (cfg.footnote) {
|
|
123
|
+
// wrap to the plot width (~92 chars at 11.5px over the full canvas)
|
|
124
|
+
const words = cfg.footnote.split(' ')
|
|
125
|
+
const lines = []
|
|
126
|
+
let cur = ''
|
|
127
|
+
for (const w of words) {
|
|
128
|
+
if ((cur + ' ' + w).trim().length > 104) {
|
|
129
|
+
lines.push(cur.trim())
|
|
130
|
+
cur = w
|
|
131
|
+
} else cur = `${cur} ${w}`
|
|
132
|
+
}
|
|
133
|
+
if (cur.trim()) lines.push(cur.trim())
|
|
134
|
+
const startY = H - 20 - (lines.length - 1) * 15
|
|
135
|
+
lines.forEach((ln, i) => {
|
|
136
|
+
parts.push(`<text x="${m.left}" y="${startY + i * 15}" font-size="11.5" fill="${palette.muted}">${esc(ln)}</text>`)
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
parts.push('</svg>')
|
|
140
|
+
return parts.join('\n')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---- the charts (numbers are the real, verified run outputs) -------------------
|
|
144
|
+
|
|
145
|
+
const humaneval = chart({
|
|
146
|
+
title: 'Verifier-grounded selection recovers the oracle ceiling',
|
|
147
|
+
subtitle: 'HumanEval · gpt-3.5-turbo · k=4 · n=50 · paired bootstrap (B=10000) + Benjamini–Hochberg',
|
|
148
|
+
bars: [
|
|
149
|
+
{ label: 'Blind\n(1 shot)', value: 80, kind: 'blind' },
|
|
150
|
+
{ label: 'Self-consistency\n(pick the consensus)', value: 82, kind: 'rival' },
|
|
151
|
+
{ label: 'Verifier-grounded\n(re-run the tests)', value: 92, kind: 'method' },
|
|
152
|
+
],
|
|
153
|
+
ceiling: { value: 92, label: 'oracle ceiling', },
|
|
154
|
+
delta: { fromIdx: 1, toIdx: 2, label: '+10.0pp CI[+2, +18]' },
|
|
155
|
+
footnote: 'Deployable, non-oracle selector: ranks k attempts by re-running the task’s own unit tests (gold never shown). It captures the FULL ceiling (gap 0); self-consistency leaves 10pp on the table. Reproduced across two independent n=50 runs (+10pp / +12pp, both BH-positive).',
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// commit0 — single-task Layer-1 VALIDATION (wcwidth, k=3). Regenerate with the powered
|
|
159
|
+
// n=10 cross-task gate when it lands. Honestly labelled as a per-task validation.
|
|
160
|
+
const commit0Validation = chart({
|
|
161
|
+
title: 'The selector signal survives real stateful coding rollouts',
|
|
162
|
+
subtitle: 'commit0 · gpt-4.1 · k=3 · single-task validation (wcwidth) — official pytest harness, graded pass-rate',
|
|
163
|
+
bars: [
|
|
164
|
+
{ label: 'Random pick\n(blind, mean-of-k)', value: 67.5, kind: 'blind' },
|
|
165
|
+
{ label: 'Verifier-grounded\n(highest test-pass)', value: 78.9, kind: 'method' },
|
|
166
|
+
],
|
|
167
|
+
ceiling: { value: 78.9, label: 'best-of-k', },
|
|
168
|
+
delta: { fromIdx: 0, toIdx: 1, label: '+11.4pp' },
|
|
169
|
+
footnote: 'Layer-1: agent clones the repo, implements, runs pytest, iterates, emits a diff. Attempts score 60.5 / 78.9 / 63.2% — within-task variance the selector exploits. Powered n=10 cross-task gate in progress.',
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
mkdirSync(assetsDir, { recursive: true })
|
|
173
|
+
writeFileSync(join(assetsDir, 'gate-humaneval.svg'), humaneval)
|
|
174
|
+
writeFileSync(join(assetsDir, 'gate-commit0-validation.svg'), commit0Validation)
|
|
175
|
+
console.log(`wrote ${join(assetsDir, 'gate-humaneval.svg')}`)
|
|
176
|
+
console.log(`wrote ${join(assetsDir, 'gate-commit0-validation.svg')}`)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
import { access, readdir } from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { promisify } from 'node:util'
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile)
|
|
8
|
+
const benchDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
9
|
+
const sourceDir = path.join(benchDir, 'src')
|
|
10
|
+
|
|
11
|
+
async function collectTests(dir) {
|
|
12
|
+
const files = []
|
|
13
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
14
|
+
const absolute = path.join(dir, entry.name)
|
|
15
|
+
if (entry.isDirectory()) files.push(...(await collectTests(absolute)))
|
|
16
|
+
else if (entry.isFile() && /\.test\.(?:mts|ts)$/.test(entry.name)) files.push(absolute)
|
|
17
|
+
}
|
|
18
|
+
return files.sort()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function run(command, args, env = process.env) {
|
|
22
|
+
try {
|
|
23
|
+
await execFileAsync(command, args, {
|
|
24
|
+
cwd: benchDir,
|
|
25
|
+
env,
|
|
26
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
27
|
+
timeout: 120_000,
|
|
28
|
+
})
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error?.stdout) process.stdout.write(error.stdout)
|
|
31
|
+
if (error?.stderr) process.stderr.write(error.stderr)
|
|
32
|
+
const invocation = [command, ...args].join(' ')
|
|
33
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
34
|
+
throw new Error(`${invocation} failed: ${message}`, { cause: error })
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const python = path.join(benchDir, '.venv', 'bin', 'python')
|
|
39
|
+
try {
|
|
40
|
+
await access(python)
|
|
41
|
+
} catch {
|
|
42
|
+
await run('python3', ['-m', 'venv', '.venv'])
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const tests = await collectTests(sourceDir)
|
|
46
|
+
const relativeTests = tests.map((file) => path.relative(benchDir, file))
|
|
47
|
+
if (relativeTests.length === 0) throw new Error('no package tests found under src/')
|
|
48
|
+
|
|
49
|
+
await run(process.execPath, ['--test', '--import', 'tsx', ...relativeTests], {
|
|
50
|
+
...process.env,
|
|
51
|
+
TSX_TSCONFIG_PATH: 'tsconfig.public.json',
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
await run(python, ['-m', 'unittest', 'discover', '-s', 'pier_agents', '-p', '*_test.py'])
|
|
55
|
+
|
|
56
|
+
console.log(`package tests passed: ${tests.length}/${tests.length} TypeScript files + Pier bridge`)
|