@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.
- package/README.md +8 -4
- package/dist/hooks/enforcement-checks.js +120 -0
- package/dist/hooks/enforcement-checks.js.map +1 -1
- package/dist/init-lane-validation.js +141 -0
- package/dist/init-lane-validation.js.map +1 -0
- package/dist/init-templates.js +36 -8
- package/dist/init-templates.js.map +1 -1
- package/dist/init.js +27 -58
- package/dist/init.js.map +1 -1
- package/dist/initiative-create.js +35 -4
- package/dist/initiative-create.js.map +1 -1
- package/dist/lane-lifecycle-process.js +364 -0
- package/dist/lane-lifecycle-process.js.map +1 -0
- package/dist/lane-lock.js +41 -0
- package/dist/lane-lock.js.map +1 -0
- package/dist/lane-setup.js +55 -0
- package/dist/lane-setup.js.map +1 -0
- package/dist/lane-status.js +38 -0
- package/dist/lane-status.js.map +1 -0
- package/dist/lane-validate.js +43 -0
- package/dist/lane-validate.js.map +1 -0
- package/dist/onboarding-smoke-test.js +17 -0
- package/dist/onboarding-smoke-test.js.map +1 -1
- package/dist/public-manifest.js +28 -0
- package/dist/public-manifest.js.map +1 -1
- package/dist/wu-claim-cloud.js +16 -0
- package/dist/wu-claim-cloud.js.map +1 -1
- package/dist/wu-claim.js +12 -2
- package/dist/wu-claim.js.map +1 -1
- package/dist/wu-create-content.js +8 -2
- package/dist/wu-create-content.js.map +1 -1
- package/dist/wu-create-validation.js +5 -3
- package/dist/wu-create-validation.js.map +1 -1
- package/dist/wu-create.js +21 -1
- package/dist/wu-create.js.map +1 -1
- package/dist/wu-done.js +57 -8
- package/dist/wu-done.js.map +1 -1
- package/dist/wu-prep.js +22 -0
- package/dist/wu-prep.js.map +1 -1
- package/package.json +15 -11
- package/dist/__tests__/agent-log-issue.test.js +0 -56
- package/dist/__tests__/agent-spawn-coordination.test.js +0 -451
- package/dist/__tests__/backlog-prune.test.js +0 -478
- package/dist/__tests__/cli-entry-point.test.js +0 -160
- package/dist/__tests__/cli-subprocess.test.js +0 -89
- package/dist/__tests__/commands/integrate.test.js +0 -165
- package/dist/__tests__/commands.test.js +0 -271
- package/dist/__tests__/deps-operations.test.js +0 -206
- package/dist/__tests__/doctor.test.js +0 -510
- package/dist/__tests__/file-operations.test.js +0 -906
- package/dist/__tests__/flow-report.test.js +0 -24
- package/dist/__tests__/gates-config.test.js +0 -303
- package/dist/__tests__/gates-integration-tests.test.js +0 -112
- package/dist/__tests__/git-operations.test.js +0 -668
- package/dist/__tests__/guard-main-branch.test.js +0 -79
- package/dist/__tests__/guards-validation.test.js +0 -416
- package/dist/__tests__/hooks/enforcement.test.js +0 -279
- package/dist/__tests__/init-config-lanes.test.js +0 -131
- package/dist/__tests__/init-docs-structure.test.js +0 -152
- package/dist/__tests__/init-greenfield.test.js +0 -247
- package/dist/__tests__/init-lane-inference.test.js +0 -125
- package/dist/__tests__/init-onboarding-docs.test.js +0 -132
- package/dist/__tests__/init-quick-ref.test.js +0 -144
- package/dist/__tests__/init-scripts.test.js +0 -207
- package/dist/__tests__/init-template-portability.test.js +0 -96
- package/dist/__tests__/init.test.js +0 -968
- package/dist/__tests__/initiative-add-wu.test.js +0 -490
- package/dist/__tests__/initiative-e2e.test.js +0 -442
- package/dist/__tests__/initiative-plan-replacement.test.js +0 -161
- package/dist/__tests__/initiative-plan.test.js +0 -340
- package/dist/__tests__/initiative-remove-wu.test.js +0 -458
- package/dist/__tests__/lumenflow-upgrade.test.js +0 -260
- package/dist/__tests__/mem-cleanup-execution.test.js +0 -19
- package/dist/__tests__/memory-integration.test.js +0 -333
- package/dist/__tests__/merge-block.test.js +0 -220
- package/dist/__tests__/metrics-cli.test.js +0 -619
- package/dist/__tests__/metrics-snapshot.test.js +0 -24
- package/dist/__tests__/no-beacon-references-docs.test.js +0 -30
- package/dist/__tests__/no-beacon-references.test.js +0 -39
- package/dist/__tests__/onboarding-smoke-test.test.js +0 -211
- package/dist/__tests__/path-centralization-cli.test.js +0 -234
- package/dist/__tests__/plan-create.test.js +0 -126
- package/dist/__tests__/plan-edit.test.js +0 -157
- package/dist/__tests__/plan-link.test.js +0 -239
- package/dist/__tests__/plan-promote.test.js +0 -181
- package/dist/__tests__/release.test.js +0 -372
- package/dist/__tests__/rotate-progress.test.js +0 -127
- package/dist/__tests__/safe-git.test.js +0 -190
- package/dist/__tests__/session-coordinator.test.js +0 -109
- package/dist/__tests__/state-bootstrap.test.js +0 -432
- package/dist/__tests__/state-doctor.test.js +0 -328
- package/dist/__tests__/sync-templates.test.js +0 -255
- package/dist/__tests__/templates-sync.test.js +0 -219
- package/dist/__tests__/trace-gen.test.js +0 -115
- package/dist/__tests__/wu-create-required-fields.test.js +0 -143
- package/dist/__tests__/wu-create-strict.test.js +0 -118
- package/dist/__tests__/wu-create.test.js +0 -121
- package/dist/__tests__/wu-done-auto-cleanup.test.js +0 -135
- package/dist/__tests__/wu-done-docs-only-policy.test.js +0 -20
- package/dist/__tests__/wu-done-staging-whitelist.test.js +0 -35
- package/dist/__tests__/wu-done.test.js +0 -36
- package/dist/__tests__/wu-edit-strict.test.js +0 -109
- package/dist/__tests__/wu-edit.test.js +0 -119
- package/dist/__tests__/wu-lifecycle-integration.test.js +0 -388
- package/dist/__tests__/wu-prep-default-exec.test.js +0 -35
- package/dist/__tests__/wu-prep.test.js +0 -140
- package/dist/__tests__/wu-proto.test.js +0 -97
- package/dist/__tests__/wu-validate-strict.test.js +0 -113
- package/dist/__tests__/wu-validate.test.js +0 -36
- package/dist/spawn-list.js +0 -143
- 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
|
-
});
|