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,271 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { spawnSync } from "node:child_process";
3
- import fs from "node:fs";
4
- import os from "node:os";
5
- import path from "node:path";
6
- import { parseWorkflow } from "../src/workflows/parser";
7
- const CLI = path.join(__dirname, "..", "src", "cli.ts");
8
- const tempDirs = [];
9
- function makeTempDir(prefix) {
10
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
11
- tempDirs.push(dir);
12
- return dir;
13
- }
14
- function createWorkflowEnv() {
15
- const stashDir = makeTempDir("akm-workflow-stash-");
16
- const xdgCache = makeTempDir("akm-workflow-cache-");
17
- const xdgConfig = makeTempDir("akm-workflow-config-");
18
- return {
19
- ...process.env,
20
- AKM_STASH_DIR: stashDir,
21
- XDG_CACHE_HOME: xdgCache,
22
- XDG_CONFIG_HOME: xdgConfig,
23
- };
24
- }
25
- function writeConfig(env, config) {
26
- const configDir = path.join(String(env.XDG_CONFIG_HOME), "akm");
27
- fs.mkdirSync(configDir, { recursive: true });
28
- fs.writeFileSync(path.join(configDir, "config.json"), `${JSON.stringify(config, null, 2)}\n`, "utf8");
29
- }
30
- /**
31
- * Pull the JSON error envelope out of stderr. Stderr may contain
32
- * preceding `warn(...)` lines (e.g. the "Importing workflow content
33
- * from outside the stash" notice) before the (possibly multi-line) JSON
34
- * envelope. We slice from the last `{` at column 0 to the end and parse
35
- * that.
36
- */
37
- function parseLastJsonLine(stderr) {
38
- const lines = stderr.split("\n");
39
- let startIdx = -1;
40
- for (let i = lines.length - 1; i >= 0; i--) {
41
- if (lines[i].startsWith("{")) {
42
- startIdx = i;
43
- break;
44
- }
45
- }
46
- if (startIdx === -1)
47
- throw new Error(`stderr did not contain a JSON envelope: ${stderr}`);
48
- const tail = lines.slice(startIdx).join("\n").trim();
49
- return JSON.parse(tail);
50
- }
51
- function runCli(args, env) {
52
- return spawnSync("bun", [CLI, ...args], {
53
- encoding: "utf8",
54
- timeout: 30_000,
55
- env,
56
- });
57
- }
58
- afterEach(() => {
59
- for (const dir of tempDirs.splice(0)) {
60
- fs.rmSync(dir, { recursive: true, force: true });
61
- }
62
- });
63
- const RELEASE_WORKFLOW = `---
64
- description: Ship a release
65
- tags:
66
- - release
67
- params:
68
- version: Version being released
69
- ---
70
-
71
- # Workflow: Ship Release
72
-
73
- ## Step: Validate Release Inputs
74
- Step ID: validate
75
-
76
- ### Instructions
77
- Confirm release notes, tag, and version are present.
78
-
79
- ### Completion Criteria
80
- - Release notes reviewed
81
- - Version matches tag
82
-
83
- ## Step: Deploy Release
84
- Step ID: deploy
85
-
86
- ### Instructions
87
- Run the deployment command and watch health checks.
88
- `;
89
- describe("workflow CLI", () => {
90
- test("template prints a valid workflow document", () => {
91
- const env = createWorkflowEnv();
92
- const result = runCli(["workflow", "template"], env);
93
- expect(result.status).toBe(0);
94
- const parsed = parseWorkflow(result.stdout, { path: "<template>" });
95
- if (!parsed.ok) {
96
- throw new Error(`template did not parse: ${parsed.errors.map((e) => e.message).join("; ")}`);
97
- }
98
- expect(parsed.document.steps.length).toBeGreaterThan(0);
99
- });
100
- test("create writes a workflow and show returns structured step data", () => {
101
- const env = createWorkflowEnv();
102
- const result = runCli(["workflow", "create", "release-flow"], env);
103
- expect(result.status).toBe(0);
104
- const created = JSON.parse(result.stdout);
105
- expect(created.ref).toBe("workflow:release-flow");
106
- expect(fs.existsSync(created.path)).toBe(true);
107
- const shown = runCli(["show", "workflow:release-flow"], env);
108
- expect(shown.status).toBe(0);
109
- const json = JSON.parse(shown.stdout);
110
- expect(json.type).toBe("workflow");
111
- expect(json.workflowTitle).toBe("Release Flow");
112
- expect(json.steps[0]?.id).toBe("release-flow-setup");
113
- });
114
- test("create --from rejects invalid workflow documents", () => {
115
- const env = createWorkflowEnv();
116
- const sourceDir = makeTempDir("akm-workflow-source-");
117
- const sourcePath = path.join(sourceDir, "invalid.md");
118
- fs.writeFileSync(sourcePath, "# Workflow: Broken\n\n## Step: Missing Instructions\nStep ID: broken\n", "utf8");
119
- const result = runCli(["workflow", "create", "broken", "--from", sourcePath], env);
120
- expect(result.status).toBe(2);
121
- const error = parseLastJsonLine(result.stderr);
122
- expect(error.error).toContain('"### Instructions" section');
123
- });
124
- test("create --from rejects duplicate step ids", () => {
125
- const env = createWorkflowEnv();
126
- const sourceDir = makeTempDir("akm-workflow-source-");
127
- const sourcePath = path.join(sourceDir, "duplicate.md");
128
- fs.writeFileSync(sourcePath, RELEASE_WORKFLOW.replace("Step ID: deploy", "Step ID: validate"), "utf8");
129
- const result = runCli(["workflow", "create", "duplicate", "--from", sourcePath], env);
130
- expect(result.status).toBe(2);
131
- const error = parseLastJsonLine(result.stderr);
132
- expect(error.error).toContain('"validate"');
133
- expect(error.error).toContain("already used");
134
- });
135
- test("start, next, complete, list, and status manage persisted workflow runs", () => {
136
- const env = createWorkflowEnv();
137
- const sourceDir = makeTempDir("akm-workflow-source-");
138
- const sourcePath = path.join(sourceDir, "release.md");
139
- fs.writeFileSync(sourcePath, RELEASE_WORKFLOW, "utf8");
140
- expect(runCli(["workflow", "create", "release", "--from", sourcePath], env).status).toBe(0);
141
- const started = runCli(["workflow", "start", "workflow:release", "--params", '{"version":"1.2.3"}'], env);
142
- expect(started.status).toBe(0);
143
- const startJson = JSON.parse(started.stdout);
144
- expect(startJson.run.currentStepId).toBe("validate");
145
- expect(startJson.run.params.version).toBe("1.2.3");
146
- const next = runCli(["workflow", "next", startJson.run.id], env);
147
- expect(next.status).toBe(0);
148
- const nextJson = JSON.parse(next.stdout);
149
- expect(nextJson.step.id).toBe("validate");
150
- expect(nextJson.step.completionCriteria).toEqual(["Release notes reviewed", "Version matches tag"]);
151
- const completed = runCli([
152
- "workflow",
153
- "complete",
154
- startJson.run.id,
155
- "--step",
156
- "validate",
157
- "--notes",
158
- "Inputs verified",
159
- "--evidence",
160
- '{"checkedBy":"copilot"}',
161
- ], env);
162
- expect(completed.status).toBe(0);
163
- const completedJson = JSON.parse(completed.stdout);
164
- expect(completedJson.run.currentStepId).toBe("deploy");
165
- expect(completedJson.workflow.steps).toHaveLength(2);
166
- const status = runCli(["workflow", "status", startJson.run.id], env);
167
- expect(status.status).toBe(0);
168
- const statusJson = JSON.parse(status.stdout);
169
- expect(statusJson.run.status).toBe("active");
170
- expect(statusJson.run.currentStepId).toBe("deploy");
171
- expect(statusJson.workflow.steps[0]).toMatchObject({
172
- id: "validate",
173
- status: "completed",
174
- notes: "Inputs verified",
175
- evidence: { checkedBy: "copilot" },
176
- });
177
- const listed = runCli(["workflow", "list", "--ref", "workflow:release", "--active"], env);
178
- expect(listed.status).toBe(0);
179
- const listJson = JSON.parse(listed.stdout);
180
- expect(listJson.runs).toHaveLength(1);
181
- expect(listJson.runs[0]?.workflowRef).toBe("workflow:release");
182
- expect(runCli(["workflow", "complete", startJson.run.id, "--step", "deploy"], env).status).toBe(0);
183
- const afterComplete = runCli(["workflow", "status", startJson.run.id], env);
184
- const finalStatus = JSON.parse(afterComplete.stdout);
185
- expect(finalStatus.run.status).toBe("completed");
186
- expect(finalStatus.run.currentStepId ?? null).toBeNull();
187
- });
188
- test("next auto-starts a workflow and run state survives full index rebuilds", () => {
189
- const env = createWorkflowEnv();
190
- const sourceDir = makeTempDir("akm-workflow-source-");
191
- const sourcePath = path.join(sourceDir, "release.md");
192
- fs.writeFileSync(sourcePath, RELEASE_WORKFLOW, "utf8");
193
- expect(runCli(["workflow", "create", "release", "--from", sourcePath], env).status).toBe(0);
194
- const indexed = runCli(["index", "--full"], env);
195
- expect(indexed.status).toBe(0);
196
- const search = runCli(["search", "health checks", "--type", "workflow", "--detail", "full"], env);
197
- expect(search.status).toBe(0);
198
- const searchJson = JSON.parse(search.stdout);
199
- expect(searchJson.hits[0]?.ref).toBe("workflow:release");
200
- expect(searchJson.hits[0]?.action).toContain("akm workflow next 'workflow:release'");
201
- const next = runCli(["workflow", "next", "workflow:release"], env);
202
- expect(next.status).toBe(0);
203
- const nextJson = JSON.parse(next.stdout);
204
- expect(nextJson.run.status).toBe("active");
205
- expect(nextJson.step.id).toBe("validate");
206
- const rebuilt = runCli(["index", "--full"], env);
207
- expect(rebuilt.status).toBe(0);
208
- const status = runCli(["workflow", "status", nextJson.run.id], env);
209
- expect(status.status).toBe(0);
210
- const statusJson = JSON.parse(status.stdout);
211
- expect(statusJson.run.id).toBe(nextJson.run.id);
212
- expect(statusJson.run.status).toBe("active");
213
- expect(statusJson.run.currentStepId).toBe("validate");
214
- });
215
- test("complete rejects non-current and finalized step updates", () => {
216
- const env = createWorkflowEnv();
217
- const sourceDir = makeTempDir("akm-workflow-source-");
218
- const sourcePath = path.join(sourceDir, "release.md");
219
- fs.writeFileSync(sourcePath, RELEASE_WORKFLOW, "utf8");
220
- expect(runCli(["workflow", "create", "release", "--from", sourcePath], env).status).toBe(0);
221
- const started = runCli(["workflow", "start", "workflow:release"], env);
222
- expect(started.status).toBe(0);
223
- const startJson = JSON.parse(started.stdout);
224
- const wrongStep = runCli(["workflow", "complete", startJson.run.id, "--step", "deploy"], env);
225
- expect(wrongStep.status).toBe(2);
226
- expect(JSON.parse(wrongStep.stderr).error).toContain("is not the current step");
227
- expect(runCli(["workflow", "complete", startJson.run.id, "--step", "validate"], env).status).toBe(0);
228
- const repeated = runCli(["workflow", "complete", startJson.run.id, "--step", "validate"], env);
229
- expect(repeated.status).toBe(2);
230
- expect(JSON.parse(repeated.stderr).error).toContain("already completed");
231
- expect(runCli(["workflow", "complete", startJson.run.id, "--step", "deploy", "--state", "blocked"], env).status).toBe(0);
232
- const blockedRun = runCli(["workflow", "complete", startJson.run.id, "--step", "deploy"], env);
233
- expect(blockedRun.status).toBe(2);
234
- expect(JSON.parse(blockedRun.stderr).error).toContain("is blocked and cannot be updated");
235
- });
236
- test("next on a blocked run starts a new run for workflow refs", () => {
237
- const env = createWorkflowEnv();
238
- const sourceDir = makeTempDir("akm-workflow-source-");
239
- const sourcePath = path.join(sourceDir, "release.md");
240
- fs.writeFileSync(sourcePath, RELEASE_WORKFLOW, "utf8");
241
- expect(runCli(["workflow", "create", "release", "--from", sourcePath], env).status).toBe(0);
242
- const started = runCli(["workflow", "start", "workflow:release"], env);
243
- expect(started.status).toBe(0);
244
- const startJson = JSON.parse(started.stdout);
245
- expect(runCli(["workflow", "complete", startJson.run.id, "--step", "validate"], env).status).toBe(0);
246
- expect(runCli(["workflow", "complete", startJson.run.id, "--step", "deploy", "--state", "blocked"], env).status).toBe(0);
247
- const next = runCli(["workflow", "next", "workflow:release"], env);
248
- expect(next.status).toBe(0);
249
- const nextJson = JSON.parse(next.stdout);
250
- expect(nextJson.run.id).not.toBe(startJson.run.id);
251
- expect(nextJson.run.status).toBe("active");
252
- expect(nextJson.step.id).toBe("validate");
253
- });
254
- test("start links workflow_entry_id for workflows from an additional stash source", () => {
255
- const env = createWorkflowEnv();
256
- const extraStash = makeTempDir("akm-workflow-extra-stash-");
257
- const workflowPath = path.join(extraStash, "workflows", "shared-release.md");
258
- fs.mkdirSync(path.dirname(workflowPath), { recursive: true });
259
- fs.writeFileSync(workflowPath, RELEASE_WORKFLOW, "utf8");
260
- writeConfig(env, {
261
- semanticSearchMode: "off",
262
- sources: [{ type: "filesystem", path: extraStash, name: "extra" }],
263
- });
264
- expect(runCli(["index", "--full"], env).status).toBe(0);
265
- const started = runCli(["workflow", "start", "extra//workflow:shared-release"], env);
266
- expect(started.status).toBe(0);
267
- const startJson = JSON.parse(started.stdout);
268
- expect(startJson.run.workflowRef).toBe("extra//workflow:shared-release");
269
- expect(typeof startJson.run.workflowEntryId).toBe("number");
270
- });
271
- });
@@ -1,171 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { parseWorkflow } from "../src/workflows/parser";
3
- const VALID_WORKFLOW = `---
4
- description: Ship a release with validation checks
5
- tags:
6
- - release
7
- - deploy
8
- params:
9
- version: Version being released
10
- ---
11
-
12
- # Workflow: Ship Release
13
-
14
- ## Step: Validate Release Inputs
15
- Step ID: validate
16
-
17
- ### Instructions
18
- Confirm release notes, tag, and version are present.
19
-
20
- ### Completion Criteria
21
- - Release notes reviewed
22
- - Version matches tag
23
-
24
- ## Step: Deploy Release
25
- Step ID: deploy
26
-
27
- ### Instructions
28
- Run the deployment command and watch health checks.
29
- `;
30
- function parse(markdown, path = "workflows/test.md") {
31
- return parseWorkflow(markdown, { path });
32
- }
33
- function expectOk(result) {
34
- if (!result.ok) {
35
- throw new Error(`Expected ok parse, got errors: ${result.errors.map((e) => `${e.line}: ${e.message}`).join("; ")}`);
36
- }
37
- }
38
- describe("parseWorkflow", () => {
39
- test("parses a valid workflow document into structured steps", () => {
40
- const result = parse(VALID_WORKFLOW);
41
- expectOk(result);
42
- const doc = result.document;
43
- expect(doc.title).toBe("Ship Release");
44
- expect(doc.description).toBe("Ship a release with validation checks");
45
- expect(doc.tags).toEqual(["release", "deploy"]);
46
- expect(doc.parameters?.map((p) => ({ name: p.name, description: p.description }))).toEqual([
47
- { name: "version", description: "Version being released" },
48
- ]);
49
- expect(doc.steps).toHaveLength(2);
50
- expect(doc.steps[0].id).toBe("validate");
51
- expect(doc.steps[0].title).toBe("Validate Release Inputs");
52
- expect(doc.steps[0].instructions.text).toBe("Confirm release notes, tag, and version are present.");
53
- expect(doc.steps[0].completionCriteria?.map((c) => c.text)).toEqual([
54
- "Release notes reviewed",
55
- "Version matches tag",
56
- ]);
57
- expect(doc.steps[0].sequenceIndex).toBe(0);
58
- expect(doc.steps[1].completionCriteria).toBeUndefined();
59
- });
60
- test("attaches accurate SourceRef line spans to steps and instructions", () => {
61
- const result = parse(VALID_WORKFLOW);
62
- expectOk(result);
63
- const [first, second] = result.document.steps;
64
- // VALID_WORKFLOW: frontmatter ends at line 8, "# Workflow: Ship Release" at line 10,
65
- // first "## Step:" at line 12, second "## Step:" at line 22.
66
- expect(first.source.path).toBe("workflows/test.md");
67
- expect(first.source.start).toBe(12);
68
- expect(first.instructions.source.start).toBeGreaterThanOrEqual(first.source.start);
69
- expect(first.instructions.source.end).toBeLessThan(second.source.start);
70
- expect(first.completionCriteria?.[0].source.start).toBeGreaterThan(first.instructions.source.end);
71
- expect(second.source.start).toBe(22);
72
- });
73
- test("rejects missing workflow title", () => {
74
- const result = parse(VALID_WORKFLOW.replace("# Workflow: Ship Release\n\n", ""));
75
- expect(result.ok).toBe(false);
76
- if (result.ok)
77
- return;
78
- expect(result.errors[0].message).toContain('"# Workflow: <title>"');
79
- });
80
- test("rejects duplicate step ids", () => {
81
- const result = parse(VALID_WORKFLOW.replace("Step ID: deploy", "Step ID: validate"));
82
- expect(result.ok).toBe(false);
83
- if (result.ok)
84
- return;
85
- expect(result.errors.some((e) => e.message.includes('"validate"') && e.message.includes("already used"))).toBe(true);
86
- });
87
- test("rejects missing instructions sections", () => {
88
- const invalid = VALID_WORKFLOW.replace("### Instructions\nRun the deployment command and watch health checks.\n", "");
89
- const result = parse(invalid);
90
- expect(result.ok).toBe(false);
91
- if (result.ok)
92
- return;
93
- expect(result.errors.some((e) => e.message.includes("Instructions"))).toBe(true);
94
- });
95
- test("rejects unknown step subsections", () => {
96
- const invalid = VALID_WORKFLOW.replace("### Completion Criteria\n- Release notes reviewed\n- Version matches tag\n", "### Notes\nDo something else\n");
97
- const result = parse(invalid);
98
- expect(result.ok).toBe(false);
99
- if (result.ok)
100
- return;
101
- expect(result.errors.some((e) => e.message.includes("Notes"))).toBe(true);
102
- });
103
- test("rejects unsupported workflow frontmatter keys", () => {
104
- const invalid = VALID_WORKFLOW.replace("---\n", "---\nmodel: gpt-5\n");
105
- const result = parse(invalid);
106
- expect(result.ok).toBe(false);
107
- if (result.ok)
108
- return;
109
- expect(result.errors.some((e) => e.message.includes("model"))).toBe(true);
110
- });
111
- test("collects every error in one pass instead of stopping at the first", () => {
112
- const broken = `# Workflow: Multi
113
-
114
- ## Step: One
115
- Step ID: A B
116
- ### Instructions
117
- do A
118
-
119
- ## Step: Two
120
- Step ID: A B
121
- ### Instructions
122
- do B
123
- `;
124
- const result = parse(broken);
125
- expect(result.ok).toBe(false);
126
- if (result.ok)
127
- return;
128
- // Both invalid step IDs should be reported, not just the first.
129
- const idErrors = result.errors.filter((e) => e.message.includes("Step ID"));
130
- expect(idErrors.length).toBeGreaterThanOrEqual(2);
131
- });
132
- });
133
- describe("parseWorkflow — intro paragraph (issue #158)", () => {
134
- const WORKFLOW_WITH_INTRO = `# Workflow: Example
135
-
136
- This workflow is advisory and should only prepare commands.
137
-
138
- ## Step: First Step
139
- Step ID: first-step
140
-
141
- ### Instructions
142
- Do the thing.
143
- `;
144
- test("parses cleanly when intro paragraph precedes first step", () => {
145
- const result = parse(WORKFLOW_WITH_INTRO);
146
- expectOk(result);
147
- expect(result.document.title).toBe("Example");
148
- expect(result.document.steps).toHaveLength(1);
149
- expect(result.document.steps[0].id).toBe("first-step");
150
- expect(result.document.steps[0].title).toBe("First Step");
151
- });
152
- test("existing valid workflows without intro paragraph parse identically", () => {
153
- const result = parse(VALID_WORKFLOW);
154
- expectOk(result);
155
- expect(result.document.title).toBe("Ship Release");
156
- expect(result.document.steps).toHaveLength(2);
157
- expect(result.document.steps[0].id).toBe("validate");
158
- expect(result.document.steps[1].id).toBe("deploy");
159
- });
160
- test("rejects workflow with intro paragraph but no steps", () => {
161
- const noSteps = `# Workflow: No Steps
162
-
163
- This workflow has an intro but no steps at all.
164
- `;
165
- const result = parse(noSteps);
166
- expect(result.ok).toBe(false);
167
- if (result.ok)
168
- return;
169
- expect(result.errors.some((e) => e.message.includes("Step"))).toBe(true);
170
- });
171
- });
@@ -1,132 +0,0 @@
1
- /**
2
- * Regression tests for issue #157:
3
- * `akm workflow create <name>` failing with "Resolved workflow path escapes the
4
- * stash" for valid bare names on systems with symlinks in the path hierarchy.
5
- *
6
- * Root cause: `safeRealpath` resolved existing directories through symlinks
7
- * (via `fs.realpathSync`) but fell back to the raw `path.resolve` for
8
- * non-existent paths. When the directory tree contains a symlink (e.g.
9
- * macOS /tmp → /private/tmp, or a HOME that is itself a symlink), the two
10
- * resolved paths could disagree, causing `isWithin` to return false.
11
- *
12
- * Fix: walk up to the nearest existing ancestor, resolve that ancestor via
13
- * `realpathSync`, then reconstruct the full path.
14
- */
15
- import { afterEach, describe, expect, test } from "bun:test";
16
- import fs from "node:fs";
17
- import os from "node:os";
18
- import path from "node:path";
19
- import { createWorkflowAsset } from "../src/workflows/authoring";
20
- const tempDirs = [];
21
- function makeTempDir(prefix) {
22
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
23
- tempDirs.push(dir);
24
- return dir;
25
- }
26
- afterEach(() => {
27
- for (const dir of tempDirs.splice(0)) {
28
- fs.rmSync(dir, { recursive: true, force: true });
29
- }
30
- delete process.env.AKM_STASH_DIR;
31
- delete process.env.XDG_CONFIG_HOME;
32
- delete process.env.XDG_CACHE_HOME;
33
- });
34
- // ── Happy path: clean stash ─────────────────────────────────────────────────
35
- describe("createWorkflowAsset — clean stash (issue #157)", () => {
36
- test("bare name resolves correctly in a freshly created stash", () => {
37
- const stashDir = makeTempDir("akm-issue157-stash-");
38
- const xdgCache = makeTempDir("akm-issue157-cache-");
39
- const xdgConfig = makeTempDir("akm-issue157-config-");
40
- process.env.AKM_STASH_DIR = stashDir;
41
- process.env.XDG_CACHE_HOME = xdgCache;
42
- process.env.XDG_CONFIG_HOME = xdgConfig;
43
- const result = createWorkflowAsset({ name: "agentic-test-workflow" });
44
- expect(result.ref).toBe("workflow:agentic-test-workflow");
45
- expect(fs.existsSync(result.path)).toBe(true);
46
- expect(result.path).toBe(path.join(stashDir, "workflows", "agentic-test-workflow.md"));
47
- });
48
- test("bare name with hyphens resolves correctly", () => {
49
- const stashDir = makeTempDir("akm-issue157-stash-");
50
- process.env.AKM_STASH_DIR = stashDir;
51
- const result = createWorkflowAsset({ name: "my-multi-step-workflow" });
52
- expect(result.ref).toBe("workflow:my-multi-step-workflow");
53
- expect(fs.existsSync(result.path)).toBe(true);
54
- });
55
- test("nested name (subdirectory) resolves correctly", () => {
56
- const stashDir = makeTempDir("akm-issue157-stash-");
57
- process.env.AKM_STASH_DIR = stashDir;
58
- const result = createWorkflowAsset({ name: "team/release-flow" });
59
- expect(result.ref).toBe("workflow:team/release-flow");
60
- expect(fs.existsSync(result.path)).toBe(true);
61
- expect(result.path).toContain(path.join("workflows", "team", "release-flow.md"));
62
- });
63
- test("resolves correctly when stash dir path contains a symlink", () => {
64
- // Create a real directory and a symlink pointing to it, then use the
65
- // symlink path as the stash dir. This simulates environments where HOME
66
- // or a parent directory is a symlink (e.g. macOS /tmp → /private/tmp).
67
- const realDir = makeTempDir("akm-issue157-real-");
68
- const symlinkDir = path.join(os.tmpdir(), `akm-issue157-link-${Date.now()}`);
69
- tempDirs.push(symlinkDir); // cleaned up by afterEach (rm -rf is ok for dead links)
70
- fs.symlinkSync(realDir, symlinkDir);
71
- process.env.AKM_STASH_DIR = symlinkDir;
72
- // Must not throw "Resolved workflow path escapes the stash"
73
- const result = createWorkflowAsset({ name: "agentic-test-workflow" });
74
- expect(result.ref).toBe("workflow:agentic-test-workflow");
75
- expect(fs.existsSync(result.path)).toBe(true);
76
- });
77
- test("--from succeeds with valid workflow markdown", () => {
78
- const stashDir = makeTempDir("akm-issue157-stash-");
79
- const srcDir = makeTempDir("akm-issue157-src-");
80
- process.env.AKM_STASH_DIR = stashDir;
81
- const srcPath = path.join(srcDir, "release.md");
82
- const content = `---
83
- description: A release workflow
84
- tags:
85
- - release
86
- ---
87
-
88
- # Workflow: Release
89
-
90
- ## Step: Validate
91
- Step ID: validate
92
-
93
- ### Instructions
94
- Check all inputs.
95
-
96
- ### Completion Criteria
97
- - Inputs confirmed
98
- `;
99
- fs.writeFileSync(srcPath, content, "utf8");
100
- const result = createWorkflowAsset({ name: "release", from: srcPath });
101
- expect(result.ref).toBe("workflow:release");
102
- expect(fs.existsSync(result.path)).toBe(true);
103
- expect(fs.readFileSync(result.path, "utf8")).toContain("# Workflow: Release");
104
- });
105
- });
106
- // ── Security: path traversal must still be rejected ─────────────────────────
107
- describe("createWorkflowAsset — path escape rejection", () => {
108
- test("../traversal is rejected", () => {
109
- const stashDir = makeTempDir("akm-issue157-stash-");
110
- process.env.AKM_STASH_DIR = stashDir;
111
- expect(() => createWorkflowAsset({ name: "../outside" })).toThrow("must be a relative path without");
112
- });
113
- test("deep traversal is rejected", () => {
114
- const stashDir = makeTempDir("akm-issue157-stash-");
115
- process.env.AKM_STASH_DIR = stashDir;
116
- expect(() => createWorkflowAsset({ name: "a/../../outside" })).toThrow("must be a relative path without");
117
- });
118
- test("absolute path is sanitized into a relative name inside the stash", () => {
119
- // normalizeWorkflowName strips leading slashes, so "/etc/passwd" becomes
120
- // "etc/passwd" — a relative name that resolves safely inside the stash.
121
- // This is by design: the function converts absolute-looking user input
122
- // into a relative name rather than treating it as a filesystem path.
123
- const stashDir = makeTempDir("akm-issue157-stash-");
124
- process.env.AKM_STASH_DIR = stashDir;
125
- const result = createWorkflowAsset({ name: "/etc/passwd" });
126
- // Leading slash is stripped → name becomes "etc/passwd"
127
- expect(result.ref).toBe("workflow:etc/passwd");
128
- // The resulting file is inside the stash workflows dir, not at /etc/passwd
129
- expect(result.path.startsWith(stashDir)).toBe(true);
130
- expect(result.path).toContain(path.join("workflows", "etc", "passwd.md"));
131
- });
132
- });