@tangle-network/agent-bench 0.3.5 → 0.3.7
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 +13 -0
- package/README.md +13 -1
- package/dist/adapters.d.ts +15 -0
- package/dist/adapters.js +43 -0
- package/dist/adapters.js.map +1 -0
- package/dist/benchmarks/_harness.d.ts +125 -0
- package/dist/benchmarks/_harness.js +33 -0
- package/dist/benchmarks/_harness.js.map +1 -0
- package/dist/benchmarks/aec-bench.d.ts +27 -0
- package/dist/benchmarks/aec-bench.js +8 -0
- package/dist/benchmarks/aec-bench.js.map +1 -0
- package/dist/benchmarks/agentbench.d.ts +16 -0
- package/dist/benchmarks/agentbench.js +10 -0
- package/dist/benchmarks/agentbench.js.map +1 -0
- package/dist/benchmarks/appworld.d.ts +37 -0
- package/dist/benchmarks/appworld.js +14 -0
- package/dist/benchmarks/appworld.js.map +1 -0
- package/dist/benchmarks/bfcl.d.ts +18 -0
- package/dist/benchmarks/bfcl.js +10 -0
- package/dist/benchmarks/bfcl.js.map +1 -0
- package/dist/benchmarks/cad-design.d.ts +45 -0
- package/dist/benchmarks/cad-design.js +7 -0
- package/dist/benchmarks/cad-design.js.map +1 -0
- package/dist/benchmarks/cadbench.d.ts +19 -0
- package/dist/benchmarks/cadbench.js +8 -0
- package/dist/benchmarks/cadbench.js.map +1 -0
- package/dist/benchmarks/cadgenbench.d.ts +22 -0
- package/dist/benchmarks/cadgenbench.js +8 -0
- package/dist/benchmarks/cadgenbench.js.map +1 -0
- package/dist/benchmarks/commit0.d.ts +31 -0
- package/dist/benchmarks/commit0.js +10 -0
- package/dist/benchmarks/commit0.js.map +1 -0
- package/dist/benchmarks/crag.d.ts +14 -0
- package/dist/benchmarks/crag.js +9 -0
- package/dist/benchmarks/crag.js.map +1 -0
- package/dist/benchmarks/dabstep.d.ts +18 -0
- package/dist/benchmarks/dabstep.js +10 -0
- package/dist/benchmarks/dabstep.js.map +1 -0
- package/dist/benchmarks/enterpriseops-gym.d.ts +38 -0
- package/dist/benchmarks/enterpriseops-gym.js +10 -0
- package/dist/benchmarks/enterpriseops-gym.js.map +1 -0
- package/dist/benchmarks/finresearchbench.d.ts +15 -0
- package/dist/benchmarks/finresearchbench.js +8 -0
- package/dist/benchmarks/finresearchbench.js.map +1 -0
- package/dist/benchmarks/finsearchcomp.d.ts +49 -0
- package/dist/benchmarks/finsearchcomp.js +7 -0
- package/dist/benchmarks/finsearchcomp.js.map +1 -0
- package/dist/benchmarks/frames.d.ts +59 -0
- package/dist/benchmarks/frames.js +13 -0
- package/dist/benchmarks/frames.js.map +1 -0
- package/dist/benchmarks/hotpotqa.d.ts +48 -0
- package/dist/benchmarks/hotpotqa.js +15 -0
- package/dist/benchmarks/hotpotqa.js.map +1 -0
- package/dist/benchmarks/humaneval.d.ts +62 -0
- package/dist/benchmarks/humaneval.js +17 -0
- package/dist/benchmarks/humaneval.js.map +1 -0
- package/dist/benchmarks/mind2web.d.ts +41 -0
- package/dist/benchmarks/mind2web.js +9 -0
- package/dist/benchmarks/mind2web.js.map +1 -0
- package/dist/benchmarks/nomiracl.d.ts +15 -0
- package/dist/benchmarks/nomiracl.js +9 -0
- package/dist/benchmarks/nomiracl.js.map +1 -0
- package/dist/benchmarks/open-rag-bench.d.ts +14 -0
- package/dist/benchmarks/open-rag-bench.js +9 -0
- package/dist/benchmarks/open-rag-bench.js.map +1 -0
- package/dist/benchmarks/programbench.d.ts +38 -0
- package/dist/benchmarks/programbench.js +10 -0
- package/dist/benchmarks/programbench.js.map +1 -0
- package/dist/benchmarks/rag-shared.d.ts +42 -0
- package/dist/benchmarks/rag-shared.js +39 -0
- package/dist/benchmarks/rag-shared.js.map +1 -0
- package/dist/benchmarks/ragbench.d.ts +16 -0
- package/dist/benchmarks/ragbench.js +9 -0
- package/dist/benchmarks/ragbench.js.map +1 -0
- package/dist/benchmarks/simpleqa.d.ts +64 -0
- package/dist/benchmarks/simpleqa.js +11 -0
- package/dist/benchmarks/simpleqa.js.map +1 -0
- package/dist/benchmarks/swe-bench.d.ts +56 -0
- package/dist/benchmarks/swe-bench.js +14 -0
- package/dist/benchmarks/swe-bench.js.map +1 -0
- package/dist/benchmarks/t2-ragbench.d.ts +14 -0
- package/dist/benchmarks/t2-ragbench.js +9 -0
- package/dist/benchmarks/t2-ragbench.js.map +1 -0
- package/dist/benchmarks/tau-bench-shared.d.ts +26 -0
- package/dist/benchmarks/tau-bench-shared.js +10 -0
- package/dist/benchmarks/tau-bench-shared.js.map +1 -0
- package/dist/benchmarks/tau2-bench.d.ts +7 -0
- package/dist/benchmarks/tau2-bench.js +11 -0
- package/dist/benchmarks/tau2-bench.js.map +1 -0
- package/dist/benchmarks/tau3-banking.d.ts +15 -0
- package/dist/benchmarks/tau3-banking.js +9 -0
- package/dist/benchmarks/tau3-banking.js.map +1 -0
- package/dist/benchmarks/terminal-bench.d.ts +24 -0
- package/dist/benchmarks/terminal-bench.js +8 -0
- package/dist/benchmarks/terminal-bench.js.map +1 -0
- package/dist/benchmarks/toollm.d.ts +16 -0
- package/dist/benchmarks/toollm.js +10 -0
- package/dist/benchmarks/toollm.js.map +1 -0
- package/dist/benchmarks/trata-hedge.d.ts +32 -0
- package/dist/benchmarks/trata-hedge.js +7 -0
- package/dist/benchmarks/trata-hedge.js.map +1 -0
- package/dist/benchmarks/types.d.ts +107 -0
- package/dist/benchmarks/types.js +1 -0
- package/dist/benchmarks/types.js.map +1 -0
- package/dist/benchmarks/webarena-verified.d.ts +16 -0
- package/dist/benchmarks/webarena-verified.js +10 -0
- package/dist/benchmarks/webarena-verified.js.map +1 -0
- package/dist/chunk-2PVVP7GN.js +197 -0
- package/dist/chunk-2PVVP7GN.js.map +1 -0
- package/dist/chunk-2XU6OGEN.js +170 -0
- package/dist/chunk-2XU6OGEN.js.map +1 -0
- package/dist/chunk-53UPUNBZ.js +325 -0
- package/dist/chunk-53UPUNBZ.js.map +1 -0
- package/dist/chunk-5H5XV76F.js +240 -0
- package/dist/chunk-5H5XV76F.js.map +1 -0
- package/dist/chunk-7WSD27QQ.js +118 -0
- package/dist/chunk-7WSD27QQ.js.map +1 -0
- package/dist/chunk-C7T7WEK2.js +103 -0
- package/dist/chunk-C7T7WEK2.js.map +1 -0
- package/dist/chunk-CKUVRZ2T.js +251 -0
- package/dist/chunk-CKUVRZ2T.js.map +1 -0
- package/dist/chunk-HBSWHQNJ.js +30 -0
- package/dist/chunk-HBSWHQNJ.js.map +1 -0
- package/dist/chunk-HHXFIHXC.js +116 -0
- package/dist/chunk-HHXFIHXC.js.map +1 -0
- package/dist/chunk-IFAV6KEM.js +276 -0
- package/dist/chunk-IFAV6KEM.js.map +1 -0
- package/dist/chunk-INNOYXCP.js +387 -0
- package/dist/chunk-INNOYXCP.js.map +1 -0
- package/dist/chunk-J3KDJNX2.js +182 -0
- package/dist/chunk-J3KDJNX2.js.map +1 -0
- package/dist/chunk-JRWWGMK7.js +148 -0
- package/dist/chunk-JRWWGMK7.js.map +1 -0
- package/dist/chunk-JTHWEDEW.js +32 -0
- package/dist/chunk-JTHWEDEW.js.map +1 -0
- package/dist/chunk-KDIKRJGB.js +120 -0
- package/dist/chunk-KDIKRJGB.js.map +1 -0
- package/dist/chunk-LRRD7NAG.js +301 -0
- package/dist/chunk-LRRD7NAG.js.map +1 -0
- package/dist/chunk-ODT47UAY.js +221 -0
- package/dist/chunk-ODT47UAY.js.map +1 -0
- package/dist/chunk-PA2ZKHJC.js +230 -0
- package/dist/chunk-PA2ZKHJC.js.map +1 -0
- package/dist/chunk-PUIRNYI7.js +189 -0
- package/dist/chunk-PUIRNYI7.js.map +1 -0
- package/dist/chunk-PWQVGAJB.js +144 -0
- package/dist/chunk-PWQVGAJB.js.map +1 -0
- package/dist/chunk-R36V2VP7.js +169 -0
- package/dist/chunk-R36V2VP7.js.map +1 -0
- package/dist/chunk-R67DFVLO.js +142 -0
- package/dist/chunk-R67DFVLO.js.map +1 -0
- package/dist/chunk-SEVJPLZC.js +260 -0
- package/dist/chunk-SEVJPLZC.js.map +1 -0
- package/dist/chunk-SYDW647C.js +318 -0
- package/dist/chunk-SYDW647C.js.map +1 -0
- package/dist/chunk-TBKU5XQI.js +228 -0
- package/dist/chunk-TBKU5XQI.js.map +1 -0
- package/dist/chunk-TSWPNOYM.js +147 -0
- package/dist/chunk-TSWPNOYM.js.map +1 -0
- package/dist/chunk-UAIOHCUK.js +27 -0
- package/dist/chunk-UAIOHCUK.js.map +1 -0
- package/dist/chunk-UPAMRDX4.js +233 -0
- package/dist/chunk-UPAMRDX4.js.map +1 -0
- package/dist/chunk-VQRS7VUC.js +342 -0
- package/dist/chunk-VQRS7VUC.js.map +1 -0
- package/dist/chunk-X3BTXCJ4.js +262 -0
- package/dist/chunk-X3BTXCJ4.js.map +1 -0
- package/dist/chunk-X5YKXC6V.js +211 -0
- package/dist/chunk-X5YKXC6V.js.map +1 -0
- package/dist/chunk-Y6O2OCUO.js +130 -0
- package/dist/chunk-Y6O2OCUO.js.map +1 -0
- package/dist/chunk-YCGY7UIZ.js +208 -0
- package/dist/chunk-YCGY7UIZ.js.map +1 -0
- package/dist/chunk-Z7ML6L77.js +162 -0
- package/dist/chunk-Z7ML6L77.js.map +1 -0
- package/dist/chunk-ZEWMTR5M.js +136 -0
- package/dist/chunk-ZEWMTR5M.js.map +1 -0
- package/dist/index.d.ts +355 -0
- package/dist/index.js +1908 -0
- package/dist/index.js.map +1 -0
- package/package.json +26 -9
- package/scripts/run-package-tests.mjs +30 -8
- package/scripts/verify-packed-consumer.mjs +12 -1
- package/scripts/verify-pier-agent.mts +1 -0
- 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/stream-observe.py +45 -0
- package/src/stream-observe.tpl.html +247 -0
- package/src/supervisor-arena.mts +816 -0
- package/src/swe-arena/analyze.ts +211 -0
- package/src/swe-arena/arms.ts +788 -0
- package/src/swe-arena/bootstrap-meta.mts +188 -0
- package/src/swe-arena/bootstrap-meta.test.mts +51 -0
- package/src/swe-arena/calibrate.ts +116 -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 +405 -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/fixtures/analyze.py +80 -0
- package/src/swe-arena/fixtures/excludes.txt +8 -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/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 +104 -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/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 +2145 -0
- package/src/swe-arena/outer-loop.test.mts +696 -0
- package/src/swe-arena/parity.test.mts +87 -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 +489 -0
- package/src/swe-arena/proposer-fanout.test.mts +372 -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 +361 -0
- package/src/swe-arena/run-supervisor.mjs +297 -0
- package/src/swe-arena/run-supervisor.test.mts +498 -0
- package/src/swe-arena/serialized-judge.ts +414 -0
- package/src/swe-arena/types.ts +166 -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
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the round-4 outer loop's pure protocol logic: the declared
|
|
3
|
+
* change-space enforcement, porcelain path parsing, the keep-if-better verdict
|
|
4
|
+
* (protocol_v2), the staircase row schema, and the frozen-arm assertion.
|
|
5
|
+
* Pure — no arms, no docker, no tokens.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from 'node:fs'
|
|
9
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
10
|
+
import { tmpdir } from 'node:os'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
import { describe, expect, it } from 'vitest'
|
|
13
|
+
import { runOk } from './proc.ts'
|
|
14
|
+
import {
|
|
15
|
+
addEvalWorktree,
|
|
16
|
+
DISPATCH_CLEANUP_GRACE_MS,
|
|
17
|
+
DEFAULT_GATE_WAIT_CEILING_MS,
|
|
18
|
+
FIXTURES_VERIFY_DIR,
|
|
19
|
+
FROZEN_ARM,
|
|
20
|
+
INSTANCE_LOCK_FILENAME,
|
|
21
|
+
LOOPS_CHANGE_SPACE,
|
|
22
|
+
RAW_TRACE_DIAGNOSIS_PATH,
|
|
23
|
+
STAIRCASE_SCHEMA,
|
|
24
|
+
SUPERVISOR_GATE_COUNT,
|
|
25
|
+
acquireInstanceLock,
|
|
26
|
+
assertFrozenArm,
|
|
27
|
+
assertLaunchEnv,
|
|
28
|
+
baselineDriftWarnings,
|
|
29
|
+
campaignDispatchCeilingMs,
|
|
30
|
+
changeSpaceInstruction,
|
|
31
|
+
changeSpaceViolations,
|
|
32
|
+
decideVerdict,
|
|
33
|
+
defaultRound4Config,
|
|
34
|
+
instanceVerdictsFromCells,
|
|
35
|
+
isPidAlive,
|
|
36
|
+
loopsCandidateVerifier,
|
|
37
|
+
normalizeRepoPath,
|
|
38
|
+
parseStaircaseRow,
|
|
39
|
+
porcelainChangedPaths,
|
|
40
|
+
purgeIgnoredArtifacts,
|
|
41
|
+
removeEvalWorktree,
|
|
42
|
+
replicateCoverageComplete,
|
|
43
|
+
resolvedInstanceCount,
|
|
44
|
+
round4BuildPrompt,
|
|
45
|
+
runWithPostGateClock,
|
|
46
|
+
type EvidenceCell,
|
|
47
|
+
type ReplicateRun,
|
|
48
|
+
type StaircaseRow,
|
|
49
|
+
} from './outer-loop.mts'
|
|
50
|
+
|
|
51
|
+
describe('changeSpaceViolations', () => {
|
|
52
|
+
it('accepts the declared change-space, including nested extension paths', () => {
|
|
53
|
+
expect(
|
|
54
|
+
changeSpaceViolations([
|
|
55
|
+
'extensions/pi/loops.ts',
|
|
56
|
+
'extensions/pi/prompts/worker-coding-system.md',
|
|
57
|
+
'extensions/pi/deep/new-module.ts',
|
|
58
|
+
'src/worker-evidence.ts',
|
|
59
|
+
'src/best-effort.ts',
|
|
60
|
+
'src/worker-clone.ts',
|
|
61
|
+
RAW_TRACE_DIAGNOSIS_PATH,
|
|
62
|
+
'./src/worker-clone.ts',
|
|
63
|
+
]),
|
|
64
|
+
).toEqual([])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('rejects everything outside the declared space', () => {
|
|
68
|
+
expect(
|
|
69
|
+
changeSpaceViolations([
|
|
70
|
+
'src/runner.ts',
|
|
71
|
+
'src/strategy-loop.ts',
|
|
72
|
+
'package.json',
|
|
73
|
+
'extensions/other/loops.ts',
|
|
74
|
+
'tests/top-model.test.ts',
|
|
75
|
+
'src/worker-evidence.ts.bak',
|
|
76
|
+
]),
|
|
77
|
+
).toEqual([
|
|
78
|
+
'src/runner.ts',
|
|
79
|
+
'src/strategy-loop.ts',
|
|
80
|
+
'package.json',
|
|
81
|
+
'extensions/other/loops.ts',
|
|
82
|
+
'tests/top-model.test.ts',
|
|
83
|
+
'src/worker-evidence.ts.bak',
|
|
84
|
+
])
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('is not fooled by prefix-sharing directories (extensions/pi2 is out)', () => {
|
|
88
|
+
expect(changeSpaceViolations(['extensions/pi2/loops.ts'])).toEqual(['extensions/pi2/loops.ts'])
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('fails closed on traversal, absolute, and empty paths', () => {
|
|
92
|
+
expect(changeSpaceViolations(['extensions/pi/../../package.json'])).toEqual([
|
|
93
|
+
'extensions/pi/../../package.json',
|
|
94
|
+
])
|
|
95
|
+
expect(changeSpaceViolations(['/etc/passwd'])).toEqual(['/etc/passwd'])
|
|
96
|
+
expect(changeSpaceViolations([''])).toEqual([''])
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('normalizes quoted and backslashed paths before matching', () => {
|
|
100
|
+
expect(normalizeRepoPath('"extensions/pi/a b.ts"')).toBe('extensions/pi/a b.ts')
|
|
101
|
+
expect(normalizeRepoPath('extensions\\pi\\loops.ts')).toBe('extensions/pi/loops.ts')
|
|
102
|
+
expect(normalizeRepoPath('../outside.ts')).toBeNull()
|
|
103
|
+
expect(changeSpaceViolations(['"extensions/pi/a b.ts"'])).toEqual([])
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('porcelainChangedPaths', () => {
|
|
108
|
+
it('parses modified, untracked, and rename entries (both rename sides)', () => {
|
|
109
|
+
const stdout = [
|
|
110
|
+
' M extensions/pi/loops.ts',
|
|
111
|
+
'?? .improve/raw-trace-diagnosis.md',
|
|
112
|
+
'R src/worker-clone.ts -> src/worker-clone-2.ts',
|
|
113
|
+
'A src/best-effort.ts',
|
|
114
|
+
'',
|
|
115
|
+
].join('\n')
|
|
116
|
+
expect(porcelainChangedPaths(stdout)).toEqual([
|
|
117
|
+
'extensions/pi/loops.ts',
|
|
118
|
+
'.improve/raw-trace-diagnosis.md',
|
|
119
|
+
'src/worker-clone.ts',
|
|
120
|
+
'src/worker-clone-2.ts',
|
|
121
|
+
'src/best-effort.ts',
|
|
122
|
+
])
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('a rename OUT of the change-space is caught end-to-end', () => {
|
|
126
|
+
const paths = porcelainChangedPaths('R src/worker-clone.ts -> src/worker-clone-moved.ts\n')
|
|
127
|
+
expect(changeSpaceViolations(paths)).toEqual(['src/worker-clone-moved.ts'])
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('decideVerdict (protocol_v2 keep-if-better)', () => {
|
|
132
|
+
const base = {
|
|
133
|
+
violations: [] as string[],
|
|
134
|
+
coverageComplete: true,
|
|
135
|
+
resolvedCount: 2,
|
|
136
|
+
parentResolvedCount: 1,
|
|
137
|
+
costRatio: 1.0,
|
|
138
|
+
costGuardRatio: 1.2,
|
|
139
|
+
}
|
|
140
|
+
it('accepts a gaining, in-space, in-budget candidate', () => {
|
|
141
|
+
expect(decideVerdict(base)).toBe('accepted')
|
|
142
|
+
})
|
|
143
|
+
it('rejects out-of-space before anything else', () => {
|
|
144
|
+
expect(decideVerdict({ ...base, violations: ['package.json'] })).toBe('rejected-out-of-space')
|
|
145
|
+
})
|
|
146
|
+
it('rejects incomplete coverage (an errored cell can never promote)', () => {
|
|
147
|
+
expect(decideVerdict({ ...base, coverageComplete: false })).toBe('rejected-incomplete')
|
|
148
|
+
})
|
|
149
|
+
it('requires a STRICT improvement-set gain (tie = reject)', () => {
|
|
150
|
+
expect(decideVerdict({ ...base, resolvedCount: 1 })).toBe('rejected-no-gain')
|
|
151
|
+
expect(decideVerdict({ ...base, resolvedCount: 0 })).toBe('rejected-no-gain')
|
|
152
|
+
})
|
|
153
|
+
it('rejects on the +20% cost guard and on unprovable cost', () => {
|
|
154
|
+
expect(decideVerdict({ ...base, costRatio: 1.21 })).toBe('rejected-cost')
|
|
155
|
+
expect(decideVerdict({ ...base, costRatio: null })).toBe('rejected-cost')
|
|
156
|
+
expect(decideVerdict({ ...base, costRatio: 1.2 })).toBe('accepted')
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('staircase row schema', () => {
|
|
161
|
+
const row: StaircaseRow = {
|
|
162
|
+
schema: STAIRCASE_SCHEMA,
|
|
163
|
+
round: 4,
|
|
164
|
+
generation: 0,
|
|
165
|
+
runId: 'r4-abc123',
|
|
166
|
+
at: '2026-07-15T00:00:00.000Z',
|
|
167
|
+
candidate: 'sha256:cand',
|
|
168
|
+
candidateCommit: 'deadbeef00',
|
|
169
|
+
parent: 'sha256:parent',
|
|
170
|
+
parentResolvedCount: 1,
|
|
171
|
+
label: 'placement-aware settle',
|
|
172
|
+
rationale: 'diagnosis: fix placement mismatch (3/3 analysts)',
|
|
173
|
+
changedFiles: ['extensions/pi/loops.ts'],
|
|
174
|
+
changeSpaceViolations: [],
|
|
175
|
+
perInstance: [
|
|
176
|
+
{
|
|
177
|
+
iid: 'django__django-11532',
|
|
178
|
+
rep: 0,
|
|
179
|
+
resolved: true,
|
|
180
|
+
verify_pass: true,
|
|
181
|
+
patch_lines: 47,
|
|
182
|
+
wall_s: 900,
|
|
183
|
+
spentTokens: 54623,
|
|
184
|
+
recoveredTokens: 61000,
|
|
185
|
+
judgeAttempts: 1,
|
|
186
|
+
costUsd: 0.12,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
resolvedCount: 2,
|
|
190
|
+
coverageComplete: true,
|
|
191
|
+
wallS: 2700,
|
|
192
|
+
baselineWallS: 2500,
|
|
193
|
+
costRatio: 1.08,
|
|
194
|
+
costGuardRatio: 1.2,
|
|
195
|
+
internallyPromoted: true,
|
|
196
|
+
verdict: 'accepted',
|
|
197
|
+
holdout: 'operator-approval-required',
|
|
198
|
+
armProvenance: { repo: '/tmp/eval-wt', commit: 'deadbeef00' },
|
|
199
|
+
diffPath: '/tmp/out/candidates/deadbeef00.patch',
|
|
200
|
+
diffSha256: 'sha256:aaaa',
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
it('round-trips through JSONL', () => {
|
|
204
|
+
const parsed = parseStaircaseRow(JSON.stringify(row))
|
|
205
|
+
expect(parsed).toEqual(row)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('rejects schema drift, bad verdicts, and missing fields', () => {
|
|
209
|
+
expect(() => parseStaircaseRow(JSON.stringify({ ...row, schema: 'v0' }))).toThrow(/unknown schema/)
|
|
210
|
+
expect(() => parseStaircaseRow(JSON.stringify({ ...row, verdict: 'kept' }))).toThrow(/unknown verdict/)
|
|
211
|
+
expect(() => parseStaircaseRow(JSON.stringify({ ...row, resolvedCount: '2' }))).toThrow(/must be a number/)
|
|
212
|
+
expect(() => parseStaircaseRow(JSON.stringify({ ...row, runId: '' }))).toThrow(/runId/)
|
|
213
|
+
expect(() => parseStaircaseRow(JSON.stringify({ ...row, perInstance: 'x' }))).toThrow(/perInstance/)
|
|
214
|
+
expect(() => parseStaircaseRow(JSON.stringify({ ...row, costRatio: 'high' }))).toThrow(/costRatio/)
|
|
215
|
+
expect(() => parseStaircaseRow(JSON.stringify({ ...row, internallyPromoted: 'yes' }))).toThrow(/booleans/)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('accepts a null-cost rejected dot (telemetry gap is data, not a zero)', () => {
|
|
219
|
+
const dot = { ...row, costRatio: null, verdict: 'rejected-cost' as const, internallyPromoted: false }
|
|
220
|
+
expect(parseStaircaseRow(JSON.stringify(dot)).costRatio).toBeNull()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('accepts a gen-3 prefilter kill dot with its killReason', () => {
|
|
224
|
+
const dot = {
|
|
225
|
+
...row,
|
|
226
|
+
candidate: 'prefilter-kill:aaaabbbbcccc',
|
|
227
|
+
candidateCommit: null,
|
|
228
|
+
verdict: 'rejected-prefilter' as const,
|
|
229
|
+
internallyPromoted: false,
|
|
230
|
+
coverageComplete: false,
|
|
231
|
+
resolvedCount: 0,
|
|
232
|
+
perInstance: [],
|
|
233
|
+
costRatio: null,
|
|
234
|
+
killReason: 'smoke: smoke astropy__astropy-13033: resolved=false — below the mechanism bar',
|
|
235
|
+
armProvenance: null,
|
|
236
|
+
}
|
|
237
|
+
const parsed = parseStaircaseRow(JSON.stringify(dot))
|
|
238
|
+
expect(parsed.verdict).toBe('rejected-prefilter')
|
|
239
|
+
expect(parsed.killReason).toContain('below the mechanism bar')
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
describe('frozen arm + default config', () => {
|
|
244
|
+
it('passes on the round-3 frozen arm and the default config', () => {
|
|
245
|
+
expect(() => assertFrozenArm(FROZEN_ARM)).not.toThrow()
|
|
246
|
+
expect(() => assertFrozenArm(defaultRound4Config().arm)).not.toThrow()
|
|
247
|
+
})
|
|
248
|
+
it('throws on any immutable-arm drift (protocol_v2)', () => {
|
|
249
|
+
expect(() => assertFrozenArm({ ...FROZEN_ARM, workerModel: 'gpt-5.5' })).toThrow(/immutable/)
|
|
250
|
+
expect(() => assertFrozenArm({ ...FROZEN_ARM, maxUsd: 16 })).toThrow(/maxUsd/)
|
|
251
|
+
expect(() => assertFrozenArm({ ...FROZEN_ARM, budget: 80 })).toThrow(/budget/)
|
|
252
|
+
})
|
|
253
|
+
it('default config: improvement set and holdout are the pre-registered, disjoint sets', () => {
|
|
254
|
+
const config = defaultRound4Config()
|
|
255
|
+
expect(config.instances).toEqual([
|
|
256
|
+
'astropy__astropy-13033',
|
|
257
|
+
'django__django-11532',
|
|
258
|
+
'matplotlib__matplotlib-20826',
|
|
259
|
+
])
|
|
260
|
+
expect(config.holdoutInstances).toHaveLength(6)
|
|
261
|
+
expect(config.instances.filter((i) => config.holdoutInstances.includes(i))).toEqual([])
|
|
262
|
+
expect(config.roundsDir).toBe('/home/drew/code/supervisor-lab/.evolve/rounds')
|
|
263
|
+
expect(config.analystModels.every((m) => m === 'glm-5.2')).toBe(true)
|
|
264
|
+
})
|
|
265
|
+
it('default config: verify scripts come from the COMMITTED fixtures dir and reps=2', () => {
|
|
266
|
+
const config = defaultRound4Config()
|
|
267
|
+
// The scratchpad copy died with a host reboot; the committed dir is the durable home.
|
|
268
|
+
expect(config.verifyDir).toBe(FIXTURES_VERIFY_DIR)
|
|
269
|
+
expect(config.verifyDir).toContain('fixtures/verify')
|
|
270
|
+
// Single-rep scoring flips instance outcomes run-to-run — round 4 runs 2.
|
|
271
|
+
expect(config.repsPerInstance).toBe(2)
|
|
272
|
+
})
|
|
273
|
+
it('default config: the premeasured baseline artifact path is required-with-default', () => {
|
|
274
|
+
const config = defaultRound4Config()
|
|
275
|
+
expect(config.premeasuredBaselinePath).toContain('/r4/premeasured-baseline.json')
|
|
276
|
+
})
|
|
277
|
+
it('default config: author-shot timeout doubled after 3 gen-1 timeouts; outDir name is overridable', () => {
|
|
278
|
+
const config = defaultRound4Config()
|
|
279
|
+
// 20-min shots died 3× under degraded capacity ("author shot timed out").
|
|
280
|
+
expect(config.proposerTimeoutMs).toBe(2_400_000)
|
|
281
|
+
expect(defaultRound4Config(undefined, { outDirName: 'r4-gen2' }).outDir.endsWith('/r4-gen2')).toBe(true)
|
|
282
|
+
expect(config.outDir.endsWith('/r4')).toBe(true)
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
describe('dispatch clocks (gate holds are never billed to the cell)', () => {
|
|
287
|
+
it('starts neither capacity checks nor work for an already-aborted caller', async () => {
|
|
288
|
+
const controller = new AbortController()
|
|
289
|
+
controller.abort(new Error('cancelled before cell'))
|
|
290
|
+
let gates = 0
|
|
291
|
+
let work = 0
|
|
292
|
+
await expect(runWithPostGateClock({
|
|
293
|
+
awaitGates: async () => { gates += 1 },
|
|
294
|
+
work: async () => { work += 1; return 'x' },
|
|
295
|
+
timeoutMs: 1_000,
|
|
296
|
+
signal: controller.signal,
|
|
297
|
+
})).rejects.toThrow('cancelled before cell')
|
|
298
|
+
expect(gates).toBe(0)
|
|
299
|
+
expect(work).toBe(0)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('passes parent cancellation through work and waits for its cleanup', async () => {
|
|
303
|
+
const controller = new AbortController()
|
|
304
|
+
let cleanupFinished = false
|
|
305
|
+
let markStarted!: () => void
|
|
306
|
+
const started = new Promise<void>((resolve) => { markStarted = resolve })
|
|
307
|
+
const running = runWithPostGateClock({
|
|
308
|
+
awaitGates: async (signal) => signal?.throwIfAborted(),
|
|
309
|
+
work: (signal) => new Promise<string>((resolve) => {
|
|
310
|
+
markStarted()
|
|
311
|
+
signal.addEventListener('abort', () => {
|
|
312
|
+
setTimeout(() => {
|
|
313
|
+
cleanupFinished = true
|
|
314
|
+
resolve('settled')
|
|
315
|
+
}, 25)
|
|
316
|
+
}, { once: true })
|
|
317
|
+
}),
|
|
318
|
+
timeoutMs: 1_000,
|
|
319
|
+
signal: controller.signal,
|
|
320
|
+
})
|
|
321
|
+
await started
|
|
322
|
+
controller.abort(new Error('operator interrupted'))
|
|
323
|
+
await expect(running).rejects.toThrow('operator interrupted')
|
|
324
|
+
expect(cleanupFinished).toBe(true)
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('campaignDispatchCeilingMs includes gate holds, both judge attempts, and cleanup', () => {
|
|
328
|
+
expect(campaignDispatchCeilingMs({ dispatchTimeoutMs: 7_200_000 })).toBe(
|
|
329
|
+
7_200_000 + SUPERVISOR_GATE_COUNT * DEFAULT_GATE_WAIT_CEILING_MS + 2 * 1_800_000 + DISPATCH_CLEANUP_GRACE_MS,
|
|
330
|
+
)
|
|
331
|
+
expect(campaignDispatchCeilingMs({
|
|
332
|
+
dispatchTimeoutMs: 1_000,
|
|
333
|
+
gateWaitCeilingMs: 500,
|
|
334
|
+
judgeTimeoutMs: 2_000,
|
|
335
|
+
})).toBe(
|
|
336
|
+
1_000 + 2 * 500 + 2 * 2_000 + DISPATCH_CLEANUP_GRACE_MS,
|
|
337
|
+
)
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('a gate hold LONGER than the work clock does not abort the cell (the pre-crash bug)', async () => {
|
|
341
|
+
// Pre-crash failure shape: 58-min capacity hold billed to the 7200s clock.
|
|
342
|
+
// Here: gate hold 120ms > work clock 60ms; the work itself takes 10ms.
|
|
343
|
+
const result = await runWithPostGateClock({
|
|
344
|
+
awaitGates: () => new Promise<void>((r) => setTimeout(r, 120)),
|
|
345
|
+
work: () => new Promise<string>((r) => setTimeout(() => r('done'), 10)),
|
|
346
|
+
timeoutMs: 60,
|
|
347
|
+
})
|
|
348
|
+
expect(result).toBe('done')
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('work exceeding the post-gate clock still fails loud', async () => {
|
|
352
|
+
await expect(
|
|
353
|
+
runWithPostGateClock({
|
|
354
|
+
awaitGates: () => Promise.resolve(),
|
|
355
|
+
work: () => new Promise<string>((r) => setTimeout(() => r('late'), 200)),
|
|
356
|
+
timeoutMs: 30,
|
|
357
|
+
label: 'R4 deadbeef00 astropy__astropy-13033 r0',
|
|
358
|
+
}),
|
|
359
|
+
).rejects.toThrow(/post-gate dispatch exceeded 30ms .*astropy__astropy-13033/)
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('waits for abort cleanup before reporting a post-gate timeout', async () => {
|
|
363
|
+
let cleanupFinished = false
|
|
364
|
+
const started = Date.now()
|
|
365
|
+
await expect(
|
|
366
|
+
runWithPostGateClock({
|
|
367
|
+
awaitGates: () => Promise.resolve(),
|
|
368
|
+
work: (signal) => new Promise<string>((resolve) => {
|
|
369
|
+
signal.addEventListener('abort', () => {
|
|
370
|
+
setTimeout(() => {
|
|
371
|
+
cleanupFinished = true
|
|
372
|
+
resolve('settled after cleanup')
|
|
373
|
+
}, 30)
|
|
374
|
+
}, { once: true })
|
|
375
|
+
}),
|
|
376
|
+
timeoutMs: 20,
|
|
377
|
+
label: 'cleanup proof',
|
|
378
|
+
}),
|
|
379
|
+
).rejects.toThrow(/post-gate dispatch exceeded 20ms .*cleanup proof/)
|
|
380
|
+
expect(cleanupFinished).toBe(true)
|
|
381
|
+
expect(Date.now() - started).toBeGreaterThanOrEqual(45)
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('preserves a process-cleanup failure after the dispatch clock expires', async () => {
|
|
385
|
+
await expect(
|
|
386
|
+
runWithPostGateClock({
|
|
387
|
+
awaitGates: () => Promise.resolve(),
|
|
388
|
+
work: (signal) => new Promise<never>((_resolve, reject) => {
|
|
389
|
+
signal.addEventListener('abort', () => {
|
|
390
|
+
reject(new Error('process group 123 survived SIGKILL'))
|
|
391
|
+
}, { once: true })
|
|
392
|
+
}),
|
|
393
|
+
timeoutMs: 20,
|
|
394
|
+
label: 'cleanup failure proof',
|
|
395
|
+
}),
|
|
396
|
+
).rejects.toThrow(/post-gate dispatch exceeded 20ms .*process group 123 survived SIGKILL/)
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
it('a gate failure rejects before the work clock ever starts', async () => {
|
|
400
|
+
let workStarted = false
|
|
401
|
+
await expect(
|
|
402
|
+
runWithPostGateClock({
|
|
403
|
+
awaitGates: () => Promise.reject(new Error('no capacity on router within ceiling')),
|
|
404
|
+
work: async () => {
|
|
405
|
+
workStarted = true
|
|
406
|
+
return 'x'
|
|
407
|
+
},
|
|
408
|
+
timeoutMs: 1_000,
|
|
409
|
+
}),
|
|
410
|
+
).rejects.toThrow(/no capacity/)
|
|
411
|
+
expect(workStarted).toBe(false)
|
|
412
|
+
})
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
describe('replicate semantics (repsPerInstance)', () => {
|
|
416
|
+
const iids = ['a', 'b', 'c']
|
|
417
|
+
const run = (iid: string, resolved: boolean | null): ReplicateRun => ({ iid, resolved })
|
|
418
|
+
|
|
419
|
+
it('an instance resolves only when ALL replicates resolve (AND, fail-closed)', () => {
|
|
420
|
+
const runs = [
|
|
421
|
+
run('a', true), run('a', true), // both reps resolved → counts
|
|
422
|
+
run('b', true), run('b', false), // flaky split → does NOT count
|
|
423
|
+
run('c', false), run('c', false),
|
|
424
|
+
]
|
|
425
|
+
expect(resolvedInstanceCount(runs, iids, 2)).toBe(1)
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
it('missing replicates never count as resolved', () => {
|
|
429
|
+
expect(resolvedInstanceCount([run('a', true)], iids, 2)).toBe(0)
|
|
430
|
+
expect(resolvedInstanceCount([run('a', true)], iids, 1)).toBe(1)
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
it('coverage requires every replicate of every instance with a conclusive verdict', () => {
|
|
434
|
+
const full = iids.flatMap((iid) => [run(iid, true), run(iid, false)])
|
|
435
|
+
expect(replicateCoverageComplete(full, iids, 2)).toBe(true)
|
|
436
|
+
expect(replicateCoverageComplete(full.slice(1), iids, 2)).toBe(false)
|
|
437
|
+
const inconclusive = [...full.slice(0, 5), run('c', null)]
|
|
438
|
+
expect(replicateCoverageComplete(inconclusive, iids, 2)).toBe(false)
|
|
439
|
+
})
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
describe('premeasured-baseline drift (the validated artifact is the only denominator)', () => {
|
|
443
|
+
const iids = ['astropy__astropy-13033', 'django__django-11532', 'matplotlib__matplotlib-20826']
|
|
444
|
+
// AND-verdicts of the premeasured artifact's campaign (measured 1/3).
|
|
445
|
+
const expected = {
|
|
446
|
+
'astropy__astropy-13033': false,
|
|
447
|
+
'django__django-11532': false,
|
|
448
|
+
'matplotlib__matplotlib-20826': true,
|
|
449
|
+
}
|
|
450
|
+
const run = (iid: string, resolved: boolean | null): ReplicateRun => ({ iid, resolved })
|
|
451
|
+
const cell = (iid: string, rep: number, resolved: boolean | null): EvidenceCell => ({
|
|
452
|
+
scenarioId: iid,
|
|
453
|
+
rep,
|
|
454
|
+
artifact:
|
|
455
|
+
resolved === null
|
|
456
|
+
? null
|
|
457
|
+
: {
|
|
458
|
+
kind: 'swe-arm',
|
|
459
|
+
iid,
|
|
460
|
+
commit: 'basecommit0',
|
|
461
|
+
resolved,
|
|
462
|
+
verifyPass: resolved,
|
|
463
|
+
patchLines: 1,
|
|
464
|
+
wallS: 10,
|
|
465
|
+
spentTokens: null,
|
|
466
|
+
spentUsd: null,
|
|
467
|
+
recoveredTokens: null,
|
|
468
|
+
workerTokIn: null,
|
|
469
|
+
workerTokOut: null,
|
|
470
|
+
judgeAttempts: null,
|
|
471
|
+
judgeWallS: null,
|
|
472
|
+
runDir: '/tmp/none',
|
|
473
|
+
patchPath: '/tmp/none.patch',
|
|
474
|
+
},
|
|
475
|
+
...(resolved === null ? { error: 'inconclusive' } : {}),
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it('instanceVerdictsFromCells ANDs replicates and omits incomplete instances', () => {
|
|
479
|
+
const cells = [
|
|
480
|
+
cell(iids[0]!, 0, false), cell(iids[0]!, 1, false),
|
|
481
|
+
cell(iids[1]!, 0, true), cell(iids[1]!, 1, false), // flaky → AND false
|
|
482
|
+
cell(iids[2]!, 0, true), // partial → omitted
|
|
483
|
+
]
|
|
484
|
+
expect(instanceVerdictsFromCells(cells, iids, 2)).toEqual({
|
|
485
|
+
'astropy__astropy-13033': false,
|
|
486
|
+
'django__django-11532': false,
|
|
487
|
+
})
|
|
488
|
+
const inconclusive = [cell(iids[0]!, 0, true), cell(iids[0]!, 1, null)]
|
|
489
|
+
expect(instanceVerdictsFromCells(inconclusive, [iids[0]!], 2)).toEqual({})
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
it('drift warnings fire on a reps-complete contradiction, both directions', () => {
|
|
493
|
+
const runs = [
|
|
494
|
+
run(iids[0]!, true), run(iids[0]!, true), // premeasured false, cached true → drift
|
|
495
|
+
run(iids[1]!, false), run(iids[1]!, false), // premeasured false, cached false → quiet
|
|
496
|
+
run(iids[2]!, true), run(iids[2]!, false), // premeasured true, cached false → drift
|
|
497
|
+
]
|
|
498
|
+
const warnings = baselineDriftWarnings(expected, runs, iids, 2)
|
|
499
|
+
expect(warnings).toHaveLength(2)
|
|
500
|
+
expect(warnings[0]).toContain('astropy__astropy-13033: premeasured=false')
|
|
501
|
+
expect(warnings[1]).toContain('matplotlib__matplotlib-20826: premeasured=true')
|
|
502
|
+
expect(warnings.every((w) => w.includes('premeasured artifact rules'))).toBe(true)
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
it('a partial or inconclusive cached record has no AND-verdict — no drift claim', () => {
|
|
506
|
+
// Partial coverage (1 of 2 reps per instance): no AND-verdict exists yet,
|
|
507
|
+
// even where the single present cell disagrees with the artifact.
|
|
508
|
+
const partial = [run(iids[1]!, true), run(iids[2]!, false)]
|
|
509
|
+
expect(baselineDriftWarnings(expected, partial, iids, 2)).toEqual([])
|
|
510
|
+
const inconclusive = [run(iids[2]!, true), run(iids[2]!, null)]
|
|
511
|
+
expect(baselineDriftWarnings(expected, inconclusive, iids, 2)).toEqual([])
|
|
512
|
+
})
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
describe('launch guards', () => {
|
|
516
|
+
it('assertLaunchEnv refuses when either key is absent or blank, naming dotenvx', () => {
|
|
517
|
+
expect(() => assertLaunchEnv({})).toThrow(/TANGLE_API_KEY \+ ZAI_API_KEY absent .*dotenvx/)
|
|
518
|
+
expect(() => assertLaunchEnv({ TANGLE_API_KEY: 'x' })).toThrow(/ZAI_API_KEY/)
|
|
519
|
+
expect(() => assertLaunchEnv({ TANGLE_API_KEY: 'x', ZAI_API_KEY: ' ' })).toThrow(/ZAI_API_KEY/)
|
|
520
|
+
expect(() => assertLaunchEnv({ TANGLE_API_KEY: 'x', ZAI_API_KEY: 'y' })).not.toThrow()
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
it('isPidAlive: own pid is alive; an absurd pid is not', () => {
|
|
524
|
+
expect(isPidAlive(process.pid)).toBe(true)
|
|
525
|
+
expect(isPidAlive(2 ** 30)).toBe(false)
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
it('instance lock: acquire, refuse a live second instance, release', async () => {
|
|
529
|
+
const dir = await mkdtemp(join(tmpdir(), 'r4-lock-'))
|
|
530
|
+
try {
|
|
531
|
+
const lock = await acquireInstanceLock(dir)
|
|
532
|
+
expect(lock.path).toBe(join(dir, INSTANCE_LOCK_FILENAME))
|
|
533
|
+
expect((await readFile(lock.path, 'utf8')).trim()).toBe(String(process.pid))
|
|
534
|
+
// A DIFFERENT live pid (init/pid 1 is always alive) must be refused.
|
|
535
|
+
await writeFile(lock.path, '1\n')
|
|
536
|
+
await expect(acquireInstanceLock(dir)).rejects.toThrow(/pid 1.*refusing to race/s)
|
|
537
|
+
await writeFile(lock.path, `${process.pid}\n`)
|
|
538
|
+
await lock.release()
|
|
539
|
+
expect(existsSync(lock.path)).toBe(false)
|
|
540
|
+
} finally {
|
|
541
|
+
await rm(dir, { recursive: true, force: true })
|
|
542
|
+
}
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
it('instance lock: a stale lock (dead pid or garbage) is reclaimed', async () => {
|
|
546
|
+
const dir = await mkdtemp(join(tmpdir(), 'r4-lock-stale-'))
|
|
547
|
+
try {
|
|
548
|
+
await writeFile(join(dir, INSTANCE_LOCK_FILENAME), `${2 ** 30}\n`)
|
|
549
|
+
const lock = await acquireInstanceLock(dir)
|
|
550
|
+
expect((await readFile(lock.path, 'utf8')).trim()).toBe(String(process.pid))
|
|
551
|
+
await lock.release()
|
|
552
|
+
await writeFile(join(dir, INSTANCE_LOCK_FILENAME), 'not-a-pid\n')
|
|
553
|
+
const lock2 = await acquireInstanceLock(dir)
|
|
554
|
+
expect((await readFile(lock2.path, 'utf8')).trim()).toBe(String(process.pid))
|
|
555
|
+
await lock2.release()
|
|
556
|
+
} finally {
|
|
557
|
+
await rm(dir, { recursive: true, force: true })
|
|
558
|
+
}
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
it('release only removes a lock this instance still owns', async () => {
|
|
562
|
+
const dir = await mkdtemp(join(tmpdir(), 'r4-lock-own-'))
|
|
563
|
+
try {
|
|
564
|
+
const lock = await acquireInstanceLock(dir)
|
|
565
|
+
await writeFile(lock.path, '424242\n') // another instance reclaimed it
|
|
566
|
+
await lock.release()
|
|
567
|
+
expect((await readFile(lock.path, 'utf8')).trim()).toBe('424242')
|
|
568
|
+
} finally {
|
|
569
|
+
await rm(dir, { recursive: true, force: true })
|
|
570
|
+
}
|
|
571
|
+
})
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
describe('round4BuildPrompt', () => {
|
|
575
|
+
it('declares the change-space and renders findings', () => {
|
|
576
|
+
const prompt = round4BuildPrompt({
|
|
577
|
+
report: undefined,
|
|
578
|
+
findings: [
|
|
579
|
+
{ severity: 'high', claim: 'fix placement mismatch', recommended_action: 'settle where maintainers expect' },
|
|
580
|
+
],
|
|
581
|
+
})
|
|
582
|
+
expect(prompt).toContain('DECLARED CHANGE-SPACE')
|
|
583
|
+
expect(prompt).toContain('extensions/pi/**')
|
|
584
|
+
expect(prompt).toContain('src/worker-evidence.ts')
|
|
585
|
+
expect(prompt).toContain('fix placement mismatch')
|
|
586
|
+
expect(prompt).toContain('→ settle where maintainers expect')
|
|
587
|
+
// No raw-trace findings ⇒ no evidence-file requirement block (the
|
|
588
|
+
// change-space instruction still NAMES the artifact path as allowed).
|
|
589
|
+
expect(prompt).not.toContain('Raw trace evidence requirement')
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
it('adds the raw-trace evidence contract when raw-trace findings are present', () => {
|
|
593
|
+
const prompt = round4BuildPrompt({
|
|
594
|
+
report: undefined,
|
|
595
|
+
findings: [{ severity: 'high', area: 'raw-trace-context', claim: 'traces at /run/gen-0' }],
|
|
596
|
+
})
|
|
597
|
+
expect(prompt).toContain('Raw trace evidence requirement')
|
|
598
|
+
expect(prompt).toContain(RAW_TRACE_DIAGNOSIS_PATH)
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
it('changeSpaceInstruction names every allowed root exactly once', () => {
|
|
602
|
+
const text = changeSpaceInstruction(LOOPS_CHANGE_SPACE)
|
|
603
|
+
for (const f of LOOPS_CHANGE_SPACE.files) expect(text).toContain(f)
|
|
604
|
+
for (const p of LOOPS_CHANGE_SPACE.prefixes) expect(text).toContain(`${p}**`)
|
|
605
|
+
})
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
describe('candidate worktree hygiene (finalize precondition + eval isolation)', () => {
|
|
609
|
+
const git = (dir: string, ...argv: string[]) =>
|
|
610
|
+
runOk('git', ['-C', dir, '-c', 'user.email=t@test', '-c', 'user.name=t', '-c', 'core.hooksPath=/dev/null', ...argv])
|
|
611
|
+
|
|
612
|
+
/** Base repo + candidate worktree with proposer-style state: a tracked edit,
|
|
613
|
+
* an untracked non-ignored deliverable, and gitignored install dirt (the
|
|
614
|
+
* exact mix round-4 gen-0 cand-1 died on). */
|
|
615
|
+
async function makeRepoWithDirtyCandidate(): Promise<{ base: string; wt: string; root: string }> {
|
|
616
|
+
const root = await mkdtemp(join(tmpdir(), 'r4-wt-hygiene-'))
|
|
617
|
+
const base = join(root, 'base')
|
|
618
|
+
await mkdir(join(base, 'src'), { recursive: true })
|
|
619
|
+
await writeFile(join(base, '.gitignore'), 'node_modules/\n*.log\n')
|
|
620
|
+
await writeFile(join(base, 'src', 'a.ts'), 'export const a = 1\n')
|
|
621
|
+
await runOk('git', ['-C', base, 'init', '-q'])
|
|
622
|
+
await git(base, 'add', '-A')
|
|
623
|
+
await git(base, 'commit', '-q', '-m', 'base')
|
|
624
|
+
const wt = join(root, 'wt')
|
|
625
|
+
await git(base, 'worktree', 'add', '-q', '-b', 'improve/test-cand', wt, 'HEAD')
|
|
626
|
+
// Proposer session: intentional edit + evidence artifact + install dirt.
|
|
627
|
+
await writeFile(join(wt, 'src', 'a.ts'), 'export const a = 2\n')
|
|
628
|
+
await mkdir(join(wt, '.improve'), { recursive: true })
|
|
629
|
+
await writeFile(join(wt, '.improve', 'raw-trace-diagnosis.md'), '# diagnosis\n')
|
|
630
|
+
await mkdir(join(wt, 'node_modules', 'pkg'), { recursive: true })
|
|
631
|
+
await writeFile(join(wt, 'node_modules', 'pkg', 'index.js'), 'module.exports = 1\n')
|
|
632
|
+
await writeFile(join(wt, 'node_modules', '.modules.yaml'), 'store: real-install\n')
|
|
633
|
+
await writeFile(join(wt, 'debug.log'), 'stray ignored file\n')
|
|
634
|
+
return { base, wt, root }
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
it('purgeIgnoredArtifacts removes ignored dirt but keeps tracked edits and untracked deliverables', async () => {
|
|
638
|
+
const { wt, root } = await makeRepoWithDirtyCandidate()
|
|
639
|
+
try {
|
|
640
|
+
await purgeIgnoredArtifacts(wt)
|
|
641
|
+
expect(existsSync(join(wt, 'node_modules'))).toBe(false)
|
|
642
|
+
expect(existsSync(join(wt, 'debug.log'))).toBe(false)
|
|
643
|
+
expect(existsSync(join(wt, '.improve', 'raw-trace-diagnosis.md'))).toBe(true)
|
|
644
|
+
expect(await readFile(join(wt, 'src', 'a.ts'), 'utf8')).toBe('export const a = 2\n')
|
|
645
|
+
// The exact finalize precondition the substrate enforces: zero ignored extras.
|
|
646
|
+
const ignored = await git(wt, 'ls-files', '--others', '--ignored', '--exclude-standard')
|
|
647
|
+
expect(ignored.stdout.trim()).toBe('')
|
|
648
|
+
} finally {
|
|
649
|
+
await rm(root, { recursive: true, force: true })
|
|
650
|
+
}
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
it('a pre-aborted candidate verifier does not purge or inspect the worktree', async () => {
|
|
654
|
+
const { wt, root } = await makeRepoWithDirtyCandidate()
|
|
655
|
+
const controller = new AbortController()
|
|
656
|
+
controller.abort(new Error('candidate verification cancelled'))
|
|
657
|
+
try {
|
|
658
|
+
await expect(
|
|
659
|
+
loopsCandidateVerifier(root)(wt, controller.signal),
|
|
660
|
+
).rejects.toThrow(/candidate verification cancelled/)
|
|
661
|
+
expect(existsSync(join(wt, 'node_modules'))).toBe(true)
|
|
662
|
+
expect(existsSync(join(wt, 'debug.log'))).toBe(true)
|
|
663
|
+
} finally {
|
|
664
|
+
await rm(root, { recursive: true, force: true })
|
|
665
|
+
}
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
it('a finalized candidate worktree tree-hash is identical before and after a mocked evaluation', async () => {
|
|
669
|
+
const { base, wt, root } = await makeRepoWithDirtyCandidate()
|
|
670
|
+
try {
|
|
671
|
+
await purgeIgnoredArtifacts(wt)
|
|
672
|
+
// Finalize the candidate: everything intentional is committed.
|
|
673
|
+
await git(wt, 'add', '-A')
|
|
674
|
+
await git(wt, 'commit', '-q', '-m', 'agentic: candidate finalized')
|
|
675
|
+
const commit = (await git(wt, 'rev-parse', 'HEAD')).stdout.trim()
|
|
676
|
+
const treeBefore = (await git(wt, 'rev-parse', 'HEAD^{tree}')).stdout.trim()
|
|
677
|
+
|
|
678
|
+
// Mocked evaluation: a detached eval worktree at the candidate commit
|
|
679
|
+
// takes ALL the runtime dirt; the CodeSurface worktree is never touched.
|
|
680
|
+
const evalWt = join(root, 'eval-wt')
|
|
681
|
+
await addEvalWorktree(base, commit, evalWt)
|
|
682
|
+
await mkdir(join(evalWt, '.loops'), { recursive: true })
|
|
683
|
+
await writeFile(join(evalWt, '.loops', 'state.json'), '{"run":"mock"}\n')
|
|
684
|
+
await writeFile(join(evalWt, 'run.log'), 'arm eval output\n')
|
|
685
|
+
await removeEvalWorktree(base, evalWt)
|
|
686
|
+
|
|
687
|
+
expect(existsSync(evalWt)).toBe(false)
|
|
688
|
+
expect((await git(wt, 'rev-parse', 'HEAD^{tree}')).stdout.trim()).toBe(treeBefore)
|
|
689
|
+
expect((await git(wt, 'status', '--porcelain=v1', '--untracked-files=all')).stdout.trim()).toBe('')
|
|
690
|
+
const ignored = await git(wt, 'ls-files', '--others', '--ignored', '--exclude-standard')
|
|
691
|
+
expect(ignored.stdout.trim()).toBe('')
|
|
692
|
+
} finally {
|
|
693
|
+
await rm(root, { recursive: true, force: true })
|
|
694
|
+
}
|
|
695
|
+
})
|
|
696
|
+
})
|