mandrel 1.59.0 → 1.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/.agents/README.md +86 -44
  2. package/.agents/docs/SDLC.md +135 -141
  3. package/.agents/docs/configuration.md +77 -20
  4. package/.agents/docs/quality-gates.md +796 -0
  5. package/.agents/docs/workflows.md +6 -9
  6. package/.agents/instructions.md +12 -11
  7. package/.agents/personas/architect.md +1 -1
  8. package/.agents/personas/product.md +1 -1
  9. package/.agents/personas/project-manager.md +14 -14
  10. package/.agents/personas/technical-writer.md +1 -1
  11. package/.agents/rules/changelog-style.md +5 -5
  12. package/.agents/rules/git-conventions.md +3 -3
  13. package/.agents/runtime-deps.json +2 -2
  14. package/.agents/schemas/agentrc.schema.json +3 -3
  15. package/.agents/schemas/dispatch-manifest.json +4 -4
  16. package/.agents/schemas/epic-spec.schema.json +15 -45
  17. package/.agents/schemas/lifecycle/README.md +1 -1
  18. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +1 -1
  19. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +1 -1
  20. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +1 -1
  21. package/.agents/schemas/validation-evidence.schema.json +1 -1
  22. package/.agents/scripts/README.md +2 -2
  23. package/.agents/scripts/acceptance-eval.js +1 -1
  24. package/.agents/scripts/acceptance-spec-reconciler.js +2 -2
  25. package/.agents/scripts/agents-bootstrap-github.js +23 -119
  26. package/.agents/scripts/analyze-execution.js +2 -2
  27. package/.agents/scripts/audit-to-stories.js +1 -1
  28. package/.agents/scripts/check-doc-links.js +2 -3
  29. package/.agents/scripts/diagnose-friction.js +1 -1
  30. package/.agents/scripts/dispatcher.js +2 -2
  31. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  32. package/.agents/scripts/epic-audit-prepare.js +3 -3
  33. package/.agents/scripts/epic-deliver-note-intervention.js +2 -2
  34. package/.agents/scripts/epic-deliver-preflight.js +6 -6
  35. package/.agents/scripts/epic-deliver-prepare.js +1 -1
  36. package/.agents/scripts/epic-execute-record-wave.js +4 -4
  37. package/.agents/scripts/epic-plan-healthcheck.js +6 -10
  38. package/.agents/scripts/epic-plan-spec-validate.js +1 -1
  39. package/.agents/scripts/epic-reconcile.js +11 -29
  40. package/.agents/scripts/evidence-gate.js +1 -1
  41. package/.agents/scripts/generate-workflows-doc.js +1 -1
  42. package/.agents/scripts/hierarchy-gate.js +7 -11
  43. package/.agents/scripts/lib/ITicketingProvider.js +1 -1
  44. package/.agents/scripts/lib/audit-suite/selector.js +1 -1
  45. package/.agents/scripts/lib/audit-to-stories/seed-epic-from-findings.js +2 -2
  46. package/.agents/scripts/lib/baseline-snapshot.js +7 -7
  47. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  48. package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -3
  49. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +1 -1
  50. package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
  51. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +47 -1
  52. package/.agents/scripts/lib/bootstrap/commit-push.js +2 -2
  53. package/.agents/scripts/lib/bootstrap/gh-preflight.js +7 -9
  54. package/.agents/scripts/lib/bootstrap/manifest.js +21 -1
  55. package/.agents/scripts/lib/bootstrap/merge-methods.js +31 -16
  56. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +32 -11
  57. package/.agents/scripts/lib/codebase-snapshot.js +1 -1
  58. package/.agents/scripts/lib/config/explain.js +1 -1
  59. package/.agents/scripts/lib/config/runners.js +2 -2
  60. package/.agents/scripts/lib/config/runtime.js +1 -1
  61. package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
  62. package/.agents/scripts/lib/config/temp-paths.js +2 -2
  63. package/.agents/scripts/lib/config-settings-schema-delivery.js +2 -2
  64. package/.agents/scripts/lib/config-settings-schema-quality.js +1 -1
  65. package/.agents/scripts/lib/config-settings-schema.js +3 -3
  66. package/.agents/scripts/lib/detect-package-manager.js +72 -0
  67. package/.agents/scripts/lib/duplicate-search.js +1 -1
  68. package/.agents/scripts/lib/dynamic-workflow/capability.js +1 -1
  69. package/.agents/scripts/lib/epic-plan-clarity.js +1 -1
  70. package/.agents/scripts/lib/epic-plan-ideation.js +1 -1
  71. package/.agents/scripts/lib/errors/index.js +4 -4
  72. package/.agents/scripts/lib/feedback-loop/memory-freshness.js +1 -1
  73. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +1 -1
  74. package/.agents/scripts/lib/findings/classify-finding.js +1 -1
  75. package/.agents/scripts/lib/findings/promote-finding.js +10 -10
  76. package/.agents/scripts/lib/label-constants.js +3 -4
  77. package/.agents/scripts/lib/label-taxonomy.js +5 -10
  78. package/.agents/scripts/lib/onboard/detect-stack.js +10 -10
  79. package/.agents/scripts/lib/onboard/init-tail.js +218 -0
  80. package/.agents/scripts/lib/onboard/scaffold-docs.js +18 -3
  81. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
  82. package/.agents/scripts/lib/orchestration/code-review.js +5 -5
  83. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +8 -9
  84. package/.agents/scripts/lib/orchestration/dependency-analyzer.js +3 -3
  85. package/.agents/scripts/lib/orchestration/detectors-phase.js +2 -2
  86. package/.agents/scripts/lib/orchestration/dispatch-engine.js +30 -38
  87. package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +9 -25
  88. package/.agents/scripts/lib/orchestration/epic-cleanup.js +1 -1
  89. package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +8 -8
  90. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +1 -1
  91. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/dag.js +7 -21
  92. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/diagnostics.js +3 -3
  93. package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +26 -13
  94. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +1 -1
  95. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +1 -1
  96. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +2 -2
  97. package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +1 -1
  98. package/.agents/scripts/lib/orchestration/epic-run-state-store.js +3 -3
  99. package/.agents/scripts/lib/orchestration/epic-runner/concurrency-gate.js +4 -4
  100. package/.agents/scripts/lib/orchestration/epic-runner/deliver-phases.js +3 -3
  101. package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +6 -21
  102. package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +7 -7
  103. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +1 -1
  104. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +2 -2
  105. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +4 -4
  106. package/.agents/scripts/lib/orchestration/epic-runner/story-launcher.js +4 -4
  107. package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +8 -8
  108. package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +4 -4
  109. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-apply.js +7 -15
  110. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +72 -41
  111. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js +2 -4
  112. package/.agents/scripts/lib/orchestration/file-assumptions.js +2 -2
  113. package/.agents/scripts/lib/orchestration/finalize/close-planning-tickets.js +1 -1
  114. package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +2 -2
  115. package/.agents/scripts/lib/orchestration/finalize/sanitize-skip-ci.js +1 -1
  116. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -3
  117. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-dispatch-end.js +1 -1
  118. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +1 -1
  119. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +1 -1
  120. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +1 -1
  121. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +1 -1
  122. package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +1 -1
  123. package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +1 -1
  124. package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +1 -1
  125. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +1 -1
  126. package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +1 -1
  127. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +1 -1
  128. package/.agents/scripts/lib/orchestration/manifest-builder.js +5 -5
  129. package/.agents/scripts/lib/orchestration/parked-follow-ons.js +2 -2
  130. package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +5 -5
  131. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +3 -3
  132. package/.agents/scripts/lib/orchestration/preflight-cache.js +1 -1
  133. package/.agents/scripts/lib/orchestration/recurring-failure-detector.js +1 -1
  134. package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +1 -1
  135. package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +2 -2
  136. package/.agents/scripts/lib/orchestration/retro-runner.js +3 -3
  137. package/.agents/scripts/lib/orchestration/review-depth.js +1 -1
  138. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +1 -1
  139. package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
  140. package/.agents/scripts/lib/orchestration/spec-renderer.js +36 -73
  141. package/.agents/scripts/lib/orchestration/spec-section-validator.js +1 -1
  142. package/.agents/scripts/lib/orchestration/story-close/baseline-friction-body.js +1 -1
  143. package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +2 -2
  144. package/.agents/scripts/lib/orchestration/task-body-validator.js +6 -6
  145. package/.agents/scripts/lib/orchestration/ticket-lease.js +1 -1
  146. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +2 -2
  147. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -10
  148. package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -70
  149. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +5 -12
  150. package/.agents/scripts/lib/orchestration/ticketing/reads.js +8 -8
  151. package/.agents/scripts/lib/orchestration/ticketing/state.js +3 -3
  152. package/.agents/scripts/lib/orchestration/wave-record-notifications.js +2 -2
  153. package/.agents/scripts/lib/orchestration/wave-record-projection.js +1 -1
  154. package/.agents/scripts/lib/plan-phase-cleanup.js +1 -1
  155. package/.agents/scripts/lib/preflight-runner.js +1 -1
  156. package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +4 -5
  157. package/.agents/scripts/lib/presentation/manifest-builder.js +28 -34
  158. package/.agents/scripts/lib/presentation/manifest-formatter.js +3 -4
  159. package/.agents/scripts/lib/presentation/manifest-helpers.js +1 -1
  160. package/.agents/scripts/lib/presentation/manifest-procedures.js +4 -4
  161. package/.agents/scripts/lib/presentation/manifest-render-waves.js +4 -23
  162. package/.agents/scripts/lib/presentation/manifest-renderer.js +1 -1
  163. package/.agents/scripts/lib/presentation/manifest-story-views.js +2 -11
  164. package/.agents/scripts/lib/runtime-deps/preflight.js +6 -6
  165. package/.agents/scripts/lib/signals/schema.js +1 -1
  166. package/.agents/scripts/lib/spec/index.js +1 -1
  167. package/.agents/scripts/lib/spec/loader.js +2 -2
  168. package/.agents/scripts/lib/spec/state.js +7 -16
  169. package/.agents/scripts/lib/story-init/context-resolver.js +3 -3
  170. package/.agents/scripts/lib/story-init/state-transitioner.js +2 -2
  171. package/.agents/scripts/lib/story-init/task-graph-builder.js +7 -7
  172. package/.agents/scripts/lib/story-lifecycle.js +8 -8
  173. package/.agents/scripts/lib/story-plan.js +1 -1
  174. package/.agents/scripts/lib/templates/decomposer-prompts.js +59 -52
  175. package/.agents/scripts/lib/wave-runner/tick.js +1 -1
  176. package/.agents/scripts/lib/worktree/node-modules-strategy.js +5 -2
  177. package/.agents/scripts/lifecycle-emit-story-dispatch.js +1 -1
  178. package/.agents/scripts/lifecycle-emit.js +1 -1
  179. package/.agents/scripts/providers/github/board-add.js +1 -1
  180. package/.agents/scripts/providers/github/errors.js +1 -1
  181. package/.agents/scripts/providers/github/mappers.js +2 -2
  182. package/.agents/scripts/providers/github/tickets.js +4 -4
  183. package/.agents/scripts/resync-status-column.js +1 -1
  184. package/.agents/scripts/retro-run.js +2 -2
  185. package/.agents/scripts/run-lint.js +1 -1
  186. package/.agents/scripts/single-story-init.js +1 -1
  187. package/.agents/scripts/stories-wave-tick.js +5 -5
  188. package/.agents/scripts/story-close.js +1 -1
  189. package/.agents/scripts/story-init.js +13 -16
  190. package/.agents/scripts/story-phase.js +5 -5
  191. package/.agents/scripts/story-plan.js +3 -3
  192. package/.agents/scripts/sync-branch-from-base.js +1 -1
  193. package/.agents/scripts/validate-docs-freshness.js +1 -1
  194. package/.agents/scripts/wave-tick.js +1 -1
  195. package/.agents/skills/core/analyze-execution/SKILL.md +2 -2
  196. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +21 -26
  197. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +23 -56
  198. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +4 -4
  199. package/.agents/skills/core/hydrate-context/SKILL.md +2 -2
  200. package/.agents/skills/core/idea-refinement/SKILL.md +4 -4
  201. package/.agents/skills/core/knowledge-transfer/SKILL.md +2 -2
  202. package/.agents/skills/core/planning-and-task-breakdown/SKILL.md +1 -1
  203. package/.agents/skills/core/scope-triage/SKILL.md +9 -10
  204. package/.agents/skills/core/using-agent-skills/SKILL.md +1 -1
  205. package/.agents/skills/skills.index.json +7 -7
  206. package/.agents/templates/agent-protocol.md +2 -2
  207. package/.agents/workflows/agents-update.md +16 -31
  208. package/.agents/workflows/audit-architecture.md +2 -2
  209. package/.agents/workflows/audit-clean-code.md +2 -2
  210. package/.agents/workflows/audit-dependencies.md +1 -1
  211. package/.agents/workflows/audit-devops.md +1 -1
  212. package/.agents/workflows/audit-documentation.md +2 -2
  213. package/.agents/workflows/audit-lighthouse.md +1 -1
  214. package/.agents/workflows/audit-performance.md +2 -2
  215. package/.agents/workflows/audit-privacy.md +1 -1
  216. package/.agents/workflows/audit-quality.md +2 -2
  217. package/.agents/workflows/audit-security.md +2 -2
  218. package/.agents/workflows/audit-seo.md +1 -1
  219. package/.agents/workflows/audit-sre.md +1 -1
  220. package/.agents/workflows/audit-to-stories.md +10 -10
  221. package/.agents/workflows/audit-ux-ui.md +1 -1
  222. package/.agents/workflows/deliver.md +85 -0
  223. package/.agents/workflows/explain.md +3 -3
  224. package/.agents/workflows/git-merge-pr.md +1 -1
  225. package/.agents/workflows/git-pr-all.md +13 -10
  226. package/.agents/workflows/git-push.md +6 -3
  227. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  228. package/.agents/workflows/helpers/acceptance-self-eval.md +1 -1
  229. package/.agents/workflows/helpers/agents-sync-config.md +3 -2
  230. package/.agents/workflows/helpers/code-review.md +5 -5
  231. package/.agents/workflows/{epic-deliver.md → helpers/deliver-epic.md} +43 -43
  232. package/.agents/workflows/{story-deliver.md → helpers/deliver-stories.md} +25 -25
  233. package/.agents/workflows/helpers/diagnose.md +1 -1
  234. package/.agents/workflows/helpers/epic-audit.md +6 -6
  235. package/.agents/workflows/helpers/epic-deliver-story.md +13 -13
  236. package/.agents/workflows/helpers/epic-plan-decompose.md +23 -23
  237. package/.agents/workflows/helpers/epic-plan-spec.md +6 -6
  238. package/.agents/workflows/helpers/epic-testing.md +3 -3
  239. package/.agents/workflows/helpers/parallel-tooling.md +1 -1
  240. package/.agents/workflows/{epic-plan.md → helpers/plan-epic.md} +84 -84
  241. package/.agents/workflows/{story-plan.md → helpers/plan-story.md} +43 -43
  242. package/.agents/workflows/helpers/signals.md +1 -1
  243. package/.agents/workflows/helpers/single-story-deliver.md +11 -11
  244. package/.agents/workflows/helpers/worktree-lifecycle.md +18 -18
  245. package/.agents/workflows/plan.md +131 -0
  246. package/.agents/workflows/qa-explore.md +1 -1
  247. package/.agents/workflows/qa-run-harness.md +1 -1
  248. package/README.md +19 -39
  249. package/bin/mandrel.js +235 -16
  250. package/docs/CHANGELOG.md +1173 -0
  251. package/lib/cli/doctor.js +45 -3
  252. package/lib/cli/init.js +97 -36
  253. package/lib/cli/registry.js +41 -145
  254. package/lib/cli/sync.js +122 -23
  255. package/lib/cli/uninstall.js +42 -7
  256. package/lib/cli/update.js +524 -210
  257. package/lib/cli/version-helpers.js +59 -0
  258. package/package.json +7 -6
  259. package/.agents/scripts/lib/orchestration/reconciler.js +0 -137
  260. package/.agents/workflows/onboard.md +0 -208
  261. package/lib/cli/__tests__/migrate.test.js +0 -268
  262. package/lib/cli/__tests__/sync-local-zone.test.js +0 -247
  263. package/lib/cli/__tests__/sync.test.js +0 -372
  264. package/lib/cli/__tests__/update-major.test.js +0 -217
  265. package/lib/cli/__tests__/update.test.js +0 -696
  266. package/lib/cli/__tests__/version-check.test.js +0 -398
  267. package/lib/migrations/__tests__/index.test.js +0 -216
