akm-cli 0.7.0 → 0.7.2

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 (332) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/{src/cli.js → cli.js} +22 -8
  3. package/dist/{src/commands → commands}/installed-stashes.js +1 -1
  4. package/dist/{src/commands → commands}/source-add.js +1 -1
  5. package/dist/{src/core → core}/common.js +16 -1
  6. package/dist/{src/core → core}/config.js +5 -2
  7. package/dist/{src/indexer → indexer}/db-search.js +16 -1
  8. package/dist/{src/indexer → indexer}/graph-extraction.js +5 -3
  9. package/dist/{src/indexer → indexer}/indexer.js +27 -11
  10. package/dist/{src/indexer → indexer}/memory-inference.js +47 -58
  11. package/dist/{src/indexer → indexer}/search-source.js +1 -1
  12. package/dist/{src/llm → llm}/client.js +61 -1
  13. package/dist/{src/llm → llm}/embedder.js +8 -5
  14. package/dist/{src/llm → llm}/embedders/local.js +8 -2
  15. package/dist/{src/llm → llm}/embedders/remote.js +4 -2
  16. package/dist/{src/llm → llm}/graph-extract.js +4 -4
  17. package/dist/llm/memory-infer.js +114 -0
  18. package/dist/{src/llm → llm}/metadata-enhance.js +2 -2
  19. package/dist/{src/output → output}/cli-hints.js +2 -0
  20. package/dist/{src/setup → setup}/setup.js +30 -20
  21. package/dist/sources/providers/website.js +27 -0
  22. package/dist/{src/sources/providers/website.js → sources/website-ingest.js} +38 -51
  23. package/docs/README.md +7 -0
  24. package/docs/migration/release-notes/0.7.0.md +14 -0
  25. package/package.json +11 -8
  26. package/dist/src/llm/memory-infer.js +0 -86
  27. package/dist/tests/add-website-source.test.js +0 -119
  28. package/dist/tests/agent/agent-config-loader.test.js +0 -70
  29. package/dist/tests/agent/agent-config.test.js +0 -221
  30. package/dist/tests/agent/agent-detect.test.js +0 -100
  31. package/dist/tests/agent/agent-spawn.test.js +0 -234
  32. package/dist/tests/agent-output.test.js +0 -186
  33. package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +0 -103
  34. package/dist/tests/architecture/agent-spawn-seam.test.js +0 -193
  35. package/dist/tests/architecture/llm-stateless-seam.test.js +0 -112
  36. package/dist/tests/asset-ref.test.js +0 -192
  37. package/dist/tests/asset-registry.test.js +0 -103
  38. package/dist/tests/asset-spec.test.js +0 -241
  39. package/dist/tests/bench/attribution.test.js +0 -996
  40. package/dist/tests/bench/cleanup-sigint.test.js +0 -83
  41. package/dist/tests/bench/cleanup.js +0 -234
  42. package/dist/tests/bench/cleanup.test.js +0 -166
  43. package/dist/tests/bench/cli.js +0 -1018
  44. package/dist/tests/bench/cli.test.js +0 -445
  45. package/dist/tests/bench/compare.test.js +0 -556
  46. package/dist/tests/bench/corpus.js +0 -317
  47. package/dist/tests/bench/corpus.test.js +0 -258
  48. package/dist/tests/bench/doctor.js +0 -525
  49. package/dist/tests/bench/driver.js +0 -401
  50. package/dist/tests/bench/driver.test.js +0 -584
  51. package/dist/tests/bench/environment.js +0 -233
  52. package/dist/tests/bench/environment.test.js +0 -199
  53. package/dist/tests/bench/evolve-metrics.js +0 -179
  54. package/dist/tests/bench/evolve-metrics.test.js +0 -187
  55. package/dist/tests/bench/evolve.js +0 -647
  56. package/dist/tests/bench/evolve.test.js +0 -624
  57. package/dist/tests/bench/failure-modes.test.js +0 -349
  58. package/dist/tests/bench/feedback-integrity.test.js +0 -457
  59. package/dist/tests/bench/leakage.test.js +0 -228
  60. package/dist/tests/bench/learning-curve.test.js +0 -134
  61. package/dist/tests/bench/metrics.js +0 -2395
  62. package/dist/tests/bench/metrics.test.js +0 -1150
  63. package/dist/tests/bench/no-os-tmpdir-invariant.test.js +0 -43
  64. package/dist/tests/bench/opencode-config.js +0 -194
  65. package/dist/tests/bench/opencode-config.test.js +0 -370
  66. package/dist/tests/bench/report.js +0 -1885
  67. package/dist/tests/bench/report.test.js +0 -1038
  68. package/dist/tests/bench/run-config.js +0 -355
  69. package/dist/tests/bench/run-config.test.js +0 -298
  70. package/dist/tests/bench/run-curate-test.js +0 -32
  71. package/dist/tests/bench/run-failing-tasks.js +0 -56
  72. package/dist/tests/bench/run-full-bench.js +0 -51
  73. package/dist/tests/bench/run-items36-targeted.js +0 -69
  74. package/dist/tests/bench/run-nano-quick.js +0 -42
  75. package/dist/tests/bench/run-waveg-targeted.js +0 -62
  76. package/dist/tests/bench/runner.js +0 -699
  77. package/dist/tests/bench/runner.test.js +0 -958
  78. package/dist/tests/bench/search-bridge.test.js +0 -331
  79. package/dist/tests/bench/tmp.js +0 -131
  80. package/dist/tests/bench/trajectory.js +0 -116
  81. package/dist/tests/bench/trajectory.test.js +0 -127
  82. package/dist/tests/bench/verifier.js +0 -114
  83. package/dist/tests/bench/verifier.test.js +0 -118
  84. package/dist/tests/bench/workflow-evaluator.js +0 -557
  85. package/dist/tests/bench/workflow-evaluator.test.js +0 -421
  86. package/dist/tests/bench/workflow-spec.js +0 -345
  87. package/dist/tests/bench/workflow-spec.test.js +0 -363
  88. package/dist/tests/bench/workflow-trace.js +0 -472
  89. package/dist/tests/bench/workflow-trace.test.js +0 -254
  90. package/dist/tests/benchmark-search-quality.js +0 -536
  91. package/dist/tests/benchmark-suite.js +0 -1441
  92. package/dist/tests/capture-cli.test.js +0 -112
  93. package/dist/tests/cli-errors.test.js +0 -204
  94. package/dist/tests/commands/events.test.js +0 -370
  95. package/dist/tests/commands/history.test.js +0 -418
  96. package/dist/tests/commands/import.test.js +0 -103
  97. package/dist/tests/commands/proposal-cli.test.js +0 -209
  98. package/dist/tests/commands/reflect-propose-cli.test.js +0 -333
  99. package/dist/tests/commands/remember.test.js +0 -97
  100. package/dist/tests/commands/scope-flags.test.js +0 -300
  101. package/dist/tests/commands/search.test.js +0 -537
  102. package/dist/tests/commands/show-indexer-parity.test.js +0 -117
  103. package/dist/tests/commands/show.test.js +0 -294
  104. package/dist/tests/common.test.js +0 -266
  105. package/dist/tests/completions.test.js +0 -142
  106. package/dist/tests/config-cli.test.js +0 -193
  107. package/dist/tests/config-llm-features.test.js +0 -139
  108. package/dist/tests/config.test.js +0 -569
  109. package/dist/tests/contracts/migration-baseline.test.js +0 -43
  110. package/dist/tests/contracts/reflect-propose-envelope.test.js +0 -139
  111. package/dist/tests/contracts/spec-helpers.js +0 -46
  112. package/dist/tests/contracts/v1-spec-section-11-proposal-queue.test.js +0 -228
  113. package/dist/tests/contracts/v1-spec-section-12-agent-config.test.js +0 -56
  114. package/dist/tests/contracts/v1-spec-section-13-lesson-type.test.js +0 -34
  115. package/dist/tests/contracts/v1-spec-section-14-llm-features.test.js +0 -94
  116. package/dist/tests/contracts/v1-spec-section-4-1-asset-types.test.js +0 -39
  117. package/dist/tests/contracts/v1-spec-section-4-2-quality-rules.test.js +0 -44
  118. package/dist/tests/contracts/v1-spec-section-5-configuration.test.js +0 -47
  119. package/dist/tests/contracts/v1-spec-section-6-orchestration.test.js +0 -40
  120. package/dist/tests/contracts/v1-spec-section-7-module-layout.test.js +0 -58
  121. package/dist/tests/contracts/v1-spec-section-8-extension-points.test.js +0 -34
  122. package/dist/tests/contracts/v1-spec-section-9-4-cli-surface.test.js +0 -75
  123. package/dist/tests/contracts/v1-spec-section-9-7-llm-agent-boundary.test.js +0 -36
  124. package/dist/tests/core/write-source.test.js +0 -366
  125. package/dist/tests/curate-command.test.js +0 -87
  126. package/dist/tests/db-scoring.test.js +0 -201
  127. package/dist/tests/db.test.js +0 -654
  128. package/dist/tests/distill-cli-flag.test.js +0 -208
  129. package/dist/tests/distill.test.js +0 -515
  130. package/dist/tests/docker-install.test.js +0 -120
  131. package/dist/tests/e2e.test.js +0 -1419
  132. package/dist/tests/embedder.test.js +0 -340
  133. package/dist/tests/embedding-model-config.test.js +0 -379
  134. package/dist/tests/feedback-command.test.js +0 -172
  135. package/dist/tests/file-context.test.js +0 -552
  136. package/dist/tests/fixtures/scripts/git/summarize-diff.js +0 -9
  137. package/dist/tests/fixtures/scripts/lint/eslint-check.js +0 -7
  138. package/dist/tests/fixtures/stashes/load.js +0 -166
  139. package/dist/tests/fixtures/stashes/load.test.js +0 -97
  140. package/dist/tests/fixtures/stashes/ranking-baseline/scripts/mem0-search.js +0 -12
  141. package/dist/tests/frontmatter.test.js +0 -190
  142. package/dist/tests/fts-field-weighting.test.js +0 -254
  143. package/dist/tests/fuzzy-search.test.js +0 -230
  144. package/dist/tests/git-provider-clone.test.js +0 -45
  145. package/dist/tests/github.test.js +0 -161
  146. package/dist/tests/graph-boost-ranking.test.js +0 -305
  147. package/dist/tests/graph-extraction.test.js +0 -282
  148. package/dist/tests/helpers/usage-events.js +0 -8
  149. package/dist/tests/index-pass-llm.test.js +0 -161
  150. package/dist/tests/indexer.test.js +0 -570
  151. package/dist/tests/info-command.test.js +0 -166
  152. package/dist/tests/init.test.js +0 -69
  153. package/dist/tests/install-script.test.js +0 -246
  154. package/dist/tests/integration/agent-real-profile.test.js +0 -94
  155. package/dist/tests/issue-36-repro.test.js +0 -304
  156. package/dist/tests/issues-191-194.test.js +0 -160
  157. package/dist/tests/lesson-lint.test.js +0 -111
  158. package/dist/tests/llm-client.test.js +0 -115
  159. package/dist/tests/llm-feature-gate.test.js +0 -151
  160. package/dist/tests/llm.test.js +0 -139
  161. package/dist/tests/lockfile.test.js +0 -216
  162. package/dist/tests/manifest.test.js +0 -205
  163. package/dist/tests/markdown.test.js +0 -126
  164. package/dist/tests/matchers-unit.test.js +0 -189
  165. package/dist/tests/memory-inference.test.js +0 -299
  166. package/dist/tests/merge-scoring.test.js +0 -136
  167. package/dist/tests/metadata.test.js +0 -313
  168. package/dist/tests/migration-help.test.js +0 -89
  169. package/dist/tests/origin-resolve.test.js +0 -124
  170. package/dist/tests/output-baseline.test.js +0 -218
  171. package/dist/tests/output-shapes-unit.test.js +0 -478
  172. package/dist/tests/parallel-search.test.js +0 -272
  173. package/dist/tests/parameter-metadata.test.js +0 -365
  174. package/dist/tests/paths.test.js +0 -177
  175. package/dist/tests/progressive-disclosure.test.js +0 -280
  176. package/dist/tests/proposals.test.js +0 -279
  177. package/dist/tests/proposed-quality.test.js +0 -271
  178. package/dist/tests/provider-registry.test.js +0 -32
  179. package/dist/tests/ranking-regression.test.js +0 -548
  180. package/dist/tests/reflect-propose.test.js +0 -455
  181. package/dist/tests/registry-build-index.test.js +0 -394
  182. package/dist/tests/registry-cli.test.js +0 -290
  183. package/dist/tests/registry-index-v2.test.js +0 -430
  184. package/dist/tests/registry-install.test.js +0 -728
  185. package/dist/tests/registry-providers/parity.test.js +0 -189
  186. package/dist/tests/registry-providers/skills-sh.test.js +0 -309
  187. package/dist/tests/registry-providers/static-index.test.js +0 -238
  188. package/dist/tests/registry-resolve.test.js +0 -126
  189. package/dist/tests/registry-search.test.js +0 -923
  190. package/dist/tests/remember-frontmatter.test.js +0 -378
  191. package/dist/tests/remember-unit.test.js +0 -123
  192. package/dist/tests/ripgrep-install.test.js +0 -251
  193. package/dist/tests/ripgrep-resolve.test.js +0 -108
  194. package/dist/tests/ripgrep.test.js +0 -163
  195. package/dist/tests/save-command.test.js +0 -94
  196. package/dist/tests/save-trust-qa-fixes.test.js +0 -270
  197. package/dist/tests/scoring-pipeline.test.js +0 -648
  198. package/dist/tests/search-include-proposed-cli.test.js +0 -118
  199. package/dist/tests/self-update.test.js +0 -442
  200. package/dist/tests/semantic-search-e2e.test.js +0 -512
  201. package/dist/tests/semantic-status.test.js +0 -471
  202. package/dist/tests/setup-run.integration.js +0 -877
  203. package/dist/tests/setup-wizard.test.js +0 -198
  204. package/dist/tests/setup.test.js +0 -131
  205. package/dist/tests/source-add.test.js +0 -11
  206. package/dist/tests/source-clone.test.js +0 -254
  207. package/dist/tests/source-manage.test.js +0 -366
  208. package/dist/tests/source-providers/filesystem.test.js +0 -82
  209. package/dist/tests/source-providers/git.test.js +0 -252
  210. package/dist/tests/source-providers/website.test.js +0 -128
  211. package/dist/tests/source-qa-fixes.test.js +0 -286
  212. package/dist/tests/source-registry.test.js +0 -350
  213. package/dist/tests/source-resolve.test.js +0 -100
  214. package/dist/tests/source-source.test.js +0 -281
  215. package/dist/tests/source.test.js +0 -533
  216. package/dist/tests/tar-utils-scan.test.js +0 -73
  217. package/dist/tests/toggle-components.test.js +0 -73
  218. package/dist/tests/usage-telemetry.test.js +0 -265
  219. package/dist/tests/utility-scoring.test.js +0 -558
  220. package/dist/tests/vault-load-error.test.js +0 -78
  221. package/dist/tests/vault-qa-fixes.test.js +0 -194
  222. package/dist/tests/vault.test.js +0 -429
  223. package/dist/tests/vector-search.test.js +0 -608
  224. package/dist/tests/walker.test.js +0 -252
  225. package/dist/tests/wave2-cluster-bc.test.js +0 -228
  226. package/dist/tests/wave2-cluster-d.test.js +0 -180
  227. package/dist/tests/wave2-cluster-e.test.js +0 -179
  228. package/dist/tests/wiki-qa-fixes.test.js +0 -270
  229. package/dist/tests/wiki.test.js +0 -529
  230. package/dist/tests/workflow-cli.test.js +0 -271
  231. package/dist/tests/workflow-markdown.test.js +0 -171
  232. package/dist/tests/workflow-path-escape.test.js +0 -132
  233. package/dist/tests/workflow-qa-fixes.test.js +0 -395
  234. package/dist/tests/workflows/indexer-rejection.test.js +0 -213
  235. /package/dist/{src/commands → commands}/completions.js +0 -0
  236. /package/dist/{src/commands → commands}/config-cli.js +0 -0
  237. /package/dist/{src/commands → commands}/curate.js +0 -0
  238. /package/dist/{src/commands → commands}/distill.js +0 -0
  239. /package/dist/{src/commands → commands}/events.js +0 -0
  240. /package/dist/{src/commands → commands}/history.js +0 -0
  241. /package/dist/{src/commands → commands}/info.js +0 -0
  242. /package/dist/{src/commands → commands}/init.js +0 -0
  243. /package/dist/{src/commands → commands}/install-audit.js +0 -0
  244. /package/dist/{src/commands → commands}/migration-help.js +0 -0
  245. /package/dist/{src/commands → commands}/proposal.js +0 -0
  246. /package/dist/{src/commands → commands}/propose.js +0 -0
  247. /package/dist/{src/commands → commands}/reflect.js +0 -0
  248. /package/dist/{src/commands → commands}/registry-search.js +0 -0
  249. /package/dist/{src/commands → commands}/remember.js +0 -0
  250. /package/dist/{src/commands → commands}/search.js +0 -0
  251. /package/dist/{src/commands → commands}/self-update.js +0 -0
  252. /package/dist/{src/commands → commands}/show.js +0 -0
  253. /package/dist/{src/commands → commands}/source-clone.js +0 -0
  254. /package/dist/{src/commands → commands}/source-manage.js +0 -0
  255. /package/dist/{src/commands → commands}/vault.js +0 -0
  256. /package/dist/{src/core → core}/asset-ref.js +0 -0
  257. /package/dist/{src/core → core}/asset-registry.js +0 -0
  258. /package/dist/{src/core → core}/asset-spec.js +0 -0
  259. /package/dist/{src/core → core}/errors.js +0 -0
  260. /package/dist/{src/core → core}/events.js +0 -0
  261. /package/dist/{src/core → core}/frontmatter.js +0 -0
  262. /package/dist/{src/core → core}/lesson-lint.js +0 -0
  263. /package/dist/{src/core → core}/markdown.js +0 -0
  264. /package/dist/{src/core → core}/paths.js +0 -0
  265. /package/dist/{src/core → core}/proposals.js +0 -0
  266. /package/dist/{src/core → core}/warn.js +0 -0
  267. /package/dist/{src/core → core}/write-source.js +0 -0
  268. /package/dist/{src/indexer → indexer}/db.js +0 -0
  269. /package/dist/{src/indexer → indexer}/file-context.js +0 -0
  270. /package/dist/{src/indexer → indexer}/graph-boost.js +0 -0
  271. /package/dist/{src/indexer → indexer}/manifest.js +0 -0
  272. /package/dist/{src/indexer → indexer}/matchers.js +0 -0
  273. /package/dist/{src/indexer → indexer}/metadata.js +0 -0
  274. /package/dist/{src/indexer → indexer}/search-fields.js +0 -0
  275. /package/dist/{src/indexer → indexer}/semantic-status.js +0 -0
  276. /package/dist/{src/indexer → indexer}/usage-events.js +0 -0
  277. /package/dist/{src/indexer → indexer}/walker.js +0 -0
  278. /package/dist/{src/integrations → integrations}/agent/config.js +0 -0
  279. /package/dist/{src/integrations → integrations}/agent/detect.js +0 -0
  280. /package/dist/{src/integrations → integrations}/agent/index.js +0 -0
  281. /package/dist/{src/integrations → integrations}/agent/profiles.js +0 -0
  282. /package/dist/{src/integrations → integrations}/agent/prompts.js +0 -0
  283. /package/dist/{src/integrations → integrations}/agent/spawn.js +0 -0
  284. /package/dist/{src/integrations → integrations}/github.js +0 -0
  285. /package/dist/{src/integrations → integrations}/lockfile.js +0 -0
  286. /package/dist/{src/llm → llm}/embedders/cache.js +0 -0
  287. /package/dist/{src/llm → llm}/embedders/types.js +0 -0
  288. /package/dist/{src/llm → llm}/feature-gate.js +0 -0
  289. /package/dist/{src/llm → llm}/index-passes.js +0 -0
  290. /package/dist/{src/output → output}/context.js +0 -0
  291. /package/dist/{src/output → output}/renderers.js +0 -0
  292. /package/dist/{src/output → output}/shapes.js +0 -0
  293. /package/dist/{src/output → output}/text.js +0 -0
  294. /package/dist/{src/registry → registry}/build-index.js +0 -0
  295. /package/dist/{src/registry → registry}/create-provider-registry.js +0 -0
  296. /package/dist/{src/registry → registry}/factory.js +0 -0
  297. /package/dist/{src/registry → registry}/origin-resolve.js +0 -0
  298. /package/dist/{src/registry → registry}/providers/index.js +0 -0
  299. /package/dist/{src/registry → registry}/providers/skills-sh.js +0 -0
  300. /package/dist/{src/registry → registry}/providers/static-index.js +0 -0
  301. /package/dist/{src/registry → registry}/providers/types.js +0 -0
  302. /package/dist/{src/registry → registry}/resolve.js +0 -0
  303. /package/dist/{src/registry → registry}/types.js +0 -0
  304. /package/dist/{src/setup → setup}/detect.js +0 -0
  305. /package/dist/{src/setup → setup}/ripgrep-install.js +0 -0
  306. /package/dist/{src/setup → setup}/ripgrep-resolve.js +0 -0
  307. /package/dist/{src/setup → setup}/steps.js +0 -0
  308. /package/dist/{src/sources → sources}/include.js +0 -0
  309. /package/dist/{src/sources → sources}/provider-factory.js +0 -0
  310. /package/dist/{src/sources → sources}/provider.js +0 -0
  311. /package/dist/{src/sources → sources}/providers/filesystem.js +0 -0
  312. /package/dist/{src/sources → sources}/providers/git.js +0 -0
  313. /package/dist/{src/sources → sources}/providers/index.js +0 -0
  314. /package/dist/{src/sources → sources}/providers/install-types.js +0 -0
  315. /package/dist/{src/sources → sources}/providers/npm.js +0 -0
  316. /package/dist/{src/sources → sources}/providers/provider-utils.js +0 -0
  317. /package/dist/{src/sources → sources}/providers/sync-from-ref.js +0 -0
  318. /package/dist/{src/sources → sources}/providers/tar-utils.js +0 -0
  319. /package/dist/{src/sources → sources}/resolve.js +0 -0
  320. /package/dist/{src/sources → sources}/types.js +0 -0
  321. /package/dist/{src/templates → templates}/wiki-templates.js +0 -0
  322. /package/dist/{src/version.js → version.js} +0 -0
  323. /package/dist/{src/wiki → wiki}/wiki.js +0 -0
  324. /package/dist/{src/workflows → workflows}/authoring.js +0 -0
  325. /package/dist/{src/workflows → workflows}/cli.js +0 -0
  326. /package/dist/{src/workflows → workflows}/db.js +0 -0
  327. /package/dist/{src/workflows → workflows}/document-cache.js +0 -0
  328. /package/dist/{src/workflows → workflows}/parser.js +0 -0
  329. /package/dist/{src/workflows → workflows}/renderer.js +0 -0
  330. /package/dist/{src/workflows → workflows}/runs.js +0 -0
  331. /package/dist/{src/workflows → workflows}/schema.js +0 -0
  332. /package/dist/{src/workflows → workflows}/validator.js +0 -0
