@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,279 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file enforcement.test.ts
|
|
3
|
-
* Tests for Claude Code enforcement hooks (WU-1367)
|
|
4
|
-
*
|
|
5
|
-
* TDD: Write failing tests first, then implement.
|
|
6
|
-
*/
|
|
7
|
-
// Test file lint exceptions
|
|
8
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
9
|
-
import * as fs from 'node:fs';
|
|
10
|
-
// Mock fs before importing module under test
|
|
11
|
-
vi.mock('node:fs');
|
|
12
|
-
vi.mock('node:child_process');
|
|
13
|
-
const TEST_PROJECT_DIR = '/test/project';
|
|
14
|
-
const CONFIG_FILE_NAME = '.lumenflow.config.yaml';
|
|
15
|
-
describe('WU-1367: Enforcement Hooks Config Schema', () => {
|
|
16
|
-
describe('ClientConfigSchema enforcement block', () => {
|
|
17
|
-
it('should accept enforcement block under agents.clients.claude-code', async () => {
|
|
18
|
-
// Import dynamically to allow mocking
|
|
19
|
-
const { ClientConfigSchema } = await import('@lumenflow/core/dist/lumenflow-config-schema.js');
|
|
20
|
-
const config = {
|
|
21
|
-
preamble: 'CLAUDE.md',
|
|
22
|
-
skillsDir: '.claude/skills',
|
|
23
|
-
enforcement: {
|
|
24
|
-
hooks: true,
|
|
25
|
-
block_outside_worktree: true,
|
|
26
|
-
require_wu_for_edits: true,
|
|
27
|
-
warn_on_stop_without_wu_done: true,
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
const result = ClientConfigSchema.safeParse(config);
|
|
31
|
-
expect(result.success).toBe(true);
|
|
32
|
-
if (result.success) {
|
|
33
|
-
expect(result.data.enforcement).toEqual({
|
|
34
|
-
hooks: true,
|
|
35
|
-
block_outside_worktree: true,
|
|
36
|
-
require_wu_for_edits: true,
|
|
37
|
-
warn_on_stop_without_wu_done: true,
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
it('should default enforcement values to false when not specified', async () => {
|
|
42
|
-
const { ClientConfigSchema } = await import('@lumenflow/core/dist/lumenflow-config-schema.js');
|
|
43
|
-
const config = {
|
|
44
|
-
preamble: 'CLAUDE.md',
|
|
45
|
-
enforcement: {},
|
|
46
|
-
};
|
|
47
|
-
const result = ClientConfigSchema.safeParse(config);
|
|
48
|
-
expect(result.success).toBe(true);
|
|
49
|
-
if (result.success) {
|
|
50
|
-
expect(result.data.enforcement?.hooks).toBe(false);
|
|
51
|
-
expect(result.data.enforcement?.block_outside_worktree).toBe(false);
|
|
52
|
-
expect(result.data.enforcement?.require_wu_for_edits).toBe(false);
|
|
53
|
-
expect(result.data.enforcement?.warn_on_stop_without_wu_done).toBe(false);
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
it('should allow enforcement to be undefined', async () => {
|
|
57
|
-
const { ClientConfigSchema } = await import('@lumenflow/core/dist/lumenflow-config-schema.js');
|
|
58
|
-
const config = {
|
|
59
|
-
preamble: 'CLAUDE.md',
|
|
60
|
-
};
|
|
61
|
-
const result = ClientConfigSchema.safeParse(config);
|
|
62
|
-
expect(result.success).toBe(true);
|
|
63
|
-
if (result.success) {
|
|
64
|
-
expect(result.data.enforcement).toBeUndefined();
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
describe('WU-1367: Hook Generation', () => {
|
|
70
|
-
beforeEach(() => {
|
|
71
|
-
vi.resetAllMocks();
|
|
72
|
-
});
|
|
73
|
-
describe('generateEnforcementHooks', () => {
|
|
74
|
-
it('should generate PreToolUse hook for Write/Edit blocking when block_outside_worktree=true', async () => {
|
|
75
|
-
const { generateEnforcementHooks } = await import('../../hooks/enforcement-generator.js');
|
|
76
|
-
const config = {
|
|
77
|
-
block_outside_worktree: true,
|
|
78
|
-
require_wu_for_edits: false,
|
|
79
|
-
warn_on_stop_without_wu_done: false,
|
|
80
|
-
};
|
|
81
|
-
const hooks = generateEnforcementHooks(config);
|
|
82
|
-
expect(hooks.preToolUse).toBeDefined();
|
|
83
|
-
expect(hooks.preToolUse?.length).toBeGreaterThan(0);
|
|
84
|
-
expect(hooks.preToolUse?.[0].matcher).toBe('Write|Edit');
|
|
85
|
-
});
|
|
86
|
-
it('should generate PreToolUse hook for WU requirement when require_wu_for_edits=true', async () => {
|
|
87
|
-
const { generateEnforcementHooks } = await import('../../hooks/enforcement-generator.js');
|
|
88
|
-
const config = {
|
|
89
|
-
block_outside_worktree: false,
|
|
90
|
-
require_wu_for_edits: true,
|
|
91
|
-
warn_on_stop_without_wu_done: false,
|
|
92
|
-
};
|
|
93
|
-
const hooks = generateEnforcementHooks(config);
|
|
94
|
-
expect(hooks.preToolUse).toBeDefined();
|
|
95
|
-
expect(hooks.preToolUse?.some((h) => h.matcher === 'Write|Edit')).toBe(true);
|
|
96
|
-
});
|
|
97
|
-
it('should generate Stop hook when warn_on_stop_without_wu_done=true', async () => {
|
|
98
|
-
const { generateEnforcementHooks } = await import('../../hooks/enforcement-generator.js');
|
|
99
|
-
const config = {
|
|
100
|
-
block_outside_worktree: false,
|
|
101
|
-
require_wu_for_edits: false,
|
|
102
|
-
warn_on_stop_without_wu_done: true,
|
|
103
|
-
};
|
|
104
|
-
const hooks = generateEnforcementHooks(config);
|
|
105
|
-
expect(hooks.stop).toBeDefined();
|
|
106
|
-
expect(hooks.stop?.length).toBeGreaterThan(0);
|
|
107
|
-
});
|
|
108
|
-
it('should return empty hooks when all enforcement options are false', async () => {
|
|
109
|
-
const { generateEnforcementHooks } = await import('../../hooks/enforcement-generator.js');
|
|
110
|
-
const config = {
|
|
111
|
-
block_outside_worktree: false,
|
|
112
|
-
require_wu_for_edits: false,
|
|
113
|
-
warn_on_stop_without_wu_done: false,
|
|
114
|
-
};
|
|
115
|
-
const hooks = generateEnforcementHooks(config);
|
|
116
|
-
expect(hooks.preToolUse).toBeUndefined();
|
|
117
|
-
expect(hooks.stop).toBeUndefined();
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
describe('WU-1367: Integrate Command', () => {
|
|
122
|
-
beforeEach(() => {
|
|
123
|
-
vi.resetAllMocks();
|
|
124
|
-
});
|
|
125
|
-
describe('integrateClaudeCode', () => {
|
|
126
|
-
it('should create .claude/hooks directory when enforcement.hooks=true', async () => {
|
|
127
|
-
const mockMkdirSync = vi.mocked(fs.mkdirSync);
|
|
128
|
-
vi.mocked(fs.writeFileSync);
|
|
129
|
-
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
130
|
-
const { integrateClaudeCode } = await import('../../commands/integrate.js');
|
|
131
|
-
const config = {
|
|
132
|
-
enforcement: {
|
|
133
|
-
hooks: true,
|
|
134
|
-
block_outside_worktree: true,
|
|
135
|
-
require_wu_for_edits: false,
|
|
136
|
-
warn_on_stop_without_wu_done: false,
|
|
137
|
-
},
|
|
138
|
-
};
|
|
139
|
-
await integrateClaudeCode(TEST_PROJECT_DIR, config);
|
|
140
|
-
expect(mockMkdirSync).toHaveBeenCalledWith(expect.stringContaining('.claude/hooks'), expect.any(Object));
|
|
141
|
-
});
|
|
142
|
-
it('should generate enforce-worktree.sh hook when block_outside_worktree=true', async () => {
|
|
143
|
-
const mockWriteFileSync = vi.mocked(fs.writeFileSync);
|
|
144
|
-
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
145
|
-
vi.mocked(fs.mkdirSync).mockReturnValue(undefined);
|
|
146
|
-
const { integrateClaudeCode } = await import('../../commands/integrate.js');
|
|
147
|
-
const config = {
|
|
148
|
-
enforcement: {
|
|
149
|
-
hooks: true,
|
|
150
|
-
block_outside_worktree: true,
|
|
151
|
-
require_wu_for_edits: false,
|
|
152
|
-
warn_on_stop_without_wu_done: false,
|
|
153
|
-
},
|
|
154
|
-
};
|
|
155
|
-
await integrateClaudeCode(TEST_PROJECT_DIR, config);
|
|
156
|
-
expect(mockWriteFileSync).toHaveBeenCalledWith(expect.stringContaining('enforce-worktree.sh'), expect.any(String), expect.any(Object));
|
|
157
|
-
});
|
|
158
|
-
it('should update settings.json with hook configuration', async () => {
|
|
159
|
-
const mockWriteFileSync = vi.mocked(fs.writeFileSync);
|
|
160
|
-
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
|
161
|
-
permissions: { allow: ['Bash'] },
|
|
162
|
-
}));
|
|
163
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
164
|
-
vi.mocked(fs.mkdirSync).mockReturnValue(undefined);
|
|
165
|
-
const { integrateClaudeCode } = await import('../../commands/integrate.js');
|
|
166
|
-
const config = {
|
|
167
|
-
enforcement: {
|
|
168
|
-
hooks: true,
|
|
169
|
-
block_outside_worktree: true,
|
|
170
|
-
require_wu_for_edits: false,
|
|
171
|
-
warn_on_stop_without_wu_done: false,
|
|
172
|
-
},
|
|
173
|
-
};
|
|
174
|
-
await integrateClaudeCode(TEST_PROJECT_DIR, config);
|
|
175
|
-
// Should write updated settings.json with hooks config
|
|
176
|
-
const settingsCall = mockWriteFileSync.mock.calls.find((call) => String(call[0]).includes('settings.json'));
|
|
177
|
-
expect(settingsCall).toBeDefined();
|
|
178
|
-
const settingsContent = JSON.parse(settingsCall[1]);
|
|
179
|
-
expect(settingsContent.hooks).toBeDefined();
|
|
180
|
-
expect(settingsContent.hooks.PreToolUse).toBeDefined();
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
describe('WU-1367: Hook Graceful Degradation', () => {
|
|
185
|
-
it('should allow operation when LumenFlow state cannot be determined', async () => {
|
|
186
|
-
// The hook should fail-open if it cannot determine LumenFlow state
|
|
187
|
-
// This prevents blocking legitimate work due to infrastructure issues
|
|
188
|
-
const { checkWorktreeEnforcement } = await import('../../hooks/enforcement-checks.js');
|
|
189
|
-
// Simulate missing .lumenflow directory
|
|
190
|
-
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
191
|
-
const result = await checkWorktreeEnforcement({
|
|
192
|
-
file_path: '/some/path/file.ts',
|
|
193
|
-
tool_name: 'Write',
|
|
194
|
-
});
|
|
195
|
-
// Should not block - graceful degradation
|
|
196
|
-
expect(result.allowed).toBe(true);
|
|
197
|
-
expect(result.reason).toContain('graceful');
|
|
198
|
-
});
|
|
199
|
-
it('should allow operation when worktree detection fails', async () => {
|
|
200
|
-
const { checkWorktreeEnforcement } = await import('../../hooks/enforcement-checks.js');
|
|
201
|
-
// Simulate git command failure
|
|
202
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
203
|
-
const mockExecFileSync = vi.fn().mockImplementation(() => {
|
|
204
|
-
throw new Error('git command failed');
|
|
205
|
-
});
|
|
206
|
-
vi.doMock('node:child_process', () => ({
|
|
207
|
-
execFileSync: mockExecFileSync,
|
|
208
|
-
}));
|
|
209
|
-
const result = await checkWorktreeEnforcement({
|
|
210
|
-
file_path: '/some/path/file.ts',
|
|
211
|
-
tool_name: 'Write',
|
|
212
|
-
});
|
|
213
|
-
// Should not block - graceful degradation
|
|
214
|
-
expect(result.allowed).toBe(true);
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
describe('WU-1367: Setup Hook Sync', () => {
|
|
218
|
-
it('should sync hooks when enforcement.hooks=true in config', async () => {
|
|
219
|
-
// This tests that pnpm setup syncs hooks appropriately
|
|
220
|
-
const mockWriteFileSync = vi.mocked(fs.writeFileSync);
|
|
221
|
-
vi.mocked(fs.mkdirSync).mockReturnValue(undefined);
|
|
222
|
-
// Mock existsSync to return false for most paths (so dirs get created)
|
|
223
|
-
// but return true for the config file
|
|
224
|
-
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
|
225
|
-
const pathStr = String(p);
|
|
226
|
-
return pathStr.endsWith(CONFIG_FILE_NAME);
|
|
227
|
-
});
|
|
228
|
-
// Config file is YAML, not JSON
|
|
229
|
-
vi.mocked(fs.readFileSync).mockImplementation((p) => {
|
|
230
|
-
const pathStr = String(p);
|
|
231
|
-
if (pathStr.endsWith(CONFIG_FILE_NAME)) {
|
|
232
|
-
return `
|
|
233
|
-
agents:
|
|
234
|
-
clients:
|
|
235
|
-
claude-code:
|
|
236
|
-
enforcement:
|
|
237
|
-
hooks: true
|
|
238
|
-
block_outside_worktree: true
|
|
239
|
-
`;
|
|
240
|
-
}
|
|
241
|
-
// Return empty JSON for settings.json
|
|
242
|
-
return '{}';
|
|
243
|
-
});
|
|
244
|
-
// Clear any previous calls
|
|
245
|
-
mockWriteFileSync.mockClear();
|
|
246
|
-
const { syncEnforcementHooks } = await import('../../hooks/enforcement-sync.js');
|
|
247
|
-
const result = await syncEnforcementHooks(TEST_PROJECT_DIR);
|
|
248
|
-
// Should have written hook files
|
|
249
|
-
expect(result).toBe(true);
|
|
250
|
-
expect(mockWriteFileSync).toHaveBeenCalled();
|
|
251
|
-
});
|
|
252
|
-
it('should skip hook sync when enforcement.hooks=false', async () => {
|
|
253
|
-
const mockWriteFileSync = vi.mocked(fs.writeFileSync);
|
|
254
|
-
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
|
255
|
-
const pathStr = String(p);
|
|
256
|
-
return pathStr.endsWith(CONFIG_FILE_NAME);
|
|
257
|
-
});
|
|
258
|
-
vi.mocked(fs.readFileSync).mockImplementation((p) => {
|
|
259
|
-
const pathStr = String(p);
|
|
260
|
-
if (pathStr.endsWith(CONFIG_FILE_NAME)) {
|
|
261
|
-
return `
|
|
262
|
-
agents:
|
|
263
|
-
clients:
|
|
264
|
-
claude-code:
|
|
265
|
-
enforcement:
|
|
266
|
-
hooks: false
|
|
267
|
-
`;
|
|
268
|
-
}
|
|
269
|
-
return '{}';
|
|
270
|
-
});
|
|
271
|
-
// Clear any previous calls
|
|
272
|
-
mockWriteFileSync.mockClear();
|
|
273
|
-
const { syncEnforcementHooks } = await import('../../hooks/enforcement-sync.js');
|
|
274
|
-
const result = await syncEnforcementHooks(TEST_PROJECT_DIR);
|
|
275
|
-
// Should NOT have written hook files
|
|
276
|
-
expect(result).toBe(false);
|
|
277
|
-
expect(mockWriteFileSync).not.toHaveBeenCalled();
|
|
278
|
-
});
|
|
279
|
-
});
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file init-config-lanes.test.ts
|
|
3
|
-
* Test: .lumenflow.config.yaml includes default lane definitions for parent lanes
|
|
4
|
-
*
|
|
5
|
-
* WU-1307: Fix lumenflow-init scaffolding
|
|
6
|
-
*
|
|
7
|
-
* The generated config must include lane definitions that match the parent
|
|
8
|
-
* lanes used in the documentation examples (Framework, Experience, Content, Operations).
|
|
9
|
-
* These lanes should have sensible defaults for code_paths and wip_limit.
|
|
10
|
-
*/
|
|
11
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
12
|
-
import * as fs from 'node:fs';
|
|
13
|
-
import * as path from 'node:path';
|
|
14
|
-
import * as os from 'node:os';
|
|
15
|
-
import YAML from 'yaml';
|
|
16
|
-
import { scaffoldProject } from '../init.js';
|
|
17
|
-
/** Config file name - extracted to avoid duplicate string lint errors */
|
|
18
|
-
const CONFIG_FILE_NAME = '.lumenflow.config.yaml';
|
|
19
|
-
describe('init config default lanes (WU-1307)', () => {
|
|
20
|
-
let tempDir;
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
// Create a temporary directory for each test
|
|
23
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-init-config-lanes-'));
|
|
24
|
-
});
|
|
25
|
-
afterEach(() => {
|
|
26
|
-
// Clean up temporary directory
|
|
27
|
-
if (tempDir && fs.existsSync(tempDir)) {
|
|
28
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
/** Helper to read and parse config from temp directory */
|
|
32
|
-
function readConfig() {
|
|
33
|
-
const configPath = path.join(tempDir, CONFIG_FILE_NAME);
|
|
34
|
-
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
35
|
-
return YAML.parse(configContent);
|
|
36
|
-
}
|
|
37
|
-
it('should generate .lumenflow.config.yaml with lanes.definitions', async () => {
|
|
38
|
-
// Arrange
|
|
39
|
-
const configPath = path.join(tempDir, CONFIG_FILE_NAME);
|
|
40
|
-
// Act
|
|
41
|
-
await scaffoldProject(tempDir, { force: true, full: true });
|
|
42
|
-
// Assert
|
|
43
|
-
expect(fs.existsSync(configPath)).toBe(true);
|
|
44
|
-
const config = readConfig();
|
|
45
|
-
// Should have lanes.definitions
|
|
46
|
-
expect(config.lanes).toBeDefined();
|
|
47
|
-
expect(config.lanes.definitions).toBeDefined();
|
|
48
|
-
expect(Array.isArray(config.lanes.definitions)).toBe(true);
|
|
49
|
-
});
|
|
50
|
-
it('should include Framework parent lane with sublanes', async () => {
|
|
51
|
-
// Act
|
|
52
|
-
await scaffoldProject(tempDir, { force: true, full: true });
|
|
53
|
-
// Assert
|
|
54
|
-
const config = readConfig();
|
|
55
|
-
const lanes = (config.lanes?.definitions || []);
|
|
56
|
-
const frameworkLanes = lanes.filter((l) => l.name.startsWith('Framework:'));
|
|
57
|
-
expect(frameworkLanes.length).toBeGreaterThan(0);
|
|
58
|
-
// Should have at least Framework: Core and Framework: CLI
|
|
59
|
-
const laneNames = frameworkLanes.map((l) => l.name);
|
|
60
|
-
expect(laneNames).toContain('Framework: Core');
|
|
61
|
-
expect(laneNames).toContain('Framework: CLI');
|
|
62
|
-
});
|
|
63
|
-
it('should include Experience parent lane for frontend work', async () => {
|
|
64
|
-
// Act
|
|
65
|
-
await scaffoldProject(tempDir, { force: true, full: true });
|
|
66
|
-
// Assert
|
|
67
|
-
const config = readConfig();
|
|
68
|
-
const lanes = (config.lanes?.definitions || []);
|
|
69
|
-
const experienceLanes = lanes.filter((l) => l.name.startsWith('Experience:'));
|
|
70
|
-
// Should have at least one Experience lane
|
|
71
|
-
expect(experienceLanes.length).toBeGreaterThan(0);
|
|
72
|
-
});
|
|
73
|
-
it('should include Content: Documentation lane', async () => {
|
|
74
|
-
// Act
|
|
75
|
-
await scaffoldProject(tempDir, { force: true, full: true });
|
|
76
|
-
// Assert
|
|
77
|
-
const config = readConfig();
|
|
78
|
-
const lanes = (config.lanes?.definitions || []);
|
|
79
|
-
const contentLane = lanes.find((l) => l.name === 'Content: Documentation');
|
|
80
|
-
expect(contentLane).toBeDefined();
|
|
81
|
-
expect(contentLane?.code_paths).toBeDefined();
|
|
82
|
-
expect(contentLane?.code_paths).toContain('docs/**');
|
|
83
|
-
});
|
|
84
|
-
it('should include Operations parent lanes', async () => {
|
|
85
|
-
// Act
|
|
86
|
-
await scaffoldProject(tempDir, { force: true, full: true });
|
|
87
|
-
// Assert
|
|
88
|
-
const config = readConfig();
|
|
89
|
-
const lanes = (config.lanes?.definitions || []);
|
|
90
|
-
const operationsLanes = lanes.filter((l) => l.name.startsWith('Operations:'));
|
|
91
|
-
expect(operationsLanes.length).toBeGreaterThan(0);
|
|
92
|
-
// Should have Infrastructure and CI/CD
|
|
93
|
-
const laneNames = operationsLanes.map((l) => l.name);
|
|
94
|
-
expect(laneNames).toContain('Operations: Infrastructure');
|
|
95
|
-
expect(laneNames).toContain('Operations: CI/CD');
|
|
96
|
-
});
|
|
97
|
-
it('should have wip_limit: 1 for code lanes by default', async () => {
|
|
98
|
-
// Act
|
|
99
|
-
await scaffoldProject(tempDir, { force: true, full: true });
|
|
100
|
-
// Assert
|
|
101
|
-
const config = readConfig();
|
|
102
|
-
const lanes = (config.lanes?.definitions || []);
|
|
103
|
-
const frameworkCore = lanes.find((l) => l.name === 'Framework: Core');
|
|
104
|
-
expect(frameworkCore).toBeDefined();
|
|
105
|
-
expect(frameworkCore?.wip_limit).toBe(1);
|
|
106
|
-
});
|
|
107
|
-
it('should have code_paths for each lane', async () => {
|
|
108
|
-
// Act
|
|
109
|
-
await scaffoldProject(tempDir, { force: true, full: true });
|
|
110
|
-
// Assert
|
|
111
|
-
const config = readConfig();
|
|
112
|
-
const lanes = (config.lanes?.definitions || []);
|
|
113
|
-
// Every lane should have code_paths
|
|
114
|
-
for (const lane of lanes) {
|
|
115
|
-
expect(lane.code_paths).toBeDefined();
|
|
116
|
-
expect(Array.isArray(lane.code_paths)).toBe(true);
|
|
117
|
-
expect(lane.code_paths?.length).toBeGreaterThan(0);
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
it('should use "Parent: Sublane" format for lane names', async () => {
|
|
121
|
-
// Act
|
|
122
|
-
await scaffoldProject(tempDir, { force: true, full: true });
|
|
123
|
-
// Assert
|
|
124
|
-
const config = readConfig();
|
|
125
|
-
const lanes = (config.lanes?.definitions || []);
|
|
126
|
-
// All lanes should follow "Parent: Sublane" format (colon + space)
|
|
127
|
-
for (const lane of lanes) {
|
|
128
|
-
expect(lane.name).toMatch(/^[A-Z][a-z]+: [A-Z]/);
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
});
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file init-docs-structure.test.ts
|
|
3
|
-
* Tests for --docs-structure flag and auto-detection (WU-1309)
|
|
4
|
-
*/
|
|
5
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
-
import * as fs from 'node:fs';
|
|
7
|
-
import * as path from 'node:path';
|
|
8
|
-
import * as os from 'node:os';
|
|
9
|
-
import { scaffoldProject, detectDocsStructure, getDocsPath, } from '../init.js';
|
|
10
|
-
// Constants to avoid duplicate strings (sonarjs/no-duplicate-string)
|
|
11
|
-
const ARC42_DOCS_STRUCTURE = 'arc42';
|
|
12
|
-
const SIMPLE_DOCS_STRUCTURE = 'simple';
|
|
13
|
-
const DOCS_04_OPERATIONS = '04-operations';
|
|
14
|
-
describe('docs-structure', () => {
|
|
15
|
-
let tempDir;
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-docs-structure-test-'));
|
|
18
|
-
});
|
|
19
|
-
afterEach(() => {
|
|
20
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
21
|
-
});
|
|
22
|
-
describe('detectDocsStructure', () => {
|
|
23
|
-
it('should return "arc42" when docs/04-operations exists', () => {
|
|
24
|
-
fs.mkdirSync(path.join(tempDir, 'docs', DOCS_04_OPERATIONS), { recursive: true });
|
|
25
|
-
const result = detectDocsStructure(tempDir);
|
|
26
|
-
expect(result).toBe(ARC42_DOCS_STRUCTURE);
|
|
27
|
-
});
|
|
28
|
-
it('should return "simple" when docs exists without 04-operations', () => {
|
|
29
|
-
fs.mkdirSync(path.join(tempDir, 'docs'), { recursive: true });
|
|
30
|
-
fs.writeFileSync(path.join(tempDir, 'docs', 'README.md'), '# Docs\n');
|
|
31
|
-
const result = detectDocsStructure(tempDir);
|
|
32
|
-
expect(result).toBe(SIMPLE_DOCS_STRUCTURE);
|
|
33
|
-
});
|
|
34
|
-
it('should return "simple" when no docs directory exists', () => {
|
|
35
|
-
const result = detectDocsStructure(tempDir);
|
|
36
|
-
expect(result).toBe(SIMPLE_DOCS_STRUCTURE);
|
|
37
|
-
});
|
|
38
|
-
it('should detect arc42 with any numbered directory (01-*, 02-*, etc.)', () => {
|
|
39
|
-
fs.mkdirSync(path.join(tempDir, 'docs', '01-introduction'), { recursive: true });
|
|
40
|
-
const result = detectDocsStructure(tempDir);
|
|
41
|
-
expect(result).toBe(ARC42_DOCS_STRUCTURE);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
describe('getDocsPath', () => {
|
|
45
|
-
it('should return simple paths for simple structure', () => {
|
|
46
|
-
const paths = getDocsPath(SIMPLE_DOCS_STRUCTURE);
|
|
47
|
-
expect(paths.operations).toBe('docs');
|
|
48
|
-
expect(paths.tasks).toBe('docs/tasks');
|
|
49
|
-
expect(paths.onboarding).toBe('docs/_frameworks/lumenflow/agent/onboarding');
|
|
50
|
-
});
|
|
51
|
-
it('should return arc42 paths for arc42 structure', () => {
|
|
52
|
-
const paths = getDocsPath(ARC42_DOCS_STRUCTURE);
|
|
53
|
-
expect(paths.operations).toBe('docs/04-operations');
|
|
54
|
-
expect(paths.tasks).toBe('docs/04-operations/tasks');
|
|
55
|
-
expect(paths.onboarding).toBe('docs/04-operations/_frameworks/lumenflow/agent/onboarding');
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
describe('scaffoldProject with --docs-structure', () => {
|
|
59
|
-
it('should scaffold simple structure with --docs-structure simple', async () => {
|
|
60
|
-
const options = {
|
|
61
|
-
force: false,
|
|
62
|
-
full: true,
|
|
63
|
-
docsStructure: SIMPLE_DOCS_STRUCTURE,
|
|
64
|
-
};
|
|
65
|
-
await scaffoldProject(tempDir, options);
|
|
66
|
-
// Simple structure: docs/tasks, not docs/04-operations/tasks
|
|
67
|
-
expect(fs.existsSync(path.join(tempDir, 'docs', 'tasks'))).toBe(true);
|
|
68
|
-
expect(fs.existsSync(path.join(tempDir, 'docs', DOCS_04_OPERATIONS))).toBe(false);
|
|
69
|
-
});
|
|
70
|
-
it('should scaffold WU template with resilient defaults (simple)', async () => {
|
|
71
|
-
const options = {
|
|
72
|
-
force: false,
|
|
73
|
-
full: true,
|
|
74
|
-
docsStructure: SIMPLE_DOCS_STRUCTURE,
|
|
75
|
-
};
|
|
76
|
-
await scaffoldProject(tempDir, options);
|
|
77
|
-
const templatePath = path.join(tempDir, 'docs', 'tasks', 'templates', 'wu-template.yaml');
|
|
78
|
-
expect(fs.existsSync(templatePath)).toBe(true);
|
|
79
|
-
const content = fs.readFileSync(templatePath, 'utf-8');
|
|
80
|
-
// Feature WUs should reference plan protocol by default (plan-less friendly).
|
|
81
|
-
expect(content).toContain('lumenflow://plans/WU-XXX-plan.md');
|
|
82
|
-
// Ensure non-empty notes to avoid strict spec-linter failures out of the box.
|
|
83
|
-
expect(content).not.toContain("notes: ''");
|
|
84
|
-
expect(content).toContain('notes:');
|
|
85
|
-
// Ensure manual test stub exists to prevent empty tests failures.
|
|
86
|
-
expect(content).toContain('Manual check:');
|
|
87
|
-
});
|
|
88
|
-
it('should scaffold arc42 structure with --docs-structure arc42', async () => {
|
|
89
|
-
const options = {
|
|
90
|
-
force: false,
|
|
91
|
-
full: true,
|
|
92
|
-
docsStructure: ARC42_DOCS_STRUCTURE,
|
|
93
|
-
};
|
|
94
|
-
await scaffoldProject(tempDir, options);
|
|
95
|
-
// Arc42 structure: docs/04-operations/tasks
|
|
96
|
-
expect(fs.existsSync(path.join(tempDir, 'docs', DOCS_04_OPERATIONS, 'tasks'))).toBe(true);
|
|
97
|
-
});
|
|
98
|
-
it('should scaffold WU template with resilient defaults (arc42)', async () => {
|
|
99
|
-
const options = {
|
|
100
|
-
force: false,
|
|
101
|
-
full: true,
|
|
102
|
-
docsStructure: ARC42_DOCS_STRUCTURE,
|
|
103
|
-
};
|
|
104
|
-
await scaffoldProject(tempDir, options);
|
|
105
|
-
const templatePath = path.join(tempDir, 'docs', DOCS_04_OPERATIONS, 'tasks', 'templates', 'wu-template.yaml');
|
|
106
|
-
expect(fs.existsSync(templatePath)).toBe(true);
|
|
107
|
-
const content = fs.readFileSync(templatePath, 'utf-8');
|
|
108
|
-
expect(content).toContain('lumenflow://plans/WU-XXX-plan.md');
|
|
109
|
-
expect(content).not.toContain("notes: ''");
|
|
110
|
-
expect(content).toContain('notes:');
|
|
111
|
-
expect(content).toContain('Manual check:');
|
|
112
|
-
});
|
|
113
|
-
it('should auto-detect arc42 when docs/04-operations exists', async () => {
|
|
114
|
-
// Create existing arc42 structure
|
|
115
|
-
fs.mkdirSync(path.join(tempDir, 'docs', DOCS_04_OPERATIONS), { recursive: true });
|
|
116
|
-
const options = {
|
|
117
|
-
force: false,
|
|
118
|
-
full: true,
|
|
119
|
-
// No docsStructure specified - should auto-detect arc42
|
|
120
|
-
};
|
|
121
|
-
await scaffoldProject(tempDir, options);
|
|
122
|
-
// Should use arc42 structure
|
|
123
|
-
expect(fs.existsSync(path.join(tempDir, 'docs', DOCS_04_OPERATIONS, 'tasks'))).toBe(true);
|
|
124
|
-
});
|
|
125
|
-
it('should auto-detect simple when only docs exists', async () => {
|
|
126
|
-
// Create existing simple structure
|
|
127
|
-
fs.mkdirSync(path.join(tempDir, 'docs'), { recursive: true });
|
|
128
|
-
fs.writeFileSync(path.join(tempDir, 'docs', 'README.md'), '# Docs\n');
|
|
129
|
-
const options = {
|
|
130
|
-
force: false,
|
|
131
|
-
full: true,
|
|
132
|
-
// No docsStructure specified - should auto-detect simple
|
|
133
|
-
};
|
|
134
|
-
await scaffoldProject(tempDir, options);
|
|
135
|
-
// Should use simple structure
|
|
136
|
-
expect(fs.existsSync(path.join(tempDir, 'docs', 'tasks'))).toBe(true);
|
|
137
|
-
expect(fs.existsSync(path.join(tempDir, 'docs', DOCS_04_OPERATIONS))).toBe(false);
|
|
138
|
-
});
|
|
139
|
-
it('should respect explicit --docs-structure over auto-detection', async () => {
|
|
140
|
-
// Create existing arc42 structure
|
|
141
|
-
fs.mkdirSync(path.join(tempDir, 'docs', DOCS_04_OPERATIONS), { recursive: true });
|
|
142
|
-
const options = {
|
|
143
|
-
force: true, // Force overwrite
|
|
144
|
-
full: true,
|
|
145
|
-
docsStructure: SIMPLE_DOCS_STRUCTURE, // Explicitly request simple
|
|
146
|
-
};
|
|
147
|
-
await scaffoldProject(tempDir, options);
|
|
148
|
-
// Should use simple structure despite arc42 existing
|
|
149
|
-
expect(fs.existsSync(path.join(tempDir, 'docs', 'tasks'))).toBe(true);
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
});
|