akm-cli 0.7.0-rc1 → 0.7.1

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 (314) hide show
  1. package/dist/{src/cli.js → cli.js} +100 -16
  2. package/dist/{src/commands → commands}/config-cli.js +42 -0
  3. package/dist/{src/commands → commands}/history.js +78 -7
  4. package/dist/{src/commands → commands}/registry-search.js +69 -6
  5. package/dist/{src/commands → commands}/search.js +30 -3
  6. package/dist/{src/commands → commands}/show.js +29 -0
  7. package/dist/{src/commands → commands}/source-add.js +5 -1
  8. package/dist/{src/commands → commands}/source-manage.js +7 -1
  9. package/dist/{src/core → core}/config.js +28 -0
  10. package/dist/{src/indexer → indexer}/db-search.js +1 -0
  11. package/dist/{src/indexer → indexer}/indexer.js +16 -2
  12. package/dist/{src/indexer → indexer}/matchers.js +1 -1
  13. package/dist/{src/indexer → indexer}/search-source.js +4 -2
  14. package/dist/{src/integrations → integrations}/agent/profiles.js +1 -1
  15. package/dist/{src/integrations → integrations}/agent/spawn.js +67 -16
  16. package/dist/{src/integrations → integrations}/github.js +9 -3
  17. package/dist/{src/llm → llm}/embedders/remote.js +37 -3
  18. package/dist/{src/output → output}/cli-hints.js +15 -2
  19. package/dist/{src/output → output}/renderers.js +3 -1
  20. package/dist/{src/output → output}/shapes.js +8 -1
  21. package/dist/{src/output → output}/text.js +156 -3
  22. package/dist/{src/registry → registry}/build-index.js +5 -4
  23. package/dist/{src/registry → registry}/providers/static-index.js +3 -1
  24. package/dist/{src/setup → setup}/setup.js +9 -0
  25. package/dist/{src/wiki → wiki}/wiki.js +54 -6
  26. package/dist/{src/workflows → workflows}/runs.js +37 -3
  27. package/package.json +8 -8
  28. package/dist/tests/add-website-source.test.js +0 -119
  29. package/dist/tests/agent/agent-config-loader.test.js +0 -70
  30. package/dist/tests/agent/agent-config.test.js +0 -221
  31. package/dist/tests/agent/agent-detect.test.js +0 -100
  32. package/dist/tests/agent/agent-spawn.test.js +0 -234
  33. package/dist/tests/agent-output.test.js +0 -186
  34. package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +0 -103
  35. package/dist/tests/architecture/agent-spawn-seam.test.js +0 -193
  36. package/dist/tests/architecture/llm-stateless-seam.test.js +0 -112
  37. package/dist/tests/asset-ref.test.js +0 -192
  38. package/dist/tests/asset-registry.test.js +0 -103
  39. package/dist/tests/asset-spec.test.js +0 -241
  40. package/dist/tests/bench/attribution.test.js +0 -995
  41. package/dist/tests/bench/cleanup-sigint.test.js +0 -83
  42. package/dist/tests/bench/cleanup.js +0 -203
  43. package/dist/tests/bench/cleanup.test.js +0 -166
  44. package/dist/tests/bench/cli.js +0 -683
  45. package/dist/tests/bench/cli.test.js +0 -177
  46. package/dist/tests/bench/compare.test.js +0 -556
  47. package/dist/tests/bench/corpus.js +0 -314
  48. package/dist/tests/bench/corpus.test.js +0 -258
  49. package/dist/tests/bench/driver.js +0 -346
  50. package/dist/tests/bench/driver.test.js +0 -443
  51. package/dist/tests/bench/evolve-metrics.js +0 -179
  52. package/dist/tests/bench/evolve-metrics.test.js +0 -187
  53. package/dist/tests/bench/evolve.js +0 -580
  54. package/dist/tests/bench/evolve.test.js +0 -616
  55. package/dist/tests/bench/failure-modes.test.js +0 -300
  56. package/dist/tests/bench/feedback-integrity.test.js +0 -456
  57. package/dist/tests/bench/leakage.test.js +0 -125
  58. package/dist/tests/bench/learning-curve.test.js +0 -133
  59. package/dist/tests/bench/metrics.js +0 -2319
  60. package/dist/tests/bench/metrics.test.js +0 -1144
  61. package/dist/tests/bench/no-os-tmpdir-invariant.test.js +0 -43
  62. package/dist/tests/bench/report.js +0 -1821
  63. package/dist/tests/bench/report.test.js +0 -989
  64. package/dist/tests/bench/runner.js +0 -536
  65. package/dist/tests/bench/runner.test.js +0 -958
  66. package/dist/tests/bench/search-bridge.test.js +0 -331
  67. package/dist/tests/bench/tmp.js +0 -41
  68. package/dist/tests/bench/trajectory.js +0 -116
  69. package/dist/tests/bench/trajectory.test.js +0 -127
  70. package/dist/tests/bench/verifier.js +0 -109
  71. package/dist/tests/bench/verifier.test.js +0 -118
  72. package/dist/tests/bench/workflow-evaluator.js +0 -557
  73. package/dist/tests/bench/workflow-evaluator.test.js +0 -421
  74. package/dist/tests/bench/workflow-spec.js +0 -358
  75. package/dist/tests/bench/workflow-spec.test.js +0 -363
  76. package/dist/tests/bench/workflow-trace.js +0 -438
  77. package/dist/tests/bench/workflow-trace.test.js +0 -254
  78. package/dist/tests/benchmark-search-quality.js +0 -536
  79. package/dist/tests/benchmark-suite.js +0 -1441
  80. package/dist/tests/capture-cli.test.js +0 -112
  81. package/dist/tests/cli-errors.test.js +0 -203
  82. package/dist/tests/commands/events.test.js +0 -370
  83. package/dist/tests/commands/history.test.js +0 -223
  84. package/dist/tests/commands/import.test.js +0 -103
  85. package/dist/tests/commands/proposal-cli.test.js +0 -209
  86. package/dist/tests/commands/reflect-propose-cli.test.js +0 -333
  87. package/dist/tests/commands/remember.test.js +0 -97
  88. package/dist/tests/commands/scope-flags.test.js +0 -300
  89. package/dist/tests/commands/search.test.js +0 -537
  90. package/dist/tests/commands/show-indexer-parity.test.js +0 -117
  91. package/dist/tests/commands/show.test.js +0 -294
  92. package/dist/tests/common.test.js +0 -266
  93. package/dist/tests/completions.test.js +0 -142
  94. package/dist/tests/config-cli.test.js +0 -193
  95. package/dist/tests/config-llm-features.test.js +0 -139
  96. package/dist/tests/config.test.js +0 -544
  97. package/dist/tests/contracts/migration-baseline.test.js +0 -43
  98. package/dist/tests/contracts/reflect-propose-envelope.test.js +0 -139
  99. package/dist/tests/contracts/spec-helpers.js +0 -46
  100. package/dist/tests/contracts/v1-spec-section-11-proposal-queue.test.js +0 -228
  101. package/dist/tests/contracts/v1-spec-section-12-agent-config.test.js +0 -56
  102. package/dist/tests/contracts/v1-spec-section-13-lesson-type.test.js +0 -34
  103. package/dist/tests/contracts/v1-spec-section-14-llm-features.test.js +0 -94
  104. package/dist/tests/contracts/v1-spec-section-4-1-asset-types.test.js +0 -39
  105. package/dist/tests/contracts/v1-spec-section-4-2-quality-rules.test.js +0 -44
  106. package/dist/tests/contracts/v1-spec-section-5-configuration.test.js +0 -47
  107. package/dist/tests/contracts/v1-spec-section-6-orchestration.test.js +0 -40
  108. package/dist/tests/contracts/v1-spec-section-7-module-layout.test.js +0 -58
  109. package/dist/tests/contracts/v1-spec-section-8-extension-points.test.js +0 -34
  110. package/dist/tests/contracts/v1-spec-section-9-4-cli-surface.test.js +0 -75
  111. package/dist/tests/contracts/v1-spec-section-9-7-llm-agent-boundary.test.js +0 -36
  112. package/dist/tests/core/write-source.test.js +0 -366
  113. package/dist/tests/curate-command.test.js +0 -87
  114. package/dist/tests/db-scoring.test.js +0 -201
  115. package/dist/tests/db.test.js +0 -654
  116. package/dist/tests/distill-cli-flag.test.js +0 -208
  117. package/dist/tests/distill.test.js +0 -515
  118. package/dist/tests/docker-install.test.js +0 -120
  119. package/dist/tests/e2e.test.js +0 -1398
  120. package/dist/tests/embedder.test.js +0 -340
  121. package/dist/tests/embedding-model-config.test.js +0 -379
  122. package/dist/tests/feedback-command.test.js +0 -172
  123. package/dist/tests/file-context.test.js +0 -552
  124. package/dist/tests/fixtures/scripts/git/summarize-diff.js +0 -9
  125. package/dist/tests/fixtures/scripts/lint/eslint-check.js +0 -7
  126. package/dist/tests/fixtures/stashes/load.js +0 -166
  127. package/dist/tests/fixtures/stashes/load.test.js +0 -88
  128. package/dist/tests/fixtures/stashes/ranking-baseline/scripts/mem0-search.js +0 -12
  129. package/dist/tests/frontmatter.test.js +0 -190
  130. package/dist/tests/fts-field-weighting.test.js +0 -254
  131. package/dist/tests/fuzzy-search.test.js +0 -230
  132. package/dist/tests/git-provider-clone.test.js +0 -45
  133. package/dist/tests/github.test.js +0 -161
  134. package/dist/tests/graph-boost-ranking.test.js +0 -305
  135. package/dist/tests/graph-extraction.test.js +0 -282
  136. package/dist/tests/helpers/usage-events.js +0 -8
  137. package/dist/tests/index-pass-llm.test.js +0 -161
  138. package/dist/tests/indexer.test.js +0 -559
  139. package/dist/tests/info-command.test.js +0 -166
  140. package/dist/tests/init.test.js +0 -69
  141. package/dist/tests/install-script.test.js +0 -246
  142. package/dist/tests/integration/agent-real-profile.test.js +0 -94
  143. package/dist/tests/issue-36-repro.test.js +0 -304
  144. package/dist/tests/issues-191-194.test.js +0 -160
  145. package/dist/tests/lesson-lint.test.js +0 -111
  146. package/dist/tests/llm-client.test.js +0 -115
  147. package/dist/tests/llm-feature-gate.test.js +0 -151
  148. package/dist/tests/llm.test.js +0 -139
  149. package/dist/tests/lockfile.test.js +0 -216
  150. package/dist/tests/manifest.test.js +0 -205
  151. package/dist/tests/markdown.test.js +0 -126
  152. package/dist/tests/matchers-unit.test.js +0 -189
  153. package/dist/tests/memory-inference.test.js +0 -299
  154. package/dist/tests/merge-scoring.test.js +0 -136
  155. package/dist/tests/metadata.test.js +0 -313
  156. package/dist/tests/migration-help.test.js +0 -89
  157. package/dist/tests/origin-resolve.test.js +0 -124
  158. package/dist/tests/output-baseline.test.js +0 -217
  159. package/dist/tests/output-shapes-unit.test.js +0 -476
  160. package/dist/tests/parallel-search.test.js +0 -272
  161. package/dist/tests/parameter-metadata.test.js +0 -365
  162. package/dist/tests/paths.test.js +0 -177
  163. package/dist/tests/progressive-disclosure.test.js +0 -280
  164. package/dist/tests/proposals.test.js +0 -279
  165. package/dist/tests/proposed-quality.test.js +0 -271
  166. package/dist/tests/provider-registry.test.js +0 -32
  167. package/dist/tests/ranking-regression.test.js +0 -548
  168. package/dist/tests/reflect-propose.test.js +0 -455
  169. package/dist/tests/registry-build-index.test.js +0 -378
  170. package/dist/tests/registry-cli.test.js +0 -290
  171. package/dist/tests/registry-index-v2.test.js +0 -430
  172. package/dist/tests/registry-install.test.js +0 -728
  173. package/dist/tests/registry-providers/parity.test.js +0 -189
  174. package/dist/tests/registry-providers/skills-sh.test.js +0 -309
  175. package/dist/tests/registry-providers/static-index.test.js +0 -204
  176. package/dist/tests/registry-resolve.test.js +0 -126
  177. package/dist/tests/registry-search.test.js +0 -723
  178. package/dist/tests/remember-frontmatter.test.js +0 -380
  179. package/dist/tests/remember-unit.test.js +0 -123
  180. package/dist/tests/ripgrep-install.test.js +0 -251
  181. package/dist/tests/ripgrep-resolve.test.js +0 -108
  182. package/dist/tests/ripgrep.test.js +0 -163
  183. package/dist/tests/save-command.test.js +0 -94
  184. package/dist/tests/save-trust-qa-fixes.test.js +0 -270
  185. package/dist/tests/scoring-pipeline.test.js +0 -648
  186. package/dist/tests/search-include-proposed-cli.test.js +0 -118
  187. package/dist/tests/self-update.test.js +0 -442
  188. package/dist/tests/semantic-search-e2e.test.js +0 -512
  189. package/dist/tests/semantic-status.test.js +0 -471
  190. package/dist/tests/setup-run.integration.js +0 -877
  191. package/dist/tests/setup-wizard.test.js +0 -198
  192. package/dist/tests/setup.test.js +0 -131
  193. package/dist/tests/source-add.test.js +0 -11
  194. package/dist/tests/source-clone.test.js +0 -254
  195. package/dist/tests/source-manage.test.js +0 -366
  196. package/dist/tests/source-providers/filesystem.test.js +0 -82
  197. package/dist/tests/source-providers/git.test.js +0 -252
  198. package/dist/tests/source-providers/website.test.js +0 -128
  199. package/dist/tests/source-qa-fixes.test.js +0 -268
  200. package/dist/tests/source-registry.test.js +0 -350
  201. package/dist/tests/source-resolve.test.js +0 -100
  202. package/dist/tests/source-source.test.js +0 -221
  203. package/dist/tests/source.test.js +0 -533
  204. package/dist/tests/tar-utils-scan.test.js +0 -73
  205. package/dist/tests/toggle-components.test.js +0 -73
  206. package/dist/tests/usage-telemetry.test.js +0 -265
  207. package/dist/tests/utility-scoring.test.js +0 -558
  208. package/dist/tests/vault-load-error.test.js +0 -78
  209. package/dist/tests/vault-qa-fixes.test.js +0 -194
  210. package/dist/tests/vault.test.js +0 -429
  211. package/dist/tests/vector-search.test.js +0 -608
  212. package/dist/tests/walker.test.js +0 -252
  213. package/dist/tests/wave2-cluster-bc.test.js +0 -228
  214. package/dist/tests/wave2-cluster-d.test.js +0 -180
  215. package/dist/tests/wave2-cluster-e.test.js +0 -179
  216. package/dist/tests/wiki-qa-fixes.test.js +0 -270
  217. package/dist/tests/wiki.test.js +0 -529
  218. package/dist/tests/workflow-cli.test.js +0 -271
  219. package/dist/tests/workflow-markdown.test.js +0 -171
  220. package/dist/tests/workflow-path-escape.test.js +0 -132
  221. package/dist/tests/workflow-qa-fixes.test.js +0 -377
  222. package/dist/tests/workflows/indexer-rejection.test.js +0 -213
  223. /package/dist/{src/commands → commands}/completions.js +0 -0
  224. /package/dist/{src/commands → commands}/curate.js +0 -0
  225. /package/dist/{src/commands → commands}/distill.js +0 -0
  226. /package/dist/{src/commands → commands}/events.js +0 -0
  227. /package/dist/{src/commands → commands}/info.js +0 -0
  228. /package/dist/{src/commands → commands}/init.js +0 -0
  229. /package/dist/{src/commands → commands}/install-audit.js +0 -0
  230. /package/dist/{src/commands → commands}/installed-stashes.js +0 -0
  231. /package/dist/{src/commands → commands}/migration-help.js +0 -0
  232. /package/dist/{src/commands → commands}/proposal.js +0 -0
  233. /package/dist/{src/commands → commands}/propose.js +0 -0
  234. /package/dist/{src/commands → commands}/reflect.js +0 -0
  235. /package/dist/{src/commands → commands}/remember.js +0 -0
  236. /package/dist/{src/commands → commands}/self-update.js +0 -0
  237. /package/dist/{src/commands → commands}/source-clone.js +0 -0
  238. /package/dist/{src/commands → commands}/vault.js +0 -0
  239. /package/dist/{src/core → core}/asset-ref.js +0 -0
  240. /package/dist/{src/core → core}/asset-registry.js +0 -0
  241. /package/dist/{src/core → core}/asset-spec.js +0 -0
  242. /package/dist/{src/core → core}/common.js +0 -0
  243. /package/dist/{src/core → core}/errors.js +0 -0
  244. /package/dist/{src/core → core}/events.js +0 -0
  245. /package/dist/{src/core → core}/frontmatter.js +0 -0
  246. /package/dist/{src/core → core}/lesson-lint.js +0 -0
  247. /package/dist/{src/core → core}/markdown.js +0 -0
  248. /package/dist/{src/core → core}/paths.js +0 -0
  249. /package/dist/{src/core → core}/proposals.js +0 -0
  250. /package/dist/{src/core → core}/warn.js +0 -0
  251. /package/dist/{src/core → core}/write-source.js +0 -0
  252. /package/dist/{src/indexer → indexer}/db.js +0 -0
  253. /package/dist/{src/indexer → indexer}/file-context.js +0 -0
  254. /package/dist/{src/indexer → indexer}/graph-boost.js +0 -0
  255. /package/dist/{src/indexer → indexer}/graph-extraction.js +0 -0
  256. /package/dist/{src/indexer → indexer}/manifest.js +0 -0
  257. /package/dist/{src/indexer → indexer}/memory-inference.js +0 -0
  258. /package/dist/{src/indexer → indexer}/metadata.js +0 -0
  259. /package/dist/{src/indexer → indexer}/search-fields.js +0 -0
  260. /package/dist/{src/indexer → indexer}/semantic-status.js +0 -0
  261. /package/dist/{src/indexer → indexer}/usage-events.js +0 -0
  262. /package/dist/{src/indexer → indexer}/walker.js +0 -0
  263. /package/dist/{src/integrations → integrations}/agent/config.js +0 -0
  264. /package/dist/{src/integrations → integrations}/agent/detect.js +0 -0
  265. /package/dist/{src/integrations → integrations}/agent/index.js +0 -0
  266. /package/dist/{src/integrations → integrations}/agent/prompts.js +0 -0
  267. /package/dist/{src/integrations → integrations}/lockfile.js +0 -0
  268. /package/dist/{src/llm → llm}/client.js +0 -0
  269. /package/dist/{src/llm → llm}/embedder.js +0 -0
  270. /package/dist/{src/llm → llm}/embedders/cache.js +0 -0
  271. /package/dist/{src/llm → llm}/embedders/local.js +0 -0
  272. /package/dist/{src/llm → llm}/embedders/types.js +0 -0
  273. /package/dist/{src/llm → llm}/feature-gate.js +0 -0
  274. /package/dist/{src/llm → llm}/graph-extract.js +0 -0
  275. /package/dist/{src/llm → llm}/index-passes.js +0 -0
  276. /package/dist/{src/llm → llm}/memory-infer.js +0 -0
  277. /package/dist/{src/llm → llm}/metadata-enhance.js +0 -0
  278. /package/dist/{src/output → output}/context.js +0 -0
  279. /package/dist/{src/registry → registry}/create-provider-registry.js +0 -0
  280. /package/dist/{src/registry → registry}/factory.js +0 -0
  281. /package/dist/{src/registry → registry}/origin-resolve.js +0 -0
  282. /package/dist/{src/registry → registry}/providers/index.js +0 -0
  283. /package/dist/{src/registry → registry}/providers/skills-sh.js +0 -0
  284. /package/dist/{src/registry → registry}/providers/types.js +0 -0
  285. /package/dist/{src/registry → registry}/resolve.js +0 -0
  286. /package/dist/{src/registry → registry}/types.js +0 -0
  287. /package/dist/{src/setup → setup}/detect.js +0 -0
  288. /package/dist/{src/setup → setup}/ripgrep-install.js +0 -0
  289. /package/dist/{src/setup → setup}/ripgrep-resolve.js +0 -0
  290. /package/dist/{src/setup → setup}/steps.js +0 -0
  291. /package/dist/{src/sources → sources}/include.js +0 -0
  292. /package/dist/{src/sources → sources}/provider-factory.js +0 -0
  293. /package/dist/{src/sources → sources}/provider.js +0 -0
  294. /package/dist/{src/sources → sources}/providers/filesystem.js +0 -0
  295. /package/dist/{src/sources → sources}/providers/git.js +0 -0
  296. /package/dist/{src/sources → sources}/providers/index.js +0 -0
  297. /package/dist/{src/sources → sources}/providers/install-types.js +0 -0
  298. /package/dist/{src/sources → sources}/providers/npm.js +0 -0
  299. /package/dist/{src/sources → sources}/providers/provider-utils.js +0 -0
  300. /package/dist/{src/sources → sources}/providers/sync-from-ref.js +0 -0
  301. /package/dist/{src/sources → sources}/providers/tar-utils.js +0 -0
  302. /package/dist/{src/sources → sources}/providers/website.js +0 -0
  303. /package/dist/{src/sources → sources}/resolve.js +0 -0
  304. /package/dist/{src/sources → sources}/types.js +0 -0
  305. /package/dist/{src/templates → templates}/wiki-templates.js +0 -0
  306. /package/dist/{src/version.js → version.js} +0 -0
  307. /package/dist/{src/workflows → workflows}/authoring.js +0 -0
  308. /package/dist/{src/workflows → workflows}/cli.js +0 -0
  309. /package/dist/{src/workflows → workflows}/db.js +0 -0
  310. /package/dist/{src/workflows → workflows}/document-cache.js +0 -0
  311. /package/dist/{src/workflows → workflows}/parser.js +0 -0
  312. /package/dist/{src/workflows → workflows}/renderer.js +0 -0
  313. /package/dist/{src/workflows → workflows}/schema.js +0 -0
  314. /package/dist/{src/workflows → workflows}/validator.js +0 -0
