@tangle-network/agent-bench 0.3.7 → 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 +4 -0
- package/HARNESS.md +43 -0
- package/dist/adapters.js +23 -23
- 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/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-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-PWQVGAJB.js → chunk-V7AEBY6U.js} +21 -21
- 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 +23 -23
- package/package.json +4 -4
- package/scripts/verify-packed-consumer.mjs +1 -1
- package/src/benchmarks/_harness.ts +20 -2
- package/src/benchmarks/humaneval.test.mts +2 -2
- 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/swe-arena/activation.mts +228 -0
- package/src/swe-arena/activation.test.mts +303 -0
- package/src/swe-arena/arms.ts +30 -14
- package/src/swe-arena/briefing.mts +217 -0
- package/src/swe-arena/briefing.test.mts +178 -0
- package/src/swe-arena/calibrate.ts +102 -1
- package/src/swe-arena/cell-evidence.mts +41 -9
- 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/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/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.ts +135 -2
- package/src/swe-arena/gepa-seat.mts +583 -0
- package/src/swe-arena/gepa-seat.test.mts +635 -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/outer-loop.mts +677 -27
- 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/proposer-fanout.mts +295 -14
- package/src/swe-arena/proposer-fanout.test.mts +247 -0
- package/src/swe-arena/proposer-provenance.mts +177 -0
- package/src/swe-arena/proposer-provenance.test.mts +106 -0
- package/src/swe-arena/run-experiment.mts +367 -1
- package/src/swe-arena/run-report.mts +75 -0
- package/src/swe-arena/run-supervisor.test.mts +5 -3
- package/src/swe-arena/score-split.mts +140 -0
- package/src/swe-arena/score-split.test.mts +123 -0
- package/src/swe-arena/types.ts +52 -0
- package/src/swe-structural.mts +245 -837
- package/dist/chunk-LRRD7NAG.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-PWQVGAJB.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,643 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory-bench runner tests: a synthetic two-commit "mirror"
|
|
3
|
+
* repo stands in for the real local mirrors, and its judge harness prints
|
|
4
|
+
* vitest-format summary lines, so the full archive → apply → overlay → run →
|
|
5
|
+
* parse pipeline is exercised through the same container boundary as real
|
|
6
|
+
* factory instances.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
10
|
+
import { existsSync } from 'node:fs'
|
|
11
|
+
import { tmpdir } from 'node:os'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
15
|
+
import {
|
|
16
|
+
calibrateFactoryInstance,
|
|
17
|
+
goldImplPatch,
|
|
18
|
+
} from './calibrate.ts'
|
|
19
|
+
import {
|
|
20
|
+
judgeFactoryPatch,
|
|
21
|
+
parseFactoryJudgeResult,
|
|
22
|
+
parseVitestSummary,
|
|
23
|
+
} from './factory-judge-child.mts'
|
|
24
|
+
import { runFactoryCommand } from './factory-command-container.ts'
|
|
25
|
+
import {
|
|
26
|
+
FACTORY_INSTANCES_DIR,
|
|
27
|
+
loadFactoryInstance,
|
|
28
|
+
loadFactoryInstances,
|
|
29
|
+
type LoadedFactoryInstance,
|
|
30
|
+
} from './fixtures.ts'
|
|
31
|
+
import { run, runOk } from './proc.ts'
|
|
32
|
+
import { FACTORY_BASE_REF, factoryLedgerKeys, materializeFactoryWorkspace } from './run-experiment.mts'
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Synthetic mirror: base commit (README + package.json, no impl), then three
|
|
36
|
+
// judge_ref variants layered on top —
|
|
37
|
+
// good: impl (add+mul) + judge tests that pin both → calibrates
|
|
38
|
+
// trivial: judge tests that pass with NO impl → base passes → reject
|
|
39
|
+
// too-hard: judge tests demanding an un-shipped sub() → gold fails → reject
|
|
40
|
+
// The judge harness is plain node (no vitest dependency) that PRINTS vitest's
|
|
41
|
+
// summary format — the protocol under test is the parse, not vitest itself.
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
const JUDGE_HARNESS = `
|
|
45
|
+
let passed = 0
|
|
46
|
+
let failed = 0
|
|
47
|
+
const check = (name, fn) => {
|
|
48
|
+
try {
|
|
49
|
+
if (fn()) passed += 1
|
|
50
|
+
else failed += 1
|
|
51
|
+
} catch {
|
|
52
|
+
failed += 1
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export async function runChecks(checks) {
|
|
56
|
+
let lib
|
|
57
|
+
try {
|
|
58
|
+
lib = await import('../lib.mjs')
|
|
59
|
+
} catch {
|
|
60
|
+
console.log(' Test Files 1 failed (1)')
|
|
61
|
+
console.log(' Tests no tests')
|
|
62
|
+
process.exit(1)
|
|
63
|
+
}
|
|
64
|
+
for (const [name, fn] of checks) check(name, () => fn(lib))
|
|
65
|
+
const total = passed + failed
|
|
66
|
+
const parts = []
|
|
67
|
+
if (failed > 0) parts.push(failed + ' failed')
|
|
68
|
+
if (passed > 0) parts.push(passed + ' passed')
|
|
69
|
+
console.log(' Test Files ' + (failed > 0 ? '1 failed (1)' : '1 passed (1)'))
|
|
70
|
+
console.log(' Tests ' + parts.join(' | ') + ' (' + total + ')')
|
|
71
|
+
process.exit(failed > 0 ? 1 : 0)
|
|
72
|
+
}
|
|
73
|
+
`
|
|
74
|
+
|
|
75
|
+
const GOOD_TESTS = `
|
|
76
|
+
import { runChecks } from './harness.mjs'
|
|
77
|
+
await runChecks([
|
|
78
|
+
['add', (lib) => lib.add(1, 2) === 3],
|
|
79
|
+
['mul', (lib) => lib.mul(2, 3) === 6],
|
|
80
|
+
])
|
|
81
|
+
`
|
|
82
|
+
|
|
83
|
+
const TRIVIAL_TESTS = `
|
|
84
|
+
console.log(' Test Files 1 passed (1)')
|
|
85
|
+
console.log(' Tests 2 passed (2)')
|
|
86
|
+
`
|
|
87
|
+
|
|
88
|
+
const TOO_HARD_TESTS = `
|
|
89
|
+
import { runChecks } from './harness.mjs'
|
|
90
|
+
await runChecks([
|
|
91
|
+
['add', (lib) => lib.add(1, 2) === 3],
|
|
92
|
+
['sub', (lib) => lib.sub(3, 1) === 2],
|
|
93
|
+
])
|
|
94
|
+
`
|
|
95
|
+
|
|
96
|
+
const SYNTHETIC_COMMAND_IMAGE =
|
|
97
|
+
'node:24-bookworm-slim@sha256:cb4e8f7c443347358b7875e717c29e27bf9befc8f5a26cf18af3c3dec80e58c5'
|
|
98
|
+
|
|
99
|
+
function credentialProbe(hostRoot: string): string {
|
|
100
|
+
const knownAmbientFiles = [
|
|
101
|
+
join(hostRoot, 'ambient-home', '.config', 'gh', 'hosts.yml'),
|
|
102
|
+
join(hostRoot, 'ambient-home', '.aws', 'credentials'),
|
|
103
|
+
join(hostRoot, 'ambient-home', '.npmrc'),
|
|
104
|
+
join(hostRoot, 'ambient-xdg', 'gh', 'hosts.yml'),
|
|
105
|
+
'/home/drew/.config/gh/hosts.yml',
|
|
106
|
+
'/home/drew/.aws/credentials',
|
|
107
|
+
'/home/drew/.npmrc',
|
|
108
|
+
'/home/runner/.config/gh/hosts.yml',
|
|
109
|
+
'/home/runner/.aws/credentials',
|
|
110
|
+
'/home/runner/.npmrc',
|
|
111
|
+
'/root/.config/gh/hosts.yml',
|
|
112
|
+
'/root/.aws/credentials',
|
|
113
|
+
'/root/.npmrc',
|
|
114
|
+
]
|
|
115
|
+
return `
|
|
116
|
+
import { lstatSync, readFileSync, writeFileSync } from 'node:fs'
|
|
117
|
+
import { join } from 'node:path'
|
|
118
|
+
|
|
119
|
+
const secretNames = [
|
|
120
|
+
'ARBITRARY_FACTORY_SECRET',
|
|
121
|
+
'GH_TOKEN',
|
|
122
|
+
'TANGLETOOLS_GH_TOKEN',
|
|
123
|
+
'DREW_GH_TOKEN',
|
|
124
|
+
'AWS_SECRET_ACCESS_KEY',
|
|
125
|
+
'NPM_TOKEN',
|
|
126
|
+
'NODE_AUTH_TOKEN',
|
|
127
|
+
'SSH_AUTH_SOCK',
|
|
128
|
+
'GPG_AGENT_INFO',
|
|
129
|
+
'DBUS_SESSION_BUS_ADDRESS',
|
|
130
|
+
]
|
|
131
|
+
const knownAmbientFiles = ${JSON.stringify(knownAmbientFiles)}
|
|
132
|
+
const leakedEnv = secretNames.filter((name) => process.env[name] !== undefined)
|
|
133
|
+
const procLeaks = []
|
|
134
|
+
const procRootFiles = []
|
|
135
|
+
let pid = process.pid
|
|
136
|
+
for (let depth = 0; depth < 32; depth += 1) {
|
|
137
|
+
let stat
|
|
138
|
+
try {
|
|
139
|
+
stat = readFileSync('/proc/' + pid + '/stat', 'utf8')
|
|
140
|
+
} catch {
|
|
141
|
+
break
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const environ = readFileSync('/proc/' + pid + '/environ')
|
|
145
|
+
const names = secretNames.filter((name) => environ.includes(Buffer.from(name + '=')))
|
|
146
|
+
if (names.length > 0) procLeaks.push({ depth, pid, names })
|
|
147
|
+
} catch {}
|
|
148
|
+
for (const path of knownAmbientFiles) {
|
|
149
|
+
const procPath = '/proc/' + pid + '/root' + path
|
|
150
|
+
try {
|
|
151
|
+
if (readFileSync(procPath).length > 0) procRootFiles.push(procPath)
|
|
152
|
+
} catch {}
|
|
153
|
+
}
|
|
154
|
+
const close = stat.lastIndexOf(')')
|
|
155
|
+
const parent = Number(stat.slice(close + 2).split(' ')[1])
|
|
156
|
+
if (!Number.isSafeInteger(parent) || parent < 1 || parent === pid) break
|
|
157
|
+
pid = parent
|
|
158
|
+
}
|
|
159
|
+
const candidateFiles = [
|
|
160
|
+
process.env.HOME && join(process.env.HOME, '.config', 'gh', 'hosts.yml'),
|
|
161
|
+
process.env.HOME && join(process.env.HOME, '.aws', 'credentials'),
|
|
162
|
+
process.env.HOME && join(process.env.HOME, '.npmrc'),
|
|
163
|
+
process.env.XDG_CONFIG_HOME && join(process.env.XDG_CONFIG_HOME, 'gh', 'hosts.yml'),
|
|
164
|
+
process.env.NPM_CONFIG_USERCONFIG,
|
|
165
|
+
process.env.npm_config_userconfig,
|
|
166
|
+
process.env.NPM_CONFIG_GLOBALCONFIG,
|
|
167
|
+
process.env.npm_config_globalconfig,
|
|
168
|
+
...knownAmbientFiles,
|
|
169
|
+
...procRootFiles,
|
|
170
|
+
].filter(Boolean)
|
|
171
|
+
const credentialFiles = []
|
|
172
|
+
for (const path of new Set(candidateFiles)) {
|
|
173
|
+
try {
|
|
174
|
+
const content = readFileSync(path, 'utf8')
|
|
175
|
+
if (content.trim()) credentialFiles.push(path)
|
|
176
|
+
} catch {}
|
|
177
|
+
}
|
|
178
|
+
const hiddenJudgeFiles = ['tests/judge.mjs', 'tests/harness.mjs'].filter((path) => {
|
|
179
|
+
try {
|
|
180
|
+
return readFileSync(path).length > 0
|
|
181
|
+
} catch {
|
|
182
|
+
return false
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
const credentialSockets = ['/var/run/docker.sock', '/run/docker.sock'].filter((path) => {
|
|
186
|
+
try {
|
|
187
|
+
return lstatSync(path).isSocket()
|
|
188
|
+
} catch {
|
|
189
|
+
return false
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
let rootWritable = false
|
|
193
|
+
try {
|
|
194
|
+
writeFileSync('/factory-root-write-probe', 'unexpected')
|
|
195
|
+
rootWritable = true
|
|
196
|
+
} catch {}
|
|
197
|
+
writeFileSync(
|
|
198
|
+
'credential-probe-result.json',
|
|
199
|
+
JSON.stringify({
|
|
200
|
+
leakedEnv,
|
|
201
|
+
procLeaks,
|
|
202
|
+
procRootFiles,
|
|
203
|
+
credentialFiles,
|
|
204
|
+
hiddenJudgeFiles,
|
|
205
|
+
credentialSockets,
|
|
206
|
+
rootWritable,
|
|
207
|
+
uid: process.getuid?.(),
|
|
208
|
+
home: process.env.HOME,
|
|
209
|
+
xdgConfigHome: process.env.XDG_CONFIG_HOME,
|
|
210
|
+
npmUserConfig: process.env.NPM_CONFIG_USERCONFIG ?? process.env.npm_config_userconfig,
|
|
211
|
+
}),
|
|
212
|
+
)
|
|
213
|
+
if (
|
|
214
|
+
leakedEnv.length > 0 ||
|
|
215
|
+
procLeaks.length > 0 ||
|
|
216
|
+
procRootFiles.length > 0 ||
|
|
217
|
+
credentialFiles.length > 0 ||
|
|
218
|
+
hiddenJudgeFiles.length > 0 ||
|
|
219
|
+
credentialSockets.length > 0 ||
|
|
220
|
+
rootWritable ||
|
|
221
|
+
process.getuid?.() === 0
|
|
222
|
+
) process.exit(86)
|
|
223
|
+
`
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const IMPL = `export const add = (a, b) => a + b\nexport const mul = (a, b) => a * b\n`
|
|
227
|
+
|
|
228
|
+
interface SyntheticMirror {
|
|
229
|
+
mirror: string
|
|
230
|
+
baseCommit: string
|
|
231
|
+
refs: { good: string; trivial: string; tooHard: string }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function git(cwd: string, ...argv: string[]): Promise<string> {
|
|
235
|
+
const res = await runOk('git', ['-C', cwd, ...argv])
|
|
236
|
+
return res.stdout.trim()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function commitAll(cwd: string, msg: string): Promise<string> {
|
|
240
|
+
await git(cwd, 'add', '-A')
|
|
241
|
+
await runOk('git', ['-C', cwd, '-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-q', '-m', msg])
|
|
242
|
+
return git(cwd, 'rev-parse', 'HEAD')
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function makeSyntheticMirror(root: string): Promise<SyntheticMirror> {
|
|
246
|
+
const mirror = join(root, 'mirror')
|
|
247
|
+
await mkdir(mirror, { recursive: true })
|
|
248
|
+
await runOk('git', ['-C', mirror, 'init', '-q', '-b', 'main'])
|
|
249
|
+
await writeFile(join(mirror, 'README.md'), '# synthetic\n')
|
|
250
|
+
await writeFile(
|
|
251
|
+
join(mirror, 'package.json'),
|
|
252
|
+
JSON.stringify({
|
|
253
|
+
name: 'synthetic',
|
|
254
|
+
type: 'module',
|
|
255
|
+
scripts: { preinstall: 'node credential-probe.mjs' },
|
|
256
|
+
}),
|
|
257
|
+
)
|
|
258
|
+
await writeFile(join(mirror, 'credential-probe.mjs'), credentialProbe(root))
|
|
259
|
+
await writeFile(join(mirror, '.gitignore'), 'node_modules\n')
|
|
260
|
+
const baseCommit = await commitAll(mirror, 'base')
|
|
261
|
+
|
|
262
|
+
const variant = async (branch: string, tests: string, withImpl: boolean): Promise<string> => {
|
|
263
|
+
await git(mirror, 'checkout', '-q', '-b', branch, baseCommit)
|
|
264
|
+
await mkdir(join(mirror, 'tests'), { recursive: true })
|
|
265
|
+
await writeFile(join(mirror, 'tests', 'harness.mjs'), JUDGE_HARNESS)
|
|
266
|
+
await writeFile(join(mirror, 'tests', 'judge.mjs'), tests)
|
|
267
|
+
if (withImpl) await writeFile(join(mirror, 'lib.mjs'), IMPL)
|
|
268
|
+
return commitAll(mirror, `merge ${branch}`)
|
|
269
|
+
}
|
|
270
|
+
const good = await variant('pr-good', GOOD_TESTS, true)
|
|
271
|
+
const trivial = await variant('pr-trivial', TRIVIAL_TESTS, true)
|
|
272
|
+
const tooHard = await variant('pr-too-hard', TOO_HARD_TESTS, true)
|
|
273
|
+
return { mirror, baseCommit, refs: { good, trivial, tooHard } }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function makeInstanceDir(
|
|
277
|
+
root: string,
|
|
278
|
+
name: string,
|
|
279
|
+
m: SyntheticMirror,
|
|
280
|
+
judgeRef: string,
|
|
281
|
+
overrides: Record<string, unknown> = {},
|
|
282
|
+
): Promise<string> {
|
|
283
|
+
const dir = join(root, name)
|
|
284
|
+
await mkdir(dir, { recursive: true })
|
|
285
|
+
await writeFile(
|
|
286
|
+
join(dir, 'manifest.json'),
|
|
287
|
+
JSON.stringify({
|
|
288
|
+
id: `factory.synthetic.${name}`,
|
|
289
|
+
repo: 'synthetic/repo',
|
|
290
|
+
repo_local_mirror: m.mirror,
|
|
291
|
+
base_commit: m.baseCommit,
|
|
292
|
+
judge_ref: judgeRef,
|
|
293
|
+
spec_md: 'spec.md',
|
|
294
|
+
judge_tests: ['tests/judge.mjs', 'tests/harness.mjs'],
|
|
295
|
+
excluded_tests: [],
|
|
296
|
+
command_image: SYNTHETIC_COMMAND_IMAGE,
|
|
297
|
+
setup_cmds: [],
|
|
298
|
+
judge_cmds: ['node tests/judge.mjs'],
|
|
299
|
+
resolved_criterion: 'all 2 judge tests pass; partial score = passed/2',
|
|
300
|
+
timeout_s: 60,
|
|
301
|
+
...overrides,
|
|
302
|
+
}),
|
|
303
|
+
)
|
|
304
|
+
await writeFile(join(dir, 'spec.md'), '# Implement lib.mjs\n\nExport `add(a,b)` and `mul(a,b)` from `lib.mjs`.\n')
|
|
305
|
+
return dir
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let root: string
|
|
309
|
+
let mirror: SyntheticMirror
|
|
310
|
+
let goodInst: LoadedFactoryInstance
|
|
311
|
+
|
|
312
|
+
beforeAll(async () => {
|
|
313
|
+
root = await mkdtemp(join(tmpdir(), 'factory-test-'))
|
|
314
|
+
mirror = await makeSyntheticMirror(root)
|
|
315
|
+
goodInst = loadFactoryInstance(await makeInstanceDir(root, 'good', mirror, mirror.refs.good))
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
afterAll(async () => {
|
|
319
|
+
await rm(root, { recursive: true, force: true })
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// parseVitestSummary — partial-credit arithmetic feeds off these counts.
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
describe('parseVitestSummary', () => {
|
|
327
|
+
it('parses an all-pass summary', () => {
|
|
328
|
+
expect(parseVitestSummary(' Test Files 2 passed (2)\n Tests 30 passed (30)\n')).toEqual({
|
|
329
|
+
passed: 30,
|
|
330
|
+
failed: 0,
|
|
331
|
+
reportedTotal: 30,
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('parses a mixed summary', () => {
|
|
336
|
+
expect(parseVitestSummary(' Tests 17 failed | 13 passed (30)\n')).toEqual({
|
|
337
|
+
passed: 13,
|
|
338
|
+
failed: 17,
|
|
339
|
+
reportedTotal: 30,
|
|
340
|
+
})
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('treats a collection failure ("no tests") as a zero-pass verdict', () => {
|
|
344
|
+
expect(parseVitestSummary(' Test Files 2 failed (2)\n Tests no tests\n')).toEqual({
|
|
345
|
+
passed: 0,
|
|
346
|
+
failed: 0,
|
|
347
|
+
reportedTotal: 0,
|
|
348
|
+
})
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('parses through ANSI color codes', () => {
|
|
352
|
+
const colored = '\u001b[2m Tests \u001b[22m \u001b[1m\u001b[32m21 passed\u001b[39m\u001b[22m\u001b[90m (21)\u001b[39m\n'
|
|
353
|
+
expect(parseVitestSummary(colored)).toEqual({ passed: 21, failed: 0, reportedTotal: 21 })
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
it('never mistakes the Test Files line for the Tests line, and returns undefined without a summary', () => {
|
|
357
|
+
expect(parseVitestSummary(' Test Files 2 failed (2)\n')).toBeUndefined()
|
|
358
|
+
expect(parseVitestSummary('some crash output\n')).toBeUndefined()
|
|
359
|
+
})
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
describe('factory command credential isolation', () => {
|
|
363
|
+
it('blocks arbitrary env, auth sockets, npm config, and home credentials from a package lifecycle script', async () => {
|
|
364
|
+
const ambientHome = join(root, 'ambient-home')
|
|
365
|
+
const ambientXdg = join(root, 'ambient-xdg')
|
|
366
|
+
await mkdir(join(ambientHome, '.config', 'gh'), { recursive: true })
|
|
367
|
+
await mkdir(join(ambientHome, '.aws'), { recursive: true })
|
|
368
|
+
await mkdir(join(ambientXdg, 'gh'), { recursive: true })
|
|
369
|
+
await writeFile(join(ambientHome, '.config', 'gh', 'hosts.yml'), 'oauth_token: ambient-secret\n')
|
|
370
|
+
await writeFile(join(ambientHome, '.aws', 'credentials'), 'aws_secret_access_key=ambient-secret\n')
|
|
371
|
+
await writeFile(join(ambientHome, '.npmrc'), '//registry.npmjs.org/:_authToken=ambient-secret\n')
|
|
372
|
+
await writeFile(join(ambientXdg, 'gh', 'hosts.yml'), 'oauth_token: ambient-xdg-secret\n')
|
|
373
|
+
|
|
374
|
+
const injected: Record<string, string> = {
|
|
375
|
+
HOME: ambientHome,
|
|
376
|
+
XDG_CONFIG_HOME: ambientXdg,
|
|
377
|
+
ARBITRARY_FACTORY_SECRET: 'arbitrary-secret',
|
|
378
|
+
GH_TOKEN: 'github-secret',
|
|
379
|
+
TANGLETOOLS_GH_TOKEN: 'tangletools-secret',
|
|
380
|
+
DREW_GH_TOKEN: 'drew-secret',
|
|
381
|
+
AWS_SECRET_ACCESS_KEY: 'aws-secret',
|
|
382
|
+
NPM_TOKEN: 'npm-secret',
|
|
383
|
+
NODE_AUTH_TOKEN: 'node-auth-secret',
|
|
384
|
+
NPM_CONFIG_USERCONFIG: join(ambientHome, '.npmrc'),
|
|
385
|
+
SSH_AUTH_SOCK: join(ambientHome, 'agent.sock'),
|
|
386
|
+
GPG_AGENT_INFO: join(ambientHome, 'gpg-agent'),
|
|
387
|
+
DBUS_SESSION_BUS_ADDRESS: 'unix:path=/tmp/operator-bus',
|
|
388
|
+
}
|
|
389
|
+
const saved = new Map(Object.keys(injected).map((name) => [name, process.env[name]]))
|
|
390
|
+
for (const [name, value] of Object.entries(injected)) process.env[name] = value
|
|
391
|
+
|
|
392
|
+
const workDir = join(root, 'credential-probe-workspace')
|
|
393
|
+
try {
|
|
394
|
+
const instance = loadFactoryInstance(
|
|
395
|
+
await makeInstanceDir(root, 'credential-probe', mirror, mirror.refs.good, {
|
|
396
|
+
setup_cmds: ['npm install --no-audit --no-fund'],
|
|
397
|
+
}),
|
|
398
|
+
)
|
|
399
|
+
const patch = await makePatch(goodInst, IMPL)
|
|
400
|
+
const { result } = await judgeFactoryPatch(instance, patch, { workDir, keepWorkspace: true })
|
|
401
|
+
expect(result).toMatchObject({ resolved: true, passed: 2, total: 2 })
|
|
402
|
+
|
|
403
|
+
const report = JSON.parse(await readFile(join(workDir, 'credential-probe-result.json'), 'utf8')) as {
|
|
404
|
+
leakedEnv: string[]
|
|
405
|
+
procLeaks: Array<{ depth: number; pid: number; names: string[] }>
|
|
406
|
+
procRootFiles: string[]
|
|
407
|
+
credentialFiles: string[]
|
|
408
|
+
hiddenJudgeFiles: string[]
|
|
409
|
+
credentialSockets: string[]
|
|
410
|
+
rootWritable: boolean
|
|
411
|
+
uid: number
|
|
412
|
+
home: string
|
|
413
|
+
xdgConfigHome: string
|
|
414
|
+
npmUserConfig: string
|
|
415
|
+
}
|
|
416
|
+
expect(report.leakedEnv).toEqual([])
|
|
417
|
+
expect(report.procLeaks).toEqual([])
|
|
418
|
+
expect(report.procRootFiles).toEqual([])
|
|
419
|
+
expect(report.credentialFiles).toEqual([])
|
|
420
|
+
expect(report.hiddenJudgeFiles).toEqual([])
|
|
421
|
+
expect(report.credentialSockets).toEqual([])
|
|
422
|
+
expect(report.rootWritable).toBe(false)
|
|
423
|
+
expect(report.uid).toBeGreaterThan(0)
|
|
424
|
+
expect(report.home).not.toBe(ambientHome)
|
|
425
|
+
expect(report.xdgConfigHome).not.toBe(ambientXdg)
|
|
426
|
+
expect(report.npmUserConfig).not.toBe(join(ambientHome, '.npmrc'))
|
|
427
|
+
} finally {
|
|
428
|
+
for (const [name, value] of saved) {
|
|
429
|
+
if (value === undefined) delete process.env[name]
|
|
430
|
+
else process.env[name] = value
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}, 60_000)
|
|
434
|
+
|
|
435
|
+
it('does not reuse writable Corepack, npm, or pnpm caches between commands', async () => {
|
|
436
|
+
const workspace = join(root, 'cache-isolation-workspace')
|
|
437
|
+
await mkdir(workspace, { recursive: true })
|
|
438
|
+
const first = await runFactoryCommand(
|
|
439
|
+
workspace,
|
|
440
|
+
'touch "$COREPACK_HOME/poison" "$NPM_CONFIG_CACHE/poison" "$npm_config_store_dir/poison" && test -e "$COREPACK_HOME/poison" && test -e "$NPM_CONFIG_CACHE/poison" && test -e "$npm_config_store_dir/poison"',
|
|
441
|
+
{
|
|
442
|
+
image: SYNTHETIC_COMMAND_IMAGE,
|
|
443
|
+
network: 'none',
|
|
444
|
+
timeoutMs: 30_000,
|
|
445
|
+
},
|
|
446
|
+
)
|
|
447
|
+
expect(first.code).toBe(0)
|
|
448
|
+
|
|
449
|
+
const second = await runFactoryCommand(
|
|
450
|
+
workspace,
|
|
451
|
+
'test ! -e "$COREPACK_HOME/poison" && test ! -e "$NPM_CONFIG_CACHE/poison" && test ! -e "$npm_config_store_dir/poison"',
|
|
452
|
+
{
|
|
453
|
+
image: SYNTHETIC_COMMAND_IMAGE,
|
|
454
|
+
network: 'none',
|
|
455
|
+
timeoutMs: 30_000,
|
|
456
|
+
},
|
|
457
|
+
)
|
|
458
|
+
expect(second.code).toBe(0)
|
|
459
|
+
}, 60_000)
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
// Manifest loader — fail-loud shape validation.
|
|
464
|
+
// ---------------------------------------------------------------------------
|
|
465
|
+
|
|
466
|
+
describe('loadFactoryInstance', () => {
|
|
467
|
+
it('loads a valid manifest and derives the calibrated denominator', () => {
|
|
468
|
+
expect(goodInst.id).toBe('factory.synthetic.good')
|
|
469
|
+
expect(goodInst.judgeTestTotal).toBe(2)
|
|
470
|
+
expect(goodInst.spec).toContain('lib.mjs')
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
it('rejects a manifest missing a required field, naming it', async () => {
|
|
474
|
+
const dir = await makeInstanceDir(root, 'bad-missing', mirror, mirror.refs.good, { base_commit: undefined })
|
|
475
|
+
expect(() => loadFactoryInstance(dir)).toThrow(/base_commit/)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it('rejects a resolved_criterion without a passed/<N> denominator', async () => {
|
|
479
|
+
const dir = await makeInstanceDir(root, 'bad-criterion', mirror, mirror.refs.good, {
|
|
480
|
+
resolved_criterion: 'all judge tests pass',
|
|
481
|
+
})
|
|
482
|
+
expect(() => loadFactoryInstance(dir)).toThrow(/passed\/<N>/)
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
it('rejects a mutable command image tag', async () => {
|
|
486
|
+
const dir = await makeInstanceDir(root, 'bad-image', mirror, mirror.refs.good, {
|
|
487
|
+
command_image: 'node:24-bookworm-slim',
|
|
488
|
+
})
|
|
489
|
+
expect(() => loadFactoryInstance(dir)).toThrow(/pinned.*sha256/)
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
it('loads the 3 shipped pilot instances with their calibrated totals', () => {
|
|
493
|
+
const pilots = loadFactoryInstances(FACTORY_INSTANCES_DIR)
|
|
494
|
+
expect(pilots.every((pilot) => /@sha256:[0-9a-f]{64}$/.test(pilot.command_image))).toBe(
|
|
495
|
+
true,
|
|
496
|
+
)
|
|
497
|
+
expect(pilots.map((p) => [p.id, p.judgeTestTotal])).toEqual([
|
|
498
|
+
['factory.agent-eval.309', 30],
|
|
499
|
+
['factory.agent-runtime.232', 21],
|
|
500
|
+
['factory.loops.28', 20],
|
|
501
|
+
])
|
|
502
|
+
})
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
// Judge child pipeline on the synthetic fixture.
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
|
|
509
|
+
describe('judgeFactoryPatch', () => {
|
|
510
|
+
it('gold (impl-only PR diff) resolves with full score', async () => {
|
|
511
|
+
const { result } = await judgeFactoryPatch(goodInst, await goldImplPatch(goodInst))
|
|
512
|
+
expect(result).toMatchObject({ resolved: true, score: 1, passed: 2, total: 2 })
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
it('empty patch (bare base) fails: judge tests cannot even collect', async () => {
|
|
516
|
+
const { result } = await judgeFactoryPatch(goodInst, '')
|
|
517
|
+
expect(result).toMatchObject({ resolved: false, score: 0, passed: 0, total: 2 })
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
it('partial impl earns partial credit, never resolved', async () => {
|
|
521
|
+
// add correct, mul wrong → 1/2.
|
|
522
|
+
const patch = await makePatch(goodInst, 'export const add = (a, b) => a + b\nexport const mul = (a, b) => a + b\n')
|
|
523
|
+
const { result } = await judgeFactoryPatch(goodInst, patch)
|
|
524
|
+
expect(result).toMatchObject({ resolved: false, score: 0.5, passed: 1, total: 2 })
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
it('an unappliable patch is the candidate\'s failure (resolved false), not infra', async () => {
|
|
528
|
+
const garbage = 'diff --git a/nope.txt b/nope.txt\n--- a/nope.txt\n+++ b/nope.txt\n@@ -1 +1 @@\n-x\n+y\n'
|
|
529
|
+
const { result } = await judgeFactoryPatch(goodInst, garbage)
|
|
530
|
+
expect(result.resolved).toBe(false)
|
|
531
|
+
expect(result.error).toMatch(/apply-failed/)
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
it('the child CLI prints one JUDGE_RESULT line the serialized-judge protocol parses', async () => {
|
|
535
|
+
const patchFile = join(root, 'gold.patch')
|
|
536
|
+
await writeFile(patchFile, await goldImplPatch(goodInst))
|
|
537
|
+
const here = fileURLToPath(new URL('.', import.meta.url))
|
|
538
|
+
const childPath = join(here, 'factory-judge-child.mts')
|
|
539
|
+
const res = await runOk('node', ['--import', 'tsx', childPath, goodInst.dir, patchFile], {
|
|
540
|
+
cwd: join(here, '..', '..'),
|
|
541
|
+
})
|
|
542
|
+
const parsed = parseFactoryJudgeResult(res.stdout)
|
|
543
|
+
expect(parsed).toMatchObject({ iid: 'factory.synthetic.good', resolved: true, passed: 2, total: 2 })
|
|
544
|
+
}, 60_000)
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
/** A worker-style patch: apply content in a scratch clone of base, git-diff it. */
|
|
548
|
+
async function makePatch(inst: LoadedFactoryInstance, libSource: string): Promise<string> {
|
|
549
|
+
const ws = await mkdtemp(join(tmpdir(), 'factory-patch-'))
|
|
550
|
+
try {
|
|
551
|
+
const { syntheticBase } = await materializeFactoryWorkspace(inst, join(ws, 'w'))
|
|
552
|
+
await writeFile(join(ws, 'w', 'lib.mjs'), libSource)
|
|
553
|
+
await git(join(ws, 'w'), 'add', '-A')
|
|
554
|
+
return (await runOk('git', ['-C', join(ws, 'w'), 'diff', '--cached', syntheticBase])).stdout
|
|
555
|
+
} finally {
|
|
556
|
+
await rm(ws, { recursive: true, force: true })
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// ---------------------------------------------------------------------------
|
|
561
|
+
// Worker workspace: synthetic history, no future refs leakable.
|
|
562
|
+
// ---------------------------------------------------------------------------
|
|
563
|
+
|
|
564
|
+
describe('materializeFactoryWorkspace', () => {
|
|
565
|
+
it('produces a fresh-history repo with SPEC.md and no trace of the real repo', async () => {
|
|
566
|
+
const dest = join(root, 'worker-ws')
|
|
567
|
+
const { syntheticBase } = await materializeFactoryWorkspace(goodInst, dest)
|
|
568
|
+
|
|
569
|
+
// SPEC.md present; judge tests absent (they only exist at judge_ref).
|
|
570
|
+
expect(existsSync(join(dest, 'SPEC.md'))).toBe(true)
|
|
571
|
+
expect(existsSync(join(dest, 'tests', 'judge.mjs'))).toBe(false)
|
|
572
|
+
|
|
573
|
+
// Exactly one synthetic commit; not the real base sha; base ref pinned.
|
|
574
|
+
const log = await git(dest, 'log', '--oneline')
|
|
575
|
+
expect(log.split('\n')).toHaveLength(1)
|
|
576
|
+
expect(syntheticBase).not.toBe(goodInst.base_commit)
|
|
577
|
+
expect(await git(dest, 'rev-parse', FACTORY_BASE_REF)).toBe(syntheticBase)
|
|
578
|
+
|
|
579
|
+
// No remotes, and the real repo's objects are unreachable.
|
|
580
|
+
expect(await git(dest, 'remote')).toBe('')
|
|
581
|
+
const baseObj = await run('git', ['-C', dest, 'cat-file', '-e', goodInst.base_commit])
|
|
582
|
+
expect(baseObj.code).not.toBe(0)
|
|
583
|
+
const judgeObj = await run('git', ['-C', dest, 'cat-file', '-e', goodInst.judge_ref])
|
|
584
|
+
expect(judgeObj.code).not.toBe(0)
|
|
585
|
+
|
|
586
|
+
// Leak grep: neither the real shas nor the mirror path appear anywhere in
|
|
587
|
+
// the workspace (including .git internals).
|
|
588
|
+
for (const needle of [goodInst.base_commit, goodInst.judge_ref, goodInst.repo_local_mirror]) {
|
|
589
|
+
const grep = await run('bash', ['-c', `grep -rF ${JSON.stringify(needle)} ${JSON.stringify(dest)}`])
|
|
590
|
+
expect(grep.stdout).toBe('')
|
|
591
|
+
expect(grep.code).not.toBe(0)
|
|
592
|
+
}
|
|
593
|
+
})
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
// ---------------------------------------------------------------------------
|
|
597
|
+
// Calibration admission gate — both rejection directions.
|
|
598
|
+
// ---------------------------------------------------------------------------
|
|
599
|
+
|
|
600
|
+
describe('calibrateFactoryInstance', () => {
|
|
601
|
+
it('admits a well-formed instance (gold passes, base fails)', async () => {
|
|
602
|
+
const r = await calibrateFactoryInstance(goodInst)
|
|
603
|
+
expect(r).toMatchObject({
|
|
604
|
+
goldResolved: true,
|
|
605
|
+
goldPassed: 2,
|
|
606
|
+
baseResolved: false,
|
|
607
|
+
basePassed: 0,
|
|
608
|
+
total: 2,
|
|
609
|
+
admitted: true,
|
|
610
|
+
})
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
it('rejects when the base already passes (judge has no signal)', async () => {
|
|
614
|
+
const inst = loadFactoryInstance(await makeInstanceDir(root, 'trivial', mirror, mirror.refs.trivial))
|
|
615
|
+
const r = await calibrateFactoryInstance(inst)
|
|
616
|
+
expect(r.baseResolved).toBe(true)
|
|
617
|
+
expect(r.admitted).toBe(false)
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
it('rejects when gold cannot pass its own judge', async () => {
|
|
621
|
+
const inst = loadFactoryInstance(await makeInstanceDir(root, 'too-hard', mirror, mirror.refs.tooHard))
|
|
622
|
+
const r = await calibrateFactoryInstance(inst)
|
|
623
|
+
expect(r.goldResolved).toBe(false)
|
|
624
|
+
expect(r.admitted).toBe(false)
|
|
625
|
+
})
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
// ---------------------------------------------------------------------------
|
|
629
|
+
// Ledger resume keys.
|
|
630
|
+
// ---------------------------------------------------------------------------
|
|
631
|
+
|
|
632
|
+
describe('factoryLedgerKeys', () => {
|
|
633
|
+
it('keys rows by iid and rep, and fails loud on corrupt lines', async () => {
|
|
634
|
+
const ledger = join(root, 'ledger.jsonl')
|
|
635
|
+
await writeFile(
|
|
636
|
+
ledger,
|
|
637
|
+
JSON.stringify({ iid: 'factory.x.1', rep: 0 }) + '\n' + JSON.stringify({ iid: 'factory.x.1', rep: 1 }) + '\n',
|
|
638
|
+
)
|
|
639
|
+
expect(await factoryLedgerKeys(ledger)).toEqual(new Set(['factory.x.1#r0', 'factory.x.1#r1']))
|
|
640
|
+
await writeFile(ledger, '{corrupt\n')
|
|
641
|
+
await expect(factoryLedgerKeys(ledger)).rejects.toThrow(/corrupt/)
|
|
642
|
+
})
|
|
643
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Calibration — factory.agent-eval.309
|
|
2
|
+
|
|
3
|
+
Run 2026-07-23, node v24.13.0, pnpm 11.15.1, local mirror `/home/drew/code/agent-eval` (read-only; work in throwaway clones).
|
|
4
|
+
|
|
5
|
+
## Gold run — base + real PR impl (tests excluded) must PASS
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
git clone /home/drew/code/agent-eval work/ae-309-gold
|
|
9
|
+
git -C work/ae-309-gold checkout 5fe8d0c83a7e38f2893b2d7a0f6cd62dd5430521
|
|
10
|
+
# impl-only patch: full PR diff minus its test files
|
|
11
|
+
git -C /home/drew/code/agent-eval diff 5fe8d0c8 9b0a4f82 \
|
|
12
|
+
-- ':(exclude)src/capability-headroom.test.ts' ':(exclude)src/paired-arms.test.ts' \
|
|
13
|
+
> ae309-impl.patch
|
|
14
|
+
git -C work/ae-309-gold apply ae309-impl.patch
|
|
15
|
+
# judge tests overlaid from the merge commit
|
|
16
|
+
git -C /home/drew/code/agent-eval show 9b0a4f82:src/paired-arms.test.ts > work/ae-309-gold/src/paired-arms.test.ts
|
|
17
|
+
git -C /home/drew/code/agent-eval show 9b0a4f82:src/capability-headroom.test.ts > work/ae-309-gold/src/capability-headroom.test.ts
|
|
18
|
+
cd work/ae-309-gold && pnpm install
|
|
19
|
+
npx vitest run src/paired-arms.test.ts src/capability-headroom.test.ts
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Output:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
✓ src/capability-headroom.test.ts (13 tests) 4ms
|
|
26
|
+
✓ src/paired-arms.test.ts (17 tests) 12ms
|
|
27
|
+
Test Files 2 passed (2)
|
|
28
|
+
Tests 30 passed (30)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Re-run (flakiness check): `PASS (30) FAIL (0)` — deterministic (seeded bootstrap; no wall-clock/net/fs dependence).
|
|
32
|
+
|
|
33
|
+
## Base run — judge tests on bare base must FAIL
|
|
34
|
+
|
|
35
|
+
Same clone/checkout of `5fe8d0c8`, overlay only the two test files, `pnpm install`, same vitest command.
|
|
36
|
+
|
|
37
|
+
Output:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
Test Files 2 failed (2)
|
|
41
|
+
Tests no tests
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Both files fail at collection: `src/paired-arms.ts` and `src/capability-headroom.ts` do not exist at base. Producing those modules is the task.
|
|
45
|
+
|
|
46
|
+
## Judge-set notes
|
|
47
|
+
|
|
48
|
+
- No flaky or env-dependent tests found; both files are pure in-process assertions (0 spawns, 0 network, 0 env keys). **Excluded: none.**
|
|
49
|
+
- The tests pin exact error-message substrings (validation regexes). Rather than trimming, those substrings were promoted into `spec.md` as explicit acceptance criteria — they are fail-loud contract, not incidental internals.
|
|
50
|
+
- Tests import by public module path (`./paired-arms`, `./capability-headroom`) and use `mcnemar` from the pre-existing `src/statistics` — the builder must produce those module paths; that IS the spec.
|
|
51
|
+
- Vitest config at base has no `include` restriction; explicit file args run colocated `src/*.test.ts` fine.
|