@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.
Files changed (265) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/HARNESS.md +43 -0
  3. package/dist/adapters.js +24 -24
  4. package/dist/benchmarks/_harness.d.ts +1 -1
  5. package/dist/benchmarks/_harness.js +1 -1
  6. package/dist/benchmarks/aec-bench.js +2 -2
  7. package/dist/benchmarks/agentbench.js +2 -2
  8. package/dist/benchmarks/appworld.js +2 -2
  9. package/dist/benchmarks/bfcl.js +2 -2
  10. package/dist/benchmarks/commit0.js +2 -2
  11. package/dist/benchmarks/crag.js +2 -2
  12. package/dist/benchmarks/dabstep.js +2 -2
  13. package/dist/benchmarks/enterpriseops-gym.js +2 -2
  14. package/dist/benchmarks/finresearchbench.js +2 -2
  15. package/dist/benchmarks/humaneval.d.ts +10 -1
  16. package/dist/benchmarks/humaneval.js +5 -3
  17. package/dist/benchmarks/nomiracl.js +2 -2
  18. package/dist/benchmarks/open-rag-bench.js +2 -2
  19. package/dist/benchmarks/programbench.js +2 -2
  20. package/dist/benchmarks/ragbench.js +2 -2
  21. package/dist/benchmarks/swe-bench.js +2 -2
  22. package/dist/benchmarks/t2-ragbench.js +2 -2
  23. package/dist/benchmarks/tau-bench-shared.js +2 -2
  24. package/dist/benchmarks/tau2-bench.js +3 -3
  25. package/dist/benchmarks/tau3-banking.js +3 -3
  26. package/dist/benchmarks/terminal-bench.js +2 -2
  27. package/dist/benchmarks/toollm.js +2 -2
  28. package/dist/benchmarks/webarena-verified.js +2 -2
  29. package/dist/{chunk-CKUVRZ2T.js → chunk-3U5TXJZS.js} +2 -2
  30. package/dist/{chunk-PPYSEKFM.js → chunk-5H5XV76F.js} +73 -15
  31. package/dist/chunk-5H5XV76F.js.map +1 -0
  32. package/dist/{chunk-YCGY7UIZ.js → chunk-7GRVHU22.js} +2 -2
  33. package/dist/{chunk-Z7ML6L77.js → chunk-HWST3SED.js} +2 -2
  34. package/dist/{chunk-SYDW647C.js → chunk-IA2FBTWC.js} +2 -2
  35. package/dist/{chunk-R67DFVLO.js → chunk-IFVINJ4B.js} +2 -2
  36. package/dist/{chunk-R36V2VP7.js → chunk-IZ5M6OAC.js} +2 -2
  37. package/dist/{chunk-ODT47UAY.js → chunk-K3BQGZCT.js} +2 -2
  38. package/dist/{chunk-IFAV6KEM.js → chunk-KP5KD6EN.js} +2 -2
  39. package/dist/{chunk-ZEWMTR5M.js → chunk-MQMRLGOG.js} +2 -2
  40. package/dist/{chunk-TSWPNOYM.js → chunk-NQG5XDSB.js} +2 -2
  41. package/dist/{chunk-7WSD27QQ.js → chunk-PB64GYIG.js} +2 -2
  42. package/dist/{chunk-HBSWHQNJ.js → chunk-RCYQEFNX.js} +3 -3
  43. package/dist/{chunk-J3KDJNX2.js → chunk-RH5F53JT.js} +2 -2
  44. package/dist/{chunk-UAIOHCUK.js → chunk-SFLA7OH3.js} +3 -3
  45. package/dist/{chunk-Y6O2OCUO.js → chunk-SHM6MRRF.js} +2 -2
  46. package/dist/{chunk-KDIKRJGB.js → chunk-SHYIRB7I.js} +2 -2
  47. package/dist/{chunk-HHXFIHXC.js → chunk-SVR2LKYI.js} +2 -2
  48. package/dist/{chunk-5SBJCB6W.js → chunk-V7AEBY6U.js} +22 -22
  49. package/dist/{chunk-LRRD7NAG.js → chunk-WSKWVEQB.js} +18 -2
  50. package/dist/chunk-WSKWVEQB.js.map +1 -0
  51. package/dist/{chunk-2PVVP7GN.js → chunk-XKEFIFIC.js} +2 -2
  52. package/dist/{chunk-JRWWGMK7.js → chunk-XYA4XSNU.js} +2 -2
  53. package/dist/{chunk-X5YKXC6V.js → chunk-YSMEKBTD.js} +2 -2
  54. package/dist/{chunk-2XU6OGEN.js → chunk-Z4TZ76N7.js} +2 -2
  55. package/dist/index.js +24 -24
  56. package/package.json +6 -5
  57. package/scripts/run-package-tests.mjs +30 -8
  58. package/scripts/verify-packed-consumer.mjs +1 -1
  59. package/scripts/verify-pier-agent.mts +1 -0
  60. package/src/benchmarks/_harness.ts +20 -2
  61. package/src/benchmarks/humaneval.test.mts +122 -0
  62. package/src/benchmarks/humaneval.ts +100 -27
  63. package/src/david-attribution.mts +78 -0
  64. package/src/david-goliath.mts +149 -0
  65. package/src/hev-improve.mts +25 -6
  66. package/src/humaneval-object-ablation.mts +201 -0
  67. package/src/live-improve-campaign-mbpp.mts +641 -0
  68. package/src/live-improve-campaign.mts +500 -0
  69. package/src/mbpp-structural.mts +12 -7
  70. package/src/quant-arena/README.md +144 -0
  71. package/src/quant-arena/backtest.test.mts +135 -0
  72. package/src/quant-arena/backtest.ts +218 -0
  73. package/src/quant-arena/data.test.mts +44 -0
  74. package/src/quant-arena/data.ts +141 -0
  75. package/src/quant-arena/driver.test.mts +253 -0
  76. package/src/quant-arena/driver.ts +219 -0
  77. package/src/quant-arena/fixtures/data/PROVENANCE.md +26 -0
  78. package/src/quant-arena/fixtures/data/holdout/IDX.csv +523 -0
  79. package/src/quant-arena/fixtures/data/holdout/S01.csv +523 -0
  80. package/src/quant-arena/fixtures/data/holdout/S02.csv +523 -0
  81. package/src/quant-arena/fixtures/data/holdout/S03.csv +523 -0
  82. package/src/quant-arena/fixtures/data/holdout/S04.csv +523 -0
  83. package/src/quant-arena/fixtures/data/holdout/S05.csv +523 -0
  84. package/src/quant-arena/fixtures/data/holdout/S06.csv +523 -0
  85. package/src/quant-arena/fixtures/data/holdout/S07.csv +523 -0
  86. package/src/quant-arena/fixtures/data/holdout/S08.csv +523 -0
  87. package/src/quant-arena/fixtures/data/holdout/S09.csv +523 -0
  88. package/src/quant-arena/fixtures/data/holdout/S10.csv +523 -0
  89. package/src/quant-arena/fixtures/data/insample/IDX.csv +2087 -0
  90. package/src/quant-arena/fixtures/data/insample/S01.csv +2087 -0
  91. package/src/quant-arena/fixtures/data/insample/S02.csv +2087 -0
  92. package/src/quant-arena/fixtures/data/insample/S03.csv +2087 -0
  93. package/src/quant-arena/fixtures/data/insample/S04.csv +2087 -0
  94. package/src/quant-arena/fixtures/data/insample/S05.csv +2087 -0
  95. package/src/quant-arena/fixtures/data/insample/S06.csv +2087 -0
  96. package/src/quant-arena/fixtures/data/insample/S07.csv +2087 -0
  97. package/src/quant-arena/fixtures/data/insample/S08.csv +2087 -0
  98. package/src/quant-arena/fixtures/data/insample/S09.csv +2087 -0
  99. package/src/quant-arena/fixtures/data/insample/S10.csv +2087 -0
  100. package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +16 -0
  101. package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +5 -0
  102. package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +171 -0
  103. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +119 -0
  104. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +119 -0
  105. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +105 -0
  106. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +102 -0
  107. package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +4 -0
  108. package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +2 -0
  109. package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +84 -0
  110. package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +117 -0
  111. package/src/quant-arena/holdout-certify.mts +206 -0
  112. package/src/quant-arena/holdout-certify.test.mts +82 -0
  113. package/src/quant-arena/leak-audit.test.mts +79 -0
  114. package/src/quant-arena/leak-audit.ts +95 -0
  115. package/src/quant-arena/make-fixtures.mts +161 -0
  116. package/src/quant-arena/multiplicity.test.mts +68 -0
  117. package/src/quant-arena/multiplicity.ts +87 -0
  118. package/src/quant-arena/nautilus-certify.ts +31 -0
  119. package/src/quant-arena/oms.ts +90 -0
  120. package/src/quant-arena/profiles/quant-researcher.profile.json +7 -0
  121. package/src/quant-arena/python/pyproject.toml +8 -0
  122. package/src/quant-arena/python/uv.lock +1297 -0
  123. package/src/quant-arena/python/vbt-worker.py +192 -0
  124. package/src/quant-arena/quant-loop.mts +813 -0
  125. package/src/quant-arena/quant-loop.test.mts +75 -0
  126. package/src/quant-arena/strategies/buy-hold-index/strategy.ts +11 -0
  127. package/src/quant-arena/strategies/equal-weight/strategy.ts +20 -0
  128. package/src/quant-arena/strategies/sma-crossover/strategy.ts +42 -0
  129. package/src/quant-arena/types.ts +133 -0
  130. package/src/quant-arena/vbt-client.ts +321 -0
  131. package/src/quant-arena/vbt-parity.test.mts +183 -0
  132. package/src/quant-arena/windows.test.mts +45 -0
  133. package/src/quant-arena/windows.ts +54 -0
  134. package/src/rollout-ledger/backfill-swe-arena.mts +606 -0
  135. package/src/rollout-ledger/backfill-swe-arena.test.mts +338 -0
  136. package/src/rollout-ledger/settle-capture.mts +442 -0
  137. package/src/rollout-ledger/settle-capture.test.mts +270 -0
  138. package/src/stream-observe.py +45 -0
  139. package/src/stream-observe.tpl.html +247 -0
  140. package/src/supervisor-arena.mts +816 -0
  141. package/src/swe-arena/activation.mts +228 -0
  142. package/src/swe-arena/activation.test.mts +303 -0
  143. package/src/swe-arena/analyze.ts +211 -0
  144. package/src/swe-arena/arms.ts +804 -0
  145. package/src/swe-arena/bootstrap-meta.mts +188 -0
  146. package/src/swe-arena/bootstrap-meta.test.mts +51 -0
  147. package/src/swe-arena/briefing.mts +217 -0
  148. package/src/swe-arena/briefing.test.mts +178 -0
  149. package/src/swe-arena/calibrate.ts +217 -0
  150. package/src/swe-arena/capabilities.mts +76 -0
  151. package/src/swe-arena/capabilities.test.mts +57 -0
  152. package/src/swe-arena/capacity.ts +194 -0
  153. package/src/swe-arena/cell-evidence.mts +437 -0
  154. package/src/swe-arena/cell-evidence.test.mts +248 -0
  155. package/src/swe-arena/diagnosis-ensemble.test.mts +210 -0
  156. package/src/swe-arena/diagnosis-ensemble.ts +520 -0
  157. package/src/swe-arena/execution.test.mts +1170 -0
  158. package/src/swe-arena/factory-command-container.ts +284 -0
  159. package/src/swe-arena/factory-judge-child.mts +228 -0
  160. package/src/swe-arena/factory.test.mts +643 -0
  161. package/src/swe-arena/fixtures/analyze.py +80 -0
  162. package/src/swe-arena/fixtures/excludes.txt +8 -0
  163. package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +51 -0
  164. package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +29 -0
  165. package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +64 -0
  166. package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +48 -0
  167. package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +29 -0
  168. package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +48 -0
  169. package/src/swe-arena/fixtures/factory/loops-28/calibration.md +47 -0
  170. package/src/swe-arena/fixtures/factory/loops-28/manifest.json +30 -0
  171. package/src/swe-arena/fixtures/factory/loops-28/spec.md +50 -0
  172. package/src/swe-arena/fixtures/gen1-salvage/README.md +45 -0
  173. package/src/swe-arena/fixtures/gen1-salvage/cand0-e6d7361.diff +116 -0
  174. package/src/swe-arena/fixtures/gen1-salvage/cand1-76a8590.diff +293 -0
  175. package/src/swe-arena/fixtures/holdout-preregister.log +12 -0
  176. package/src/swe-arena/fixtures/holdout.json +44 -0
  177. package/src/swe-arena/fixtures/instances.json +146 -0
  178. package/src/swe-arena/fixtures/ledger.jsonl +12 -0
  179. package/src/swe-arena/fixtures/patches/pallets__flask-5014.solo.patch +36 -0
  180. package/src/swe-arena/fixtures/patches/pydata__xarray-4687.sup.patch +33 -0
  181. package/src/swe-arena/fixtures/rejudge.jsonl +15 -0
  182. package/src/swe-arena/fixtures/rematch.jsonl +3 -0
  183. package/src/swe-arena/fixtures/rematch2.jsonl +3 -0
  184. package/src/swe-arena/fixtures/rematch3.jsonl +3 -0
  185. package/src/swe-arena/fixtures/run-report/README.md +43 -0
  186. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +173 -0
  187. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +100 -0
  188. package/src/swe-arena/fixtures/run-report/gen3-rollup.json +551 -0
  189. package/src/swe-arena/fixtures/run-report/gen3-rollup.md +64 -0
  190. package/src/swe-arena/fixtures/sup-journal-true.json +19 -0
  191. package/src/swe-arena/fixtures/verify/astropy__astropy-13033.sh +48 -0
  192. package/src/swe-arena/fixtures/verify/django__django-11532.sh +50 -0
  193. package/src/swe-arena/fixtures/verify/matplotlib__matplotlib-20826.sh +76 -0
  194. package/src/swe-arena/fixtures/verify/pydata__xarray-4687.sh +44 -0
  195. package/src/swe-arena/fixtures/verify/pytest-dev__pytest-6197.sh +32 -0
  196. package/src/swe-arena/fixtures/verify/sphinx-doc__sphinx-9658.sh +51 -0
  197. package/src/swe-arena/fixtures/worker-tokens.json +42 -0
  198. package/src/swe-arena/fixtures.ts +237 -0
  199. package/src/swe-arena/gepa-seat.mts +583 -0
  200. package/src/swe-arena/gepa-seat.test.mts +635 -0
  201. package/src/swe-arena/holdout-certify.mts +408 -0
  202. package/src/swe-arena/holdout-certify.test.mts +160 -0
  203. package/src/swe-arena/judge-child.mts +37 -0
  204. package/src/swe-arena/ledger-orphans.mts +77 -0
  205. package/src/swe-arena/ledger-orphans.test.mts +147 -0
  206. package/src/swe-arena/lineage-record.mts +164 -0
  207. package/src/swe-arena/lineage-record.test.mts +115 -0
  208. package/src/swe-arena/manifest.mts +293 -0
  209. package/src/swe-arena/manifest.test.mts +169 -0
  210. package/src/swe-arena/materialize.ts +142 -0
  211. package/src/swe-arena/outer-loop.mts +2795 -0
  212. package/src/swe-arena/outer-loop.test.mts +696 -0
  213. package/src/swe-arena/parity.test.mts +87 -0
  214. package/src/swe-arena/premeasured-from-cells.mts +281 -0
  215. package/src/swe-arena/premeasured-from-cells.test.mts +180 -0
  216. package/src/swe-arena/proc.test.mts +174 -0
  217. package/src/swe-arena/proc.ts +260 -0
  218. package/src/swe-arena/profiles/default-author.profile.json +4 -0
  219. package/src/swe-arena/proposer-fanout.mts +770 -0
  220. package/src/swe-arena/proposer-fanout.test.mts +619 -0
  221. package/src/swe-arena/proposer-provenance.mts +177 -0
  222. package/src/swe-arena/proposer-provenance.test.mts +106 -0
  223. package/src/swe-arena/reconcile.ts +0 -0
  224. package/src/swe-arena/replay.mts +183 -0
  225. package/src/swe-arena/replay.test.mts +300 -0
  226. package/src/swe-arena/run-experiment.mts +727 -0
  227. package/src/swe-arena/run-report.mts +75 -0
  228. package/src/swe-arena/run-supervisor.mjs +297 -0
  229. package/src/swe-arena/run-supervisor.test.mts +500 -0
  230. package/src/swe-arena/score-split.mts +140 -0
  231. package/src/swe-arena/score-split.test.mts +123 -0
  232. package/src/swe-arena/serialized-judge.ts +414 -0
  233. package/src/swe-arena/types.ts +218 -0
  234. package/src/swe-code-improve.mts +328 -0
  235. package/src/swe-emit-patch.mts +104 -0
  236. package/src/swe-improve.mts +232 -0
  237. package/src/swe-jail.ts +2 -2
  238. package/src/swe-local-proof.mts +169 -0
  239. package/src/swe-repro-calibrate.mts +446 -0
  240. package/src/swe-stream.mts +1497 -0
  241. package/src/swe-structural.mts +245 -837
  242. package/dist/chunk-LRRD7NAG.js.map +0 -1
  243. package/dist/chunk-PPYSEKFM.js.map +0 -1
  244. /package/dist/{chunk-CKUVRZ2T.js.map → chunk-3U5TXJZS.js.map} +0 -0
  245. /package/dist/{chunk-YCGY7UIZ.js.map → chunk-7GRVHU22.js.map} +0 -0
  246. /package/dist/{chunk-Z7ML6L77.js.map → chunk-HWST3SED.js.map} +0 -0
  247. /package/dist/{chunk-SYDW647C.js.map → chunk-IA2FBTWC.js.map} +0 -0
  248. /package/dist/{chunk-R67DFVLO.js.map → chunk-IFVINJ4B.js.map} +0 -0
  249. /package/dist/{chunk-R36V2VP7.js.map → chunk-IZ5M6OAC.js.map} +0 -0
  250. /package/dist/{chunk-ODT47UAY.js.map → chunk-K3BQGZCT.js.map} +0 -0
  251. /package/dist/{chunk-IFAV6KEM.js.map → chunk-KP5KD6EN.js.map} +0 -0
  252. /package/dist/{chunk-ZEWMTR5M.js.map → chunk-MQMRLGOG.js.map} +0 -0
  253. /package/dist/{chunk-TSWPNOYM.js.map → chunk-NQG5XDSB.js.map} +0 -0
  254. /package/dist/{chunk-7WSD27QQ.js.map → chunk-PB64GYIG.js.map} +0 -0
  255. /package/dist/{chunk-HBSWHQNJ.js.map → chunk-RCYQEFNX.js.map} +0 -0
  256. /package/dist/{chunk-J3KDJNX2.js.map → chunk-RH5F53JT.js.map} +0 -0
  257. /package/dist/{chunk-UAIOHCUK.js.map → chunk-SFLA7OH3.js.map} +0 -0
  258. /package/dist/{chunk-Y6O2OCUO.js.map → chunk-SHM6MRRF.js.map} +0 -0
  259. /package/dist/{chunk-KDIKRJGB.js.map → chunk-SHYIRB7I.js.map} +0 -0
  260. /package/dist/{chunk-HHXFIHXC.js.map → chunk-SVR2LKYI.js.map} +0 -0
  261. /package/dist/{chunk-5SBJCB6W.js.map → chunk-V7AEBY6U.js.map} +0 -0
  262. /package/dist/{chunk-2PVVP7GN.js.map → chunk-XKEFIFIC.js.map} +0 -0
  263. /package/dist/{chunk-JRWWGMK7.js.map → chunk-XYA4XSNU.js.map} +0 -0
  264. /package/dist/{chunk-X5YKXC6V.js.map → chunk-YSMEKBTD.js.map} +0 -0
  265. /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
+ })