@lumenflow/cli 2.7.0 → 2.8.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.
- package/README.md +120 -105
- package/dist/__tests__/agent-spawn-coordination.test.js +451 -0
- package/dist/__tests__/commands/integrate.test.js +165 -0
- package/dist/__tests__/gates-config.test.js +0 -1
- package/dist/__tests__/hooks/enforcement.test.js +279 -0
- package/dist/__tests__/init-greenfield.test.js +247 -0
- package/dist/__tests__/init-quick-ref.test.js +0 -1
- package/dist/__tests__/init-template-portability.test.js +0 -1
- package/dist/__tests__/init.test.js +27 -0
- package/dist/__tests__/initiative-e2e.test.js +442 -0
- package/dist/__tests__/initiative-plan-replacement.test.js +0 -1
- package/dist/__tests__/memory-integration.test.js +333 -0
- package/dist/__tests__/release.test.js +1 -1
- package/dist/__tests__/safe-git.test.js +0 -1
- package/dist/__tests__/state-doctor.test.js +54 -0
- package/dist/__tests__/sync-templates.test.js +255 -0
- package/dist/__tests__/wu-create-required-fields.test.js +121 -0
- package/dist/__tests__/wu-done-auto-cleanup.test.js +135 -0
- package/dist/__tests__/wu-lifecycle-integration.test.js +388 -0
- package/dist/backlog-prune.js +0 -1
- package/dist/cli-entry-point.js +0 -1
- package/dist/commands/integrate.js +229 -0
- package/dist/docs-sync.js +46 -0
- package/dist/doctor.js +0 -2
- package/dist/gates.js +0 -7
- package/dist/hooks/enforcement-checks.js +209 -0
- package/dist/hooks/enforcement-generator.js +365 -0
- package/dist/hooks/enforcement-sync.js +243 -0
- package/dist/hooks/index.js +7 -0
- package/dist/init.js +256 -13
- package/dist/initiative-add-wu.js +0 -2
- package/dist/initiative-create.js +0 -3
- package/dist/initiative-edit.js +0 -5
- package/dist/initiative-plan.js +0 -1
- package/dist/initiative-remove-wu.js +0 -2
- package/dist/lane-health.js +0 -2
- package/dist/lane-suggest.js +0 -1
- package/dist/mem-checkpoint.js +0 -2
- package/dist/mem-cleanup.js +0 -2
- package/dist/mem-context.js +0 -3
- package/dist/mem-create.js +0 -2
- package/dist/mem-delete.js +0 -3
- package/dist/mem-inbox.js +0 -2
- package/dist/mem-index.js +0 -1
- package/dist/mem-init.js +0 -2
- package/dist/mem-profile.js +0 -1
- package/dist/mem-promote.js +0 -1
- package/dist/mem-ready.js +0 -2
- package/dist/mem-signal.js +0 -2
- package/dist/mem-start.js +0 -2
- package/dist/mem-summarize.js +0 -2
- package/dist/metrics-cli.js +1 -1
- package/dist/metrics-snapshot.js +1 -1
- package/dist/onboarding-smoke-test.js +0 -5
- package/dist/orchestrate-init-status.js +0 -1
- package/dist/orchestrate-initiative.js +0 -1
- package/dist/orchestrate-monitor.js +0 -1
- package/dist/plan-create.js +0 -2
- package/dist/plan-edit.js +0 -2
- package/dist/plan-link.js +0 -2
- package/dist/plan-promote.js +0 -2
- package/dist/signal-cleanup.js +0 -4
- package/dist/state-bootstrap.js +0 -1
- package/dist/state-cleanup.js +0 -4
- package/dist/state-doctor-fix.js +5 -8
- package/dist/state-doctor.js +0 -11
- package/dist/sync-templates.js +188 -34
- package/dist/wu-block.js +100 -48
- package/dist/wu-claim.js +1 -22
- package/dist/wu-cleanup.js +0 -1
- package/dist/wu-create.js +0 -2
- package/dist/wu-done-auto-cleanup.js +139 -0
- package/dist/wu-done.js +11 -4
- package/dist/wu-edit.js +0 -12
- package/dist/wu-preflight.js +0 -1
- package/dist/wu-prep.js +0 -1
- package/dist/wu-proto.js +0 -1
- package/dist/wu-spawn.js +0 -3
- package/dist/wu-unblock.js +0 -2
- package/dist/wu-validate.js +0 -1
- package/package.json +8 -7
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Initiative Orchestration E2E Tests (WU-1363)
|
|
3
|
+
*
|
|
4
|
+
* End-to-end tests for initiative orchestration:
|
|
5
|
+
* - AC5: E2E test for initiative orchestration
|
|
6
|
+
*
|
|
7
|
+
* These tests validate the complete initiative workflow:
|
|
8
|
+
* - Creating initiatives with phases
|
|
9
|
+
* - Adding WUs to initiatives
|
|
10
|
+
* - Tracking initiative progress
|
|
11
|
+
* - Wave-based orchestration
|
|
12
|
+
*
|
|
13
|
+
* TDD: Tests written BEFORE implementation verification.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
16
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
import { execFileSync } from 'node:child_process';
|
|
20
|
+
import { parseYAML, stringifyYAML } from '@lumenflow/core/dist/wu-yaml.js';
|
|
21
|
+
import { WU_STATUS } from '@lumenflow/core/dist/wu-constants.js';
|
|
22
|
+
// Test constants
|
|
23
|
+
const TEST_INIT_ID = 'INIT-901';
|
|
24
|
+
const TEST_INIT_TITLE = 'Test Initiative';
|
|
25
|
+
const TEST_WU_ID_1 = 'WU-9930';
|
|
26
|
+
const TEST_WU_ID_2 = 'WU-9931';
|
|
27
|
+
const TEST_WU_ID_3 = 'WU-9932';
|
|
28
|
+
const TEST_LANE = 'Framework: CLI';
|
|
29
|
+
/**
|
|
30
|
+
* Helper to create a test project for initiative orchestration
|
|
31
|
+
*/
|
|
32
|
+
function createInitiativeProject(baseDir) {
|
|
33
|
+
const dirs = [
|
|
34
|
+
'docs/04-operations/tasks/wu',
|
|
35
|
+
'docs/04-operations/tasks/initiatives',
|
|
36
|
+
'.lumenflow/state',
|
|
37
|
+
'.lumenflow/memory',
|
|
38
|
+
'.lumenflow/stamps',
|
|
39
|
+
'packages/@lumenflow/cli/src',
|
|
40
|
+
];
|
|
41
|
+
for (const dir of dirs) {
|
|
42
|
+
mkdirSync(join(baseDir, dir), { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
// Create config
|
|
45
|
+
const configContent = `
|
|
46
|
+
version: 1
|
|
47
|
+
lanes:
|
|
48
|
+
definitions:
|
|
49
|
+
- name: 'Framework: CLI'
|
|
50
|
+
wip_limit: 1
|
|
51
|
+
code_paths:
|
|
52
|
+
- 'packages/@lumenflow/cli/**'
|
|
53
|
+
- name: 'Framework: Core'
|
|
54
|
+
wip_limit: 1
|
|
55
|
+
code_paths:
|
|
56
|
+
- 'packages/@lumenflow/core/**'
|
|
57
|
+
git:
|
|
58
|
+
requireRemote: false
|
|
59
|
+
initiatives:
|
|
60
|
+
enabled: true
|
|
61
|
+
`;
|
|
62
|
+
writeFileSync(join(baseDir, '.lumenflow.config.yaml'), configContent);
|
|
63
|
+
// Initialize git
|
|
64
|
+
execFileSync('git', ['init'], { cwd: baseDir, stdio: 'pipe' });
|
|
65
|
+
execFileSync('git', ['config', 'user.email', 'test@test.com'], { cwd: baseDir, stdio: 'pipe' });
|
|
66
|
+
execFileSync('git', ['config', 'user.name', 'Test'], { cwd: baseDir, stdio: 'pipe' });
|
|
67
|
+
writeFileSync(join(baseDir, 'README.md'), '# Test\n');
|
|
68
|
+
execFileSync('git', ['add', '.'], { cwd: baseDir, stdio: 'pipe' });
|
|
69
|
+
execFileSync('git', ['commit', '-m', 'init'], { cwd: baseDir, stdio: 'pipe' });
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Helper to create an initiative YAML file
|
|
73
|
+
*/
|
|
74
|
+
function createInitiative(baseDir, id, options = {}) {
|
|
75
|
+
const initDir = join(baseDir, 'docs/04-operations/tasks/initiatives');
|
|
76
|
+
const initPath = join(initDir, `${id}.yaml`);
|
|
77
|
+
const doc = {
|
|
78
|
+
id,
|
|
79
|
+
slug: id.toLowerCase().replace('init-', 'initiative-'),
|
|
80
|
+
title: options.title || TEST_INIT_TITLE,
|
|
81
|
+
status: options.status || 'open',
|
|
82
|
+
created: '2026-02-03',
|
|
83
|
+
description: 'Test initiative for E2E testing',
|
|
84
|
+
phases: options.phases || [
|
|
85
|
+
{ name: 'Phase 1: Foundation', status: 'in_progress' },
|
|
86
|
+
{ name: 'Phase 2: Features', status: 'pending' },
|
|
87
|
+
],
|
|
88
|
+
wus: options.wus || [],
|
|
89
|
+
};
|
|
90
|
+
writeFileSync(initPath, stringifyYAML(doc));
|
|
91
|
+
return initPath;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Helper to create a WU linked to an initiative
|
|
95
|
+
*/
|
|
96
|
+
function createWUForInitiative(baseDir, id, options = {}) {
|
|
97
|
+
const wuDir = join(baseDir, 'docs/04-operations/tasks/wu');
|
|
98
|
+
const wuPath = join(wuDir, `${id}.yaml`);
|
|
99
|
+
const doc = {
|
|
100
|
+
id,
|
|
101
|
+
title: `WU for ${options.initiative || 'testing'}`,
|
|
102
|
+
lane: options.lane || TEST_LANE,
|
|
103
|
+
status: options.status || WU_STATUS.READY,
|
|
104
|
+
type: 'feature',
|
|
105
|
+
priority: 'P2',
|
|
106
|
+
created: '2026-02-03',
|
|
107
|
+
description: 'Context: Test. Problem: Testing. Solution: Test it.',
|
|
108
|
+
acceptance: ['Test passes'],
|
|
109
|
+
code_paths: ['packages/@lumenflow/cli/src'],
|
|
110
|
+
tests: { unit: ['test.test.ts'] },
|
|
111
|
+
exposure: 'backend-only',
|
|
112
|
+
dependencies: options.dependencies || [],
|
|
113
|
+
};
|
|
114
|
+
if (options.initiative) {
|
|
115
|
+
doc.initiative = options.initiative;
|
|
116
|
+
}
|
|
117
|
+
if (options.phase !== undefined) {
|
|
118
|
+
doc.phase = options.phase;
|
|
119
|
+
}
|
|
120
|
+
writeFileSync(wuPath, stringifyYAML(doc));
|
|
121
|
+
return wuPath;
|
|
122
|
+
}
|
|
123
|
+
describe('Initiative Orchestration E2E Tests (WU-1363)', () => {
|
|
124
|
+
let tempDir;
|
|
125
|
+
let originalCwd;
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
tempDir = join(tmpdir(), `initiative-e2e-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
128
|
+
mkdirSync(tempDir, { recursive: true });
|
|
129
|
+
originalCwd = process.cwd();
|
|
130
|
+
createInitiativeProject(tempDir);
|
|
131
|
+
vi.resetModules();
|
|
132
|
+
});
|
|
133
|
+
afterEach(() => {
|
|
134
|
+
process.chdir(originalCwd);
|
|
135
|
+
if (existsSync(tempDir)) {
|
|
136
|
+
try {
|
|
137
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Ignore cleanup errors
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
vi.clearAllMocks();
|
|
144
|
+
});
|
|
145
|
+
describe('AC5: E2E test for initiative orchestration', () => {
|
|
146
|
+
describe('initiative creation', () => {
|
|
147
|
+
it('should create an initiative with phases', () => {
|
|
148
|
+
// Arrange
|
|
149
|
+
process.chdir(tempDir);
|
|
150
|
+
// Act
|
|
151
|
+
const initPath = createInitiative(tempDir, TEST_INIT_ID, {
|
|
152
|
+
title: TEST_INIT_TITLE,
|
|
153
|
+
phases: [
|
|
154
|
+
{ name: 'Phase 1: MVP', status: 'pending' },
|
|
155
|
+
{ name: 'Phase 2: Polish', status: 'pending' },
|
|
156
|
+
{ name: 'Phase 3: Launch', status: 'pending' },
|
|
157
|
+
],
|
|
158
|
+
});
|
|
159
|
+
// Assert
|
|
160
|
+
expect(existsSync(initPath)).toBe(true);
|
|
161
|
+
const content = readFileSync(initPath, 'utf-8');
|
|
162
|
+
const doc = parseYAML(content);
|
|
163
|
+
expect(doc.id).toBe(TEST_INIT_ID);
|
|
164
|
+
expect(doc.phases).toHaveLength(3);
|
|
165
|
+
});
|
|
166
|
+
it('should track initiative status', () => {
|
|
167
|
+
// Arrange
|
|
168
|
+
process.chdir(tempDir);
|
|
169
|
+
createInitiative(tempDir, TEST_INIT_ID, { status: 'open' });
|
|
170
|
+
// Act
|
|
171
|
+
const initPath = join(tempDir, 'docs/04-operations/tasks/initiatives', `${TEST_INIT_ID}.yaml`);
|
|
172
|
+
const doc = parseYAML(readFileSync(initPath, 'utf-8'));
|
|
173
|
+
// Assert
|
|
174
|
+
expect(doc.status).toBe('open');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
describe('WU linkage', () => {
|
|
178
|
+
it('should link WUs to initiatives', () => {
|
|
179
|
+
// Arrange
|
|
180
|
+
process.chdir(tempDir);
|
|
181
|
+
createInitiative(tempDir, TEST_INIT_ID, { wus: [] });
|
|
182
|
+
// Act - Create WU linked to initiative
|
|
183
|
+
const wuPath = createWUForInitiative(tempDir, TEST_WU_ID_1, {
|
|
184
|
+
initiative: TEST_INIT_ID,
|
|
185
|
+
phase: 1,
|
|
186
|
+
});
|
|
187
|
+
// Update initiative with WU reference
|
|
188
|
+
const initPath = join(tempDir, 'docs/04-operations/tasks/initiatives', `${TEST_INIT_ID}.yaml`);
|
|
189
|
+
const initDoc = parseYAML(readFileSync(initPath, 'utf-8'));
|
|
190
|
+
initDoc.wus = [TEST_WU_ID_1];
|
|
191
|
+
writeFileSync(initPath, stringifyYAML(initDoc));
|
|
192
|
+
// Assert
|
|
193
|
+
expect(existsSync(wuPath)).toBe(true);
|
|
194
|
+
const wuDoc = parseYAML(readFileSync(wuPath, 'utf-8'));
|
|
195
|
+
expect(wuDoc.initiative).toBe(TEST_INIT_ID);
|
|
196
|
+
const updatedInit = parseYAML(readFileSync(initPath, 'utf-8'));
|
|
197
|
+
expect(updatedInit.wus).toContain(TEST_WU_ID_1);
|
|
198
|
+
});
|
|
199
|
+
it('should track multiple WUs per phase', () => {
|
|
200
|
+
// Arrange
|
|
201
|
+
process.chdir(tempDir);
|
|
202
|
+
createInitiative(tempDir, TEST_INIT_ID);
|
|
203
|
+
// Act - Create multiple WUs for phase 1
|
|
204
|
+
createWUForInitiative(tempDir, TEST_WU_ID_1, { initiative: TEST_INIT_ID, phase: 1 });
|
|
205
|
+
createWUForInitiative(tempDir, TEST_WU_ID_2, { initiative: TEST_INIT_ID, phase: 1 });
|
|
206
|
+
createWUForInitiative(tempDir, TEST_WU_ID_3, { initiative: TEST_INIT_ID, phase: 2 });
|
|
207
|
+
// Update initiative
|
|
208
|
+
const initPath = join(tempDir, 'docs/04-operations/tasks/initiatives', `${TEST_INIT_ID}.yaml`);
|
|
209
|
+
const initDoc = parseYAML(readFileSync(initPath, 'utf-8'));
|
|
210
|
+
initDoc.wus = [TEST_WU_ID_1, TEST_WU_ID_2, TEST_WU_ID_3];
|
|
211
|
+
writeFileSync(initPath, stringifyYAML(initDoc));
|
|
212
|
+
// Assert
|
|
213
|
+
const updatedInit = parseYAML(readFileSync(initPath, 'utf-8'));
|
|
214
|
+
expect(updatedInit.wus).toHaveLength(3);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
describe('progress tracking', () => {
|
|
218
|
+
it('should calculate initiative progress from WU statuses', () => {
|
|
219
|
+
// Arrange
|
|
220
|
+
process.chdir(tempDir);
|
|
221
|
+
createInitiative(tempDir, TEST_INIT_ID, {
|
|
222
|
+
wus: [TEST_WU_ID_1, TEST_WU_ID_2, TEST_WU_ID_3],
|
|
223
|
+
});
|
|
224
|
+
createWUForInitiative(tempDir, TEST_WU_ID_1, {
|
|
225
|
+
initiative: TEST_INIT_ID,
|
|
226
|
+
status: WU_STATUS.DONE,
|
|
227
|
+
});
|
|
228
|
+
createWUForInitiative(tempDir, TEST_WU_ID_2, {
|
|
229
|
+
initiative: TEST_INIT_ID,
|
|
230
|
+
status: WU_STATUS.IN_PROGRESS,
|
|
231
|
+
});
|
|
232
|
+
createWUForInitiative(tempDir, TEST_WU_ID_3, {
|
|
233
|
+
initiative: TEST_INIT_ID,
|
|
234
|
+
status: WU_STATUS.READY,
|
|
235
|
+
});
|
|
236
|
+
// Act - Calculate progress
|
|
237
|
+
const wuStatuses = [TEST_WU_ID_1, TEST_WU_ID_2, TEST_WU_ID_3].map((id) => {
|
|
238
|
+
const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${id}.yaml`);
|
|
239
|
+
const doc = parseYAML(readFileSync(wuPath, 'utf-8'));
|
|
240
|
+
return doc.status;
|
|
241
|
+
});
|
|
242
|
+
const doneCount = wuStatuses.filter((s) => s === WU_STATUS.DONE).length;
|
|
243
|
+
const totalCount = wuStatuses.length;
|
|
244
|
+
const progressPercent = Math.round((doneCount / totalCount) * 100);
|
|
245
|
+
// Assert
|
|
246
|
+
expect(doneCount).toBe(1);
|
|
247
|
+
expect(totalCount).toBe(3);
|
|
248
|
+
expect(progressPercent).toBe(33);
|
|
249
|
+
});
|
|
250
|
+
it('should track phase completion', () => {
|
|
251
|
+
// Arrange
|
|
252
|
+
process.chdir(tempDir);
|
|
253
|
+
const initPath = createInitiative(tempDir, TEST_INIT_ID, {
|
|
254
|
+
phases: [
|
|
255
|
+
{ name: 'Phase 1', status: 'in_progress' },
|
|
256
|
+
{ name: 'Phase 2', status: 'pending' },
|
|
257
|
+
],
|
|
258
|
+
});
|
|
259
|
+
// Create phase 1 WUs (all done)
|
|
260
|
+
createWUForInitiative(tempDir, TEST_WU_ID_1, {
|
|
261
|
+
initiative: TEST_INIT_ID,
|
|
262
|
+
phase: 1,
|
|
263
|
+
status: WU_STATUS.DONE,
|
|
264
|
+
});
|
|
265
|
+
createWUForInitiative(tempDir, TEST_WU_ID_2, {
|
|
266
|
+
initiative: TEST_INIT_ID,
|
|
267
|
+
phase: 1,
|
|
268
|
+
status: WU_STATUS.DONE,
|
|
269
|
+
});
|
|
270
|
+
// Act - Mark phase 1 as done
|
|
271
|
+
const initDoc = parseYAML(readFileSync(initPath, 'utf-8'));
|
|
272
|
+
initDoc.phases[0].status = 'done';
|
|
273
|
+
initDoc.phases[1].status = 'in_progress';
|
|
274
|
+
writeFileSync(initPath, stringifyYAML(initDoc));
|
|
275
|
+
// Assert
|
|
276
|
+
const updatedDoc = parseYAML(readFileSync(initPath, 'utf-8'));
|
|
277
|
+
expect(updatedDoc.phases[0].status).toBe('done');
|
|
278
|
+
expect(updatedDoc.phases[1].status).toBe('in_progress');
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
describe('wave-based orchestration', () => {
|
|
282
|
+
it('should identify parallelizable WUs (no dependencies)', () => {
|
|
283
|
+
// Arrange
|
|
284
|
+
process.chdir(tempDir);
|
|
285
|
+
createInitiative(tempDir, TEST_INIT_ID, {
|
|
286
|
+
wus: [TEST_WU_ID_1, TEST_WU_ID_2, TEST_WU_ID_3],
|
|
287
|
+
});
|
|
288
|
+
// Create WUs with different lanes (parallelizable)
|
|
289
|
+
createWUForInitiative(tempDir, TEST_WU_ID_1, {
|
|
290
|
+
initiative: TEST_INIT_ID,
|
|
291
|
+
lane: 'Framework: CLI',
|
|
292
|
+
dependencies: [],
|
|
293
|
+
});
|
|
294
|
+
createWUForInitiative(tempDir, TEST_WU_ID_2, {
|
|
295
|
+
initiative: TEST_INIT_ID,
|
|
296
|
+
lane: 'Framework: Core',
|
|
297
|
+
dependencies: [],
|
|
298
|
+
});
|
|
299
|
+
createWUForInitiative(tempDir, TEST_WU_ID_3, {
|
|
300
|
+
initiative: TEST_INIT_ID,
|
|
301
|
+
lane: 'Content: Documentation',
|
|
302
|
+
dependencies: [],
|
|
303
|
+
});
|
|
304
|
+
// Act - Identify parallel WUs
|
|
305
|
+
const wus = [TEST_WU_ID_1, TEST_WU_ID_2, TEST_WU_ID_3].map((id) => {
|
|
306
|
+
const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${id}.yaml`);
|
|
307
|
+
return parseYAML(readFileSync(wuPath, 'utf-8'));
|
|
308
|
+
});
|
|
309
|
+
const parallelWUs = wus.filter((wu) => !wu.dependencies || wu.dependencies.length === 0);
|
|
310
|
+
// Assert - All three can run in parallel (different lanes, no dependencies)
|
|
311
|
+
expect(parallelWUs).toHaveLength(3);
|
|
312
|
+
});
|
|
313
|
+
it('should respect dependencies for wave ordering', () => {
|
|
314
|
+
// Arrange
|
|
315
|
+
process.chdir(tempDir);
|
|
316
|
+
createInitiative(tempDir, TEST_INIT_ID, {
|
|
317
|
+
wus: [TEST_WU_ID_1, TEST_WU_ID_2, TEST_WU_ID_3],
|
|
318
|
+
});
|
|
319
|
+
// WU-1 has no dependencies (wave 1)
|
|
320
|
+
createWUForInitiative(tempDir, TEST_WU_ID_1, {
|
|
321
|
+
initiative: TEST_INIT_ID,
|
|
322
|
+
dependencies: [],
|
|
323
|
+
});
|
|
324
|
+
// WU-2 depends on WU-1 (wave 2)
|
|
325
|
+
createWUForInitiative(tempDir, TEST_WU_ID_2, {
|
|
326
|
+
initiative: TEST_INIT_ID,
|
|
327
|
+
dependencies: [TEST_WU_ID_1],
|
|
328
|
+
});
|
|
329
|
+
// WU-3 depends on WU-2 (wave 3)
|
|
330
|
+
createWUForInitiative(tempDir, TEST_WU_ID_3, {
|
|
331
|
+
initiative: TEST_INIT_ID,
|
|
332
|
+
dependencies: [TEST_WU_ID_2],
|
|
333
|
+
});
|
|
334
|
+
// Act - Compute waves
|
|
335
|
+
const wus = [TEST_WU_ID_1, TEST_WU_ID_2, TEST_WU_ID_3].map((id) => {
|
|
336
|
+
const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${id}.yaml`);
|
|
337
|
+
return parseYAML(readFileSync(wuPath, 'utf-8'));
|
|
338
|
+
});
|
|
339
|
+
const wave1 = wus.filter((wu) => !wu.dependencies || wu.dependencies.length === 0);
|
|
340
|
+
const wave2 = wus.filter((wu) => wu.dependencies &&
|
|
341
|
+
wu.dependencies.length > 0 &&
|
|
342
|
+
wu.dependencies.every((dep) => wave1.some((w) => w.id === dep)));
|
|
343
|
+
const wave3 = wus.filter((wu) => wu.dependencies &&
|
|
344
|
+
wu.dependencies.length > 0 &&
|
|
345
|
+
wu.dependencies.every((dep) => wave2.some((w) => w.id === dep)));
|
|
346
|
+
// Assert
|
|
347
|
+
expect(wave1).toHaveLength(1);
|
|
348
|
+
expect(wave1[0].id).toBe(TEST_WU_ID_1);
|
|
349
|
+
expect(wave2).toHaveLength(1);
|
|
350
|
+
expect(wave2[0].id).toBe(TEST_WU_ID_2);
|
|
351
|
+
expect(wave3).toHaveLength(1);
|
|
352
|
+
expect(wave3[0].id).toBe(TEST_WU_ID_3);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
describe('complete initiative workflow', () => {
|
|
356
|
+
it('should execute full initiative lifecycle', () => {
|
|
357
|
+
// This test validates the complete initiative workflow:
|
|
358
|
+
// 1. Create initiative with phases
|
|
359
|
+
// 2. Add WUs to initiative
|
|
360
|
+
// 3. Execute WUs (simulated status changes)
|
|
361
|
+
// 4. Track progress
|
|
362
|
+
// 5. Complete initiative
|
|
363
|
+
// Arrange
|
|
364
|
+
process.chdir(tempDir);
|
|
365
|
+
// Step 1: Create initiative
|
|
366
|
+
const initPath = createInitiative(tempDir, TEST_INIT_ID, {
|
|
367
|
+
title: 'E2E Test Initiative',
|
|
368
|
+
status: 'open',
|
|
369
|
+
phases: [
|
|
370
|
+
{ name: 'Phase 1: Core', status: 'pending' },
|
|
371
|
+
{ name: 'Phase 2: Features', status: 'pending' },
|
|
372
|
+
],
|
|
373
|
+
wus: [],
|
|
374
|
+
});
|
|
375
|
+
expect(existsSync(initPath)).toBe(true);
|
|
376
|
+
// Step 2: Add WUs to initiative
|
|
377
|
+
createWUForInitiative(tempDir, TEST_WU_ID_1, {
|
|
378
|
+
initiative: TEST_INIT_ID,
|
|
379
|
+
phase: 1,
|
|
380
|
+
status: WU_STATUS.READY,
|
|
381
|
+
});
|
|
382
|
+
createWUForInitiative(tempDir, TEST_WU_ID_2, {
|
|
383
|
+
initiative: TEST_INIT_ID,
|
|
384
|
+
phase: 1,
|
|
385
|
+
dependencies: [TEST_WU_ID_1],
|
|
386
|
+
status: WU_STATUS.READY,
|
|
387
|
+
});
|
|
388
|
+
createWUForInitiative(tempDir, TEST_WU_ID_3, {
|
|
389
|
+
initiative: TEST_INIT_ID,
|
|
390
|
+
phase: 2,
|
|
391
|
+
status: WU_STATUS.READY,
|
|
392
|
+
});
|
|
393
|
+
let initDoc = parseYAML(readFileSync(initPath, 'utf-8'));
|
|
394
|
+
initDoc.wus = [TEST_WU_ID_1, TEST_WU_ID_2, TEST_WU_ID_3];
|
|
395
|
+
initDoc.phases[0].status = 'in_progress';
|
|
396
|
+
writeFileSync(initPath, stringifyYAML(initDoc));
|
|
397
|
+
// Step 3: Execute Phase 1 WUs
|
|
398
|
+
// Complete WU-1
|
|
399
|
+
const wu1Path = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID_1}.yaml`);
|
|
400
|
+
const wu1 = parseYAML(readFileSync(wu1Path, 'utf-8'));
|
|
401
|
+
wu1.status = WU_STATUS.DONE;
|
|
402
|
+
writeFileSync(wu1Path, stringifyYAML(wu1));
|
|
403
|
+
// Complete WU-2
|
|
404
|
+
const wu2Path = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID_2}.yaml`);
|
|
405
|
+
const wu2 = parseYAML(readFileSync(wu2Path, 'utf-8'));
|
|
406
|
+
wu2.status = WU_STATUS.DONE;
|
|
407
|
+
writeFileSync(wu2Path, stringifyYAML(wu2));
|
|
408
|
+
// Step 4: Check progress
|
|
409
|
+
const wuPaths = [
|
|
410
|
+
join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID_1}.yaml`),
|
|
411
|
+
join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID_2}.yaml`),
|
|
412
|
+
join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID_3}.yaml`),
|
|
413
|
+
];
|
|
414
|
+
const statuses = wuPaths.map((p) => parseYAML(readFileSync(p, 'utf-8')).status);
|
|
415
|
+
const doneCount = statuses.filter((s) => s === WU_STATUS.DONE).length;
|
|
416
|
+
expect(doneCount).toBe(2); // 2 out of 3 done
|
|
417
|
+
// Mark Phase 1 as done
|
|
418
|
+
initDoc = parseYAML(readFileSync(initPath, 'utf-8'));
|
|
419
|
+
initDoc.phases[0].status = 'done';
|
|
420
|
+
initDoc.phases[1].status = 'in_progress';
|
|
421
|
+
writeFileSync(initPath, stringifyYAML(initDoc));
|
|
422
|
+
// Complete WU-3 (Phase 2)
|
|
423
|
+
const wu3Path = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID_3}.yaml`);
|
|
424
|
+
const wu3 = parseYAML(readFileSync(wu3Path, 'utf-8'));
|
|
425
|
+
wu3.status = WU_STATUS.DONE;
|
|
426
|
+
writeFileSync(wu3Path, stringifyYAML(wu3));
|
|
427
|
+
// Step 5: Complete initiative
|
|
428
|
+
initDoc = parseYAML(readFileSync(initPath, 'utf-8'));
|
|
429
|
+
initDoc.phases[1].status = 'done';
|
|
430
|
+
initDoc.status = 'completed';
|
|
431
|
+
initDoc.completed_at = new Date().toISOString();
|
|
432
|
+
writeFileSync(initPath, stringifyYAML(initDoc));
|
|
433
|
+
// Final assertions
|
|
434
|
+
const finalInit = parseYAML(readFileSync(initPath, 'utf-8'));
|
|
435
|
+
expect(finalInit.status).toBe('completed');
|
|
436
|
+
expect(finalInit.phases.every((p) => p.status === 'done')).toBe(true);
|
|
437
|
+
const finalWuStatuses = wuPaths.map((p) => parseYAML(readFileSync(p, 'utf-8')).status);
|
|
438
|
+
expect(finalWuStatuses.every((s) => s === WU_STATUS.DONE)).toBe(true);
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
});
|
|
@@ -16,7 +16,6 @@ const TEST_INIT_DIR = 'docs/04-operations/tasks/initiatives';
|
|
|
16
16
|
const TEST_INIT_ID = 'INIT-001';
|
|
17
17
|
const TEST_INIT_PLAN_URI = `lumenflow://plans/${TEST_INIT_ID}-plan.md`;
|
|
18
18
|
const TEST_PLANS_DIR = 'docs/04-operations/plans';
|
|
19
|
-
// eslint-disable-next-line sonarjs/publicly-writable-directories -- test constant for bad path detection
|
|
20
19
|
const TEST_LUMENFLOW_HOME_BAD = '/tmp/lumenflow-home-should-not-be-used';
|
|
21
20
|
const TEST_INIT_SLUG = 'test-initiative';
|
|
22
21
|
const TEST_INIT_TITLE = 'Test Initiative';
|