@tangle-network/agent-bench 0.1.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.
Files changed (111) hide show
  1. package/README.md +15 -0
  2. package/package.json +35 -0
  3. package/src/adapters.ts +60 -0
  4. package/src/aec-gate.mts +217 -0
  5. package/src/atom-humaneval.mts +197 -0
  6. package/src/atom-mcp-e2e.mts +223 -0
  7. package/src/benchmarks/_harness.ts +206 -0
  8. package/src/benchmarks/aec-bench.test.mts +53 -0
  9. package/src/benchmarks/aec-bench.ts +319 -0
  10. package/src/benchmarks/appworld.test.mts +45 -0
  11. package/src/benchmarks/appworld.ts +426 -0
  12. package/src/benchmarks/cad-design.ts +429 -0
  13. package/src/benchmarks/cadbench.ts +135 -0
  14. package/src/benchmarks/cadgenbench.ts +121 -0
  15. package/src/benchmarks/commit0.test.mts +71 -0
  16. package/src/benchmarks/commit0.ts +266 -0
  17. package/src/benchmarks/enterpriseops-gym.test.mts +77 -0
  18. package/src/benchmarks/enterpriseops-gym.ts +343 -0
  19. package/src/benchmarks/finsearchcomp.ts +371 -0
  20. package/src/benchmarks/frames.ts +520 -0
  21. package/src/benchmarks/hotpotqa.ts +320 -0
  22. package/src/benchmarks/humaneval.ts +251 -0
  23. package/src/benchmarks/mind2web.ts +311 -0
  24. package/src/benchmarks/programbench.test.mts +60 -0
  25. package/src/benchmarks/programbench.ts +210 -0
  26. package/src/benchmarks/simpleqa.ts +390 -0
  27. package/src/benchmarks/swe-bench.ts +152 -0
  28. package/src/benchmarks/terminal-bench.ts +182 -0
  29. package/src/benchmarks/trata-hedge.ts +496 -0
  30. package/src/benchmarks/types.ts +58 -0
  31. package/src/browser/adapters/bad-design-audit.ts +153 -0
  32. package/src/browser/adapters/bad-design-audit.verify.ts +85 -0
  33. package/src/browser/adapters/bad.ts +165 -0
  34. package/src/browser/agent-adapter.ts +145 -0
  35. package/src/browser/process-adapter.ts +146 -0
  36. package/src/browser/run-to-spans.ts +105 -0
  37. package/src/browser/run-to-spans.verify.ts +89 -0
  38. package/src/browser/ui-reviewer.ts +200 -0
  39. package/src/browser/ui-reviewer.verify.ts +94 -0
  40. package/src/browser/verify.ts +96 -0
  41. package/src/clbench-codebase-gate.mts +314 -0
  42. package/src/clbench-context-gate.mts +305 -0
  43. package/src/cloud-loop.mts +138 -0
  44. package/src/coding-skills/minimal-diff.md +9 -0
  45. package/src/coding-skills/read-before-edit.md +9 -0
  46. package/src/coding-skills/reproduce-first.md +10 -0
  47. package/src/coding-skills/run-tests-after-edit.md +9 -0
  48. package/src/coding-skills/trace-the-failure.md +9 -0
  49. package/src/commit0-env-run.mts +59 -0
  50. package/src/commit0-env.ts +173 -0
  51. package/src/commit0-gate.mts +529 -0
  52. package/src/commit0-prereqs.sh +48 -0
  53. package/src/corpus-replay.mts +300 -0
  54. package/src/corpus-report.mts +504 -0
  55. package/src/corpus.test.mts +273 -0
  56. package/src/corpus.ts +325 -0
  57. package/src/decoder-live.mts +133 -0
  58. package/src/directives.ts +84 -0
  59. package/src/diverse-gate.mjs +112 -0
  60. package/src/egress-probe.mts +26 -0
  61. package/src/eops-skills/address-every-subtask.md +5 -0
  62. package/src/eops-skills/exact-tools-and-args.md +5 -0
  63. package/src/eops-skills/full-sequence-to-goal.md +5 -0
  64. package/src/eops-skills/ground-every-value.md +5 -0
  65. package/src/eops-skills/honor-the-policies.md +5 -0
  66. package/src/examples/README.md +58 -0
  67. package/src/examples/math-demo.mts +110 -0
  68. package/src/examples/strategy-demo.mts +119 -0
  69. package/src/fleet.mts +121 -0
  70. package/src/gate-cli.mts +101 -0
  71. package/src/gate.test.mts +129 -0
  72. package/src/gate.ts +460 -0
  73. package/src/generate-eval/certify.ts +178 -0
  74. package/src/generate-eval/schema.ts +78 -0
  75. package/src/humaneval-gate.mts +204 -0
  76. package/src/humaneval-repair-gate.mts +143 -0
  77. package/src/index.ts +19 -0
  78. package/src/mcp-mount-probe.mts +126 -0
  79. package/src/profile-coordinates.ts +134 -0
  80. package/src/profiles.ts +128 -0
  81. package/src/refine-loop.test.mts +106 -0
  82. package/src/refine-loop.ts +106 -0
  83. package/src/research-gate.mts +132 -0
  84. package/src/research-shot.ts +134 -0
  85. package/src/resolve-client.ts +58 -0
  86. package/src/router-executor.ts +51 -0
  87. package/src/run-pool.ts +48 -0
  88. package/src/runtime-hook-recorder.ts +137 -0
  89. package/src/sandbox-run.ts +125 -0
  90. package/src/search-bench/bridge.ts +124 -0
  91. package/src/search-bench/export.mts +0 -0
  92. package/src/search-bench/parametric-check.mts +63 -0
  93. package/src/search-bench/profiles.ts +98 -0
  94. package/src/search-bench/run.mts +287 -0
  95. package/src/search-bench/tasks-fresh.ts +688 -0
  96. package/src/search-bench/tasks.ts +129 -0
  97. package/src/search-tool.ts +95 -0
  98. package/src/selector.test.mts +189 -0
  99. package/src/selector.ts +366 -0
  100. package/src/skill-sandbox-smoke.mts +100 -0
  101. package/src/stats.mts +90 -0
  102. package/src/terminal-compare.ts +519 -0
  103. package/src/trajectory-assemble.mjs +130 -0
  104. package/src/trata-gate.mts +243 -0
  105. package/src/trata-gepa.mts +434 -0
  106. package/src/worker-blender.ts +230 -0
  107. package/src/worker-browser.ts +102 -0
  108. package/src/worker-build123d.ts +143 -0
  109. package/src/worker-cad.ts +451 -0
  110. package/src/worker.ts +136 -0
  111. package/src/workspace-loop.mts +133 -0
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Offline commit0 adapter test. The judge needs the official `commit0` harness +
3
+ * a Docker daemon, neither installed in CI, so this exercises the parts that run
4
+ * offline (fixtures loadTasks, the diff OutputAdapter, goldArtifact) and asserts
5
+ * the harness-backed path FAILS LOUD with the documented fix — never a fake score.
6
+ * Run: COMMIT0_FIXTURES=1 npx tsx --test src/benchmarks/commit0.test.mts
7
+ */
8
+ import assert from 'node:assert/strict'
9
+ import { test } from 'node:test'
10
+ import { commit0DiffOutput, createCommit0Adapter } from './commit0'
11
+
12
+ process.env.COMMIT0_FIXTURES = '1'
13
+
14
+ type Events = Parameters<typeof commit0DiffOutput.parse>[0]
15
+ const stream = (text: string): Events => [{ data: { finalText: text } }] as unknown as Events
16
+
17
+ test('loadTasks (fixtures) yields stubbed-library tasks with diff-deliverable prompt + metadata', async () => {
18
+ const a = createCommit0Adapter()
19
+ const tasks = await a.loadTasks({ ids: ['commit-0/wcwidth'] })
20
+ assert.equal(tasks.length, 1)
21
+ const t = tasks[0]
22
+ assert.equal(t.id, 'commit-0/wcwidth')
23
+ assert.match(t.prompt, /```diff/)
24
+ const md = t.metadata as Record<string, unknown>
25
+ assert.equal(md.instanceId, 'commit-0/wcwidth')
26
+ assert.equal(typeof md.referenceCommit, 'string')
27
+ assert.equal(md.srcDir, 'wcwidth/')
28
+ })
29
+
30
+ test('loadTasks limit slices the fixture set', async () => {
31
+ const a = createCommit0Adapter()
32
+ const tasks = await a.loadTasks({ limit: 2 })
33
+ assert.equal(tasks.length, 2)
34
+ })
35
+
36
+ test('diff OutputAdapter: last fenced ```diff wins; fence-less falls back to trimmed text', () => {
37
+ // The body is newline-terminated (git apply rejects a patch that is not).
38
+ const fenced = commit0DiffOutput.parse(stream('preamble\n```diff\n--- a/x\n+++ b/x\n@@\n+1\n```\n'))
39
+ assert.equal(fenced, '--- a/x\n+++ b/x\n@@\n+1\n')
40
+ const last = commit0DiffOutput.parse(stream('```diff\nFIRST\n```\nmid\n```diff\nSECOND\n```'))
41
+ assert.equal(last, 'SECOND\n')
42
+ const raw = commit0DiffOutput.parse(stream(' bare patch text '))
43
+ assert.equal(raw, 'bare patch text\n')
44
+ // an empty deliverable stays empty (no spurious newline)
45
+ assert.equal(commit0DiffOutput.parse(stream(' ')), '')
46
+ })
47
+
48
+ test('goldArtifact is undefined — oracle is a git ref, documented, not a fabricated diff', async () => {
49
+ const a = createCommit0Adapter()
50
+ const [t] = await a.loadTasks({ ids: ['commit-0/wcwidth'] })
51
+ assert.equal(await a.goldArtifact(t), undefined)
52
+ })
53
+
54
+ test('preflight FAILS LOUD with the install/Docker fix when the harness venv is absent', async () => {
55
+ // Point the isolated-venv override at a non-existent dir so the interpreter is
56
+ // missing — proves preflight throws the documented fix rather than fabricating a
57
+ // score, independent of whether a real .venv-commit0 happens to be installed.
58
+ const prev = process.env.COMMIT0_VENV
59
+ process.env.COMMIT0_VENV = '.venv-commit0-does-not-exist'
60
+ try {
61
+ const a = createCommit0Adapter()
62
+ await assert.rejects(a.preflight(), (e: Error) => {
63
+ assert.match(e.message, /pip install commit0/)
64
+ assert.match(e.message, /Docker daemon/)
65
+ return true
66
+ })
67
+ } finally {
68
+ if (prev === undefined) delete process.env.COMMIT0_VENV
69
+ else process.env.COMMIT0_VENV = prev
70
+ }
71
+ })
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Commit0 adapter (wentingzhao/commit0_combined) — implement-a-library-from-scratch.
3
+ * Each record is a Python repo stubbed at `base_commit` (public function bodies
4
+ * emptied to `pass`) + the full test suite + a natural-language spec URL. Worker
5
+ * artifact = a unified diff that fills in `src_dir`. Judge = the official
6
+ * `commit0` test harness: it stages the starter repo, applies the worker's diff,
7
+ * builds the library's deps and runs `pytest`, writing a per-repo pytest-json
8
+ * `report.json`. Score = (passed + xfail) / total — GRADED / partial-credit (the
9
+ * macro-averaged unit-test pass-rate the leaderboard reports). Fully
10
+ * deterministic — no LLM judge.
11
+ *
12
+ * The OutputAdapter is stream-only (the SWE wrinkle), so the worker emits its
13
+ * implementation as a fenced ```diff against the stubbed repo — same deliverable
14
+ * shape as swe-bench. The expensive per-repo clone+build+test is delegated to the
15
+ * real `commit0` harness on a local Docker backend (NOT reimplemented here).
16
+ *
17
+ * Requires for a live run: an ISOLATED `.venv-commit0` with `commit0` installed
18
+ * (its deps conflict with the shared bench `.venv`; override with COMMIT0_VENV) +
19
+ * a Docker daemon (`--backend local`). For offline/CI dataset listing set
20
+ * COMMIT0_FIXTURES=1 to load the committed lite rows (bench/fixtures/commit0.json)
21
+ * — judging still needs the harness + Docker and fails loud, never a fabricated score.
22
+ */
23
+
24
+ import { join } from 'node:path'
25
+ import { readFile } from 'node:fs/promises'
26
+ import type { OutputAdapter } from '@tangle-network/agent-runtime/loops'
27
+ import { benchRoot, preflightVenvImports, runVenvScriptStdin, venvPythonAt } from './_harness'
28
+ import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
29
+
30
+ const FIXTURES = join(benchRoot, 'fixtures', 'commit0.json')
31
+
32
+ // commit0's pip deps (pydantic/sqlalchemy v1, modal, …) conflict with the shared
33
+ // bench .venv, so its harness runs in an ISOLATED venv. Override with COMMIT0_VENV.
34
+ // Resolved at call-time so the env is honored at run-time (not frozen at import).
35
+ const commit0VenvDir = (): string => process.env.COMMIT0_VENV ?? '.venv-commit0'
36
+ const commit0Python = (): string => venvPythonAt(commit0VenvDir())
37
+
38
+ const DATASET = 'wentingzhao/commit0_combined'
39
+ const DATASET_SPLIT = 'test'
40
+ // HF rows server — columnar pull with no `datasets` install required for listing.
41
+ const ROWS_API = `https://datasets-server.huggingface.co/rows?dataset=${encodeURIComponent(DATASET)}&config=default&split=${DATASET_SPLIT}`
42
+
43
+ /** Reuse the SWE patch extractor shape: last fenced diff/patch, else raw text. */
44
+ export const commit0DiffOutput: OutputAdapter<string> = {
45
+ parse(events) {
46
+ let text = ''
47
+ for (const ev of events) {
48
+ const d = (ev as { data?: Record<string, unknown> })?.data
49
+ const t = d?.finalText ?? d?.text ?? d?.result
50
+ if (typeof t === 'string' && t.length > 0) text = t
51
+ }
52
+ const fences = [...text.matchAll(/```(?:diff|patch)?\s*\n([\s\S]*?)```/g)]
53
+ const body = (fences.at(-1)?.[1] ?? text).trim()
54
+ // `git apply` rejects a patch that is not newline-terminated ("corrupt patch at
55
+ // line N+1"); the .trim() above strips the final newline, so restore exactly one.
56
+ return body.length > 0 ? `${body}\n` : body
57
+ },
58
+ }
59
+
60
+ interface Commit0Setup {
61
+ install: string
62
+ packages: string[] | null
63
+ pip_packages: string[] | null
64
+ pre_install: string[] | null
65
+ python: string
66
+ specification: string
67
+ }
68
+
69
+ interface Commit0Row {
70
+ instance_id: string
71
+ repo: string
72
+ original_repo: string
73
+ base_commit: string
74
+ reference_commit: string
75
+ setup: Commit0Setup
76
+ test: { test_cmd: string; test_dir: string }
77
+ src_dir: string
78
+ }
79
+
80
+ interface Commit0Meta {
81
+ instanceId: string
82
+ repo: string
83
+ originalRepo: string
84
+ baseCommit: string
85
+ referenceCommit: string
86
+ srcDir: string
87
+ testDir: string
88
+ testCmd: string
89
+ specification: string
90
+ }
91
+
92
+ function rowToTask(row: Commit0Row): BenchTask {
93
+ const meta: Commit0Meta = {
94
+ instanceId: row.instance_id,
95
+ repo: row.repo,
96
+ originalRepo: row.original_repo,
97
+ baseCommit: row.base_commit,
98
+ referenceCommit: row.reference_commit,
99
+ srcDir: row.src_dir,
100
+ testDir: row.test.test_dir,
101
+ testCmd: row.test.test_cmd,
102
+ specification: row.setup.specification,
103
+ }
104
+ return {
105
+ id: row.instance_id,
106
+ split: DATASET_SPLIT,
107
+ prompt: [
108
+ `Clone https://github.com/${row.repo} into /work, then \`cd /work && git checkout ${row.base_commit}\`.`,
109
+ `This is the STUBBED library: the public functions/classes under \`${row.src_dir}\` have empty bodies (\`pass\`/\`...\`).`,
110
+ `Fill in COMPLETE implementations under \`${row.src_dir}\` so the existing test suite under \`${row.test.test_dir}\` passes. Read those tests and the spec to learn the required behavior.`,
111
+ `Specification / docs: ${row.setup.specification}`,
112
+ 'Do NOT edit the test files — the evaluation re-runs the existing tests on a fresh clone. Implement only the source.',
113
+ `When done, from /work run EXACTLY: \`git add -A && git diff --cached -- ${row.src_dir}\` and END your reply with its COMPLETE output as the LAST thing, fenced exactly as \`\`\`diff … \`\`\` (nothing after the closing fence). That fenced diff (against ${row.base_commit}) is the only deliverable.`,
114
+ ].join('\n'),
115
+ metadata: meta as unknown as Record<string, unknown>,
116
+ }
117
+ }
118
+
119
+ function readMeta(task: BenchTask): Commit0Meta {
120
+ const md = task.metadata
121
+ if (!md || typeof md.instanceId !== 'string' || typeof md.referenceCommit !== 'string' || typeof md.srcDir !== 'string') {
122
+ throw new Error(`commit0 task ${task.id} missing metadata — loadTasks did not populate it`)
123
+ }
124
+ return md as unknown as Commit0Meta
125
+ }
126
+
127
+ function selectRows(rows: Commit0Row[], opts: LoadOptions): BenchTask[] {
128
+ let tasks = rows.map(rowToTask)
129
+ if (opts.ids) {
130
+ const want = new Set(opts.ids)
131
+ tasks = tasks.filter((t) => want.has(t.id))
132
+ } else if (opts.limit !== undefined) {
133
+ tasks = tasks.slice(0, opts.limit)
134
+ }
135
+ return tasks
136
+ }
137
+
138
+ async function loadFixtures(opts: LoadOptions): Promise<BenchTask[]> {
139
+ const rows = JSON.parse(await readFile(FIXTURES, 'utf8')) as Commit0Row[]
140
+ console.warn(
141
+ `[commit0] COMMIT0_FIXTURES=1 — loading ${rows.length} committed lite rows from ${FIXTURES} (no HF fetch)`,
142
+ )
143
+ return selectRows(rows, opts)
144
+ }
145
+
146
+ /** Pull real rows from the HF rows server (paged). limit caps the pull; ids
147
+ * filtered client-side. Throws on a non-OK response (fail loud). */
148
+ async function fetchRows(opts: LoadOptions): Promise<Commit0Row[]> {
149
+ const target = opts.ids ? opts.ids.length * 4 : (opts.limit ?? 16)
150
+ const rows: Commit0Row[] = []
151
+ const want = opts.ids ? new Set(opts.ids) : null
152
+ const page = 100
153
+ for (let offset = 0; offset < 64 && rows.length < target; offset += page) {
154
+ const res = await fetch(`${ROWS_API}&offset=${offset}&length=${page}`)
155
+ if (!res.ok) throw new Error(`commit0 rows HTTP ${res.status} (offset ${offset}): ${(await res.text()).slice(0, 200)}`)
156
+ const body = (await res.json()) as { rows?: Array<{ row: Commit0Row }>; num_rows_total?: number }
157
+ const got = body.rows ?? []
158
+ if (got.length === 0) break
159
+ for (const r of got) {
160
+ if (want && !want.has(r.row.instance_id)) continue
161
+ rows.push(r.row)
162
+ }
163
+ if (got.length < page) break
164
+ }
165
+ if (rows.length === 0) throw new Error(`commit0: no rows matched ${JSON.stringify(opts)} from ${DATASET}`)
166
+ return rows
167
+ }
168
+
169
+ /**
170
+ * Run the official commit0 harness for one repo over the worker's diff. The
171
+ * harness clones base_commit, applies the diff into src_dir, installs deps and
172
+ * runs pytest, then writes a per-repo pytest-json report. We read that report and
173
+ * compute (passed + xfail) / total — the leaderboard's per-repo pass-rate.
174
+ *
175
+ * This is the expensive, Docker-backed boundary; it is DELEGATED to `commit0`,
176
+ * not reimplemented. The driver script lives in scripts/commit0_judge.py so the
177
+ * harness call + report parse are one auditable python entrypoint.
178
+ */
179
+ async function runHarness(meta: Commit0Meta, artifact: string): Promise<BenchScore> {
180
+ const judge = join(benchRoot, 'scripts', 'commit0_judge.py')
181
+ let stdout: string
182
+ try {
183
+ // The worker's diff is piped to the driver's stdin (shared stdin-aware runner —
184
+ // execFile's `input` option is not honored async and hangs the reader).
185
+ stdout = await runVenvScriptStdin(
186
+ judge,
187
+ ['--dataset', DATASET, '--split', DATASET_SPLIT, '--instance', meta.instanceId, '--src-dir', meta.srcDir],
188
+ artifact,
189
+ { cwd: benchRoot, python: commit0Python() },
190
+ )
191
+ } catch (err) {
192
+ const e = err as { message?: string }
193
+ throw new Error(`commit0 harness failed for ${meta.instanceId}: ${(e.message || String(err)).slice(0, 1500)}`)
194
+ }
195
+ const report = JSON.parse(stdout.trim().split('\n').at(-1) ?? '{}') as {
196
+ passed?: number
197
+ total?: number
198
+ error?: string
199
+ }
200
+ if (report.error) throw new Error(`commit0 harness error for ${meta.instanceId}: ${report.error}`)
201
+ if (typeof report.passed !== 'number' || typeof report.total !== 'number') {
202
+ throw new Error(`commit0 judge returned no {passed,total}: ${stdout.slice(0, 400)}`)
203
+ }
204
+ // total=0 means the harness MEASURED NOTHING (collection error with no declared
205
+ // test ids) — an unmeasured attempt, not a 0% one. Throw so the caller excludes
206
+ // it as infra instead of recording a fabricated zero.
207
+ if (report.total <= 0) {
208
+ throw new Error(`commit0 judge measured no tests for ${meta.instanceId} (total=0): ${stdout.slice(0, 400)}`)
209
+ }
210
+ const score = report.passed / report.total
211
+ return {
212
+ resolved: report.passed === report.total,
213
+ score,
214
+ detail: JSON.stringify({ instanceId: meta.instanceId, passed: report.passed, total: report.total }),
215
+ }
216
+ }
217
+
218
+ export function createCommit0Adapter(): BenchmarkAdapter {
219
+ const fixturesMode = process.env.COMMIT0_FIXTURES === '1'
220
+
221
+ return {
222
+ name: 'commit0',
223
+ output: commit0DiffOutput,
224
+
225
+ async preflight() {
226
+ await preflightVenvImports({
227
+ modules: ['commit0'],
228
+ requireDocker: true,
229
+ python: commit0Python(),
230
+ fix:
231
+ `Fix: (1) python3 -m venv bench/${commit0VenvDir()} && bench/${commit0VenvDir()}/bin/pip install commit0 datasets ` +
232
+ `(an ISOLATED venv — commit0's deps conflict with the shared bench/.venv; override the dir with COMMIT0_VENV) ; ` +
233
+ `(2) ensure the Docker daemon is running (commit0 --backend local builds per-repo images). ` +
234
+ `Dataset rows come from the HF rows server; set COMMIT0_FIXTURES=1 to list the committed lite rows offline.`,
235
+ })
236
+ },
237
+
238
+ async loadTasks(opts: LoadOptions = {}) {
239
+ if (fixturesMode) return loadFixtures(opts)
240
+ let rows: Commit0Row[]
241
+ try {
242
+ rows = await fetchRows(opts)
243
+ } catch (err) {
244
+ console.warn(
245
+ `[commit0] live rows fetch failed (${err instanceof Error ? err.message : err}); falling back to committed fixtures at ${FIXTURES}`,
246
+ )
247
+ return loadFixtures(opts)
248
+ }
249
+ return selectRows(rows, opts)
250
+ },
251
+
252
+ async goldArtifact() {
253
+ // The oracle is the reference_commit's src_dir, which the commit0 harness
254
+ // checks out by ref — not expressible as a portable diff string without
255
+ // cloning. verify-judge against this adapter requires the live harness, so
256
+ // we return undefined (no offline gold-diff). Judge correctness is proven by
257
+ // running the harness on a real solve, not by a synthetic gold patch.
258
+ return undefined
259
+ },
260
+
261
+ async judge(task: BenchTask, artifact: string): Promise<BenchScore> {
262
+ const meta = readMeta(task)
263
+ return runHarness(meta, artifact)
264
+ },
265
+ }
266
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Offline EnterpriseOps-Gym adapter test. The judge needs a live, freshly-seeded
3
+ * gym MCP server (Docker), not installed in CI, so this exercises the parts that
4
+ * run offline (fixtures loadTasks, the transcript OutputAdapter, goldArtifact) and
5
+ * asserts the judge FAILS LOUD with the documented docker fix when no server is
6
+ * reachable — never a fake score. Run:
7
+ * EOPS_FIXTURES=1 npx tsx --test src/benchmarks/enterpriseops-gym.test.mts
8
+ */
9
+ import assert from 'node:assert/strict'
10
+ import { test } from 'node:test'
11
+ import { createEnterpriseOpsGymAdapter, enterpriseOpsTranscriptOutput } from './enterpriseops-gym'
12
+
13
+ process.env.EOPS_FIXTURES = '1'
14
+
15
+ type Events = Parameters<typeof enterpriseOpsTranscriptOutput.parse>[0]
16
+ const stream = (text: string): Events => [{ data: { finalText: text } }] as unknown as Events
17
+
18
+ const itsmId = 'task_20251212_172511_458_e6427839_47076c15'
19
+
20
+ test('loadTasks (fixtures) yields enterprise tasks with tool-list + SQL-verifier metadata', async () => {
21
+ const a = createEnterpriseOpsGymAdapter()
22
+ const tasks = await a.loadTasks({ ids: [itsmId] })
23
+ assert.equal(tasks.length, 1)
24
+ const t = tasks[0]
25
+ assert.equal(t.id, itsmId)
26
+ assert.equal(t.split, 'itsm')
27
+ assert.match(t.prompt, /```json/)
28
+ assert.match(t.prompt, /update_incident/)
29
+ const md = t.metadata as Record<string, unknown>
30
+ assert.equal(md.taskId, itsmId)
31
+ assert.equal(md.domain, 'itsm')
32
+ assert.ok(Array.isArray(md.servers) && (md.servers as unknown[]).length === 1)
33
+ const verifiers = md.verifiers as Array<{ verifier_type: string; validation_config: { query: string } }>
34
+ assert.equal(verifiers.length, 2)
35
+ assert.equal(verifiers[0].verifier_type, 'database_state')
36
+ assert.match(verifiers[0].validation_config.query, /SELECT COUNT/)
37
+ })
38
+
39
+ test('loadTasks scopes by domain split and limit', async () => {
40
+ const a = createEnterpriseOpsGymAdapter()
41
+ const cal = await a.loadTasks({ split: 'calendar' })
42
+ assert.equal(cal.length, 1)
43
+ assert.equal(cal[0].split, 'calendar')
44
+ const capped = await a.loadTasks({ limit: 1 })
45
+ assert.equal(capped.length, 1)
46
+ })
47
+
48
+ test('transcript OutputAdapter: last fenced ```json wins; fence-less falls back to trimmed text', () => {
49
+ const fenced = enterpriseOpsTranscriptOutput.parse(
50
+ stream('preamble\n```json\n{"calls":[{"tool":"update_incident","arguments":{}}]}\n```\n'),
51
+ )
52
+ assert.equal(fenced, '{"calls":[{"tool":"update_incident","arguments":{}}]}')
53
+ const last = enterpriseOpsTranscriptOutput.parse(stream('```json\nFIRST\n```\nmid\n```json\nSECOND\n```'))
54
+ assert.equal(last, 'SECOND')
55
+ const raw = enterpriseOpsTranscriptOutput.parse(stream(' {"calls":[]} '))
56
+ assert.equal(raw, '{"calls":[]}')
57
+ })
58
+
59
+ test('goldArtifact is undefined — oracle is the seeded DB state, documented, not a fabricated transcript', async () => {
60
+ const a = createEnterpriseOpsGymAdapter()
61
+ const [t] = await a.loadTasks({ ids: [itsmId] })
62
+ assert.equal(await a.goldArtifact(t), undefined)
63
+ })
64
+
65
+ test('judge FAILS LOUD with the docker fix when no gym server is reachable (no fake score)', async () => {
66
+ const a = createEnterpriseOpsGymAdapter()
67
+ const [t] = await a.loadTasks({ ids: [itsmId] })
68
+ // Point the metadata at a definitely-dead port so the SQL-runner POST is refused.
69
+ const md = t.metadata as Record<string, unknown>
70
+ const servers = md.servers as Array<{ mcp_server_url: string }>
71
+ servers[0].mcp_server_url = 'http://127.0.0.1:1'
72
+ await assert.rejects(a.judge(t, '{"calls":[]}'), (e: Error) => {
73
+ assert.match(e.message, /enterpriseops-gym judge failed/)
74
+ assert.match(e.message, /docker pull shivakrishnareddyma225\/enterpriseops-gym-mcp-itsm/)
75
+ return true
76
+ })
77
+ })