@tangle-network/agent-bench 0.11.3 → 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 (170) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/HARNESS.md +6 -2
  3. package/README.md +1 -4
  4. package/package.json +5 -5
  5. package/scripts/run-package-tests.mjs +2 -2
  6. package/src/quant-arena/README.md +0 -144
  7. package/src/quant-arena/backtest.test.mts +0 -135
  8. package/src/quant-arena/backtest.ts +0 -218
  9. package/src/quant-arena/data.test.mts +0 -44
  10. package/src/quant-arena/data.ts +0 -141
  11. package/src/quant-arena/driver.test.mts +0 -253
  12. package/src/quant-arena/driver.ts +0 -219
  13. package/src/quant-arena/fixtures/data/PROVENANCE.md +0 -26
  14. package/src/quant-arena/fixtures/data/holdout/IDX.csv +0 -523
  15. package/src/quant-arena/fixtures/data/holdout/S01.csv +0 -523
  16. package/src/quant-arena/fixtures/data/holdout/S02.csv +0 -523
  17. package/src/quant-arena/fixtures/data/holdout/S03.csv +0 -523
  18. package/src/quant-arena/fixtures/data/holdout/S04.csv +0 -523
  19. package/src/quant-arena/fixtures/data/holdout/S05.csv +0 -523
  20. package/src/quant-arena/fixtures/data/holdout/S06.csv +0 -523
  21. package/src/quant-arena/fixtures/data/holdout/S07.csv +0 -523
  22. package/src/quant-arena/fixtures/data/holdout/S08.csv +0 -523
  23. package/src/quant-arena/fixtures/data/holdout/S09.csv +0 -523
  24. package/src/quant-arena/fixtures/data/holdout/S10.csv +0 -523
  25. package/src/quant-arena/fixtures/data/insample/IDX.csv +0 -2087
  26. package/src/quant-arena/fixtures/data/insample/S01.csv +0 -2087
  27. package/src/quant-arena/fixtures/data/insample/S02.csv +0 -2087
  28. package/src/quant-arena/fixtures/data/insample/S03.csv +0 -2087
  29. package/src/quant-arena/fixtures/data/insample/S04.csv +0 -2087
  30. package/src/quant-arena/fixtures/data/insample/S05.csv +0 -2087
  31. package/src/quant-arena/fixtures/data/insample/S06.csv +0 -2087
  32. package/src/quant-arena/fixtures/data/insample/S07.csv +0 -2087
  33. package/src/quant-arena/fixtures/data/insample/S08.csv +0 -2087
  34. package/src/quant-arena/fixtures/data/insample/S09.csv +0 -2087
  35. package/src/quant-arena/fixtures/data/insample/S10.csv +0 -2087
  36. package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +0 -16
  37. package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +0 -5
  38. package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +0 -171
  39. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +0 -119
  40. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +0 -119
  41. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +0 -105
  42. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +0 -102
  43. package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +0 -4
  44. package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +0 -2
  45. package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +0 -84
  46. package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +0 -117
  47. package/src/quant-arena/holdout-certify.mts +0 -206
  48. package/src/quant-arena/holdout-certify.test.mts +0 -82
  49. package/src/quant-arena/leak-audit.test.mts +0 -79
  50. package/src/quant-arena/leak-audit.ts +0 -95
  51. package/src/quant-arena/make-fixtures.mts +0 -161
  52. package/src/quant-arena/multiplicity.test.mts +0 -68
  53. package/src/quant-arena/multiplicity.ts +0 -87
  54. package/src/quant-arena/nautilus-certify.ts +0 -31
  55. package/src/quant-arena/oms.ts +0 -90
  56. package/src/quant-arena/profiles/quant-researcher.profile.json +0 -12
  57. package/src/quant-arena/python/pyproject.toml +0 -8
  58. package/src/quant-arena/python/uv.lock +0 -1297
  59. package/src/quant-arena/python/vbt-worker.py +0 -192
  60. package/src/quant-arena/quant-loop.mts +0 -840
  61. package/src/quant-arena/quant-loop.test.mts +0 -75
  62. package/src/quant-arena/strategies/buy-hold-index/strategy.ts +0 -11
  63. package/src/quant-arena/strategies/equal-weight/strategy.ts +0 -20
  64. package/src/quant-arena/strategies/sma-crossover/strategy.ts +0 -42
  65. package/src/quant-arena/types.ts +0 -133
  66. package/src/quant-arena/vbt-client.ts +0 -321
  67. package/src/quant-arena/vbt-parity.test.mts +0 -183
  68. package/src/quant-arena/windows.test.mts +0 -45
  69. package/src/quant-arena/windows.ts +0 -54
  70. package/src/rollout-ledger/backfill-swe-arena.mts +0 -610
  71. package/src/rollout-ledger/backfill-swe-arena.test.mts +0 -347
  72. package/src/rollout-ledger/settle-capture.mts +0 -448
  73. package/src/rollout-ledger/settle-capture.test.mts +0 -270
  74. package/src/swe-arena/activation.mts +0 -225
  75. package/src/swe-arena/activation.test.mts +0 -300
  76. package/src/swe-arena/analyze.ts +0 -211
  77. package/src/swe-arena/arms.ts +0 -862
  78. package/src/swe-arena/bootstrap-meta.mts +0 -188
  79. package/src/swe-arena/bootstrap-meta.test.mts +0 -51
  80. package/src/swe-arena/briefing.mts +0 -217
  81. package/src/swe-arena/briefing.test.mts +0 -179
  82. package/src/swe-arena/calibrate.ts +0 -217
  83. package/src/swe-arena/capabilities.mts +0 -76
  84. package/src/swe-arena/capabilities.test.mts +0 -57
  85. package/src/swe-arena/capacity.ts +0 -198
  86. package/src/swe-arena/cell-evidence.mts +0 -437
  87. package/src/swe-arena/cell-evidence.test.mts +0 -248
  88. package/src/swe-arena/diagnosis-ensemble.test.mts +0 -210
  89. package/src/swe-arena/diagnosis-ensemble.ts +0 -523
  90. package/src/swe-arena/execution.test.mts +0 -1171
  91. package/src/swe-arena/factory-command-container.ts +0 -284
  92. package/src/swe-arena/factory-judge-child.mts +0 -228
  93. package/src/swe-arena/factory.test.mts +0 -645
  94. package/src/swe-arena/fixtures/analyze.py +0 -80
  95. package/src/swe-arena/fixtures/excludes.txt +0 -8
  96. package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +0 -51
  97. package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +0 -29
  98. package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +0 -64
  99. package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +0 -48
  100. package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +0 -29
  101. package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +0 -48
  102. package/src/swe-arena/fixtures/factory/loops-28/calibration.md +0 -47
  103. package/src/swe-arena/fixtures/factory/loops-28/manifest.json +0 -30
  104. package/src/swe-arena/fixtures/factory/loops-28/spec.md +0 -50
  105. package/src/swe-arena/fixtures/gen1-salvage/README.md +0 -45
  106. package/src/swe-arena/fixtures/gen1-salvage/cand0-e6d7361.diff +0 -116
  107. package/src/swe-arena/fixtures/gen1-salvage/cand1-76a8590.diff +0 -293
  108. package/src/swe-arena/fixtures/holdout-preregister.log +0 -12
  109. package/src/swe-arena/fixtures/holdout.json +0 -44
  110. package/src/swe-arena/fixtures/instances.json +0 -146
  111. package/src/swe-arena/fixtures/ledger.jsonl +0 -12
  112. package/src/swe-arena/fixtures/patches/pallets__flask-5014.solo.patch +0 -36
  113. package/src/swe-arena/fixtures/patches/pydata__xarray-4687.sup.patch +0 -33
  114. package/src/swe-arena/fixtures/rejudge.jsonl +0 -15
  115. package/src/swe-arena/fixtures/rematch.jsonl +0 -3
  116. package/src/swe-arena/fixtures/rematch2.jsonl +0 -3
  117. package/src/swe-arena/fixtures/rematch3.jsonl +0 -3
  118. package/src/swe-arena/fixtures/run-report/README.md +0 -43
  119. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +0 -173
  120. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +0 -100
  121. package/src/swe-arena/fixtures/run-report/gen3-rollup.json +0 -551
  122. package/src/swe-arena/fixtures/run-report/gen3-rollup.md +0 -64
  123. package/src/swe-arena/fixtures/sup-journal-true.json +0 -19
  124. package/src/swe-arena/fixtures/verify/astropy__astropy-13033.sh +0 -48
  125. package/src/swe-arena/fixtures/verify/django__django-11532.sh +0 -50
  126. package/src/swe-arena/fixtures/verify/matplotlib__matplotlib-20826.sh +0 -76
  127. package/src/swe-arena/fixtures/verify/pydata__xarray-4687.sh +0 -44
  128. package/src/swe-arena/fixtures/verify/pytest-dev__pytest-6197.sh +0 -32
  129. package/src/swe-arena/fixtures/verify/sphinx-doc__sphinx-9658.sh +0 -51
  130. package/src/swe-arena/fixtures/worker-tokens.json +0 -42
  131. package/src/swe-arena/fixtures.ts +0 -237
  132. package/src/swe-arena/gepa-seat.mts +0 -886
  133. package/src/swe-arena/gepa-seat.test.mts +0 -1136
  134. package/src/swe-arena/holdout-certify.mts +0 -408
  135. package/src/swe-arena/holdout-certify.test.mts +0 -160
  136. package/src/swe-arena/implementation-ref.test.mts +0 -64
  137. package/src/swe-arena/implementation-ref.ts +0 -62
  138. package/src/swe-arena/judge-child.mts +0 -37
  139. package/src/swe-arena/ledger-orphans.mts +0 -77
  140. package/src/swe-arena/ledger-orphans.test.mts +0 -149
  141. package/src/swe-arena/manifest.mts +0 -293
  142. package/src/swe-arena/manifest.test.mts +0 -169
  143. package/src/swe-arena/materialize.ts +0 -142
  144. package/src/swe-arena/outer-loop.mts +0 -2854
  145. package/src/swe-arena/outer-loop.test.mts +0 -714
  146. package/src/swe-arena/parity.test.mts +0 -87
  147. package/src/swe-arena/premeasured-from-cells.mts +0 -296
  148. package/src/swe-arena/premeasured-from-cells.test.mts +0 -201
  149. package/src/swe-arena/proc.test.mts +0 -172
  150. package/src/swe-arena/proc.ts +0 -260
  151. package/src/swe-arena/profiles/deepseek-author.profile.json +0 -12
  152. package/src/swe-arena/profiles/default-author.profile.json +0 -12
  153. package/src/swe-arena/proposer-fanout.mts +0 -736
  154. package/src/swe-arena/proposer-fanout.test.mts +0 -660
  155. package/src/swe-arena/proposer-provenance.mts +0 -176
  156. package/src/swe-arena/proposer-provenance.test.mts +0 -106
  157. package/src/swe-arena/reconcile.ts +0 -0
  158. package/src/swe-arena/replay.mts +0 -183
  159. package/src/swe-arena/replay.test.mts +0 -300
  160. package/src/swe-arena/run-experiment.mts +0 -729
  161. package/src/swe-arena/run-report.mts +0 -75
  162. package/src/swe-arena/run-supervisor.mjs +0 -297
  163. package/src/swe-arena/run-supervisor.test.mts +0 -539
  164. package/src/swe-arena/score-split.mts +0 -140
  165. package/src/swe-arena/score-split.test.mts +0 -123
  166. package/src/swe-arena/scratch-worktree-serialization.test.mts +0 -72
  167. package/src/swe-arena/scratch-worktree.test.mts +0 -56
  168. package/src/swe-arena/scratch-worktree.ts +0 -64
  169. package/src/swe-arena/serialized-judge.ts +0 -414
  170. 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
- })