@lumenflow/cli 2.4.0 → 2.5.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 (147) hide show
  1. package/README.md +11 -8
  2. package/dist/__tests__/init-config-lanes.test.js +131 -0
  3. package/dist/__tests__/init-docs-structure.test.js +119 -0
  4. package/dist/__tests__/init-lane-inference.test.js +125 -0
  5. package/dist/__tests__/init-onboarding-docs.test.js +132 -0
  6. package/dist/__tests__/init-quick-ref.test.js +145 -0
  7. package/dist/__tests__/init-scripts.test.js +207 -0
  8. package/dist/__tests__/init-template-portability.test.js +97 -0
  9. package/dist/__tests__/init.test.js +7 -2
  10. package/dist/__tests__/initiative-add-wu.test.js +420 -0
  11. package/dist/__tests__/initiative-plan-replacement.test.js +162 -0
  12. package/dist/__tests__/initiative-remove-wu.test.js +458 -0
  13. package/dist/__tests__/onboarding-smoke-test.test.js +211 -0
  14. package/dist/__tests__/path-centralization-cli.test.js +234 -0
  15. package/dist/__tests__/plan-create.test.js +126 -0
  16. package/dist/__tests__/plan-edit.test.js +157 -0
  17. package/dist/__tests__/plan-link.test.js +239 -0
  18. package/dist/__tests__/plan-promote.test.js +181 -0
  19. package/dist/__tests__/templates-sync.test.js +219 -0
  20. package/dist/__tests__/wu-create-strict.test.js +118 -0
  21. package/dist/__tests__/wu-edit-strict.test.js +109 -0
  22. package/dist/__tests__/wu-validate-strict.test.js +113 -0
  23. package/dist/flow-bottlenecks.js +4 -2
  24. package/dist/gates.js +22 -0
  25. package/dist/init.js +670 -87
  26. package/dist/initiative-add-wu.js +112 -16
  27. package/dist/initiative-remove-wu.js +248 -0
  28. package/dist/onboarding-smoke-test.js +400 -0
  29. package/dist/orchestrate-init-status.js +37 -9
  30. package/dist/orchestrate-initiative.js +10 -4
  31. package/dist/plan-create.js +199 -0
  32. package/dist/plan-edit.js +235 -0
  33. package/dist/plan-link.js +233 -0
  34. package/dist/plan-promote.js +231 -0
  35. package/dist/sync-templates.js +137 -5
  36. package/dist/wu-block.js +16 -5
  37. package/dist/wu-claim.js +15 -9
  38. package/dist/wu-create.js +50 -2
  39. package/dist/wu-deps.js +3 -1
  40. package/dist/wu-done.js +14 -5
  41. package/dist/wu-edit.js +35 -0
  42. package/dist/wu-prep.js +131 -8
  43. package/dist/wu-spawn.js +14 -1
  44. package/dist/wu-unblock.js +34 -2
  45. package/dist/wu-validate.js +25 -17
  46. package/package.json +11 -7
  47. package/templates/core/.lumenflow/constraints.md.template +61 -3
  48. package/templates/core/AGENTS.md.template +2 -2
  49. package/templates/core/LUMENFLOW.md.template +85 -23
  50. package/templates/core/ai/onboarding/agent-invocation-guide.md.template +157 -0
  51. package/templates/core/ai/onboarding/agent-safety-card.md.template +227 -0
  52. package/templates/core/ai/onboarding/docs-generation.md.template +277 -0
  53. package/templates/core/ai/onboarding/first-wu-mistakes.md.template +49 -7
  54. package/templates/core/ai/onboarding/quick-ref-commands.md.template +343 -110
  55. package/templates/core/ai/onboarding/release-process.md.template +8 -2
  56. package/templates/core/ai/onboarding/starting-prompt.md.template +407 -0
  57. package/templates/core/ai/onboarding/test-ratchet.md.template +131 -0
  58. package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +91 -38
  59. package/templates/core/ai/onboarding/vendor-support.md.template +219 -0
  60. package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +13 -1
  61. package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +14 -16
  62. package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +48 -4
  63. package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +5 -1
  64. package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +19 -8
  65. package/dist/__tests__/init-plan.test.js +0 -340
  66. package/dist/agent-issues-query.d.ts +0 -16
  67. package/dist/agent-log-issue.d.ts +0 -10
  68. package/dist/agent-session-end.d.ts +0 -10
  69. package/dist/agent-session.d.ts +0 -10
  70. package/dist/backlog-prune.d.ts +0 -84
  71. package/dist/cli-entry-point.d.ts +0 -8
  72. package/dist/deps-add.d.ts +0 -91
  73. package/dist/deps-remove.d.ts +0 -17
  74. package/dist/docs-sync.d.ts +0 -50
  75. package/dist/file-delete.d.ts +0 -84
  76. package/dist/file-edit.d.ts +0 -82
  77. package/dist/file-read.d.ts +0 -92
  78. package/dist/file-write.d.ts +0 -90
  79. package/dist/flow-bottlenecks.d.ts +0 -16
  80. package/dist/flow-report.d.ts +0 -16
  81. package/dist/gates.d.ts +0 -94
  82. package/dist/git-branch.d.ts +0 -65
  83. package/dist/git-diff.d.ts +0 -58
  84. package/dist/git-log.d.ts +0 -69
  85. package/dist/git-status.d.ts +0 -58
  86. package/dist/guard-locked.d.ts +0 -62
  87. package/dist/guard-main-branch.d.ts +0 -50
  88. package/dist/guard-worktree-commit.d.ts +0 -59
  89. package/dist/index.d.ts +0 -10
  90. package/dist/init-plan.d.ts +0 -80
  91. package/dist/init-plan.js +0 -337
  92. package/dist/init.d.ts +0 -46
  93. package/dist/initiative-add-wu.d.ts +0 -22
  94. package/dist/initiative-bulk-assign-wus.d.ts +0 -16
  95. package/dist/initiative-create.d.ts +0 -28
  96. package/dist/initiative-edit.d.ts +0 -34
  97. package/dist/initiative-list.d.ts +0 -12
  98. package/dist/initiative-status.d.ts +0 -11
  99. package/dist/lumenflow-upgrade.d.ts +0 -103
  100. package/dist/mem-checkpoint.d.ts +0 -16
  101. package/dist/mem-cleanup.d.ts +0 -29
  102. package/dist/mem-create.d.ts +0 -17
  103. package/dist/mem-export.d.ts +0 -10
  104. package/dist/mem-inbox.d.ts +0 -35
  105. package/dist/mem-init.d.ts +0 -15
  106. package/dist/mem-ready.d.ts +0 -16
  107. package/dist/mem-signal.d.ts +0 -16
  108. package/dist/mem-start.d.ts +0 -16
  109. package/dist/mem-summarize.d.ts +0 -22
  110. package/dist/mem-triage.d.ts +0 -22
  111. package/dist/metrics-cli.d.ts +0 -90
  112. package/dist/metrics-snapshot.d.ts +0 -18
  113. package/dist/orchestrate-init-status.d.ts +0 -11
  114. package/dist/orchestrate-initiative.d.ts +0 -12
  115. package/dist/orchestrate-monitor.d.ts +0 -11
  116. package/dist/release.d.ts +0 -117
  117. package/dist/rotate-progress.d.ts +0 -48
  118. package/dist/session-coordinator.d.ts +0 -74
  119. package/dist/spawn-list.d.ts +0 -16
  120. package/dist/state-bootstrap.d.ts +0 -92
  121. package/dist/sync-templates.d.ts +0 -52
  122. package/dist/trace-gen.d.ts +0 -84
  123. package/dist/validate-agent-skills.d.ts +0 -50
  124. package/dist/validate-agent-sync.d.ts +0 -36
  125. package/dist/validate-backlog-sync.d.ts +0 -37
  126. package/dist/validate-skills-spec.d.ts +0 -40
  127. package/dist/validate.d.ts +0 -60
  128. package/dist/wu-block.d.ts +0 -16
  129. package/dist/wu-claim.d.ts +0 -74
  130. package/dist/wu-cleanup.d.ts +0 -35
  131. package/dist/wu-create.d.ts +0 -69
  132. package/dist/wu-delete.d.ts +0 -21
  133. package/dist/wu-deps.d.ts +0 -13
  134. package/dist/wu-done.d.ts +0 -225
  135. package/dist/wu-edit.d.ts +0 -63
  136. package/dist/wu-infer-lane.d.ts +0 -17
  137. package/dist/wu-preflight.d.ts +0 -47
  138. package/dist/wu-prune.d.ts +0 -16
  139. package/dist/wu-recover.d.ts +0 -37
  140. package/dist/wu-release.d.ts +0 -19
  141. package/dist/wu-repair.d.ts +0 -60
  142. package/dist/wu-spawn-completion.d.ts +0 -10
  143. package/dist/wu-spawn.d.ts +0 -192
  144. package/dist/wu-status.d.ts +0 -25
  145. package/dist/wu-unblock.d.ts +0 -16
  146. package/dist/wu-unlock-lane.d.ts +0 -19
  147. package/dist/wu-validate.d.ts +0 -16
