@tangle-network/agent-bench 0.11.2 → 0.13.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 (174) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/HARNESS.md +6 -2
  3. package/README.md +1 -4
  4. package/dist/benchmarks/swe-bench.js +4 -9
  5. package/dist/benchmarks/swe-bench.js.map +1 -1
  6. package/package.json +5 -5
  7. package/scripts/run-package-tests.mjs +2 -2
  8. package/src/benchmarks/swe-bench.test.mts +49 -0
  9. package/src/benchmarks/swe-bench.ts +4 -9
  10. package/src/quant-arena/README.md +0 -144
  11. package/src/quant-arena/backtest.test.mts +0 -135
  12. package/src/quant-arena/backtest.ts +0 -218
  13. package/src/quant-arena/data.test.mts +0 -44
  14. package/src/quant-arena/data.ts +0 -141
  15. package/src/quant-arena/driver.test.mts +0 -253
  16. package/src/quant-arena/driver.ts +0 -219
  17. package/src/quant-arena/fixtures/data/PROVENANCE.md +0 -26
  18. package/src/quant-arena/fixtures/data/holdout/IDX.csv +0 -523
  19. package/src/quant-arena/fixtures/data/holdout/S01.csv +0 -523
  20. package/src/quant-arena/fixtures/data/holdout/S02.csv +0 -523
  21. package/src/quant-arena/fixtures/data/holdout/S03.csv +0 -523
  22. package/src/quant-arena/fixtures/data/holdout/S04.csv +0 -523
  23. package/src/quant-arena/fixtures/data/holdout/S05.csv +0 -523
  24. package/src/quant-arena/fixtures/data/holdout/S06.csv +0 -523
  25. package/src/quant-arena/fixtures/data/holdout/S07.csv +0 -523
  26. package/src/quant-arena/fixtures/data/holdout/S08.csv +0 -523
  27. package/src/quant-arena/fixtures/data/holdout/S09.csv +0 -523
  28. package/src/quant-arena/fixtures/data/holdout/S10.csv +0 -523
  29. package/src/quant-arena/fixtures/data/insample/IDX.csv +0 -2087
  30. package/src/quant-arena/fixtures/data/insample/S01.csv +0 -2087
  31. package/src/quant-arena/fixtures/data/insample/S02.csv +0 -2087
  32. package/src/quant-arena/fixtures/data/insample/S03.csv +0 -2087
  33. package/src/quant-arena/fixtures/data/insample/S04.csv +0 -2087
  34. package/src/quant-arena/fixtures/data/insample/S05.csv +0 -2087
  35. package/src/quant-arena/fixtures/data/insample/S06.csv +0 -2087
  36. package/src/quant-arena/fixtures/data/insample/S07.csv +0 -2087
  37. package/src/quant-arena/fixtures/data/insample/S08.csv +0 -2087
  38. package/src/quant-arena/fixtures/data/insample/S09.csv +0 -2087
  39. package/src/quant-arena/fixtures/data/insample/S10.csv +0 -2087
  40. package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +0 -16
  41. package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +0 -5
  42. package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +0 -171
  43. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +0 -119
  44. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +0 -119
  45. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +0 -105
  46. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +0 -102
  47. package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +0 -4
  48. package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +0 -2
  49. package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +0 -84
  50. package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +0 -117
  51. package/src/quant-arena/holdout-certify.mts +0 -206
  52. package/src/quant-arena/holdout-certify.test.mts +0 -82
  53. package/src/quant-arena/leak-audit.test.mts +0 -79
  54. package/src/quant-arena/leak-audit.ts +0 -95
  55. package/src/quant-arena/make-fixtures.mts +0 -161
  56. package/src/quant-arena/multiplicity.test.mts +0 -68
  57. package/src/quant-arena/multiplicity.ts +0 -87
  58. package/src/quant-arena/nautilus-certify.ts +0 -31
  59. package/src/quant-arena/oms.ts +0 -90
  60. package/src/quant-arena/profiles/quant-researcher.profile.json +0 -12
  61. package/src/quant-arena/python/pyproject.toml +0 -8
  62. package/src/quant-arena/python/uv.lock +0 -1297
  63. package/src/quant-arena/python/vbt-worker.py +0 -192
  64. package/src/quant-arena/quant-loop.mts +0 -840
  65. package/src/quant-arena/quant-loop.test.mts +0 -75
  66. package/src/quant-arena/strategies/buy-hold-index/strategy.ts +0 -11
  67. package/src/quant-arena/strategies/equal-weight/strategy.ts +0 -20
  68. package/src/quant-arena/strategies/sma-crossover/strategy.ts +0 -42
  69. package/src/quant-arena/types.ts +0 -133
  70. package/src/quant-arena/vbt-client.ts +0 -321
  71. package/src/quant-arena/vbt-parity.test.mts +0 -183
  72. package/src/quant-arena/windows.test.mts +0 -45
  73. package/src/quant-arena/windows.ts +0 -54
  74. package/src/rollout-ledger/backfill-swe-arena.mts +0 -610
  75. package/src/rollout-ledger/backfill-swe-arena.test.mts +0 -347
  76. package/src/rollout-ledger/settle-capture.mts +0 -448
  77. package/src/rollout-ledger/settle-capture.test.mts +0 -270
  78. package/src/swe-arena/activation.mts +0 -225
  79. package/src/swe-arena/activation.test.mts +0 -300
  80. package/src/swe-arena/analyze.ts +0 -211
  81. package/src/swe-arena/arms.ts +0 -862
  82. package/src/swe-arena/bootstrap-meta.mts +0 -188
  83. package/src/swe-arena/bootstrap-meta.test.mts +0 -51
  84. package/src/swe-arena/briefing.mts +0 -217
  85. package/src/swe-arena/briefing.test.mts +0 -179
  86. package/src/swe-arena/calibrate.ts +0 -217
  87. package/src/swe-arena/capabilities.mts +0 -76
  88. package/src/swe-arena/capabilities.test.mts +0 -57
  89. package/src/swe-arena/capacity.ts +0 -198
  90. package/src/swe-arena/cell-evidence.mts +0 -437
  91. package/src/swe-arena/cell-evidence.test.mts +0 -248
  92. package/src/swe-arena/diagnosis-ensemble.test.mts +0 -210
  93. package/src/swe-arena/diagnosis-ensemble.ts +0 -523
  94. package/src/swe-arena/execution.test.mts +0 -1171
  95. package/src/swe-arena/factory-command-container.ts +0 -284
  96. package/src/swe-arena/factory-judge-child.mts +0 -228
  97. package/src/swe-arena/factory.test.mts +0 -645
  98. package/src/swe-arena/fixtures/analyze.py +0 -80
  99. package/src/swe-arena/fixtures/excludes.txt +0 -8
  100. package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +0 -51
  101. package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +0 -29
  102. package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +0 -64
  103. package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +0 -48
  104. package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +0 -29
  105. package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +0 -48
  106. package/src/swe-arena/fixtures/factory/loops-28/calibration.md +0 -47
  107. package/src/swe-arena/fixtures/factory/loops-28/manifest.json +0 -30
  108. package/src/swe-arena/fixtures/factory/loops-28/spec.md +0 -50
  109. package/src/swe-arena/fixtures/gen1-salvage/README.md +0 -45
  110. package/src/swe-arena/fixtures/gen1-salvage/cand0-e6d7361.diff +0 -116
  111. package/src/swe-arena/fixtures/gen1-salvage/cand1-76a8590.diff +0 -293
  112. package/src/swe-arena/fixtures/holdout-preregister.log +0 -12
  113. package/src/swe-arena/fixtures/holdout.json +0 -44
  114. package/src/swe-arena/fixtures/instances.json +0 -146
  115. package/src/swe-arena/fixtures/ledger.jsonl +0 -12
  116. package/src/swe-arena/fixtures/patches/pallets__flask-5014.solo.patch +0 -36
  117. package/src/swe-arena/fixtures/patches/pydata__xarray-4687.sup.patch +0 -33
  118. package/src/swe-arena/fixtures/rejudge.jsonl +0 -15
  119. package/src/swe-arena/fixtures/rematch.jsonl +0 -3
  120. package/src/swe-arena/fixtures/rematch2.jsonl +0 -3
  121. package/src/swe-arena/fixtures/rematch3.jsonl +0 -3
  122. package/src/swe-arena/fixtures/run-report/README.md +0 -43
  123. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +0 -173
  124. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +0 -100
  125. package/src/swe-arena/fixtures/run-report/gen3-rollup.json +0 -551
  126. package/src/swe-arena/fixtures/run-report/gen3-rollup.md +0 -64
  127. package/src/swe-arena/fixtures/sup-journal-true.json +0 -19
  128. package/src/swe-arena/fixtures/verify/astropy__astropy-13033.sh +0 -48
  129. package/src/swe-arena/fixtures/verify/django__django-11532.sh +0 -50
  130. package/src/swe-arena/fixtures/verify/matplotlib__matplotlib-20826.sh +0 -76
  131. package/src/swe-arena/fixtures/verify/pydata__xarray-4687.sh +0 -44
  132. package/src/swe-arena/fixtures/verify/pytest-dev__pytest-6197.sh +0 -32
  133. package/src/swe-arena/fixtures/verify/sphinx-doc__sphinx-9658.sh +0 -51
  134. package/src/swe-arena/fixtures/worker-tokens.json +0 -42
  135. package/src/swe-arena/fixtures.ts +0 -237
  136. package/src/swe-arena/gepa-seat.mts +0 -886
  137. package/src/swe-arena/gepa-seat.test.mts +0 -1136
  138. package/src/swe-arena/holdout-certify.mts +0 -408
  139. package/src/swe-arena/holdout-certify.test.mts +0 -160
  140. package/src/swe-arena/implementation-ref.test.mts +0 -64
  141. package/src/swe-arena/implementation-ref.ts +0 -62
  142. package/src/swe-arena/judge-child.mts +0 -37
  143. package/src/swe-arena/ledger-orphans.mts +0 -77
  144. package/src/swe-arena/ledger-orphans.test.mts +0 -149
  145. package/src/swe-arena/manifest.mts +0 -293
  146. package/src/swe-arena/manifest.test.mts +0 -169
  147. package/src/swe-arena/materialize.ts +0 -142
  148. package/src/swe-arena/outer-loop.mts +0 -2854
  149. package/src/swe-arena/outer-loop.test.mts +0 -714
  150. package/src/swe-arena/parity.test.mts +0 -87
  151. package/src/swe-arena/premeasured-from-cells.mts +0 -296
  152. package/src/swe-arena/premeasured-from-cells.test.mts +0 -201
  153. package/src/swe-arena/proc.test.mts +0 -172
  154. package/src/swe-arena/proc.ts +0 -260
  155. package/src/swe-arena/profiles/deepseek-author.profile.json +0 -12
  156. package/src/swe-arena/profiles/default-author.profile.json +0 -12
  157. package/src/swe-arena/proposer-fanout.mts +0 -736
  158. package/src/swe-arena/proposer-fanout.test.mts +0 -660
  159. package/src/swe-arena/proposer-provenance.mts +0 -176
  160. package/src/swe-arena/proposer-provenance.test.mts +0 -106
  161. package/src/swe-arena/reconcile.ts +0 -0
  162. package/src/swe-arena/replay.mts +0 -183
  163. package/src/swe-arena/replay.test.mts +0 -300
  164. package/src/swe-arena/run-experiment.mts +0 -729
  165. package/src/swe-arena/run-report.mts +0 -75
  166. package/src/swe-arena/run-supervisor.mjs +0 -297
  167. package/src/swe-arena/run-supervisor.test.mts +0 -539
  168. package/src/swe-arena/score-split.mts +0 -140
  169. package/src/swe-arena/score-split.test.mts +0 -123
  170. package/src/swe-arena/scratch-worktree-serialization.test.mts +0 -72
  171. package/src/swe-arena/scratch-worktree.test.mts +0 -56
  172. package/src/swe-arena/scratch-worktree.ts +0 -64
  173. package/src/swe-arena/serialized-judge.ts +0 -414
  174. package/src/swe-arena/types.ts +0 -218
