akm-cli 0.6.1 → 0.7.0

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 (333) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/{cli.js → src/cli.js} +712 -34
  3. package/dist/{commands → src/commands}/config-cli.js +47 -4
  4. package/dist/src/commands/distill.js +283 -0
  5. package/dist/src/commands/events.js +108 -0
  6. package/dist/src/commands/history.js +191 -0
  7. package/dist/{commands → src/commands}/installed-stashes.js +1 -1
  8. package/dist/src/commands/proposal.js +119 -0
  9. package/dist/src/commands/propose.js +171 -0
  10. package/dist/src/commands/reflect.js +193 -0
  11. package/dist/{commands → src/commands}/registry-search.js +71 -7
  12. package/dist/{commands → src/commands}/remember.js +12 -0
  13. package/dist/{commands → src/commands}/search.js +104 -4
  14. package/dist/{commands → src/commands}/self-update.js +4 -3
  15. package/dist/{commands → src/commands}/show.js +73 -0
  16. package/dist/{commands → src/commands}/source-add.js +5 -1
  17. package/dist/{commands → src/commands}/source-manage.js +7 -1
  18. package/dist/{core → src/core}/asset-ref.js +5 -5
  19. package/dist/{core → src/core}/asset-spec.js +12 -0
  20. package/dist/{core → src/core}/common.js +1 -1
  21. package/dist/{core → src/core}/config.js +203 -121
  22. package/dist/{core → src/core}/errors.js +4 -0
  23. package/dist/src/core/events.js +239 -0
  24. package/dist/src/core/lesson-lint.js +86 -0
  25. package/dist/src/core/proposals.js +406 -0
  26. package/dist/src/core/warn.js +72 -0
  27. package/dist/{core → src/core}/write-source.js +80 -5
  28. package/dist/{indexer → src/indexer}/db-search.js +114 -24
  29. package/dist/{indexer → src/indexer}/db.js +76 -23
  30. package/dist/{indexer → src/indexer}/file-context.js +0 -3
  31. package/dist/src/indexer/graph-boost.js +179 -0
  32. package/dist/src/indexer/graph-extraction.js +212 -0
  33. package/dist/{indexer → src/indexer}/indexer.js +88 -7
  34. package/dist/{indexer → src/indexer}/matchers.js +1 -1
  35. package/dist/src/indexer/memory-inference.js +263 -0
  36. package/dist/{indexer → src/indexer}/metadata.js +111 -3
  37. package/dist/{indexer → src/indexer}/search-source.js +4 -2
  38. package/dist/src/integrations/agent/config.js +292 -0
  39. package/dist/src/integrations/agent/detect.js +94 -0
  40. package/dist/src/integrations/agent/index.js +17 -0
  41. package/dist/src/integrations/agent/profiles.js +65 -0
  42. package/dist/src/integrations/agent/prompts.js +167 -0
  43. package/dist/src/integrations/agent/spawn.js +272 -0
  44. package/dist/{integrations → src/integrations}/github.js +9 -3
  45. package/dist/{integrations → src/integrations}/lockfile.js +0 -26
  46. package/dist/{llm → src/llm}/client.js +33 -2
  47. package/dist/{llm → src/llm}/embedders/remote.js +37 -3
  48. package/dist/src/llm/feature-gate.js +108 -0
  49. package/dist/src/llm/graph-extract.js +107 -0
  50. package/dist/src/llm/index-passes.js +35 -0
  51. package/dist/src/llm/memory-infer.js +86 -0
  52. package/dist/{output → src/output}/cli-hints.js +15 -2
  53. package/dist/{output → src/output}/renderers.js +63 -2
  54. package/dist/src/output/shapes.js +523 -0
  55. package/dist/src/output/text.js +1116 -0
  56. package/dist/{registry → src/registry}/build-index.js +19 -8
  57. package/dist/{registry → src/registry}/factory.js +0 -8
  58. package/dist/{registry → src/registry}/providers/static-index.js +6 -3
  59. package/dist/{registry → src/registry}/resolve.js +68 -2
  60. package/dist/{setup → src/setup}/setup.js +52 -5
  61. package/dist/{sources → src/sources}/providers/git.js +7 -15
  62. package/dist/{wiki → src/wiki}/wiki.js +54 -6
  63. package/dist/{workflows → src/workflows}/runs.js +37 -3
  64. package/dist/tests/add-website-source.test.js +119 -0
  65. package/dist/tests/agent/agent-config-loader.test.js +70 -0
  66. package/dist/tests/agent/agent-config.test.js +221 -0
  67. package/dist/tests/agent/agent-detect.test.js +100 -0
  68. package/dist/tests/agent/agent-spawn.test.js +234 -0
  69. package/dist/tests/agent-output.test.js +186 -0
  70. package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +103 -0
  71. package/dist/tests/architecture/agent-spawn-seam.test.js +193 -0
  72. package/dist/tests/architecture/llm-stateless-seam.test.js +112 -0
  73. package/dist/tests/asset-ref.test.js +192 -0
  74. package/dist/tests/asset-registry.test.js +103 -0
  75. package/dist/tests/asset-spec.test.js +241 -0
  76. package/dist/tests/bench/attribution.test.js +996 -0
  77. package/dist/tests/bench/cleanup-sigint.test.js +83 -0
  78. package/dist/tests/bench/cleanup.js +234 -0
  79. package/dist/tests/bench/cleanup.test.js +166 -0
  80. package/dist/tests/bench/cli.js +1018 -0
  81. package/dist/tests/bench/cli.test.js +445 -0
  82. package/dist/tests/bench/compare.test.js +556 -0
  83. package/dist/tests/bench/corpus.js +317 -0
  84. package/dist/tests/bench/corpus.test.js +258 -0
  85. package/dist/tests/bench/doctor.js +525 -0
  86. package/dist/tests/bench/driver.js +401 -0
  87. package/dist/tests/bench/driver.test.js +584 -0
  88. package/dist/tests/bench/environment.js +233 -0
  89. package/dist/tests/bench/environment.test.js +199 -0
  90. package/dist/tests/bench/evolve-metrics.js +179 -0
  91. package/dist/tests/bench/evolve-metrics.test.js +187 -0
  92. package/dist/tests/bench/evolve.js +647 -0
  93. package/dist/tests/bench/evolve.test.js +624 -0
  94. package/dist/tests/bench/failure-modes.test.js +349 -0
  95. package/dist/tests/bench/feedback-integrity.test.js +457 -0
  96. package/dist/tests/bench/leakage.test.js +228 -0
  97. package/dist/tests/bench/learning-curve.test.js +134 -0
  98. package/dist/tests/bench/metrics.js +2395 -0
  99. package/dist/tests/bench/metrics.test.js +1150 -0
  100. package/dist/tests/bench/no-os-tmpdir-invariant.test.js +43 -0
  101. package/dist/tests/bench/opencode-config.js +194 -0
  102. package/dist/tests/bench/opencode-config.test.js +370 -0
  103. package/dist/tests/bench/report.js +1885 -0
  104. package/dist/tests/bench/report.test.js +1038 -0
  105. package/dist/tests/bench/run-config.js +355 -0
  106. package/dist/tests/bench/run-config.test.js +298 -0
  107. package/dist/tests/bench/run-curate-test.js +32 -0
  108. package/dist/tests/bench/run-failing-tasks.js +56 -0
  109. package/dist/tests/bench/run-full-bench.js +51 -0
  110. package/dist/tests/bench/run-items36-targeted.js +69 -0
  111. package/dist/tests/bench/run-nano-quick.js +42 -0
  112. package/dist/tests/bench/run-waveg-targeted.js +62 -0
  113. package/dist/tests/bench/runner.js +699 -0
  114. package/dist/tests/bench/runner.test.js +958 -0
  115. package/dist/tests/bench/search-bridge.test.js +331 -0
  116. package/dist/tests/bench/tmp.js +131 -0
  117. package/dist/tests/bench/trajectory.js +116 -0
  118. package/dist/tests/bench/trajectory.test.js +127 -0
  119. package/dist/tests/bench/verifier.js +114 -0
  120. package/dist/tests/bench/verifier.test.js +118 -0
  121. package/dist/tests/bench/workflow-evaluator.js +557 -0
  122. package/dist/tests/bench/workflow-evaluator.test.js +421 -0
  123. package/dist/tests/bench/workflow-spec.js +345 -0
  124. package/dist/tests/bench/workflow-spec.test.js +363 -0
  125. package/dist/tests/bench/workflow-trace.js +472 -0
  126. package/dist/tests/bench/workflow-trace.test.js +254 -0
  127. package/dist/tests/benchmark-search-quality.js +536 -0
  128. package/dist/tests/benchmark-suite.js +1441 -0
  129. package/dist/tests/capture-cli.test.js +112 -0
  130. package/dist/tests/cli-errors.test.js +204 -0
  131. package/dist/tests/commands/events.test.js +370 -0
  132. package/dist/tests/commands/history.test.js +418 -0
  133. package/dist/tests/commands/import.test.js +103 -0
  134. package/dist/tests/commands/proposal-cli.test.js +209 -0
  135. package/dist/tests/commands/reflect-propose-cli.test.js +333 -0
  136. package/dist/tests/commands/remember.test.js +97 -0
  137. package/dist/tests/commands/scope-flags.test.js +300 -0
  138. package/dist/tests/commands/search.test.js +537 -0
  139. package/dist/tests/commands/show-indexer-parity.test.js +117 -0
  140. package/dist/tests/commands/show.test.js +294 -0
  141. package/dist/tests/common.test.js +266 -0
  142. package/dist/tests/completions.test.js +142 -0
  143. package/dist/tests/config-cli.test.js +193 -0
  144. package/dist/tests/config-llm-features.test.js +139 -0
  145. package/dist/tests/config.test.js +569 -0
  146. package/dist/tests/contracts/migration-baseline.test.js +43 -0
  147. package/dist/tests/contracts/reflect-propose-envelope.test.js +139 -0
  148. package/dist/tests/contracts/spec-helpers.js +46 -0
  149. package/dist/tests/contracts/v1-spec-section-11-proposal-queue.test.js +228 -0
  150. package/dist/tests/contracts/v1-spec-section-12-agent-config.test.js +56 -0
  151. package/dist/tests/contracts/v1-spec-section-13-lesson-type.test.js +34 -0
  152. package/dist/tests/contracts/v1-spec-section-14-llm-features.test.js +94 -0
  153. package/dist/tests/contracts/v1-spec-section-4-1-asset-types.test.js +39 -0
  154. package/dist/tests/contracts/v1-spec-section-4-2-quality-rules.test.js +44 -0
  155. package/dist/tests/contracts/v1-spec-section-5-configuration.test.js +47 -0
  156. package/dist/tests/contracts/v1-spec-section-6-orchestration.test.js +40 -0
  157. package/dist/tests/contracts/v1-spec-section-7-module-layout.test.js +58 -0
  158. package/dist/tests/contracts/v1-spec-section-8-extension-points.test.js +34 -0
  159. package/dist/tests/contracts/v1-spec-section-9-4-cli-surface.test.js +75 -0
  160. package/dist/tests/contracts/v1-spec-section-9-7-llm-agent-boundary.test.js +36 -0
  161. package/dist/tests/core/write-source.test.js +366 -0
  162. package/dist/tests/curate-command.test.js +87 -0
  163. package/dist/tests/db-scoring.test.js +201 -0
  164. package/dist/tests/db.test.js +654 -0
  165. package/dist/tests/distill-cli-flag.test.js +208 -0
  166. package/dist/tests/distill.test.js +515 -0
  167. package/dist/tests/docker-install.test.js +120 -0
  168. package/dist/tests/e2e.test.js +1419 -0
  169. package/dist/tests/embedder.test.js +340 -0
  170. package/dist/tests/embedding-model-config.test.js +379 -0
  171. package/dist/tests/feedback-command.test.js +172 -0
  172. package/dist/tests/file-context.test.js +552 -0
  173. package/dist/tests/fixtures/scripts/git/summarize-diff.js +9 -0
  174. package/dist/tests/fixtures/scripts/lint/eslint-check.js +7 -0
  175. package/dist/tests/fixtures/stashes/load.js +166 -0
  176. package/dist/tests/fixtures/stashes/load.test.js +97 -0
  177. package/dist/tests/fixtures/stashes/ranking-baseline/scripts/mem0-search.js +12 -0
  178. package/dist/tests/frontmatter.test.js +190 -0
  179. package/dist/tests/fts-field-weighting.test.js +254 -0
  180. package/dist/tests/fuzzy-search.test.js +230 -0
  181. package/dist/tests/git-provider-clone.test.js +45 -0
  182. package/dist/tests/github.test.js +161 -0
  183. package/dist/tests/graph-boost-ranking.test.js +305 -0
  184. package/dist/tests/graph-extraction.test.js +282 -0
  185. package/dist/tests/helpers/usage-events.js +8 -0
  186. package/dist/tests/index-pass-llm.test.js +161 -0
  187. package/dist/tests/indexer.test.js +570 -0
  188. package/dist/tests/info-command.test.js +166 -0
  189. package/dist/tests/init.test.js +69 -0
  190. package/dist/tests/install-script.test.js +246 -0
  191. package/dist/tests/integration/agent-real-profile.test.js +94 -0
  192. package/dist/tests/issue-36-repro.test.js +304 -0
  193. package/dist/tests/issues-191-194.test.js +160 -0
  194. package/dist/tests/lesson-lint.test.js +111 -0
  195. package/dist/tests/llm-client.test.js +115 -0
  196. package/dist/tests/llm-feature-gate.test.js +151 -0
  197. package/dist/tests/llm.test.js +139 -0
  198. package/dist/tests/lockfile.test.js +216 -0
  199. package/dist/tests/manifest.test.js +205 -0
  200. package/dist/tests/markdown.test.js +126 -0
  201. package/dist/tests/matchers-unit.test.js +189 -0
  202. package/dist/tests/memory-inference.test.js +299 -0
  203. package/dist/tests/merge-scoring.test.js +136 -0
  204. package/dist/tests/metadata.test.js +313 -0
  205. package/dist/tests/migration-help.test.js +89 -0
  206. package/dist/tests/origin-resolve.test.js +124 -0
  207. package/dist/tests/output-baseline.test.js +218 -0
  208. package/dist/tests/output-shapes-unit.test.js +478 -0
  209. package/dist/tests/parallel-search.test.js +272 -0
  210. package/dist/tests/parameter-metadata.test.js +365 -0
  211. package/dist/tests/paths.test.js +177 -0
  212. package/dist/tests/progressive-disclosure.test.js +280 -0
  213. package/dist/tests/proposals.test.js +279 -0
  214. package/dist/tests/proposed-quality.test.js +271 -0
  215. package/dist/tests/provider-registry.test.js +32 -0
  216. package/dist/tests/ranking-regression.test.js +548 -0
  217. package/dist/tests/reflect-propose.test.js +455 -0
  218. package/dist/tests/registry-build-index.test.js +394 -0
  219. package/dist/tests/registry-cli.test.js +290 -0
  220. package/dist/tests/registry-index-v2.test.js +430 -0
  221. package/dist/tests/registry-install.test.js +728 -0
  222. package/dist/tests/registry-providers/parity.test.js +189 -0
  223. package/dist/tests/registry-providers/skills-sh.test.js +309 -0
  224. package/dist/tests/registry-providers/static-index.test.js +238 -0
  225. package/dist/tests/registry-resolve.test.js +126 -0
  226. package/dist/tests/registry-search.test.js +923 -0
  227. package/dist/tests/remember-frontmatter.test.js +378 -0
  228. package/dist/tests/remember-unit.test.js +123 -0
  229. package/dist/tests/ripgrep-install.test.js +251 -0
  230. package/dist/tests/ripgrep-resolve.test.js +108 -0
  231. package/dist/tests/ripgrep.test.js +163 -0
  232. package/dist/tests/save-command.test.js +94 -0
  233. package/dist/tests/save-trust-qa-fixes.test.js +270 -0
  234. package/dist/tests/scoring-pipeline.test.js +648 -0
  235. package/dist/tests/search-include-proposed-cli.test.js +118 -0
  236. package/dist/tests/self-update.test.js +442 -0
  237. package/dist/tests/semantic-search-e2e.test.js +512 -0
  238. package/dist/tests/semantic-status.test.js +471 -0
  239. package/dist/tests/setup-run.integration.js +877 -0
  240. package/dist/tests/setup-wizard.test.js +198 -0
  241. package/dist/tests/setup.test.js +131 -0
  242. package/dist/tests/source-add.test.js +11 -0
  243. package/dist/tests/source-clone.test.js +254 -0
  244. package/dist/tests/source-manage.test.js +366 -0
  245. package/dist/tests/source-providers/filesystem.test.js +82 -0
  246. package/dist/tests/source-providers/git.test.js +252 -0
  247. package/dist/tests/source-providers/website.test.js +128 -0
  248. package/dist/tests/source-qa-fixes.test.js +286 -0
  249. package/dist/tests/source-registry.test.js +350 -0
  250. package/dist/tests/source-resolve.test.js +100 -0
  251. package/dist/tests/source-source.test.js +281 -0
  252. package/dist/tests/source.test.js +533 -0
  253. package/dist/tests/tar-utils-scan.test.js +73 -0
  254. package/dist/tests/toggle-components.test.js +73 -0
  255. package/dist/tests/usage-telemetry.test.js +265 -0
  256. package/dist/tests/utility-scoring.test.js +558 -0
  257. package/dist/tests/vault-load-error.test.js +78 -0
  258. package/dist/tests/vault-qa-fixes.test.js +194 -0
  259. package/dist/tests/vault.test.js +429 -0
  260. package/dist/tests/vector-search.test.js +608 -0
  261. package/dist/tests/walker.test.js +252 -0
  262. package/dist/tests/wave2-cluster-bc.test.js +228 -0
  263. package/dist/tests/wave2-cluster-d.test.js +180 -0
  264. package/dist/tests/wave2-cluster-e.test.js +179 -0
  265. package/dist/tests/wiki-qa-fixes.test.js +270 -0
  266. package/dist/tests/wiki.test.js +529 -0
  267. package/dist/tests/workflow-cli.test.js +271 -0
  268. package/dist/tests/workflow-markdown.test.js +171 -0
  269. package/dist/tests/workflow-path-escape.test.js +132 -0
  270. package/dist/tests/workflow-qa-fixes.test.js +395 -0
  271. package/dist/tests/workflows/indexer-rejection.test.js +213 -0
  272. package/docs/README.md +8 -0
  273. package/docs/migration/release-notes/0.7.0.md +244 -0
  274. package/package.json +2 -2
  275. package/dist/core/warn.js +0 -27
  276. package/dist/output/shapes.js +0 -212
  277. package/dist/output/text.js +0 -520
  278. /package/dist/{commands → src/commands}/completions.js +0 -0
  279. /package/dist/{commands → src/commands}/curate.js +0 -0
  280. /package/dist/{commands → src/commands}/info.js +0 -0
  281. /package/dist/{commands → src/commands}/init.js +0 -0
  282. /package/dist/{commands → src/commands}/install-audit.js +0 -0
  283. /package/dist/{commands → src/commands}/migration-help.js +0 -0
  284. /package/dist/{commands → src/commands}/source-clone.js +0 -0
  285. /package/dist/{commands → src/commands}/vault.js +0 -0
  286. /package/dist/{core → src/core}/asset-registry.js +0 -0
  287. /package/dist/{core → src/core}/frontmatter.js +0 -0
  288. /package/dist/{core → src/core}/markdown.js +0 -0
  289. /package/dist/{core → src/core}/paths.js +0 -0
  290. /package/dist/{indexer → src/indexer}/manifest.js +0 -0
  291. /package/dist/{indexer → src/indexer}/search-fields.js +0 -0
  292. /package/dist/{indexer → src/indexer}/semantic-status.js +0 -0
  293. /package/dist/{indexer → src/indexer}/usage-events.js +0 -0
  294. /package/dist/{indexer → src/indexer}/walker.js +0 -0
  295. /package/dist/{llm → src/llm}/embedder.js +0 -0
  296. /package/dist/{llm → src/llm}/embedders/cache.js +0 -0
  297. /package/dist/{llm → src/llm}/embedders/local.js +0 -0
  298. /package/dist/{llm → src/llm}/embedders/types.js +0 -0
  299. /package/dist/{llm → src/llm}/metadata-enhance.js +0 -0
  300. /package/dist/{output → src/output}/context.js +0 -0
  301. /package/dist/{registry → src/registry}/create-provider-registry.js +0 -0
  302. /package/dist/{registry → src/registry}/origin-resolve.js +0 -0
  303. /package/dist/{registry → src/registry}/providers/index.js +0 -0
  304. /package/dist/{registry → src/registry}/providers/skills-sh.js +0 -0
  305. /package/dist/{registry → src/registry}/providers/types.js +0 -0
  306. /package/dist/{registry → src/registry}/types.js +0 -0
  307. /package/dist/{setup → src/setup}/detect.js +0 -0
  308. /package/dist/{setup → src/setup}/ripgrep-install.js +0 -0
  309. /package/dist/{setup → src/setup}/ripgrep-resolve.js +0 -0
  310. /package/dist/{setup → src/setup}/steps.js +0 -0
  311. /package/dist/{sources → src/sources}/include.js +0 -0
  312. /package/dist/{sources → src/sources}/provider-factory.js +0 -0
  313. /package/dist/{sources → src/sources}/provider.js +0 -0
  314. /package/dist/{sources → src/sources}/providers/filesystem.js +0 -0
  315. /package/dist/{sources → src/sources}/providers/index.js +0 -0
  316. /package/dist/{sources → src/sources}/providers/install-types.js +0 -0
  317. /package/dist/{sources → src/sources}/providers/npm.js +0 -0
  318. /package/dist/{sources → src/sources}/providers/provider-utils.js +0 -0
  319. /package/dist/{sources → src/sources}/providers/sync-from-ref.js +0 -0
  320. /package/dist/{sources → src/sources}/providers/tar-utils.js +0 -0
  321. /package/dist/{sources → src/sources}/providers/website.js +0 -0
  322. /package/dist/{sources → src/sources}/resolve.js +0 -0
  323. /package/dist/{sources → src/sources}/types.js +0 -0
  324. /package/dist/{templates → src/templates}/wiki-templates.js +0 -0
  325. /package/dist/{version.js → src/version.js} +0 -0
  326. /package/dist/{workflows → src/workflows}/authoring.js +0 -0
  327. /package/dist/{workflows → src/workflows}/cli.js +0 -0
  328. /package/dist/{workflows → src/workflows}/db.js +0 -0
  329. /package/dist/{workflows → src/workflows}/document-cache.js +0 -0
  330. /package/dist/{workflows → src/workflows}/parser.js +0 -0
  331. /package/dist/{workflows → src/workflows}/renderer.js +0 -0
  332. /package/dist/{workflows → src/workflows}/schema.js +0 -0
  333. /package/dist/{workflows → src/workflows}/validator.js +0 -0
