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