@lumenflow/cli 2.3.2 → 2.5.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 (135) hide show
  1. package/dist/__tests__/init-config-lanes.test.js +131 -0
  2. package/dist/__tests__/init-docs-structure.test.js +119 -0
  3. package/dist/__tests__/init-lane-inference.test.js +125 -0
  4. package/dist/__tests__/init-onboarding-docs.test.js +132 -0
  5. package/dist/__tests__/init-quick-ref.test.js +145 -0
  6. package/dist/__tests__/init-scripts.test.js +96 -0
  7. package/dist/__tests__/init-template-portability.test.js +97 -0
  8. package/dist/__tests__/init.test.js +199 -3
  9. package/dist/__tests__/initiative-add-wu.test.js +420 -0
  10. package/dist/__tests__/initiative-plan-replacement.test.js +162 -0
  11. package/dist/__tests__/initiative-remove-wu.test.js +458 -0
  12. package/dist/__tests__/onboarding-smoke-test.test.js +211 -0
  13. package/dist/__tests__/path-centralization-cli.test.js +234 -0
  14. package/dist/__tests__/plan-create.test.js +126 -0
  15. package/dist/__tests__/plan-edit.test.js +157 -0
  16. package/dist/__tests__/plan-link.test.js +239 -0
  17. package/dist/__tests__/plan-promote.test.js +181 -0
  18. package/dist/__tests__/wu-create-strict.test.js +118 -0
  19. package/dist/__tests__/wu-edit-strict.test.js +109 -0
  20. package/dist/__tests__/wu-validate-strict.test.js +113 -0
  21. package/dist/flow-bottlenecks.js +4 -2
  22. package/dist/flow-report.js +3 -2
  23. package/dist/gates.js +202 -2
  24. package/dist/init.js +720 -40
  25. package/dist/initiative-add-wu.js +112 -16
  26. package/dist/initiative-plan.js +3 -2
  27. package/dist/initiative-remove-wu.js +248 -0
  28. package/dist/mem-context.js +0 -0
  29. package/dist/metrics-snapshot.js +3 -2
  30. package/dist/onboarding-smoke-test.js +400 -0
  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/rotate-progress.js +8 -5
  36. package/dist/spawn-list.js +4 -3
  37. package/dist/state-bootstrap.js +6 -4
  38. package/dist/state-doctor-fix.js +5 -4
  39. package/dist/state-doctor.js +32 -2
  40. package/dist/trace-gen.js +6 -3
  41. package/dist/wu-block.js +16 -5
  42. package/dist/wu-claim.js +15 -9
  43. package/dist/wu-create.js +50 -2
  44. package/dist/wu-deps.js +3 -1
  45. package/dist/wu-done.js +14 -5
  46. package/dist/wu-edit.js +35 -0
  47. package/dist/wu-infer-lane.js +3 -1
  48. package/dist/wu-spawn.js +8 -0
  49. package/dist/wu-unblock.js +34 -2
  50. package/dist/wu-validate.js +25 -17
  51. package/package.json +12 -6
  52. package/templates/core/AGENTS.md.template +2 -2
  53. package/dist/__tests__/init-plan.test.js +0 -340
  54. package/dist/agent-issues-query.d.ts +0 -16
  55. package/dist/agent-log-issue.d.ts +0 -10
  56. package/dist/agent-session-end.d.ts +0 -10
  57. package/dist/agent-session.d.ts +0 -10
  58. package/dist/backlog-prune.d.ts +0 -84
  59. package/dist/cli-entry-point.d.ts +0 -8
  60. package/dist/deps-add.d.ts +0 -91
  61. package/dist/deps-remove.d.ts +0 -17
  62. package/dist/docs-sync.d.ts +0 -50
  63. package/dist/file-delete.d.ts +0 -84
  64. package/dist/file-edit.d.ts +0 -82
  65. package/dist/file-read.d.ts +0 -92
  66. package/dist/file-write.d.ts +0 -90
  67. package/dist/flow-bottlenecks.d.ts +0 -16
  68. package/dist/flow-report.d.ts +0 -16
  69. package/dist/gates.d.ts +0 -94
  70. package/dist/git-branch.d.ts +0 -65
  71. package/dist/git-diff.d.ts +0 -58
  72. package/dist/git-log.d.ts +0 -69
  73. package/dist/git-status.d.ts +0 -58
  74. package/dist/guard-locked.d.ts +0 -62
  75. package/dist/guard-main-branch.d.ts +0 -50
  76. package/dist/guard-worktree-commit.d.ts +0 -59
  77. package/dist/index.d.ts +0 -10
  78. package/dist/init-plan.d.ts +0 -80
  79. package/dist/init-plan.js +0 -337
  80. package/dist/init.d.ts +0 -46
  81. package/dist/initiative-add-wu.d.ts +0 -22
  82. package/dist/initiative-bulk-assign-wus.d.ts +0 -16
  83. package/dist/initiative-create.d.ts +0 -28
  84. package/dist/initiative-edit.d.ts +0 -34
  85. package/dist/initiative-list.d.ts +0 -12
  86. package/dist/initiative-status.d.ts +0 -11
  87. package/dist/lumenflow-upgrade.d.ts +0 -103
  88. package/dist/mem-checkpoint.d.ts +0 -16
  89. package/dist/mem-cleanup.d.ts +0 -29
  90. package/dist/mem-create.d.ts +0 -17
  91. package/dist/mem-export.d.ts +0 -10
  92. package/dist/mem-inbox.d.ts +0 -35
  93. package/dist/mem-init.d.ts +0 -15
  94. package/dist/mem-ready.d.ts +0 -16
  95. package/dist/mem-signal.d.ts +0 -16
  96. package/dist/mem-start.d.ts +0 -16
  97. package/dist/mem-summarize.d.ts +0 -22
  98. package/dist/mem-triage.d.ts +0 -22
  99. package/dist/metrics-cli.d.ts +0 -90
  100. package/dist/metrics-snapshot.d.ts +0 -18
  101. package/dist/orchestrate-init-status.d.ts +0 -11
  102. package/dist/orchestrate-initiative.d.ts +0 -12
  103. package/dist/orchestrate-monitor.d.ts +0 -11
  104. package/dist/release.d.ts +0 -117
  105. package/dist/rotate-progress.d.ts +0 -48
  106. package/dist/session-coordinator.d.ts +0 -74
  107. package/dist/spawn-list.d.ts +0 -16
  108. package/dist/state-bootstrap.d.ts +0 -92
  109. package/dist/sync-templates.d.ts +0 -52
  110. package/dist/trace-gen.d.ts +0 -84
  111. package/dist/validate-agent-skills.d.ts +0 -50
  112. package/dist/validate-agent-sync.d.ts +0 -36
  113. package/dist/validate-backlog-sync.d.ts +0 -37
  114. package/dist/validate-skills-spec.d.ts +0 -40
  115. package/dist/validate.d.ts +0 -60
  116. package/dist/wu-block.d.ts +0 -16
  117. package/dist/wu-claim.d.ts +0 -74
  118. package/dist/wu-cleanup.d.ts +0 -35
  119. package/dist/wu-create.d.ts +0 -69
  120. package/dist/wu-delete.d.ts +0 -21
  121. package/dist/wu-deps.d.ts +0 -13
  122. package/dist/wu-done.d.ts +0 -225
  123. package/dist/wu-edit.d.ts +0 -63
  124. package/dist/wu-infer-lane.d.ts +0 -17
  125. package/dist/wu-preflight.d.ts +0 -47
  126. package/dist/wu-prune.d.ts +0 -16
  127. package/dist/wu-recover.d.ts +0 -37
  128. package/dist/wu-release.d.ts +0 -19
  129. package/dist/wu-repair.d.ts +0 -60
  130. package/dist/wu-spawn-completion.d.ts +0 -10
  131. package/dist/wu-spawn.d.ts +0 -192
  132. package/dist/wu-status.d.ts +0 -25
  133. package/dist/wu-unblock.d.ts +0 -16
  134. package/dist/wu-unlock-lane.d.ts +0 -19
  135. package/dist/wu-validate.d.ts +0 -16
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Tests for plan:link command (WU-1313)
3
+ *
4
+ * The plan:link command links existing plan files to WUs (via spec_refs)
5
+ * or initiatives (via related_plan). This replaces the initiative:plan command.
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
+ import { parseYAML, stringifyYAML } from '@lumenflow/core/dist/wu-yaml.js';
14
+ /** Test constants - avoid sonarjs/no-duplicate-string */
15
+ const TEST_WU_DIR = 'docs/04-operations/tasks/wu';
16
+ const TEST_INIT_DIR = 'docs/04-operations/tasks/initiatives';
17
+ const TEST_PLANS_DIR = 'docs/04-operations/plans';
18
+ const TEST_WU_ID = 'WU-1313';
19
+ const TEST_INIT_ID = 'INIT-001';
20
+ const TEST_WU_PLAN_URI = `lumenflow://plans/${TEST_WU_ID}-plan.md`;
21
+ const TEST_INIT_PLAN_URI = `lumenflow://plans/${TEST_INIT_ID}-plan.md`;
22
+ const TEST_LANE = 'Framework: CLI';
23
+ const TEST_INIT_SLUG = 'test-initiative';
24
+ const TEST_INIT_TITLE = 'Test Initiative';
25
+ const TEST_WU_TITLE = 'Test WU';
26
+ const TEST_STATUS_OPEN = 'open';
27
+ const TEST_DATE = '2026-01-25';
28
+ // Mock modules before importing
29
+ vi.mock('@lumenflow/core/dist/git-adapter.js', () => ({
30
+ getGitForCwd: vi.fn(() => ({
31
+ branch: vi.fn().mockResolvedValue({ current: 'main' }),
32
+ status: vi.fn().mockResolvedValue({ isClean: () => true }),
33
+ })),
34
+ }));
35
+ vi.mock('@lumenflow/core/dist/wu-helpers.js', () => ({
36
+ ensureOnMain: vi.fn().mockResolvedValue(undefined),
37
+ }));
38
+ vi.mock('@lumenflow/core/dist/micro-worktree.js', () => ({
39
+ withMicroWorktree: vi.fn(async ({ execute }) => {
40
+ const tempDir = join(tmpdir(), `plan-link-test-${Date.now()}`);
41
+ mkdirSync(tempDir, { recursive: true });
42
+ return execute({ worktreePath: tempDir });
43
+ }),
44
+ }));
45
+ describe('plan:link command', () => {
46
+ let tempDir;
47
+ let originalCwd;
48
+ beforeEach(() => {
49
+ tempDir = join(tmpdir(), `plan-link-test-${Date.now()}`);
50
+ mkdirSync(tempDir, { recursive: true });
51
+ originalCwd = process.cwd();
52
+ });
53
+ afterEach(() => {
54
+ process.chdir(originalCwd);
55
+ if (existsSync(tempDir)) {
56
+ rmSync(tempDir, { recursive: true, force: true });
57
+ }
58
+ vi.clearAllMocks();
59
+ });
60
+ describe('linkPlanToWU', () => {
61
+ it('should add spec_refs field to WU YAML', async () => {
62
+ const { linkPlanToWU } = await import('../plan-link.js');
63
+ // Setup mock WU file
64
+ const wuDir = join(tempDir, ...TEST_WU_DIR.split('/'));
65
+ mkdirSync(wuDir, { recursive: true });
66
+ const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
67
+ const wuDoc = {
68
+ id: TEST_WU_ID,
69
+ title: TEST_WU_TITLE,
70
+ lane: TEST_LANE,
71
+ status: 'ready',
72
+ type: 'feature',
73
+ };
74
+ writeFileSync(wuPath, stringifyYAML(wuDoc));
75
+ // Link plan
76
+ const changed = linkPlanToWU(tempDir, TEST_WU_ID, TEST_WU_PLAN_URI);
77
+ expect(changed).toBe(true);
78
+ // Verify the file was updated
79
+ const updated = parseYAML(readFileSync(wuPath, 'utf-8'));
80
+ expect(updated.spec_refs).toContain(TEST_WU_PLAN_URI);
81
+ });
82
+ it('should append to existing spec_refs', async () => {
83
+ const { linkPlanToWU } = await import('../plan-link.js');
84
+ // Setup mock WU file with existing spec_refs
85
+ const wuDir = join(tempDir, ...TEST_WU_DIR.split('/'));
86
+ mkdirSync(wuDir, { recursive: true });
87
+ const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
88
+ const wuDoc = {
89
+ id: TEST_WU_ID,
90
+ title: TEST_WU_TITLE,
91
+ lane: TEST_LANE,
92
+ status: 'ready',
93
+ type: 'feature',
94
+ spec_refs: ['lumenflow://plans/existing-plan.md'],
95
+ };
96
+ writeFileSync(wuPath, stringifyYAML(wuDoc));
97
+ // Link additional plan
98
+ const changed = linkPlanToWU(tempDir, TEST_WU_ID, TEST_WU_PLAN_URI);
99
+ expect(changed).toBe(true);
100
+ // Verify both refs are present
101
+ const updated = parseYAML(readFileSync(wuPath, 'utf-8'));
102
+ expect(updated.spec_refs).toContain('lumenflow://plans/existing-plan.md');
103
+ expect(updated.spec_refs).toContain(TEST_WU_PLAN_URI);
104
+ });
105
+ it('should be idempotent if plan already linked', async () => {
106
+ const { linkPlanToWU } = await import('../plan-link.js');
107
+ // Setup mock WU file with plan already linked
108
+ const wuDir = join(tempDir, ...TEST_WU_DIR.split('/'));
109
+ mkdirSync(wuDir, { recursive: true });
110
+ const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
111
+ const wuDoc = {
112
+ id: TEST_WU_ID,
113
+ title: TEST_WU_TITLE,
114
+ lane: TEST_LANE,
115
+ status: 'ready',
116
+ type: 'feature',
117
+ spec_refs: [TEST_WU_PLAN_URI],
118
+ };
119
+ writeFileSync(wuPath, stringifyYAML(wuDoc));
120
+ // Link same plan again
121
+ const changed = linkPlanToWU(tempDir, TEST_WU_ID, TEST_WU_PLAN_URI);
122
+ expect(changed).toBe(false);
123
+ });
124
+ it('should throw if WU not found', async () => {
125
+ const { linkPlanToWU } = await import('../plan-link.js');
126
+ expect(() => linkPlanToWU(tempDir, 'WU-9999', 'lumenflow://plans/plan.md')).toThrow();
127
+ });
128
+ });
129
+ describe('linkPlanToInitiative', () => {
130
+ it('should add related_plan field to initiative YAML', async () => {
131
+ const { linkPlanToInitiative } = await import('../plan-link.js');
132
+ // Setup mock initiative file
133
+ const initDir = join(tempDir, ...TEST_INIT_DIR.split('/'));
134
+ mkdirSync(initDir, { recursive: true });
135
+ const initPath = join(initDir, `${TEST_INIT_ID}.yaml`);
136
+ const initDoc = {
137
+ id: TEST_INIT_ID,
138
+ slug: TEST_INIT_SLUG,
139
+ title: TEST_INIT_TITLE,
140
+ status: TEST_STATUS_OPEN,
141
+ created: TEST_DATE,
142
+ };
143
+ writeFileSync(initPath, stringifyYAML(initDoc));
144
+ // Link plan
145
+ const changed = linkPlanToInitiative(tempDir, TEST_INIT_ID, TEST_INIT_PLAN_URI);
146
+ expect(changed).toBe(true);
147
+ // Verify the file was updated
148
+ const updated = parseYAML(readFileSync(initPath, 'utf-8'));
149
+ expect(updated.related_plan).toBe(TEST_INIT_PLAN_URI);
150
+ });
151
+ it('should be idempotent if plan already linked', async () => {
152
+ const { linkPlanToInitiative } = await import('../plan-link.js');
153
+ // Setup mock initiative file with plan already linked
154
+ const initDir = join(tempDir, ...TEST_INIT_DIR.split('/'));
155
+ mkdirSync(initDir, { recursive: true });
156
+ const initPath = join(initDir, `${TEST_INIT_ID}.yaml`);
157
+ const initDoc = {
158
+ id: TEST_INIT_ID,
159
+ slug: TEST_INIT_SLUG,
160
+ title: TEST_INIT_TITLE,
161
+ status: TEST_STATUS_OPEN,
162
+ created: TEST_DATE,
163
+ related_plan: TEST_INIT_PLAN_URI,
164
+ };
165
+ writeFileSync(initPath, stringifyYAML(initDoc));
166
+ // Link same plan again
167
+ const changed = linkPlanToInitiative(tempDir, TEST_INIT_ID, TEST_INIT_PLAN_URI);
168
+ expect(changed).toBe(false);
169
+ });
170
+ it('should warn but proceed if replacing existing plan', async () => {
171
+ const { linkPlanToInitiative } = await import('../plan-link.js');
172
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
173
+ // Setup mock initiative with different plan
174
+ const initDir = join(tempDir, ...TEST_INIT_DIR.split('/'));
175
+ mkdirSync(initDir, { recursive: true });
176
+ const initPath = join(initDir, `${TEST_INIT_ID}.yaml`);
177
+ const initDoc = {
178
+ id: TEST_INIT_ID,
179
+ slug: TEST_INIT_SLUG,
180
+ title: TEST_INIT_TITLE,
181
+ status: TEST_STATUS_OPEN,
182
+ created: TEST_DATE,
183
+ related_plan: 'lumenflow://plans/old-plan.md',
184
+ };
185
+ writeFileSync(initPath, stringifyYAML(initDoc));
186
+ // Link new plan
187
+ const changed = linkPlanToInitiative(tempDir, TEST_INIT_ID, 'lumenflow://plans/new-plan.md');
188
+ expect(changed).toBe(true);
189
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Replacing existing'));
190
+ consoleSpy.mockRestore();
191
+ });
192
+ });
193
+ describe('validatePlanExists', () => {
194
+ it('should pass for existing plan file', async () => {
195
+ const { validatePlanExists } = await import('../plan-link.js');
196
+ const plansDir = join(tempDir, ...TEST_PLANS_DIR.split('/'));
197
+ mkdirSync(plansDir, { recursive: true });
198
+ const planPath = join(plansDir, `${TEST_WU_ID}-plan.md`);
199
+ writeFileSync(planPath, '# Plan');
200
+ expect(() => validatePlanExists(tempDir, TEST_WU_PLAN_URI)).not.toThrow();
201
+ });
202
+ it('should throw for non-existent plan file', async () => {
203
+ const { validatePlanExists } = await import('../plan-link.js');
204
+ expect(() => validatePlanExists(tempDir, 'lumenflow://plans/nonexistent.md')).toThrow();
205
+ });
206
+ });
207
+ describe('resolveTargetType', () => {
208
+ it('should detect WU IDs', async () => {
209
+ const { resolveTargetType } = await import('../plan-link.js');
210
+ expect(resolveTargetType(TEST_WU_ID)).toBe('wu');
211
+ expect(resolveTargetType('WU-001')).toBe('wu');
212
+ expect(resolveTargetType('WU-99999')).toBe('wu');
213
+ });
214
+ it('should detect initiative IDs', async () => {
215
+ const { resolveTargetType } = await import('../plan-link.js');
216
+ expect(resolveTargetType(TEST_INIT_ID)).toBe('initiative');
217
+ expect(resolveTargetType('INIT-TOOLING')).toBe('initiative');
218
+ });
219
+ it('should throw for invalid IDs', async () => {
220
+ const { resolveTargetType } = await import('../plan-link.js');
221
+ expect(() => resolveTargetType('invalid')).toThrow();
222
+ expect(() => resolveTargetType('')).toThrow();
223
+ });
224
+ });
225
+ });
226
+ describe('plan:link CLI exports', () => {
227
+ it('should export main function for CLI entry', async () => {
228
+ const planLink = await import('../plan-link.js');
229
+ expect(typeof planLink.main).toBe('function');
230
+ });
231
+ it('should export all required functions', async () => {
232
+ const planLink = await import('../plan-link.js');
233
+ expect(typeof planLink.linkPlanToWU).toBe('function');
234
+ expect(typeof planLink.linkPlanToInitiative).toBe('function');
235
+ expect(typeof planLink.validatePlanExists).toBe('function');
236
+ expect(typeof planLink.resolveTargetType).toBe('function');
237
+ expect(typeof planLink.LOG_PREFIX).toBe('string');
238
+ });
239
+ });
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Tests for plan:promote command (WU-1313)
3
+ *
4
+ * The plan:promote command promotes a plan from draft to approved status,
5
+ * or creates WUs from plan sections.
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-promote-test-${Date.now()}`);
29
+ mkdirSync(tempDir, { recursive: true });
30
+ return execute({ worktreePath: tempDir });
31
+ }),
32
+ }));
33
+ describe('plan:promote command', () => {
34
+ let tempDir;
35
+ let originalCwd;
36
+ beforeEach(() => {
37
+ tempDir = join(tmpdir(), `plan-promote-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('promotePlan', () => {
49
+ it('should add approved status marker to plan', async () => {
50
+ const { promotePlan } = await import('../plan-promote.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
+ Created: 2026-02-01
58
+
59
+ ## Goal
60
+
61
+ Implement plan tooling.
62
+ `);
63
+ // Promote plan
64
+ const changed = promotePlan(planPath);
65
+ expect(changed).toBe(true);
66
+ const content = readFileSync(planPath, 'utf-8');
67
+ expect(content).toContain('Status: approved');
68
+ expect(content).toContain('Approved:');
69
+ });
70
+ it('should return false if plan already approved', async () => {
71
+ const { promotePlan } = await import('../plan-promote.js');
72
+ // Setup plan file with approved status
73
+ const plansDir = join(tempDir, ...TEST_PLANS_DIR.split('/'));
74
+ mkdirSync(plansDir, { recursive: true });
75
+ const planPath = join(plansDir, `${TEST_WU_ID}-plan.md`);
76
+ writeFileSync(planPath, `# WU-1313 Plan
77
+
78
+ Created: 2026-02-01
79
+ Status: approved
80
+ Approved: 2026-02-01
81
+
82
+ ## Goal
83
+
84
+ Implement plan tooling.
85
+ `);
86
+ // Try to promote again
87
+ const changed = promotePlan(planPath);
88
+ expect(changed).toBe(false);
89
+ });
90
+ it('should throw if plan file not found', async () => {
91
+ const { promotePlan } = await import('../plan-promote.js');
92
+ const planPath = join(tempDir, 'nonexistent.md');
93
+ expect(() => promotePlan(planPath)).toThrow();
94
+ });
95
+ });
96
+ describe('validatePlanComplete', () => {
97
+ it('should pass for complete plan', async () => {
98
+ const { validatePlanComplete } = await import('../plan-promote.js');
99
+ // Setup complete plan file
100
+ const plansDir = join(tempDir, ...TEST_PLANS_DIR.split('/'));
101
+ mkdirSync(plansDir, { recursive: true });
102
+ const planPath = join(plansDir, `${TEST_WU_ID}-plan.md`);
103
+ writeFileSync(planPath, `# WU-1313 Plan
104
+
105
+ Created: 2026-02-01
106
+
107
+ ## Goal
108
+
109
+ Clear goal statement here.
110
+
111
+ ## Scope
112
+
113
+ - In scope: A
114
+ - Out of scope: B
115
+
116
+ ## Approach
117
+
118
+ Step 1: Do X
119
+ Step 2: Do Y
120
+ `);
121
+ const result = validatePlanComplete(planPath);
122
+ expect(result.valid).toBe(true);
123
+ expect(result.errors).toHaveLength(0);
124
+ });
125
+ it('should fail for plan with empty sections', async () => {
126
+ const { validatePlanComplete } = await import('../plan-promote.js');
127
+ // Setup incomplete plan file
128
+ const plansDir = join(tempDir, ...TEST_PLANS_DIR.split('/'));
129
+ mkdirSync(plansDir, { recursive: true });
130
+ const planPath = join(plansDir, `${TEST_WU_ID}-plan.md`);
131
+ writeFileSync(planPath, `# WU-1313 Plan
132
+
133
+ Created: 2026-02-01
134
+
135
+ ## Goal
136
+
137
+ ## Scope
138
+
139
+ ## Approach
140
+ `);
141
+ const result = validatePlanComplete(planPath);
142
+ expect(result.valid).toBe(false);
143
+ expect(result.errors.length).toBeGreaterThan(0);
144
+ expect(result.errors.some((e) => e.includes('Goal'))).toBe(true);
145
+ });
146
+ });
147
+ describe('getPlanPath', () => {
148
+ it('should resolve plan path from ID', async () => {
149
+ const { getPlanPath } = await import('../plan-promote.js');
150
+ // Setup plan file
151
+ const plansDir = join(tempDir, ...TEST_PLANS_DIR.split('/'));
152
+ mkdirSync(plansDir, { recursive: true });
153
+ const planPath = join(plansDir, `${TEST_WU_ID}-plan.md`);
154
+ writeFileSync(planPath, '# Plan');
155
+ process.chdir(tempDir);
156
+ const resolved = getPlanPath('WU-1313');
157
+ expect(resolved).toContain(`${TEST_WU_ID}-plan.md`);
158
+ });
159
+ });
160
+ describe('getCommitMessage', () => {
161
+ it('should generate correct commit message', async () => {
162
+ const { getCommitMessage } = await import('../plan-promote.js');
163
+ expect(getCommitMessage('WU-1313')).toBe('docs: promote wu-1313 plan to approved');
164
+ expect(getCommitMessage('INIT-001')).toBe('docs: promote init-001 plan to approved');
165
+ });
166
+ });
167
+ });
168
+ describe('plan:promote CLI exports', () => {
169
+ it('should export main function for CLI entry', async () => {
170
+ const planPromote = await import('../plan-promote.js');
171
+ expect(typeof planPromote.main).toBe('function');
172
+ });
173
+ it('should export all required functions', async () => {
174
+ const planPromote = await import('../plan-promote.js');
175
+ expect(typeof planPromote.promotePlan).toBe('function');
176
+ expect(typeof planPromote.validatePlanComplete).toBe('function');
177
+ expect(typeof planPromote.getPlanPath).toBe('function');
178
+ expect(typeof planPromote.getCommitMessage).toBe('function');
179
+ expect(typeof planPromote.LOG_PREFIX).toBe('string');
180
+ });
181
+ });
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @file wu-create-strict.test.ts
3
+ * Test suite for wu:create strict validation behavior (WU-1329)
4
+ *
5
+ * WU-1329: Make wu:create run strict validation by default
6
+ *
7
+ * Tests:
8
+ * - Strict validation runs by default (validates code_paths/test_paths exist)
9
+ * - --no-strict flag bypasses strict validation
10
+ * - --no-strict usage is logged
11
+ */
12
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
13
+ // Import test utilities (WU-1329)
14
+ import { validateCreateSpec } from '../wu-create.js';
15
+ // WU-1329: Constants for test file paths
16
+ const NON_EXISTENT_CODE_PATH = 'non-existent/file.ts';
17
+ const NON_EXISTENT_TEST_PATH = 'non-existent/test.test.ts';
18
+ describe('wu:create strict validation (WU-1329)', () => {
19
+ describe('validateCreateSpec strict mode', () => {
20
+ const baseOpts = {
21
+ id: 'WU-9999',
22
+ lane: 'Framework: CLI',
23
+ title: 'Test WU',
24
+ priority: 'P2',
25
+ type: 'feature',
26
+ opts: {
27
+ description: 'Context: test context.\nProblem: test problem.\nSolution: test solution that exceeds minimum length requirement.',
28
+ acceptance: ['Acceptance criterion 1'],
29
+ codePaths: ['packages/@lumenflow/cli/src/wu-create.ts'],
30
+ testPathsUnit: ['packages/@lumenflow/cli/src/__tests__/wu-create-strict.test.ts'],
31
+ exposure: 'backend-only',
32
+ specRefs: ['lumenflow://plans/WU-9999-plan.md'],
33
+ },
34
+ };
35
+ it('should pass validation with valid spec in non-strict mode', () => {
36
+ const result = validateCreateSpec({
37
+ ...baseOpts,
38
+ opts: {
39
+ ...baseOpts.opts,
40
+ strict: false,
41
+ },
42
+ });
43
+ expect(result.valid).toBe(true);
44
+ expect(result.errors).toHaveLength(0);
45
+ });
46
+ // WU-1329: This test verifies that strict validation is the default
47
+ it('should validate code_paths existence by default (strict=true)', () => {
48
+ const result = validateCreateSpec({
49
+ ...baseOpts,
50
+ opts: {
51
+ ...baseOpts.opts,
52
+ codePaths: [NON_EXISTENT_CODE_PATH],
53
+ // strict is not explicitly set - should default to true
54
+ },
55
+ });
56
+ // In strict mode, non-existent paths should be caught
57
+ expect(result.valid).toBe(false);
58
+ expect(result.errors.some((e) => e.includes('code_paths') || e.includes('not found'))).toBe(true);
59
+ });
60
+ // WU-1329: This test verifies --no-strict bypasses path validation
61
+ it('should skip code_paths existence check when strict=false', () => {
62
+ const result = validateCreateSpec({
63
+ ...baseOpts,
64
+ opts: {
65
+ ...baseOpts.opts,
66
+ codePaths: [NON_EXISTENT_CODE_PATH],
67
+ strict: false,
68
+ },
69
+ });
70
+ // In non-strict mode, non-existent paths should not fail validation
71
+ // (other schema validation may still fail)
72
+ expect(result.errors.every((e) => !e.includes('not found') && !e.includes('does not exist'))).toBe(true);
73
+ });
74
+ // WU-1329: This test verifies test_paths validation in strict mode
75
+ it('should validate test_paths existence by default (strict=true)', () => {
76
+ const result = validateCreateSpec({
77
+ ...baseOpts,
78
+ opts: {
79
+ ...baseOpts.opts,
80
+ testPathsUnit: [NON_EXISTENT_TEST_PATH],
81
+ },
82
+ });
83
+ // In strict mode, non-existent test paths should be caught
84
+ expect(result.valid).toBe(false);
85
+ expect(result.errors.some((e) => e.includes('test') || e.includes('not found'))).toBe(true);
86
+ });
87
+ });
88
+ describe('--no-strict logging', () => {
89
+ let consoleSpy;
90
+ beforeEach(() => {
91
+ consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
92
+ });
93
+ afterEach(() => {
94
+ consoleSpy.mockRestore();
95
+ });
96
+ // WU-1329: This test verifies --no-strict usage is logged
97
+ it('should log warning when --no-strict is used', () => {
98
+ validateCreateSpec({
99
+ id: 'WU-9999',
100
+ lane: 'Framework: CLI',
101
+ title: 'Test WU',
102
+ priority: 'P2',
103
+ type: 'feature',
104
+ opts: {
105
+ description: 'Context: test context.\nProblem: test problem.\nSolution: test solution that exceeds minimum.',
106
+ acceptance: ['Acceptance criterion'],
107
+ codePaths: [NON_EXISTENT_CODE_PATH],
108
+ testPathsUnit: [NON_EXISTENT_TEST_PATH],
109
+ exposure: 'backend-only',
110
+ specRefs: ['lumenflow://plans/WU-9999-plan.md'],
111
+ strict: false,
112
+ },
113
+ });
114
+ // Should log that strict validation was bypassed
115
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('strict validation bypassed'));
116
+ });
117
+ });
118
+ });
@@ -0,0 +1,109 @@
1
+ /**
2
+ * @file wu-edit-strict.test.ts
3
+ * Test suite for wu:edit strict validation behavior (WU-1329)
4
+ *
5
+ * WU-1329: Make wu:edit run strict validation by default
6
+ *
7
+ * Tests:
8
+ * - applyEdits function works correctly (unit test for edit logic)
9
+ * - validateDoneWUEdits properly validates done WU edits
10
+ * - The main() function validates paths in strict mode (covered by e2e tests)
11
+ */
12
+ import { describe, it, expect } from 'vitest';
13
+ // Import test utilities (WU-1329)
14
+ import { applyEdits, validateDoneWUEdits, validateExposureValue } from '../wu-edit.js';
15
+ // WU-1329: Constants for test file paths
16
+ const TEST_NEW_FILE_PATH = 'new/file.ts';
17
+ const TEST_NEW_TEST_PATH = 'new/test.test.ts';
18
+ describe('wu:edit strict validation (WU-1329)', () => {
19
+ describe('applyEdits code_paths handling', () => {
20
+ const baseWU = {
21
+ id: 'WU-9999',
22
+ title: 'Test WU',
23
+ lane: 'Framework: CLI',
24
+ type: 'feature',
25
+ status: 'ready',
26
+ priority: 'P2',
27
+ description: 'Context: test context.\nProblem: test problem.\nSolution: test solution that exceeds minimum length.',
28
+ acceptance: ['Existing criterion'],
29
+ code_paths: ['packages/@lumenflow/cli/src/wu-edit.ts'],
30
+ tests: {
31
+ unit: ['packages/@lumenflow/cli/src/__tests__/wu-edit-strict.test.ts'],
32
+ manual: [],
33
+ e2e: [],
34
+ },
35
+ };
36
+ // WU-1329: applyEdits transforms WU object - path validation is done separately
37
+ it('should apply code_paths edits correctly', () => {
38
+ const result = applyEdits(baseWU, {
39
+ codePaths: [TEST_NEW_FILE_PATH],
40
+ replaceCodePaths: true,
41
+ });
42
+ // applyEdits transforms the WU, validation happens later in main()
43
+ expect(result.code_paths).toContain(TEST_NEW_FILE_PATH);
44
+ expect(result.code_paths).toHaveLength(1);
45
+ });
46
+ it('should append code_paths by default', () => {
47
+ const result = applyEdits(baseWU, {
48
+ codePaths: [TEST_NEW_FILE_PATH],
49
+ });
50
+ expect(result.code_paths).toContain('packages/@lumenflow/cli/src/wu-edit.ts');
51
+ expect(result.code_paths).toContain(TEST_NEW_FILE_PATH);
52
+ expect(result.code_paths).toHaveLength(2);
53
+ });
54
+ it('should apply test_paths edits correctly', () => {
55
+ const result = applyEdits(baseWU, {
56
+ testPathsUnit: [TEST_NEW_TEST_PATH],
57
+ });
58
+ // Should append test paths
59
+ expect(result.tests.unit).toContain('packages/@lumenflow/cli/src/__tests__/wu-edit-strict.test.ts');
60
+ expect(result.tests.unit).toContain(TEST_NEW_TEST_PATH);
61
+ });
62
+ });
63
+ describe('validateDoneWUEdits', () => {
64
+ // WU-1329: Done WUs only allow initiative/phase/exposure edits
65
+ it('should allow exposure edits on done WUs', () => {
66
+ const result = validateDoneWUEdits({ exposure: 'backend-only' });
67
+ expect(result.valid).toBe(true);
68
+ expect(result.disallowedEdits).toHaveLength(0);
69
+ });
70
+ it('should disallow code_paths edits on done WUs', () => {
71
+ const result = validateDoneWUEdits({ codePaths: [TEST_NEW_FILE_PATH] });
72
+ expect(result.valid).toBe(false);
73
+ expect(result.disallowedEdits).toContain('--code-paths');
74
+ });
75
+ it('should disallow description edits on done WUs', () => {
76
+ const result = validateDoneWUEdits({ description: 'new description' });
77
+ expect(result.valid).toBe(false);
78
+ expect(result.disallowedEdits).toContain('--description');
79
+ });
80
+ });
81
+ describe('validateExposureValue', () => {
82
+ it('should accept valid exposure values', () => {
83
+ expect(validateExposureValue('ui').valid).toBe(true);
84
+ expect(validateExposureValue('api').valid).toBe(true);
85
+ expect(validateExposureValue('backend-only').valid).toBe(true);
86
+ expect(validateExposureValue('documentation').valid).toBe(true);
87
+ });
88
+ it('should reject invalid exposure values', () => {
89
+ const result = validateExposureValue('invalid-exposure');
90
+ expect(result.valid).toBe(false);
91
+ expect(result.error).toContain('Invalid exposure value');
92
+ });
93
+ });
94
+ describe('noStrict option support', () => {
95
+ // WU-1329: Verify CLI option parsing pattern
96
+ it('should support noStrict option in CLI argument pattern', () => {
97
+ // CLI uses --no-strict which Commander.js parses as noStrict
98
+ // The main() function converts this to strict: !noStrict
99
+ const cliArgs = { noStrict: true };
100
+ const strict = !cliArgs.noStrict;
101
+ expect(strict).toBe(false);
102
+ });
103
+ it('should default to strict=true when noStrict is undefined', () => {
104
+ const cliArgs = { noStrict: undefined };
105
+ const strict = !cliArgs.noStrict;
106
+ expect(strict).toBe(true);
107
+ });
108
+ });
109
+ });