@@ -1,1398 +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
- test("cli: akm search --limit 2 respects limit", async () => {
504
- // QA #14: empty query now throws UsageError (exit 2); use a real query
505
- const result = runCli("search", "docker", "--limit", "2");
506
- expect(result.exitCode).toBe(0);
507
- const json = parseJson(result.stdout);
508
- expect(json.hits.length).toBeLessThanOrEqual(2);
509
- });
510
- test("cli: akm search defaults to brief JSON output", async () => {
511
- const result = runCli("search", "docker", "--type", "script");
512
- expect(result.exitCode).toBe(0);
513
- const json = parseJson(result.stdout);
514
- expect(json.source).toBeUndefined();
515
- expect(json.timing).toBeUndefined();
516
- expect(json.hits.every((h) => h.ref === undefined)).toBe(true);
517
- expect(json.hits.every((h) => h.type !== undefined)).toBe(true);
518
- expect(json.hits.every((h) => h.name !== undefined)).toBe(true);
519
- expect(json.hits.every((h) => h.action !== undefined)).toBe(true);
520
- });
521
- test("cli: akm search default source is stash", async () => {
522
- const result = runCli("search", "docker", "--detail", "full");
523
- expect(result.exitCode).toBe(0);
524
- const json = parseJson(result.stdout);
525
- expect(json.source).toBe("stash");
526
- expect(json.hits.every((h) => h.type !== "registry")).toBe(true);
527
- });
528
- test("cli: akm search --detail normal includes score and description", async () => {
529
- const result = runCli("search", "docker", "--type", "script", "--detail", "normal");
530
- expect(result.exitCode).toBe(0);
531
- const json = parseJson(result.stdout);
532
- expect(json.hits.some((h) => h.score !== undefined)).toBe(true);
533
- expect(json.hits.some((h) => h.action !== undefined)).toBe(true);
534
- });
535
- test("cli: akm search --detail full includes timing and whyMatched", async () => {
536
- const result = runCli("search", "docker", "--detail", "full");
537
- expect(result.exitCode).toBe(0);
538
- const json = parseJson(result.stdout);
539
- expect(json.timing).toBeDefined();
540
- expect(json.hits.every((h) => h.whyMatched !== undefined)).toBe(true);
541
- });
542
- test("cli: akm search --format yaml returns valid YAML output", async () => {
543
- const result = spawnSync("bun", [CLI, "search", "docker", "--format", "yaml"], {
544
- encoding: "utf8",
545
- timeout: 30_000,
546
- env: { ...process.env },
547
- });
548
- expect(result.status).toBe(0);
549
- // Output must be YAML (unquoted keys like "hits:"), not JSON (quoted keys like '"hits"')
550
- const hasYaml = result.stdout.includes("hits:") && !result.stdout.includes('"hits"');
551
- expect(hasYaml).toBe(true);
552
- expect(result.stdout).toContain("docker");
553
- // Should not contain the fallback warning
554
- expect(result.stderr).not.toContain("YAML output not available");
555
- });
556
- test("cli: akm show --format text includes execution fields", async () => {
557
- const result = spawnSync("bun", [CLI, "show", "command:release.md", "--format", "text"], {
558
- encoding: "utf8",
559
- timeout: 30_000,
560
- env: { ...process.env },
561
- });
562
- expect(result.status).toBe(0);
563
- expect(result.stdout).toContain("# command: release");
564
- expect(result.stdout).toContain("description:");
565
- expect(result.stdout).toContain("Create a new release");
566
- expect(result.stdout).toContain("npm version");
567
- });
568
- test("cli: akm search --format invalid value fails with clear error", async () => {
569
- const result = runCli("search", "docker", "--format", "xml");
570
- expect(result.exitCode).not.toBe(0);
571
- const output = result.stdout + result.stderr;
572
- expect(output).toContain("Invalid value for --format: xml. Expected one of: json|yaml|text");
573
- });
574
- test("cli: akm search --detail invalid value fails with clear error", async () => {
575
- const result = runCli("search", "docker", "--detail", "max");
576
- expect(result.exitCode).not.toBe(0);
577
- const output = result.stdout + result.stderr;
578
- expect(output).toContain("Invalid value for --detail: max. Expected one of: brief|normal|full");
579
- });
580
- test("cli: akm search --source invalid value fails with clear error", async () => {
581
- const result = runCli("search", "docker", "--source", "bad");
582
- expect(result.exitCode).not.toBe(0);
583
- const output = result.stdout + result.stderr;
584
- expect(output).toContain("Invalid value for --source: bad. Expected one of: stash|registry|both");
585
- });
586
- test("cli: akm search default output excludes full-detail fields", async () => {
587
- const result = runCli("search", "docker");
588
- expect(result.exitCode).toBe(0);
589
- const json = parseJson(result.stdout);
590
- expect(json.hits.length).toBeGreaterThan(0);
591
- for (const hit of json.hits) {
592
- expect(hit.whyMatched).toBeUndefined();
593
- expect(hit.editable).toBeUndefined();
594
- expect(hit.editHint).toBeUndefined();
595
- }
596
- expect(json.timing).toBeUndefined();
597
- });
598
- test("cli: akm show returns asset content", async () => {
599
- const result = runCli("show", "skill:code-review");
600
- expect(result.exitCode).toBe(0);
601
- const json = parseJson(result.stdout);
602
- expect(json.type).toBe("skill");
603
- expect(json.content).toContain("Code Review Skill");
604
- // QA #7: path is now always included in the JSON shape (not just --detail full)
605
- expect(json.path).toBeDefined();
606
- });
607
- test("cli: akm show --detail full includes schemaVersion and path", async () => {
608
- const result = runCli("show", "command:release.md", "--detail", "full");
609
- expect(result.exitCode).toBe(0);
610
- const json = parseJson(result.stdout);
611
- expect(json.type).toBe("command");
612
- expect(json.schemaVersion).toBe(1);
613
- expect(json.path).toBeTruthy();
614
- });
615
- test("cli: akm show command returns template", async () => {
616
- const result = runCli("show", "command:release.md");
617
- expect(result.exitCode).toBe(0);
618
- const json = parseJson(result.stdout);
619
- expect(json.type).toBe("command");
620
- expect(json.description).toBeTruthy();
621
- expect(json.template).toContain("npm version");
622
- });
623
- test("cli: akm index builds index and reports stats", async () => {
624
- const result = runCli("index");
625
- expect(result.exitCode).toBe(0);
626
- const json = parseJson(result.stdout);
627
- expect(json.totalEntries).toBeGreaterThan(0);
628
- expect(json.indexPath).toBeTruthy();
629
- });
630
- test("cli: akm index --full returns mode full", async () => {
631
- const result = runCli("index", "--full");
632
- expect(result.exitCode).toBe(0);
633
- const json = parseJson(result.stdout);
634
- expect(json.mode).toBe("full");
635
- });
636
- test("cli: akm config set/get manages llm settings via JSON", async () => {
637
- const setResult = runCli("config", "set", "llm", '{"endpoint":"http://localhost:11434/v1/chat/completions","model":"llama3.2","maxTokens":256}');
638
- expect(setResult.exitCode).toBe(0);
639
- const getResult = runCli("config", "get", "llm");
640
- expect(getResult.exitCode).toBe(0);
641
- const json = parseJson(getResult.stdout);
642
- expect(json).toMatchObject({
643
- endpoint: "http://localhost:11434/v1/chat/completions",
644
- model: "llama3.2",
645
- maxTokens: 256,
646
- });
647
- });
648
- test("cli: akm with no command prints usage", async () => {
649
- const result = runCli();
650
- expect(result.exitCode).not.toBe(0);
651
- const output = result.stdout + result.stderr;
652
- expect(output).toContain("No command specified");
653
- });
654
- test("cli: akm show with no ref prints error", async () => {
655
- const result = runCli("show");
656
- expect(result.exitCode).not.toBe(0);
657
- const output = result.stdout + result.stderr;
658
- expect(output).toContain("Missing required positional argument");
659
- });
660
- });
661
- describe("Scenario: Registry lifecycle CLI (no network)", () => {
662
- let savedStashDir;
663
- beforeEach(() => {
664
- savedStashDir = process.env.AKM_STASH_DIR;
665
- });
666
- afterEach(() => {
667
- if (savedStashDir === undefined) {
668
- delete process.env.AKM_STASH_DIR;
669
- }
670
- else {
671
- process.env.AKM_STASH_DIR = savedStashDir;
672
- }
673
- });
674
- test("cli: akm list returns empty installed set when none configured", async () => {
675
- const stashDir = createEmptyStashDir("akm-e2e-registry-empty-");
676
- process.env.AKM_STASH_DIR = stashDir;
677
- saveConfig({ semanticSearchMode: "off" });
678
- try {
679
- const result = runCli("list");
680
- expect(result.exitCode).toBe(0);
681
- const json = parseJson(result.stdout);
682
- expect(json.totalSources).toBe(0);
683
- expect(json.sources).toEqual([]);
684
- }
685
- finally {
686
- fs.rmSync(stashDir, { recursive: true, force: true });
687
- }
688
- });
689
- test("cli: registered external wikis appear in list and wiki remove clears stale search hits", async () => {
690
- const stashDir = createEmptyStashDir("akm-e2e-wiki-source-");
691
- const externalWiki = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-ext-wiki-"));
692
- process.env.AKM_STASH_DIR = stashDir;
693
- saveConfig({ semanticSearchMode: "off" });
694
- fs.mkdirSync(path.join(externalWiki, "tools", "documentation", "how-to"), { recursive: true });
695
- fs.writeFileSync(path.join(externalWiki, "tools", "documentation", "how-to", "001-get-started.md"), "---\ndescription: External docs\n---\n# Start\n", "utf8");
696
- try {
697
- const registerResult = runCli("wiki", "register", "ics-docs", externalWiki, "--format", "json");
698
- expect(registerResult.exitCode).toBe(0);
699
- const registerJson = parseJson(registerResult.stdout);
700
- expect(registerJson.ref).toBe("ics-docs");
701
- expect(registerJson.sourceAdded).toEqual(expect.objectContaining({
702
- type: "filesystem",
703
- name: "ics-docs",
704
- wiki: "ics-docs",
705
- }));
706
- const listResult = runCli("list", "--format", "json");
707
- expect(listResult.exitCode).toBe(0);
708
- const listJson = parseJson(listResult.stdout);
709
- expect(listJson.sources).toEqual(expect.arrayContaining([
710
- expect.objectContaining({
711
- name: "ics-docs",
712
- kind: "filesystem",
713
- wiki: "ics-docs",
714
- }),
715
- ]));
716
- const searchResult = runCli("search", "documentation", "--type", "wiki", "--detail", "normal", "--format", "json");
717
- expect(searchResult.exitCode).toBe(0);
718
- const searchJson = parseJson(searchResult.stdout);
719
- expect(searchJson.hits).toEqual(expect.arrayContaining([
720
- expect.objectContaining({
721
- name: "ics-docs/tools/documentation/how-to/001-get-started",
722
- action: "akm show wiki:ics-docs/tools/documentation/how-to/001-get-started -> read the wiki page",
723
- }),
724
- ]));
725
- const removeResult = runCli("wiki", "remove", "ics-docs", "--force", "--format", "json");
726
- expect(removeResult.exitCode).toBe(0);
727
- const removedSearchResult = runCli("search", "documentation", "--type", "wiki", "--format", "json");
728
- expect(removedSearchResult.exitCode).toBe(0);
729
- const removedSearchJson = parseJson(removedSearchResult.stdout);
730
- expect(removedSearchJson.hits).toEqual([]);
731
- }
732
- finally {
733
- fs.rmSync(stashDir, { recursive: true, force: true });
734
- fs.rmSync(externalWiki, { recursive: true, force: true });
735
- }
736
- });
737
- test("cli: wiki remove --force hides preserved raw-only leftovers from wiki list", async () => {
738
- const stashDir = createEmptyStashDir("akm-e2e-wiki-remove-");
739
- const rawSource = path.join(stashDir, "source.md");
740
- process.env.AKM_STASH_DIR = stashDir;
741
- saveConfig({ semanticSearchMode: "off" });
742
- fs.writeFileSync(rawSource, "# Test\n", "utf8");
743
- try {
744
- const createResult = runCli("wiki", "create", "my-notes", "--format", "json");
745
- expect(createResult.exitCode).toBe(0);
746
- const stashResult = runCli("wiki", "stash", "my-notes", rawSource, "--as", "test-page", "--format", "json");
747
- expect(stashResult.exitCode).toBe(0);
748
- const removeResult = runCli("wiki", "remove", "my-notes", "--force", "--format", "text");
749
- expect(removeResult.exitCode).toBe(0);
750
- expect(removeResult.stdout).toContain("raw/ preserved at");
751
- const listResult = runCli("wiki", "list", "--format", "json");
752
- expect(listResult.exitCode).toBe(0);
753
- const listJson = parseJson(listResult.stdout);
754
- expect(listJson.wikis).toEqual([]);
755
- const showResult = runCli("wiki", "show", "my-notes", "--format", "json");
756
- expect(showResult.exitCode).not.toBe(0);
757
- expect(showResult.stderr).toContain("Wiki not found: my-notes");
758
- const cleanupResult = runCli("wiki", "remove", "my-notes", "--force", "--with-sources", "--format", "json");
759
- expect(cleanupResult.exitCode).toBe(0);
760
- }
761
- finally {
762
- fs.rmSync(stashDir, { recursive: true, force: true });
763
- }
764
- });
765
- test("cli: akm index text output surfaces skipped-asset warnings", async () => {
766
- const stashDir = createEmptyStashDir("akm-e2e-index-warn-");
767
- process.env.AKM_STASH_DIR = stashDir;
768
- saveConfig({ semanticSearchMode: "off" });
769
- fs.mkdirSync(path.join(stashDir, "workflows"), { recursive: true });
770
- fs.writeFileSync(path.join(stashDir, "workflows", "good.md"), [
771
- "---",
772
- "description: Good workflow",
773
- "---",
774
- "",
775
- "# Workflow: Good",
776
- "",
777
- "## Step: First",
778
- "Step ID: first",
779
- "### Instructions",
780
- "Do it.",
781
- "",
782
- ].join("\n"), "utf8");
783
- // Truly malformed: no `# Workflow:` heading. Intro prose between the
784
- // title and the first step is now permitted (#158).
785
- fs.writeFileSync(path.join(stashDir, "workflows", "bad.md"), [
786
- "---",
787
- "description: Bad workflow",
788
- "---",
789
- "",
790
- "## Step: First",
791
- "Step ID: first",
792
- "### Instructions",
793
- "Do it.",
794
- "",
795
- ].join("\n"), "utf8");
796
- try {
797
- const result = runCli("index", "--full", "--format", "text");
798
- expect(result.exitCode).toBe(0);
799
- expect(result.stdout).toContain("Warnings (");
800
- expect(result.stdout).toContain(path.join(stashDir, "workflows", "bad.md"));
801
- }
802
- finally {
803
- fs.rmSync(stashDir, { recursive: true, force: true });
804
- }
805
- });
806
- test("cli: akm remove resolves parsed ref id and removes cache directory", async () => {
807
- const stashDir = createEmptyStashDir("akm-e2e-registry-remove-");
808
- const stashRoot = path.join(stashDir, "registry-stash");
809
- const cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-cache-remove-"));
810
- fs.mkdirSync(path.join(stashRoot, "scripts"), { recursive: true });
811
- process.env.AKM_STASH_DIR = stashDir;
812
- saveConfig({
813
- semanticSearchMode: "off",
814
- installed: [
815
- {
816
- id: "npm:@scope/stash",
817
- source: "npm",
818
- ref: "npm:@scope/stash@1.0.0",
819
- artifactUrl: "https://registry.npmjs.org/@scope/stash/-/stash-1.0.0.tgz",
820
- resolvedVersion: "1.0.0",
821
- resolvedRevision: "abc123",
822
- stashRoot,
823
- cacheDir,
824
- installedAt: new Date().toISOString(),
825
- },
826
- ],
827
- });
828
- try {
829
- const result = runCli("remove", "npm:@scope/stash@latest");
830
- expect(result.exitCode).toBe(0);
831
- const json = parseJson(result.stdout);
832
- expect(expectDefined(json.removed).id).toBe("npm:@scope/stash");
833
- const config = loadConfig();
834
- expect(config.installed).toBeUndefined();
835
- expect(fs.existsSync(cacheDir)).toBe(false);
836
- }
837
- finally {
838
- fs.rmSync(stashDir, { recursive: true, force: true });
839
- fs.rmSync(cacheDir, { recursive: true, force: true });
840
- }
841
- });
842
- test("cli: akm update requires target or --all", async () => {
843
- const stashDir = createEmptyStashDir("akm-e2e-registry-update-");
844
- process.env.AKM_STASH_DIR = stashDir;
845
- saveConfig({ semanticSearchMode: "off" });
846
- try {
847
- const result = runCli("update");
848
- expect(result.exitCode).not.toBe(0);
849
- const output = result.stdout + result.stderr;
850
- expect(output).toContain("Either <target> or --all is required.");
851
- }
852
- finally {
853
- fs.rmSync(stashDir, { recursive: true, force: true });
854
- }
855
- });
856
- test("cli: akm update rejects target with --all", async () => {
857
- const stashDir = createEmptyStashDir("akm-e2e-registry-update-both-");
858
- process.env.AKM_STASH_DIR = stashDir;
859
- saveConfig({ semanticSearchMode: "off" });
860
- try {
861
- const result = runCli("update", "npm:@scope/stash", "--all");
862
- expect(result.exitCode).not.toBe(0);
863
- const output = result.stdout + result.stderr;
864
- expect(output).toContain("Specify either <target> or --all, not both.");
865
- }
866
- finally {
867
- fs.rmSync(stashDir, { recursive: true, force: true });
868
- }
869
- });
870
- test("cli: akm update missing target returns stable not-installed error", async () => {
871
- const stashDir = createEmptyStashDir("akm-e2e-registry-missing-");
872
- process.env.AKM_STASH_DIR = stashDir;
873
- saveConfig({ semanticSearchMode: "off" });
874
- try {
875
- const result = runCli("update", "npm:@scope/stash");
876
- expect(result.exitCode).not.toBe(0);
877
- const output = result.stdout + result.stderr;
878
- expect(output).toContain("No matching source for target: npm:@scope/stash");
879
- }
880
- finally {
881
- fs.rmSync(stashDir, { recursive: true, force: true });
882
- }
883
- });
884
- });
885
- // ═══════════════════════════════════════════════════════════════════════════
886
- // Scenario 3a: CLI upgrade and update --force commands
887
- // ═══════════════════════════════════════════════════════════════════════════
888
- describe("Scenario: upgrade and update --force (no network)", () => {
889
- let savedStashDir;
890
- beforeEach(() => {
891
- savedStashDir = process.env.AKM_STASH_DIR;
892
- });
893
- afterEach(() => {
894
- if (savedStashDir === undefined) {
895
- delete process.env.AKM_STASH_DIR;
896
- }
897
- else {
898
- process.env.AKM_STASH_DIR = savedStashDir;
899
- }
900
- });
901
- test("upgrade --check returns version info (mocked fetch)", async () => {
902
- const { checkForUpdate } = await import("../src/commands/self-update");
903
- const result = await withMockedFetch(() => Response.json({ tag_name: "v0.0.14" }), () => checkForUpdate("0.0.13"));
904
- expect(result.currentVersion).toBe("0.0.13");
905
- expect(result.latestVersion).toBe("0.0.14");
906
- expect(result.updateAvailable).toBe(true);
907
- expect(["binary", "bun", "npm", "pnpm", "unknown"]).toContain(result.installMethod);
908
- });
909
- test("performUpgrade detects non-binary install and returns guidance", async () => {
910
- const { performUpgrade } = await import("../src/commands/self-update");
911
- const result = await performUpgrade({
912
- currentVersion: "0.0.13",
913
- latestVersion: "0.0.14",
914
- updateAvailable: true,
915
- installMethod: "unknown",
916
- });
917
- expect(result.upgraded).toBe(false);
918
- expect(["bun", "npm", "pnpm", "unknown"]).toContain(result.installMethod);
919
- expect(result.message).toBeTruthy();
920
- });
921
- test("cli: akm update --help shows --force flag", async () => {
922
- const result = spawnSync("bun", [CLI, "update", "--help"], {
923
- encoding: "utf8",
924
- timeout: 10_000,
925
- });
926
- const output = (result.stdout ?? "") + (result.stderr ?? "");
927
- expect(output).toContain("--force");
928
- expect(output).toContain("Force fresh download");
929
- });
930
- test("cli: akm update --force requires target or --all", async () => {
931
- const stashDir = createEmptyStashDir("akm-e2e-update-force-");
932
- process.env.AKM_STASH_DIR = stashDir;
933
- saveConfig({ semanticSearchMode: "off" });
934
- try {
935
- const result = runCli("update", "--force");
936
- expect(result.exitCode).not.toBe(0);
937
- const output = result.stdout + result.stderr;
938
- expect(output).toContain("Either <target> or --all is required.");
939
- }
940
- finally {
941
- fs.rmSync(stashDir, { recursive: true, force: true });
942
- }
943
- });
944
- test("cli: akm upgrade --help shows --check and --force flags", async () => {
945
- const result = spawnSync("bun", [CLI, "upgrade", "--help"], {
946
- encoding: "utf8",
947
- timeout: 10_000,
948
- });
949
- const output = (result.stdout ?? "") + (result.stderr ?? "");
950
- expect(output).toContain("--check");
951
- expect(output).toContain("--force");
952
- });
953
- test("cli: akm help migrate prints migration guidance for a release", async () => {
954
- const result = spawnSync("bun", [CLI, "help", "migrate", "0.5.0"], {
955
- encoding: "utf8",
956
- timeout: 10_000,
957
- });
958
- const output = (result.stdout ?? "") + (result.stderr ?? "");
959
- expect(result.status).toBe(0);
960
- expect(output).toContain("Migration notes for akm v0.5.0");
961
- expect(output).toContain("## [0.5.0]");
962
- });
963
- test("cli: akm --help lists the help command", async () => {
964
- const result = spawnSync("bun", [CLI, "--help"], {
965
- encoding: "utf8",
966
- timeout: 10_000,
967
- });
968
- const output = (result.stdout ?? "") + (result.stderr ?? "");
969
- expect(output).toContain("help");
970
- expect(output).toContain("focused help topics");
971
- });
972
- test("cli: akm setup --help shows the wizard description (issue #273)", async () => {
973
- // The setup subcommand should advertise its purpose so operators can
974
- // see what the wizard configures (embeddings, LLM, registries, sources,
975
- // agent profiles) without running the interactive flow.
976
- const result = spawnSync("bun", [CLI, "setup", "--help"], {
977
- encoding: "utf8",
978
- timeout: 10_000,
979
- });
980
- const output = (result.stdout ?? "") + (result.stderr ?? "");
981
- expect(output).toContain("Interactive configuration wizard");
982
- expect(output).toContain("embeddings");
983
- expect(output).toContain("LLM");
984
- expect(output).toContain("registries");
985
- expect(output).toContain("sources");
986
- expect(output).toContain("agent profiles");
987
- });
988
- });
989
- // ═══════════════════════════════════════════════════════════════════════════
990
- // Scenario 3b: CLI knowledge view modes (positional syntax)
991
- // ═══════════════════════════════════════════════════════════════════════════
992
- describe("Scenario: CLI knowledge view modes (positional)", () => {
993
- let stashDir;
994
- beforeAll(async () => {
995
- stashDir = copyFixturesToTmp();
996
- process.env.AKM_STASH_DIR = stashDir;
997
- });
998
- beforeEach(() => {
999
- process.env.AKM_STASH_DIR = stashDir;
1000
- });
1001
- afterAll(() => {
1002
- fs.rmSync(stashDir, { recursive: true, force: true });
1003
- });
1004
- test("cli: show knowledge with positional toc", async () => {
1005
- const result = runCli("show", "knowledge:guide.md", "toc");
1006
- expect(result.exitCode).toBe(0);
1007
- const json = parseJson(result.stdout);
1008
- expect(json.type).toBe("knowledge");
1009
- expect(json.content).toContain("# API Reference Guide");
1010
- expect(json.content).toContain("## Getting Started");
1011
- expect(json.content).toContain("lines total");
1012
- });
1013
- test("cli: show knowledge with positional section heading", async () => {
1014
- const result = runCli("show", "knowledge:guide.md", "section", "Getting Started");
1015
- expect(result.exitCode).toBe(0);
1016
- const json = parseJson(result.stdout);
1017
- expect(json.type).toBe("knowledge");
1018
- expect(json.content).toContain("Getting Started");
1019
- expect(json.content).toContain("install the package");
1020
- // Should include sub-headings but not sibling headings
1021
- expect(json.content).toContain("Prerequisites");
1022
- expect(json.content).not.toContain("Authentication");
1023
- });
1024
- test("cli: show knowledge with positional lines start end", async () => {
1025
- const result = runCli("show", "knowledge:guide.md", "lines", "1", "5");
1026
- expect(result.exitCode).toBe(0);
1027
- const json = parseJson(result.stdout);
1028
- expect(json.type).toBe("knowledge");
1029
- // Lines 1-5 cover the frontmatter start
1030
- expect(json.content).toContain("---");
1031
- });
1032
- test("cli: show knowledge rejects legacy --view flag", async () => {
1033
- const result = runCli("show", "knowledge:guide.md", "--view", "toc");
1034
- expect(result.exitCode).not.toBe(0);
1035
- expect(result.stderr).toContain("Legacy show flags are no longer supported");
1036
- });
1037
- test("cli: show knowledge default (no view mode) returns full content", async () => {
1038
- const result = runCli("show", "knowledge:guide.md");
1039
- expect(result.exitCode).toBe(0);
1040
- const json = parseJson(result.stdout);
1041
- expect(json.type).toBe("knowledge");
1042
- expect(json.content).toBeDefined();
1043
- // Full content should include everything
1044
- expect(json.content).toContain("API Reference Guide");
1045
- expect(json.content).toContain("Getting Started");
1046
- expect(json.content).toContain("Authentication");
1047
- });
1048
- test("cli: show knowledge --detail brief omits full content", async () => {
1049
- const result = runCli("show", "knowledge:guide.md", "--detail", "brief");
1050
- expect(result.exitCode).toBe(0);
1051
- const json = parseJson(result.stdout);
1052
- expect(json.type).toBe("knowledge");
1053
- expect(json.name).toBe("guide.md");
1054
- expect(json.description).toBe("Comprehensive guide for the example API");
1055
- expect(json.action).toContain("Reference material");
1056
- expect(json.content).toBeUndefined();
1057
- });
1058
- });
1059
- // ═══════════════════════════════════════════════════════════════════════════
1060
- // Scenario 4: Progressive improvement — user drops scripts, indexes later
1061
- // ═══════════════════════════════════════════════════════════════════════════
1062
- describe("Scenario: Zero-config progressive improvement", () => {
1063
- let stashDir;
1064
- let scenarioCacheDir;
1065
- beforeAll(async () => {
1066
- stashDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-prog-"));
1067
- scenarioCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-cache-s4-"));
1068
- for (const sub of ["scripts", "skills", "commands", "agents"]) {
1069
- fs.mkdirSync(path.join(stashDir, sub), { recursive: true });
1070
- }
1071
- process.env.AKM_STASH_DIR = stashDir;
1072
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
1073
- });
1074
- beforeEach(() => {
1075
- process.env.AKM_STASH_DIR = stashDir;
1076
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
1077
- });
1078
- afterAll(() => {
1079
- fs.rmSync(stashDir, { recursive: true, force: true });
1080
- fs.rmSync(scenarioCacheDir, { recursive: true, force: true });
1081
- });
1082
- test("user drops a script in scripts/ — search finds it by name (no index)", async () => {
1083
- fs.mkdirSync(path.join(stashDir, "scripts", "format"), { recursive: true });
1084
- fs.writeFileSync(path.join(stashDir, "scripts", "format", "prettier-check.sh"), "#!/usr/bin/env bash\n# Format code with Prettier\nprettier --check .\n");
1085
- const result = await akmSearch({ query: "prettier", type: "script" });
1086
- expect(result.hits.length).toBe(1);
1087
- expect(result.hits[0].name).toContain("prettier");
1088
- });
1089
- test("user runs index — metadata generated in database with description from comments", async () => {
1090
- await akmIndex({ stashDir });
1091
- // No .stash.json should be auto-generated
1092
- expect(fs.existsSync(path.join(stashDir, "scripts", "format", ".stash.json"))).toBe(false);
1093
- const db = openDatabase();
1094
- const entries = getAllEntries(db, "script");
1095
- const formatEntry = entries.find((e) => e.entry.name.includes("prettier"));
1096
- expect(formatEntry).toBeDefined();
1097
- expect(formatEntry?.entry.quality).toBe("generated");
1098
- expect(formatEntry?.entry.description).toContain("Format code");
1099
- closeDatabase(db);
1100
- });
1101
- test("user adds more scripts — re-index picks them up", async () => {
1102
- fs.mkdirSync(path.join(stashDir, "scripts", "db"), { recursive: true });
1103
- fs.writeFileSync(path.join(stashDir, "scripts", "db", "migrate.sh"), "#!/usr/bin/env bash\n# Run database migrations\necho 'migrating...'\n");
1104
- const result = await akmIndex({ stashDir });
1105
- expect(result.totalEntries).toBeGreaterThanOrEqual(2);
1106
- const db = openDatabase();
1107
- const entries = getAllEntries(db, "script");
1108
- const migrateEntry = entries.find((e) => e.entry.name.includes("migrate"));
1109
- expect(migrateEntry).toBeDefined();
1110
- expect(migrateEntry?.entry.description).toContain("database migrations");
1111
- closeDatabase(db);
1112
- });
1113
- test("user creates .stash.json manually — overrides used on next index", async () => {
1114
- // User creates a .stash.json override for the format script
1115
- const stashPath = path.join(stashDir, "scripts", "format", ".stash.json");
1116
- fs.writeFileSync(stashPath, JSON.stringify({
1117
- entries: [
1118
- {
1119
- name: "prettier-check",
1120
- type: "script",
1121
- description: "Check code formatting with Prettier",
1122
- tags: ["prettier", "format", "style"],
1123
- filename: "prettier-check.sh",
1124
- },
1125
- ],
1126
- }, null, 2));
1127
- // Re-index — should use user override
1128
- await akmIndex({ stashDir });
1129
- const reloaded = expectDefined(loadStashFile(path.join(stashDir, "scripts", "format")));
1130
- expect(reloaded.entries[0].description).toBe("Check code formatting with Prettier");
1131
- expect(reloaded.entries[0].tags).toContain("prettier");
1132
- expect(reloaded.entries[0].quality).not.toBe("generated");
1133
- });
1134
- test("semantic search finds user-edited metadata after re-index", async () => {
1135
- // Re-index to pick up manual edits in the search index
1136
- await akmIndex({ stashDir });
1137
- const result = await akmSearch({ query: "format code style" });
1138
- expect(result.hits.length).toBeGreaterThan(0);
1139
- expect(result.hits.some((h) => h.name.includes("prettier"))).toBe(true);
1140
- });
1141
- });
1142
- // ═══════════════════════════════════════════════════════════════════════════
1143
- // Scenario 5: Multi-script directory with .stash.json
1144
- // ═══════════════════════════════════════════════════════════════════════════
1145
- describe("Scenario: Multi-script directory with hand-written .stash.json", () => {
1146
- let stashDir;
1147
- let scenarioCacheDir;
1148
- beforeAll(async () => {
1149
- stashDir = copyFixturesToTmp();
1150
- scenarioCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-cache-s5-"));
1151
- process.env.AKM_STASH_DIR = stashDir;
1152
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
1153
- await akmIndex({ stashDir });
1154
- });
1155
- beforeEach(() => {
1156
- process.env.AKM_STASH_DIR = stashDir;
1157
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
1158
- });
1159
- afterAll(() => {
1160
- fs.rmSync(stashDir, { recursive: true, force: true });
1161
- fs.rmSync(scenarioCacheDir, { recursive: true, force: true });
1162
- });
1163
- test("docker/ directory exposes two scripts from single .stash.json", async () => {
1164
- const stash = expectDefined(loadStashFile(path.join(stashDir, "scripts", "docker")));
1165
- expect(stash.entries).toHaveLength(2);
1166
- const names = stash.entries.map((e) => e.name);
1167
- expect(names).toContain("docker-build");
1168
- expect(names).toContain("docker-compose");
1169
- });
1170
- test("search for 'docker build' returns docker-build as top result", async () => {
1171
- const result = await akmSearch({ query: "docker build" });
1172
- expect(result.hits[0].name).toContain("docker");
1173
- expect(result.hits[0].description).toContain("Docker image");
1174
- });
1175
- test("search for 'compose development' returns docker-compose", async () => {
1176
- const result = await akmSearch({ query: "compose development" });
1177
- const composeHit = result.hits.find((h) => h.name.includes("compose"));
1178
- expect(composeHit).toBeDefined();
1179
- expect(composeHit?.tags).toContain("compose");
1180
- });
1181
- });
1182
- // ═══════════════════════════════════════════════════════════════════════════
1183
- // Scenario 6: Index persistence and cache
1184
- // ═══════════════════════════════════════════════════════════════════════════
1185
- describe("Scenario: Index persistence across sessions", () => {
1186
- let stashDir;
1187
- let scenarioCacheDir;
1188
- beforeAll(async () => {
1189
- stashDir = copyFixturesToTmp();
1190
- scenarioCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-cache-s6-"));
1191
- process.env.AKM_STASH_DIR = stashDir;
1192
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
1193
- });
1194
- beforeEach(() => {
1195
- process.env.AKM_STASH_DIR = stashDir;
1196
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
1197
- });
1198
- afterAll(() => {
1199
- fs.rmSync(stashDir, { recursive: true, force: true });
1200
- fs.rmSync(scenarioCacheDir, { recursive: true, force: true });
1201
- });
1202
- test("index is persisted and loadable", async () => {
1203
- await akmIndex({ stashDir });
1204
- const db = openDatabase();
1205
- const version = getMeta(db, "version");
1206
- expect(version).toBe(String(DB_VERSION));
1207
- const storedStashDir = getMeta(db, "stashDir");
1208
- expect(storedStashDir).toBe(stashDir);
1209
- const entries = getAllEntries(db);
1210
- expect(entries.length).toBeGreaterThan(0);
1211
- const builtAt = getMeta(db, "builtAt");
1212
- expect(builtAt).toBeTruthy();
1213
- closeDatabase(db);
1214
- });
1215
- test("search uses persisted index (simulates new session)", async () => {
1216
- // First index
1217
- await akmIndex({ stashDir });
1218
- // Simulate a new session by just doing search (no re-index)
1219
- const result = await akmSearch({ query: "docker" });
1220
- expect(result.hits.length).toBeGreaterThan(0);
1221
- // Should have scores from semantic search, not substring
1222
- expect(result.hits[0].score).toBeDefined();
1223
- });
1224
- test("re-index updates the persisted index", async () => {
1225
- await akmIndex({ stashDir });
1226
- const db1 = openDatabase();
1227
- const entries1 = getAllEntries(db1);
1228
- const builtAt1 = expectDefined(getMeta(db1, "builtAt"));
1229
- closeDatabase(db1);
1230
- // Add a new script
1231
- fs.mkdirSync(path.join(stashDir, "scripts", "new-script"), { recursive: true });
1232
- fs.writeFileSync(path.join(stashDir, "scripts", "new-script", "hello.sh"), "#!/bin/bash\necho hello\n");
1233
- await akmIndex({ stashDir });
1234
- const db2 = openDatabase();
1235
- const entries2 = getAllEntries(db2);
1236
- const builtAt2 = expectDefined(getMeta(db2, "builtAt"));
1237
- closeDatabase(db2);
1238
- expect(entries2.length).toBeGreaterThan(entries1.length);
1239
- expect(new Date(builtAt2).getTime()).toBeGreaterThanOrEqual(new Date(builtAt1).getTime());
1240
- });
1241
- });
1242
- // ═══════════════════════════════════════════════════════════════════════════
1243
- // Scenario 7: Error handling and edge cases
1244
- // ═══════════════════════════════════════════════════════════════════════════
1245
- describe("Scenario: Error handling and edge cases", () => {
1246
- let savedStashDir;
1247
- beforeEach(() => {
1248
- savedStashDir = process.env.AKM_STASH_DIR;
1249
- });
1250
- afterEach(() => {
1251
- if (savedStashDir === undefined) {
1252
- delete process.env.AKM_STASH_DIR;
1253
- }
1254
- else {
1255
- process.env.AKM_STASH_DIR = savedStashDir;
1256
- }
1257
- });
1258
- test("search with non-existent AKM_STASH_DIR throws clear error", async () => {
1259
- const orig = process.env.AKM_STASH_DIR;
1260
- process.env.AKM_STASH_DIR = "/nonexistent/path";
1261
- try {
1262
- await expect(akmSearch({ query: "test" })).rejects.toThrow(/Unable to read/);
1263
- }
1264
- finally {
1265
- if (orig === undefined)
1266
- delete process.env.AKM_STASH_DIR;
1267
- else
1268
- process.env.AKM_STASH_DIR = orig;
1269
- }
1270
- });
1271
- test("search with unset AKM_STASH_DIR throws clear error", async () => {
1272
- const orig = process.env.AKM_STASH_DIR;
1273
- const origHome = process.env.HOME;
1274
- delete process.env.AKM_STASH_DIR;
1275
- // Point HOME somewhere without an akm directory to force the "no stash" error
1276
- const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-nohome-"));
1277
- process.env.HOME = tmpHome;
1278
- try {
1279
- await expect(akmSearch({ query: "test" })).rejects.toThrow(/No stash directory found/);
1280
- }
1281
- finally {
1282
- if (orig === undefined)
1283
- delete process.env.AKM_STASH_DIR;
1284
- else
1285
- process.env.AKM_STASH_DIR = orig;
1286
- if (origHome === undefined)
1287
- delete process.env.HOME;
1288
- else
1289
- process.env.HOME = origHome;
1290
- fs.rmSync(tmpHome, { recursive: true, force: true });
1291
- }
1292
- });
1293
- test("show with invalid ref format throws", async () => {
1294
- const stashDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-err-"));
1295
- process.env.AKM_STASH_DIR = stashDir;
1296
- try {
1297
- await expect(akmShow({ ref: "badref" })).rejects.toThrow(/Invalid ref/);
1298
- }
1299
- finally {
1300
- fs.rmSync(stashDir, { recursive: true, force: true });
1301
- }
1302
- });
1303
- test("show with unknown type throws", async () => {
1304
- const stashDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-err-"));
1305
- process.env.AKM_STASH_DIR = stashDir;
1306
- try {
1307
- await expect(akmShow({ ref: "widget:foo" })).rejects.toThrow(/Invalid asset type/);
1308
- }
1309
- finally {
1310
- fs.rmSync(stashDir, { recursive: true, force: true });
1311
- }
1312
- });
1313
- test("show with path traversal attempt throws", async () => {
1314
- const stashDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-err-"));
1315
- fs.mkdirSync(path.join(stashDir, "scripts"), { recursive: true });
1316
- process.env.AKM_STASH_DIR = stashDir;
1317
- try {
1318
- await expect(akmShow({ ref: "script:../../etc/passwd" })).rejects.toThrow(/Path traversal/);
1319
- }
1320
- finally {
1321
- fs.rmSync(stashDir, { recursive: true, force: true });
1322
- }
1323
- });
1324
- test("search on empty stash returns no hits with tip", async () => {
1325
- const stashDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-empty-"));
1326
- for (const sub of ["scripts", "skills", "commands", "agents"]) {
1327
- fs.mkdirSync(path.join(stashDir, sub), { recursive: true });
1328
- }
1329
- process.env.AKM_STASH_DIR = stashDir;
1330
- try {
1331
- const result = await akmSearch({ query: "anything" });
1332
- expect(result.hits).toHaveLength(0);
1333
- expect(result.tip).toBeTruthy();
1334
- }
1335
- finally {
1336
- fs.rmSync(stashDir, { recursive: true, force: true });
1337
- }
1338
- });
1339
- test("index on empty stash succeeds with zero entries", async () => {
1340
- const stashDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-empty-"));
1341
- for (const sub of ["scripts", "skills", "commands", "agents"]) {
1342
- fs.mkdirSync(path.join(stashDir, sub), { recursive: true });
1343
- }
1344
- try {
1345
- const result = await akmIndex({ stashDir });
1346
- expect(result.totalEntries).toBe(0);
1347
- expect(result.generatedMetadata).toBe(0);
1348
- }
1349
- finally {
1350
- fs.rmSync(stashDir, { recursive: true, force: true });
1351
- }
1352
- });
1353
- });
1354
- // ═══════════════════════════════════════════════════════════════════════════
1355
- // Scenario 8: Mixed asset type discovery
1356
- // ═══════════════════════════════════════════════════════════════════════════
1357
- describe("Scenario: Cross-type discovery", () => {
1358
- let stashDir;
1359
- let scenarioCacheDir;
1360
- beforeAll(async () => {
1361
- stashDir = copyFixturesToTmp();
1362
- scenarioCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-e2e-cache-s8-"));
1363
- process.env.AKM_STASH_DIR = stashDir;
1364
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
1365
- await akmIndex({ stashDir });
1366
- });
1367
- beforeEach(() => {
1368
- process.env.AKM_STASH_DIR = stashDir;
1369
- process.env.XDG_CACHE_HOME = scenarioCacheDir;
1370
- });
1371
- afterAll(() => {
1372
- fs.rmSync(stashDir, { recursive: true, force: true });
1373
- fs.rmSync(scenarioCacheDir, { recursive: true, force: true });
1374
- });
1375
- test("search 'any' type returns mixed results across scripts, skills, commands, agents", async () => {
1376
- const result = await akmSearch({ query: "", type: "any" });
1377
- const types = new Set(result.hits.map((h) => h.type));
1378
- // Should have at least scripts and one other type
1379
- expect(types.has("script")).toBe(true);
1380
- expect(types.size).toBeGreaterThan(1);
1381
- });
1382
- test("each hit has a valid ref that can be used with show", async () => {
1383
- const result = await akmSearch({ query: "", type: "any", limit: 10 });
1384
- for (const hit of result.hits.filter(isLocalHit)) {
1385
- expect(hit.ref).toBeTruthy();
1386
- expect(hit.ref).toContain(":");
1387
- // Should not throw when opening
1388
- const openResult = await akmShow({ ref: expectDefined(hit.ref) });
1389
- expect(openResult.type).toBe(hit.type);
1390
- }
1391
- });
1392
- test("script hits have run", async () => {
1393
- const result = await akmSearch({ query: "", type: "script" });
1394
- for (const hit of result.hits) {
1395
- expect(hit.type).toBe("script");
1396
- }
1397
- });
1398
- });