@@ -1,43 +0,0 @@
1
- /**
2
- * Invariant: no bench source under `tests/bench/*.ts` may reference
3
- * `os.tmpdir`. All bench tmp dirs MUST go through `benchTmpRoot()` /
4
- * `benchMkdtemp()` from `./tmp`, which redirects to
5
- * `${AKM_CACHE_DIR}/bench/` (#276).
6
- *
7
- * Allowlist: `tests/bench/tmp.ts` and this test file. The helper is
8
- * permitted to mention `os.tmpdir` in its docstrings/comments because it
9
- * documents what it replaces; this test file mentions the symbol in its
10
- * grep target.
11
- */
12
- import { describe, expect, test } from "bun:test";
13
- import fs from "node:fs";
14
- import path from "node:path";
15
- const ALLOWLIST = new Set(["tmp.ts", "no-os-tmpdir-invariant.test.ts"]);
16
- const benchDir = path.resolve(import.meta.dir);
17
- describe("bench source invariant: no os.tmpdir (#276)", () => {
18
- test("no bench *.ts file (outside the allowlist) references os.tmpdir", () => {
19
- const offenders = [];
20
- for (const entry of fs.readdirSync(benchDir, { withFileTypes: true })) {
21
- if (!entry.isFile())
22
- continue;
23
- if (!entry.name.endsWith(".ts"))
24
- continue;
25
- if (ALLOWLIST.has(entry.name))
26
- continue;
27
- const full = path.join(benchDir, entry.name);
28
- const lines = fs.readFileSync(full, "utf8").split("\n");
29
- for (let i = 0; i < lines.length; i += 1) {
30
- const line = lines[i] ?? "";
31
- if (/os\.tmpdir/.test(line) || /\btmpdir\s*\(/.test(line)) {
32
- offenders.push({ file: entry.name, line: i + 1, text: line.trim() });
33
- }
34
- }
35
- }
36
- if (offenders.length > 0) {
37
- const detail = offenders.map((o) => ` ${o.file}:${o.line} ${o.text}`).join("\n");
38
- throw new Error(`Found ${offenders.length} disallowed os.tmpdir reference(s) under tests/bench/.\n` +
39
- `Use benchTmpRoot()/benchMkdtemp() from ./tmp instead (#276):\n${detail}`);
40
- }
41
- expect(offenders).toEqual([]);
42
- });
43
- });
@@ -1,194 +0,0 @@
1
- /**
2
- * opencode-config.ts — config-driven opencode provider materialisation.
3
- *
4
- * Loads the operator's bench provider file (committed fixture or
5
- * gitignored `.local.json` overlay), validates it for safety (no hard-coded
6
- * credentials, no extra top-level keys), and writes a minimal
7
- * `opencode.json` into the per-run isolated `OPENCODE_CONFIG` directory.
8
- *
9
- * Design: `tests/bench/BENCH.md` §"Config-driven opencode provider".
10
- */
11
- import fs from "node:fs";
12
- import path from "node:path";
13
- /**
14
- * Error class for bench provider-config problems.
15
- *
16
- * `isUsageError: true` → the caller should exit 2 (USAGE).
17
- * `isUsageError: false` → the caller should exit 78 (CONFIG).
18
- */
19
- export class BenchConfigError extends Error {
20
- code = "BENCH_CONFIG";
21
- isUsageError;
22
- constructor(message, isUsageError) {
23
- super(message);
24
- this.name = "BenchConfigError";
25
- this.isUsageError = isUsageError;
26
- }
27
- }
28
- /**
29
- * Top-level keys that belong in a full opencode user-config but are FORBIDDEN
30
- * in the bench provider file. The bench file is intentionally minimal — it
31
- * only specifies provider entries. Any of these keys in the file means the
32
- * operator has pasted a full opencode config into the bench slot, which could
33
- * contain credentials, plugins, or permission overrides that the bench MUST
34
- * NOT inherit.
35
- */
36
- const FORBIDDEN_TOPLEVEL_KEYS = new Set([
37
- "plugin",
38
- "mcp",
39
- "permission",
40
- "disabled_providers",
41
- "small_model",
42
- "snapshot",
43
- ]);
44
- /**
45
- * Regex that an `apiKey` string value MUST match when present. The only
46
- * allowed form is an env-ref placeholder: `{env:VAR_NAME}`.
47
- */
48
- const ENV_REF_RE = /^\{env:[A-Z_][A-Z0-9_]*\}$/;
49
- /** Heuristic to detect literal API credentials accidentally pasted into the file. */
50
- const CREDENTIAL_RE = /^sk-[A-Za-z0-9_-]{20,}$/;
51
- /**
52
- * Recursively scan `node` for credential heuristic violations and literal
53
- * `apiKey` values that are not env-refs. Throws `BenchConfigError` on the
54
- * first violation found.
55
- *
56
- * @param node The value to scan (any JSON value).
57
- * @param jspath JSON-path-like string for error messages, e.g. `providers.myProvider.apiKey`.
58
- */
59
- function scanForCredentials(node, jspath) {
60
- if (typeof node === "string") {
61
- // Heuristic: reject anything that looks like an OpenAI/Anthropic-style key.
62
- if (CREDENTIAL_RE.test(node)) {
63
- throw new BenchConfigError(`bench provider file: credential heuristic triggered at "${jspath}" — literal API key detected; use {env:VAR_NAME} instead`, false);
64
- }
65
- return;
66
- }
67
- if (Array.isArray(node)) {
68
- for (let i = 0; i < node.length; i++) {
69
- scanForCredentials(node[i], `${jspath}[${i}]`);
70
- }
71
- return;
72
- }
73
- if (node !== null && typeof node === "object") {
74
- for (const [key, value] of Object.entries(node)) {
75
- const childPath = `${jspath}.${key}`;
76
- // apiKey must be an env-ref if present as a string.
77
- if (key === "apiKey" && typeof value === "string") {
78
- if (!ENV_REF_RE.test(value)) {
79
- throw new BenchConfigError(`bench provider file: "${childPath}" must be an env-ref (e.g. {env:MY_API_KEY}), not a literal value`, false);
80
- }
81
- // An env-ref is fine — don't recurse further into it.
82
- continue;
83
- }
84
- scanForCredentials(value, childPath);
85
- }
86
- }
87
- }
88
- /**
89
- * Load and validate a bench opencode providers JSON file.
90
- *
91
- * Throws:
92
- * - `BenchConfigError(isUsageError: true)` if the file does not exist.
93
- * - `BenchConfigError(isUsageError: false)` if JSON parse fails or the file
94
- * fails validation (bad schema version, forbidden top-level keys, detected
95
- * credentials).
96
- */
97
- export function loadOpencodeProviders(absPath) {
98
- // ── File existence ────────────────────────────────────────────────────────
99
- let raw;
100
- try {
101
- raw = fs.readFileSync(absPath, "utf8");
102
- }
103
- catch (err) {
104
- const isEnoent = err.code === "ENOENT";
105
- if (isEnoent) {
106
- throw new BenchConfigError(`bench provider file not found: ${absPath}`, true);
107
- }
108
- throw new BenchConfigError(`bench provider file: could not read "${absPath}": ${err instanceof Error ? err.message : String(err)}`, false);
109
- }
110
- // ── JSON parse ────────────────────────────────────────────────────────────
111
- let parsed;
112
- try {
113
- parsed = JSON.parse(raw);
114
- }
115
- catch (err) {
116
- throw new BenchConfigError(`bench provider file: JSON parse error in "${absPath}": ${err instanceof Error ? err.message : String(err)}`, false);
117
- }
118
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
119
- throw new BenchConfigError(`bench provider file: root must be a JSON object (got ${Array.isArray(parsed) ? "array" : typeof parsed})`, false);
120
- }
121
- const obj = parsed;
122
- // ── Forbidden top-level keys ──────────────────────────────────────────────
123
- for (const key of Object.keys(obj)) {
124
- if (FORBIDDEN_TOPLEVEL_KEYS.has(key)) {
125
- throw new BenchConfigError(`bench provider file: forbidden top-level key "${key}" — the bench provider file must contain only "schemaVersion", "defaultModel", and "providers"`, false);
126
- }
127
- }
128
- // ── schemaVersion ─────────────────────────────────────────────────────────
129
- if (obj.schemaVersion !== 1) {
130
- throw new BenchConfigError(`bench provider file: unsupported schemaVersion ${JSON.stringify(obj.schemaVersion)}; expected 1`, false);
131
- }
132
- // ── providers ─────────────────────────────────────────────────────────────
133
- if (obj.providers === null || typeof obj.providers !== "object" || Array.isArray(obj.providers)) {
134
- throw new BenchConfigError(`bench provider file: "providers" must be an object`, false);
135
- }
136
- const providers = obj.providers;
137
- // ── Credential scan ───────────────────────────────────────────────────────
138
- scanForCredentials(providers, "providers");
139
- return {
140
- source: absPath,
141
- providers,
142
- ...(typeof obj.defaultModel === "string" ? { defaultModel: obj.defaultModel } : {}),
143
- };
144
- }
145
- /**
146
- * Given a model ID (e.g. `"don/mlx-community/qwen3.6-35b-a3b"`), split on
147
- * the first `/` to get the provider key and look it up in `loaded.providers`.
148
- *
149
- * Throws `BenchConfigError` if the provider key is not found.
150
- */
151
- export function selectProviderForModel(loaded, modelId) {
152
- const slashIdx = modelId.indexOf("/");
153
- const providerKey = slashIdx === -1 ? modelId : modelId.slice(0, slashIdx);
154
- if (!(providerKey in loaded.providers)) {
155
- throw new BenchConfigError(`bench provider file: model ID "${modelId}" maps to provider key "${providerKey}", which is not present in ${loaded.source}; available: ${Object.keys(loaded.providers).join(", ") || "(none)"}`, false);
156
- }
157
- return { providerKey, entry: loaded.providers[providerKey] };
158
- }
159
- /**
160
- * Write a minimal `opencode.json` into `opencodeConfigDir` for the given
161
- * provider selection. The file contains exactly two top-level keys:
162
- * `$schema` and `provider`.
163
- *
164
- * Written with mode `0o600` so the file is not world-readable (it may
165
- * contain env-ref placeholders that hint at secret variable names).
166
- */
167
- export function materializeOpencodeConfig(opencodeConfigDir, selected,
168
- /** Full model id (e.g. "don/mlx-community/qwen3.6-35b-a3b") written as the
169
- * top-level `model` key so opencode uses it without a --model flag. */
170
- modelId) {
171
- const config = {
172
- $schema: "https://opencode.ai/config.json",
173
- model: modelId,
174
- provider: {
175
- [selected.providerKey]: selected.entry,
176
- },
177
- // Explicitly allow all tools so opencode run (non-interactive) doesn't
178
- // silently skip bash/file operations due to missing permission config.
179
- permission: {
180
- bash: "allow",
181
- edit: "allow",
182
- write: "allow",
183
- read: "allow",
184
- webfetch: "allow",
185
- },
186
- // Disable operator plugins during bench runs. Plugins like akm-opencode
187
- // run their own session lifecycle hooks (warmIndexInBackground, akm setup
188
- // prompts, AKM_STASH_DIR overrides in shell.env) that interfere with the
189
- // bench's isolated fixture stash and cause stash mismatch failures.
190
- plugin: [],
191
- };
192
- const outPath = path.join(opencodeConfigDir, "opencode.json");
193
- fs.writeFileSync(outPath, JSON.stringify(config, null, 2), { mode: 0o600 });
194
- }
@@ -1,370 +0,0 @@
1
- /**
2
- * Tests for the bench opencode-config module.
3
- *
4
- * Covers all cases described in the design spec:
5
- * - loads canonical fixture without error
6
- * - rejects literal apiKey (not env-ref)
7
- * - accepts {env:VAR} apiKey form
8
- * - rejects sk-XXXX credential heuristic anywhere in tree
9
- * - rejects top-level plugin / mcp / permission keys
10
- * - rejects unknown schemaVersion
11
- * - isUsageError: true when file missing
12
- * - selectProviderForModel picks correct provider
13
- * - selectProviderForModel throws on unknown provider prefix
14
- * - materializeOpencodeConfig writes exactly $schema + provider keys, mode 0o600
15
- */
16
- import { afterAll, beforeAll, describe, expect, test } from "bun:test";
17
- import fs from "node:fs";
18
- import path from "node:path";
19
- import { BenchConfigError, loadOpencodeProviders, materializeOpencodeConfig, selectProviderForModel, } from "./opencode-config";
20
- import { benchMkdtemp } from "./tmp";
21
- /** Absolute path to the committed fixture. */
22
- const FIXTURE_PATH = path.resolve(__dirname, "..", "fixtures", "bench", "opencode-providers.json");
23
- /** Write a temp JSON file and return its path. */
24
- function writeTmp(dir, name, content) {
25
- const p = path.join(dir, name);
26
- fs.writeFileSync(p, JSON.stringify(content));
27
- return p;
28
- }
29
- describe("loadOpencodeProviders", () => {
30
- let tmp;
31
- beforeAll(() => {
32
- tmp = benchMkdtemp("bench-opencode-config-test-");
33
- });
34
- afterAll(() => {
35
- fs.rmSync(tmp, { recursive: true, force: true });
36
- });
37
- // ── Canonical fixture ─────────────────────────────────────────────────────
38
- test("loads the canonical committed fixture without error", () => {
39
- expect(() => loadOpencodeProviders(FIXTURE_PATH)).not.toThrow();
40
- const loaded = loadOpencodeProviders(FIXTURE_PATH);
41
- expect(loaded.source).toBe(FIXTURE_PATH);
42
- expect(loaded.providers).toBeDefined();
43
- expect(typeof loaded.providers).toBe("object");
44
- expect(loaded.defaultModel).toBe("local/qwen/qwen3.5-9b");
45
- expect("local" in loaded.providers).toBe(true);
46
- });
47
- // ── File not found ────────────────────────────────────────────────────────
48
- test("throws BenchConfigError with isUsageError: true when file does not exist", () => {
49
- const missing = path.join(tmp, "does-not-exist.json");
50
- let err;
51
- try {
52
- loadOpencodeProviders(missing);
53
- }
54
- catch (e) {
55
- err = e;
56
- }
57
- expect(err).toBeInstanceOf(BenchConfigError);
58
- const bce = err;
59
- expect(bce.code).toBe("BENCH_CONFIG");
60
- expect(bce.isUsageError).toBe(true);
61
- expect(bce.message).toContain("not found");
62
- });
63
- // ── JSON parse failure ────────────────────────────────────────────────────
64
- test("throws BenchConfigError with isUsageError: false on malformed JSON", () => {
65
- const p = path.join(tmp, "bad.json");
66
- fs.writeFileSync(p, "{ this is not json }");
67
- let err;
68
- try {
69
- loadOpencodeProviders(p);
70
- }
71
- catch (e) {
72
- err = e;
73
- }
74
- expect(err).toBeInstanceOf(BenchConfigError);
75
- expect(err.isUsageError).toBe(false);
76
- expect(err.message).toContain("JSON parse error");
77
- });
78
- // ── schemaVersion ─────────────────────────────────────────────────────────
79
- test("rejects unknown schemaVersion", () => {
80
- const p = writeTmp(tmp, "bad-version.json", {
81
- schemaVersion: 2,
82
- providers: {},
83
- });
84
- expect(() => loadOpencodeProviders(p)).toThrow(BenchConfigError);
85
- let err;
86
- try {
87
- loadOpencodeProviders(p);
88
- }
89
- catch (e) {
90
- if (e instanceof BenchConfigError)
91
- err = e;
92
- }
93
- expect(err?.isUsageError).toBe(false);
94
- expect(err?.message).toContain("schemaVersion");
95
- });
96
- test("rejects schemaVersion: 0", () => {
97
- const p = writeTmp(tmp, "version-0.json", { schemaVersion: 0, providers: {} });
98
- expect(() => loadOpencodeProviders(p)).toThrow(BenchConfigError);
99
- });
100
- // ── Forbidden top-level keys ──────────────────────────────────────────────
101
- test("rejects top-level 'plugin' key", () => {
102
- const p = writeTmp(tmp, "has-plugin.json", {
103
- schemaVersion: 1,
104
- providers: {},
105
- plugin: [],
106
- });
107
- let err;
108
- try {
109
- loadOpencodeProviders(p);
110
- }
111
- catch (e) {
112
- if (e instanceof BenchConfigError)
113
- err = e;
114
- }
115
- expect(err).toBeDefined();
116
- expect(err?.isUsageError).toBe(false);
117
- expect(err?.message).toContain("plugin");
118
- });
119
- test("rejects top-level 'mcp' key", () => {
120
- const p = writeTmp(tmp, "has-mcp.json", {
121
- schemaVersion: 1,
122
- providers: {},
123
- mcp: {},
124
- });
125
- expect(() => loadOpencodeProviders(p)).toThrow(BenchConfigError);
126
- });
127
- test("rejects top-level 'permission' key", () => {
128
- const p = writeTmp(tmp, "has-permission.json", {
129
- schemaVersion: 1,
130
- providers: {},
131
- permission: {},
132
- });
133
- expect(() => loadOpencodeProviders(p)).toThrow(BenchConfigError);
134
- });
135
- test("rejects top-level 'disabled_providers' key", () => {
136
- const p = writeTmp(tmp, "has-disabled.json", {
137
- schemaVersion: 1,
138
- providers: {},
139
- disabled_providers: [],
140
- });
141
- expect(() => loadOpencodeProviders(p)).toThrow(BenchConfigError);
142
- });
143
- test("rejects top-level 'small_model' key", () => {
144
- const p = writeTmp(tmp, "has-small-model.json", {
145
- schemaVersion: 1,
146
- providers: {},
147
- small_model: "anthropic/claude-haiku-4-5",
148
- });
149
- expect(() => loadOpencodeProviders(p)).toThrow(BenchConfigError);
150
- });
151
- test("rejects top-level 'snapshot' key", () => {
152
- const p = writeTmp(tmp, "has-snapshot.json", {
153
- schemaVersion: 1,
154
- providers: {},
155
- snapshot: true,
156
- });
157
- expect(() => loadOpencodeProviders(p)).toThrow(BenchConfigError);
158
- });
159
- // ── apiKey validation ─────────────────────────────────────────────────────
160
- test("rejects literal apiKey string (not an env-ref)", () => {
161
- const p = writeTmp(tmp, "literal-apikey.json", {
162
- schemaVersion: 1,
163
- providers: {
164
- myProvider: {
165
- apiKey: "not-an-env-ref",
166
- },
167
- },
168
- });
169
- let err;
170
- try {
171
- loadOpencodeProviders(p);
172
- }
173
- catch (e) {
174
- if (e instanceof BenchConfigError)
175
- err = e;
176
- }
177
- expect(err).toBeDefined();
178
- expect(err?.isUsageError).toBe(false);
179
- expect(err?.message).toContain("apiKey");
180
- expect(err?.message).toContain("env-ref");
181
- });
182
- test("accepts {env:VAR} form for apiKey", () => {
183
- const p = writeTmp(tmp, "env-ref-apikey.json", {
184
- schemaVersion: 1,
185
- providers: {
186
- myProvider: {
187
- npm: "@ai-sdk/openai-compatible",
188
- apiKey: "{env:MY_API_KEY}",
189
- options: { baseURL: "http://localhost:1234/v1" },
190
- },
191
- },
192
- });
193
- expect(() => loadOpencodeProviders(p)).not.toThrow();
194
- const loaded = loadOpencodeProviders(p);
195
- expect("myProvider" in loaded.providers).toBe(true);
196
- });
197
- test("accepts {env:UNDERSCORE_KEY_123} env-ref form", () => {
198
- const p = writeTmp(tmp, "env-ref-underscore.json", {
199
- schemaVersion: 1,
200
- providers: {
201
- p: { apiKey: "{env:MY_KEY_123}" },
202
- },
203
- });
204
- expect(() => loadOpencodeProviders(p)).not.toThrow();
205
- });
206
- test("rejects apiKey starting with lowercase (not a valid env-ref)", () => {
207
- const p = writeTmp(tmp, "bad-env-ref.json", {
208
- schemaVersion: 1,
209
- providers: {
210
- p: { apiKey: "{env:my_lowercase_key}" },
211
- },
212
- });
213
- expect(() => loadOpencodeProviders(p)).toThrow(BenchConfigError);
214
- });
215
- // ── Credential heuristic ──────────────────────────────────────────────────
216
- test("rejects sk-XXXX credential anywhere in the providers tree", () => {
217
- const p = writeTmp(tmp, "has-sk-key.json", {
218
- schemaVersion: 1,
219
- providers: {
220
- openai: {
221
- npm: "@ai-sdk/openai",
222
- secret: "sk-abcdefghijklmnopqrstuvwxyz0123456789",
223
- },
224
- },
225
- });
226
- let err;
227
- try {
228
- loadOpencodeProviders(p);
229
- }
230
- catch (e) {
231
- if (e instanceof BenchConfigError)
232
- err = e;
233
- }
234
- expect(err).toBeDefined();
235
- expect(err?.isUsageError).toBe(false);
236
- expect(err?.message).toContain("credential heuristic");
237
- });
238
- test("rejects sk-XXXX credential in a nested object", () => {
239
- const p = writeTmp(tmp, "nested-sk-key.json", {
240
- schemaVersion: 1,
241
- providers: {
242
- p: {
243
- options: {
244
- headers: {
245
- Authorization: "sk-proj-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
246
- },
247
- },
248
- },
249
- },
250
- });
251
- expect(() => loadOpencodeProviders(p)).toThrow(BenchConfigError);
252
- });
253
- // ── Valid minimal file ────────────────────────────────────────────────────
254
- test("accepts a valid minimal file with no defaultModel", () => {
255
- const p = writeTmp(tmp, "minimal.json", {
256
- schemaVersion: 1,
257
- providers: {
258
- local: {
259
- npm: "@ai-sdk/openai-compatible",
260
- options: { baseURL: "http://localhost:1234/v1" },
261
- },
262
- },
263
- });
264
- const loaded = loadOpencodeProviders(p);
265
- expect(loaded.defaultModel).toBeUndefined();
266
- expect("local" in loaded.providers).toBe(true);
267
- });
268
- });
269
- describe("selectProviderForModel", () => {
270
- const loaded = {
271
- source: "/fake/path.json",
272
- providers: {
273
- don: { npm: "@ai-sdk/openai-compatible", name: "Don LM Studio" },
274
- ollama: { npm: "@ai-sdk/openai-compatible", name: "Ollama" },
275
- },
276
- defaultModel: "don/mlx-community/qwen3.6-35b-a3b",
277
- };
278
- test("splits on first slash and returns the correct provider entry", () => {
279
- const result = selectProviderForModel(loaded, "don/mlx-community/qwen3.6-35b-a3b");
280
- expect(result.providerKey).toBe("don");
281
- expect(result.entry).toBe(loaded.providers.don);
282
- });
283
- test("handles a model with no slash (entire string is the provider key)", () => {
284
- const result = selectProviderForModel(loaded, "ollama");
285
- expect(result.providerKey).toBe("ollama");
286
- expect(result.entry).toBe(loaded.providers.ollama);
287
- });
288
- test("throws BenchConfigError when provider key is not in loaded.providers", () => {
289
- let err;
290
- try {
291
- selectProviderForModel(loaded, "unknown/some-model");
292
- }
293
- catch (e) {
294
- if (e instanceof BenchConfigError)
295
- err = e;
296
- }
297
- expect(err).toBeDefined();
298
- expect(err?.code).toBe("BENCH_CONFIG");
299
- expect(err?.isUsageError).toBe(false);
300
- expect(err?.message).toContain("unknown");
301
- expect(err?.message).toContain("provider key");
302
- });
303
- test("error message lists available provider keys", () => {
304
- let err;
305
- try {
306
- selectProviderForModel(loaded, "missing/model");
307
- }
308
- catch (e) {
309
- if (e instanceof BenchConfigError)
310
- err = e;
311
- }
312
- expect(err?.message).toContain("don");
313
- expect(err?.message).toContain("ollama");
314
- });
315
- });
316
- describe("materializeOpencodeConfig", () => {
317
- let tmp;
318
- beforeAll(() => {
319
- tmp = benchMkdtemp("bench-materialize-test-");
320
- });
321
- afterAll(() => {
322
- fs.rmSync(tmp, { recursive: true, force: true });
323
- });
324
- test("writes opencode.json with required bench isolation invariants and provider", () => {
325
- const configDir = path.join(tmp, "run-config");
326
- fs.mkdirSync(configDir, { recursive: true });
327
- const entry = { npm: "@ai-sdk/openai-compatible", name: "Test Provider" };
328
- materializeOpencodeConfig(configDir, { providerKey: "test", entry }, "test/my-model");
329
- const outPath = path.join(configDir, "opencode.json");
330
- expect(fs.existsSync(outPath)).toBe(true);
331
- const contents = JSON.parse(fs.readFileSync(outPath, "utf8"));
332
- expect(contents.model).toBe("test/my-model");
333
- expect(contents.$schema).toBe("https://opencode.ai/config.json");
334
- // Bench isolation invariants: plugin:[] prevents operator plugin interference;
335
- // permission block ensures opencode run (non-interactive) allows bash/file tools.
336
- expect(contents.plugin).toEqual([]);
337
- expect(contents.permission?.bash).toBe("allow");
338
- // Provider block is written correctly.
339
- const provider = contents.provider;
340
- expect(Object.keys(provider)).toEqual(["test"]);
341
- expect(provider.test).toEqual(entry);
342
- });
343
- test("does not write mcp into the config", () => {
344
- const configDir = path.join(tmp, "run-config-2");
345
- fs.mkdirSync(configDir, { recursive: true });
346
- materializeOpencodeConfig(configDir, { providerKey: "p", entry: {} }, "p/model");
347
- const contents = JSON.parse(fs.readFileSync(path.join(configDir, "opencode.json"), "utf8"));
348
- expect(contents.mcp).toBeUndefined();
349
- });
350
- test("writes the file with mode 0o600 (not world-readable)", () => {
351
- const configDir = path.join(tmp, "run-config-3");
352
- fs.mkdirSync(configDir, { recursive: true });
353
- materializeOpencodeConfig(configDir, { providerKey: "p", entry: {} }, "p/model");
354
- const stat = fs.statSync(path.join(configDir, "opencode.json"));
355
- // Mode 0o600 means only owner can read/write (no group or other bits).
356
- // On Linux/macOS the lower 9 bits are 0o600 = 0o110000000 in binary.
357
- const mode = stat.mode & 0o777;
358
- expect(mode).toBe(0o600);
359
- });
360
- test("can be called twice (overwrites an existing opencode.json)", () => {
361
- const configDir = path.join(tmp, "run-config-4");
362
- fs.mkdirSync(configDir, { recursive: true });
363
- materializeOpencodeConfig(configDir, { providerKey: "a", entry: { name: "first" } }, "a/m1");
364
- materializeOpencodeConfig(configDir, { providerKey: "b", entry: { name: "second" } }, "b/m2");
365
- const contents = JSON.parse(fs.readFileSync(path.join(configDir, "opencode.json"), "utf8"));
366
- const provider = contents.provider;
367
- expect("b" in provider).toBe(true);
368
- expect("a" in provider).toBe(false);
369
- });
370
- });