@@ -1,1171 +0,0 @@
1
- /**
2
- * Unit gate for the M2 typed execution path (materialize / calibrate /
3
- * serialized-judge / arms / capacity / run-experiment). Everything here is
4
- * hermetic: no docker, no network, no model tokens — fake judge children,
5
- * synthetic journals, throwaway git repos and an in-test sqlite store. The
6
- * docker-touching parity gate lives in parity.test.mts.
7
- */
8
-
9
- import { createHash } from 'node:crypto'
10
- import { chmod, mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
11
- import { tmpdir } from 'node:os'
12
- import { join } from 'node:path'
13
- import { DatabaseSync } from 'node:sqlite'
14
- import { afterAll, describe, expect, it } from 'vitest'
15
- import {
16
- armProvenance,
17
- assertSoloProcessCompleted,
18
- assertSupervisorProcessCompleted,
19
- loadExcludes,
20
- dotenvxBash,
21
- extractPatch,
22
- parseOcUsage,
23
- parseSupervisorArtifacts,
24
- prepareIsolatedCellEnvironment,
25
- recoverSupSpend,
26
- supervisorDriverKillGraceMs,
27
- toArmIdentity,
28
- SUP_DEFAULT_ENV_KNOBS,
29
- WORKER_PROMPT_SUFFIX,
30
- type SoloArmResult,
31
- type SupervisorArmResult,
32
- } from './arms.ts'
33
- import { containerName, materializeWorkspace } from './materialize.ts'
34
- import {
35
- gatesForArmKind,
36
- httpCapacityProbe,
37
- probeWindow,
38
- waitForCapacity,
39
- } from './capacity.ts'
40
- import { run, runOk } from './proc.ts'
41
- import {
42
- createSerializedJudge,
43
- instanceShortName,
44
- JUDGE_TIMEOUT_FLOOR_MS,
45
- type JudgeVerdict,
46
- withJudgeLock,
47
- } from './serialized-judge.ts'
48
- import { loadLedger, readFixture } from './fixtures.ts'
49
- import {
50
- buildLedgerRow,
51
- diffChangedFiles,
52
- ledgerIids,
53
- loadInstanceImages,
54
- PARITY_CASES,
55
- } from './run-experiment.mts'
56
-
57
- const tmpDirs: string[] = []
58
- async function scratch(prefix: string): Promise<string> {
59
- const dir = await mkdtemp(join(tmpdir(), prefix))
60
- tmpDirs.push(dir)
61
- return dir
62
- }
63
- afterAll(async () => {
64
- for (const dir of tmpDirs) await rm(dir, { recursive: true, force: true })
65
- })
66
-
67
- describe('vendored execution fixtures', () => {
68
- const sha256 = (name: string): string => createHash('sha256').update(readFixture(name)).digest('hex')
69
-
70
- // Frozen exactly like M1's fixture-integrity block: these bytes ARE the
71
- // parity contract (excludes drive extraction; the patches drive the verdicts).
72
- it.each([
73
- ['excludes.txt', '0e7e3c024389cb0407cf34a2062fd9623dd7124d04b1f6d36d9217e8e128e48a'],
74
- ['patches/pallets__flask-5014.solo.patch', '9c76eea47eb3631fa0159437e4f7cb12b9cd0f460a148f67d20b7ce60c7266ad'],
75
- ['patches/pydata__xarray-4687.sup.patch', 'bb2f5c814880e34fb17da1763b8c126480721cf071829799af0973119b882773'],
76
- ['instances.json', '930b5f64b1dbe5a2cf14ce7b8a6f3ccf389c31a3a768e8c383fda40e489d05ee'],
77
- ])('%s is byte-identical to the captured artifact', (name, expected) => {
78
- expect(sha256(name)).toBe(expected)
79
- })
80
-
81
- it('excludes.txt carries the 8 test-file pathspecs the bash arms used', async () => {
82
- const excludes = await loadExcludes()
83
- expect(excludes).toHaveLength(8)
84
- expect(excludes.every((e) => e.startsWith(':(exclude,glob)'))).toBe(true)
85
- expect(excludes).toContain(':(exclude,glob)**/tests/**')
86
- expect(excludes).toContain(':(exclude,glob)**/conftest.py')
87
- })
88
-
89
- it('instances.json maps every ledger instance + both parity cases to an image', async () => {
90
- const images = await loadInstanceImages()
91
- for (const row of loadLedger()) {
92
- const entry = images[row.iid]
93
- expect(entry, row.iid).toBeDefined()
94
- expect(entry.image).toMatch(/^swebench\/sweb\.eval\.x86_64\./)
95
- expect(entry.base_commit).toMatch(/^[0-9a-f]{7,40}$/)
96
- }
97
- for (const c of PARITY_CASES) expect(images[c.iid]).toBeDefined()
98
- })
99
-
100
- it('solo prompt suffix is byte-identical to solo.sh', () => {
101
- expect(WORKER_PROMPT_SUFFIX).toBe(
102
- '\n\nWork only in your current working directory. Implement the change completely and correctly — do not stub, weaken, or special-case anything, and keep the repository\'s existing tests passing. An automated check verifies your work after you finish.',
103
- )
104
- })
105
- })
106
-
107
- describe('materialize: container naming', () => {
108
- it('sanitizes iid like setup-ws.sh (tr _/ --) and is collision-resistant', () => {
109
- const a = containerName('pallets__flask-5014')
110
- expect(a).toMatch(/^ext-pallets--flask-5014-\d+-[a-f0-9-]{36}$/)
111
- expect(containerName('a_b/c')).toMatch(/^ext-a-b-c-/)
112
- expect(containerName('x')).not.toBe(containerName('x'))
113
- })
114
-
115
- it('honors a pre-aborted cell before deleting or invoking Docker', async () => {
116
- const dest = await scratch('swe-arena-materialize-abort-')
117
- const canary = join(dest, 'keep.txt')
118
- await writeFile(canary, 'keep\n')
119
- const controller = new AbortController()
120
- const reason = new Error('cell deadline')
121
- controller.abort(reason)
122
-
123
- await expect(materializeWorkspace({
124
- instanceId: 'example__repo-1',
125
- image: 'unused:latest',
126
- baseCommit: 'deadbeef',
127
- dest,
128
- signal: controller.signal,
129
- })).rejects.toBe(reason)
130
- expect(await readFile(canary, 'utf8')).toBe('keep\n')
131
- })
132
-
133
- it('fails when Docker cannot remove a successfully-created container', async () => {
134
- const root = await scratch('swe-arena-materialize-cleanup-')
135
- const binDir = join(root, 'bin')
136
- const docker = join(binDir, 'docker')
137
- const dest = join(root, 'workspace')
138
- await mkdir(binDir, { recursive: true })
139
- await writeFile(
140
- docker,
141
- [
142
- '#!/usr/bin/env bash',
143
- 'case "$1" in',
144
- ' create) exit 0 ;;',
145
- ' cp) mkdir -p "$3/.git"; exit 0 ;;',
146
- ' rm) echo "simulated container leak" >&2; exit 9 ;;',
147
- 'esac',
148
- ].join('\n'),
149
- )
150
- await chmod(docker, 0o755)
151
- const originalPath = process.env.PATH
152
- process.env.PATH = `${binDir}:${originalPath ?? ''}`
153
- try {
154
- await expect(materializeWorkspace({
155
- instanceId: 'example__repo-1',
156
- image: 'fake:latest',
157
- baseCommit: 'deadbeef',
158
- dest,
159
- })).rejects.toThrow(/cleanup-fail.*exit=9.*simulated container leak/)
160
- } finally {
161
- if (originalPath === undefined) delete process.env.PATH
162
- else process.env.PATH = originalPath
163
- }
164
- })
165
-
166
- it('preserves both copy and cleanup failures', async () => {
167
- const root = await scratch('swe-arena-materialize-double-failure-')
168
- const binDir = join(root, 'bin')
169
- const docker = join(binDir, 'docker')
170
- await mkdir(binDir, { recursive: true })
171
- await writeFile(
172
- docker,
173
- [
174
- '#!/usr/bin/env bash',
175
- 'case "$1" in',
176
- ' create) exit 0 ;;',
177
- ' cp) echo "simulated copy failure" >&2; exit 7 ;;',
178
- ' rm) echo "simulated cleanup failure" >&2; exit 9 ;;',
179
- 'esac',
180
- ].join('\n'),
181
- )
182
- await chmod(docker, 0o755)
183
- const originalPath = process.env.PATH
184
- process.env.PATH = `${binDir}:${originalPath ?? ''}`
185
- try {
186
- let error: unknown
187
- try {
188
- await materializeWorkspace({
189
- instanceId: 'example__repo-2',
190
- image: 'fake:latest',
191
- baseCommit: 'deadbeef',
192
- dest: join(root, 'workspace'),
193
- })
194
- } catch (caught) {
195
- error = caught
196
- }
197
- expect(error).toBeInstanceOf(AggregateError)
198
- const errors = (error as AggregateError).errors as Error[]
199
- expect(errors[0]?.message).toMatch(/cp-fail.*simulated copy failure/)
200
- expect(errors[1]?.message).toMatch(/cleanup-fail.*simulated cleanup failure/)
201
- } finally {
202
- if (originalPath === undefined) delete process.env.PATH
203
- else process.env.PATH = originalPath
204
- }
205
- })
206
-
207
- it('attempts idempotent cleanup after an uncertain create result', async () => {
208
- const root = await scratch('swe-arena-materialize-create-failure-')
209
- const binDir = join(root, 'bin')
210
- const docker = join(binDir, 'docker')
211
- const calls = join(root, 'docker-calls.log')
212
- await mkdir(binDir, { recursive: true })
213
- await writeFile(
214
- docker,
215
- [
216
- '#!/usr/bin/env bash',
217
- `echo "$1" >> ${JSON.stringify(calls)}`,
218
- 'case "$1" in',
219
- ' create) echo "uncertain create failure" >&2; exit 7 ;;',
220
- ' rm) echo "Error response from daemon: No such container: expected" >&2; exit 1 ;;',
221
- 'esac',
222
- ].join('\n'),
223
- )
224
- await chmod(docker, 0o755)
225
- const originalPath = process.env.PATH
226
- process.env.PATH = `${binDir}:${originalPath ?? ''}`
227
- try {
228
- await expect(materializeWorkspace({
229
- instanceId: 'example__repo-3',
230
- image: 'fake:latest',
231
- baseCommit: 'deadbeef',
232
- dest: join(root, 'workspace'),
233
- })).rejects.toThrow(/create-fail.*uncertain create failure/)
234
- expect((await readFile(calls, 'utf8')).trim().split('\n')).toEqual(['create', 'rm'])
235
- } finally {
236
- if (originalPath === undefined) delete process.env.PATH
237
- else process.env.PATH = originalPath
238
- }
239
- })
240
- })
241
-
242
- describe('arms: per-cell host isolation', () => {
243
- it('separates OpenCode state, auth injection, temp files, and Python imports', async () => {
244
- const ambient = await scratch('swe-arena-ambient-')
245
- const ambientConfigDir = join(ambient, '.config', 'opencode')
246
- const ambientDataDir = join(ambient, '.local', 'share', 'opencode')
247
- const binDir = join(ambient, 'bin')
248
- await mkdir(ambientConfigDir, { recursive: true })
249
- await mkdir(ambientDataDir, { recursive: true })
250
- await mkdir(binDir, { recursive: true })
251
- await writeFile(join(ambientConfigDir, 'opencode.json'), '{"small_model":"opencode/gpt-5-nano"}\n')
252
- await writeFile(join(ambientDataDir, 'auth.json'), '{"fake":"credential"}\n')
253
- await writeFile(
254
- join(binDir, 'dotenvx'),
255
- '#!/usr/bin/env bash\nwhile [ "$#" -gt 0 ] && [ "$1" != "--" ]; do shift; done\nshift\nexec "$@"\n',
256
- )
257
- await writeFile(
258
- join(binDir, 'opencode'),
259
- '#!/usr/bin/env bash\nif [ "$1" = "db" ] && [ "$2" = "path" ]; then printf "%s\\n" "$OPENCODE_DB"; exit 0; fi\nexit 64\n',
260
- )
261
- await Promise.all(['dotenvx', 'opencode'].map((name) => chmod(join(binDir, name), 0o755)))
262
-
263
- const ambientEnv: NodeJS.ProcessEnv = {
264
- ...process.env,
265
- HOME: ambient,
266
- PATH: `${binDir}:${process.env.PATH ?? ''}`,
267
- XDG_CONFIG_HOME: undefined,
268
- XDG_DATA_HOME: undefined,
269
- OPENCODE_CONFIG: undefined,
270
- OPENCODE_CONFIG_CONTENT: undefined,
271
- SWE_ARENA_OPENCODE_AUTH_FILE: undefined,
272
- }
273
-
274
- const cellA = await prepareIsolatedCellEnvironment(join(await scratch('swe-arena-cell-a-'), 'run'), {
275
- ...ambientEnv,
276
- PYTHONHOME: '/tmp/ambient-python-home',
277
- PYTHONPATH: '/tmp/ambient-python-path',
278
- })
279
- const cellB = await prepareIsolatedCellEnvironment(join(await scratch('swe-arena-cell-b-'), 'run'), ambientEnv)
280
-
281
- expect(cellA.env.HOME).not.toBe(ambient)
282
- expect(cellA.env.XDG_DATA_HOME).not.toBe(cellB.env.XDG_DATA_HOME)
283
- expect(cellA.env.OPENCODE_DB).toBe(cellA.opencodeDb)
284
- expect(cellA.env.PYTHONHOME).toBeUndefined()
285
- expect(cellA.env.PYTHONPATH).toBeUndefined()
286
- expect(await readFile(cellA.env.OPENCODE_CONFIG!, 'utf8')).toBe('{"small_model":"opencode/gpt-5-nano"}\n')
287
-
288
- const authCheck = await dotenvxBash(
289
- { secretsDir: ambient, envFiles: [] },
290
- `test "$OPENCODE_AUTH_CONTENT" = '{"fake":"credential"}'`,
291
- { env: cellA.env, timeoutMs: 5_000 },
292
- )
293
- expect(authCheck.code, authCheck.stderr).toBe(0)
294
-
295
- const dbPath = await runOk('opencode', ['db', 'path'], { env: cellA.env, timeoutMs: 10_000 })
296
- expect(dbPath.stdout.trim()).toBe(cellA.opencodeDb)
297
-
298
- const siteProbe = await runOk('python3', [
299
- '-c',
300
- 'import json,site,sys; print(json.dumps({"enabled":site.ENABLE_USER_SITE,"user":site.getusersitepackages(),"path":sys.path}))',
301
- ], { env: cellA.env })
302
- const site = JSON.parse(siteProbe.stdout) as { enabled: boolean; user: string; path: string[] }
303
- expect(site.enabled).toBe(true)
304
- expect(site.user).toContain(cellA.env.PYTHONUSERBASE!)
305
- await mkdir(join(site.user, 'cell_a_canary'), { recursive: true })
306
- await writeFile(join(site.user, 'cell_a_canary', '__init__.py'), 'VALUE = 1\n')
307
-
308
- const ownCellImport = await runOk('python3', [
309
- '-c',
310
- 'import cell_a_canary; assert cell_a_canary.VALUE == 1',
311
- ], { env: cellA.env })
312
- expect(ownCellImport.code).toBe(0)
313
-
314
- const crossCellImport = await run('python3', ['-c', 'import cell_a_canary'], { env: cellB.env })
315
- expect(crossCellImport.code).not.toBe(0)
316
- expect(crossCellImport.stderr).toContain('ModuleNotFoundError')
317
- })
318
-
319
- it('clears every managed directory before retrying the same cell', async () => {
320
- const runDir = join(await scratch('swe-arena-cell-retry-'), 'run')
321
- const first = await prepareIsolatedCellEnvironment(runDir, { ...process.env })
322
- const canaries = [
323
- join(first.env.HOME!, 'old-home'),
324
- join(first.env.XDG_DATA_HOME!, 'old-data'),
325
- join(first.env.XDG_CACHE_HOME!, 'old-cache'),
326
- join(first.env.XDG_STATE_HOME!, 'old-state'),
327
- join(first.env.XDG_RUNTIME_DIR!, 'old-runtime'),
328
- join(first.env.TMPDIR!, 'old-tmp'),
329
- join(first.env.PYTHONUSERBASE!, 'old-python'),
330
- ]
331
- await Promise.all(canaries.map((path) => writeFile(path, 'stale\n')))
332
-
333
- const second = await prepareIsolatedCellEnvironment(runDir, { ...process.env })
334
- expect(second.env.HOME).toBe(first.env.HOME)
335
- expect(second.opencodeDb).toBe(first.opencodeDb)
336
- for (const path of canaries) {
337
- await expect(readFile(path, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' })
338
- }
339
- })
340
- })
341
-
342
- describe('arms: patch extraction (real git, no docker)', () => {
343
- it('stages everything and excludes test files + .loops state, like solo.sh/sup4.sh', async () => {
344
- const ws = await scratch('swe-arena-git-')
345
- const git = (...argv: string[]) => runOk('git', ['-C', ws, ...argv])
346
- await runOk('git', ['init', '-q', ws])
347
- await runOk('git', ['-C', ws, 'config', 'core.hooksPath', '/dev/null'])
348
- await git('config', 'user.email', 't@t')
349
- await git('config', 'user.name', 't')
350
- await writeFile(join(ws, 'lib.py'), 'x = 1\n')
351
- await mkdir(join(ws, 'tests'), { recursive: true })
352
- await writeFile(join(ws, 'tests', 'test_lib.py'), 'assert True\n')
353
- await git('add', '-A')
354
- await git('commit', '-qm', 'base')
355
- const base = (await git('rev-parse', 'HEAD')).stdout.trim()
356
-
357
- // Arm-style edits: source fix + test edit + supervisor state + conftest.
358
- await writeFile(join(ws, 'lib.py'), 'x = 2\n')
359
- await writeFile(join(ws, 'tests', 'test_lib.py'), 'assert False\n')
360
- await writeFile(join(ws, 'conftest.py'), 'pass\n')
361
- await mkdir(join(ws, '.loops', 'supervisor'), { recursive: true })
362
- await writeFile(join(ws, '.loops', 'supervisor', 'state.json'), '{}')
363
-
364
- const patch = await extractPatch(ws, base, await loadExcludes())
365
- expect(patch).toContain('diff --git a/lib.py b/lib.py')
366
- expect(patch).toContain('+x = 2')
367
- expect(patch).not.toContain('test_lib.py')
368
- expect(patch).not.toContain('conftest.py')
369
- expect(patch).not.toContain('.loops')
370
- })
371
-
372
- it('honors a pre-aborted cell before spawning git', async () => {
373
- const controller = new AbortController()
374
- const reason = new Error('cell deadline')
375
- controller.abort(reason)
376
- await expect(extractPatch('/unused', 'deadbeef', [], controller.signal)).rejects.toBe(reason)
377
- })
378
- })
379
-
380
- describe('arms: opencode usage parse (oc_usage.py port)', () => {
381
- it('sums token parts, tracks max ctx, rounds cost to 6dp, skips noise', () => {
382
- const lines = [
383
- 'not json',
384
- '{"part":{"tokens":{"input":100,"output":20,"reasoning":5,"cache":{"write":7,"read":11},"total":150},"cost":0.001}}',
385
- '{"part":{"no_tokens_here":true}}',
386
- '{"part":{"tokens":{"input":200,"output":40,"total":400},"cost":0.0002345678}}',
387
- '{"other":"shape"}',
388
- '{broken json',
389
- ].join('\n')
390
- expect(parseOcUsage(lines)).toEqual({
391
- steps: 2,
392
- in: 300,
393
- out: 60,
394
- reasoning: 5,
395
- cache_w: 7,
396
- cache_r: 11,
397
- max_ctx: 400,
398
- oc_cost: 0.001235, // 0.0012345678 → 6dp (python round() parity)
399
- total_io: 365,
400
- })
401
- })
402
-
403
- it('empty stream = zero usage (the SOLO 0-token crash rows)', () => {
404
- expect(parseOcUsage('').total_io).toBe(0)
405
- expect(parseOcUsage('').steps).toBe(0)
406
- })
407
- })
408
-
409
- describe('arms: supervisor artifacts parse (sup4.sh port)', () => {
410
- it('reads state.json + journal spawned/settled/workers/goals', async () => {
411
- const ws = await scratch('swe-arena-sup-')
412
- const runDir = join(ws, '.loops', 'supervisor', 'sup-1-abc123')
413
- await mkdir(join(runDir, 'workers'), { recursive: true })
414
- await writeFile(
415
- join(runDir, 'state.json'),
416
- JSON.stringify({
417
- status: 'completed',
418
- verdict: 'delivered',
419
- result: { delivered: true, spentTokens: 15731, spentUsd: 0.0124 },
420
- }),
421
- )
422
- await writeFile(
423
- join(runDir, 'journal.jsonl'),
424
- [
425
- '{"kind":"spawned","label":"root"}',
426
- `{"kind":"spawned","label":"${'g'.repeat(150)}"}`,
427
- '{"kind":"spawned","label":"fix the blueprint name check"}',
428
- '{"kind":"settled","label":"w-0"}',
429
- '{"kind":"metered","spend":{"tokens":{"input":1,"output":2}}}',
430
- 'garbage line',
431
- ].join('\n'),
432
- )
433
- const a = await parseSupervisorArtifacts(ws)
434
- expect(a.status).toBe('completed')
435
- expect(a.verdict).toBe('delivered')
436
- expect(a.delivered).toBe(true)
437
- expect(a.spentTokens).toBe(15731)
438
- expect(a.spentUsd).toBe(0.0124)
439
- expect(a.spawned).toBe(3)
440
- expect(a.workers).toBe(2)
441
- expect(a.settled).toBe(1)
442
- expect(a.subtasks).toEqual(['g'.repeat(100), 'fix the blueprint name check'])
443
- expect(a.supRunDir).toBe(runDir)
444
- })
445
-
446
- it('no supervisor dir at all = null/zero summary (driver died pre-spawn)', async () => {
447
- const ws = await scratch('swe-arena-sup-empty-')
448
- const a = await parseSupervisorArtifacts(ws)
449
- expect(a.status).toBeNull()
450
- expect(a.spawned).toBe(0)
451
- expect(a.supRunDir).toBeNull()
452
- })
453
- })
454
-
455
- describe('arms: cost recovery (true_sup_spend.py + opencode sqlite join)', () => {
456
- it('brain from journal metered, workers from ndjson settled + sqlite by cwd', async () => {
457
- const dir = await scratch('swe-arena-spend-')
458
- const supRunDir = join(dir, 'sup-1-x')
459
- await mkdir(join(supRunDir, 'workers'), { recursive: true })
460
- await writeFile(
461
- join(supRunDir, 'journal.jsonl'),
462
- [
463
- '{"kind":"metered","spend":{"tokens":{"input":1000,"output":200},"usd":0.011}}',
464
- '{"kind":"metered","spend":{"tokens":{"input":500,"output":100},"usd":0.004}}',
465
- '{"kind":"spawned","label":"w-0"}',
466
- ].join('\n'),
467
- )
468
- await writeFile(
469
- join(supRunDir, 'workers', 'w-0.ndjson'),
470
- [
471
- '{"kind":"started","label":"w-0","cwd":"/tmp/loops-w-0-TESTAA"}',
472
- '{"kind":"settled","spend":{"tokens":{"input":30,"output":12}}}',
473
- ].join('\n'),
474
- )
475
- // The opencode store, reduced to the columns the join reads.
476
- const dbPath = join(dir, 'opencode.db')
477
- const db = new DatabaseSync(dbPath)
478
- db.exec(
479
- 'CREATE TABLE session (id TEXT PRIMARY KEY, directory TEXT, tokens_input INT, tokens_output INT, tokens_reasoning INT)',
480
- )
481
- db.prepare('INSERT INTO session VALUES (?,?,?,?,?)').run('s1', '/tmp/loops-w-0-TESTAA', 40000, 6000, 737)
482
- db.prepare('INSERT INTO session VALUES (?,?,?,?,?)').run('s2', '/somewhere/else', 999, 999, 0)
483
- db.close()
484
-
485
- const spend = await recoverSupSpend(supRunDir, { opencodeDb: dbPath })
486
- // Brain/driver spend is NOT re-parsed from journal metered events anymore:
487
- // the supervise runtime's spend tree reaches state.json on both result
488
- // arms (parseSupervisorArtifacts carries it). Worker recovery only here.
489
- expect(spend.workerTokJournal).toBe(42)
490
- expect(spend.workerCwds).toEqual(['/tmp/loops-w-0-TESTAA'])
491
- expect(spend.workerSessions).toBe(1)
492
- expect(spend.workerTokIn).toBe(40000)
493
- expect(spend.workerTokOut).toBe(6737)
494
- expect(spend.workerTokSqlite).toBe(46737) // flask-5014's real worker number, by construction
495
- })
496
-
497
- it('missing sqlite store = null (telemetry gap), never a silent zero', async () => {
498
- const dir = await scratch('swe-arena-spend-gap-')
499
- const supRunDir = join(dir, 'sup-1-y')
500
- await mkdir(join(supRunDir, 'workers'), { recursive: true })
501
- await writeFile(join(supRunDir, 'journal.jsonl'), '')
502
- await writeFile(join(supRunDir, 'workers', 'w-0.ndjson'), '{"kind":"started","cwd":"/tmp/loops-w-0-GONE"}')
503
- const spend = await recoverSupSpend(supRunDir, { opencodeDb: join(dir, 'no-such.db') })
504
- expect(spend.workerSessions).toBeNull()
505
- expect(spend.workerTokSqlite).toBeNull()
506
- })
507
-
508
- it('no workers spawned = hard zero (a real measurement, not a gap)', async () => {
509
- const dir = await scratch('swe-arena-spend-zero-')
510
- const supRunDir = join(dir, 'sup-1-z')
511
- await mkdir(supRunDir, { recursive: true })
512
- await writeFile(join(supRunDir, 'journal.jsonl'), '')
513
- const spend = await recoverSupSpend(supRunDir)
514
- expect(spend.workerSessions).toBe(0)
515
- expect(spend.workerTokSqlite).toBe(0)
516
- })
517
- })
518
-
519
- describe('arms: provenance + identity', () => {
520
- it('supervisor identity records the live loops commit + env knobs', async () => {
521
- // The bench repo itself stands in for the loops checkout — provenance only
522
- // needs a git HEAD to pin.
523
- const here = new URL('../..', import.meta.url).pathname
524
- const identity = await toArmIdentity({
525
- kind: 'supervisor',
526
- name: 'SUP4',
527
- workerModel: 'zai-coding-plan/glm-5.2',
528
- driverModel: 'glm-5.2',
529
- loopsRepo: here,
530
- })
531
- expect(identity.kind).toBe('supervisor')
532
- expect(identity.provenance.repo).toBe(here)
533
- expect(identity.provenance.commit).toMatch(/^[0-9a-f]{40}$/)
534
- expect(identity.env).toEqual(SUP_DEFAULT_ENV_KNOBS)
535
- expect(identity.env.LOOPS_BRAIN_RETRIES).toBe('30')
536
- expect(identity.env.DRIVER_DEADLINE_MS).toBe('2600000')
537
- })
538
-
539
- it('honors a pre-aborted cell before reading the loops commit', async () => {
540
- const controller = new AbortController()
541
- const reason = new Error('cell deadline')
542
- controller.abort(reason)
543
- await expect(armProvenance('/unused', {}, controller.signal)).rejects.toBe(reason)
544
- })
545
- })
546
-
547
- describe('arms: failed process outcomes are never scored', () => {
548
- it('accepts only a zero-exit, non-cancelled solo process', () => {
549
- expect(() => assertSoloProcessCompleted({ code: 0, timedOut: false, aborted: false })).not.toThrow()
550
- expect(() => assertSoloProcessCompleted({ code: 124, timedOut: true, aborted: false })).toThrow(/timed out/)
551
- expect(() => assertSoloProcessCompleted({ code: 130, timedOut: false, aborted: true })).toThrow(/cancelled/)
552
- expect(() => assertSoloProcessCompleted({ code: 2, timedOut: false, aborted: false })).toThrow(/exited 2/)
553
- })
554
-
555
- it('requires both a successful driver and completed supervisor state', () => {
556
- expect(() => assertSupervisorProcessCompleted(
557
- { code: 0, timedOut: false, aborted: false },
558
- 'completed',
559
- )).not.toThrow()
560
- expect(() => assertSupervisorProcessCompleted(
561
- { code: 124, timedOut: true, aborted: false },
562
- 'running',
563
- )).toThrow(/timed out.*state running/)
564
- expect(() => assertSupervisorProcessCompleted(
565
- { code: 130, timedOut: false, aborted: true },
566
- 'cancelled',
567
- )).toThrow(/cancelled.*state cancelled/)
568
- expect(() => assertSupervisorProcessCompleted(
569
- { code: 3, timedOut: false, aborted: false },
570
- 'completed',
571
- )).toThrow(/exited 3.*state completed/)
572
- for (const status of [null, 'running', 'failed', 'cancelled']) {
573
- expect(() => assertSupervisorProcessCompleted(
574
- { code: 0, timedOut: false, aborted: false },
575
- status,
576
- )).toThrow(/expected completed/)
577
- }
578
- })
579
-
580
- it('gives outer termination more time than the driver cancellation deadline', () => {
581
- expect(supervisorDriverKillGraceMs({})).toBeGreaterThan(30_000)
582
- expect(supervisorDriverKillGraceMs({ DRIVER_CANCEL_TIMEOUT_MS: '45000' })).toBeGreaterThan(45_000)
583
- expect(() => supervisorDriverKillGraceMs({ DRIVER_CANCEL_TIMEOUT_MS: '0' })).toThrow(/positive number/)
584
- })
585
- })
586
-
587
- describe('serialized-judge', () => {
588
- const fakeCommand = (script: string) => (iid: string, patchPath: string) => ({
589
- bin: 'bash',
590
- argv: ['-c', script, 'judge', iid, patchPath],
591
- cwd: tmpdir(),
592
- })
593
-
594
- async function patchFile(content: string): Promise<string> {
595
- const dir = await scratch('swe-arena-judge-')
596
- const p = join(dir, 'x.patch')
597
- await writeFile(p, content)
598
- return p
599
- }
600
-
601
- async function waitForFile(path: string, timeoutMs = 3_000): Promise<void> {
602
- const deadline = Date.now() + timeoutMs
603
- while (Date.now() < deadline) {
604
- if (await readFile(path, 'utf8').then(() => true, () => false)) return
605
- await new Promise((resolve) => setTimeout(resolve, 10))
606
- }
607
- throw new Error(`timed out waiting for ${path}`)
608
- }
609
-
610
- async function startExternalFlock(lockFile: string, readyFile: string) {
611
- const controller = new AbortController()
612
- const done = run(
613
- 'flock',
614
- [
615
- '--exclusive',
616
- lockFile,
617
- 'bash',
618
- '-c',
619
- 'touch "$1"; trap "exit 0" TERM; sleep 30',
620
- 'holder',
621
- readyFile,
622
- ],
623
- { timeoutMs: 10_000, signal: controller.signal },
624
- )
625
- await waitForFile(readyFile)
626
- return {
627
- stop: async () => {
628
- controller.abort(new Error('release external flock'))
629
- await done
630
- },
631
- }
632
- }
633
-
634
- it('enforces the 1800s ceiling floor on the real judge', () => {
635
- expect(() => createSerializedJudge({ timeoutMs: 700_000 })).toThrow(/floor/)
636
- expect(JUDGE_TIMEOUT_FLOOR_MS).toBe(1_800_000)
637
- })
638
-
639
- it('short-circuits empty patches without ever invoking the child', async () => {
640
- const dir = await scratch('swe-arena-judge-empty-')
641
- const marker = join(dir, 'invoked')
642
- const judge = createSerializedJudge({
643
- command: fakeCommand(`touch ${marker}; echo JUDGE_RESULT '{"iid":"x","resolved":true}'`),
644
- timeoutMs: 5_000,
645
- unsafeAllowShortTimeout: true,
646
- lockFile: join(dir, 'lock'),
647
- })
648
- const verdict = await judge.judge('pallets__flask-5014', await patchFile(' \n'))
649
- expect(verdict).toEqual({ iid: 'pallets__flask-5014', resolved: false, score: 0, note: 'empty-patch', attempts: 0 })
650
- await expect(readFile(marker, 'utf8')).rejects.toThrow()
651
- })
652
-
653
- it('rejects a missing patch instead of scoring it as empty', async () => {
654
- const dir = await scratch('swe-arena-judge-missing-')
655
- const marker = join(dir, 'invoked')
656
- const judge = createSerializedJudge({
657
- command: fakeCommand(`touch ${marker}; echo JUDGE_RESULT '{"iid":"x","resolved":true}'`),
658
- timeoutMs: 5_000,
659
- unsafeAllowShortTimeout: true,
660
- lockFile: join(dir, 'lock'),
661
- })
662
-
663
- await expect(judge.judge('pallets__flask-5014', join(dir, 'missing.patch'))).rejects.toMatchObject({
664
- code: 'ENOENT',
665
- })
666
- await expect(readFile(marker, 'utf8')).rejects.toThrow()
667
- })
668
-
669
- it('rejects an unreadable patch instead of scoring it as empty', async () => {
670
- const dir = await scratch('swe-arena-judge-unreadable-')
671
- const marker = join(dir, 'invoked')
672
- const patch = await patchFile('diff --git a/x b/x\n')
673
- const judge = createSerializedJudge({
674
- command: fakeCommand(`touch ${marker}; echo JUDGE_RESULT '{"iid":"x","resolved":true}'`),
675
- timeoutMs: 5_000,
676
- unsafeAllowShortTimeout: true,
677
- lockFile: join(dir, 'lock'),
678
- })
679
- await chmod(patch, 0)
680
- try {
681
- await expect(judge.judge('pallets__flask-5014', patch)).rejects.toMatchObject({ code: 'EACCES' })
682
- } finally {
683
- await chmod(patch, 0o600)
684
- }
685
- await expect(readFile(marker, 'utf8')).rejects.toThrow()
686
- })
687
-
688
- it('parses a JUDGE_RESULT line on the first attempt', async () => {
689
- const dir = await scratch('swe-arena-judge-ok-')
690
- const judge = createSerializedJudge({
691
- command: fakeCommand(
692
- `echo noise; echo 'JUDGE_RESULT {"iid":"i","resolved":true,"score":1,"secs":3,"patch_bytes":9}'`,
693
- ),
694
- timeoutMs: 5_000,
695
- unsafeAllowShortTimeout: true,
696
- lockFile: join(dir, 'lock'),
697
- })
698
- const verdict = await judge.judge('i', await patchFile('diff --git a/x b/x\n'))
699
- expect(verdict).toEqual({ iid: 'i', resolved: true, score: 1, secs: 3, patch_bytes: 9, attempts: 1 })
700
- })
701
-
702
- it('retries ONCE on unparseable output, then succeeds (observed judge flake)', async () => {
703
- const dir = await scratch('swe-arena-judge-retry-')
704
- const flag = join(dir, 'first-attempt-done')
705
- const judge = createSerializedJudge({
706
- command: fakeCommand(
707
- `if [ ! -f ${flag} ]; then touch ${flag}; echo garbled; else echo 'JUDGE_RESULT {"iid":"i","resolved":false,"score":0}'; fi`,
708
- ),
709
- timeoutMs: 5_000,
710
- unsafeAllowShortTimeout: true,
711
- lockFile: join(dir, 'lock'),
712
- })
713
- const verdict = await judge.judge('i', await patchFile('diff --git a/x b/x\n'))
714
- expect(verdict.resolved).toBe(false)
715
- expect(verdict.attempts).toBe(2)
716
- })
717
-
718
- it('two failures = resolved null (inconclusive), never a fabricated verdict', async () => {
719
- const dir = await scratch('swe-arena-judge-fail-')
720
- const judge = createSerializedJudge({
721
- command: fakeCommand('echo not-a-result'),
722
- timeoutMs: 5_000,
723
- unsafeAllowShortTimeout: true,
724
- lockFile: join(dir, 'lock'),
725
- })
726
- const verdict = await judge.judge('i', await patchFile('diff --git a/x b/x\n'))
727
- expect(verdict.resolved).toBeNull()
728
- expect(verdict.error).toBe('parse-or-timeout')
729
- expect(verdict.attempts).toBe(2)
730
- })
731
-
732
- it('a timed-out child is inconclusive, not resolved:false', async () => {
733
- const dir = await scratch('swe-arena-judge-timeout-')
734
- const judge = createSerializedJudge({
735
- command: fakeCommand('sleep 30'),
736
- timeoutMs: 300,
737
- unsafeAllowShortTimeout: true,
738
- lockFile: join(dir, 'lock'),
739
- })
740
- const verdict = await judge.judge('i', await patchFile('diff --git a/x b/x\n'))
741
- expect(verdict.resolved).toBeNull()
742
- expect(verdict.raw).toMatch(/timeout/)
743
- }, 15_000)
744
-
745
- it('rejects a success marker printed before the child times out', async () => {
746
- const dir = await scratch('swe-arena-judge-marker-timeout-')
747
- const judge = createSerializedJudge({
748
- command: fakeCommand(`echo 'JUDGE_RESULT {"iid":"i","resolved":true,"score":1}'; sleep 30`),
749
- timeoutMs: 100,
750
- unsafeAllowShortTimeout: true,
751
- lockFile: join(dir, 'lock'),
752
- })
753
- const verdict = await judge.judge('i', await patchFile('diff --git a/x b/x\n'))
754
- expect(verdict.resolved).toBeNull()
755
- expect(verdict.attempts).toBe(2)
756
- expect(verdict.raw).toMatch(/timeout/)
757
- }, 15_000)
758
-
759
- it('rejects a success marker from a child that exits nonzero', async () => {
760
- const dir = await scratch('swe-arena-judge-marker-nonzero-')
761
- const judge = createSerializedJudge({
762
- command: fakeCommand(`echo 'JUDGE_RESULT {"iid":"i","resolved":true,"score":1}'; exit 7`),
763
- timeoutMs: 5_000,
764
- unsafeAllowShortTimeout: true,
765
- lockFile: join(dir, 'lock'),
766
- })
767
- const verdict = await judge.judge('i', await patchFile('diff --git a/x b/x\n'))
768
- expect(verdict.resolved).toBeNull()
769
- expect(verdict.attempts).toBe(2)
770
- expect(verdict.raw).toMatch(/exit 7/)
771
- })
772
-
773
- it('serializes concurrent judges — the flock-race regression test', async () => {
774
- const dir = await scratch('swe-arena-judge-mutex-')
775
- const logPath = join(dir, 'order.log')
776
- const judge = createSerializedJudge({
777
- command: fakeCommand(
778
- `echo "start $$" >> ${logPath}; sleep 0.3; echo "end $$" >> ${logPath}; echo 'JUDGE_RESULT {"iid":"i","resolved":true}'`,
779
- ),
780
- timeoutMs: 5_000,
781
- unsafeAllowShortTimeout: true,
782
- lockFile: join(dir, 'lock'),
783
- })
784
- const p = await patchFile('diff --git a/x b/x\n')
785
- const [a, b] = await Promise.all([judge.judge('i', p), judge.judge('i', p)])
786
- expect(a.resolved).toBe(true)
787
- expect(b.resolved).toBe(true)
788
- const events = (await readFile(logPath, 'utf8')).trim().split('\n')
789
- // Strict alternation: start,end,start,end — overlap would interleave starts.
790
- expect(events).toHaveLength(4)
791
- expect(events[0]).toMatch(/^start /)
792
- expect(events[1]).toMatch(/^end /)
793
- expect(events[2]).toMatch(/^start /)
794
- expect(events[3]).toMatch(/^end /)
795
- }, 15_000)
796
-
797
- it('aborts an in-process waiter without abandoning or bypassing the live holder', async () => {
798
- const dir = await scratch('swe-arena-judge-chain-abort-')
799
- const lockFile = join(dir, 'lock')
800
- let releaseHolder!: () => void
801
- let announceHolder!: () => void
802
- const holderRelease = new Promise<void>((resolve) => {
803
- releaseHolder = resolve
804
- })
805
- const holderStarted = new Promise<void>((resolve) => {
806
- announceHolder = resolve
807
- })
808
- const holder = withJudgeLock(lockFile, async () => {
809
- announceHolder()
810
- await holderRelease
811
- return 'holder-finished'
812
- })
813
- await holderStarted
814
-
815
- let cancelledWaiterRan = false
816
- const controller = new AbortController()
817
- const waiter = withJudgeLock(
818
- lockFile,
819
- async () => {
820
- cancelledWaiterRan = true
821
- },
822
- { timeoutMs: 2_000, signal: controller.signal },
823
- )
824
- controller.abort(new Error('cancelled queued judge'))
825
- await expect(waiter).rejects.toThrow('cancelled queued judge')
826
- expect(cancelledWaiterRan).toBe(false)
827
- const blockedProbe = await run('flock', ['--exclusive', '--nonblock', lockFile, 'true'])
828
- expect(blockedProbe.code).toBe(1)
829
-
830
- let successorRan = false
831
- const successor = withJudgeLock(
832
- lockFile,
833
- async () => {
834
- successorRan = true
835
- },
836
- { timeoutMs: 2_000 },
837
- )
838
- await new Promise((resolve) => setTimeout(resolve, 50))
839
- expect(successorRan).toBe(false)
840
-
841
- releaseHolder()
842
- await expect(holder).resolves.toBe('holder-finished')
843
- await expect(successor).resolves.toBeUndefined()
844
- expect(successorRan).toBe(true)
845
- const releasedProbe = await run('flock', ['--exclusive', '--nonblock', lockFile, 'true'])
846
- expect(releasedProbe.code).toBe(0)
847
- })
848
-
849
- it('bounds and cancels cross-process flock acquisition before the callback runs', async () => {
850
- const dir = await scratch('swe-arena-judge-flock-wait-')
851
- const timeoutLock = join(dir, 'timeout.lock')
852
- const timeoutHolder = await startExternalFlock(timeoutLock, join(dir, 'timeout-ready'))
853
- let timedOutCallbackRan = false
854
- await expect(
855
- withJudgeLock(
856
- timeoutLock,
857
- async () => {
858
- timedOutCallbackRan = true
859
- },
860
- { timeoutMs: 40 },
861
- ),
862
- ).rejects.toThrow('lock wait exceeded 40ms')
863
- expect(timedOutCallbackRan).toBe(false)
864
- await timeoutHolder.stop()
865
-
866
- // A waiter that observed the old holder cannot delete or bypass a newer
867
- // holder: kernel ownership follows the open fd, never the lock-file bytes.
868
- let releaseSuccessor!: () => void
869
- let announceSuccessor!: () => void
870
- const successorRelease = new Promise<void>((resolve) => {
871
- releaseSuccessor = resolve
872
- })
873
- const successorStarted = new Promise<void>((resolve) => {
874
- announceSuccessor = resolve
875
- })
876
- const successor = withJudgeLock(timeoutLock, async () => {
877
- announceSuccessor()
878
- await successorRelease
879
- }, { timeoutMs: 2_000 })
880
- await successorStarted
881
- const successorProbe = await run('flock', ['--exclusive', '--nonblock', timeoutLock, 'true'])
882
- expect(successorProbe.code).toBe(1)
883
- releaseSuccessor()
884
- await successor
885
-
886
- const abortLock = join(dir, 'abort.lock')
887
- const abortHolder = await startExternalFlock(abortLock, join(dir, 'abort-ready'))
888
- let abortedCallbackRan = false
889
- const controller = new AbortController()
890
- const waiter = withJudgeLock(
891
- abortLock,
892
- async () => {
893
- abortedCallbackRan = true
894
- },
895
- { timeoutMs: 2_000, signal: controller.signal },
896
- )
897
- await new Promise((resolve) => setTimeout(resolve, 20))
898
- controller.abort(new Error('cancelled flock wait'))
899
- await expect(waiter).rejects.toThrow('cancelled flock wait')
900
- expect(abortedCallbackRan).toBe(false)
901
- await abortHolder.stop()
902
- })
903
-
904
- it('ignores stale lock-file bytes because ownership is kernel-backed', async () => {
905
- const dir = await scratch('swe-arena-judge-flock-stale-bytes-')
906
- const lockFile = join(dir, 'lock')
907
- await writeFile(lockFile, 'half-written-or-dead-owner\n')
908
- await expect(withJudgeLock(lockFile, async () => 'acquired', { timeoutMs: 5_000 })).resolves.toBe('acquired')
909
- expect(await readFile(lockFile, 'utf8')).toBe('half-written-or-dead-owner\n')
910
- })
911
-
912
- it('cancels the active judge process and never starts its retry', async () => {
913
- const dir = await scratch('swe-arena-judge-active-abort-')
914
- const attempts = join(dir, 'attempts.log')
915
- const lockFile = join(dir, 'lock')
916
- const judge = createSerializedJudge({
917
- command: fakeCommand(`echo attempt >> ${attempts}; trap 'exit 0' TERM; sleep 30; echo garbled`),
918
- timeoutMs: 5_000,
919
- lockWaitTimeoutMs: 5_000,
920
- unsafeAllowShortTimeout: true,
921
- lockFile,
922
- })
923
- const controller = new AbortController()
924
- const result = judge.judge('i', await patchFile('diff --git a/x b/x\n'), 'abort-test', controller.signal)
925
- await waitForFile(attempts)
926
- controller.abort(new Error('cancelled active judge'))
927
-
928
- await expect(result).rejects.toThrow('cancelled active judge')
929
- expect((await readFile(attempts, 'utf8')).trim().split('\n')).toEqual(['attempt'])
930
- await expect(withJudgeLock(lockFile, async () => 'released', { timeoutMs: 5_000 })).resolves.toBe('released')
931
- }, 15_000)
932
-
933
- it('bounds stale-container cleanup before starting the judge child', async () => {
934
- const dir = await scratch('swe-arena-judge-clean-timeout-')
935
- const binDir = join(dir, 'bin')
936
- const docker = join(binDir, 'docker')
937
- const judgeStarted = join(dir, 'judge-started')
938
- await mkdir(binDir, { recursive: true })
939
- await writeFile(docker, '#!/usr/bin/env bash\nsleep 30\n')
940
- await chmod(docker, 0o755)
941
- const originalPath = process.env.PATH
942
- process.env.PATH = `${binDir}:${originalPath ?? ''}`
943
- try {
944
- const judge = createSerializedJudge({
945
- command: fakeCommand(`touch ${judgeStarted}; echo 'JUDGE_RESULT {"iid":"i","resolved":true}'`),
946
- timeoutMs: 5_000,
947
- preCleanTimeoutMs: 100,
948
- unsafeAllowShortTimeout: true,
949
- lockFile: join(dir, 'lock'),
950
- })
951
- const startedAt = Date.now()
952
- await expect(judge.judge('i', await patchFile('diff --git a/x b/x\n'))).rejects.toThrow(
953
- 'docker ps pre-clean timed out after 100ms',
954
- )
955
- expect(Date.now() - startedAt).toBeLessThan(2_000)
956
- await expect(readFile(judgeStarted, 'utf8')).rejects.toThrow()
957
- } finally {
958
- if (originalPath === undefined) delete process.env.PATH
959
- else process.env.PATH = originalPath
960
- }
961
- }, 15_000)
962
-
963
- it('fails closed on nonzero stale-container cleanup before starting the judge child', async () => {
964
- const dir = await scratch('swe-arena-judge-clean-nonzero-')
965
- const binDir = join(dir, 'bin')
966
- const docker = join(binDir, 'docker')
967
- const judgeStarted = join(dir, 'judge-started')
968
- await mkdir(binDir, { recursive: true })
969
- await writeFile(docker, '#!/usr/bin/env bash\necho daemon-unavailable >&2\nexit 7\n')
970
- await chmod(docker, 0o755)
971
- const originalPath = process.env.PATH
972
- process.env.PATH = `${binDir}:${originalPath ?? ''}`
973
- try {
974
- const judge = createSerializedJudge({
975
- command: fakeCommand(`touch ${judgeStarted}; echo 'JUDGE_RESULT {"iid":"i","resolved":true}'`),
976
- timeoutMs: 5_000,
977
- preCleanTimeoutMs: 1_000,
978
- unsafeAllowShortTimeout: true,
979
- lockFile: join(dir, 'lock'),
980
- })
981
- await expect(judge.judge('i', await patchFile('diff --git a/x b/x\n'))).rejects.toThrow(
982
- 'docker ps pre-clean exited 7: daemon-unavailable',
983
- )
984
- await expect(readFile(judgeStarted, 'utf8')).rejects.toThrow()
985
- } finally {
986
- if (originalPath === undefined) delete process.env.PATH
987
- else process.env.PATH = originalPath
988
- }
989
- })
990
-
991
- it('cancels stale-container cleanup without starting the judge child', async () => {
992
- const dir = await scratch('swe-arena-judge-clean-abort-')
993
- const binDir = join(dir, 'bin')
994
- const docker = join(binDir, 'docker')
995
- const cleanStarted = join(dir, 'clean-started')
996
- const judgeStarted = join(dir, 'judge-started')
997
- await mkdir(binDir, { recursive: true })
998
- await writeFile(docker, `#!/usr/bin/env bash\ntouch ${cleanStarted}\ntrap 'exit 0' TERM\nsleep 30\n`)
999
- await chmod(docker, 0o755)
1000
- const originalPath = process.env.PATH
1001
- process.env.PATH = `${binDir}:${originalPath ?? ''}`
1002
- try {
1003
- const judge = createSerializedJudge({
1004
- command: fakeCommand(`touch ${judgeStarted}; echo 'JUDGE_RESULT {"iid":"i","resolved":true}'`),
1005
- timeoutMs: 5_000,
1006
- preCleanTimeoutMs: 5_000,
1007
- unsafeAllowShortTimeout: true,
1008
- lockFile: join(dir, 'lock'),
1009
- })
1010
- const controller = new AbortController()
1011
- const result = judge.judge('i', await patchFile('diff --git a/x b/x\n'), 'clean-abort', controller.signal)
1012
- await waitForFile(cleanStarted)
1013
- controller.abort(new Error('cancelled pre-clean'))
1014
- await expect(result).rejects.toThrow('cancelled pre-clean')
1015
- await expect(readFile(judgeStarted, 'utf8')).rejects.toThrow()
1016
- } finally {
1017
- if (originalPath === undefined) delete process.env.PATH
1018
- else process.env.PATH = originalPath
1019
- }
1020
- }, 15_000)
1021
-
1022
- it('instanceShortName mirrors judge.sh (strip through __)', () => {
1023
- expect(instanceShortName('psf__requests-1766')).toBe('requests-1766')
1024
- expect(instanceShortName('pallets__flask-5014')).toBe('flask-5014')
1025
- })
1026
- })
1027
-
1028
- describe('capacity gate', () => {
1029
- const scripted = (outcomes: boolean[]) => {
1030
- let i = 0
1031
- return async () => outcomes[Math.min(i++, outcomes.length - 1)]
1032
- }
1033
-
1034
- it('probeWindow counts k of n (probe-capacity.sh: 3/4)', async () => {
1035
- const gate = {
1036
- name: 't',
1037
- probe: scripted([true, false, true, true]),
1038
- kOfN: { k: 3, n: 4 },
1039
- waitCeilingMs: 1000,
1040
- probeIntervalMs: 0,
1041
- }
1042
- expect(await probeWindow(gate)).toEqual({ ok: 3, passed: true })
1043
- })
1044
-
1045
- it('waitForCapacity demands steadyM consecutive passing windows', async () => {
1046
- // Window outcomes: pass, fail, pass, pass → steadyM=2 opens on window 4.
1047
- const perWindow = [true, false, true, true]
1048
- let w = 0
1049
- const gate = {
1050
- name: 't',
1051
- probe: async () => perWindow[Math.min(w++, perWindow.length - 1)],
1052
- kOfN: { k: 1, n: 1 },
1053
- steadyM: 2,
1054
- waitCeilingMs: 10_000,
1055
- probeIntervalMs: 0,
1056
- retryDelayMs: 1,
1057
- }
1058
- expect(await waitForCapacity(gate)).toBe(true)
1059
- expect(w).toBe(4)
1060
- })
1061
-
1062
- it('gives up (false) at the wait ceiling instead of blocking forever', async () => {
1063
- const gate = {
1064
- name: 't',
1065
- probe: async () => false,
1066
- kOfN: { k: 1, n: 1 },
1067
- waitCeilingMs: 30,
1068
- probeIntervalMs: 0,
1069
- retryDelayMs: 5,
1070
- }
1071
- expect(await waitForCapacity(gate)).toBe(false)
1072
- })
1073
-
1074
- it('supervisor arms gate on BOTH worker and router paths; solo on worker only', () => {
1075
- const secrets = { secretsDir: '/dev/null', envFiles: ['agent-state.env'] }
1076
- expect(gatesForArmKind('solo', secrets).map((g) => g.name)).toEqual(['z.ai-coding'])
1077
- expect(gatesForArmKind('supervisor', secrets).map((g) => g.name)).toEqual(['z.ai-coding', 'router'])
1078
- })
1079
-
1080
- it('rejects a non-env-shaped key name (no value smuggling)', () => {
1081
- expect(() =>
1082
- httpCapacityProbe({
1083
- url: 'https://x',
1084
- apiKeyEnv: 'k; cat /etc/passwd',
1085
- provider: 'test',
1086
- model: 'm',
1087
- }),
1088
- ).toThrow(/apiKeyEnv/)
1089
- })
1090
- })
1091
-
1092
- describe('run-experiment: ledger assembly + resume', () => {
1093
- const solo: SoloArmResult = {
1094
- arm: 'SOLO',
1095
- iid: 'pallets__flask-5014',
1096
- oc_rc: 0,
1097
- wall_s: 116,
1098
- patch_lines: 35,
1099
- verify_rc: 0,
1100
- verify_pass: true,
1101
- usage: { steps: 1, in: 30000, out: 738, reasoning: 0, cache_w: 0, cache_r: 0, max_ctx: 30738, oc_cost: 0, total_io: 30738 },
1102
- patchPath: '/tmp/p.solo.patch',
1103
- ws: '/tmp/ws',
1104
- }
1105
- const sup: SupervisorArmResult = {
1106
- arm: 'SUP4',
1107
- iid: 'pallets__flask-5014',
1108
- driver_rc: 0,
1109
- wall_s: 261,
1110
- patch_lines: 12,
1111
- verify_rc: 0,
1112
- verify_pass: true,
1113
- sup_status: 'completed',
1114
- sup_verdict: 'delivered',
1115
- delivered: true,
1116
- spentTokens: 15731,
1117
- spentUsd: 0.0124,
1118
- spawned: 2,
1119
- workers: 1,
1120
- settled: 2,
1121
- subtasks: ['fix it'],
1122
- patchPath: '/tmp/p.sup.patch',
1123
- ws: '/tmp/ws2',
1124
- provenance: { repo: '/home/drew/code/loops', commit: 'deadbeef', envKnobs: {} },
1125
- recoveredSpend: null,
1126
- }
1127
- const yes: JudgeVerdict = { iid: 'pallets__flask-5014', resolved: true, score: 1 }
1128
- const no: JudgeVerdict = { iid: 'pallets__flask-5014', resolved: false, score: 0 }
1129
-
1130
- it('produces exactly the M1 LedgerRow field set (schema compatibility)', () => {
1131
- const row = buildLedgerRow(solo, yes, sup, no)
1132
- const fixtureKeys = Object.keys(
1133
- JSON.parse(readFixture('ledger.jsonl').split('\n').filter(Boolean)[1]) as object, // row 1 has sup_workers
1134
- ).filter((k) => k !== '_note')
1135
- expect(new Set(Object.keys(row))).toEqual(new Set(fixtureKeys))
1136
- expect(row.solo_resolved).toBe(true)
1137
- expect(row.sup_resolved).toBe(false)
1138
- expect(row.solo_tokens).toBe(30738)
1139
- expect(row.sup_workers).toBe(1)
1140
- })
1141
-
1142
- it('refuses to write an inconclusive verdict into a boolean column', () => {
1143
- const inconclusive: JudgeVerdict = { iid: 'pallets__flask-5014', resolved: null, error: 'parse-or-timeout' }
1144
- expect(() => buildLedgerRow(solo, inconclusive, sup, no)).toThrow(/inconclusive/)
1145
- })
1146
-
1147
- it('refuses an unknown sup verdict/status instead of silently typing it', () => {
1148
- expect(() => buildLedgerRow(solo, yes, { ...sup, sup_verdict: 'triumph' }, no)).toThrow(/sup_verdict/)
1149
- expect(() => buildLedgerRow(solo, yes, { ...sup, sup_status: 'exploded' }, no)).toThrow(/sup_status/)
1150
- })
1151
-
1152
- it('ledgerIids resumes on existing rows and fails loud on corrupt ones', async () => {
1153
- const dir = await scratch('swe-arena-ledger-')
1154
- const ledger = join(dir, 'ledger.jsonl')
1155
- await writeFile(ledger, '{"iid":"a"}\n{"iid":"b"}\n')
1156
- expect(await ledgerIids(ledger)).toEqual(new Set(['a', 'b']))
1157
- expect(await ledgerIids(join(dir, 'missing.jsonl'))).toEqual(new Set())
1158
- await writeFile(ledger, '{"iid":"a"}\n{broken\n')
1159
- await expect(ledgerIids(ledger)).rejects.toThrow(/corrupt ledger line/)
1160
- })
1161
-
1162
- it('diffChangedFiles reads the b/ side of the committed parity patches', () => {
1163
- expect(diffChangedFiles(readFixture('patches/pallets__flask-5014.solo.patch'))).toEqual([
1164
- 'CHANGES.rst',
1165
- 'src/flask/blueprints.py',
1166
- ])
1167
- expect(diffChangedFiles(readFixture('patches/pydata__xarray-4687.sup.patch'))).toEqual([
1168
- 'xarray/core/computation.py',
1169
- ])
1170
- })
1171
- })