@lumenflow/cli 2.4.0 → 2.5.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 +11 -8
- package/dist/__tests__/init-config-lanes.test.js +131 -0
- package/dist/__tests__/init-docs-structure.test.js +119 -0
- package/dist/__tests__/init-lane-inference.test.js +125 -0
- package/dist/__tests__/init-onboarding-docs.test.js +132 -0
- package/dist/__tests__/init-quick-ref.test.js +145 -0
- package/dist/__tests__/init-scripts.test.js +207 -0
- package/dist/__tests__/init-template-portability.test.js +97 -0
- package/dist/__tests__/init.test.js +7 -2
- package/dist/__tests__/initiative-add-wu.test.js +420 -0
- package/dist/__tests__/initiative-plan-replacement.test.js +162 -0
- package/dist/__tests__/initiative-remove-wu.test.js +458 -0
- package/dist/__tests__/onboarding-smoke-test.test.js +211 -0
- package/dist/__tests__/path-centralization-cli.test.js +234 -0
- package/dist/__tests__/plan-create.test.js +126 -0
- package/dist/__tests__/plan-edit.test.js +157 -0
- package/dist/__tests__/plan-link.test.js +239 -0
- package/dist/__tests__/plan-promote.test.js +181 -0
- package/dist/__tests__/templates-sync.test.js +219 -0
- package/dist/__tests__/wu-create-strict.test.js +118 -0
- package/dist/__tests__/wu-edit-strict.test.js +109 -0
- package/dist/__tests__/wu-validate-strict.test.js +113 -0
- package/dist/flow-bottlenecks.js +4 -2
- package/dist/gates.js +22 -0
- package/dist/init.js +670 -87
- package/dist/initiative-add-wu.js +112 -16
- package/dist/initiative-remove-wu.js +248 -0
- package/dist/onboarding-smoke-test.js +400 -0
- package/dist/orchestrate-init-status.js +37 -9
- package/dist/orchestrate-initiative.js +10 -4
- package/dist/plan-create.js +199 -0
- package/dist/plan-edit.js +235 -0
- package/dist/plan-link.js +233 -0
- package/dist/plan-promote.js +231 -0
- package/dist/sync-templates.js +137 -5
- package/dist/wu-block.js +16 -5
- package/dist/wu-claim.js +15 -9
- package/dist/wu-create.js +50 -2
- package/dist/wu-deps.js +3 -1
- package/dist/wu-done.js +14 -5
- package/dist/wu-edit.js +35 -0
- package/dist/wu-prep.js +131 -8
- package/dist/wu-spawn.js +14 -1
- package/dist/wu-unblock.js +34 -2
- package/dist/wu-validate.js +25 -17
- package/package.json +11 -7
- package/templates/core/.lumenflow/constraints.md.template +61 -3
- package/templates/core/AGENTS.md.template +2 -2
- package/templates/core/LUMENFLOW.md.template +85 -23
- package/templates/core/ai/onboarding/agent-invocation-guide.md.template +157 -0
- package/templates/core/ai/onboarding/agent-safety-card.md.template +227 -0
- package/templates/core/ai/onboarding/docs-generation.md.template +277 -0
- package/templates/core/ai/onboarding/first-wu-mistakes.md.template +49 -7
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +343 -110
- package/templates/core/ai/onboarding/release-process.md.template +8 -2
- package/templates/core/ai/onboarding/starting-prompt.md.template +407 -0
- package/templates/core/ai/onboarding/test-ratchet.md.template +131 -0
- package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +91 -38
- package/templates/core/ai/onboarding/vendor-support.md.template +219 -0
- package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +13 -1
- package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +14 -16
- package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +48 -4
- package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +5 -1
- package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +19 -8
- package/dist/__tests__/init-plan.test.js +0 -340
- package/dist/agent-issues-query.d.ts +0 -16
- package/dist/agent-log-issue.d.ts +0 -10
- package/dist/agent-session-end.d.ts +0 -10
- package/dist/agent-session.d.ts +0 -10
- package/dist/backlog-prune.d.ts +0 -84
- package/dist/cli-entry-point.d.ts +0 -8
- package/dist/deps-add.d.ts +0 -91
- package/dist/deps-remove.d.ts +0 -17
- package/dist/docs-sync.d.ts +0 -50
- package/dist/file-delete.d.ts +0 -84
- package/dist/file-edit.d.ts +0 -82
- package/dist/file-read.d.ts +0 -92
- package/dist/file-write.d.ts +0 -90
- package/dist/flow-bottlenecks.d.ts +0 -16
- package/dist/flow-report.d.ts +0 -16
- package/dist/gates.d.ts +0 -94
- package/dist/git-branch.d.ts +0 -65
- package/dist/git-diff.d.ts +0 -58
- package/dist/git-log.d.ts +0 -69
- package/dist/git-status.d.ts +0 -58
- package/dist/guard-locked.d.ts +0 -62
- package/dist/guard-main-branch.d.ts +0 -50
- package/dist/guard-worktree-commit.d.ts +0 -59
- package/dist/index.d.ts +0 -10
- package/dist/init-plan.d.ts +0 -80
- package/dist/init-plan.js +0 -337
- package/dist/init.d.ts +0 -46
- package/dist/initiative-add-wu.d.ts +0 -22
- package/dist/initiative-bulk-assign-wus.d.ts +0 -16
- package/dist/initiative-create.d.ts +0 -28
- package/dist/initiative-edit.d.ts +0 -34
- package/dist/initiative-list.d.ts +0 -12
- package/dist/initiative-status.d.ts +0 -11
- package/dist/lumenflow-upgrade.d.ts +0 -103
- package/dist/mem-checkpoint.d.ts +0 -16
- package/dist/mem-cleanup.d.ts +0 -29
- package/dist/mem-create.d.ts +0 -17
- package/dist/mem-export.d.ts +0 -10
- package/dist/mem-inbox.d.ts +0 -35
- package/dist/mem-init.d.ts +0 -15
- package/dist/mem-ready.d.ts +0 -16
- package/dist/mem-signal.d.ts +0 -16
- package/dist/mem-start.d.ts +0 -16
- package/dist/mem-summarize.d.ts +0 -22
- package/dist/mem-triage.d.ts +0 -22
- package/dist/metrics-cli.d.ts +0 -90
- package/dist/metrics-snapshot.d.ts +0 -18
- package/dist/orchestrate-init-status.d.ts +0 -11
- package/dist/orchestrate-initiative.d.ts +0 -12
- package/dist/orchestrate-monitor.d.ts +0 -11
- package/dist/release.d.ts +0 -117
- package/dist/rotate-progress.d.ts +0 -48
- package/dist/session-coordinator.d.ts +0 -74
- package/dist/spawn-list.d.ts +0 -16
- package/dist/state-bootstrap.d.ts +0 -92
- package/dist/sync-templates.d.ts +0 -52
- package/dist/trace-gen.d.ts +0 -84
- package/dist/validate-agent-skills.d.ts +0 -50
- package/dist/validate-agent-sync.d.ts +0 -36
- package/dist/validate-backlog-sync.d.ts +0 -37
- package/dist/validate-skills-spec.d.ts +0 -40
- package/dist/validate.d.ts +0 -60
- package/dist/wu-block.d.ts +0 -16
- package/dist/wu-claim.d.ts +0 -74
- package/dist/wu-cleanup.d.ts +0 -35
- package/dist/wu-create.d.ts +0 -69
- package/dist/wu-delete.d.ts +0 -21
- package/dist/wu-deps.d.ts +0 -13
- package/dist/wu-done.d.ts +0 -225
- package/dist/wu-edit.d.ts +0 -63
- package/dist/wu-infer-lane.d.ts +0 -17
- package/dist/wu-preflight.d.ts +0 -47
- package/dist/wu-prune.d.ts +0 -16
- package/dist/wu-recover.d.ts +0 -37
- package/dist/wu-release.d.ts +0 -19
- package/dist/wu-repair.d.ts +0 -60
- package/dist/wu-spawn-completion.d.ts +0 -10
- package/dist/wu-spawn.d.ts +0 -192
- package/dist/wu-status.d.ts +0 -25
- package/dist/wu-unblock.d.ts +0 -16
- package/dist/wu-unlock-lane.d.ts +0 -19
- package/dist/wu-validate.d.ts +0 -16
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for initiative:remove-wu command (WU-1328)
|
|
3
|
+
*
|
|
4
|
+
* The initiative:remove-wu command unlinks a WU from an initiative bidirectionally:
|
|
5
|
+
* 1. Removes `initiative` field from WU YAML
|
|
6
|
+
* 2. Removes WU ID from initiative `wus: []` array
|
|
7
|
+
*
|
|
8
|
+
* Uses micro-worktree isolation for atomic operations.
|
|
9
|
+
*
|
|
10
|
+
* TDD: These tests are written BEFORE the implementation.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll } from 'vitest';
|
|
13
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { parseYAML, stringifyYAML } from '@lumenflow/core/dist/wu-yaml.js';
|
|
17
|
+
// Test constants to avoid lint warnings about duplicate strings
|
|
18
|
+
const TEST_WU_ID = 'WU-123';
|
|
19
|
+
const TEST_WU_ID_2 = 'WU-456';
|
|
20
|
+
const TEST_WU_ID_3 = 'WU-789';
|
|
21
|
+
const TEST_INIT_ID = 'INIT-001';
|
|
22
|
+
const TEST_INIT_ID_2 = 'INIT-002';
|
|
23
|
+
const TEST_LANE = 'Framework: CLI';
|
|
24
|
+
const WU_REL_PATH = 'docs/04-operations/tasks/wu';
|
|
25
|
+
const INIT_REL_PATH = 'docs/04-operations/tasks/initiatives';
|
|
26
|
+
const TEST_INIT_SLUG = 'test-initiative';
|
|
27
|
+
const TEST_INIT_TITLE = 'Test Initiative';
|
|
28
|
+
const TEST_INIT_STATUS = 'open';
|
|
29
|
+
const TEST_DATE = '2026-01-25';
|
|
30
|
+
// Pre-import the module to ensure coverage tracking includes the module itself
|
|
31
|
+
beforeAll(async () => {
|
|
32
|
+
await import('../initiative-remove-wu.js');
|
|
33
|
+
});
|
|
34
|
+
// Mock modules before importing the module under test
|
|
35
|
+
const mockGit = {
|
|
36
|
+
branch: vi.fn().mockResolvedValue({ current: 'main' }),
|
|
37
|
+
status: vi.fn().mockResolvedValue({ isClean: () => true }),
|
|
38
|
+
};
|
|
39
|
+
vi.mock('@lumenflow/core/dist/git-adapter.js', () => ({
|
|
40
|
+
getGitForCwd: vi.fn(() => mockGit),
|
|
41
|
+
}));
|
|
42
|
+
vi.mock('@lumenflow/core/dist/wu-helpers.js', () => ({
|
|
43
|
+
ensureOnMain: vi.fn().mockResolvedValue(undefined),
|
|
44
|
+
}));
|
|
45
|
+
vi.mock('@lumenflow/core/dist/micro-worktree.js', async (importOriginal) => {
|
|
46
|
+
const actual = await importOriginal();
|
|
47
|
+
return {
|
|
48
|
+
...actual,
|
|
49
|
+
withMicroWorktree: vi.fn(async ({ execute }) => {
|
|
50
|
+
// Simulate micro-worktree by executing in temp dir
|
|
51
|
+
const tempDir = join(tmpdir(), `init-remove-wu-test-${Date.now()}`);
|
|
52
|
+
mkdirSync(tempDir, { recursive: true });
|
|
53
|
+
try {
|
|
54
|
+
await execute({ worktreePath: tempDir });
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
// Cleanup handled by test
|
|
58
|
+
}
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
describe('initiative:remove-wu command', () => {
|
|
63
|
+
let tempDir;
|
|
64
|
+
let originalCwd;
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
tempDir = join(tmpdir(), `init-remove-wu-test-${Date.now()}`);
|
|
67
|
+
mkdirSync(tempDir, { recursive: true });
|
|
68
|
+
originalCwd = process.cwd();
|
|
69
|
+
});
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
process.chdir(originalCwd);
|
|
72
|
+
if (existsSync(tempDir)) {
|
|
73
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
vi.clearAllMocks();
|
|
76
|
+
});
|
|
77
|
+
describe('validateInitIdFormat', () => {
|
|
78
|
+
it('should accept valid INIT-NNN format', async () => {
|
|
79
|
+
const { validateInitIdFormat } = await import('../initiative-remove-wu.js');
|
|
80
|
+
// Should not throw
|
|
81
|
+
expect(() => validateInitIdFormat('INIT-001')).not.toThrow();
|
|
82
|
+
expect(() => validateInitIdFormat('INIT-123')).not.toThrow();
|
|
83
|
+
});
|
|
84
|
+
it('should accept valid INIT-NAME format', async () => {
|
|
85
|
+
const { validateInitIdFormat } = await import('../initiative-remove-wu.js');
|
|
86
|
+
expect(() => validateInitIdFormat('INIT-TOOLING')).not.toThrow();
|
|
87
|
+
expect(() => validateInitIdFormat('INIT-A1')).not.toThrow();
|
|
88
|
+
});
|
|
89
|
+
it('should reject invalid formats', async () => {
|
|
90
|
+
const { validateInitIdFormat } = await import('../initiative-remove-wu.js');
|
|
91
|
+
expect(() => validateInitIdFormat('init-001')).toThrow();
|
|
92
|
+
expect(() => validateInitIdFormat('INIT001')).toThrow();
|
|
93
|
+
expect(() => validateInitIdFormat('WU-001')).toThrow();
|
|
94
|
+
expect(() => validateInitIdFormat('')).toThrow();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe('validateWuIdFormat', () => {
|
|
98
|
+
it('should accept valid WU-NNN format', async () => {
|
|
99
|
+
const { validateWuIdFormat } = await import('../initiative-remove-wu.js');
|
|
100
|
+
expect(() => validateWuIdFormat('WU-123')).not.toThrow();
|
|
101
|
+
expect(() => validateWuIdFormat('WU-1')).not.toThrow();
|
|
102
|
+
expect(() => validateWuIdFormat('WU-99999')).not.toThrow();
|
|
103
|
+
});
|
|
104
|
+
it('should reject invalid formats', async () => {
|
|
105
|
+
const { validateWuIdFormat } = await import('../initiative-remove-wu.js');
|
|
106
|
+
expect(() => validateWuIdFormat('wu-123')).toThrow();
|
|
107
|
+
expect(() => validateWuIdFormat('WU123')).toThrow();
|
|
108
|
+
expect(() => validateWuIdFormat('INIT-001')).toThrow();
|
|
109
|
+
expect(() => validateWuIdFormat('')).toThrow();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe('checkWUExists', () => {
|
|
113
|
+
it('should return WU doc if found', async () => {
|
|
114
|
+
const { checkWUExists } = await import('../initiative-remove-wu.js');
|
|
115
|
+
// Create a mock WU file
|
|
116
|
+
const wuDir = join(tempDir, WU_REL_PATH);
|
|
117
|
+
mkdirSync(wuDir, { recursive: true });
|
|
118
|
+
const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
|
|
119
|
+
const wuDoc = {
|
|
120
|
+
id: TEST_WU_ID,
|
|
121
|
+
title: 'Test WU',
|
|
122
|
+
lane: TEST_LANE,
|
|
123
|
+
status: 'ready',
|
|
124
|
+
initiative: TEST_INIT_ID,
|
|
125
|
+
};
|
|
126
|
+
writeFileSync(wuPath, stringifyYAML(wuDoc));
|
|
127
|
+
process.chdir(tempDir);
|
|
128
|
+
const result = checkWUExists(TEST_WU_ID);
|
|
129
|
+
expect(result.id).toBe(TEST_WU_ID);
|
|
130
|
+
expect(result.initiative).toBe(TEST_INIT_ID);
|
|
131
|
+
});
|
|
132
|
+
it('should throw if WU not found', async () => {
|
|
133
|
+
const { checkWUExists } = await import('../initiative-remove-wu.js');
|
|
134
|
+
process.chdir(tempDir);
|
|
135
|
+
expect(() => checkWUExists('WU-999')).toThrow();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('checkInitiativeExists', () => {
|
|
139
|
+
it('should return initiative doc if found', async () => {
|
|
140
|
+
const { checkInitiativeExists } = await import('../initiative-remove-wu.js');
|
|
141
|
+
// Create a mock initiative file
|
|
142
|
+
const initDir = join(tempDir, INIT_REL_PATH);
|
|
143
|
+
mkdirSync(initDir, { recursive: true });
|
|
144
|
+
const initPath = join(initDir, `${TEST_INIT_ID}.yaml`);
|
|
145
|
+
const initDoc = {
|
|
146
|
+
id: TEST_INIT_ID,
|
|
147
|
+
slug: TEST_INIT_SLUG,
|
|
148
|
+
title: TEST_INIT_TITLE,
|
|
149
|
+
status: TEST_INIT_STATUS,
|
|
150
|
+
created: TEST_DATE,
|
|
151
|
+
wus: [TEST_WU_ID, TEST_WU_ID_2],
|
|
152
|
+
};
|
|
153
|
+
writeFileSync(initPath, stringifyYAML(initDoc));
|
|
154
|
+
process.chdir(tempDir);
|
|
155
|
+
const result = checkInitiativeExists(TEST_INIT_ID);
|
|
156
|
+
expect(result.id).toBe(TEST_INIT_ID);
|
|
157
|
+
expect(result.wus).toContain(TEST_WU_ID);
|
|
158
|
+
});
|
|
159
|
+
it('should throw if initiative not found', async () => {
|
|
160
|
+
const { checkInitiativeExists } = await import('../initiative-remove-wu.js');
|
|
161
|
+
process.chdir(tempDir);
|
|
162
|
+
expect(() => checkInitiativeExists('INIT-999')).toThrow();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
describe('checkWUIsLinked', () => {
|
|
166
|
+
it('should return true if WU is linked to initiative', async () => {
|
|
167
|
+
const { checkWUIsLinked } = await import('../initiative-remove-wu.js');
|
|
168
|
+
const wuDoc = { initiative: TEST_INIT_ID };
|
|
169
|
+
const initDoc = { wus: [TEST_WU_ID, TEST_WU_ID_2] };
|
|
170
|
+
expect(checkWUIsLinked(wuDoc, initDoc, TEST_WU_ID, TEST_INIT_ID)).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
it('should return false if WU is not linked (no initiative field)', async () => {
|
|
173
|
+
const { checkWUIsLinked } = await import('../initiative-remove-wu.js');
|
|
174
|
+
const wuDoc = { initiative: undefined }; // No initiative field
|
|
175
|
+
const initDoc = { wus: [TEST_WU_ID_2] };
|
|
176
|
+
expect(checkWUIsLinked(wuDoc, initDoc, TEST_WU_ID, TEST_INIT_ID)).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
it('should return false if WU is not in initiative wus list', async () => {
|
|
179
|
+
const { checkWUIsLinked } = await import('../initiative-remove-wu.js');
|
|
180
|
+
const wuDoc = { initiative: TEST_INIT_ID };
|
|
181
|
+
const initDoc = { wus: [TEST_WU_ID_2] }; // TEST_WU_ID not in list
|
|
182
|
+
expect(checkWUIsLinked(wuDoc, initDoc, TEST_WU_ID, TEST_INIT_ID)).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
it('should return false if WU is linked to different initiative', async () => {
|
|
185
|
+
const { checkWUIsLinked } = await import('../initiative-remove-wu.js');
|
|
186
|
+
const wuDoc = { initiative: TEST_INIT_ID_2 }; // Different initiative
|
|
187
|
+
const initDoc = { wus: [TEST_WU_ID] };
|
|
188
|
+
expect(checkWUIsLinked(wuDoc, initDoc, TEST_WU_ID, TEST_INIT_ID)).toBe(false);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
describe('updateWUInWorktree (remove initiative)', () => {
|
|
192
|
+
it('should remove initiative field from WU', async () => {
|
|
193
|
+
const { updateWUInWorktree } = await import('../initiative-remove-wu.js');
|
|
194
|
+
// Setup mock WU
|
|
195
|
+
const wuDir = join(tempDir, WU_REL_PATH);
|
|
196
|
+
mkdirSync(wuDir, { recursive: true });
|
|
197
|
+
const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
|
|
198
|
+
const wuDoc = {
|
|
199
|
+
id: TEST_WU_ID,
|
|
200
|
+
title: 'Test WU',
|
|
201
|
+
lane: TEST_LANE,
|
|
202
|
+
status: 'in_progress',
|
|
203
|
+
initiative: TEST_INIT_ID,
|
|
204
|
+
};
|
|
205
|
+
writeFileSync(wuPath, stringifyYAML(wuDoc));
|
|
206
|
+
// Update WU
|
|
207
|
+
const changed = updateWUInWorktree(tempDir, TEST_WU_ID, TEST_INIT_ID);
|
|
208
|
+
expect(changed).toBe(true);
|
|
209
|
+
// Verify the file was updated
|
|
210
|
+
const updated = parseYAML(readFileSync(wuPath, 'utf-8'));
|
|
211
|
+
expect(updated.initiative).toBeUndefined();
|
|
212
|
+
});
|
|
213
|
+
it('should return false if initiative field does not exist (idempotent)', async () => {
|
|
214
|
+
const { updateWUInWorktree } = await import('../initiative-remove-wu.js');
|
|
215
|
+
// Setup mock WU without initiative field
|
|
216
|
+
const wuDir = join(tempDir, WU_REL_PATH);
|
|
217
|
+
mkdirSync(wuDir, { recursive: true });
|
|
218
|
+
const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
|
|
219
|
+
const wuDoc = {
|
|
220
|
+
id: TEST_WU_ID,
|
|
221
|
+
title: 'Test WU',
|
|
222
|
+
lane: TEST_LANE,
|
|
223
|
+
status: 'ready',
|
|
224
|
+
};
|
|
225
|
+
writeFileSync(wuPath, stringifyYAML(wuDoc));
|
|
226
|
+
// Update WU
|
|
227
|
+
const changed = updateWUInWorktree(tempDir, TEST_WU_ID, TEST_INIT_ID);
|
|
228
|
+
expect(changed).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
it('should return false if initiative field is different (idempotent)', async () => {
|
|
231
|
+
const { updateWUInWorktree } = await import('../initiative-remove-wu.js');
|
|
232
|
+
// Setup mock WU with different initiative
|
|
233
|
+
const wuDir = join(tempDir, WU_REL_PATH);
|
|
234
|
+
mkdirSync(wuDir, { recursive: true });
|
|
235
|
+
const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
|
|
236
|
+
const wuDoc = {
|
|
237
|
+
id: TEST_WU_ID,
|
|
238
|
+
title: 'Test WU',
|
|
239
|
+
lane: TEST_LANE,
|
|
240
|
+
status: 'ready',
|
|
241
|
+
initiative: TEST_INIT_ID_2, // Different initiative
|
|
242
|
+
};
|
|
243
|
+
writeFileSync(wuPath, stringifyYAML(wuDoc));
|
|
244
|
+
// Try to remove different initiative - should not change
|
|
245
|
+
const changed = updateWUInWorktree(tempDir, TEST_WU_ID, TEST_INIT_ID);
|
|
246
|
+
expect(changed).toBe(false);
|
|
247
|
+
// Verify the file was NOT updated
|
|
248
|
+
const updated = parseYAML(readFileSync(wuPath, 'utf-8'));
|
|
249
|
+
expect(updated.initiative).toBe(TEST_INIT_ID_2);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
describe('updateInitiativeInWorktree (remove WU)', () => {
|
|
253
|
+
it('should remove WU from initiative wus list', async () => {
|
|
254
|
+
const { updateInitiativeInWorktree } = await import('../initiative-remove-wu.js');
|
|
255
|
+
// Setup mock initiative
|
|
256
|
+
const initDir = join(tempDir, INIT_REL_PATH);
|
|
257
|
+
mkdirSync(initDir, { recursive: true });
|
|
258
|
+
const initPath = join(initDir, `${TEST_INIT_ID}.yaml`);
|
|
259
|
+
const initDoc = {
|
|
260
|
+
id: TEST_INIT_ID,
|
|
261
|
+
slug: TEST_INIT_SLUG,
|
|
262
|
+
title: TEST_INIT_TITLE,
|
|
263
|
+
status: TEST_INIT_STATUS,
|
|
264
|
+
created: TEST_DATE,
|
|
265
|
+
wus: [TEST_WU_ID, TEST_WU_ID_2, TEST_WU_ID_3],
|
|
266
|
+
};
|
|
267
|
+
writeFileSync(initPath, stringifyYAML(initDoc));
|
|
268
|
+
// Update initiative
|
|
269
|
+
const changed = updateInitiativeInWorktree(tempDir, TEST_INIT_ID, TEST_WU_ID_2);
|
|
270
|
+
expect(changed).toBe(true);
|
|
271
|
+
// Verify the file was updated
|
|
272
|
+
const updated = parseYAML(readFileSync(initPath, 'utf-8'));
|
|
273
|
+
expect(updated.wus).toEqual([TEST_WU_ID, TEST_WU_ID_3]);
|
|
274
|
+
expect(updated.wus).not.toContain(TEST_WU_ID_2);
|
|
275
|
+
});
|
|
276
|
+
it('should return false if WU not in list (idempotent)', async () => {
|
|
277
|
+
const { updateInitiativeInWorktree } = await import('../initiative-remove-wu.js');
|
|
278
|
+
// Setup mock initiative without the WU
|
|
279
|
+
const initDir = join(tempDir, INIT_REL_PATH);
|
|
280
|
+
mkdirSync(initDir, { recursive: true });
|
|
281
|
+
const initPath = join(initDir, `${TEST_INIT_ID}.yaml`);
|
|
282
|
+
const initDoc = {
|
|
283
|
+
id: TEST_INIT_ID,
|
|
284
|
+
slug: TEST_INIT_SLUG,
|
|
285
|
+
title: TEST_INIT_TITLE,
|
|
286
|
+
status: TEST_INIT_STATUS,
|
|
287
|
+
created: TEST_DATE,
|
|
288
|
+
wus: [TEST_WU_ID, TEST_WU_ID_3],
|
|
289
|
+
};
|
|
290
|
+
writeFileSync(initPath, stringifyYAML(initDoc));
|
|
291
|
+
// Try to remove WU that's not in list
|
|
292
|
+
const changed = updateInitiativeInWorktree(tempDir, TEST_INIT_ID, TEST_WU_ID_2);
|
|
293
|
+
expect(changed).toBe(false);
|
|
294
|
+
});
|
|
295
|
+
it('should handle empty wus array', async () => {
|
|
296
|
+
const { updateInitiativeInWorktree } = await import('../initiative-remove-wu.js');
|
|
297
|
+
// Setup mock initiative with empty wus array
|
|
298
|
+
const initDir = join(tempDir, INIT_REL_PATH);
|
|
299
|
+
mkdirSync(initDir, { recursive: true });
|
|
300
|
+
const initPath = join(initDir, `${TEST_INIT_ID}.yaml`);
|
|
301
|
+
const initDoc = {
|
|
302
|
+
id: TEST_INIT_ID,
|
|
303
|
+
slug: TEST_INIT_SLUG,
|
|
304
|
+
title: TEST_INIT_TITLE,
|
|
305
|
+
status: TEST_INIT_STATUS,
|
|
306
|
+
created: TEST_DATE,
|
|
307
|
+
wus: [],
|
|
308
|
+
};
|
|
309
|
+
writeFileSync(initPath, stringifyYAML(initDoc));
|
|
310
|
+
// Try to remove WU from empty list
|
|
311
|
+
const changed = updateInitiativeInWorktree(tempDir, TEST_INIT_ID, TEST_WU_ID);
|
|
312
|
+
expect(changed).toBe(false);
|
|
313
|
+
});
|
|
314
|
+
it('should handle missing wus array', async () => {
|
|
315
|
+
const { updateInitiativeInWorktree } = await import('../initiative-remove-wu.js');
|
|
316
|
+
// Setup mock initiative without wus array
|
|
317
|
+
const initDir = join(tempDir, INIT_REL_PATH);
|
|
318
|
+
mkdirSync(initDir, { recursive: true });
|
|
319
|
+
const initPath = join(initDir, `${TEST_INIT_ID}.yaml`);
|
|
320
|
+
const initDoc = {
|
|
321
|
+
id: TEST_INIT_ID,
|
|
322
|
+
slug: TEST_INIT_SLUG,
|
|
323
|
+
title: TEST_INIT_TITLE,
|
|
324
|
+
status: TEST_INIT_STATUS,
|
|
325
|
+
created: TEST_DATE,
|
|
326
|
+
// No wus field
|
|
327
|
+
};
|
|
328
|
+
writeFileSync(initPath, stringifyYAML(initDoc));
|
|
329
|
+
// Try to remove WU from non-existent list
|
|
330
|
+
const changed = updateInitiativeInWorktree(tempDir, TEST_INIT_ID, TEST_WU_ID);
|
|
331
|
+
expect(changed).toBe(false);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
describe('LOG_PREFIX', () => {
|
|
335
|
+
it('should use correct log prefix', async () => {
|
|
336
|
+
const { LOG_PREFIX } = await import('../initiative-remove-wu.js');
|
|
337
|
+
expect(LOG_PREFIX).toBe('[initiative:remove-wu]');
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
describe('OPERATION_NAME', () => {
|
|
341
|
+
it('should have correct operation name', async () => {
|
|
342
|
+
const { OPERATION_NAME } = await import('../initiative-remove-wu.js');
|
|
343
|
+
expect(OPERATION_NAME).toBe('initiative-remove-wu');
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
describe('initiative:remove-wu CLI integration', () => {
|
|
348
|
+
it('should require --initiative and --wu flags', async () => {
|
|
349
|
+
// This test verifies that the CLI requires both flags
|
|
350
|
+
const { WU_OPTIONS } = await import('@lumenflow/core/dist/arg-parser.js');
|
|
351
|
+
expect(WU_OPTIONS.initiative).toBeDefined();
|
|
352
|
+
expect(WU_OPTIONS.initiative.flags).toContain('--initiative');
|
|
353
|
+
expect(WU_OPTIONS.wu).toBeDefined();
|
|
354
|
+
expect(WU_OPTIONS.wu.flags).toContain('--wu');
|
|
355
|
+
});
|
|
356
|
+
it('should export main function for CLI entry', async () => {
|
|
357
|
+
const initRemoveWu = await import('../initiative-remove-wu.js');
|
|
358
|
+
expect(typeof initRemoveWu.main).toBe('function');
|
|
359
|
+
});
|
|
360
|
+
it('should export all required functions', async () => {
|
|
361
|
+
const initRemoveWu = await import('../initiative-remove-wu.js');
|
|
362
|
+
expect(typeof initRemoveWu.validateInitIdFormat).toBe('function');
|
|
363
|
+
expect(typeof initRemoveWu.validateWuIdFormat).toBe('function');
|
|
364
|
+
expect(typeof initRemoveWu.checkWUExists).toBe('function');
|
|
365
|
+
expect(typeof initRemoveWu.checkInitiativeExists).toBe('function');
|
|
366
|
+
expect(typeof initRemoveWu.checkWUIsLinked).toBe('function');
|
|
367
|
+
expect(typeof initRemoveWu.updateWUInWorktree).toBe('function');
|
|
368
|
+
expect(typeof initRemoveWu.updateInitiativeInWorktree).toBe('function');
|
|
369
|
+
expect(typeof initRemoveWu.LOG_PREFIX).toBe('string');
|
|
370
|
+
expect(typeof initRemoveWu.OPERATION_NAME).toBe('string');
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
/**
|
|
374
|
+
* Note on main() function testing:
|
|
375
|
+
*
|
|
376
|
+
* The main() function is intentionally not unit-tested because:
|
|
377
|
+
* 1. It calls die() which invokes process.exit() - difficult to mock without complex test infrastructure
|
|
378
|
+
* 2. It involves micro-worktree operations with git
|
|
379
|
+
* 3. All business logic functions it calls ARE thoroughly tested above
|
|
380
|
+
*
|
|
381
|
+
* The main() function is integration/orchestration code that composes the tested helper functions.
|
|
382
|
+
* Integration testing via subprocess (pnpm initiative:remove-wu) is the appropriate testing strategy for main().
|
|
383
|
+
*
|
|
384
|
+
* Coverage statistics:
|
|
385
|
+
* - All exported helper functions: ~100% coverage
|
|
386
|
+
* - main() function: Not unit tested (orchestration code)
|
|
387
|
+
* - Overall file coverage: ~50% (acceptable for CLI commands)
|
|
388
|
+
*/
|
|
389
|
+
/**
|
|
390
|
+
* WU-1333: Retry handling tests for initiative:remove-wu
|
|
391
|
+
*
|
|
392
|
+
* When origin/main moves during operation, the micro-worktree layer handles retry.
|
|
393
|
+
* When retries are exhausted, the error message should include actionable next steps.
|
|
394
|
+
*/
|
|
395
|
+
describe('initiative:remove-wu retry handling (WU-1333)', () => {
|
|
396
|
+
describe('isRetryExhaustionError', () => {
|
|
397
|
+
it('should detect retry exhaustion from error message', async () => {
|
|
398
|
+
const { isRetryExhaustionError } = await import('../initiative-remove-wu.js');
|
|
399
|
+
// Should detect retry exhaustion error
|
|
400
|
+
const retryError = new Error('Push failed after 3 attempts. Origin main may have significant traffic.');
|
|
401
|
+
expect(isRetryExhaustionError(retryError)).toBe(true);
|
|
402
|
+
});
|
|
403
|
+
it('should detect retry exhaustion with any attempt count', async () => {
|
|
404
|
+
const { isRetryExhaustionError } = await import('../initiative-remove-wu.js');
|
|
405
|
+
// Different attempt counts should still match
|
|
406
|
+
const error5 = new Error('Push failed after 5 attempts. Something.');
|
|
407
|
+
expect(isRetryExhaustionError(error5)).toBe(true);
|
|
408
|
+
const error1 = new Error('Push failed after 1 attempts. Something.');
|
|
409
|
+
expect(isRetryExhaustionError(error1)).toBe(true);
|
|
410
|
+
});
|
|
411
|
+
it('should not match other errors', async () => {
|
|
412
|
+
const { isRetryExhaustionError } = await import('../initiative-remove-wu.js');
|
|
413
|
+
const otherError = new Error('Some other error');
|
|
414
|
+
expect(isRetryExhaustionError(otherError)).toBe(false);
|
|
415
|
+
const networkError = new Error('Network unreachable');
|
|
416
|
+
expect(isRetryExhaustionError(networkError)).toBe(false);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
describe('formatRetryExhaustionError', () => {
|
|
420
|
+
it('should include actionable next steps', async () => {
|
|
421
|
+
const { formatRetryExhaustionError } = await import('../initiative-remove-wu.js');
|
|
422
|
+
const retryError = new Error('Push failed after 3 attempts. Origin main may have significant traffic.');
|
|
423
|
+
const formatted = formatRetryExhaustionError(retryError, TEST_WU_ID, TEST_INIT_ID);
|
|
424
|
+
// Should include the original error
|
|
425
|
+
expect(formatted).toContain('Push failed after 3 attempts');
|
|
426
|
+
// Should include next steps heading
|
|
427
|
+
expect(formatted).toContain('Next steps:');
|
|
428
|
+
// Should include actionable suggestions
|
|
429
|
+
expect(formatted).toContain('Wait a few seconds and retry');
|
|
430
|
+
expect(formatted).toContain('initiative:remove-wu');
|
|
431
|
+
});
|
|
432
|
+
it('should include the retry command', async () => {
|
|
433
|
+
const { formatRetryExhaustionError } = await import('../initiative-remove-wu.js');
|
|
434
|
+
const retryError = new Error('Push failed after 3 attempts.');
|
|
435
|
+
const formatted = formatRetryExhaustionError(retryError, TEST_WU_ID, TEST_INIT_ID);
|
|
436
|
+
// Should include command to retry
|
|
437
|
+
expect(formatted).toContain(`--wu ${TEST_WU_ID}`);
|
|
438
|
+
expect(formatted).toContain(`--initiative ${TEST_INIT_ID}`);
|
|
439
|
+
});
|
|
440
|
+
it('should suggest checking for concurrent agents', async () => {
|
|
441
|
+
const { formatRetryExhaustionError } = await import('../initiative-remove-wu.js');
|
|
442
|
+
const retryError = new Error('Push failed after 3 attempts.');
|
|
443
|
+
const formatted = formatRetryExhaustionError(retryError, TEST_WU_ID, TEST_INIT_ID);
|
|
444
|
+
// Should mention concurrent agents as possible cause
|
|
445
|
+
expect(formatted).toMatch(/concurrent|agent|traffic/i);
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
describe('exports for WU-1333', () => {
|
|
449
|
+
it('should export isRetryExhaustionError function', async () => {
|
|
450
|
+
const mod = await import('../initiative-remove-wu.js');
|
|
451
|
+
expect(typeof mod.isRetryExhaustionError).toBe('function');
|
|
452
|
+
});
|
|
453
|
+
it('should export formatRetryExhaustionError function', async () => {
|
|
454
|
+
const mod = await import('../initiative-remove-wu.js');
|
|
455
|
+
expect(typeof mod.formatRetryExhaustionError).toBe('function');
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
});
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file onboarding-smoke-test.test.ts
|
|
3
|
+
* Tests for onboarding smoke-test gate (WU-1315)
|
|
4
|
+
*
|
|
5
|
+
* This gate verifies the lumenflow init + wu:create flows work correctly
|
|
6
|
+
* by running them in an isolated temp directory.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import * as os from 'node:os';
|
|
12
|
+
// Import the smoke-test module
|
|
13
|
+
import { runOnboardingSmokeTest, validateInitScripts, validateLaneInferenceFormat, } from '../onboarding-smoke-test.js';
|
|
14
|
+
/** Constants for test files to avoid duplicate string literals */
|
|
15
|
+
const PACKAGE_JSON_FILE = 'package.json';
|
|
16
|
+
const LANE_INFERENCE_FILE = '.lumenflow.lane-inference.yaml';
|
|
17
|
+
const TEST_PROJECT_NAME = 'test-project';
|
|
18
|
+
describe('onboarding smoke-test gate (WU-1315)', () => {
|
|
19
|
+
let tempDir;
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
// Create a temporary directory for each test
|
|
22
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-smoke-test-'));
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
// Clean up temporary directory
|
|
26
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
27
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
describe('runOnboardingSmokeTest', () => {
|
|
31
|
+
it('should return success when all validations pass', async () => {
|
|
32
|
+
// This is an integration test - it runs the full smoke test
|
|
33
|
+
const result = await runOnboardingSmokeTest({ tempDir });
|
|
34
|
+
expect(result.success).toBe(true);
|
|
35
|
+
expect(result.errors).toHaveLength(0);
|
|
36
|
+
});
|
|
37
|
+
it('should clean up temp directory after test', async () => {
|
|
38
|
+
// Run with a specific temp dir
|
|
39
|
+
const testTempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-smoke-cleanup-'));
|
|
40
|
+
await runOnboardingSmokeTest({ tempDir: testTempDir, cleanup: true });
|
|
41
|
+
// Temp dir should be cleaned up
|
|
42
|
+
expect(fs.existsSync(testTempDir)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
it('should preserve temp directory when cleanup is false', async () => {
|
|
45
|
+
const testTempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-smoke-preserve-'));
|
|
46
|
+
try {
|
|
47
|
+
await runOnboardingSmokeTest({ tempDir: testTempDir, cleanup: false });
|
|
48
|
+
// Temp dir should still exist
|
|
49
|
+
expect(fs.existsSync(testTempDir)).toBe(true);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
// Manual cleanup
|
|
53
|
+
if (fs.existsSync(testTempDir)) {
|
|
54
|
+
fs.rmSync(testTempDir, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
it('should report errors when validation fails', async () => {
|
|
59
|
+
// Create a directory without proper package.json scripts (init doesn't create them by default without --full)
|
|
60
|
+
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-smoke-invalid-'));
|
|
61
|
+
try {
|
|
62
|
+
// Create an empty package.json (missing required scripts)
|
|
63
|
+
fs.writeFileSync(path.join(testDir, PACKAGE_JSON_FILE), JSON.stringify({ name: 'test', scripts: {} }, null, 2));
|
|
64
|
+
// Run smoke test - should skip scaffolding and validate existing state
|
|
65
|
+
const result = validateInitScripts({ projectDir: testDir });
|
|
66
|
+
expect(result.valid).toBe(false);
|
|
67
|
+
expect(result.missingScripts.length).toBeGreaterThan(0);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
if (fs.existsSync(testDir)) {
|
|
71
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe('validateInitScripts', () => {
|
|
77
|
+
it('should pass when all required scripts are present', () => {
|
|
78
|
+
// Create package.json with required scripts
|
|
79
|
+
const packageJson = {
|
|
80
|
+
name: TEST_PROJECT_NAME,
|
|
81
|
+
scripts: {
|
|
82
|
+
'wu:claim': 'wu-claim',
|
|
83
|
+
'wu:done': 'wu-done',
|
|
84
|
+
'wu:create': 'wu-create',
|
|
85
|
+
gates: 'gates',
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
fs.writeFileSync(path.join(tempDir, PACKAGE_JSON_FILE), JSON.stringify(packageJson, null, 2));
|
|
89
|
+
const result = validateInitScripts({ projectDir: tempDir });
|
|
90
|
+
expect(result.valid).toBe(true);
|
|
91
|
+
expect(result.missingScripts).toHaveLength(0);
|
|
92
|
+
});
|
|
93
|
+
it('should fail when required scripts are missing', () => {
|
|
94
|
+
// Create package.json without LumenFlow scripts
|
|
95
|
+
const packageJson = {
|
|
96
|
+
name: TEST_PROJECT_NAME,
|
|
97
|
+
scripts: {
|
|
98
|
+
test: 'vitest',
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
fs.writeFileSync(path.join(tempDir, PACKAGE_JSON_FILE), JSON.stringify(packageJson, null, 2));
|
|
102
|
+
const result = validateInitScripts({ projectDir: tempDir });
|
|
103
|
+
expect(result.valid).toBe(false);
|
|
104
|
+
expect(result.missingScripts).toContain('wu:claim');
|
|
105
|
+
expect(result.missingScripts).toContain('wu:done');
|
|
106
|
+
expect(result.missingScripts).toContain('gates');
|
|
107
|
+
});
|
|
108
|
+
it('should fail when package.json does not exist', () => {
|
|
109
|
+
const result = validateInitScripts({ projectDir: tempDir });
|
|
110
|
+
expect(result.valid).toBe(false);
|
|
111
|
+
expect(result.error).toContain('package.json');
|
|
112
|
+
});
|
|
113
|
+
it('should verify scripts use standalone binary format', () => {
|
|
114
|
+
// Scripts should be 'wu-claim' not 'pnpm exec lumenflow wu:claim'
|
|
115
|
+
const packageJson = {
|
|
116
|
+
name: TEST_PROJECT_NAME,
|
|
117
|
+
scripts: {
|
|
118
|
+
'wu:claim': 'pnpm exec lumenflow wu:claim', // Wrong format
|
|
119
|
+
'wu:done': 'wu-done',
|
|
120
|
+
'wu:create': 'wu-create',
|
|
121
|
+
gates: 'gates',
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
fs.writeFileSync(path.join(tempDir, PACKAGE_JSON_FILE), JSON.stringify(packageJson, null, 2));
|
|
125
|
+
const result = validateInitScripts({ projectDir: tempDir });
|
|
126
|
+
expect(result.valid).toBe(false);
|
|
127
|
+
expect(result.invalidScripts).toContain('wu:claim');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe('validateLaneInferenceFormat', () => {
|
|
131
|
+
it('should pass when lane-inference.yaml has correct hierarchical format', () => {
|
|
132
|
+
// Create lane-inference.yaml with correct format
|
|
133
|
+
const laneInference = `# Lane Inference Configuration
|
|
134
|
+
Framework:
|
|
135
|
+
Core:
|
|
136
|
+
description: 'Core library'
|
|
137
|
+
code_paths:
|
|
138
|
+
- 'packages/core/**'
|
|
139
|
+
keywords:
|
|
140
|
+
- 'core'
|
|
141
|
+
|
|
142
|
+
Content:
|
|
143
|
+
Documentation:
|
|
144
|
+
description: 'Documentation'
|
|
145
|
+
code_paths:
|
|
146
|
+
- 'docs/**'
|
|
147
|
+
keywords:
|
|
148
|
+
- 'docs'
|
|
149
|
+
`;
|
|
150
|
+
fs.writeFileSync(path.join(tempDir, LANE_INFERENCE_FILE), laneInference);
|
|
151
|
+
const result = validateLaneInferenceFormat({ projectDir: tempDir });
|
|
152
|
+
expect(result.valid).toBe(true);
|
|
153
|
+
expect(result.errors).toHaveLength(0);
|
|
154
|
+
});
|
|
155
|
+
it('should fail when lane-inference.yaml uses flat lanes array', () => {
|
|
156
|
+
// Create lane-inference.yaml with old flat format
|
|
157
|
+
const laneInference = `# Lane Inference Configuration
|
|
158
|
+
lanes:
|
|
159
|
+
- name: Framework
|
|
160
|
+
code_paths:
|
|
161
|
+
- 'packages/**'
|
|
162
|
+
`;
|
|
163
|
+
fs.writeFileSync(path.join(tempDir, LANE_INFERENCE_FILE), laneInference);
|
|
164
|
+
const result = validateLaneInferenceFormat({ projectDir: tempDir });
|
|
165
|
+
expect(result.valid).toBe(false);
|
|
166
|
+
expect(result.errors.some((e) => e.includes('lanes'))).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
it('should fail when lane-inference.yaml does not exist', () => {
|
|
169
|
+
const result = validateLaneInferenceFormat({ projectDir: tempDir });
|
|
170
|
+
expect(result.valid).toBe(false);
|
|
171
|
+
expect(result.error).toContain('.lumenflow.lane-inference.yaml');
|
|
172
|
+
});
|
|
173
|
+
it('should validate parent lane names are capitalized', () => {
|
|
174
|
+
const laneInference = `# Lane Inference Configuration
|
|
175
|
+
framework: # Should be 'Framework'
|
|
176
|
+
core:
|
|
177
|
+
description: 'Core library'
|
|
178
|
+
code_paths:
|
|
179
|
+
- 'packages/core/**'
|
|
180
|
+
`;
|
|
181
|
+
fs.writeFileSync(path.join(tempDir, LANE_INFERENCE_FILE), laneInference);
|
|
182
|
+
const result = validateLaneInferenceFormat({ projectDir: tempDir });
|
|
183
|
+
expect(result.valid).toBe(false);
|
|
184
|
+
expect(result.errors.some((e) => e.includes('capitalized'))).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
it('should validate sub-lanes have required fields', () => {
|
|
187
|
+
const laneInference = `# Lane Inference Configuration
|
|
188
|
+
Framework:
|
|
189
|
+
Core:
|
|
190
|
+
# Missing description and code_paths
|
|
191
|
+
keywords:
|
|
192
|
+
- 'core'
|
|
193
|
+
`;
|
|
194
|
+
fs.writeFileSync(path.join(tempDir, LANE_INFERENCE_FILE), laneInference);
|
|
195
|
+
const result = validateLaneInferenceFormat({ projectDir: tempDir });
|
|
196
|
+
expect(result.valid).toBe(false);
|
|
197
|
+
expect(result.errors.some((e) => e.includes('description') || e.includes('code_paths'))).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
describe('wu:create with requireRemote=false', () => {
|
|
201
|
+
it('should run smoke test with requireRemote=false config', async () => {
|
|
202
|
+
const result = await runOnboardingSmokeTest({
|
|
203
|
+
tempDir,
|
|
204
|
+
skipWuCreate: false,
|
|
205
|
+
});
|
|
206
|
+
// The test should have validated wu:create works without a remote
|
|
207
|
+
expect(result.wuCreateValidation).toBeDefined();
|
|
208
|
+
expect(result.wuCreateValidation?.success).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|