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,1419 +0,0 @@
1
- /**
2
- * End-to-end tests that replicate real-world usage of akm.
3
- *
4
- * Uses realistic fixtures in tests/fixtures/ representing a typical user's
5
- * stash directory with scripts, skills, commands, and agents.
6
- *
7
- * Tests cover:
8
- * - Full lifecycle: index → search → show
9
- * - CLI interface via subprocess
10
- * - Metadata generation and persistence
11
- * - Semantic search ranking quality
12
- * - Ripgrep pre-filtering
13
- * - Multi-script directories with .stash.json
14
- * - Graceful degradation (no index, no ripgrep)
15
- * - Edge cases and error handling
16
- */
17
- import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
18
- import { spawnSync } from "node:child_process";
19
- import fs from "node:fs";
20
- import os from "node:os";
21
- import path from "node:path";
22
- import { akmSearch } from "../src/commands/search";
23
- import { akmShowUnified as akmShow } from "../src/commands/show";
24
- import { loadConfig, saveConfig } from "../src/core/config";
25
- import { closeDatabase, DB_VERSION, getAllEntries, getMeta, openDatabase } from "../src/indexer/db";
26
- import { akmIndex } from "../src/indexer/indexer";
27
- import { loadStashFile } from "../src/indexer/metadata";
28
- // ── Helpers ─────────────────────────────────────────────────────────────────
29
- const FIXTURES = path.join(__dirname, "fixtures");
30
- const CLI = path.join(__dirname, "..", "src", "cli.ts");
31
- function expectDefined(value) {
32
- expect(value).toBeDefined();
33
- if (value === undefined || value === null) {
34
- throw new Error("Expected value to be defined");
35
- }
36
- return value;
37
- }
38
- function isLocalHit(hit) {
39
- return hit.type !== "registry";
40
- }
41
- function copyFixturesToTmp() {
42
- const tmpStash = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-"));
43
- copyDirRecursive(FIXTURES, tmpStash);
44
- return tmpStash;
45
- }
46
- // `tests/fixtures/stashes/` is the shared fixture-stash tree (issue #235); it
47
- // must not be copied into the per-scenario e2e fixture root or its assets
48
- // will leak into search results.
49
- const E2E_FIXTURE_SKIP_AT_ROOT = new Set(["stashes"]);
50
- function copyDirRecursive(src, dest, depth = 0) {
51
- fs.mkdirSync(dest, { recursive: true });
52
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
53
- if (depth === 0 && E2E_FIXTURE_SKIP_AT_ROOT.has(entry.name))
54
- continue;
55
- const srcPath = path.join(src, entry.name);
56
- const destPath = path.join(dest, entry.name);
57
- if (entry.isDirectory()) {
58
- copyDirRecursive(srcPath, destPath, depth + 1);
59
- }
60
- else {
61
- fs.copyFileSync(srcPath, destPath);
62
- }
63
- }
64
- }
65
- function runCli(...args) {
66
- const result = spawnSync("bun", [CLI, ...args], {
67
- encoding: "utf8",
68
- timeout: 30_000,
69
- env: { ...process.env },
70
- });
71
- return {
72
- stdout: result.stdout?.trim() ?? "",
73
- stderr: result.stderr?.trim() ?? "",
74
- exitCode: result.status ?? 1,
75
- };
76
- }
77
- function parseJson(text) {
78
- return JSON.parse(text);
79
- }
80
- function createEmptyStashDir(prefix) {
81
- const stashDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
82
- for (const sub of ["skills", "commands", "agents", "knowledge", "scripts"]) {
83
- fs.mkdirSync(path.join(stashDir, sub), { recursive: true });
84
- }
85
- return stashDir;
86
- }
87
- async function withMockedFetch(handler, run) {
88
- const originalFetch = globalThis.fetch;
89
- globalThis.fetch = (async (input) => {
90
- const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
91
- return handler(url);
92
- });
93
- try {
94
- return await run();
95
- }
96
- finally {
97
- globalThis.fetch = originalFetch;
98
- }
99
- }
100
- const originalXdgCacheHome = process.env.XDG_CACHE_HOME;
101
- const originalXdgConfigHome = process.env.XDG_CONFIG_HOME;
102
- const originalAkmStashDir = process.env.AKM_STASH_DIR;
103
- let testCacheDir = "";
104
- let testConfigDir = "";
105
- beforeAll(async () => {
106
- testCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-cache-"));
107
- testConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-config-"));
108
- process.env.XDG_CACHE_HOME = testCacheDir;
109
- process.env.XDG_CONFIG_HOME = testConfigDir;
110
- });
111
- beforeEach(() => {
112
- // Re-create per-test config dir for isolation (describe-level beforeAll
113
- // already set XDG_CONFIG_HOME so akmIndex doesn't read real user config)
114
- if (testConfigDir && fs.existsSync(testConfigDir)) {
115
- fs.rmSync(testConfigDir, { recursive: true, force: true });
116
- }
117
- testConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-config-"));
118
- process.env.XDG_CONFIG_HOME = testConfigDir;
119
- });
120
- afterAll(() => {
121
- if (originalXdgCacheHome === undefined) {
122
- delete process.env.XDG_CACHE_HOME;
123
- }
124
- else {
125
- process.env.XDG_CACHE_HOME = originalXdgCacheHome;
126
- }
127
- if (originalXdgConfigHome === undefined) {
128
- delete process.env.XDG_CONFIG_HOME;
129
- }
130
- else {
131
- process.env.XDG_CONFIG_HOME = originalXdgConfigHome;
132
- }
133
- if (originalAkmStashDir === undefined) {
134
- delete process.env.AKM_STASH_DIR;
135
- }
136
- else {
137
- process.env.AKM_STASH_DIR = originalAkmStashDir;
138
- }
139
- if (testCacheDir) {
140
- fs.rmSync(testCacheDir, { recursive: true, force: true });
141
- testCacheDir = "";
142
- }
143
- if (testConfigDir) {
144
- fs.rmSync(testConfigDir, { recursive: true, force: true });
145
- testConfigDir = "";
146
- }
147
- });
148
- afterEach(() => {
149
- // Don't restore XDG_CONFIG_HOME here — it's managed by beforeAll/afterAll.
150
- // Restoring it between tests would expose real user config to describe-level
151
- // beforeAll hooks that run between describe blocks.
152
- if (testConfigDir) {
153
- fs.rmSync(testConfigDir, { recursive: true, force: true });
154
- testConfigDir = "";
155
- }
156
- });
157
- // ═══════════════════════════════════════════════════════════════════════════
158
- // Scenario 1: Full lifecycle — user sets up stash, indexes, searches, runs
159
- // ═══════════════════════════════════════════════════════════════════════════
160
- describe("Scenario: Full lifecycle (index → search → show)", () => {
161
- let stashDir;
162
- let scenarioCacheDir;
163
- beforeAll(async () => {
164
- stashDir = copyFixturesToTmp();
165
- scenarioCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-cache-s1-"));
166
- process.env.AKM_STASH_DIR = stashDir;
167
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
168
- });
169
- beforeEach(() => {
170
- process.env.AKM_STASH_DIR = stashDir;
171
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
172
- });
173
- afterAll(() => {
174
- fs.rmSync(stashDir, { recursive: true, force: true });
175
- fs.rmSync(scenarioCacheDir, { recursive: true, force: true });
176
- });
177
- test("search works without index (substring fallback)", async () => {
178
- const result = await akmSearch({ query: "deploy", type: "script" });
179
- expect(result.hits.length).toBeGreaterThan(0);
180
- expect(result.hits.some((h) => h.name.includes("deploy"))).toBe(true);
181
- // Substring mode computes a relevance score
182
- expect(result.hits[0].score).toBeGreaterThan(0);
183
- expect(result.hits[0].score).toBeLessThanOrEqual(1);
184
- });
185
- test("index generates metadata and builds search index", async () => {
186
- const result = await akmIndex({ stashDir });
187
- expect(result.stashDir).toBe(stashDir);
188
- expect(result.totalEntries).toBeGreaterThanOrEqual(8);
189
- expect(result.generatedMetadata).toBeGreaterThan(0);
190
- expect(fs.existsSync(result.indexPath)).toBe(true);
191
- });
192
- test("index generates metadata in database for directories that lack .stash.json", async () => {
193
- // git/ directory had no .stash.json — metadata generated in DB only
194
- expect(fs.existsSync(path.join(stashDir, "scripts", "git", ".stash.json"))).toBe(false);
195
- const db = openDatabase();
196
- const entries = getAllEntries(db, "script");
197
- const gitEntries = entries.filter((e) => e.dirPath.includes(path.join("scripts", "git")));
198
- expect(gitEntries.length).toBeGreaterThanOrEqual(2);
199
- // Each generated entry should be marked
200
- for (const e of gitEntries) {
201
- expect(e.entry.quality).toBe("generated");
202
- expect(e.entry.type).toBe("script");
203
- expect(e.entry.filename).toBeTruthy();
204
- }
205
- closeDatabase(db);
206
- });
207
- test("index preserves hand-written .stash.json (docker/ has intent fields)", async () => {
208
- const dockerStash = loadStashFile(path.join(stashDir, "scripts", "docker"));
209
- expect(dockerStash).not.toBeNull();
210
- expect(dockerStash?.entries.length).toBe(2);
211
- // These were hand-written, should NOT have generated flag
212
- expect(dockerStash?.entries[0].quality).not.toBe("generated");
213
- expect(dockerStash?.entries[0].intent).toBeDefined();
214
- expect(dockerStash?.entries[0].intent?.when).toBeTruthy();
215
- });
216
- test("index extracts description from code comments", async () => {
217
- const db = openDatabase();
218
- const entries = getAllEntries(db, "script");
219
- const diffEntry = entries.find((e) => e.entry.name.includes("summarize-diff"));
220
- expect(diffEntry).toBeDefined();
221
- // Should have extracted the JSDoc comment as description
222
- expect(diffEntry?.entry.description).toBeTruthy();
223
- expect(diffEntry?.entry.description?.toLowerCase()).toContain("git diff");
224
- closeDatabase(db);
225
- });
226
- test("index extracts metadata from package.json", async () => {
227
- const db = openDatabase();
228
- const entries = getAllEntries(db, "script");
229
- const lintEntry = entries.find((e) => e.entry.name.includes("eslint-check"));
230
- expect(lintEntry).toBeDefined();
231
- // package.json had description and keywords
232
- expect(lintEntry?.entry.description).toContain("ESLint");
233
- expect(lintEntry?.entry.tags).toContain("eslint");
234
- closeDatabase(db);
235
- });
236
- test("search with index returns scored results with descriptions", async () => {
237
- const result = await akmSearch({ query: "docker build image", type: "any" });
238
- expect(result.schemaVersion).toBe(1);
239
- expect(result.hits.length).toBeGreaterThan(0);
240
- // Docker-build should be ranked first
241
- const topHit = result.hits[0];
242
- expect(topHit.name).toContain("docker");
243
- expect(topHit.score).toBeDefined();
244
- expect(topHit.score).toBeGreaterThan(0);
245
- expect(topHit.description).toBeTruthy();
246
- });
247
- test.skipIf(!!process.env.CI)("search ranks keyword-relevant results higher (FTS-only)", async () => {
248
- // NOTE: This test uses FTS keyword matching (BM25), not semantic/vector search.
249
- const result = await akmSearch({ query: "summarize commit changes", type: "any" });
250
- expect(result.hits.length).toBeGreaterThan(0);
251
- // Git scripts should rank higher than docker scripts for this query
252
- const topNames = result.hits.slice(0, 5).map((h) => h.name.toLowerCase());
253
- const hasGitRelated = topNames.some((n) => n.includes("git") || n.includes("diff") || n.includes("commit") || n.includes("summarize"));
254
- expect(hasGitRelated).toBe(true);
255
- });
256
- test("search type filter restricts results to that type", async () => {
257
- const skillResult = await akmSearch({ query: "review", type: "skill" });
258
- expect(skillResult.hits.every((h) => h.type === "skill")).toBe(true);
259
- const cmdResult = await akmSearch({ query: "", type: "command" });
260
- expect(cmdResult.hits.every((h) => h.type === "command")).toBe(true);
261
- });
262
- test("search with empty query returns all entries of that type", async () => {
263
- const result = await akmSearch({ query: "", type: "agent" });
264
- expect(result.hits.length).toBe(2); // architect.md and debugger.md
265
- });
266
- test("search respects limit parameter", async () => {
267
- const result = await akmSearch({ query: "", type: "any", limit: 3 });
268
- expect(result.hits.length).toBeLessThanOrEqual(3);
269
- });
270
- test("show a script returns run", async () => {
271
- const searchResult = await akmSearch({ query: "deploy", type: "script" });
272
- const deployHit = searchResult.hits.find((h) => isLocalHit(h) && h.name.includes("deploy"));
273
- const resolvedDeployHit = expectDefined(deployHit);
274
- const openResult = await akmShow({ ref: expectDefined(resolvedDeployHit.ref) });
275
- expect(openResult.type).toBe("script");
276
- expect(openResult.run).toBeTruthy();
277
- expect(openResult.run).toContain("bash");
278
- });
279
- test("show a skill returns full SKILL.md content", async () => {
280
- const openResult = await akmShow({ ref: "skill:code-review" });
281
- expect(openResult.type).toBe("skill");
282
- expect(openResult.content).toContain("Code Review Skill");
283
- expect(openResult.content).toContain("security vulnerabilities");
284
- });
285
- test("show a command returns template and description", async () => {
286
- const openResult = await akmShow({ ref: "command:release.md" });
287
- expect(openResult.type).toBe("command");
288
- expect(openResult.description).toBe("Create a new release with changelog and version bump");
289
- expect(openResult.template).toContain("npm version");
290
- });
291
- test("show an agent returns prompt, description, model hint, and tool policy", async () => {
292
- const openResult = await akmShow({ ref: "agent:architect.md" });
293
- expect(openResult.type).toBe("agent");
294
- expect(openResult.description).toContain("architect");
295
- expect(openResult.prompt).toContain("software architect");
296
- expect(openResult.modelHint).toBe("claude-sonnet-4-20250514");
297
- expect(openResult.toolPolicy).toEqual({ allow: "Read,Glob,Grep" });
298
- });
299
- });
300
- // ═══════════════════════════════════════════════════════════════════════════
301
- // Scenario 2: Agent workflow — discover capability for a natural language task
302
- // ═══════════════════════════════════════════════════════════════════════════
303
- describe("Scenario: Agent discovers capabilities for task", () => {
304
- let stashDir;
305
- let scenarioCacheDir;
306
- beforeAll(async () => {
307
- stashDir = copyFixturesToTmp();
308
- scenarioCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-cache-s2-"));
309
- process.env.AKM_STASH_DIR = stashDir;
310
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
311
- await akmIndex({ stashDir });
312
- });
313
- beforeEach(() => {
314
- process.env.AKM_STASH_DIR = stashDir;
315
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
316
- });
317
- afterAll(() => {
318
- fs.rmSync(stashDir, { recursive: true, force: true });
319
- fs.rmSync(scenarioCacheDir, { recursive: true, force: true });
320
- });
321
- test.skipIf(!!process.env.CI)("agent asks 'set up local dev environment' → docker-compose ranks high", async () => {
322
- const result = await akmSearch({ query: "set up local development environment" });
323
- const names = result.hits.map((h) => h.name.toLowerCase());
324
- // Docker compose should appear because its intent says "start local development services"
325
- expect(names.some((n) => n.includes("compose") || n.includes("docker"))).toBe(true);
326
- });
327
- test("agent asks 'check code quality' → lint script ranks high", async () => {
328
- const result = await akmSearch({ query: "check code quality style" });
329
- expect(result.hits.length).toBeGreaterThan(0);
330
- const names = result.hits.map((h) => h.name.toLowerCase());
331
- expect(names.some((n) => n.includes("lint") || n.includes("eslint"))).toBe(true);
332
- });
333
- test.skipIf(!!process.env.CI)("agent asks 'review my pull request' → code-review skill found", async () => {
334
- const result = await akmSearch({ query: "review pull request code changes" });
335
- expect(result.hits.length).toBeGreaterThan(0);
336
- // Skill ref contains "code-review" (directory name), even though display name is "SKILL"
337
- expect(result.hits.some((h) => (isLocalHit(h) && h.ref.includes("code-review")) || h.description?.toLowerCase().includes("review"))).toBe(true);
338
- });
339
- test.skipIf(!!process.env.CI)("agent asks 'help me design the system' → architect agent found", async () => {
340
- const result = await akmSearch({ query: "system design architecture" });
341
- expect(result.hits.length).toBeGreaterThan(0);
342
- expect(result.hits.some((h) => h.name.includes("architect"))).toBe(true);
343
- });
344
- test("agent workflow: search → show (end-to-end)", async () => {
345
- // Step 1: Agent searches for a script to run tests
346
- const searchResult = await akmSearch({ query: "run tests" });
347
- expect(searchResult.hits.length).toBeGreaterThan(0);
348
- const testScript = searchResult.hits.find((h) => isLocalHit(h) && h.type === "script" && h.name.includes("test"));
349
- const resolvedTestScript = expectDefined(testScript);
350
- // Step 2: Agent reads the script to get run command for host execution
351
- const showResult = await akmShow({ ref: expectDefined(resolvedTestScript.ref) });
352
- expect(showResult.run).toBeTruthy();
353
- });
354
- });
355
- describe("Scenario: Mixed local + registry search compatibility", () => {
356
- let stashDir;
357
- let scenarioCacheDir;
358
- beforeAll(async () => {
359
- stashDir = copyFixturesToTmp();
360
- scenarioCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-cache-s2b-"));
361
- process.env.AKM_STASH_DIR = stashDir;
362
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
363
- await akmIndex({ stashDir });
364
- });
365
- // Isolate registry index cache per test so mocked fetch responses
366
- // aren't shadowed by a cached index from a previous test.
367
- // We use a per-test cache dir but copy the index DB so local search still works.
368
- beforeEach(() => {
369
- process.env.AKM_STASH_DIR = stashDir;
370
- const perTestCache = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-reg-cache-"));
371
- // Copy the scenario's index DB so local search still finds it
372
- const srcDb = path.join(scenarioCacheDir, "akm", "index.db");
373
- const destDir = path.join(perTestCache, "akm");
374
- if (fs.existsSync(srcDb)) {
375
- fs.mkdirSync(destDir, { recursive: true });
376
- fs.copyFileSync(srcDb, path.join(destDir, "index.db"));
377
- }
378
- process.env.XDG_CACHE_HOME = perTestCache;
379
- });
380
- afterEach(() => {
381
- const tmpCache = process.env.XDG_CACHE_HOME;
382
- if (tmpCache && tmpCache !== scenarioCacheDir) {
383
- fs.rmSync(tmpCache, { recursive: true, force: true });
384
- }
385
- });
386
- afterAll(() => {
387
- fs.rmSync(stashDir, { recursive: true, force: true });
388
- fs.rmSync(scenarioCacheDir, { recursive: true, force: true });
389
- });
390
- test("local source does not call registry providers", async () => {
391
- const result = await withMockedFetch(() => {
392
- throw new Error("fetch should not be called for source=local");
393
- }, () => akmSearch({ query: "docker", source: "local" }));
394
- expect(result.source).toBe("stash");
395
- expect(result.hits.length).toBeGreaterThan(0);
396
- expect(result.hits.every((h) => h.type !== "registry")).toBe(true);
397
- });
398
- test("registry source returns install guidance", async () => {
399
- const registryIndex = {
400
- version: 3,
401
- updatedAt: "2026-03-09T00:00:00Z",
402
- stashes: [
403
- {
404
- id: "npm:@scope/stash",
405
- name: "@scope/stash",
406
- description: "Example registry stash",
407
- ref: "@scope/stash",
408
- source: "npm",
409
- homepage: "https://www.npmjs.com/package/@scope/stash",
410
- tags: ["stash"],
411
- latestVersion: "1.2.3",
412
- },
413
- {
414
- id: "github:itlackey/example-stash",
415
- name: "Example Stash",
416
- description: "Example GitHub stash",
417
- ref: "itlackey/example-stash",
418
- source: "github",
419
- homepage: "https://github.com/itlackey/example-stash",
420
- tags: ["stash"],
421
- },
422
- ],
423
- };
424
- const result = await withMockedFetch(() => new Response(JSON.stringify(registryIndex), { status: 200 }), () => akmSearch({ query: "stash", source: "registry" }));
425
- expect(result.source).toBe("registry");
426
- expect(result.hits.length).toBe(0);
427
- expect(result.registryHits).toBeDefined();
428
- expect(result.registryHits?.length).toBeGreaterThan(0);
429
- const registryHits = result.registryHits ?? [];
430
- for (const hit of registryHits) {
431
- expect(hit.type).toBe("registry");
432
- expect(hit.action?.startsWith("akm add ")).toBe(true);
433
- expect(hit.id.length).toBeGreaterThan(0);
434
- }
435
- });
436
- test("both source includes local and registry hits", async () => {
437
- const registryIndex = {
438
- version: 3,
439
- updatedAt: "2026-03-09T00:00:00Z",
440
- stashes: [
441
- {
442
- id: "npm:docker-stash",
443
- name: "docker-stash",
444
- description: "Registry docker helper",
445
- ref: "docker-stash",
446
- source: "npm",
447
- tags: ["docker"],
448
- latestVersion: "0.1.0",
449
- },
450
- ],
451
- };
452
- const result = await withMockedFetch(() => new Response(JSON.stringify(registryIndex), { status: 200 }), () => akmSearch({ query: "docker", source: "both", limit: 10 }));
453
- expect(result.source).toBe("both");
454
- expect(result.hits.some((h) => h.type !== "registry")).toBe(true);
455
- expect(result.registryHits).toBeDefined();
456
- expect(result.registryHits?.length).toBeGreaterThan(0);
457
- });
458
- });
459
- // ═══════════════════════════════════════════════════════════════════════════
460
- // Scenario 3: CLI interface — real subprocess execution
461
- // ═══════════════════════════════════════════════════════════════════════════
462
- describe("Scenario: CLI subprocess execution", () => {
463
- let stashDir;
464
- let scenarioCacheDir;
465
- beforeAll(async () => {
466
- stashDir = copyFixturesToTmp();
467
- scenarioCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-cache-s3-"));
468
- process.env.AKM_STASH_DIR = stashDir;
469
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
470
- await akmIndex({ stashDir });
471
- });
472
- beforeEach(() => {
473
- process.env.AKM_STASH_DIR = stashDir;
474
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
475
- });
476
- afterAll(() => {
477
- fs.rmSync(stashDir, { recursive: true, force: true });
478
- fs.rmSync(scenarioCacheDir, { recursive: true, force: true });
479
- });
480
- test("cli: akm search returns JSON with hits", async () => {
481
- const result = runCli("search", "docker");
482
- expect(result.exitCode).toBe(0);
483
- const json = parseJson(result.stdout);
484
- expect(json.hits).toBeInstanceOf(Array);
485
- expect(json.hits.length).toBeGreaterThan(0);
486
- expect(json.stashDir).toBeUndefined();
487
- expect(json.hits[0].name).toBeTruthy();
488
- expect(json.hits[0].action).toBeTruthy();
489
- });
490
- test("cli: akm search --type script filters by type", async () => {
491
- const result = runCli("search", "deploy", "--type", "script");
492
- expect(result.exitCode).toBe(0);
493
- const json = parseJson(result.stdout);
494
- expect(json.hits.every((h) => h.type === "script")).toBe(true);
495
- });
496
- test("cli: akm search --type knowledge filters by type", async () => {
497
- const result = runCli("search", "guide", "--type", "knowledge");
498
- expect(result.exitCode).toBe(0);
499
- const json = parseJson(result.stdout);
500
- expect(json.hits.length).toBeGreaterThan(0);
501
- expect(json.hits.every((h) => h.type === "knowledge")).toBe(true);
502
- });
503
- // Issue 9: empty query must list all assets rather than throwing UsageError.
504
- // The CLI contract (--help text) promises omitting the query returns all
505
- // indexed assets. The akmSearch function handles empty queries end-to-end
506
- // via getAllEntries (DB path) and the substring-fallback's query-less branch.
507
- test("cli: akm search with no query lists all assets (empty-query listing)", async () => {
508
- const result = runCli("search");
509
- expect(result.exitCode).toBe(0);
510
- const json = parseJson(result.stdout);
511
- expect(json.hits).toBeInstanceOf(Array);
512
- expect(json.hits.length).toBeGreaterThan(0);
513
- // Brief output: ref is NOT present (only full/agent levels include ref)
514
- expect(json.hits.every((h) => h.type !== undefined)).toBe(true);
515
- expect(json.hits.every((h) => h.name !== undefined)).toBe(true);
516
- });
517
- test("cli: akm search with no query and --type filters by type", async () => {
518
- const result = runCli("search", "--type", "skill");
519
- expect(result.exitCode).toBe(0);
520
- const json = parseJson(result.stdout);
521
- expect(json.hits).toBeInstanceOf(Array);
522
- expect(json.hits.every((h) => h.type === "skill")).toBe(true);
523
- });
524
- test("cli: akm search --limit 2 respects limit", async () => {
525
- const result = runCli("search", "docker", "--limit", "2");
526
- expect(result.exitCode).toBe(0);
527
- const json = parseJson(result.stdout);
528
- expect(json.hits.length).toBeLessThanOrEqual(2);
529
- });
530
- test("cli: akm search defaults to brief JSON output", async () => {
531
- const result = runCli("search", "docker", "--type", "script");
532
- expect(result.exitCode).toBe(0);
533
- const json = parseJson(result.stdout);
534
- expect(json.source).toBeUndefined();
535
- expect(json.timing).toBeUndefined();
536
- // REC-03: ref is now included at brief detail so agents can run `akm show <ref>`
537
- expect(json.hits.every((h) => h.ref !== undefined)).toBe(true);
538
- expect(json.hits.every((h) => h.type !== undefined)).toBe(true);
539
- expect(json.hits.every((h) => h.name !== undefined)).toBe(true);
540
- expect(json.hits.every((h) => h.action !== undefined)).toBe(true);
541
- });
542
- test("cli: akm search default source is stash", async () => {
543
- const result = runCli("search", "docker", "--detail", "full");
544
- expect(result.exitCode).toBe(0);
545
- const json = parseJson(result.stdout);
546
- expect(json.source).toBe("stash");
547
- expect(json.hits.every((h) => h.type !== "registry")).toBe(true);
548
- });
549
- test("cli: akm search --detail normal includes score and description", async () => {
550
- const result = runCli("search", "docker", "--type", "script", "--detail", "normal");
551
- expect(result.exitCode).toBe(0);
552
- const json = parseJson(result.stdout);
553
- expect(json.hits.some((h) => h.score !== undefined)).toBe(true);
554
- expect(json.hits.some((h) => h.action !== undefined)).toBe(true);
555
- });
556
- test("cli: akm search --detail full includes timing and whyMatched", async () => {
557
- const result = runCli("search", "docker", "--detail", "full");
558
- expect(result.exitCode).toBe(0);
559
- const json = parseJson(result.stdout);
560
- expect(json.timing).toBeDefined();
561
- expect(json.hits.every((h) => h.whyMatched !== undefined)).toBe(true);
562
- });
563
- test("cli: akm search --format yaml returns valid YAML output", async () => {
564
- const result = spawnSync("bun", [CLI, "search", "docker", "--format", "yaml"], {
565
- encoding: "utf8",
566
- timeout: 30_000,
567
- env: { ...process.env },
568
- });
569
- expect(result.status).toBe(0);
570
- // Output must be YAML (unquoted keys like "hits:"), not JSON (quoted keys like '"hits"')
571
- const hasYaml = result.stdout.includes("hits:") && !result.stdout.includes('"hits"');
572
- expect(hasYaml).toBe(true);
573
- expect(result.stdout).toContain("docker");
574
- // Should not contain the fallback warning
575
- expect(result.stderr).not.toContain("YAML output not available");
576
- });
577
- test("cli: akm show --format text includes execution fields", async () => {
578
- const result = spawnSync("bun", [CLI, "show", "command:release.md", "--format", "text"], {
579
- encoding: "utf8",
580
- timeout: 30_000,
581
- env: { ...process.env },
582
- });
583
- expect(result.status).toBe(0);
584
- expect(result.stdout).toContain("# command: release");
585
- expect(result.stdout).toContain("description:");
586
- expect(result.stdout).toContain("Create a new release");
587
- expect(result.stdout).toContain("npm version");
588
- });
589
- test("cli: akm search --format invalid value fails with clear error", async () => {
590
- const result = runCli("search", "docker", "--format", "xml");
591
- expect(result.exitCode).not.toBe(0);
592
- const output = result.stdout + result.stderr;
593
- expect(output).toContain("Invalid value for --format: xml. Expected one of: json|yaml|text");
594
- });
595
- test("cli: akm search --detail invalid value fails with clear error", async () => {
596
- const result = runCli("search", "docker", "--detail", "max");
597
- expect(result.exitCode).not.toBe(0);
598
- const output = result.stdout + result.stderr;
599
- expect(output).toContain("Invalid value for --detail: max. Expected one of: brief|normal|full");
600
- });
601
- test("cli: akm search --source invalid value fails with clear error", async () => {
602
- const result = runCli("search", "docker", "--source", "bad");
603
- expect(result.exitCode).not.toBe(0);
604
- const output = result.stdout + result.stderr;
605
- expect(output).toContain("Invalid value for --source: bad. Expected one of: stash|registry|both");
606
- });
607
- test("cli: akm search default output excludes full-detail fields", async () => {
608
- const result = runCli("search", "docker");
609
- expect(result.exitCode).toBe(0);
610
- const json = parseJson(result.stdout);
611
- expect(json.hits.length).toBeGreaterThan(0);
612
- for (const hit of json.hits) {
613
- expect(hit.whyMatched).toBeUndefined();
614
- expect(hit.editable).toBeUndefined();
615
- expect(hit.editHint).toBeUndefined();
616
- }
617
- expect(json.timing).toBeUndefined();
618
- });
619
- test("cli: akm show returns asset content", async () => {
620
- const result = runCli("show", "skill:code-review");
621
- expect(result.exitCode).toBe(0);
622
- const json = parseJson(result.stdout);
623
- expect(json.type).toBe("skill");
624
- expect(json.content).toContain("Code Review Skill");
625
- // QA #7: path is now always included in the JSON shape (not just --detail full)
626
- expect(json.path).toBeDefined();
627
- });
628
- test("cli: akm show --detail full includes schemaVersion and path", async () => {
629
- const result = runCli("show", "command:release.md", "--detail", "full");
630
- expect(result.exitCode).toBe(0);
631
- const json = parseJson(result.stdout);
632
- expect(json.type).toBe("command");
633
- expect(json.schemaVersion).toBe(1);
634
- expect(json.path).toBeTruthy();
635
- });
636
- test("cli: akm show command returns template", async () => {
637
- const result = runCli("show", "command:release.md");
638
- expect(result.exitCode).toBe(0);
639
- const json = parseJson(result.stdout);
640
- expect(json.type).toBe("command");
641
- expect(json.description).toBeTruthy();
642
- expect(json.template).toContain("npm version");
643
- });
644
- test("cli: akm index builds index and reports stats", async () => {
645
- const result = runCli("index");
646
- expect(result.exitCode).toBe(0);
647
- const json = parseJson(result.stdout);
648
- expect(json.totalEntries).toBeGreaterThan(0);
649
- expect(json.indexPath).toBeTruthy();
650
- });
651
- test("cli: akm index --full returns mode full", async () => {
652
- const result = runCli("index", "--full");
653
- expect(result.exitCode).toBe(0);
654
- const json = parseJson(result.stdout);
655
- expect(json.mode).toBe("full");
656
- });
657
- test("cli: akm config set/get manages llm settings via JSON", async () => {
658
- const setResult = runCli("config", "set", "llm", '{"endpoint":"http://localhost:11434/v1/chat/completions","model":"llama3.2","maxTokens":256}');
659
- expect(setResult.exitCode).toBe(0);
660
- const getResult = runCli("config", "get", "llm");
661
- expect(getResult.exitCode).toBe(0);
662
- const json = parseJson(getResult.stdout);
663
- expect(json).toMatchObject({
664
- endpoint: "http://localhost:11434/v1/chat/completions",
665
- model: "llama3.2",
666
- maxTokens: 256,
667
- });
668
- });
669
- test("cli: akm with no command prints usage", async () => {
670
- const result = runCli();
671
- expect(result.exitCode).not.toBe(0);
672
- const output = result.stdout + result.stderr;
673
- expect(output).toContain("No command specified");
674
- });
675
- test("cli: akm show with no ref prints error", async () => {
676
- const result = runCli("show");
677
- expect(result.exitCode).not.toBe(0);
678
- const output = result.stdout + result.stderr;
679
- expect(output).toContain("Missing required positional argument");
680
- });
681
- });
682
- describe("Scenario: Registry lifecycle CLI (no network)", () => {
683
- let savedStashDir;
684
- beforeEach(() => {
685
- savedStashDir = process.env.AKM_STASH_DIR;
686
- });
687
- afterEach(() => {
688
- if (savedStashDir === undefined) {
689
- delete process.env.AKM_STASH_DIR;
690
- }
691
- else {
692
- process.env.AKM_STASH_DIR = savedStashDir;
693
- }
694
- });
695
- test("cli: akm list returns empty installed set when none configured", async () => {
696
- const stashDir = createEmptyStashDir("akm-e2e-registry-empty-");
697
- process.env.AKM_STASH_DIR = stashDir;
698
- saveConfig({ semanticSearchMode: "off" });
699
- try {
700
- const result = runCli("list");
701
- expect(result.exitCode).toBe(0);
702
- const json = parseJson(result.stdout);
703
- expect(json.totalSources).toBe(0);
704
- expect(json.sources).toEqual([]);
705
- }
706
- finally {
707
- fs.rmSync(stashDir, { recursive: true, force: true });
708
- }
709
- });
710
- test("cli: registered external wikis appear in list and wiki remove clears stale search hits", async () => {
711
- const stashDir = createEmptyStashDir("akm-e2e-wiki-source-");
712
- const externalWiki = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-ext-wiki-"));
713
- process.env.AKM_STASH_DIR = stashDir;
714
- saveConfig({ semanticSearchMode: "off" });
715
- fs.mkdirSync(path.join(externalWiki, "tools", "documentation", "how-to"), { recursive: true });
716
- fs.writeFileSync(path.join(externalWiki, "tools", "documentation", "how-to", "001-get-started.md"), "---\ndescription: External docs\n---\n# Start\n", "utf8");
717
- try {
718
- const registerResult = runCli("wiki", "register", "ics-docs", externalWiki, "--format", "json");
719
- expect(registerResult.exitCode).toBe(0);
720
- const registerJson = parseJson(registerResult.stdout);
721
- expect(registerJson.ref).toBe("ics-docs");
722
- expect(registerJson.sourceAdded).toEqual(expect.objectContaining({
723
- type: "filesystem",
724
- name: "ics-docs",
725
- wiki: "ics-docs",
726
- }));
727
- const listResult = runCli("list", "--format", "json");
728
- expect(listResult.exitCode).toBe(0);
729
- const listJson = parseJson(listResult.stdout);
730
- expect(listJson.sources).toEqual(expect.arrayContaining([
731
- expect.objectContaining({
732
- name: "ics-docs",
733
- kind: "filesystem",
734
- wiki: "ics-docs",
735
- }),
736
- ]));
737
- const searchResult = runCli("search", "documentation", "--type", "wiki", "--detail", "normal", "--format", "json");
738
- expect(searchResult.exitCode).toBe(0);
739
- const searchJson = parseJson(searchResult.stdout);
740
- expect(searchJson.hits).toEqual(expect.arrayContaining([
741
- expect.objectContaining({
742
- name: "ics-docs/tools/documentation/how-to/001-get-started",
743
- action: "akm show wiki:ics-docs/tools/documentation/how-to/001-get-started -> read the wiki page",
744
- }),
745
- ]));
746
- const removeResult = runCli("wiki", "remove", "ics-docs", "--force", "--format", "json");
747
- expect(removeResult.exitCode).toBe(0);
748
- const removedSearchResult = runCli("search", "documentation", "--type", "wiki", "--format", "json");
749
- expect(removedSearchResult.exitCode).toBe(0);
750
- const removedSearchJson = parseJson(removedSearchResult.stdout);
751
- expect(removedSearchJson.hits).toEqual([]);
752
- }
753
- finally {
754
- fs.rmSync(stashDir, { recursive: true, force: true });
755
- fs.rmSync(externalWiki, { recursive: true, force: true });
756
- }
757
- });
758
- test("cli: wiki remove --force hides preserved raw-only leftovers from wiki list", async () => {
759
- const stashDir = createEmptyStashDir("akm-e2e-wiki-remove-");
760
- const rawSource = path.join(stashDir, "source.md");
761
- process.env.AKM_STASH_DIR = stashDir;
762
- saveConfig({ semanticSearchMode: "off" });
763
- fs.writeFileSync(rawSource, "# Test\n", "utf8");
764
- try {
765
- const createResult = runCli("wiki", "create", "my-notes", "--format", "json");
766
- expect(createResult.exitCode).toBe(0);
767
- const stashResult = runCli("wiki", "stash", "my-notes", rawSource, "--as", "test-page", "--format", "json");
768
- expect(stashResult.exitCode).toBe(0);
769
- const removeResult = runCli("wiki", "remove", "my-notes", "--force", "--format", "text");
770
- expect(removeResult.exitCode).toBe(0);
771
- expect(removeResult.stdout).toContain("raw/ preserved at");
772
- const listResult = runCli("wiki", "list", "--format", "json");
773
- expect(listResult.exitCode).toBe(0);
774
- const listJson = parseJson(listResult.stdout);
775
- expect(listJson.wikis).toEqual([]);
776
- const showResult = runCli("wiki", "show", "my-notes", "--format", "json");
777
- expect(showResult.exitCode).not.toBe(0);
778
- expect(showResult.stderr).toContain("Wiki not found: my-notes");
779
- const cleanupResult = runCli("wiki", "remove", "my-notes", "--force", "--with-sources", "--format", "json");
780
- expect(cleanupResult.exitCode).toBe(0);
781
- }
782
- finally {
783
- fs.rmSync(stashDir, { recursive: true, force: true });
784
- }
785
- });
786
- test("cli: akm index text output surfaces skipped-asset warnings", async () => {
787
- const stashDir = createEmptyStashDir("akm-e2e-index-warn-");
788
- process.env.AKM_STASH_DIR = stashDir;
789
- saveConfig({ semanticSearchMode: "off" });
790
- fs.mkdirSync(path.join(stashDir, "workflows"), { recursive: true });
791
- fs.writeFileSync(path.join(stashDir, "workflows", "good.md"), [
792
- "---",
793
- "description: Good workflow",
794
- "---",
795
- "",
796
- "# Workflow: Good",
797
- "",
798
- "## Step: First",
799
- "Step ID: first",
800
- "### Instructions",
801
- "Do it.",
802
- "",
803
- ].join("\n"), "utf8");
804
- // Truly malformed: no `# Workflow:` heading. Intro prose between the
805
- // title and the first step is now permitted (#158).
806
- fs.writeFileSync(path.join(stashDir, "workflows", "bad.md"), [
807
- "---",
808
- "description: Bad workflow",
809
- "---",
810
- "",
811
- "## Step: First",
812
- "Step ID: first",
813
- "### Instructions",
814
- "Do it.",
815
- "",
816
- ].join("\n"), "utf8");
817
- try {
818
- const result = runCli("index", "--full", "--format", "text");
819
- expect(result.exitCode).toBe(0);
820
- expect(result.stdout).toContain("Warnings (");
821
- expect(result.stdout).toContain(path.join(stashDir, "workflows", "bad.md"));
822
- }
823
- finally {
824
- fs.rmSync(stashDir, { recursive: true, force: true });
825
- }
826
- });
827
- test("cli: akm remove resolves parsed ref id and removes cache directory", async () => {
828
- const stashDir = createEmptyStashDir("akm-e2e-registry-remove-");
829
- const stashRoot = path.join(stashDir, "registry-stash");
830
- const cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-cache-remove-"));
831
- fs.mkdirSync(path.join(stashRoot, "scripts"), { recursive: true });
832
- process.env.AKM_STASH_DIR = stashDir;
833
- saveConfig({
834
- semanticSearchMode: "off",
835
- installed: [
836
- {
837
- id: "npm:@scope/stash",
838
- source: "npm",
839
- ref: "npm:@scope/stash@1.0.0",
840
- artifactUrl: "https://registry.npmjs.org/@scope/stash/-/stash-1.0.0.tgz",
841
- resolvedVersion: "1.0.0",
842
- resolvedRevision: "abc123",
843
- stashRoot,
844
- cacheDir,
845
- installedAt: new Date().toISOString(),
846
- },
847
- ],
848
- });
849
- try {
850
- const result = runCli("remove", "npm:@scope/stash@latest");
851
- expect(result.exitCode).toBe(0);
852
- const json = parseJson(result.stdout);
853
- expect(expectDefined(json.removed).id).toBe("npm:@scope/stash");
854
- const config = loadConfig();
855
- expect(config.installed).toBeUndefined();
856
- expect(fs.existsSync(cacheDir)).toBe(false);
857
- }
858
- finally {
859
- fs.rmSync(stashDir, { recursive: true, force: true });
860
- fs.rmSync(cacheDir, { recursive: true, force: true });
861
- }
862
- });
863
- test("cli: akm update requires target or --all", async () => {
864
- const stashDir = createEmptyStashDir("akm-e2e-registry-update-");
865
- process.env.AKM_STASH_DIR = stashDir;
866
- saveConfig({ semanticSearchMode: "off" });
867
- try {
868
- const result = runCli("update");
869
- expect(result.exitCode).not.toBe(0);
870
- const output = result.stdout + result.stderr;
871
- expect(output).toContain("Either <target> or --all is required.");
872
- }
873
- finally {
874
- fs.rmSync(stashDir, { recursive: true, force: true });
875
- }
876
- });
877
- test("cli: akm update rejects target with --all", async () => {
878
- const stashDir = createEmptyStashDir("akm-e2e-registry-update-both-");
879
- process.env.AKM_STASH_DIR = stashDir;
880
- saveConfig({ semanticSearchMode: "off" });
881
- try {
882
- const result = runCli("update", "npm:@scope/stash", "--all");
883
- expect(result.exitCode).not.toBe(0);
884
- const output = result.stdout + result.stderr;
885
- expect(output).toContain("Specify either <target> or --all, not both.");
886
- }
887
- finally {
888
- fs.rmSync(stashDir, { recursive: true, force: true });
889
- }
890
- });
891
- test("cli: akm update missing target returns stable not-installed error", async () => {
892
- const stashDir = createEmptyStashDir("akm-e2e-registry-missing-");
893
- process.env.AKM_STASH_DIR = stashDir;
894
- saveConfig({ semanticSearchMode: "off" });
895
- try {
896
- const result = runCli("update", "npm:@scope/stash");
897
- expect(result.exitCode).not.toBe(0);
898
- const output = result.stdout + result.stderr;
899
- expect(output).toContain("No matching source for target: npm:@scope/stash");
900
- }
901
- finally {
902
- fs.rmSync(stashDir, { recursive: true, force: true });
903
- }
904
- });
905
- });
906
- // ═══════════════════════════════════════════════════════════════════════════
907
- // Scenario 3a: CLI upgrade and update --force commands
908
- // ═══════════════════════════════════════════════════════════════════════════
909
- describe("Scenario: upgrade and update --force (no network)", () => {
910
- let savedStashDir;
911
- beforeEach(() => {
912
- savedStashDir = process.env.AKM_STASH_DIR;
913
- });
914
- afterEach(() => {
915
- if (savedStashDir === undefined) {
916
- delete process.env.AKM_STASH_DIR;
917
- }
918
- else {
919
- process.env.AKM_STASH_DIR = savedStashDir;
920
- }
921
- });
922
- test("upgrade --check returns version info (mocked fetch)", async () => {
923
- const { checkForUpdate } = await import("../src/commands/self-update");
924
- const result = await withMockedFetch(() => Response.json({ tag_name: "v0.0.14" }), () => checkForUpdate("0.0.13"));
925
- expect(result.currentVersion).toBe("0.0.13");
926
- expect(result.latestVersion).toBe("0.0.14");
927
- expect(result.updateAvailable).toBe(true);
928
- expect(["binary", "bun", "npm", "pnpm", "unknown"]).toContain(result.installMethod);
929
- });
930
- test("performUpgrade detects non-binary install and returns guidance", async () => {
931
- const { performUpgrade } = await import("../src/commands/self-update");
932
- const result = await performUpgrade({
933
- currentVersion: "0.0.13",
934
- latestVersion: "0.0.14",
935
- updateAvailable: true,
936
- installMethod: "unknown",
937
- });
938
- expect(result.upgraded).toBe(false);
939
- expect(["bun", "npm", "pnpm", "unknown"]).toContain(result.installMethod);
940
- expect(result.message).toBeTruthy();
941
- });
942
- test("cli: akm update --help shows --force flag", async () => {
943
- const result = spawnSync("bun", [CLI, "update", "--help"], {
944
- encoding: "utf8",
945
- timeout: 10_000,
946
- });
947
- const output = (result.stdout ?? "") + (result.stderr ?? "");
948
- expect(output).toContain("--force");
949
- expect(output).toContain("Force fresh download");
950
- });
951
- test("cli: akm update --force requires target or --all", async () => {
952
- const stashDir = createEmptyStashDir("akm-e2e-update-force-");
953
- process.env.AKM_STASH_DIR = stashDir;
954
- saveConfig({ semanticSearchMode: "off" });
955
- try {
956
- const result = runCli("update", "--force");
957
- expect(result.exitCode).not.toBe(0);
958
- const output = result.stdout + result.stderr;
959
- expect(output).toContain("Either <target> or --all is required.");
960
- }
961
- finally {
962
- fs.rmSync(stashDir, { recursive: true, force: true });
963
- }
964
- });
965
- test("cli: akm upgrade --help shows --check and --force flags", async () => {
966
- const result = spawnSync("bun", [CLI, "upgrade", "--help"], {
967
- encoding: "utf8",
968
- timeout: 10_000,
969
- });
970
- const output = (result.stdout ?? "") + (result.stderr ?? "");
971
- expect(output).toContain("--check");
972
- expect(output).toContain("--force");
973
- });
974
- test("cli: akm help migrate prints migration guidance for a release", async () => {
975
- const result = spawnSync("bun", [CLI, "help", "migrate", "0.5.0"], {
976
- encoding: "utf8",
977
- timeout: 10_000,
978
- });
979
- const output = (result.stdout ?? "") + (result.stderr ?? "");
980
- expect(result.status).toBe(0);
981
- expect(output).toContain("Migration notes for akm v0.5.0");
982
- expect(output).toContain("## [0.5.0]");
983
- });
984
- test("cli: akm --help lists the help command", async () => {
985
- const result = spawnSync("bun", [CLI, "--help"], {
986
- encoding: "utf8",
987
- timeout: 10_000,
988
- });
989
- const output = (result.stdout ?? "") + (result.stderr ?? "");
990
- expect(output).toContain("help");
991
- expect(output).toContain("focused help topics");
992
- });
993
- test("cli: akm setup --help shows the wizard description (issue #273)", async () => {
994
- // The setup subcommand should advertise its purpose so operators can
995
- // see what the wizard configures (embeddings, LLM, registries, sources,
996
- // agent profiles) without running the interactive flow.
997
- const result = spawnSync("bun", [CLI, "setup", "--help"], {
998
- encoding: "utf8",
999
- timeout: 10_000,
1000
- });
1001
- const output = (result.stdout ?? "") + (result.stderr ?? "");
1002
- expect(output).toContain("Interactive configuration wizard");
1003
- expect(output).toContain("embeddings");
1004
- expect(output).toContain("LLM");
1005
- expect(output).toContain("registries");
1006
- expect(output).toContain("sources");
1007
- expect(output).toContain("agent profiles");
1008
- });
1009
- });
1010
- // ═══════════════════════════════════════════════════════════════════════════
1011
- // Scenario 3b: CLI knowledge view modes (positional syntax)
1012
- // ═══════════════════════════════════════════════════════════════════════════
1013
- describe("Scenario: CLI knowledge view modes (positional)", () => {
1014
- let stashDir;
1015
- beforeAll(async () => {
1016
- stashDir = copyFixturesToTmp();
1017
- process.env.AKM_STASH_DIR = stashDir;
1018
- });
1019
- beforeEach(() => {
1020
- process.env.AKM_STASH_DIR = stashDir;
1021
- });
1022
- afterAll(() => {
1023
- fs.rmSync(stashDir, { recursive: true, force: true });
1024
- });
1025
- test("cli: show knowledge with positional toc", async () => {
1026
- const result = runCli("show", "knowledge:guide.md", "toc");
1027
- expect(result.exitCode).toBe(0);
1028
- const json = parseJson(result.stdout);
1029
- expect(json.type).toBe("knowledge");
1030
- expect(json.content).toContain("# API Reference Guide");
1031
- expect(json.content).toContain("## Getting Started");
1032
- expect(json.content).toContain("lines total");
1033
- });
1034
- test("cli: show knowledge with positional section heading", async () => {
1035
- const result = runCli("show", "knowledge:guide.md", "section", "Getting Started");
1036
- expect(result.exitCode).toBe(0);
1037
- const json = parseJson(result.stdout);
1038
- expect(json.type).toBe("knowledge");
1039
- expect(json.content).toContain("Getting Started");
1040
- expect(json.content).toContain("install the package");
1041
- // Should include sub-headings but not sibling headings
1042
- expect(json.content).toContain("Prerequisites");
1043
- expect(json.content).not.toContain("Authentication");
1044
- });
1045
- test("cli: show knowledge with positional lines start end", async () => {
1046
- const result = runCli("show", "knowledge:guide.md", "lines", "1", "5");
1047
- expect(result.exitCode).toBe(0);
1048
- const json = parseJson(result.stdout);
1049
- expect(json.type).toBe("knowledge");
1050
- // Lines 1-5 cover the frontmatter start
1051
- expect(json.content).toContain("---");
1052
- });
1053
- test("cli: show knowledge rejects legacy --view flag", async () => {
1054
- const result = runCli("show", "knowledge:guide.md", "--view", "toc");
1055
- expect(result.exitCode).not.toBe(0);
1056
- expect(result.stderr).toContain("Legacy show flags are no longer supported");
1057
- });
1058
- test("cli: show knowledge default (no view mode) returns full content", async () => {
1059
- const result = runCli("show", "knowledge:guide.md");
1060
- expect(result.exitCode).toBe(0);
1061
- const json = parseJson(result.stdout);
1062
- expect(json.type).toBe("knowledge");
1063
- expect(json.content).toBeDefined();
1064
- // Full content should include everything
1065
- expect(json.content).toContain("API Reference Guide");
1066
- expect(json.content).toContain("Getting Started");
1067
- expect(json.content).toContain("Authentication");
1068
- });
1069
- test("cli: show knowledge --detail brief omits full content", async () => {
1070
- const result = runCli("show", "knowledge:guide.md", "--detail", "brief");
1071
- expect(result.exitCode).toBe(0);
1072
- const json = parseJson(result.stdout);
1073
- expect(json.type).toBe("knowledge");
1074
- expect(json.name).toBe("guide.md");
1075
- expect(json.description).toBe("Comprehensive guide for the example API");
1076
- expect(json.action).toContain("Reference material");
1077
- expect(json.content).toBeUndefined();
1078
- });
1079
- });
1080
- // ═══════════════════════════════════════════════════════════════════════════
1081
- // Scenario 4: Progressive improvement — user drops scripts, indexes later
1082
- // ═══════════════════════════════════════════════════════════════════════════
1083
- describe("Scenario: Zero-config progressive improvement", () => {
1084
- let stashDir;
1085
- let scenarioCacheDir;
1086
- beforeAll(async () => {
1087
- stashDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-prog-"));
1088
- scenarioCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-cache-s4-"));
1089
- for (const sub of ["scripts", "skills", "commands", "agents"]) {
1090
- fs.mkdirSync(path.join(stashDir, sub), { recursive: true });
1091
- }
1092
- process.env.AKM_STASH_DIR = stashDir;
1093
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
1094
- });
1095
- beforeEach(() => {
1096
- process.env.AKM_STASH_DIR = stashDir;
1097
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
1098
- });
1099
- afterAll(() => {
1100
- fs.rmSync(stashDir, { recursive: true, force: true });
1101
- fs.rmSync(scenarioCacheDir, { recursive: true, force: true });
1102
- });
1103
- test("user drops a script in scripts/ — search finds it by name (no index)", async () => {
1104
- fs.mkdirSync(path.join(stashDir, "scripts", "format"), { recursive: true });
1105
- fs.writeFileSync(path.join(stashDir, "scripts", "format", "prettier-check.sh"), "#!/usr/bin/env bash\n# Format code with Prettier\nprettier --check .\n");
1106
- const result = await akmSearch({ query: "prettier", type: "script" });
1107
- expect(result.hits.length).toBe(1);
1108
- expect(result.hits[0].name).toContain("prettier");
1109
- });
1110
- test("user runs index — metadata generated in database with description from comments", async () => {
1111
- await akmIndex({ stashDir });
1112
- // No .stash.json should be auto-generated
1113
- expect(fs.existsSync(path.join(stashDir, "scripts", "format", ".stash.json"))).toBe(false);
1114
- const db = openDatabase();
1115
- const entries = getAllEntries(db, "script");
1116
- const formatEntry = entries.find((e) => e.entry.name.includes("prettier"));
1117
- expect(formatEntry).toBeDefined();
1118
- expect(formatEntry?.entry.quality).toBe("generated");
1119
- expect(formatEntry?.entry.description).toContain("Format code");
1120
- closeDatabase(db);
1121
- });
1122
- test("user adds more scripts — re-index picks them up", async () => {
1123
- fs.mkdirSync(path.join(stashDir, "scripts", "db"), { recursive: true });
1124
- fs.writeFileSync(path.join(stashDir, "scripts", "db", "migrate.sh"), "#!/usr/bin/env bash\n# Run database migrations\necho 'migrating...'\n");
1125
- const result = await akmIndex({ stashDir });
1126
- expect(result.totalEntries).toBeGreaterThanOrEqual(2);
1127
- const db = openDatabase();
1128
- const entries = getAllEntries(db, "script");
1129
- const migrateEntry = entries.find((e) => e.entry.name.includes("migrate"));
1130
- expect(migrateEntry).toBeDefined();
1131
- expect(migrateEntry?.entry.description).toContain("database migrations");
1132
- closeDatabase(db);
1133
- });
1134
- test("user creates .stash.json manually — overrides used on next index", async () => {
1135
- // User creates a .stash.json override for the format script
1136
- const stashPath = path.join(stashDir, "scripts", "format", ".stash.json");
1137
- fs.writeFileSync(stashPath, JSON.stringify({
1138
- entries: [
1139
- {
1140
- name: "prettier-check",
1141
- type: "script",
1142
- description: "Check code formatting with Prettier",
1143
- tags: ["prettier", "format", "style"],
1144
- filename: "prettier-check.sh",
1145
- },
1146
- ],
1147
- }, null, 2));
1148
- // Re-index — should use user override
1149
- await akmIndex({ stashDir });
1150
- const reloaded = expectDefined(loadStashFile(path.join(stashDir, "scripts", "format")));
1151
- expect(reloaded.entries[0].description).toBe("Check code formatting with Prettier");
1152
- expect(reloaded.entries[0].tags).toContain("prettier");
1153
- expect(reloaded.entries[0].quality).not.toBe("generated");
1154
- });
1155
- test("semantic search finds user-edited metadata after re-index", async () => {
1156
- // Re-index to pick up manual edits in the search index
1157
- await akmIndex({ stashDir });
1158
- const result = await akmSearch({ query: "format code style" });
1159
- expect(result.hits.length).toBeGreaterThan(0);
1160
- expect(result.hits.some((h) => h.name.includes("prettier"))).toBe(true);
1161
- });
1162
- });
1163
- // ═══════════════════════════════════════════════════════════════════════════
1164
- // Scenario 5: Multi-script directory with .stash.json
1165
- // ═══════════════════════════════════════════════════════════════════════════
1166
- describe("Scenario: Multi-script directory with hand-written .stash.json", () => {
1167
- let stashDir;
1168
- let scenarioCacheDir;
1169
- beforeAll(async () => {
1170
- stashDir = copyFixturesToTmp();
1171
- scenarioCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-cache-s5-"));
1172
- process.env.AKM_STASH_DIR = stashDir;
1173
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
1174
- await akmIndex({ stashDir });
1175
- });
1176
- beforeEach(() => {
1177
- process.env.AKM_STASH_DIR = stashDir;
1178
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
1179
- });
1180
- afterAll(() => {
1181
- fs.rmSync(stashDir, { recursive: true, force: true });
1182
- fs.rmSync(scenarioCacheDir, { recursive: true, force: true });
1183
- });
1184
- test("docker/ directory exposes two scripts from single .stash.json", async () => {
1185
- const stash = expectDefined(loadStashFile(path.join(stashDir, "scripts", "docker")));
1186
- expect(stash.entries).toHaveLength(2);
1187
- const names = stash.entries.map((e) => e.name);
1188
- expect(names).toContain("docker-build");
1189
- expect(names).toContain("docker-compose");
1190
- });
1191
- test("search for 'docker build' returns docker-build as top result", async () => {
1192
- const result = await akmSearch({ query: "docker build" });
1193
- expect(result.hits[0].name).toContain("docker");
1194
- expect(result.hits[0].description).toContain("Docker image");
1195
- });
1196
- test("search for 'compose development' returns docker-compose", async () => {
1197
- const result = await akmSearch({ query: "compose development" });
1198
- const composeHit = result.hits.find((h) => h.name.includes("compose"));
1199
- expect(composeHit).toBeDefined();
1200
- expect(composeHit?.tags).toContain("compose");
1201
- });
1202
- });
1203
- // ═══════════════════════════════════════════════════════════════════════════
1204
- // Scenario 6: Index persistence and cache
1205
- // ═══════════════════════════════════════════════════════════════════════════
1206
- describe("Scenario: Index persistence across sessions", () => {
1207
- let stashDir;
1208
- let scenarioCacheDir;
1209
- beforeAll(async () => {
1210
- stashDir = copyFixturesToTmp();
1211
- scenarioCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-cache-s6-"));
1212
- process.env.AKM_STASH_DIR = stashDir;
1213
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
1214
- });
1215
- beforeEach(() => {
1216
- process.env.AKM_STASH_DIR = stashDir;
1217
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
1218
- });
1219
- afterAll(() => {
1220
- fs.rmSync(stashDir, { recursive: true, force: true });
1221
- fs.rmSync(scenarioCacheDir, { recursive: true, force: true });
1222
- });
1223
- test("index is persisted and loadable", async () => {
1224
- await akmIndex({ stashDir });
1225
- const db = openDatabase();
1226
- const version = getMeta(db, "version");
1227
- expect(version).toBe(String(DB_VERSION));
1228
- const storedStashDir = getMeta(db, "stashDir");
1229
- expect(storedStashDir).toBe(stashDir);
1230
- const entries = getAllEntries(db);
1231
- expect(entries.length).toBeGreaterThan(0);
1232
- const builtAt = getMeta(db, "builtAt");
1233
- expect(builtAt).toBeTruthy();
1234
- closeDatabase(db);
1235
- });
1236
- test("search uses persisted index (simulates new session)", async () => {
1237
- // First index
1238
- await akmIndex({ stashDir });
1239
- // Simulate a new session by just doing search (no re-index)
1240
- const result = await akmSearch({ query: "docker" });
1241
- expect(result.hits.length).toBeGreaterThan(0);
1242
- // Should have scores from semantic search, not substring
1243
- expect(result.hits[0].score).toBeDefined();
1244
- });
1245
- test("re-index updates the persisted index", async () => {
1246
- await akmIndex({ stashDir });
1247
- const db1 = openDatabase();
1248
- const entries1 = getAllEntries(db1);
1249
- const builtAt1 = expectDefined(getMeta(db1, "builtAt"));
1250
- closeDatabase(db1);
1251
- // Add a new script
1252
- fs.mkdirSync(path.join(stashDir, "scripts", "new-script"), { recursive: true });
1253
- fs.writeFileSync(path.join(stashDir, "scripts", "new-script", "hello.sh"), "#!/bin/bash\necho hello\n");
1254
- await akmIndex({ stashDir });
1255
- const db2 = openDatabase();
1256
- const entries2 = getAllEntries(db2);
1257
- const builtAt2 = expectDefined(getMeta(db2, "builtAt"));
1258
- closeDatabase(db2);
1259
- expect(entries2.length).toBeGreaterThan(entries1.length);
1260
- expect(new Date(builtAt2).getTime()).toBeGreaterThanOrEqual(new Date(builtAt1).getTime());
1261
- });
1262
- });
1263
- // ═══════════════════════════════════════════════════════════════════════════
1264
- // Scenario 7: Error handling and edge cases
1265
- // ═══════════════════════════════════════════════════════════════════════════
1266
- describe("Scenario: Error handling and edge cases", () => {
1267
- let savedStashDir;
1268
- beforeEach(() => {
1269
- savedStashDir = process.env.AKM_STASH_DIR;
1270
- });
1271
- afterEach(() => {
1272
- if (savedStashDir === undefined) {
1273
- delete process.env.AKM_STASH_DIR;
1274
- }
1275
- else {
1276
- process.env.AKM_STASH_DIR = savedStashDir;
1277
- }
1278
- });
1279
- test("search with non-existent AKM_STASH_DIR throws clear error", async () => {
1280
- const orig = process.env.AKM_STASH_DIR;
1281
- process.env.AKM_STASH_DIR = "/nonexistent/path";
1282
- try {
1283
- await expect(akmSearch({ query: "test" })).rejects.toThrow(/Unable to read/);
1284
- }
1285
- finally {
1286
- if (orig === undefined)
1287
- delete process.env.AKM_STASH_DIR;
1288
- else
1289
- process.env.AKM_STASH_DIR = orig;
1290
- }
1291
- });
1292
- test("search with unset AKM_STASH_DIR throws clear error", async () => {
1293
- const orig = process.env.AKM_STASH_DIR;
1294
- const origHome = process.env.HOME;
1295
- delete process.env.AKM_STASH_DIR;
1296
- // Point HOME somewhere without an akm directory to force the "no stash" error
1297
- const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-nohome-"));
1298
- process.env.HOME = tmpHome;
1299
- try {
1300
- await expect(akmSearch({ query: "test" })).rejects.toThrow(/No stash directory found/);
1301
- }
1302
- finally {
1303
- if (orig === undefined)
1304
- delete process.env.AKM_STASH_DIR;
1305
- else
1306
- process.env.AKM_STASH_DIR = orig;
1307
- if (origHome === undefined)
1308
- delete process.env.HOME;
1309
- else
1310
- process.env.HOME = origHome;
1311
- fs.rmSync(tmpHome, { recursive: true, force: true });
1312
- }
1313
- });
1314
- test("show with invalid ref format throws", async () => {
1315
- const stashDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-err-"));
1316
- process.env.AKM_STASH_DIR = stashDir;
1317
- try {
1318
- await expect(akmShow({ ref: "badref" })).rejects.toThrow(/Invalid ref/);
1319
- }
1320
- finally {
1321
- fs.rmSync(stashDir, { recursive: true, force: true });
1322
- }
1323
- });
1324
- test("show with unknown type throws", async () => {
1325
- const stashDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-err-"));
1326
- process.env.AKM_STASH_DIR = stashDir;
1327
- try {
1328
- await expect(akmShow({ ref: "widget:foo" })).rejects.toThrow(/Invalid asset type/);
1329
- }
1330
- finally {
1331
- fs.rmSync(stashDir, { recursive: true, force: true });
1332
- }
1333
- });
1334
- test("show with path traversal attempt throws", async () => {
1335
- const stashDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-err-"));
1336
- fs.mkdirSync(path.join(stashDir, "scripts"), { recursive: true });
1337
- process.env.AKM_STASH_DIR = stashDir;
1338
- try {
1339
- await expect(akmShow({ ref: "script:../../etc/passwd" })).rejects.toThrow(/Path traversal/);
1340
- }
1341
- finally {
1342
- fs.rmSync(stashDir, { recursive: true, force: true });
1343
- }
1344
- });
1345
- test("search on empty stash returns no hits with tip", async () => {
1346
- const stashDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-empty-"));
1347
- for (const sub of ["scripts", "skills", "commands", "agents"]) {
1348
- fs.mkdirSync(path.join(stashDir, sub), { recursive: true });
1349
- }
1350
- process.env.AKM_STASH_DIR = stashDir;
1351
- try {
1352
- const result = await akmSearch({ query: "anything" });
1353
- expect(result.hits).toHaveLength(0);
1354
- expect(result.tip).toBeTruthy();
1355
- }
1356
- finally {
1357
- fs.rmSync(stashDir, { recursive: true, force: true });
1358
- }
1359
- });
1360
- test("index on empty stash succeeds with zero entries", async () => {
1361
- const stashDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-empty-"));
1362
- for (const sub of ["scripts", "skills", "commands", "agents"]) {
1363
- fs.mkdirSync(path.join(stashDir, sub), { recursive: true });
1364
- }
1365
- try {
1366
- const result = await akmIndex({ stashDir });
1367
- expect(result.totalEntries).toBe(0);
1368
- expect(result.generatedMetadata).toBe(0);
1369
- }
1370
- finally {
1371
- fs.rmSync(stashDir, { recursive: true, force: true });
1372
- }
1373
- });
1374
- });
1375
- // ═══════════════════════════════════════════════════════════════════════════
1376
- // Scenario 8: Mixed asset type discovery
1377
- // ═══════════════════════════════════════════════════════════════════════════
1378
- describe("Scenario: Cross-type discovery", () => {
1379
- let stashDir;
1380
- let scenarioCacheDir;
1381
- beforeAll(async () => {
1382
- stashDir = copyFixturesToTmp();
1383
- scenarioCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-cache-s8-"));
1384
- process.env.AKM_STASH_DIR = stashDir;
1385
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
1386
- await akmIndex({ stashDir });
1387
- });
1388
- beforeEach(() => {
1389
- process.env.AKM_STASH_DIR = stashDir;
1390
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
1391
- });
1392
- afterAll(() => {
1393
- fs.rmSync(stashDir, { recursive: true, force: true });
1394
- fs.rmSync(scenarioCacheDir, { recursive: true, force: true });
1395
- });
1396
- test("search 'any' type returns mixed results across scripts, skills, commands, agents", async () => {
1397
- const result = await akmSearch({ query: "", type: "any" });
1398
- const types = new Set(result.hits.map((h) => h.type));
1399
- // Should have at least scripts and one other type
1400
- expect(types.has("script")).toBe(true);
1401
- expect(types.size).toBeGreaterThan(1);
1402
- });
1403
- test("each hit has a valid ref that can be used with show", async () => {
1404
- const result = await akmSearch({ query: "", type: "any", limit: 10 });
1405
- for (const hit of result.hits.filter(isLocalHit)) {
1406
- expect(hit.ref).toBeTruthy();
1407
- expect(hit.ref).toContain(":");
1408
- // Should not throw when opening
1409
- const openResult = await akmShow({ ref: expectDefined(hit.ref) });
1410
- expect(openResult.type).toBe(hit.type);
1411
- }
1412
- });
1413
- test("script hits have run", async () => {
1414
- const result = await akmSearch({ query: "", type: "script" });
1415
- for (const hit of result.hits) {
1416
- expect(hit.type).toBe("script");
1417
- }
1418
- });
1419
- });