@lumenflow/cli 2.20.1 → 2.21.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 (111) hide show
  1. package/README.md +8 -4
  2. package/dist/hooks/enforcement-checks.js +120 -0
  3. package/dist/hooks/enforcement-checks.js.map +1 -1
  4. package/dist/init-lane-validation.js +141 -0
  5. package/dist/init-lane-validation.js.map +1 -0
  6. package/dist/init-templates.js +36 -8
  7. package/dist/init-templates.js.map +1 -1
  8. package/dist/init.js +27 -58
  9. package/dist/init.js.map +1 -1
  10. package/dist/initiative-create.js +35 -4
  11. package/dist/initiative-create.js.map +1 -1
  12. package/dist/lane-lifecycle-process.js +364 -0
  13. package/dist/lane-lifecycle-process.js.map +1 -0
  14. package/dist/lane-lock.js +41 -0
  15. package/dist/lane-lock.js.map +1 -0
  16. package/dist/lane-setup.js +55 -0
  17. package/dist/lane-setup.js.map +1 -0
  18. package/dist/lane-status.js +38 -0
  19. package/dist/lane-status.js.map +1 -0
  20. package/dist/lane-validate.js +43 -0
  21. package/dist/lane-validate.js.map +1 -0
  22. package/dist/onboarding-smoke-test.js +17 -0
  23. package/dist/onboarding-smoke-test.js.map +1 -1
  24. package/dist/public-manifest.js +28 -0
  25. package/dist/public-manifest.js.map +1 -1
  26. package/dist/wu-claim-cloud.js +16 -0
  27. package/dist/wu-claim-cloud.js.map +1 -1
  28. package/dist/wu-claim.js +12 -2
  29. package/dist/wu-claim.js.map +1 -1
  30. package/dist/wu-create-content.js +8 -2
  31. package/dist/wu-create-content.js.map +1 -1
  32. package/dist/wu-create-validation.js +5 -3
  33. package/dist/wu-create-validation.js.map +1 -1
  34. package/dist/wu-create.js +21 -1
  35. package/dist/wu-create.js.map +1 -1
  36. package/dist/wu-done.js +57 -8
  37. package/dist/wu-done.js.map +1 -1
  38. package/dist/wu-prep.js +22 -0
  39. package/dist/wu-prep.js.map +1 -1
  40. package/package.json +15 -11
  41. package/dist/__tests__/agent-log-issue.test.js +0 -56
  42. package/dist/__tests__/agent-spawn-coordination.test.js +0 -451
  43. package/dist/__tests__/backlog-prune.test.js +0 -478
  44. package/dist/__tests__/cli-entry-point.test.js +0 -160
  45. package/dist/__tests__/cli-subprocess.test.js +0 -89
  46. package/dist/__tests__/commands/integrate.test.js +0 -165
  47. package/dist/__tests__/commands.test.js +0 -271
  48. package/dist/__tests__/deps-operations.test.js +0 -206
  49. package/dist/__tests__/doctor.test.js +0 -510
  50. package/dist/__tests__/file-operations.test.js +0 -906
  51. package/dist/__tests__/flow-report.test.js +0 -24
  52. package/dist/__tests__/gates-config.test.js +0 -303
  53. package/dist/__tests__/gates-integration-tests.test.js +0 -112
  54. package/dist/__tests__/git-operations.test.js +0 -668
  55. package/dist/__tests__/guard-main-branch.test.js +0 -79
  56. package/dist/__tests__/guards-validation.test.js +0 -416
  57. package/dist/__tests__/hooks/enforcement.test.js +0 -279
  58. package/dist/__tests__/init-config-lanes.test.js +0 -131
  59. package/dist/__tests__/init-docs-structure.test.js +0 -152
  60. package/dist/__tests__/init-greenfield.test.js +0 -247
  61. package/dist/__tests__/init-lane-inference.test.js +0 -125
  62. package/dist/__tests__/init-onboarding-docs.test.js +0 -132
  63. package/dist/__tests__/init-quick-ref.test.js +0 -144
  64. package/dist/__tests__/init-scripts.test.js +0 -207
  65. package/dist/__tests__/init-template-portability.test.js +0 -96
  66. package/dist/__tests__/init.test.js +0 -968
  67. package/dist/__tests__/initiative-add-wu.test.js +0 -490
  68. package/dist/__tests__/initiative-e2e.test.js +0 -442
  69. package/dist/__tests__/initiative-plan-replacement.test.js +0 -161
  70. package/dist/__tests__/initiative-plan.test.js +0 -340
  71. package/dist/__tests__/initiative-remove-wu.test.js +0 -458
  72. package/dist/__tests__/lumenflow-upgrade.test.js +0 -260
  73. package/dist/__tests__/mem-cleanup-execution.test.js +0 -19
  74. package/dist/__tests__/memory-integration.test.js +0 -333
  75. package/dist/__tests__/merge-block.test.js +0 -220
  76. package/dist/__tests__/metrics-cli.test.js +0 -619
  77. package/dist/__tests__/metrics-snapshot.test.js +0 -24
  78. package/dist/__tests__/no-beacon-references-docs.test.js +0 -30
  79. package/dist/__tests__/no-beacon-references.test.js +0 -39
  80. package/dist/__tests__/onboarding-smoke-test.test.js +0 -211
  81. package/dist/__tests__/path-centralization-cli.test.js +0 -234
  82. package/dist/__tests__/plan-create.test.js +0 -126
  83. package/dist/__tests__/plan-edit.test.js +0 -157
  84. package/dist/__tests__/plan-link.test.js +0 -239
  85. package/dist/__tests__/plan-promote.test.js +0 -181
  86. package/dist/__tests__/release.test.js +0 -372
  87. package/dist/__tests__/rotate-progress.test.js +0 -127
  88. package/dist/__tests__/safe-git.test.js +0 -190
  89. package/dist/__tests__/session-coordinator.test.js +0 -109
  90. package/dist/__tests__/state-bootstrap.test.js +0 -432
  91. package/dist/__tests__/state-doctor.test.js +0 -328
  92. package/dist/__tests__/sync-templates.test.js +0 -255
  93. package/dist/__tests__/templates-sync.test.js +0 -219
  94. package/dist/__tests__/trace-gen.test.js +0 -115
  95. package/dist/__tests__/wu-create-required-fields.test.js +0 -143
  96. package/dist/__tests__/wu-create-strict.test.js +0 -118
  97. package/dist/__tests__/wu-create.test.js +0 -121
  98. package/dist/__tests__/wu-done-auto-cleanup.test.js +0 -135
  99. package/dist/__tests__/wu-done-docs-only-policy.test.js +0 -20
  100. package/dist/__tests__/wu-done-staging-whitelist.test.js +0 -35
  101. package/dist/__tests__/wu-done.test.js +0 -36
  102. package/dist/__tests__/wu-edit-strict.test.js +0 -109
  103. package/dist/__tests__/wu-edit.test.js +0 -119
  104. package/dist/__tests__/wu-lifecycle-integration.test.js +0 -388
  105. package/dist/__tests__/wu-prep-default-exec.test.js +0 -35
  106. package/dist/__tests__/wu-prep.test.js +0 -140
  107. package/dist/__tests__/wu-proto.test.js +0 -97
  108. package/dist/__tests__/wu-validate-strict.test.js +0 -113
  109. package/dist/__tests__/wu-validate.test.js +0 -36
  110. package/dist/spawn-list.js +0 -143
  111. package/dist/spawn-list.js.map +0 -1