@@ -0,0 +1,294 @@
1
+ import { afterAll, afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { akmShowUnified as akmShow } from "../../src/commands/show";
6
+ import { saveConfig } from "../../src/core/config";
7
+ // Trigger source-provider self-registration
8
+ import "../../src/sources/providers/index";
9
+ const createdTmpDirs = [];
10
+ function createTmpDir(prefix = "akm-show-") {
11
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
12
+ createdTmpDirs.push(dir);
13
+ return dir;
14
+ }
15
+ function writeFile(filePath, content = "") {
16
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
17
+ fs.writeFileSync(filePath, content);
18
+ }
19
+ afterAll(() => {
20
+ for (const dir of createdTmpDirs) {
21
+ fs.rmSync(dir, { recursive: true, force: true });
22
+ }
23
+ });
24
+ const originalXdgCacheHome = process.env.XDG_CACHE_HOME;
25
+ const originalXdgConfigHome = process.env.XDG_CONFIG_HOME;
26
+ const originalStashDir = process.env.AKM_STASH_DIR;
27
+ let testCacheDir = "";
28
+ let testConfigDir = "";
29
+ let stashDir = "";
30
+ beforeEach(() => {
31
+ testCacheDir = createTmpDir("akm-show-cache-");
32
+ testConfigDir = createTmpDir("akm-show-config-");
33
+ stashDir = createTmpDir("akm-show-stash-");
34
+ for (const sub of ["scripts", "skills", "commands", "agents", "knowledge"]) {
35
+ fs.mkdirSync(path.join(stashDir, sub), { recursive: true });
36
+ }
37
+ process.env.XDG_CACHE_HOME = testCacheDir;
38
+ process.env.XDG_CONFIG_HOME = testConfigDir;
39
+ process.env.AKM_STASH_DIR = stashDir;
40
+ });
41
+ afterEach(() => {
42
+ if (originalXdgCacheHome === undefined) {
43
+ delete process.env.XDG_CACHE_HOME;
44
+ }
45
+ else {
46
+ process.env.XDG_CACHE_HOME = originalXdgCacheHome;
47
+ }
48
+ if (originalXdgConfigHome === undefined) {
49
+ delete process.env.XDG_CONFIG_HOME;
50
+ }
51
+ else {
52
+ process.env.XDG_CONFIG_HOME = originalXdgConfigHome;
53
+ }
54
+ if (originalStashDir === undefined) {
55
+ delete process.env.AKM_STASH_DIR;
56
+ }
57
+ else {
58
+ process.env.AKM_STASH_DIR = originalStashDir;
59
+ }
60
+ if (testCacheDir) {
61
+ fs.rmSync(testCacheDir, { recursive: true, force: true });
62
+ testCacheDir = "";
63
+ }
64
+ if (testConfigDir) {
65
+ fs.rmSync(testConfigDir, { recursive: true, force: true });
66
+ testConfigDir = "";
67
+ }
68
+ });
69
+ // ── Installed ref with missing asset ─────────────────────────────────────────
70
+ describe("akmShow installed ref", () => {
71
+ test("throws with add guidance when origin is not installed", async () => {
72
+ const installedStashRoot = createTmpDir("akm-show-installed-root-");
73
+ // Create the type subdirectory so it is a valid stash root, but do NOT
74
+ // create the actual asset file.
75
+ fs.mkdirSync(path.join(installedStashRoot, "scripts"), { recursive: true });
76
+ saveConfig({
77
+ semanticSearchMode: "off",
78
+ installed: [
79
+ {
80
+ id: "test-pkg",
81
+ source: "npm",
82
+ ref: "test-pkg",
83
+ artifactUrl: "https://example.com/test-pkg.tgz",
84
+ stashRoot: installedStashRoot,
85
+ cacheDir: installedStashRoot,
86
+ installedAt: new Date().toISOString(),
87
+ },
88
+ ],
89
+ });
90
+ // Use an origin that is NOT installed so resolveSourcesForOrigin returns
91
+ // empty, triggering the add-guidance error path.
92
+ await expect(akmShow({ ref: "npm:@other/missing-pkg//script:missing.sh" })).rejects.toThrow(/akm add/);
93
+ });
94
+ test("resolves installed-stash style nested agent refs", async () => {
95
+ const installedStashRoot = createTmpDir("akm-show-installed-agent-");
96
+ writeFile(path.join(installedStashRoot, "tools", "agents", "svelte-file-editor.md"), ["---", "name: svelte-file-editor", "description: Svelte editor", "---", "Use Svelte tools."].join("\n"));
97
+ saveConfig({
98
+ semanticSearchMode: "off",
99
+ installed: [
100
+ {
101
+ id: "github:sveltejs/ai-tools",
102
+ source: "github",
103
+ ref: "github:sveltejs/ai-tools",
104
+ artifactUrl: "https://example.com/svelte-tools.tgz",
105
+ stashRoot: installedStashRoot,
106
+ cacheDir: installedStashRoot,
107
+ installedAt: new Date().toISOString(),
108
+ },
109
+ ],
110
+ });
111
+ const result = await akmShow({ ref: "github:sveltejs/ai-tools//agent:tools/agents/svelte-file-editor" });
112
+ expect(result.type).toBe("agent");
113
+ expect(result.origin).toBe("github:sveltejs/ai-tools");
114
+ expect(result.path).toContain(path.join("tools", "agents", "svelte-file-editor.md"));
115
+ expect(result.prompt).toContain("Use Svelte tools.");
116
+ });
117
+ test("resolves installed-stash style nested skill refs", async () => {
118
+ const installedStashRoot = createTmpDir("akm-show-installed-skill-");
119
+ writeFile(path.join(installedStashRoot, "tools", "skills", "svelte-code-writer", "SKILL.md"), ["---", "name: svelte-code-writer", "description: Svelte writer", "---", "# Svelte writer"].join("\n"));
120
+ saveConfig({
121
+ semanticSearchMode: "off",
122
+ installed: [
123
+ {
124
+ id: "github:sveltejs/ai-tools",
125
+ source: "github",
126
+ ref: "github:sveltejs/ai-tools",
127
+ artifactUrl: "https://example.com/svelte-tools.tgz",
128
+ stashRoot: installedStashRoot,
129
+ cacheDir: installedStashRoot,
130
+ installedAt: new Date().toISOString(),
131
+ },
132
+ ],
133
+ });
134
+ const result = await akmShow({ ref: "github:sveltejs/ai-tools//skill:tools/skills/svelte-code-writer" });
135
+ expect(result.type).toBe("skill");
136
+ expect(result.origin).toBe("github:sveltejs/ai-tools");
137
+ expect(result.path).toContain(path.join("tools", "skills", "svelte-code-writer", "SKILL.md"));
138
+ expect(result.content).toContain("# Svelte writer");
139
+ });
140
+ });
141
+ // ── Search path resolution ───────────────────────────────────────────────────
142
+ describe("akmShow search path", () => {
143
+ test("resolves from search path directories", async () => {
144
+ const searchPathDir = createTmpDir("akm-show-searchpath-");
145
+ writeFile(path.join(searchPathDir, "scripts", "deploy.sh"), "#!/usr/bin/env bash\necho deploy\n");
146
+ saveConfig({ semanticSearchMode: "off", sources: [{ type: "filesystem", path: searchPathDir }] });
147
+ const result = await akmShow({ ref: "script:deploy.sh" });
148
+ expect(result.type).toBe("script");
149
+ expect(result.name).toBe("deploy.sh");
150
+ expect(result.path).toContain(searchPathDir);
151
+ });
152
+ });
153
+ // ── editability flags ────────────────────────────────────────────────────────
154
+ describe("akmShow editability", () => {
155
+ test("working stash asset has editable true", async () => {
156
+ writeFile(path.join(stashDir, "scripts", "local.sh"), "#!/usr/bin/env bash\necho local\n");
157
+ saveConfig({ semanticSearchMode: "off" });
158
+ const result = await akmShow({ ref: "script:local.sh" });
159
+ expect(result.type).toBe("script");
160
+ expect(result.origin).toBeNull();
161
+ expect(result.action).toContain("Execute the run command");
162
+ expect(result.editable).toBe(true);
163
+ expect(result.editHint).toBeUndefined();
164
+ });
165
+ test("search path asset has editable true", async () => {
166
+ const searchPathDir = createTmpDir("akm-show-searchpath-editable-");
167
+ writeFile(path.join(searchPathDir, "scripts", "remote.sh"), "#!/usr/bin/env bash\necho remote\n");
168
+ saveConfig({ semanticSearchMode: "off", sources: [{ type: "filesystem", path: searchPathDir }] });
169
+ const result = await akmShow({ ref: "script:remote.sh" });
170
+ expect(result.type).toBe("script");
171
+ expect(result.origin).toBeNull();
172
+ expect(result.editable).toBe(true);
173
+ expect(result.editHint).toBeUndefined();
174
+ });
175
+ test("installed (cache-managed) asset has editable false with editHint", async () => {
176
+ const installedStashRoot = createTmpDir("akm-show-installed-resolve-");
177
+ writeFile(path.join(installedStashRoot, "scripts", "deploy.sh"), "#!/usr/bin/env bash\necho deploy\n");
178
+ saveConfig({
179
+ semanticSearchMode: "off",
180
+ installed: [
181
+ {
182
+ id: "installed-pkg",
183
+ source: "npm",
184
+ ref: "npm:installed-pkg",
185
+ artifactUrl: "https://example.com/installed-pkg.tgz",
186
+ stashRoot: installedStashRoot,
187
+ cacheDir: installedStashRoot,
188
+ installedAt: new Date().toISOString(),
189
+ },
190
+ ],
191
+ });
192
+ const result = await akmShow({ ref: "script:deploy.sh" });
193
+ expect(result.type).toBe("script");
194
+ expect(result.origin).toBe("installed-pkg");
195
+ expect(result.editable).toBe(false);
196
+ expect(result.editHint).toContain("akm clone");
197
+ expect(result.editHint).toContain("script:deploy.sh");
198
+ });
199
+ });
200
+ // ── Content-based classification via new renderer pipeline ─────────────────
201
+ describe("akmShow content-based classification", () => {
202
+ test("model alone in commands/ stays a command (directory wins over weak agent signal)", async () => {
203
+ // model is shared frontmatter (OpenCode convention). In commands/,
204
+ // the directory matcher (specificity 10) beats the model-only agent
205
+ // signal (specificity 8), so this stays a command.
206
+ writeFile(path.join(stashDir, "commands", "deploy.md"), ["---", "model: gpt-4", "description: Deploy command", "---", "Deploy $ARGUMENTS."].join("\n"));
207
+ saveConfig({ semanticSearchMode: "off" });
208
+ const result = await akmShow({ ref: "command:deploy.md" });
209
+ expect(result.type).toBe("command");
210
+ expect(result.template).toBe("Deploy $ARGUMENTS.");
211
+ expect(result.modelHint).toBe("gpt-4");
212
+ expect(result.parameters).toEqual(["ARGUMENTS"]);
213
+ });
214
+ test("tools frontmatter in commands/ overrides to agent (strong signal)", async () => {
215
+ // tools/toolPolicy are agent-exclusive signals at specificity 20,
216
+ // which beats the commands/ directory matcher at 10.
217
+ writeFile(path.join(stashDir, "commands", "hybrid.md"), ["---", "tools:", " read: allow", "model: gpt-4", "---", "You are a hybrid agent."].join("\n"));
218
+ saveConfig({ semanticSearchMode: "off" });
219
+ const result = await akmShow({ ref: "command:hybrid.md" });
220
+ expect(result.type).toBe("agent");
221
+ expect(result.action).toContain("verbatim");
222
+ expect(result.prompt).toContain("You are a hybrid agent.");
223
+ });
224
+ test("command in commands/ directory extracts OpenCode-style frontmatter", async () => {
225
+ writeFile(path.join(stashDir, "commands", "deploy.md"), [
226
+ "---",
227
+ "description: Deploy to production",
228
+ "model: claude-sonnet-4-20250514",
229
+ "agent: build",
230
+ "---",
231
+ "Deploy $ARGUMENTS to production.",
232
+ ].join("\n"));
233
+ saveConfig({ semanticSearchMode: "off" });
234
+ const result = await akmShow({ ref: "command:deploy.md" });
235
+ expect(result.type).toBe("command");
236
+ expect(result.template).toBe("Deploy $ARGUMENTS to production.");
237
+ expect(result.description).toBe("Deploy to production");
238
+ expect(result.modelHint).toBe("claude-sonnet-4-20250514");
239
+ expect(result.agent).toBe("build");
240
+ expect(result.parameters).toEqual(["ARGUMENTS"]);
241
+ });
242
+ test("command parameter extraction includes positional placeholders", async () => {
243
+ writeFile(path.join(stashDir, "commands", "positional.md"), ["---", "description: Positional args", "---", "Run release $1 with notes from $2 and flag $9."].join("\n"));
244
+ saveConfig({ semanticSearchMode: "off" });
245
+ const result = await akmShow({ ref: "command:positional.md" });
246
+ expect(result.type).toBe("command");
247
+ expect(result.parameters).toEqual(["$1", "$2", "$9"]);
248
+ });
249
+ test("command parameter extraction includes named placeholders", async () => {
250
+ writeFile(path.join(stashDir, "commands", "named.md"), ["---", "description: Named args", "---", "Deploy {{env}} with {{version}} using {{env}} again."].join("\n"));
251
+ saveConfig({ semanticSearchMode: "off" });
252
+ const result = await akmShow({ ref: "command:named.md" });
253
+ expect(result.type).toBe("command");
254
+ expect(result.parameters).toEqual(["env", "version"]);
255
+ });
256
+ test("script in scripts/ directory uses new renderer pipeline", async () => {
257
+ writeFile(path.join(stashDir, "scripts", "build.sh"), "#!/usr/bin/env bash\necho build\n");
258
+ saveConfig({ semanticSearchMode: "off" });
259
+ const result = await akmShow({ ref: "script:build.sh" });
260
+ expect(result.type).toBe("script");
261
+ expect(result.run).toBeDefined();
262
+ expect(result.run).toContain("bash");
263
+ });
264
+ test("$ARGUMENTS in body classifies .md as command even outside commands/", async () => {
265
+ writeFile(path.join(stashDir, "knowledge", "deploy-cmd.md"), ["---", "description: Deploy helper", "---", "Deploy $ARGUMENTS to staging."].join("\n"));
266
+ saveConfig({ semanticSearchMode: "off" });
267
+ // $ARGUMENTS placeholder (specificity 18) beats knowledge/ directory hint (10)
268
+ const result = await akmShow({ ref: "knowledge:deploy-cmd.md" });
269
+ expect(result.type).toBe("command");
270
+ expect(result.template).toBe("Deploy $ARGUMENTS to staging.");
271
+ expect(result.description).toBe("Deploy helper");
272
+ });
273
+ test("agent frontmatter classifies .md as command even outside commands/", async () => {
274
+ writeFile(path.join(stashDir, "agents", "build-cmd.md"), ["---", "agent: build", "description: Build dispatch", "---", "Build the project."].join("\n"));
275
+ saveConfig({ semanticSearchMode: "off" });
276
+ // agent frontmatter (specificity 18) beats agents/ directory hint (15)
277
+ const result = await akmShow({ ref: "agent:build-cmd.md" });
278
+ expect(result.type).toBe("command");
279
+ expect(result.template).toBe("Build the project.");
280
+ expect(result.agent).toBe("build");
281
+ });
282
+ test("knowledge view modes work through new renderer pipeline", async () => {
283
+ writeFile(path.join(stashDir, "knowledge", "guide.md"), ["# Intro", "Welcome.", "", "## Setup", "Install things.", "", "## Usage", "Use things."].join("\n"));
284
+ saveConfig({ semanticSearchMode: "off" });
285
+ const tocResult = await akmShow({ ref: "knowledge:guide.md", view: { mode: "toc" } });
286
+ expect(tocResult.content).toContain("Intro");
287
+ expect(tocResult.content).toContain("Setup");
288
+ const sectionResult = await akmShow({
289
+ ref: "knowledge:guide.md",
290
+ view: { mode: "section", heading: "Setup" },
291
+ });
292
+ expect(sectionResult.content).toContain("Install things.");
293
+ });
294
+ });
@@ -0,0 +1,266 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { hasErrnoCode, isAssetType, isWithin, jsonWithByteCap, ResponseTooLargeError, readBodyWithByteCap, resolveStashDir, toPosix, } from "../src/core/common";
6
+ // ── resolveStashDir ──────────────────────────────────────────────────────────
7
+ describe("resolveStashDir", () => {
8
+ const origEnv = process.env.AKM_STASH_DIR;
9
+ const origXdgConfigHome = process.env.XDG_CONFIG_HOME;
10
+ const origHome = process.env.HOME;
11
+ let testConfigHome;
12
+ beforeEach(() => {
13
+ testConfigHome = fs.mkdtempSync(path.join(os.tmpdir(), "akm-common-test-config-"));
14
+ process.env.XDG_CONFIG_HOME = testConfigHome;
15
+ });
16
+ afterEach(() => {
17
+ if (origEnv === undefined) {
18
+ delete process.env.AKM_STASH_DIR;
19
+ }
20
+ else {
21
+ process.env.AKM_STASH_DIR = origEnv;
22
+ }
23
+ if (origXdgConfigHome === undefined) {
24
+ delete process.env.XDG_CONFIG_HOME;
25
+ }
26
+ else {
27
+ process.env.XDG_CONFIG_HOME = origXdgConfigHome;
28
+ }
29
+ if (origHome === undefined) {
30
+ delete process.env.HOME;
31
+ }
32
+ else {
33
+ process.env.HOME = origHome;
34
+ }
35
+ if (testConfigHome) {
36
+ fs.rmSync(testConfigHome, { recursive: true, force: true });
37
+ }
38
+ });
39
+ test("throws when no stash dir is configured and default does not exist", () => {
40
+ delete process.env.AKM_STASH_DIR;
41
+ // Point HOME to a tmp dir without an akm subdirectory
42
+ const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "akm-common-test-home-"));
43
+ process.env.HOME = tmpHome;
44
+ try {
45
+ expect(() => resolveStashDir()).toThrow("No stash directory found");
46
+ }
47
+ finally {
48
+ fs.rmSync(tmpHome, { recursive: true, force: true });
49
+ }
50
+ });
51
+ test("throws when AKM_STASH_DIR points to nonexistent path", () => {
52
+ process.env.AKM_STASH_DIR = "/nonexistent/path/that/does/not/exist";
53
+ expect(() => resolveStashDir()).toThrow("Unable to read");
54
+ });
55
+ test("throws when AKM_STASH_DIR path is a file, not a directory", () => {
56
+ const tmpFile = path.join(os.tmpdir(), `akm-common-test-file-${Date.now()}`);
57
+ fs.writeFileSync(tmpFile, "not a directory");
58
+ try {
59
+ process.env.AKM_STASH_DIR = tmpFile;
60
+ expect(() => resolveStashDir()).toThrow("must point to a directory");
61
+ }
62
+ finally {
63
+ fs.unlinkSync(tmpFile);
64
+ }
65
+ });
66
+ test("returns resolved path for valid AKM_STASH_DIR", () => {
67
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-common-test-"));
68
+ try {
69
+ process.env.AKM_STASH_DIR = tmpDir;
70
+ const result = resolveStashDir();
71
+ expect(result).toBe(path.resolve(tmpDir));
72
+ }
73
+ finally {
74
+ fs.rmSync(tmpDir, { recursive: true, force: true });
75
+ }
76
+ });
77
+ test("reads stashDir from config.json when env var is not set", () => {
78
+ delete process.env.AKM_STASH_DIR;
79
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-common-test-stash-"));
80
+ try {
81
+ const configDir = path.join(testConfigHome, "akm");
82
+ fs.mkdirSync(configDir, { recursive: true });
83
+ fs.writeFileSync(path.join(configDir, "config.json"), JSON.stringify({ stashDir: tmpDir }));
84
+ const result = resolveStashDir();
85
+ expect(result).toBe(path.resolve(tmpDir));
86
+ }
87
+ finally {
88
+ fs.rmSync(tmpDir, { recursive: true, force: true });
89
+ }
90
+ });
91
+ test("uses default stash dir when it exists", () => {
92
+ delete process.env.AKM_STASH_DIR;
93
+ const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "akm-common-test-home-"));
94
+ const defaultStash = path.join(tmpHome, "akm");
95
+ fs.mkdirSync(defaultStash, { recursive: true });
96
+ process.env.HOME = tmpHome;
97
+ try {
98
+ const result = resolveStashDir();
99
+ expect(result).toBe(defaultStash);
100
+ }
101
+ finally {
102
+ fs.rmSync(tmpHome, { recursive: true, force: true });
103
+ }
104
+ });
105
+ test("env var takes precedence over config.json stashDir", () => {
106
+ const envDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-common-test-env-"));
107
+ const configDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-common-test-cfg-"));
108
+ try {
109
+ process.env.AKM_STASH_DIR = envDir;
110
+ const configRoot = path.join(testConfigHome, "akm");
111
+ fs.mkdirSync(configRoot, { recursive: true });
112
+ fs.writeFileSync(path.join(configRoot, "config.json"), JSON.stringify({ stashDir: configDir }));
113
+ const result = resolveStashDir();
114
+ expect(result).toBe(path.resolve(envDir));
115
+ }
116
+ finally {
117
+ fs.rmSync(envDir, { recursive: true, force: true });
118
+ fs.rmSync(configDir, { recursive: true, force: true });
119
+ }
120
+ });
121
+ });
122
+ // ── toPosix ──────────────────────────────────────────────────────────────────
123
+ describe("toPosix", () => {
124
+ test("already-posix paths are unchanged", () => {
125
+ expect(toPosix("foo/bar/baz")).toBe("foo/bar/baz");
126
+ });
127
+ test("backslash paths are converted to forward slashes", () => {
128
+ expect(toPosix("foo\\bar\\baz")).toBe("foo/bar/baz");
129
+ });
130
+ test("mixed separators are normalized", () => {
131
+ expect(toPosix("foo\\bar/baz")).toBe("foo/bar/baz");
132
+ });
133
+ test("empty string returns empty string", () => {
134
+ expect(toPosix("")).toBe("");
135
+ });
136
+ });
137
+ // ── hasErrnoCode ─────────────────────────────────────────────────────────────
138
+ describe("hasErrnoCode", () => {
139
+ test("returns true for error with matching code", () => {
140
+ const err = Object.assign(new Error("fail"), { code: "ENOENT" });
141
+ expect(hasErrnoCode(err, "ENOENT")).toBe(true);
142
+ });
143
+ test("returns false for error with non-matching code", () => {
144
+ const err = Object.assign(new Error("fail"), { code: "EACCES" });
145
+ expect(hasErrnoCode(err, "ENOENT")).toBe(false);
146
+ });
147
+ test("returns false for string error", () => {
148
+ expect(hasErrnoCode("some string error", "ENOENT")).toBe(false);
149
+ });
150
+ test("returns false for null", () => {
151
+ expect(hasErrnoCode(null, "ENOENT")).toBe(false);
152
+ });
153
+ test("returns false for object without code property", () => {
154
+ expect(hasErrnoCode({ message: "fail" }, "ENOENT")).toBe(false);
155
+ });
156
+ test("returns false for undefined", () => {
157
+ expect(hasErrnoCode(undefined, "ENOENT")).toBe(false);
158
+ });
159
+ });
160
+ // ── isAssetType ──────────────────────────────────────────────────────────────
161
+ describe("isAssetType", () => {
162
+ test("returns true for all valid types", () => {
163
+ expect(isAssetType("skill")).toBe(true);
164
+ expect(isAssetType("command")).toBe(true);
165
+ expect(isAssetType("agent")).toBe(true);
166
+ expect(isAssetType("knowledge")).toBe(true);
167
+ expect(isAssetType("script")).toBe(true);
168
+ });
169
+ test("returns false for invalid strings", () => {
170
+ expect(isAssetType("widget")).toBe(false);
171
+ expect(isAssetType("")).toBe(false);
172
+ expect(isAssetType("tool")).toBe(false);
173
+ expect(isAssetType("plugin")).toBe(false);
174
+ });
175
+ });
176
+ // ── isWithin ────────────────────────────────────────────────────────────────
177
+ describe("isWithin", () => {
178
+ test("returns true for path inside root", () => {
179
+ expect(isWithin("/root/sub/file.txt", "/root")).toBe(true);
180
+ });
181
+ test("returns true for path equal to root", () => {
182
+ expect(isWithin("/root", "/root")).toBe(true);
183
+ });
184
+ test("returns false for path outside root", () => {
185
+ expect(isWithin("/other/file.txt", "/root")).toBe(false);
186
+ });
187
+ test("returns false for parent traversal", () => {
188
+ expect(isWithin("/root/../etc/passwd", "/root")).toBe(false);
189
+ });
190
+ test("returns true for nested subdirectory", () => {
191
+ expect(isWithin("/root/a/b/c/d.txt", "/root")).toBe(true);
192
+ });
193
+ test("returns false for sibling directory with similar prefix", () => {
194
+ expect(isWithin("/root-other/file.txt", "/root")).toBe(false);
195
+ });
196
+ });
197
+ // ── readBodyWithByteCap / jsonWithByteCap ────────────────────────────────────
198
+ describe("readBodyWithByteCap", () => {
199
+ function makeResponse(body, init) {
200
+ const res = new Response(body, { headers: init?.headers });
201
+ if (init?.url)
202
+ Object.defineProperty(res, "url", { value: init.url });
203
+ return res;
204
+ }
205
+ test("reads small bodies verbatim", async () => {
206
+ const text = await readBodyWithByteCap(makeResponse("hello world"), 1024);
207
+ expect(text).toBe("hello world");
208
+ });
209
+ test("handles empty bodies", async () => {
210
+ const text = await readBodyWithByteCap(makeResponse(""), 1024);
211
+ expect(text).toBe("");
212
+ });
213
+ test("refuses before reading if Content-Length exceeds cap", async () => {
214
+ const body = "x".repeat(1000);
215
+ const response = makeResponse(body, {
216
+ headers: { "content-length": "1000" },
217
+ url: "http://example.invalid/too-big",
218
+ });
219
+ await expect(readBodyWithByteCap(response, 100)).rejects.toBeInstanceOf(ResponseTooLargeError);
220
+ });
221
+ test("aborts mid-stream when Content-Length is absent but body exceeds cap", async () => {
222
+ const stream = new ReadableStream({
223
+ start(controller) {
224
+ // 5 chunks of 1000 bytes each = 5000 bytes; cap at 2500.
225
+ for (let i = 0; i < 5; i++) {
226
+ controller.enqueue(new TextEncoder().encode("x".repeat(1000)));
227
+ }
228
+ controller.close();
229
+ },
230
+ });
231
+ const response = new Response(stream);
232
+ await expect(readBodyWithByteCap(response, 2500)).rejects.toBeInstanceOf(ResponseTooLargeError);
233
+ });
234
+ test("accepts body right at the cap", async () => {
235
+ const body = "x".repeat(100);
236
+ const text = await readBodyWithByteCap(makeResponse(body), 100);
237
+ expect(text.length).toBe(100);
238
+ });
239
+ test("decodes multi-chunk UTF-8 bodies correctly", async () => {
240
+ const stream = new ReadableStream({
241
+ start(controller) {
242
+ controller.enqueue(new TextEncoder().encode("hello "));
243
+ controller.enqueue(new TextEncoder().encode("world"));
244
+ controller.close();
245
+ },
246
+ });
247
+ const response = new Response(stream);
248
+ const text = await readBodyWithByteCap(response, 1024);
249
+ expect(text).toBe("hello world");
250
+ });
251
+ });
252
+ describe("jsonWithByteCap", () => {
253
+ test("parses small JSON bodies", async () => {
254
+ const data = await jsonWithByteCap(new Response(JSON.stringify({ hello: "world" })), 1024);
255
+ expect(data.hello).toBe("world");
256
+ });
257
+ test("rejects oversized JSON before parse", async () => {
258
+ const big = JSON.stringify({ data: "x".repeat(2000) });
259
+ const response = new Response(big, { headers: { "content-length": String(big.length) } });
260
+ await expect(jsonWithByteCap(response, 500)).rejects.toBeInstanceOf(ResponseTooLargeError);
261
+ });
262
+ test("propagates JSON.parse errors for malformed input", async () => {
263
+ const response = new Response("{not json");
264
+ await expect(jsonWithByteCap(response, 1024)).rejects.toThrow();
265
+ });
266
+ });