@@ -1,398 +0,0 @@
1
- // lib/cli/__tests__/version-check.test.js
2
- /**
3
- * Unit tests for lib/cli/version-check.js — the daily-cached version freshness
4
- * check (Story #3500, Epic #3437).
5
- *
6
- * Every test drives the module through injectable seams (cachePath, now,
7
- * runner, fs, log) backed by an in-memory filesystem fake, so no real disk I/O
8
- * and no real network access occur (testing-standards § Unit: all filesystem
9
- * and network I/O MUST be mocked).
10
- *
11
- * Coverage contract (per Story #3500 AC):
12
- * 1. Module shape — exports isStale, readCache, refreshCache.
13
- * 2. isStale with a FRESH cache (< 24h) returns the cached version and
14
- * NEVER invokes the network runner seam.
15
- * 3. isStale with a STALE cache (> 24h) invokes the runner exactly once and
16
- * refreshes the cache.
17
- * 4. isStale with a MISSING cache invokes the runner and writes a new cache.
18
- * 5. refreshCache persists { latestVersion, checkedAt } JSON under the path
19
- * it is given, creating the temp-root directory.
20
- * 6. readCache returns null for missing / malformed / incomplete caches.
21
- * 7. Logging contract — the log seam receives only version strings and
22
- * paths, never tokens or raw file contents.
23
- */
24
-
25
- import assert from 'node:assert/strict';
26
- import path from 'node:path';
27
- import { describe, it } from 'node:test';
28
-
29
- import {
30
- DEFAULT_CACHE_FILENAME,
31
- isStale,
32
- readCache,
33
- refreshCache,
34
- STALE_AFTER_MS,
35
- } from '../version-check.js';
36
-
37
- // ---------------------------------------------------------------------------
38
- // In-memory filesystem fake
39
- // ---------------------------------------------------------------------------
40
-
41
- /**
42
- * Build an in-memory fs whose `seed` maps absolute file paths → string
43
- * contents. Tracks mkdir/write calls so tests can assert on persistence.
44
- */
45
- function makeFs(seed = {}) {
46
- const files = new Map(Object.entries(seed));
47
- const mkdirCalls = [];
48
- const writeCalls = [];
49
-
50
- return {
51
- files,
52
- mkdirCalls,
53
- writeCalls,
54
- readFileSync(p, _enc) {
55
- if (!files.has(p)) {
56
- const err = new Error(`ENOENT: no such file ${p}`);
57
- err.code = 'ENOENT';
58
- throw err;
59
- }
60
- return files.get(p);
61
- },
62
- writeFileSync(p, contents, _enc) {
63
- writeCalls.push({ path: p, contents });
64
- files.set(p, contents);
65
- },
66
- mkdirSync(dir, opts) {
67
- mkdirCalls.push({ dir, opts });
68
- },
69
- };
70
- }
71
-
72
- const CACHE_PATH = path.join('/tmp', 'mandrel', DEFAULT_CACHE_FILENAME);
73
-
74
- /** Capturing log seam. */
75
- function makeLog() {
76
- const lines = [];
77
- const log = (msg) => lines.push(msg);
78
- log.lines = lines;
79
- return log;
80
- }
81
-
82
- // ---------------------------------------------------------------------------
83
- // 1. Module shape
84
- // ---------------------------------------------------------------------------
85
-
86
- describe('version-check module shape', () => {
87
- it('exports isStale, readCache, and refreshCache functions', () => {
88
- assert.equal(typeof isStale, 'function');
89
- assert.equal(typeof readCache, 'function');
90
- assert.equal(typeof refreshCache, 'function');
91
- });
92
-
93
- it('exposes a 24h staleness window constant', () => {
94
- assert.equal(STALE_AFTER_MS, 24 * 60 * 60 * 1000);
95
- });
96
- });
97
-
98
- // ---------------------------------------------------------------------------
99
- // 2. isStale — fresh cache → NO network
100
- // ---------------------------------------------------------------------------
101
-
102
- describe('isStale with a fresh cache', () => {
103
- it('returns the cached version and never invokes the runner seam', async () => {
104
- const now = new Date('2026-06-03T12:00:00.000Z');
105
- // Checked 1 hour ago — well within the 24h window.
106
- const checkedAt = new Date(now.getTime() - 60 * 60 * 1000).toISOString();
107
- const fs = makeFs({
108
- [CACHE_PATH]: JSON.stringify({ latestVersion: '1.2.3', checkedAt }),
109
- });
110
-
111
- let runnerCalls = 0;
112
- const runner = () => {
113
- runnerCalls++;
114
- return '9.9.9';
115
- };
116
-
117
- const result = await isStale({ cachePath: CACHE_PATH, now, runner, fs });
118
-
119
- assert.equal(runnerCalls, 0, 'fresh cache must not hit the network');
120
- assert.equal(result.stale, false);
121
- assert.equal(result.refreshed, false);
122
- assert.equal(result.latestVersion, '1.2.3');
123
- assert.equal(result.checkedAt, checkedAt);
124
- // No new write happened — cache untouched.
125
- assert.equal(fs.writeCalls.length, 0);
126
- });
127
-
128
- it('treats a cache just under 24h old as fresh (boundary)', async () => {
129
- const now = new Date('2026-06-03T12:00:00.000Z');
130
- const checkedAt = new Date(
131
- now.getTime() - (STALE_AFTER_MS - 1000),
132
- ).toISOString();
133
- const fs = makeFs({
134
- [CACHE_PATH]: JSON.stringify({ latestVersion: '2.0.0', checkedAt }),
135
- });
136
-
137
- let runnerCalls = 0;
138
- const result = await isStale({
139
- cachePath: CACHE_PATH,
140
- now,
141
- runner: () => {
142
- runnerCalls++;
143
- return '3.0.0';
144
- },
145
- fs,
146
- });
147
-
148
- assert.equal(runnerCalls, 0);
149
- assert.equal(result.stale, false);
150
- assert.equal(result.latestVersion, '2.0.0');
151
- });
152
- });
153
-
154
- // ---------------------------------------------------------------------------
155
- // 3. isStale — stale cache → ONE network call + refresh
156
- // ---------------------------------------------------------------------------
157
-
158
- describe('isStale with a stale cache', () => {
159
- it('invokes the runner exactly once and refreshes the cache', async () => {
160
- const now = new Date('2026-06-03T12:00:00.000Z');
161
- // Checked 25 hours ago — past the 24h window.
162
- const checkedAt = new Date(
163
- now.getTime() - 25 * 60 * 60 * 1000,
164
- ).toISOString();
165
- const fs = makeFs({
166
- [CACHE_PATH]: JSON.stringify({ latestVersion: '1.0.0', checkedAt }),
167
- });
168
-
169
- let runnerCalls = 0;
170
- const runner = () => {
171
- runnerCalls++;
172
- return '1.5.0';
173
- };
174
-
175
- const result = await isStale({ cachePath: CACHE_PATH, now, runner, fs });
176
-
177
- assert.equal(
178
- runnerCalls,
179
- 1,
180
- 'stale cache must probe the network exactly once',
181
- );
182
- assert.equal(result.stale, true);
183
- assert.equal(result.refreshed, true);
184
- assert.equal(result.latestVersion, '1.5.0');
185
- assert.equal(result.checkedAt, now.toISOString());
186
-
187
- // The cache on disk now carries the refreshed record.
188
- const persisted = JSON.parse(fs.files.get(CACHE_PATH));
189
- assert.equal(persisted.latestVersion, '1.5.0');
190
- assert.equal(persisted.checkedAt, now.toISOString());
191
- });
192
-
193
- it('treats an exactly-24h-old cache as stale (boundary)', async () => {
194
- const now = new Date('2026-06-03T12:00:00.000Z');
195
- const checkedAt = new Date(now.getTime() - STALE_AFTER_MS).toISOString();
196
- const fs = makeFs({
197
- [CACHE_PATH]: JSON.stringify({ latestVersion: '1.0.0', checkedAt }),
198
- });
199
-
200
- let runnerCalls = 0;
201
- const result = await isStale({
202
- cachePath: CACHE_PATH,
203
- now,
204
- runner: () => {
205
- runnerCalls++;
206
- return '1.0.1';
207
- },
208
- fs,
209
- });
210
-
211
- assert.equal(runnerCalls, 1);
212
- assert.equal(result.stale, true);
213
- assert.equal(result.latestVersion, '1.0.1');
214
- });
215
-
216
- it('awaits an async runner that returns a promise', async () => {
217
- const now = new Date('2026-06-03T12:00:00.000Z');
218
- const checkedAt = new Date(
219
- now.getTime() - 48 * 60 * 60 * 1000,
220
- ).toISOString();
221
- const fs = makeFs({
222
- [CACHE_PATH]: JSON.stringify({ latestVersion: '1.0.0', checkedAt }),
223
- });
224
-
225
- const result = await isStale({
226
- cachePath: CACHE_PATH,
227
- now,
228
- runner: async () => Promise.resolve('4.4.4'),
229
- fs,
230
- });
231
-
232
- assert.equal(result.latestVersion, '4.4.4');
233
- assert.equal(result.refreshed, true);
234
- });
235
- });
236
-
237
- // ---------------------------------------------------------------------------
238
- // 4. isStale — missing cache → runner + write
239
- // ---------------------------------------------------------------------------
240
-
241
- describe('isStale with a missing cache', () => {
242
- it('probes the network and writes a fresh cache', async () => {
243
- const now = new Date('2026-06-03T12:00:00.000Z');
244
- const fs = makeFs({}); // no cache file seeded
245
-
246
- let runnerCalls = 0;
247
- const result = await isStale({
248
- cachePath: CACHE_PATH,
249
- now,
250
- runner: () => {
251
- runnerCalls++;
252
- return '7.7.7';
253
- },
254
- fs,
255
- });
256
-
257
- assert.equal(runnerCalls, 1);
258
- assert.equal(result.stale, true);
259
- assert.equal(result.refreshed, true);
260
- assert.equal(result.latestVersion, '7.7.7');
261
- // Created the temp-root dir and wrote the cache.
262
- assert.equal(fs.mkdirCalls.length, 1);
263
- assert.equal(fs.writeCalls.length, 1);
264
- assert.ok(fs.files.has(CACHE_PATH));
265
- });
266
-
267
- it('throws when the cache is stale and no runner seam is provided', async () => {
268
- const fs = makeFs({}); // missing cache forces a refresh path
269
- await assert.rejects(
270
- () => isStale({ cachePath: CACHE_PATH, now: new Date(), fs }),
271
- /runner seam is required/,
272
- );
273
- });
274
- });
275
-
276
- // ---------------------------------------------------------------------------
277
- // 5. refreshCache — persistence contract
278
- // ---------------------------------------------------------------------------
279
-
280
- describe('refreshCache', () => {
281
- it('persists { latestVersion, checkedAt } JSON under the given path', () => {
282
- const now = new Date('2026-06-03T12:00:00.000Z');
283
- const fs = makeFs({});
284
-
285
- const record = refreshCache({
286
- cachePath: CACHE_PATH,
287
- latestVersion: '5.6.7',
288
- now,
289
- fs,
290
- });
291
-
292
- assert.deepEqual(record, {
293
- latestVersion: '5.6.7',
294
- checkedAt: now.toISOString(),
295
- });
296
-
297
- // Directory (temp root) created recursively, then file written.
298
- assert.equal(fs.mkdirCalls.length, 1);
299
- assert.equal(fs.mkdirCalls[0].dir, path.dirname(CACHE_PATH));
300
- assert.deepEqual(fs.mkdirCalls[0].opts, { recursive: true });
301
-
302
- const persisted = JSON.parse(fs.files.get(CACHE_PATH));
303
- assert.deepEqual(persisted, {
304
- latestVersion: '5.6.7',
305
- checkedAt: now.toISOString(),
306
- });
307
- });
308
- });
309
-
310
- // ---------------------------------------------------------------------------
311
- // 6. readCache — null on missing / malformed / incomplete
312
- // ---------------------------------------------------------------------------
313
-
314
- describe('readCache', () => {
315
- it('returns the parsed record for a well-formed cache', () => {
316
- const checkedAt = '2026-06-03T00:00:00.000Z';
317
- const fs = makeFs({
318
- [CACHE_PATH]: JSON.stringify({ latestVersion: '1.1.1', checkedAt }),
319
- });
320
- assert.deepEqual(readCache({ cachePath: CACHE_PATH, fs }), {
321
- latestVersion: '1.1.1',
322
- checkedAt,
323
- });
324
- });
325
-
326
- it('returns null when the file is missing', () => {
327
- const fs = makeFs({});
328
- assert.equal(readCache({ cachePath: CACHE_PATH, fs }), null);
329
- });
330
-
331
- it('returns null when the file is malformed JSON', () => {
332
- const fs = makeFs({ [CACHE_PATH]: 'not json {' });
333
- assert.equal(readCache({ cachePath: CACHE_PATH, fs }), null);
334
- });
335
-
336
- it('returns null when required fields are missing', () => {
337
- const fs = makeFs({
338
- [CACHE_PATH]: JSON.stringify({ latestVersion: '1.0.0' }),
339
- });
340
- assert.equal(readCache({ cachePath: CACHE_PATH, fs }), null);
341
- });
342
-
343
- it('returns null when cachePath is falsy', () => {
344
- const fs = makeFs({});
345
- assert.equal(readCache({ cachePath: '', fs }), null);
346
- });
347
- });
348
-
349
- // ---------------------------------------------------------------------------
350
- // 7. Logging contract — versions and paths only, never secrets/contents
351
- // ---------------------------------------------------------------------------
352
-
353
- describe('logging contract (security-baseline § 5)', () => {
354
- it('logs only version strings and paths on a fresh cache', async () => {
355
- const now = new Date('2026-06-03T12:00:00.000Z');
356
- const checkedAt = new Date(now.getTime() - 60 * 60 * 1000).toISOString();
357
- const secretToken = 'npm_supersecrettoken1234567890';
358
- const fs = makeFs({
359
- [CACHE_PATH]: JSON.stringify({ latestVersion: '1.2.3', checkedAt }),
360
- });
361
- const log = makeLog();
362
-
363
- await isStale({
364
- cachePath: CACHE_PATH,
365
- now,
366
- runner: () => secretToken, // never invoked on a fresh cache
367
- fs,
368
- log,
369
- });
370
-
371
- const joined = log.lines.join('\n');
372
- assert.ok(joined.includes('1.2.3'), 'should log the version string');
373
- assert.ok(!joined.includes(secretToken), 'must never log a token');
374
- });
375
-
376
- it('logs the version and path but not raw file contents on refresh', async () => {
377
- const now = new Date('2026-06-03T12:00:00.000Z');
378
- const fs = makeFs({});
379
- const log = makeLog();
380
-
381
- await isStale({
382
- cachePath: CACHE_PATH,
383
- now,
384
- runner: () => '8.8.8',
385
- fs,
386
- log,
387
- });
388
-
389
- const joined = log.lines.join('\n');
390
- assert.ok(joined.includes('8.8.8'), 'should log the refreshed version');
391
- assert.ok(joined.includes(CACHE_PATH), 'should log the cache path');
392
- // The raw JSON payload (with its quoted keys) must not be echoed wholesale.
393
- assert.ok(
394
- !joined.includes('"latestVersion"'),
395
- 'must not log raw file contents',
396
- );
397
- });
398
- });
@@ -1,216 +0,0 @@
1
- // lib/migrations/__tests__/index.test.js
2
- /**
3
- * Unit tests for lib/migrations/index.js — the version-keyed migration runner.
4
- *
5
- * All tests drive runMigrations through injected seams (a `log` capture and a
6
- * fixture `registry`) over an in-memory plain-object context, so no real
7
- * stdout write and no real filesystem I/O occur (testing-standards § Unit).
8
- *
9
- * Coverage contract (Story #3501 AC):
10
- * - Module shape: runMigrations named export + ordered `migrations` registry
11
- * array (which ships empty).
12
- * - Version filtering: only steps with fromVersion < version <= toVersion
13
- * apply, in ascending version order.
14
- * - Idempotency: a second pass over the same context applies nothing
15
- * (detect() returns false post-apply).
16
- * - Log seam: each applied step prints `migrated <version>: <description>`.
17
- */
18
-
19
- import assert from 'node:assert/strict';
20
- import { describe, it } from 'node:test';
21
-
22
- import runMigrations, { compareVersions, migrations } from '../index.js';
23
-
24
- // ---------------------------------------------------------------------------
25
- // Fixture steps
26
- // ---------------------------------------------------------------------------
27
-
28
- /**
29
- * Build a fixture step keyed on a context flag. `detect` returns true only
30
- * while the flag is unset; `apply` sets it. This satisfies the idempotency
31
- * contract: once applied, detect returns false.
32
- *
33
- * @param {string} version
34
- * @param {string} flag - ctx property the step toggles.
35
- * @returns {{ version: string, description: string, detect: Function, apply: Function }}
36
- */
37
- function makeStep(version, flag) {
38
- return {
39
- version,
40
- description: `set ${flag}`,
41
- detect: (ctx) => ctx[flag] !== true,
42
- apply: (ctx) => {
43
- ctx[flag] = true;
44
- },
45
- };
46
- }
47
-
48
- /** A deliberately out-of-order registry to prove the runner sorts ascending. */
49
- function fixtureRegistry() {
50
- return [
51
- makeStep('1.4.0', 'a140'),
52
- makeStep('1.2.0', 'a120'),
53
- makeStep('1.3.0', 'a130'),
54
- makeStep('1.5.0', 'a150'),
55
- ];
56
- }
57
-
58
- // ---------------------------------------------------------------------------
59
- // Module shape
60
- // ---------------------------------------------------------------------------
61
-
62
- describe('migrations module exports', () => {
63
- it('exports runMigrations as the default function', () => {
64
- assert.equal(typeof runMigrations, 'function');
65
- });
66
-
67
- it('exports an ordered `migrations` registry array', () => {
68
- assert.ok(Array.isArray(migrations));
69
- });
70
-
71
- it('ships an empty registry (no real contract break yet)', () => {
72
- assert.equal(migrations.length, 0);
73
- });
74
- });
75
-
76
- // ---------------------------------------------------------------------------
77
- // compareVersions
78
- // ---------------------------------------------------------------------------
79
-
80
- describe('compareVersions', () => {
81
- it('orders by major, then minor, then patch', () => {
82
- assert.ok(compareVersions('1.2.0', '1.3.0') < 0);
83
- assert.ok(compareVersions('2.0.0', '1.9.9') > 0);
84
- assert.equal(compareVersions('1.4.0', '1.4.0'), 0);
85
- assert.ok(compareVersions('1.4.1', '1.4.0') > 0);
86
- });
87
- });
88
-
89
- // ---------------------------------------------------------------------------
90
- // AC — version filtering + ascending order
91
- // ---------------------------------------------------------------------------
92
-
93
- describe('runMigrations — version filtering and ordering', () => {
94
- it('applies only steps with fromVersion < version <= toVersion, ascending', () => {
95
- const ctx = {};
96
- const order = [];
97
- const log = (msg) => order.push(msg);
98
-
99
- const result = runMigrations({
100
- fromVersion: '1.2.0',
101
- toVersion: '1.4.0',
102
- ctx,
103
- log,
104
- registry: fixtureRegistry(),
105
- });
106
-
107
- // 1.2.0 == fromVersion → excluded (already in the tree).
108
- // 1.5.0 > toVersion → excluded.
109
- // 1.3.0 and 1.4.0 apply, in ascending order.
110
- assert.deepEqual(result.applied, ['1.3.0', '1.4.0']);
111
- assert.deepEqual(order, [
112
- 'migrated 1.3.0: set a130',
113
- 'migrated 1.4.0: set a140',
114
- ]);
115
- assert.equal(ctx.a120, undefined);
116
- assert.equal(ctx.a130, true);
117
- assert.equal(ctx.a140, true);
118
- assert.equal(ctx.a150, undefined);
119
- });
120
-
121
- it('includes the step at exactly toVersion (inclusive upper bound)', () => {
122
- const ctx = {};
123
- const result = runMigrations({
124
- fromVersion: '1.2.0',
125
- toVersion: '1.5.0',
126
- ctx,
127
- log: () => {},
128
- registry: fixtureRegistry(),
129
- });
130
- assert.deepEqual(result.applied, ['1.3.0', '1.4.0', '1.5.0']);
131
- });
132
-
133
- it('excludes the step at exactly fromVersion (exclusive lower bound)', () => {
134
- const ctx = {};
135
- const result = runMigrations({
136
- fromVersion: '1.3.0',
137
- toVersion: '1.5.0',
138
- ctx,
139
- log: () => {},
140
- registry: fixtureRegistry(),
141
- });
142
- assert.deepEqual(result.applied, ['1.4.0', '1.5.0']);
143
- // 1.2.0 and 1.3.0 never ran.
144
- assert.equal(ctx.a120, undefined);
145
- assert.equal(ctx.a130, undefined);
146
- });
147
-
148
- it('applies nothing when the range is empty', () => {
149
- const ctx = {};
150
- const result = runMigrations({
151
- fromVersion: '1.5.0',
152
- toVersion: '1.5.0',
153
- ctx,
154
- log: () => {},
155
- registry: fixtureRegistry(),
156
- });
157
- assert.deepEqual(result.applied, []);
158
- });
159
- });
160
-
161
- // ---------------------------------------------------------------------------
162
- // AC — idempotency
163
- // ---------------------------------------------------------------------------
164
-
165
- describe('runMigrations — idempotency', () => {
166
- it('applies nothing on a second pass over the same context', () => {
167
- const ctx = {};
168
- const registry = fixtureRegistry();
169
- const range = { fromVersion: '1.2.0', toVersion: '1.5.0', ctx, registry };
170
-
171
- const first = runMigrations({ ...range, log: () => {} });
172
- assert.deepEqual(first.applied, ['1.3.0', '1.4.0', '1.5.0']);
173
-
174
- const secondLog = [];
175
- const second = runMigrations({
176
- ...range,
177
- log: (msg) => secondLog.push(msg),
178
- });
179
-
180
- // Second pass: every in-range step is skipped because detect() now
181
- // returns false post-apply.
182
- assert.deepEqual(second.applied, []);
183
- assert.deepEqual(second.skipped, ['1.3.0', '1.4.0', '1.5.0']);
184
- assert.equal(secondLog.length, 0);
185
- });
186
- });
187
-
188
- // ---------------------------------------------------------------------------
189
- // AC — log seam
190
- // ---------------------------------------------------------------------------
191
-
192
- describe('runMigrations — log seam', () => {
193
- it('prints `migrated <version>: <description>` for each applied step', () => {
194
- const lines = [];
195
- runMigrations({
196
- fromVersion: '1.0.0',
197
- toVersion: '1.3.0',
198
- ctx: {},
199
- log: (msg) => lines.push(msg),
200
- registry: [makeStep('1.3.0', 'a130')],
201
- });
202
- assert.deepEqual(lines, ['migrated 1.3.0: set a130']);
203
- });
204
-
205
- it('does not log a step that detect skips', () => {
206
- const lines = [];
207
- runMigrations({
208
- fromVersion: '1.0.0',
209
- toVersion: '1.3.0',
210
- ctx: { a130: true }, // already applied
211
- log: (msg) => lines.push(msg),
212
- registry: [makeStep('1.3.0', 'a130')],
213
- });
214
- assert.deepEqual(lines, []);
215
- });
216
- });