@@ -1,490 +0,0 @@
1
- /**
2
- * Tests for initiative:add-wu command validation (WU-1330)
3
- *
4
- * The initiative:add-wu command now validates WU specs before linking.
5
- * This ensures only valid, complete WUs can be linked to initiatives.
6
- *
7
- * TDD: These tests are written BEFORE the implementation.
8
- */
9
- import { describe, it, expect, vi, beforeEach, afterEach, beforeAll } from 'vitest';
10
- import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
11
- import { join } from 'node:path';
12
- import { tmpdir } from 'node:os';
13
- import { stringifyYAML, readWU } from '@lumenflow/core/dist/wu-yaml.js';
14
- import { readInitiative } from '@lumenflow/initiatives/dist/initiative-yaml.js';
15
- // Test constants to avoid lint warnings about duplicate strings
16
- const TEST_WU_ID = 'WU-123';
17
- const TEST_INIT_ID = 'INIT-001';
18
- const TEST_LANE = 'Framework: CLI';
19
- const WU_REL_PATH = 'docs/04-operations/tasks/wu';
20
- const INIT_REL_PATH = 'docs/04-operations/tasks/initiatives';
21
- const TEST_INIT_SLUG = 'test-initiative';
22
- const TEST_INIT_TITLE = 'Test Initiative';
23
- const TEST_INIT_STATUS = 'open';
24
- const TEST_DATE = '2026-01-25';
25
- const MIN_DESCRIPTION_LENGTH = 50;
26
- const TEST_WU_ID_2 = 'WU-124';
27
- // Valid WU document template
28
- const createValidWUDoc = (overrides = {}) => ({
29
- id: TEST_WU_ID,
30
- title: 'Test Work Unit Title',
31
- lane: TEST_LANE,
32
- status: 'ready',
33
- type: 'feature',
34
- priority: 'P2',
35
- created: TEST_DATE,
36
- description: 'Context: Testing WU validation. Problem: No validation on add-wu. Solution: Add strict validation before linking.',
37
- acceptance: ['WU validates schema', 'Invalid WUs rejected', 'Valid WUs linked bidirectionally'],
38
- code_paths: ['packages/@lumenflow/cli/src/initiative-add-wu.ts'],
39
- tests: { unit: ['packages/@lumenflow/cli/src/__tests__/initiative-add-wu.test.ts'] },
40
- exposure: 'backend-only',
41
- ...overrides,
42
- });
43
- // Valid initiative document template
44
- const createValidInitDoc = (overrides = {}) => ({
45
- id: TEST_INIT_ID,
46
- slug: TEST_INIT_SLUG,
47
- title: TEST_INIT_TITLE,
48
- status: TEST_INIT_STATUS,
49
- created: TEST_DATE,
50
- wus: [],
51
- ...overrides,
52
- });
53
- // Pre-import the module to ensure coverage tracking includes the module itself
54
- beforeAll(async () => {
55
- await import('../initiative-add-wu.js');
56
- });
57
- // Mock modules before importing the module under test
58
- const mockGit = {
59
- branch: vi.fn().mockResolvedValue({ current: 'main' }),
60
- status: vi.fn().mockResolvedValue({ isClean: () => true }),
61
- };
62
- vi.mock('@lumenflow/core/dist/git-adapter.js', () => ({
63
- getGitForCwd: vi.fn(() => mockGit),
64
- }));
65
- vi.mock('@lumenflow/core/dist/wu-helpers.js', () => ({
66
- ensureOnMain: vi.fn().mockResolvedValue(undefined),
67
- }));
68
- vi.mock('@lumenflow/core/dist/micro-worktree.js', async (importOriginal) => {
69
- const actual = await importOriginal();
70
- return {
71
- ...actual,
72
- withMicroWorktree: vi.fn(async ({ execute }) => {
73
- // Simulate micro-worktree by executing in temp dir
74
- const tempDir = join(tmpdir(), `init-add-wu-test-${Date.now()}`);
75
- mkdirSync(tempDir, { recursive: true });
76
- try {
77
- await execute({ worktreePath: tempDir });
78
- }
79
- finally {
80
- // Cleanup handled by test
81
- }
82
- }),
83
- };
84
- });
85
- describe('initiative:add-wu WU validation (WU-1330)', () => {
86
- let tempDir;
87
- let originalCwd;
88
- beforeEach(() => {
89
- tempDir = join(tmpdir(), `init-add-wu-validation-test-${Date.now()}`);
90
- mkdirSync(tempDir, { recursive: true });
91
- originalCwd = process.cwd();
92
- });
93
- afterEach(() => {
94
- process.chdir(originalCwd);
95
- if (existsSync(tempDir)) {
96
- rmSync(tempDir, { recursive: true, force: true });
97
- }
98
- vi.clearAllMocks();
99
- });
100
- describe('validateWUForLinking', () => {
101
- it('should return valid for a well-formed WU', async () => {
102
- const { validateWUForLinking } = await import('../initiative-add-wu.js');
103
- // Create a valid WU file
104
- const wuDir = join(tempDir, WU_REL_PATH);
105
- mkdirSync(wuDir, { recursive: true });
106
- const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
107
- writeFileSync(wuPath, stringifyYAML(createValidWUDoc()));
108
- process.chdir(tempDir);
109
- const result = validateWUForLinking(TEST_WU_ID);
110
- expect(result.valid).toBe(true);
111
- expect(result.errors).toHaveLength(0);
112
- });
113
- it('should reject WU with missing required fields', async () => {
114
- const { validateWUForLinking } = await import('../initiative-add-wu.js');
115
- // Create a WU missing required fields (no description)
116
- const wuDir = join(tempDir, WU_REL_PATH);
117
- mkdirSync(wuDir, { recursive: true });
118
- const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
119
- writeFileSync(wuPath, stringifyYAML({
120
- id: TEST_WU_ID,
121
- title: 'Test',
122
- lane: TEST_LANE,
123
- status: 'ready',
124
- created: TEST_DATE,
125
- // Missing: description, acceptance, code_paths
126
- }));
127
- process.chdir(tempDir);
128
- const result = validateWUForLinking(TEST_WU_ID);
129
- expect(result.valid).toBe(false);
130
- expect(result.errors.length).toBeGreaterThan(0);
131
- expect(result.errors.some((e) => e.toLowerCase().includes('description'))).toBe(true);
132
- });
133
- it('should reject WU with invalid schema', async () => {
134
- const { validateWUForLinking } = await import('../initiative-add-wu.js');
135
- // Create a WU with invalid status
136
- const wuDir = join(tempDir, WU_REL_PATH);
137
- mkdirSync(wuDir, { recursive: true });
138
- const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
139
- writeFileSync(wuPath, stringifyYAML({
140
- ...createValidWUDoc(),
141
- status: 'invalid_status', // Invalid status value
142
- }));
143
- process.chdir(tempDir);
144
- const result = validateWUForLinking(TEST_WU_ID);
145
- expect(result.valid).toBe(false);
146
- expect(result.errors.some((e) => e.toLowerCase().includes('status'))).toBe(true);
147
- });
148
- it('should reject WU with description containing placeholder marker', async () => {
149
- const { validateWUForLinking } = await import('../initiative-add-wu.js');
150
- // Create a WU with placeholder in description
151
- const wuDir = join(tempDir, WU_REL_PATH);
152
- mkdirSync(wuDir, { recursive: true });
153
- const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
154
- writeFileSync(wuPath, stringifyYAML({
155
- ...createValidWUDoc(),
156
- description: '[PLACEHOLDER] This is a placeholder description that is long enough.',
157
- }));
158
- process.chdir(tempDir);
159
- const result = validateWUForLinking(TEST_WU_ID);
160
- expect(result.valid).toBe(false);
161
- expect(result.errors.some((e) => e.includes('PLACEHOLDER'))).toBe(true);
162
- });
163
- it('should reject WU with too short description', async () => {
164
- const { validateWUForLinking } = await import('../initiative-add-wu.js');
165
- // Create a WU with short description
166
- const wuDir = join(tempDir, WU_REL_PATH);
167
- mkdirSync(wuDir, { recursive: true });
168
- const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
169
- writeFileSync(wuPath, stringifyYAML({
170
- ...createValidWUDoc(),
171
- description: 'Too short', // Less than MIN_DESCRIPTION_LENGTH
172
- }));
173
- process.chdir(tempDir);
174
- const result = validateWUForLinking(TEST_WU_ID);
175
- expect(result.valid).toBe(false);
176
- expect(result.errors.some((e) => e.includes(`${MIN_DESCRIPTION_LENGTH}`))).toBe(true);
177
- });
178
- it('should reject WU with invalid ID format', async () => {
179
- const { validateWUForLinking } = await import('../initiative-add-wu.js');
180
- // Create a WU with invalid ID
181
- const invalidId = 'INVALID-123';
182
- const wuDir = join(tempDir, WU_REL_PATH);
183
- mkdirSync(wuDir, { recursive: true });
184
- const wuPath = join(wuDir, `${invalidId}.yaml`);
185
- writeFileSync(wuPath, stringifyYAML({
186
- ...createValidWUDoc(),
187
- id: invalidId,
188
- }));
189
- process.chdir(tempDir);
190
- const result = validateWUForLinking(invalidId);
191
- expect(result.valid).toBe(false);
192
- expect(result.errors.some((e) => e.toLowerCase().includes('id'))).toBe(true);
193
- });
194
- it('should reject WU that does not exist', async () => {
195
- const { validateWUForLinking } = await import('../initiative-add-wu.js');
196
- process.chdir(tempDir);
197
- const result = validateWUForLinking('WU-999');
198
- expect(result.valid).toBe(false);
199
- expect(result.errors.some((e) => e.toLowerCase().includes('not found'))).toBe(true);
200
- });
201
- it('should reject WU with empty acceptance criteria', async () => {
202
- const { validateWUForLinking } = await import('../initiative-add-wu.js');
203
- // Create a WU with empty acceptance
204
- const wuDir = join(tempDir, WU_REL_PATH);
205
- mkdirSync(wuDir, { recursive: true });
206
- const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
207
- writeFileSync(wuPath, stringifyYAML({
208
- ...createValidWUDoc(),
209
- acceptance: [], // Empty array
210
- }));
211
- process.chdir(tempDir);
212
- const result = validateWUForLinking(TEST_WU_ID);
213
- expect(result.valid).toBe(false);
214
- expect(result.errors.some((e) => e.toLowerCase().includes('acceptance'))).toBe(true);
215
- });
216
- it('should aggregate multiple errors', async () => {
217
- const { validateWUForLinking } = await import('../initiative-add-wu.js');
218
- // Create a WU with multiple issues
219
- const wuDir = join(tempDir, WU_REL_PATH);
220
- mkdirSync(wuDir, { recursive: true });
221
- const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
222
- writeFileSync(wuPath, stringifyYAML({
223
- id: TEST_WU_ID,
224
- title: '', // Empty title
225
- lane: TEST_LANE,
226
- status: 'invalid_status', // Invalid status
227
- created: TEST_DATE,
228
- description: 'short', // Too short
229
- acceptance: [], // Empty
230
- }));
231
- process.chdir(tempDir);
232
- const result = validateWUForLinking(TEST_WU_ID);
233
- expect(result.valid).toBe(false);
234
- // Should have multiple errors aggregated
235
- expect(result.errors.length).toBeGreaterThanOrEqual(2);
236
- });
237
- it('should include warnings but still be valid', async () => {
238
- const { validateWUForLinking } = await import('../initiative-add-wu.js');
239
- // Create a valid WU that might have warnings (missing optional recommended fields)
240
- const wuDir = join(tempDir, WU_REL_PATH);
241
- mkdirSync(wuDir, { recursive: true });
242
- const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
243
- writeFileSync(wuPath, stringifyYAML({
244
- ...createValidWUDoc(),
245
- notes: '', // Empty notes - should produce warning
246
- spec_refs: [], // Empty spec_refs for feature - should produce warning
247
- }));
248
- process.chdir(tempDir);
249
- const result = validateWUForLinking(TEST_WU_ID);
250
- // Should be valid (warnings don't block)
251
- expect(result.valid).toBe(true);
252
- // But should have warnings
253
- expect(result.warnings.length).toBeGreaterThan(0);
254
- });
255
- });
256
- describe('checkWUExists with validation', () => {
257
- it('should throw for invalid WU when strict validation enabled', async () => {
258
- const { checkWUExistsAndValidate } = await import('../initiative-add-wu.js');
259
- // Create an invalid WU file
260
- const wuDir = join(tempDir, WU_REL_PATH);
261
- mkdirSync(wuDir, { recursive: true });
262
- const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
263
- writeFileSync(wuPath, stringifyYAML({
264
- id: TEST_WU_ID,
265
- title: 'Test',
266
- lane: TEST_LANE,
267
- status: 'ready',
268
- created: TEST_DATE,
269
- description: 'short', // Too short
270
- }));
271
- process.chdir(tempDir);
272
- // Should throw with aggregated validation errors
273
- expect(() => checkWUExistsAndValidate(TEST_WU_ID)).toThrow();
274
- });
275
- it('should return WU doc when validation passes', async () => {
276
- const { checkWUExistsAndValidate } = await import('../initiative-add-wu.js');
277
- // Create a valid WU file
278
- const wuDir = join(tempDir, WU_REL_PATH);
279
- mkdirSync(wuDir, { recursive: true });
280
- const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
281
- writeFileSync(wuPath, stringifyYAML(createValidWUDoc()));
282
- process.chdir(tempDir);
283
- const result = checkWUExistsAndValidate(TEST_WU_ID);
284
- expect(result.id).toBe(TEST_WU_ID);
285
- });
286
- });
287
- describe('initiative:add-wu integration', () => {
288
- it('should reject linking invalid WU with clear error message', async () => {
289
- // This is an integration test scenario - main() calls validation before linking
290
- // The main() function should call validateWUForLinking and die() with aggregated errors
291
- const { validateWUForLinking } = await import('../initiative-add-wu.js');
292
- // Setup invalid WU
293
- const wuDir = join(tempDir, WU_REL_PATH);
294
- mkdirSync(wuDir, { recursive: true });
295
- const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
296
- writeFileSync(wuPath, stringifyYAML({
297
- id: TEST_WU_ID,
298
- title: 'Test',
299
- lane: TEST_LANE,
300
- status: 'ready',
301
- created: TEST_DATE,
302
- description: 'Too short',
303
- }));
304
- process.chdir(tempDir);
305
- const result = validateWUForLinking(TEST_WU_ID);
306
- // The error message should be suitable for display to user
307
- expect(result.valid).toBe(false);
308
- expect(result.errors.join('\n')).toContain('50'); // Should mention minimum length
309
- });
310
- it('should successfully link valid WU bidirectionally', async () => {
311
- // This test verifies that after validation passes, bidirectional linking works
312
- // The existing functionality should still work for valid WUs
313
- const { validateWUForLinking } = await import('../initiative-add-wu.js');
314
- // Setup valid WU and initiative
315
- const wuDir = join(tempDir, WU_REL_PATH);
316
- const initDir = join(tempDir, INIT_REL_PATH);
317
- mkdirSync(wuDir, { recursive: true });
318
- mkdirSync(initDir, { recursive: true });
319
- const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
320
- const initPath = join(initDir, `${TEST_INIT_ID}.yaml`);
321
- writeFileSync(wuPath, stringifyYAML(createValidWUDoc()));
322
- writeFileSync(initPath, stringifyYAML(createValidInitDoc()));
323
- process.chdir(tempDir);
324
- // Validation should pass
325
- const result = validateWUForLinking(TEST_WU_ID);
326
- expect(result.valid).toBe(true);
327
- });
328
- });
329
- describe('batch linking (WU-1460)', () => {
330
- it('should normalize repeatable --wu values with dedupe and order preservation', async () => {
331
- const { normalizeWuIds } = await import('../initiative-add-wu.js');
332
- expect(normalizeWuIds(TEST_WU_ID)).toEqual([TEST_WU_ID]);
333
- expect(normalizeWuIds([TEST_WU_ID, TEST_WU_ID_2, TEST_WU_ID])).toEqual([
334
- TEST_WU_ID,
335
- TEST_WU_ID_2,
336
- ]);
337
- });
338
- it('should update multiple WUs and initiative in one execute call', async () => {
339
- const { buildAddWuMicroWorktreeOptions } = await import('../initiative-add-wu.js');
340
- // Setup valid WUs and initiative
341
- const wuDir = join(tempDir, WU_REL_PATH);
342
- const initDir = join(tempDir, INIT_REL_PATH);
343
- mkdirSync(wuDir, { recursive: true });
344
- mkdirSync(initDir, { recursive: true });
345
- const wuPath1 = join(wuDir, `${TEST_WU_ID}.yaml`);
346
- const wuPath2 = join(wuDir, `${TEST_WU_ID_2}.yaml`);
347
- const initPath = join(initDir, `${TEST_INIT_ID}.yaml`);
348
- writeFileSync(wuPath1, stringifyYAML(createValidWUDoc({ id: TEST_WU_ID })));
349
- writeFileSync(wuPath2, stringifyYAML(createValidWUDoc({ id: TEST_WU_ID_2 })));
350
- writeFileSync(initPath, stringifyYAML(createValidInitDoc()));
351
- process.chdir(tempDir);
352
- const options = buildAddWuMicroWorktreeOptions([TEST_WU_ID, TEST_WU_ID_2], TEST_INIT_ID);
353
- const result = await options.execute({ worktreePath: tempDir });
354
- expect(result.files).toContain(`${WU_REL_PATH}/${TEST_WU_ID}.yaml`);
355
- expect(result.files).toContain(`${WU_REL_PATH}/${TEST_WU_ID_2}.yaml`);
356
- expect(result.files).toContain(`${INIT_REL_PATH}/${TEST_INIT_ID}.yaml`);
357
- const updatedWu1 = readWU(wuPath1, TEST_WU_ID);
358
- const updatedWu2 = readWU(wuPath2, TEST_WU_ID_2);
359
- const updatedInit = readInitiative(initPath, TEST_INIT_ID);
360
- expect(updatedWu1.initiative).toBe(TEST_INIT_ID);
361
- expect(updatedWu2.initiative).toBe(TEST_INIT_ID);
362
- expect(updatedInit.wus).toContain(TEST_WU_ID);
363
- expect(updatedInit.wus).toContain(TEST_WU_ID_2);
364
- });
365
- it('should validate conflicting links across multiple WUs', async () => {
366
- const { validateNoConflictingLinks } = await import('../initiative-add-wu.js');
367
- expect(() => validateNoConflictingLinks([
368
- { id: TEST_WU_ID, initiative: TEST_INIT_ID },
369
- { id: TEST_WU_ID_2, initiative: 'INIT-999' },
370
- ], TEST_INIT_ID)).toThrow();
371
- });
372
- });
373
- describe('error formatting', () => {
374
- it('should format errors in human-readable format', async () => {
375
- const { formatValidationErrors } = await import('../initiative-add-wu.js');
376
- const errors = ['description: Description is required', 'acceptance: At least one criterion'];
377
- const wuId = TEST_WU_ID;
378
- const formatted = formatValidationErrors(wuId, errors);
379
- expect(formatted).toContain(wuId);
380
- expect(formatted).toContain('description');
381
- expect(formatted).toContain('acceptance');
382
- });
383
- });
384
- describe('exports', () => {
385
- it('should export validateWUForLinking function', async () => {
386
- const mod = await import('../initiative-add-wu.js');
387
- expect(typeof mod.validateWUForLinking).toBe('function');
388
- });
389
- it('should export checkWUExistsAndValidate function', async () => {
390
- const mod = await import('../initiative-add-wu.js');
391
- expect(typeof mod.checkWUExistsAndValidate).toBe('function');
392
- });
393
- it('should export formatValidationErrors function', async () => {
394
- const mod = await import('../initiative-add-wu.js');
395
- expect(typeof mod.formatValidationErrors).toBe('function');
396
- });
397
- it('should export isRetryExhaustionError function (WU-1333)', async () => {
398
- const mod = await import('../initiative-add-wu.js');
399
- expect(typeof mod.isRetryExhaustionError).toBe('function');
400
- });
401
- it('should export formatRetryExhaustionError function (WU-1333)', async () => {
402
- const mod = await import('../initiative-add-wu.js');
403
- expect(typeof mod.formatRetryExhaustionError).toBe('function');
404
- });
405
- it('should export operation-level push retry override (WU-1459)', async () => {
406
- const mod = await import('../initiative-add-wu.js');
407
- expect(mod.INITIATIVE_ADD_WU_PUSH_RETRY_OVERRIDE).toBeDefined();
408
- expect(mod.INITIATIVE_ADD_WU_PUSH_RETRY_OVERRIDE.retries).toBeGreaterThan(3);
409
- expect(mod.INITIATIVE_ADD_WU_PUSH_RETRY_OVERRIDE.min_delay_ms).toBeGreaterThan(100);
410
- });
411
- it('should export helper to build micro-worktree options (WU-1459)', async () => {
412
- const mod = await import('../initiative-add-wu.js');
413
- expect(typeof mod.buildAddWuMicroWorktreeOptions).toBe('function');
414
- const options = mod.buildAddWuMicroWorktreeOptions(TEST_WU_ID, TEST_INIT_ID);
415
- expect(options.pushOnly).toBe(true);
416
- expect(options.pushRetryOverride).toEqual(mod.INITIATIVE_ADD_WU_PUSH_RETRY_OVERRIDE);
417
- });
418
- it('should export batch helpers (WU-1460)', async () => {
419
- const mod = await import('../initiative-add-wu.js');
420
- expect(typeof mod.normalizeWuIds).toBe('function');
421
- expect(typeof mod.validateNoConflictingLinks).toBe('function');
422
- });
423
- });
424
- });
425
- /**
426
- * WU-1333: Retry handling tests for initiative:add-wu
427
- *
428
- * When origin/main moves during operation, the micro-worktree layer handles retry.
429
- * When retries are exhausted, the error message should include actionable next steps.
430
- */
431
- describe('initiative:add-wu retry handling (WU-1333)', () => {
432
- describe('isRetryExhaustionError', () => {
433
- it('should detect retry exhaustion from error message', async () => {
434
- const { isRetryExhaustionError } = await import('../initiative-add-wu.js');
435
- // Should detect retry exhaustion error
436
- const retryError = new Error('Push failed after 3 attempts. Origin main may have significant traffic.');
437
- expect(isRetryExhaustionError(retryError)).toBe(true);
438
- });
439
- it('should detect retry exhaustion with any attempt count', async () => {
440
- const { isRetryExhaustionError } = await import('../initiative-add-wu.js');
441
- // Different attempt counts should still match
442
- const error5 = new Error('Push failed after 5 attempts. Something.');
443
- expect(isRetryExhaustionError(error5)).toBe(true);
444
- const error1 = new Error('Push failed after 1 attempts. Something.');
445
- expect(isRetryExhaustionError(error1)).toBe(true);
446
- });
447
- it('should not match other errors', async () => {
448
- const { isRetryExhaustionError } = await import('../initiative-add-wu.js');
449
- const otherError = new Error('Some other error');
450
- expect(isRetryExhaustionError(otherError)).toBe(false);
451
- const networkError = new Error('Network unreachable');
452
- expect(isRetryExhaustionError(networkError)).toBe(false);
453
- });
454
- });
455
- describe('formatRetryExhaustionError', () => {
456
- it('should include actionable next steps', async () => {
457
- const { formatRetryExhaustionError } = await import('../initiative-add-wu.js');
458
- const retryError = new Error('Push failed after 3 attempts. Origin main may have significant traffic.');
459
- const formatted = formatRetryExhaustionError(retryError, TEST_WU_ID, TEST_INIT_ID);
460
- // Should include the original error
461
- expect(formatted).toContain('Push failed after 3 attempts');
462
- // Should include next steps heading
463
- expect(formatted).toContain('Next steps:');
464
- // Should include actionable suggestions
465
- expect(formatted).toContain('Wait a few seconds and retry');
466
- expect(formatted).toContain('initiative:add-wu');
467
- });
468
- it('should include the retry command', async () => {
469
- const { formatRetryExhaustionError } = await import('../initiative-add-wu.js');
470
- const retryError = new Error('Push failed after 3 attempts.');
471
- const formatted = formatRetryExhaustionError(retryError, TEST_WU_ID, TEST_INIT_ID);
472
- // Should include command to retry
473
- expect(formatted).toContain(`--wu ${TEST_WU_ID}`);
474
- expect(formatted).toContain(`--initiative ${TEST_INIT_ID}`);
475
- });
476
- it('should suggest checking for concurrent agents', async () => {
477
- const { formatRetryExhaustionError } = await import('../initiative-add-wu.js');
478
- const retryError = new Error('Push failed after 3 attempts.');
479
- const formatted = formatRetryExhaustionError(retryError, TEST_WU_ID, TEST_INIT_ID);
480
- // Should mention concurrent agents as possible cause
481
- expect(formatted).toMatch(/concurrent|agent|traffic/i);
482
- });
483
- it('should include git.push_retry tuning guidance', async () => {
484
- const { formatRetryExhaustionError } = await import('../initiative-add-wu.js');
485
- const retryError = new Error('Push failed after 3 attempts.');
486
- const formatted = formatRetryExhaustionError(retryError, TEST_WU_ID, TEST_INIT_ID);
487
- expect(formatted).toContain('git.push_retry.retries');
488
- });
489
- });
490
- });