@@ -0,0 +1,234 @@
1
+ /**
2
+ * @fileoverview Tests for WU-1311: CLI path centralization
3
+ *
4
+ * Tests that CLI commands use WU_PATHS/getResolvedPaths/getConfig
5
+ * instead of hardcoded 'docs/04-operations' paths.
6
+ *
7
+ * WU-1311 Acceptance Criteria:
8
+ * - No hardcoded docs/04-operations paths remain in CLI commands (use WU_PATHS/getResolvedPaths)
9
+ * - state-doctor warns when configured paths are missing
10
+ * - Config overrides are respected across wu-* commands and diagnostics
11
+ * - Unit tests cover CLI path usage and warnings
12
+ *
13
+ * @module __tests__/path-centralization-cli.test
14
+ */
15
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
16
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
17
+ import { tmpdir } from 'node:os';
18
+ import path from 'node:path';
19
+ import * as yaml from 'yaml';
20
+ import { clearConfigCache, getConfig } from '@lumenflow/core/dist/lumenflow-config.js';
21
+ import { WU_PATHS, createWuPaths } from '@lumenflow/core/dist/wu-paths.js';
22
+ /** Config file name constant */
23
+ const CONFIG_FILE = '.lumenflow.config.yaml';
24
+ describe('WU-1311: CLI path centralization', () => {
25
+ let tempDir;
26
+ beforeEach(async () => {
27
+ tempDir = await mkdtemp(path.join(tmpdir(), 'cli-path-test-'));
28
+ clearConfigCache();
29
+ });
30
+ afterEach(async () => {
31
+ await rm(tempDir, { recursive: true, force: true });
32
+ clearConfigCache();
33
+ });
34
+ describe('AC1: No hardcoded docs/04-operations paths in CLI commands', () => {
35
+ it('should use config-based paths for WU file path generation', () => {
36
+ const paths = createWuPaths({ projectRoot: tempDir });
37
+ // Paths should be from config, not hardcoded
38
+ expect(paths.WU('WU-1311')).toBe('docs/04-operations/tasks/wu/WU-1311.yaml');
39
+ expect(paths.STATUS()).toBe('docs/04-operations/tasks/status.md');
40
+ expect(paths.BACKLOG()).toBe('docs/04-operations/tasks/backlog.md');
41
+ });
42
+ it('should respect custom config paths for WU operations', async () => {
43
+ const customConfig = {
44
+ version: '1.0.0',
45
+ directories: {
46
+ wuDir: 'custom/wu',
47
+ backlogPath: 'custom/backlog.md',
48
+ statusPath: 'custom/status.md',
49
+ },
50
+ };
51
+ await writeFile(path.join(tempDir, CONFIG_FILE), yaml.stringify(customConfig), 'utf-8');
52
+ clearConfigCache();
53
+ const paths = createWuPaths({ projectRoot: tempDir });
54
+ expect(paths.WU('WU-1311')).toBe('custom/wu/WU-1311.yaml');
55
+ expect(paths.STATUS()).toBe('custom/status.md');
56
+ expect(paths.BACKLOG()).toBe('custom/backlog.md');
57
+ });
58
+ it('should use WU_PATHS for stamp file generation', () => {
59
+ const stampPath = WU_PATHS.STAMP('WU-1311');
60
+ expect(stampPath).toContain('WU-1311.done');
61
+ });
62
+ });
63
+ describe('AC2: state-doctor warns when configured paths are missing', () => {
64
+ it('should detect missing WU directory', async () => {
65
+ // Create a config pointing to non-existent paths
66
+ const customConfig = {
67
+ version: '1.0.0',
68
+ directories: {
69
+ wuDir: 'nonexistent/wu',
70
+ },
71
+ };
72
+ await writeFile(path.join(tempDir, CONFIG_FILE), yaml.stringify(customConfig), 'utf-8');
73
+ clearConfigCache();
74
+ // getResolvedPaths returns paths even if they don't exist
75
+ // state-doctor should check existence and warn
76
+ const { getResolvedPaths } = await import('@lumenflow/core/dist/lumenflow-config.js');
77
+ const paths = getResolvedPaths({ projectRoot: tempDir });
78
+ expect(paths.wuDir).toBe(path.join(tempDir, 'nonexistent/wu'));
79
+ // The warnMissingPaths function in state-doctor.ts checks existsSync
80
+ // This verifies the path is properly resolved for checking
81
+ });
82
+ });
83
+ describe('AC3: Config overrides are respected across wu-* commands', () => {
84
+ it('should use custom initiatives directory from config', async () => {
85
+ const customConfig = {
86
+ version: '1.0.0',
87
+ directories: {
88
+ initiativesDir: 'custom/initiatives',
89
+ },
90
+ };
91
+ await writeFile(path.join(tempDir, CONFIG_FILE), yaml.stringify(customConfig), 'utf-8');
92
+ clearConfigCache();
93
+ const paths = createWuPaths({ projectRoot: tempDir });
94
+ expect(paths.INITIATIVES_DIR()).toBe('custom/initiatives');
95
+ });
96
+ it('should use custom plans directory from config', async () => {
97
+ const customConfig = {
98
+ version: '1.0.0',
99
+ directories: {
100
+ plansDir: 'custom/plans',
101
+ },
102
+ };
103
+ await writeFile(path.join(tempDir, CONFIG_FILE), yaml.stringify(customConfig), 'utf-8');
104
+ clearConfigCache();
105
+ const paths = createWuPaths({ projectRoot: tempDir });
106
+ expect(paths.PLANS_DIR()).toBe('custom/plans');
107
+ });
108
+ it('should use custom templates directory from config', async () => {
109
+ const customConfig = {
110
+ version: '1.0.0',
111
+ directories: {
112
+ templatesDir: 'custom/templates',
113
+ },
114
+ };
115
+ await writeFile(path.join(tempDir, CONFIG_FILE), yaml.stringify(customConfig), 'utf-8');
116
+ clearConfigCache();
117
+ const paths = createWuPaths({ projectRoot: tempDir });
118
+ expect(paths.TEMPLATES_DIR()).toBe('custom/templates');
119
+ });
120
+ it('should use custom onboarding directory from config', async () => {
121
+ const customConfig = {
122
+ version: '1.0.0',
123
+ directories: {
124
+ onboardingDir: 'custom/onboarding',
125
+ },
126
+ };
127
+ await writeFile(path.join(tempDir, CONFIG_FILE), yaml.stringify(customConfig), 'utf-8');
128
+ clearConfigCache();
129
+ const paths = createWuPaths({ projectRoot: tempDir });
130
+ expect(paths.ONBOARDING_DIR()).toBe('custom/onboarding');
131
+ });
132
+ });
133
+ describe('AC4: Whitelist paths use config-based values', () => {
134
+ it('should generate correct whitelist paths for wu:done staged file validation', async () => {
135
+ const customConfig = {
136
+ version: '1.0.0',
137
+ directories: {
138
+ wuDir: 'tasks/wu',
139
+ backlogPath: 'tasks/backlog.md',
140
+ statusPath: 'tasks/status.md',
141
+ },
142
+ };
143
+ await writeFile(path.join(tempDir, CONFIG_FILE), yaml.stringify(customConfig), 'utf-8');
144
+ clearConfigCache();
145
+ const config = getConfig({ projectRoot: tempDir });
146
+ const wuId = 'WU-1311';
147
+ // The whitelist should use config paths, not hardcoded ones
148
+ const expectedWuPath = path.join(config.directories.wuDir, `${wuId}.yaml`);
149
+ const expectedBacklogPath = config.directories.backlogPath;
150
+ const expectedStatusPath = config.directories.statusPath;
151
+ expect(expectedWuPath).toBe('tasks/wu/WU-1311.yaml');
152
+ expect(expectedBacklogPath).toBe('tasks/backlog.md');
153
+ expect(expectedStatusPath).toBe('tasks/status.md');
154
+ });
155
+ });
156
+ describe('AC5: Error messages use config-based paths', () => {
157
+ it('should provide config-aware error messages for missing WU', async () => {
158
+ const customConfig = {
159
+ version: '1.0.0',
160
+ directories: {
161
+ wuDir: 'custom/wu',
162
+ },
163
+ };
164
+ await writeFile(path.join(tempDir, CONFIG_FILE), yaml.stringify(customConfig), 'utf-8');
165
+ clearConfigCache();
166
+ const config = getConfig({ projectRoot: tempDir });
167
+ // Error messages should reference the configured path, not hardcoded
168
+ const expectedDir = config.directories.wuDir;
169
+ expect(expectedDir).toBe('custom/wu');
170
+ // CLI commands should use this in error messages like:
171
+ // "WU not found in ${config.directories.wuDir}/"
172
+ });
173
+ });
174
+ });
175
+ describe('WU-1311: validateStagedFiles whitelist paths', () => {
176
+ let tempDir;
177
+ beforeEach(async () => {
178
+ tempDir = await mkdtemp(path.join(tmpdir(), 'whitelist-test-'));
179
+ clearConfigCache();
180
+ });
181
+ afterEach(async () => {
182
+ await rm(tempDir, { recursive: true, force: true });
183
+ clearConfigCache();
184
+ });
185
+ it('should generate whitelist paths from config', async () => {
186
+ const customConfig = {
187
+ version: '1.0.0',
188
+ directories: {
189
+ wuDir: 'my/tasks/wu',
190
+ backlogPath: 'my/tasks/backlog.md',
191
+ statusPath: 'my/tasks/status.md',
192
+ },
193
+ };
194
+ await writeFile(path.join(tempDir, CONFIG_FILE), yaml.stringify(customConfig), 'utf-8');
195
+ clearConfigCache();
196
+ const config = getConfig({ projectRoot: tempDir });
197
+ // Helper function that CLI should use to generate whitelist
198
+ const generateWhitelist = (id) => [
199
+ path.join(config.directories.wuDir, `${id}.yaml`),
200
+ config.directories.statusPath,
201
+ config.directories.backlogPath,
202
+ ];
203
+ const whitelist = generateWhitelist('WU-1311');
204
+ expect(whitelist).toContain('my/tasks/wu/WU-1311.yaml');
205
+ expect(whitelist).toContain('my/tasks/backlog.md');
206
+ expect(whitelist).toContain('my/tasks/status.md');
207
+ });
208
+ });
209
+ describe('WU-1311: getWorktreeCommitFiles config paths', () => {
210
+ let tempDir;
211
+ beforeEach(async () => {
212
+ tempDir = await mkdtemp(path.join(tmpdir(), 'commit-files-test-'));
213
+ clearConfigCache();
214
+ });
215
+ afterEach(async () => {
216
+ await rm(tempDir, { recursive: true, force: true });
217
+ clearConfigCache();
218
+ });
219
+ it('should use config-based WU path in commit file list', async () => {
220
+ const customConfig = {
221
+ version: '1.0.0',
222
+ directories: {
223
+ wuDir: 'custom/wu',
224
+ },
225
+ };
226
+ await writeFile(path.join(tempDir, CONFIG_FILE), yaml.stringify(customConfig), 'utf-8');
227
+ clearConfigCache();
228
+ const config = getConfig({ projectRoot: tempDir });
229
+ // The getWorktreeCommitFiles function should use config paths
230
+ const wuId = 'WU-1311';
231
+ const expectedWuPath = path.join(config.directories.wuDir, `${wuId}.yaml`);
232
+ expect(expectedWuPath).toBe('custom/wu/WU-1311.yaml');
233
+ });
234
+ });
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Tests for plan:create command (WU-1313)
3
+ *
4
+ * The plan:create command creates plan files in the repo-native plansDir.
5
+ * Plans can be linked to WUs (via spec_refs) or initiatives (via related_plan).
6
+ *
7
+ * TDD: These tests are written BEFORE the implementation.
8
+ */
9
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
10
+ import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { tmpdir } from 'node:os';
13
+ // Mock modules before importing
14
+ vi.mock('@lumenflow/core/dist/git-adapter.js', () => ({
15
+ getGitForCwd: vi.fn(() => ({
16
+ branch: vi.fn().mockResolvedValue({ current: 'main' }),
17
+ status: vi.fn().mockResolvedValue({ isClean: () => true }),
18
+ })),
19
+ }));
20
+ vi.mock('@lumenflow/core/dist/wu-helpers.js', () => ({
21
+ ensureOnMain: vi.fn().mockResolvedValue(undefined),
22
+ }));
23
+ vi.mock('@lumenflow/core/dist/micro-worktree.js', () => ({
24
+ withMicroWorktree: vi.fn(async ({ execute }) => {
25
+ const tempDir = join(tmpdir(), `plan-create-test-${Date.now()}`);
26
+ mkdirSync(tempDir, { recursive: true });
27
+ return execute({ worktreePath: tempDir });
28
+ }),
29
+ }));
30
+ describe('plan:create command', () => {
31
+ let tempDir;
32
+ let originalCwd;
33
+ beforeEach(() => {
34
+ tempDir = join(tmpdir(), `plan-create-test-${Date.now()}`);
35
+ mkdirSync(tempDir, { recursive: true });
36
+ originalCwd = process.cwd();
37
+ });
38
+ afterEach(() => {
39
+ process.chdir(originalCwd);
40
+ if (existsSync(tempDir)) {
41
+ rmSync(tempDir, { recursive: true, force: true });
42
+ }
43
+ vi.clearAllMocks();
44
+ });
45
+ describe('createPlan', () => {
46
+ it('should create a plan file in repo plansDir', async () => {
47
+ const { createPlan } = await import('../plan-create.js');
48
+ const plansDir = join(tempDir, 'docs', '04-operations', 'plans');
49
+ mkdirSync(plansDir, { recursive: true });
50
+ const planPath = createPlan(tempDir, 'WU-1313', 'Implement plan tooling');
51
+ expect(existsSync(planPath)).toBe(true);
52
+ const content = readFileSync(planPath, 'utf-8');
53
+ expect(content).toContain('# WU-1313');
54
+ expect(content).toContain('Implement plan tooling');
55
+ expect(content).toContain('## Goal');
56
+ expect(content).toContain('## Scope');
57
+ expect(content).toContain('## Approach');
58
+ });
59
+ it('should create plans directory if it does not exist', async () => {
60
+ const { createPlan } = await import('../plan-create.js');
61
+ // Do NOT pre-create the plans directory
62
+ const planPath = createPlan(tempDir, 'WU-1313', 'Test Plan');
63
+ expect(existsSync(planPath)).toBe(true);
64
+ expect(planPath).toContain('docs/04-operations/plans');
65
+ });
66
+ it('should not overwrite existing plan file', async () => {
67
+ const { createPlan } = await import('../plan-create.js');
68
+ const plansDir = join(tempDir, 'docs', '04-operations', 'plans');
69
+ mkdirSync(plansDir, { recursive: true });
70
+ // Create existing file
71
+ const existingPath = join(plansDir, 'WU-1313-plan.md');
72
+ writeFileSync(existingPath, '# Existing Content');
73
+ expect(() => createPlan(tempDir, 'WU-1313', 'New Title')).toThrow();
74
+ });
75
+ it('should support initiative ID format', async () => {
76
+ const { createPlan } = await import('../plan-create.js');
77
+ const planPath = createPlan(tempDir, 'INIT-001', 'Initiative Plan');
78
+ expect(existsSync(planPath)).toBe(true);
79
+ const content = readFileSync(planPath, 'utf-8');
80
+ expect(content).toContain('# INIT-001');
81
+ expect(content).toContain('Initiative Plan');
82
+ });
83
+ });
84
+ describe('getPlanUri', () => {
85
+ it('should return lumenflow:// URI for plan', async () => {
86
+ const { getPlanUri } = await import('../plan-create.js');
87
+ expect(getPlanUri('WU-1313')).toBe('lumenflow://plans/WU-1313-plan.md');
88
+ expect(getPlanUri('INIT-001')).toBe('lumenflow://plans/INIT-001-plan.md');
89
+ });
90
+ });
91
+ describe('validatePlanId', () => {
92
+ it('should accept valid WU and INIT IDs', async () => {
93
+ const { validatePlanId } = await import('../plan-create.js');
94
+ expect(() => validatePlanId('WU-1313')).not.toThrow();
95
+ expect(() => validatePlanId('INIT-001')).not.toThrow();
96
+ expect(() => validatePlanId('INIT-TOOLING')).not.toThrow();
97
+ });
98
+ it('should reject invalid IDs', async () => {
99
+ const { validatePlanId } = await import('../plan-create.js');
100
+ expect(() => validatePlanId('invalid')).toThrow();
101
+ expect(() => validatePlanId('')).toThrow();
102
+ expect(() => validatePlanId('WU1313')).toThrow();
103
+ });
104
+ });
105
+ describe('getCommitMessage', () => {
106
+ it('should generate correct commit message', async () => {
107
+ const { getCommitMessage } = await import('../plan-create.js');
108
+ expect(getCommitMessage('WU-1313', 'Feature Plan')).toBe('docs: create plan for wu-1313 - Feature Plan');
109
+ expect(getCommitMessage('INIT-001', 'Auth System')).toBe('docs: create plan for init-001 - Auth System');
110
+ });
111
+ });
112
+ });
113
+ describe('plan:create CLI exports', () => {
114
+ it('should export main function for CLI entry', async () => {
115
+ const planCreate = await import('../plan-create.js');
116
+ expect(typeof planCreate.main).toBe('function');
117
+ });
118
+ it('should export all required functions', async () => {
119
+ const planCreate = await import('../plan-create.js');
120
+ expect(typeof planCreate.createPlan).toBe('function');
121
+ expect(typeof planCreate.getPlanUri).toBe('function');
122
+ expect(typeof planCreate.validatePlanId).toBe('function');
123
+ expect(typeof planCreate.getCommitMessage).toBe('function');
124
+ expect(typeof planCreate.LOG_PREFIX).toBe('string');
125
+ });
126
+ });
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Tests for plan:edit command (WU-1313)
3
+ *
4
+ * The plan:edit command edits existing plan files in the repo-native plansDir.
5
+ * Uses micro-worktree isolation for atomic commits.
6
+ *
7
+ * TDD: These tests are written BEFORE the implementation.
8
+ */
9
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
10
+ import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { tmpdir } from 'node:os';
13
+ /** Test constants - avoid sonarjs/no-duplicate-string */
14
+ const TEST_PLANS_DIR = 'docs/04-operations/plans';
15
+ const TEST_WU_ID = 'WU-1313';
16
+ // Mock modules before importing
17
+ vi.mock('@lumenflow/core/dist/git-adapter.js', () => ({
18
+ getGitForCwd: vi.fn(() => ({
19
+ branch: vi.fn().mockResolvedValue({ current: 'main' }),
20
+ status: vi.fn().mockResolvedValue({ isClean: () => true }),
21
+ })),
22
+ }));
23
+ vi.mock('@lumenflow/core/dist/wu-helpers.js', () => ({
24
+ ensureOnMain: vi.fn().mockResolvedValue(undefined),
25
+ }));
26
+ vi.mock('@lumenflow/core/dist/micro-worktree.js', () => ({
27
+ withMicroWorktree: vi.fn(async ({ execute }) => {
28
+ const tempDir = join(tmpdir(), `plan-edit-test-${Date.now()}`);
29
+ mkdirSync(tempDir, { recursive: true });
30
+ return execute({ worktreePath: tempDir });
31
+ }),
32
+ }));
33
+ describe('plan:edit command', () => {
34
+ let tempDir;
35
+ let originalCwd;
36
+ beforeEach(() => {
37
+ tempDir = join(tmpdir(), `plan-edit-test-${Date.now()}`);
38
+ mkdirSync(tempDir, { recursive: true });
39
+ originalCwd = process.cwd();
40
+ });
41
+ afterEach(() => {
42
+ process.chdir(originalCwd);
43
+ if (existsSync(tempDir)) {
44
+ rmSync(tempDir, { recursive: true, force: true });
45
+ }
46
+ vi.clearAllMocks();
47
+ });
48
+ describe('updatePlanSection', () => {
49
+ it('should update a section in the plan', async () => {
50
+ const { updatePlanSection } = await import('../plan-edit.js');
51
+ // Setup plan file
52
+ const plansDir = join(tempDir, ...TEST_PLANS_DIR.split('/'));
53
+ mkdirSync(plansDir, { recursive: true });
54
+ const planPath = join(plansDir, `${TEST_WU_ID}-plan.md`);
55
+ writeFileSync(planPath, `# WU-1313 Plan
56
+
57
+ ## Goal
58
+
59
+ Original goal content.
60
+
61
+ ## Scope
62
+
63
+ In scope items.
64
+ `);
65
+ // Update goal section
66
+ const changed = updatePlanSection(planPath, 'Goal', 'New goal content from edit.');
67
+ expect(changed).toBe(true);
68
+ const content = readFileSync(planPath, 'utf-8');
69
+ expect(content).toContain('New goal content from edit.');
70
+ expect(content).not.toContain('Original goal content.');
71
+ expect(content).toContain('In scope items.'); // Other sections unchanged
72
+ });
73
+ it('should return false if section not found', async () => {
74
+ const { updatePlanSection } = await import('../plan-edit.js');
75
+ // Setup plan file without the target section
76
+ const plansDir = join(tempDir, ...TEST_PLANS_DIR.split('/'));
77
+ mkdirSync(plansDir, { recursive: true });
78
+ const planPath = join(plansDir, `${TEST_WU_ID}-plan.md`);
79
+ writeFileSync(planPath, `# WU-1313 Plan
80
+
81
+ ## Goal
82
+
83
+ Goal content.
84
+ `);
85
+ // Try to update non-existent section
86
+ const changed = updatePlanSection(planPath, 'NonExistent', 'New content');
87
+ expect(changed).toBe(false);
88
+ });
89
+ it('should throw if plan file not found', async () => {
90
+ const { updatePlanSection } = await import('../plan-edit.js');
91
+ const planPath = join(tempDir, 'nonexistent.md');
92
+ expect(() => updatePlanSection(planPath, 'Goal', 'content')).toThrow();
93
+ });
94
+ });
95
+ describe('appendToSection', () => {
96
+ it('should append content to an existing section', async () => {
97
+ const { appendToSection } = await import('../plan-edit.js');
98
+ // Setup plan file
99
+ const plansDir = join(tempDir, ...TEST_PLANS_DIR.split('/'));
100
+ mkdirSync(plansDir, { recursive: true });
101
+ const planPath = join(plansDir, `${TEST_WU_ID}-plan.md`);
102
+ writeFileSync(planPath, `# WU-1313 Plan
103
+
104
+ ## Risks
105
+
106
+ - Risk 1
107
+
108
+ ## References
109
+ `);
110
+ // Append to risks section
111
+ const changed = appendToSection(planPath, 'Risks', '- Risk 2 from append');
112
+ expect(changed).toBe(true);
113
+ const content = readFileSync(planPath, 'utf-8');
114
+ expect(content).toContain('- Risk 1');
115
+ expect(content).toContain('- Risk 2 from append');
116
+ });
117
+ });
118
+ describe('getPlanPath', () => {
119
+ it('should resolve plan path from ID', async () => {
120
+ const { getPlanPath } = await import('../plan-edit.js');
121
+ // Setup plan file
122
+ const plansDir = join(tempDir, ...TEST_PLANS_DIR.split('/'));
123
+ mkdirSync(plansDir, { recursive: true });
124
+ const planPath = join(plansDir, `${TEST_WU_ID}-plan.md`);
125
+ writeFileSync(planPath, '# Plan');
126
+ process.chdir(tempDir);
127
+ const resolved = getPlanPath('WU-1313');
128
+ expect(resolved).toContain(`${TEST_WU_ID}-plan.md`);
129
+ });
130
+ it('should throw if plan not found', async () => {
131
+ const { getPlanPath } = await import('../plan-edit.js');
132
+ process.chdir(tempDir);
133
+ expect(() => getPlanPath('WU-9999')).toThrow();
134
+ });
135
+ });
136
+ describe('getCommitMessage', () => {
137
+ it('should generate correct commit message', async () => {
138
+ const { getCommitMessage } = await import('../plan-edit.js');
139
+ expect(getCommitMessage('WU-1313', 'Goal')).toBe('docs: update Goal section in wu-1313 plan');
140
+ expect(getCommitMessage('INIT-001', 'Scope')).toBe('docs: update Scope section in init-001 plan');
141
+ });
142
+ });
143
+ });
144
+ describe('plan:edit CLI exports', () => {
145
+ it('should export main function for CLI entry', async () => {
146
+ const planEdit = await import('../plan-edit.js');
147
+ expect(typeof planEdit.main).toBe('function');
148
+ });
149
+ it('should export all required functions', async () => {
150
+ const planEdit = await import('../plan-edit.js');
151
+ expect(typeof planEdit.updatePlanSection).toBe('function');
152
+ expect(typeof planEdit.appendToSection).toBe('function');
153
+ expect(typeof planEdit.getPlanPath).toBe('function');
154
+ expect(typeof planEdit.getCommitMessage).toBe('function');
155
+ expect(typeof planEdit.LOG_PREFIX).toBe('string');
156
+ });
157
+ });