@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,442 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for initiative:plan replacement by plan:link (WU-1313)
|
|
3
|
-
*
|
|
4
|
-
* Validates that the existing initiative:plan functionality is preserved
|
|
5
|
-
* when replaced by plan:link --target INIT-XXX.
|
|
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_INIT_DIR = 'docs/04-operations/tasks/initiatives';
|
|
16
|
-
const TEST_INIT_ID = 'INIT-001';
|
|
17
|
-
const TEST_INIT_PLAN_URI = `lumenflow://plans/${TEST_INIT_ID}-plan.md`;
|
|
18
|
-
const TEST_PLANS_DIR = 'docs/04-operations/plans';
|
|
19
|
-
const TEST_LUMENFLOW_HOME_BAD = '/tmp/lumenflow-home-should-not-be-used';
|
|
20
|
-
const TEST_INIT_SLUG = 'test-initiative';
|
|
21
|
-
const TEST_INIT_TITLE = 'Test Initiative';
|
|
22
|
-
const TEST_STATUS_OPEN = 'open';
|
|
23
|
-
const TEST_DATE = '2026-01-25';
|
|
24
|
-
// Mock modules before importing
|
|
25
|
-
vi.mock('@lumenflow/core/dist/git-adapter.js', () => ({
|
|
26
|
-
getGitForCwd: vi.fn(() => ({
|
|
27
|
-
branch: vi.fn().mockResolvedValue({ current: 'main' }),
|
|
28
|
-
status: vi.fn().mockResolvedValue({ isClean: () => true }),
|
|
29
|
-
})),
|
|
30
|
-
}));
|
|
31
|
-
vi.mock('@lumenflow/core/dist/wu-helpers.js', () => ({
|
|
32
|
-
ensureOnMain: vi.fn().mockResolvedValue(undefined),
|
|
33
|
-
}));
|
|
34
|
-
vi.mock('@lumenflow/core/dist/micro-worktree.js', () => ({
|
|
35
|
-
withMicroWorktree: vi.fn(async ({ execute }) => {
|
|
36
|
-
const tempDir = join(tmpdir(), `init-plan-replace-test-${Date.now()}`);
|
|
37
|
-
mkdirSync(tempDir, { recursive: true });
|
|
38
|
-
return execute({ worktreePath: tempDir });
|
|
39
|
-
}),
|
|
40
|
-
}));
|
|
41
|
-
describe('initiative:plan replaced by plan:link', () => {
|
|
42
|
-
let tempDir;
|
|
43
|
-
let originalCwd;
|
|
44
|
-
beforeEach(() => {
|
|
45
|
-
tempDir = join(tmpdir(), `init-plan-replace-test-${Date.now()}`);
|
|
46
|
-
mkdirSync(tempDir, { recursive: true });
|
|
47
|
-
originalCwd = process.cwd();
|
|
48
|
-
});
|
|
49
|
-
afterEach(() => {
|
|
50
|
-
process.chdir(originalCwd);
|
|
51
|
-
if (existsSync(tempDir)) {
|
|
52
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
53
|
-
}
|
|
54
|
-
vi.clearAllMocks();
|
|
55
|
-
});
|
|
56
|
-
describe('plan:link for initiatives (backwards compatibility)', () => {
|
|
57
|
-
it('should link plan to initiative via related_plan field', async () => {
|
|
58
|
-
const { linkPlanToInitiative } = await import('../plan-link.js');
|
|
59
|
-
// Setup mock initiative file
|
|
60
|
-
const initDir = join(tempDir, ...TEST_INIT_DIR.split('/'));
|
|
61
|
-
mkdirSync(initDir, { recursive: true });
|
|
62
|
-
const initPath = join(initDir, `${TEST_INIT_ID}.yaml`);
|
|
63
|
-
const initDoc = {
|
|
64
|
-
id: TEST_INIT_ID,
|
|
65
|
-
slug: TEST_INIT_SLUG,
|
|
66
|
-
title: TEST_INIT_TITLE,
|
|
67
|
-
status: TEST_STATUS_OPEN,
|
|
68
|
-
created: TEST_DATE,
|
|
69
|
-
};
|
|
70
|
-
writeFileSync(initPath, stringifyYAML(initDoc));
|
|
71
|
-
// Link plan (same operation as initiative:plan --initiative --plan)
|
|
72
|
-
const changed = linkPlanToInitiative(tempDir, TEST_INIT_ID, TEST_INIT_PLAN_URI);
|
|
73
|
-
expect(changed).toBe(true);
|
|
74
|
-
// Verify the file was updated
|
|
75
|
-
const updated = parseYAML(readFileSync(initPath, 'utf-8'));
|
|
76
|
-
expect(updated.related_plan).toBe(TEST_INIT_PLAN_URI);
|
|
77
|
-
});
|
|
78
|
-
it('should create plan and link in single operation', async () => {
|
|
79
|
-
const { createPlan } = await import('../plan-create.js');
|
|
80
|
-
const { linkPlanToInitiative } = await import('../plan-link.js');
|
|
81
|
-
// Setup mock initiative file
|
|
82
|
-
const initDir = join(tempDir, ...TEST_INIT_DIR.split('/'));
|
|
83
|
-
mkdirSync(initDir, { recursive: true });
|
|
84
|
-
const initPath = join(initDir, `${TEST_INIT_ID}.yaml`);
|
|
85
|
-
const initDoc = {
|
|
86
|
-
id: TEST_INIT_ID,
|
|
87
|
-
slug: TEST_INIT_SLUG,
|
|
88
|
-
title: TEST_INIT_TITLE,
|
|
89
|
-
status: TEST_STATUS_OPEN,
|
|
90
|
-
created: TEST_DATE,
|
|
91
|
-
};
|
|
92
|
-
writeFileSync(initPath, stringifyYAML(initDoc));
|
|
93
|
-
// Create plan (like initiative:plan --create)
|
|
94
|
-
const planPath = createPlan(tempDir, TEST_INIT_ID, TEST_INIT_TITLE);
|
|
95
|
-
expect(existsSync(planPath)).toBe(true);
|
|
96
|
-
// Link plan
|
|
97
|
-
const changed = linkPlanToInitiative(tempDir, TEST_INIT_ID, TEST_INIT_PLAN_URI);
|
|
98
|
-
expect(changed).toBe(true);
|
|
99
|
-
// Verify both plan file and initiative were updated
|
|
100
|
-
const updated = parseYAML(readFileSync(initPath, 'utf-8'));
|
|
101
|
-
expect(updated.related_plan).toBe(TEST_INIT_PLAN_URI);
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
describe('initiative:plan deprecation', () => {
|
|
105
|
-
it('should warn when using deprecated initiative:plan command', async () => {
|
|
106
|
-
// The initiative:plan command should still work but warn about deprecation
|
|
107
|
-
// and suggest using plan:link instead
|
|
108
|
-
const initPlan = await import('../initiative-plan.js');
|
|
109
|
-
expect(typeof initPlan.main).toBe('function');
|
|
110
|
-
// The deprecation warning will be in the main function
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
describe('plan:link auto-detection', () => {
|
|
114
|
-
it('should auto-detect INIT target and call linkPlanToInitiative', async () => {
|
|
115
|
-
const { resolveTargetType } = await import('../plan-link.js');
|
|
116
|
-
expect(resolveTargetType(TEST_INIT_ID)).toBe('initiative');
|
|
117
|
-
expect(resolveTargetType('INIT-TOOLING')).toBe('initiative');
|
|
118
|
-
});
|
|
119
|
-
it('should auto-detect WU target and call linkPlanToWU', async () => {
|
|
120
|
-
const { resolveTargetType } = await import('../plan-link.js');
|
|
121
|
-
expect(resolveTargetType('WU-1313')).toBe('wu');
|
|
122
|
-
expect(resolveTargetType('WU-001')).toBe('wu');
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
describe('plan storage defaults to repo plansDir', () => {
|
|
127
|
-
let tempDir;
|
|
128
|
-
let originalCwd;
|
|
129
|
-
beforeEach(() => {
|
|
130
|
-
tempDir = join(tmpdir(), `plan-storage-test-${Date.now()}`);
|
|
131
|
-
mkdirSync(tempDir, { recursive: true });
|
|
132
|
-
originalCwd = process.cwd();
|
|
133
|
-
});
|
|
134
|
-
afterEach(() => {
|
|
135
|
-
process.chdir(originalCwd);
|
|
136
|
-
if (existsSync(tempDir)) {
|
|
137
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
138
|
-
}
|
|
139
|
-
vi.clearAllMocks();
|
|
140
|
-
});
|
|
141
|
-
it('should create plans in repo directories.plansDir, not LUMENFLOW_HOME', async () => {
|
|
142
|
-
const { createPlan } = await import('../plan-create.js');
|
|
143
|
-
// Set LUMENFLOW_HOME to a different location (should be ignored)
|
|
144
|
-
const oldLfHome = process.env.LUMENFLOW_HOME;
|
|
145
|
-
process.env.LUMENFLOW_HOME = TEST_LUMENFLOW_HOME_BAD;
|
|
146
|
-
try {
|
|
147
|
-
const planPath = createPlan(tempDir, 'WU-1313', 'Test Plan');
|
|
148
|
-
// Plan should be in repo plansDir, not LUMENFLOW_HOME
|
|
149
|
-
expect(planPath).toContain(TEST_PLANS_DIR);
|
|
150
|
-
expect(planPath).not.toContain(TEST_LUMENFLOW_HOME_BAD);
|
|
151
|
-
}
|
|
152
|
-
finally {
|
|
153
|
-
if (oldLfHome === undefined) {
|
|
154
|
-
delete process.env.LUMENFLOW_HOME;
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
process.env.LUMENFLOW_HOME = oldLfHome;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
});
|