@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,968 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file init.test.ts
|
|
3
|
-
* Tests for lumenflow init command (WU-1171)
|
|
4
|
-
*
|
|
5
|
-
* Tests the new --merge mode, --client flag, AGENTS.md creation,
|
|
6
|
-
* and updated vendor overlay paths.
|
|
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 { scaffoldProject } from '../init.js';
|
|
13
|
-
// Constants to avoid sonarjs/no-duplicate-string
|
|
14
|
-
const LUMENFLOW_MD = 'LUMENFLOW.md';
|
|
15
|
-
const VENDOR_RULES_FILE = 'lumenflow.md';
|
|
16
|
-
// WU-1300: Additional constants for lint compliance
|
|
17
|
-
const ONBOARDING_DOCS_PATH = 'docs/04-operations/_frameworks/lumenflow/agent/onboarding';
|
|
18
|
-
const DOCS_OPS_DIR = 'docs/04-operations';
|
|
19
|
-
const PACKAGE_JSON_FILE = 'package.json';
|
|
20
|
-
describe('lumenflow init', () => {
|
|
21
|
-
let tempDir;
|
|
22
|
-
beforeEach(() => {
|
|
23
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-init-test-'));
|
|
24
|
-
});
|
|
25
|
-
afterEach(() => {
|
|
26
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
27
|
-
});
|
|
28
|
-
describe('AGENTS.md creation', () => {
|
|
29
|
-
it('should create AGENTS.md by default', async () => {
|
|
30
|
-
const options = {
|
|
31
|
-
force: false,
|
|
32
|
-
full: false,
|
|
33
|
-
};
|
|
34
|
-
await scaffoldProject(tempDir, options);
|
|
35
|
-
const agentsPath = path.join(tempDir, 'AGENTS.md');
|
|
36
|
-
expect(fs.existsSync(agentsPath)).toBe(true);
|
|
37
|
-
const content = fs.readFileSync(agentsPath, 'utf-8');
|
|
38
|
-
expect(content).toContain(LUMENFLOW_MD);
|
|
39
|
-
expect(content).toContain('universal');
|
|
40
|
-
});
|
|
41
|
-
it('should link AGENTS.md to LUMENFLOW.md', async () => {
|
|
42
|
-
const options = {
|
|
43
|
-
force: false,
|
|
44
|
-
full: false,
|
|
45
|
-
};
|
|
46
|
-
await scaffoldProject(tempDir, options);
|
|
47
|
-
const agentsContent = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
48
|
-
expect(agentsContent).toContain(`[${LUMENFLOW_MD}]`);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
describe('--client flag', () => {
|
|
52
|
-
it('should accept --client claude', async () => {
|
|
53
|
-
const options = {
|
|
54
|
-
force: false,
|
|
55
|
-
full: false,
|
|
56
|
-
client: 'claude',
|
|
57
|
-
};
|
|
58
|
-
const result = await scaffoldProject(tempDir, options);
|
|
59
|
-
expect(fs.existsSync(path.join(tempDir, 'CLAUDE.md'))).toBe(true);
|
|
60
|
-
expect(result.created).toContain('CLAUDE.md');
|
|
61
|
-
});
|
|
62
|
-
it('should accept --client cursor', async () => {
|
|
63
|
-
const options = {
|
|
64
|
-
force: false,
|
|
65
|
-
full: false,
|
|
66
|
-
client: 'cursor',
|
|
67
|
-
};
|
|
68
|
-
await scaffoldProject(tempDir, options);
|
|
69
|
-
// Cursor uses .cursor/rules/lumenflow.md (not .cursor/rules.md)
|
|
70
|
-
expect(fs.existsSync(path.join(tempDir, '.cursor', 'rules', VENDOR_RULES_FILE))).toBe(true);
|
|
71
|
-
});
|
|
72
|
-
it('should accept --client windsurf', async () => {
|
|
73
|
-
const options = {
|
|
74
|
-
force: false,
|
|
75
|
-
full: false,
|
|
76
|
-
client: 'windsurf',
|
|
77
|
-
};
|
|
78
|
-
await scaffoldProject(tempDir, options);
|
|
79
|
-
// Windsurf uses .windsurf/rules/lumenflow.md (not .windsurfrules)
|
|
80
|
-
expect(fs.existsSync(path.join(tempDir, '.windsurf', 'rules', VENDOR_RULES_FILE))).toBe(true);
|
|
81
|
-
});
|
|
82
|
-
it('should accept --client codex', async () => {
|
|
83
|
-
const options = {
|
|
84
|
-
force: false,
|
|
85
|
-
full: false,
|
|
86
|
-
client: 'codex',
|
|
87
|
-
};
|
|
88
|
-
await scaffoldProject(tempDir, options);
|
|
89
|
-
// Codex reads AGENTS.md directly, minimal extra config
|
|
90
|
-
expect(fs.existsSync(path.join(tempDir, 'AGENTS.md'))).toBe(true);
|
|
91
|
-
});
|
|
92
|
-
it('should accept --client all', async () => {
|
|
93
|
-
const options = {
|
|
94
|
-
force: false,
|
|
95
|
-
full: false,
|
|
96
|
-
client: 'all',
|
|
97
|
-
};
|
|
98
|
-
await scaffoldProject(tempDir, options);
|
|
99
|
-
// Should create all vendor files
|
|
100
|
-
expect(fs.existsSync(path.join(tempDir, 'AGENTS.md'))).toBe(true);
|
|
101
|
-
expect(fs.existsSync(path.join(tempDir, 'CLAUDE.md'))).toBe(true);
|
|
102
|
-
expect(fs.existsSync(path.join(tempDir, '.cursor', 'rules', VENDOR_RULES_FILE))).toBe(true);
|
|
103
|
-
expect(fs.existsSync(path.join(tempDir, '.windsurf', 'rules', VENDOR_RULES_FILE))).toBe(true);
|
|
104
|
-
});
|
|
105
|
-
it('should treat --vendor as alias for --client (backwards compatibility)', async () => {
|
|
106
|
-
const options = {
|
|
107
|
-
force: false,
|
|
108
|
-
full: false,
|
|
109
|
-
vendor: 'claude', // Using old --vendor flag
|
|
110
|
-
};
|
|
111
|
-
await scaffoldProject(tempDir, options);
|
|
112
|
-
expect(fs.existsSync(path.join(tempDir, 'CLAUDE.md'))).toBe(true);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
describe('--merge mode', () => {
|
|
116
|
-
it('should insert LUMENFLOW block into existing file', async () => {
|
|
117
|
-
// Create existing AGENTS.md
|
|
118
|
-
const existingContent = '# My Project Agents\n\nCustom content here.\n';
|
|
119
|
-
fs.writeFileSync(path.join(tempDir, 'AGENTS.md'), existingContent);
|
|
120
|
-
const options = {
|
|
121
|
-
force: false,
|
|
122
|
-
full: false,
|
|
123
|
-
merge: true,
|
|
124
|
-
};
|
|
125
|
-
await scaffoldProject(tempDir, options);
|
|
126
|
-
const content = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
127
|
-
// Should preserve original content
|
|
128
|
-
expect(content).toContain('# My Project Agents');
|
|
129
|
-
expect(content).toContain('Custom content here.');
|
|
130
|
-
// Should add LumenFlow block
|
|
131
|
-
expect(content).toContain('<!-- LUMENFLOW:START -->');
|
|
132
|
-
expect(content).toContain('<!-- LUMENFLOW:END -->');
|
|
133
|
-
expect(content).toContain(LUMENFLOW_MD);
|
|
134
|
-
});
|
|
135
|
-
it('should be idempotent (running twice produces no diff)', async () => {
|
|
136
|
-
// Create existing file
|
|
137
|
-
fs.writeFileSync(path.join(tempDir, 'AGENTS.md'), '# My Project\n');
|
|
138
|
-
const options = {
|
|
139
|
-
force: false,
|
|
140
|
-
full: false,
|
|
141
|
-
merge: true,
|
|
142
|
-
};
|
|
143
|
-
// First run
|
|
144
|
-
await scaffoldProject(tempDir, options);
|
|
145
|
-
const firstContent = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
146
|
-
// Second run
|
|
147
|
-
await scaffoldProject(tempDir, options);
|
|
148
|
-
const secondContent = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
149
|
-
expect(firstContent).toBe(secondContent);
|
|
150
|
-
});
|
|
151
|
-
it('should preserve CRLF line endings', async () => {
|
|
152
|
-
// Create existing file with CRLF
|
|
153
|
-
const existingContent = '# My Project\r\n\r\nWindows style.\r\n';
|
|
154
|
-
fs.writeFileSync(path.join(tempDir, 'AGENTS.md'), existingContent);
|
|
155
|
-
const options = {
|
|
156
|
-
force: false,
|
|
157
|
-
full: false,
|
|
158
|
-
merge: true,
|
|
159
|
-
};
|
|
160
|
-
await scaffoldProject(tempDir, options);
|
|
161
|
-
const content = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
162
|
-
// Should preserve CRLF
|
|
163
|
-
expect(content).toContain('\r\n');
|
|
164
|
-
// Should not have mixed line endings (standalone LF without preceding CR)
|
|
165
|
-
const lfCount = (content.match(/(?<!\r)\n/g) || []).length;
|
|
166
|
-
expect(lfCount).toBe(0);
|
|
167
|
-
});
|
|
168
|
-
it('should warn on malformed markers and append fresh block', async () => {
|
|
169
|
-
// Create file with only START marker (malformed)
|
|
170
|
-
const malformedContent = '# My Project\n\n<!-- LUMENFLOW:START -->\nOrphan block\n';
|
|
171
|
-
fs.writeFileSync(path.join(tempDir, 'AGENTS.md'), malformedContent);
|
|
172
|
-
const options = {
|
|
173
|
-
force: false,
|
|
174
|
-
full: false,
|
|
175
|
-
merge: true,
|
|
176
|
-
};
|
|
177
|
-
const result = await scaffoldProject(tempDir, options);
|
|
178
|
-
// Should have warnings about malformed markers
|
|
179
|
-
expect(result.warnings).toBeDefined();
|
|
180
|
-
expect(result.warnings?.some((w) => w.includes('malformed'))).toBe(true);
|
|
181
|
-
const content = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
182
|
-
// Should have complete block
|
|
183
|
-
expect(content).toContain('<!-- LUMENFLOW:END -->');
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
describe('createFile mode option', () => {
|
|
187
|
-
it('should skip existing files in skip mode (default)', async () => {
|
|
188
|
-
const existingContent = 'Original content';
|
|
189
|
-
fs.writeFileSync(path.join(tempDir, 'AGENTS.md'), existingContent);
|
|
190
|
-
const options = {
|
|
191
|
-
force: false,
|
|
192
|
-
full: false,
|
|
193
|
-
// Default mode is 'skip'
|
|
194
|
-
};
|
|
195
|
-
const result = await scaffoldProject(tempDir, options);
|
|
196
|
-
expect(result.skipped).toContain('AGENTS.md');
|
|
197
|
-
const content = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
198
|
-
expect(content).toBe(existingContent);
|
|
199
|
-
});
|
|
200
|
-
it('should overwrite in force mode', async () => {
|
|
201
|
-
fs.writeFileSync(path.join(tempDir, 'AGENTS.md'), 'Original');
|
|
202
|
-
const options = {
|
|
203
|
-
force: true,
|
|
204
|
-
full: false,
|
|
205
|
-
};
|
|
206
|
-
const result = await scaffoldProject(tempDir, options);
|
|
207
|
-
expect(result.created).toContain('AGENTS.md');
|
|
208
|
-
const content = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
209
|
-
expect(content).not.toBe('Original');
|
|
210
|
-
});
|
|
211
|
-
it('should merge in merge mode', async () => {
|
|
212
|
-
fs.writeFileSync(path.join(tempDir, 'AGENTS.md'), '# Custom Header\n');
|
|
213
|
-
const options = {
|
|
214
|
-
force: false,
|
|
215
|
-
full: false,
|
|
216
|
-
merge: true,
|
|
217
|
-
};
|
|
218
|
-
const result = await scaffoldProject(tempDir, options);
|
|
219
|
-
expect(result.merged).toContain('AGENTS.md');
|
|
220
|
-
const content = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
221
|
-
expect(content).toContain('# Custom Header');
|
|
222
|
-
expect(content).toContain('<!-- LUMENFLOW:START -->');
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
describe('vendor overlay paths', () => {
|
|
226
|
-
it('should use .cursor/rules/lumenflow.md for Cursor', async () => {
|
|
227
|
-
const options = {
|
|
228
|
-
force: false,
|
|
229
|
-
full: false,
|
|
230
|
-
client: 'cursor',
|
|
231
|
-
};
|
|
232
|
-
await scaffoldProject(tempDir, options);
|
|
233
|
-
// Should NOT create old path
|
|
234
|
-
expect(fs.existsSync(path.join(tempDir, '.cursor', 'rules.md'))).toBe(false);
|
|
235
|
-
// Should create new path
|
|
236
|
-
expect(fs.existsSync(path.join(tempDir, '.cursor', 'rules', VENDOR_RULES_FILE))).toBe(true);
|
|
237
|
-
});
|
|
238
|
-
it('should use .windsurf/rules/lumenflow.md for Windsurf', async () => {
|
|
239
|
-
const options = {
|
|
240
|
-
force: false,
|
|
241
|
-
full: false,
|
|
242
|
-
client: 'windsurf',
|
|
243
|
-
};
|
|
244
|
-
await scaffoldProject(tempDir, options);
|
|
245
|
-
// Should NOT create old path
|
|
246
|
-
expect(fs.existsSync(path.join(tempDir, '.windsurfrules'))).toBe(false);
|
|
247
|
-
// Should create new path
|
|
248
|
-
expect(fs.existsSync(path.join(tempDir, '.windsurf', 'rules', VENDOR_RULES_FILE))).toBe(true);
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
describe('CLAUDE.md location', () => {
|
|
252
|
-
it('should create single CLAUDE.md at root only', async () => {
|
|
253
|
-
const options = {
|
|
254
|
-
force: false,
|
|
255
|
-
full: false,
|
|
256
|
-
client: 'claude',
|
|
257
|
-
};
|
|
258
|
-
await scaffoldProject(tempDir, options);
|
|
259
|
-
// Should create root CLAUDE.md
|
|
260
|
-
expect(fs.existsSync(path.join(tempDir, 'CLAUDE.md'))).toBe(true);
|
|
261
|
-
// Should NOT create .claude/CLAUDE.md (no duplication)
|
|
262
|
-
expect(fs.existsSync(path.join(tempDir, '.claude', 'CLAUDE.md'))).toBe(false);
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
// WU-1286: --full is now the default
|
|
266
|
-
describe('--full default and --minimal flag', () => {
|
|
267
|
-
it('should scaffold agent onboarding docs by default (full=true)', async () => {
|
|
268
|
-
const options = {
|
|
269
|
-
force: false,
|
|
270
|
-
full: true, // This is now the default when parsed
|
|
271
|
-
docsStructure: 'arc42', // WU-1309: Explicitly request arc42 for legacy test
|
|
272
|
-
};
|
|
273
|
-
await scaffoldProject(tempDir, options);
|
|
274
|
-
// Should create agent onboarding docs
|
|
275
|
-
const onboardingDir = path.join(tempDir, ONBOARDING_DOCS_PATH);
|
|
276
|
-
expect(fs.existsSync(path.join(onboardingDir, 'quick-ref-commands.md'))).toBe(true);
|
|
277
|
-
expect(fs.existsSync(path.join(onboardingDir, 'first-wu-mistakes.md'))).toBe(true);
|
|
278
|
-
expect(fs.existsSync(path.join(onboardingDir, 'troubleshooting-wu-done.md'))).toBe(true);
|
|
279
|
-
});
|
|
280
|
-
it('should skip agent onboarding docs when full=false (minimal mode)', async () => {
|
|
281
|
-
const options = {
|
|
282
|
-
force: false,
|
|
283
|
-
full: false, // Explicitly minimal
|
|
284
|
-
};
|
|
285
|
-
await scaffoldProject(tempDir, options);
|
|
286
|
-
// Should NOT create agent onboarding docs
|
|
287
|
-
const onboardingDir = path.join(tempDir, ONBOARDING_DOCS_PATH);
|
|
288
|
-
expect(fs.existsSync(path.join(onboardingDir, 'quick-ref-commands.md'))).toBe(false);
|
|
289
|
-
});
|
|
290
|
-
it('should still create core files in minimal mode', async () => {
|
|
291
|
-
const options = {
|
|
292
|
-
force: false,
|
|
293
|
-
full: false,
|
|
294
|
-
};
|
|
295
|
-
await scaffoldProject(tempDir, options);
|
|
296
|
-
// Core files should always be created
|
|
297
|
-
expect(fs.existsSync(path.join(tempDir, 'AGENTS.md'))).toBe(true);
|
|
298
|
-
expect(fs.existsSync(path.join(tempDir, LUMENFLOW_MD))).toBe(true);
|
|
299
|
-
expect(fs.existsSync(path.join(tempDir, '.lumenflow.config.yaml'))).toBe(true);
|
|
300
|
-
expect(fs.existsSync(path.join(tempDir, '.lumenflow', 'constraints.md'))).toBe(true);
|
|
301
|
-
});
|
|
302
|
-
});
|
|
303
|
-
// WU-1300: Scaffolding fixes and template portability
|
|
304
|
-
describe('WU-1300: scaffolding fixes', () => {
|
|
305
|
-
describe('lane-inference.yaml generation', () => {
|
|
306
|
-
it('should scaffold .lumenflow.lane-inference.yaml with --full', async () => {
|
|
307
|
-
const options = {
|
|
308
|
-
force: false,
|
|
309
|
-
full: true,
|
|
310
|
-
};
|
|
311
|
-
await scaffoldProject(tempDir, options);
|
|
312
|
-
const laneInferencePath = path.join(tempDir, '.lumenflow.lane-inference.yaml');
|
|
313
|
-
expect(fs.existsSync(laneInferencePath)).toBe(true);
|
|
314
|
-
const content = fs.readFileSync(laneInferencePath, 'utf-8');
|
|
315
|
-
// WU-1307: Should have hierarchical lane definitions (not flat lanes: array)
|
|
316
|
-
expect(content).toContain('Framework:');
|
|
317
|
-
expect(content).toContain('Content:');
|
|
318
|
-
expect(content).toContain('Operations:');
|
|
319
|
-
});
|
|
320
|
-
it('should scaffold lane-inference with framework-specific lanes when --framework is provided', async () => {
|
|
321
|
-
const options = {
|
|
322
|
-
force: false,
|
|
323
|
-
full: true,
|
|
324
|
-
framework: 'Next.js',
|
|
325
|
-
};
|
|
326
|
-
await scaffoldProject(tempDir, options);
|
|
327
|
-
const laneInferencePath = path.join(tempDir, '.lumenflow.lane-inference.yaml');
|
|
328
|
-
expect(fs.existsSync(laneInferencePath)).toBe(true);
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
describe('starting-prompt.md scaffolding', () => {
|
|
332
|
-
it('should scaffold starting-prompt.md in onboarding docs with --full', async () => {
|
|
333
|
-
const options = {
|
|
334
|
-
force: false,
|
|
335
|
-
full: true,
|
|
336
|
-
docsStructure: 'arc42', // WU-1309: Explicitly request arc42 for legacy test
|
|
337
|
-
};
|
|
338
|
-
await scaffoldProject(tempDir, options);
|
|
339
|
-
const onboardingDir = path.join(tempDir, ONBOARDING_DOCS_PATH);
|
|
340
|
-
const startingPromptPath = path.join(onboardingDir, 'starting-prompt.md');
|
|
341
|
-
expect(fs.existsSync(startingPromptPath)).toBe(true);
|
|
342
|
-
const content = fs.readFileSync(startingPromptPath, 'utf-8');
|
|
343
|
-
expect(content).toContain(LUMENFLOW_MD);
|
|
344
|
-
expect(content).toContain('constraints');
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
describe('template path portability', () => {
|
|
348
|
-
it('should not have absolute paths in generated templates', async () => {
|
|
349
|
-
const options = {
|
|
350
|
-
force: false,
|
|
351
|
-
full: true,
|
|
352
|
-
};
|
|
353
|
-
await scaffoldProject(tempDir, options);
|
|
354
|
-
// Check common files for absolute paths
|
|
355
|
-
const filesToCheck = ['AGENTS.md', LUMENFLOW_MD, '.lumenflow/constraints.md'];
|
|
356
|
-
for (const file of filesToCheck) {
|
|
357
|
-
const filePath = path.join(tempDir, file);
|
|
358
|
-
if (fs.existsSync(filePath)) {
|
|
359
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
360
|
-
// Should not contain absolute paths (unix home dirs or macOS user dirs)
|
|
361
|
-
// Build patterns dynamically to avoid triggering pre-commit hook
|
|
362
|
-
const homePattern = new RegExp('/' + 'home' + '/' + '\\w+');
|
|
363
|
-
const usersPattern = new RegExp('/' + 'Users' + '/' + '\\w+');
|
|
364
|
-
expect(content).not.toMatch(homePattern);
|
|
365
|
-
expect(content).not.toMatch(usersPattern);
|
|
366
|
-
// Should use <project-root> placeholder for project root references
|
|
367
|
-
// or relative paths like ./docs/
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
});
|
|
371
|
-
it('should use <project-root> placeholder in templates where project root is needed', async () => {
|
|
372
|
-
const options = {
|
|
373
|
-
force: false,
|
|
374
|
-
full: true,
|
|
375
|
-
};
|
|
376
|
-
await scaffoldProject(tempDir, options);
|
|
377
|
-
const agentsContent = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
378
|
-
// AGENTS.md should have placeholder for cd command back to project root
|
|
379
|
-
// Using {{PROJECT_ROOT}} token which gets replaced with actual path
|
|
380
|
-
expect(agentsContent).toMatch(/cd\s+[\w./\\${}]+/); // Should have cd command with path
|
|
381
|
-
});
|
|
382
|
-
});
|
|
383
|
-
describe('AGENTS.md quick-ref link', () => {
|
|
384
|
-
it('should have correct quick-ref-commands.md link in AGENTS.md when --full', async () => {
|
|
385
|
-
const options = {
|
|
386
|
-
force: false,
|
|
387
|
-
full: true,
|
|
388
|
-
docsStructure: 'arc42', // WU-1309: Explicitly request arc42 for legacy test
|
|
389
|
-
};
|
|
390
|
-
await scaffoldProject(tempDir, options);
|
|
391
|
-
const agentsContent = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
392
|
-
// If quick-ref is mentioned, link should point to correct location
|
|
393
|
-
// docs/04-operations/_frameworks/lumenflow/agent/onboarding/quick-ref-commands.md
|
|
394
|
-
if (agentsContent.includes('quick-ref')) {
|
|
395
|
-
expect(agentsContent).toContain(`${ONBOARDING_DOCS_PATH}/quick-ref-commands.md`);
|
|
396
|
-
}
|
|
397
|
-
});
|
|
398
|
-
});
|
|
399
|
-
describe('--docs-structure flag', () => {
|
|
400
|
-
it('should accept --docs-structure simple', async () => {
|
|
401
|
-
const options = {
|
|
402
|
-
force: false,
|
|
403
|
-
full: true,
|
|
404
|
-
docsStructure: 'simple',
|
|
405
|
-
};
|
|
406
|
-
await scaffoldProject(tempDir, options);
|
|
407
|
-
// Simple structure uses docs/ directly, not arc42 structure
|
|
408
|
-
expect(fs.existsSync(path.join(tempDir, 'docs'))).toBe(true);
|
|
409
|
-
});
|
|
410
|
-
it('should accept --docs-structure arc42', async () => {
|
|
411
|
-
const options = {
|
|
412
|
-
force: false,
|
|
413
|
-
full: true,
|
|
414
|
-
docsStructure: 'arc42',
|
|
415
|
-
};
|
|
416
|
-
await scaffoldProject(tempDir, options);
|
|
417
|
-
// Arc42 uses numbered directories: 01-*, 02-*, etc.
|
|
418
|
-
// The current default is arc42-style with 04-operations
|
|
419
|
-
const operationsDir = path.join(tempDir, DOCS_OPS_DIR);
|
|
420
|
-
expect(fs.existsSync(operationsDir)).toBe(true);
|
|
421
|
-
});
|
|
422
|
-
it('should auto-detect existing docs structure', async () => {
|
|
423
|
-
// Create existing simple structure
|
|
424
|
-
fs.mkdirSync(path.join(tempDir, 'docs'), { recursive: true });
|
|
425
|
-
fs.writeFileSync(path.join(tempDir, 'docs/README.md'), '# Docs\n');
|
|
426
|
-
const options = {
|
|
427
|
-
force: false,
|
|
428
|
-
full: true,
|
|
429
|
-
// No docsStructure specified - should auto-detect
|
|
430
|
-
};
|
|
431
|
-
await scaffoldProject(tempDir, options);
|
|
432
|
-
// Should preserve existing structure
|
|
433
|
-
expect(fs.existsSync(path.join(tempDir, 'docs/README.md'))).toBe(true);
|
|
434
|
-
});
|
|
435
|
-
});
|
|
436
|
-
describe('package.json scripts injection', () => {
|
|
437
|
-
it('should inject LumenFlow scripts into existing package.json', async () => {
|
|
438
|
-
// Create existing package.json
|
|
439
|
-
const existingPackageJson = {
|
|
440
|
-
name: 'test-project',
|
|
441
|
-
version: '1.0.0',
|
|
442
|
-
scripts: {
|
|
443
|
-
test: 'vitest',
|
|
444
|
-
build: 'tsc',
|
|
445
|
-
},
|
|
446
|
-
};
|
|
447
|
-
fs.writeFileSync(path.join(tempDir, PACKAGE_JSON_FILE), JSON.stringify(existingPackageJson, null, 2));
|
|
448
|
-
const options = {
|
|
449
|
-
force: false,
|
|
450
|
-
full: true,
|
|
451
|
-
};
|
|
452
|
-
await scaffoldProject(tempDir, options);
|
|
453
|
-
const packageJson = JSON.parse(fs.readFileSync(path.join(tempDir, PACKAGE_JSON_FILE), 'utf-8'));
|
|
454
|
-
// Should preserve existing scripts
|
|
455
|
-
expect(packageJson.scripts.test).toBe('vitest');
|
|
456
|
-
expect(packageJson.scripts.build).toBe('tsc');
|
|
457
|
-
// Should add LumenFlow scripts
|
|
458
|
-
expect(packageJson.scripts['wu:claim']).toBeDefined();
|
|
459
|
-
expect(packageJson.scripts['wu:done']).toBeDefined();
|
|
460
|
-
expect(packageJson.scripts.gates).toBeDefined();
|
|
461
|
-
});
|
|
462
|
-
it('should not overwrite existing LumenFlow scripts unless --force', async () => {
|
|
463
|
-
// Create existing package.json with custom wu:claim
|
|
464
|
-
const existingPackageJson = {
|
|
465
|
-
name: 'test-project',
|
|
466
|
-
scripts: {
|
|
467
|
-
'wu:claim': 'custom-claim-command',
|
|
468
|
-
},
|
|
469
|
-
};
|
|
470
|
-
fs.writeFileSync(path.join(tempDir, PACKAGE_JSON_FILE), JSON.stringify(existingPackageJson, null, 2));
|
|
471
|
-
const options = {
|
|
472
|
-
force: false,
|
|
473
|
-
full: true,
|
|
474
|
-
};
|
|
475
|
-
await scaffoldProject(tempDir, options);
|
|
476
|
-
const packageJson = JSON.parse(fs.readFileSync(path.join(tempDir, PACKAGE_JSON_FILE), 'utf-8'));
|
|
477
|
-
// Should preserve custom script
|
|
478
|
-
expect(packageJson.scripts['wu:claim']).toBe('custom-claim-command');
|
|
479
|
-
});
|
|
480
|
-
it('should create package.json with LumenFlow scripts if none exists', async () => {
|
|
481
|
-
const options = {
|
|
482
|
-
force: false,
|
|
483
|
-
full: true,
|
|
484
|
-
};
|
|
485
|
-
await scaffoldProject(tempDir, options);
|
|
486
|
-
const packageJsonPath = path.join(tempDir, PACKAGE_JSON_FILE);
|
|
487
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
488
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
489
|
-
expect(packageJson.scripts).toBeDefined();
|
|
490
|
-
}
|
|
491
|
-
});
|
|
492
|
-
});
|
|
493
|
-
});
|
|
494
|
-
// WU-1382: Improved templates for agent clarity
|
|
495
|
-
describe('WU-1382: improved templates for agent clarity', () => {
|
|
496
|
-
describe('CLAUDE.md template enhancements', () => {
|
|
497
|
-
it('should include CLI commands table inline in CLAUDE.md', async () => {
|
|
498
|
-
const options = {
|
|
499
|
-
force: false,
|
|
500
|
-
full: false,
|
|
501
|
-
client: 'claude',
|
|
502
|
-
};
|
|
503
|
-
await scaffoldProject(tempDir, options);
|
|
504
|
-
const claudeMdPath = path.join(tempDir, 'CLAUDE.md');
|
|
505
|
-
expect(fs.existsSync(claudeMdPath)).toBe(true);
|
|
506
|
-
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
507
|
-
// Should have CLI commands table with common commands
|
|
508
|
-
expect(content).toContain('| Command');
|
|
509
|
-
expect(content).toContain('wu:claim');
|
|
510
|
-
expect(content).toContain('wu:done');
|
|
511
|
-
expect(content).toContain('wu:status');
|
|
512
|
-
expect(content).toContain('gates');
|
|
513
|
-
});
|
|
514
|
-
it('should include warning about manual YAML editing in CLAUDE.md', async () => {
|
|
515
|
-
const options = {
|
|
516
|
-
force: false,
|
|
517
|
-
full: false,
|
|
518
|
-
client: 'claude',
|
|
519
|
-
};
|
|
520
|
-
await scaffoldProject(tempDir, options);
|
|
521
|
-
const claudeMdPath = path.join(tempDir, 'CLAUDE.md');
|
|
522
|
-
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
523
|
-
// Should warn against manual WU YAML edits
|
|
524
|
-
expect(content).toMatch(/do\s+not\s+(manually\s+)?edit|never\s+(manually\s+)?edit/i);
|
|
525
|
-
expect(content).toMatch(/wu.*yaml|yaml.*wu/i);
|
|
526
|
-
});
|
|
527
|
-
});
|
|
528
|
-
describe('config.yaml managed file header', () => {
|
|
529
|
-
it('should include managed file header in .lumenflow.config.yaml', async () => {
|
|
530
|
-
const options = {
|
|
531
|
-
force: false,
|
|
532
|
-
full: false,
|
|
533
|
-
};
|
|
534
|
-
await scaffoldProject(tempDir, options);
|
|
535
|
-
const configPath = path.join(tempDir, '.lumenflow.config.yaml');
|
|
536
|
-
expect(fs.existsSync(configPath)).toBe(true);
|
|
537
|
-
const content = fs.readFileSync(configPath, 'utf-8');
|
|
538
|
-
// Should have managed file header
|
|
539
|
-
expect(content).toMatch(/LUMENFLOW\s+MANAGED\s+FILE/i);
|
|
540
|
-
expect(content).toMatch(/do\s+not\s+(manually\s+)?edit/i);
|
|
541
|
-
});
|
|
542
|
-
});
|
|
543
|
-
describe('lane-inference.yaml managed file header', () => {
|
|
544
|
-
it('should include managed file header in .lumenflow.lane-inference.yaml', async () => {
|
|
545
|
-
const options = {
|
|
546
|
-
force: false,
|
|
547
|
-
full: true,
|
|
548
|
-
};
|
|
549
|
-
await scaffoldProject(tempDir, options);
|
|
550
|
-
const laneInferencePath = path.join(tempDir, '.lumenflow.lane-inference.yaml');
|
|
551
|
-
expect(fs.existsSync(laneInferencePath)).toBe(true);
|
|
552
|
-
const content = fs.readFileSync(laneInferencePath, 'utf-8');
|
|
553
|
-
// Should have managed file header
|
|
554
|
-
expect(content).toMatch(/LUMENFLOW\s+MANAGED\s+FILE/i);
|
|
555
|
-
expect(content).toMatch(/do\s+not\s+(manually\s+)?edit/i);
|
|
556
|
-
});
|
|
557
|
-
});
|
|
558
|
-
});
|
|
559
|
-
// WU-1383: CLI safeguards against manual file editing
|
|
560
|
-
describe('WU-1383: CLI safeguards for Claude client', () => {
|
|
561
|
-
const CONFIG_FILE_NAME = '.lumenflow.config.yaml';
|
|
562
|
-
describe('enforcement hooks enabled by default for --client claude', () => {
|
|
563
|
-
it('should add enforcement hooks config when --client claude is used', async () => {
|
|
564
|
-
const options = {
|
|
565
|
-
force: false,
|
|
566
|
-
full: false,
|
|
567
|
-
client: 'claude',
|
|
568
|
-
};
|
|
569
|
-
await scaffoldProject(tempDir, options);
|
|
570
|
-
const configPath = path.join(tempDir, CONFIG_FILE_NAME);
|
|
571
|
-
expect(fs.existsSync(configPath)).toBe(true);
|
|
572
|
-
const content = fs.readFileSync(configPath, 'utf-8');
|
|
573
|
-
// Should have enforcement hooks enabled for claude-code
|
|
574
|
-
expect(content).toContain('claude-code');
|
|
575
|
-
expect(content).toContain('enforcement');
|
|
576
|
-
expect(content).toContain('hooks: true');
|
|
577
|
-
});
|
|
578
|
-
it('should set block_outside_worktree to true by default for claude client', async () => {
|
|
579
|
-
const options = {
|
|
580
|
-
force: false,
|
|
581
|
-
full: false,
|
|
582
|
-
client: 'claude',
|
|
583
|
-
};
|
|
584
|
-
await scaffoldProject(tempDir, options);
|
|
585
|
-
const configPath = path.join(tempDir, CONFIG_FILE_NAME);
|
|
586
|
-
const content = fs.readFileSync(configPath, 'utf-8');
|
|
587
|
-
expect(content).toContain('block_outside_worktree: true');
|
|
588
|
-
});
|
|
589
|
-
it('should NOT add enforcement hooks for other clients like cursor', async () => {
|
|
590
|
-
const options = {
|
|
591
|
-
force: false,
|
|
592
|
-
full: false,
|
|
593
|
-
client: 'cursor',
|
|
594
|
-
};
|
|
595
|
-
await scaffoldProject(tempDir, options);
|
|
596
|
-
const configPath = path.join(tempDir, CONFIG_FILE_NAME);
|
|
597
|
-
const content = fs.readFileSync(configPath, 'utf-8');
|
|
598
|
-
// Should NOT have claude-code enforcement section (check for the nested enforcement block)
|
|
599
|
-
// Note: The default config has agents.defaultClient: claude-code, but no enforcement section
|
|
600
|
-
expect(content).not.toContain('block_outside_worktree');
|
|
601
|
-
expect(content).not.toMatch(/claude-code:\s*\n\s*enforcement/);
|
|
602
|
-
});
|
|
603
|
-
});
|
|
604
|
-
describe('warning when config already exists', () => {
|
|
605
|
-
it('should add warning to result when config yaml already exists', async () => {
|
|
606
|
-
// Create existing config file
|
|
607
|
-
fs.writeFileSync(path.join(tempDir, CONFIG_FILE_NAME), '# Existing config\ndirectories:\n tasksDir: docs/tasks\n');
|
|
608
|
-
const options = {
|
|
609
|
-
force: false,
|
|
610
|
-
full: false,
|
|
611
|
-
};
|
|
612
|
-
const result = await scaffoldProject(tempDir, options);
|
|
613
|
-
// Should have warning about existing config
|
|
614
|
-
expect(result.warnings).toBeDefined();
|
|
615
|
-
expect(result.warnings?.some((w) => w.includes('already exists'))).toBe(true);
|
|
616
|
-
// Warning should suggest CLI commands
|
|
617
|
-
expect(result.warnings?.some((w) => w.includes('CLI') || w.includes('lumenflow'))).toBe(true);
|
|
618
|
-
});
|
|
619
|
-
it('should skip config file when it already exists (not force)', async () => {
|
|
620
|
-
const existingContent = '# My custom config\n';
|
|
621
|
-
fs.writeFileSync(path.join(tempDir, CONFIG_FILE_NAME), existingContent);
|
|
622
|
-
const options = {
|
|
623
|
-
force: false,
|
|
624
|
-
full: false,
|
|
625
|
-
};
|
|
626
|
-
const result = await scaffoldProject(tempDir, options);
|
|
627
|
-
expect(result.skipped).toContain(CONFIG_FILE_NAME);
|
|
628
|
-
// Content should not be changed
|
|
629
|
-
const content = fs.readFileSync(path.join(tempDir, CONFIG_FILE_NAME), 'utf-8');
|
|
630
|
-
expect(content).toBe(existingContent);
|
|
631
|
-
});
|
|
632
|
-
});
|
|
633
|
-
describe('post-init output shows CLI commands prominently', () => {
|
|
634
|
-
// Note: These test the ScaffoldResult which contains info for the CLI output
|
|
635
|
-
// The main() function uses these to print output
|
|
636
|
-
it('should include CLI usage guidance in warnings when config exists', async () => {
|
|
637
|
-
fs.writeFileSync(path.join(tempDir, CONFIG_FILE_NAME), '# Existing\n');
|
|
638
|
-
const options = {
|
|
639
|
-
force: false,
|
|
640
|
-
full: false,
|
|
641
|
-
};
|
|
642
|
-
const result = await scaffoldProject(tempDir, options);
|
|
643
|
-
// Warning should mention CLI commands for editing config
|
|
644
|
-
expect(result.warnings?.some((w) => /pnpm|lumenflow|CLI/i.test(w))).toBe(true);
|
|
645
|
-
});
|
|
646
|
-
});
|
|
647
|
-
describe('warning message suggests CLI commands not manual editing', () => {
|
|
648
|
-
it('should warn users to use CLI commands instead of manual editing', async () => {
|
|
649
|
-
fs.writeFileSync(path.join(tempDir, CONFIG_FILE_NAME), '# Existing config\n');
|
|
650
|
-
const options = {
|
|
651
|
-
force: false,
|
|
652
|
-
full: false,
|
|
653
|
-
};
|
|
654
|
-
const result = await scaffoldProject(tempDir, options);
|
|
655
|
-
// Should have a warning that mentions not to manually edit
|
|
656
|
-
expect(result.warnings?.some((w) => w.includes('manual') || w.includes('CLI') || w.includes('lumenflow'))).toBe(true);
|
|
657
|
-
});
|
|
658
|
-
});
|
|
659
|
-
});
|
|
660
|
-
// WU-1362: Branch guard tests for init.ts
|
|
661
|
-
describe('WU-1362: branch guard for tracked file writes', () => {
|
|
662
|
-
it('should block scaffold when on main branch and targeting main checkout', async () => {
|
|
663
|
-
// This test verifies that scaffoldProject checks branch before writing
|
|
664
|
-
// Note: This test uses a temp directory (not on main), so it should pass
|
|
665
|
-
// The actual blocking only applies when targeting main checkout on main branch
|
|
666
|
-
const options = {
|
|
667
|
-
force: false,
|
|
668
|
-
full: false,
|
|
669
|
-
};
|
|
670
|
-
// Since we're in a temp dir, not on main branch, this should work
|
|
671
|
-
const result = await scaffoldProject(tempDir, options);
|
|
672
|
-
expect(result.created.length).toBeGreaterThan(0);
|
|
673
|
-
});
|
|
674
|
-
it('should allow scaffold in worktree directory', async () => {
|
|
675
|
-
// Simulate worktree-like path by creating directory structure
|
|
676
|
-
const worktreePath = path.join(tempDir, 'worktrees', 'operations-wu-999');
|
|
677
|
-
fs.mkdirSync(worktreePath, { recursive: true });
|
|
678
|
-
const options = {
|
|
679
|
-
force: false,
|
|
680
|
-
full: false,
|
|
681
|
-
};
|
|
682
|
-
// Should succeed when in worktree-like path
|
|
683
|
-
const result = await scaffoldProject(worktreePath, options);
|
|
684
|
-
expect(result.created.length).toBeGreaterThan(0);
|
|
685
|
-
});
|
|
686
|
-
});
|
|
687
|
-
// WU-1385: Include wu-sizing-guide.md in lumenflow init onboarding docs
|
|
688
|
-
describe('WU-1385: wu-sizing-guide.md scaffolding', () => {
|
|
689
|
-
describe('wu-sizing-guide.md creation with --full', () => {
|
|
690
|
-
it('should scaffold wu-sizing-guide.md in onboarding docs with --full', async () => {
|
|
691
|
-
const options = {
|
|
692
|
-
force: false,
|
|
693
|
-
full: true,
|
|
694
|
-
docsStructure: 'arc42',
|
|
695
|
-
};
|
|
696
|
-
await scaffoldProject(tempDir, options);
|
|
697
|
-
const onboardingDir = path.join(tempDir, ONBOARDING_DOCS_PATH);
|
|
698
|
-
const sizingGuidePath = path.join(onboardingDir, 'wu-sizing-guide.md');
|
|
699
|
-
expect(fs.existsSync(sizingGuidePath)).toBe(true);
|
|
700
|
-
});
|
|
701
|
-
it('should include key sizing guide content', async () => {
|
|
702
|
-
const options = {
|
|
703
|
-
force: false,
|
|
704
|
-
full: true,
|
|
705
|
-
docsStructure: 'arc42',
|
|
706
|
-
};
|
|
707
|
-
await scaffoldProject(tempDir, options);
|
|
708
|
-
const onboardingDir = path.join(tempDir, ONBOARDING_DOCS_PATH);
|
|
709
|
-
const sizingGuidePath = path.join(onboardingDir, 'wu-sizing-guide.md');
|
|
710
|
-
const content = fs.readFileSync(sizingGuidePath, 'utf-8');
|
|
711
|
-
// Should have key content from the sizing guide
|
|
712
|
-
expect(content).toContain('Complexity');
|
|
713
|
-
expect(content).toContain('Tool Calls');
|
|
714
|
-
expect(content).toContain('Context');
|
|
715
|
-
});
|
|
716
|
-
it('should not scaffold wu-sizing-guide.md with --minimal (full=false)', async () => {
|
|
717
|
-
const options = {
|
|
718
|
-
force: false,
|
|
719
|
-
full: false,
|
|
720
|
-
};
|
|
721
|
-
await scaffoldProject(tempDir, options);
|
|
722
|
-
const onboardingDir = path.join(tempDir, ONBOARDING_DOCS_PATH);
|
|
723
|
-
const sizingGuidePath = path.join(onboardingDir, 'wu-sizing-guide.md');
|
|
724
|
-
expect(fs.existsSync(sizingGuidePath)).toBe(false);
|
|
725
|
-
});
|
|
726
|
-
});
|
|
727
|
-
describe('starting-prompt.md references sizing guide', () => {
|
|
728
|
-
it('should reference wu-sizing-guide.md in starting-prompt.md', async () => {
|
|
729
|
-
const options = {
|
|
730
|
-
force: false,
|
|
731
|
-
full: true,
|
|
732
|
-
docsStructure: 'arc42',
|
|
733
|
-
};
|
|
734
|
-
await scaffoldProject(tempDir, options);
|
|
735
|
-
const onboardingDir = path.join(tempDir, ONBOARDING_DOCS_PATH);
|
|
736
|
-
const startingPromptPath = path.join(onboardingDir, 'starting-prompt.md');
|
|
737
|
-
const content = fs.readFileSync(startingPromptPath, 'utf-8');
|
|
738
|
-
// Should reference the sizing guide
|
|
739
|
-
expect(content).toContain('wu-sizing-guide.md');
|
|
740
|
-
});
|
|
741
|
-
});
|
|
742
|
-
});
|
|
743
|
-
// WU-1408: safe-git and pre-commit hook scaffolding
|
|
744
|
-
describe('WU-1408: safe-git and pre-commit scaffolding', () => {
|
|
745
|
-
describe('safe-git wrapper', () => {
|
|
746
|
-
it('should scaffold scripts/safe-git', async () => {
|
|
747
|
-
const options = {
|
|
748
|
-
force: false,
|
|
749
|
-
full: true,
|
|
750
|
-
};
|
|
751
|
-
await scaffoldProject(tempDir, options);
|
|
752
|
-
const safeGitPath = path.join(tempDir, 'scripts', 'safe-git');
|
|
753
|
-
expect(fs.existsSync(safeGitPath)).toBe(true);
|
|
754
|
-
});
|
|
755
|
-
it('should make safe-git executable', async () => {
|
|
756
|
-
const options = {
|
|
757
|
-
force: false,
|
|
758
|
-
full: true,
|
|
759
|
-
};
|
|
760
|
-
await scaffoldProject(tempDir, options);
|
|
761
|
-
const safeGitPath = path.join(tempDir, 'scripts', 'safe-git');
|
|
762
|
-
const stats = fs.statSync(safeGitPath);
|
|
763
|
-
// Check for executable bit (owner, group, or other)
|
|
764
|
-
// eslint-disable-next-line no-bitwise
|
|
765
|
-
const isExecutable = (stats.mode & 0o111) !== 0;
|
|
766
|
-
expect(isExecutable).toBe(true);
|
|
767
|
-
});
|
|
768
|
-
it('should include worktree remove block in safe-git', async () => {
|
|
769
|
-
const options = {
|
|
770
|
-
force: false,
|
|
771
|
-
full: true,
|
|
772
|
-
};
|
|
773
|
-
await scaffoldProject(tempDir, options);
|
|
774
|
-
const safeGitPath = path.join(tempDir, 'scripts', 'safe-git');
|
|
775
|
-
const content = fs.readFileSync(safeGitPath, 'utf-8');
|
|
776
|
-
expect(content).toContain('worktree');
|
|
777
|
-
expect(content).toContain('remove');
|
|
778
|
-
expect(content).toContain('BLOCKED');
|
|
779
|
-
});
|
|
780
|
-
it('should scaffold safe-git even in minimal mode', async () => {
|
|
781
|
-
const options = {
|
|
782
|
-
force: false,
|
|
783
|
-
full: false, // minimal mode
|
|
784
|
-
};
|
|
785
|
-
await scaffoldProject(tempDir, options);
|
|
786
|
-
const safeGitPath = path.join(tempDir, 'scripts', 'safe-git');
|
|
787
|
-
expect(fs.existsSync(safeGitPath)).toBe(true);
|
|
788
|
-
});
|
|
789
|
-
});
|
|
790
|
-
describe('pre-commit hook', () => {
|
|
791
|
-
it('should scaffold .husky/pre-commit', async () => {
|
|
792
|
-
const options = {
|
|
793
|
-
force: false,
|
|
794
|
-
full: true,
|
|
795
|
-
};
|
|
796
|
-
await scaffoldProject(tempDir, options);
|
|
797
|
-
const preCommitPath = path.join(tempDir, '.husky', 'pre-commit');
|
|
798
|
-
expect(fs.existsSync(preCommitPath)).toBe(true);
|
|
799
|
-
});
|
|
800
|
-
it('should make pre-commit executable', async () => {
|
|
801
|
-
const options = {
|
|
802
|
-
force: false,
|
|
803
|
-
full: true,
|
|
804
|
-
};
|
|
805
|
-
await scaffoldProject(tempDir, options);
|
|
806
|
-
const preCommitPath = path.join(tempDir, '.husky', 'pre-commit');
|
|
807
|
-
const stats = fs.statSync(preCommitPath);
|
|
808
|
-
// eslint-disable-next-line no-bitwise
|
|
809
|
-
const isExecutable = (stats.mode & 0o111) !== 0;
|
|
810
|
-
expect(isExecutable).toBe(true);
|
|
811
|
-
});
|
|
812
|
-
it('should NOT run pnpm test in pre-commit hook', async () => {
|
|
813
|
-
const options = {
|
|
814
|
-
force: false,
|
|
815
|
-
full: true,
|
|
816
|
-
};
|
|
817
|
-
await scaffoldProject(tempDir, options);
|
|
818
|
-
const preCommitPath = path.join(tempDir, '.husky', 'pre-commit');
|
|
819
|
-
const content = fs.readFileSync(preCommitPath, 'utf-8');
|
|
820
|
-
// The pre-commit hook should NOT assume pnpm test exists
|
|
821
|
-
expect(content).not.toContain('pnpm test');
|
|
822
|
-
expect(content).not.toContain('npm test');
|
|
823
|
-
});
|
|
824
|
-
it('should block commits to main/master in pre-commit', async () => {
|
|
825
|
-
const options = {
|
|
826
|
-
force: false,
|
|
827
|
-
full: true,
|
|
828
|
-
};
|
|
829
|
-
await scaffoldProject(tempDir, options);
|
|
830
|
-
const preCommitPath = path.join(tempDir, '.husky', 'pre-commit');
|
|
831
|
-
const content = fs.readFileSync(preCommitPath, 'utf-8');
|
|
832
|
-
// Should protect main branch
|
|
833
|
-
expect(content).toContain('main');
|
|
834
|
-
expect(content).toContain('BLOCK');
|
|
835
|
-
});
|
|
836
|
-
it('should scaffold pre-commit even in minimal mode', async () => {
|
|
837
|
-
const options = {
|
|
838
|
-
force: false,
|
|
839
|
-
full: false, // minimal mode
|
|
840
|
-
};
|
|
841
|
-
await scaffoldProject(tempDir, options);
|
|
842
|
-
const preCommitPath = path.join(tempDir, '.husky', 'pre-commit');
|
|
843
|
-
expect(fs.existsSync(preCommitPath)).toBe(true);
|
|
844
|
-
});
|
|
845
|
-
});
|
|
846
|
-
});
|
|
847
|
-
// WU-1413: MCP server configuration scaffolding
|
|
848
|
-
describe('WU-1413: .mcp.json scaffolding', () => {
|
|
849
|
-
const MCP_JSON_FILE = '.mcp.json';
|
|
850
|
-
describe('.mcp.json creation with --client claude', () => {
|
|
851
|
-
it('should scaffold .mcp.json when --client claude is used', async () => {
|
|
852
|
-
const options = {
|
|
853
|
-
force: false,
|
|
854
|
-
full: false,
|
|
855
|
-
client: 'claude',
|
|
856
|
-
};
|
|
857
|
-
await scaffoldProject(tempDir, options);
|
|
858
|
-
const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
|
|
859
|
-
expect(fs.existsSync(mcpJsonPath)).toBe(true);
|
|
860
|
-
});
|
|
861
|
-
it('should include lumenflow MCP server configuration', async () => {
|
|
862
|
-
const options = {
|
|
863
|
-
force: false,
|
|
864
|
-
full: false,
|
|
865
|
-
client: 'claude',
|
|
866
|
-
};
|
|
867
|
-
await scaffoldProject(tempDir, options);
|
|
868
|
-
const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
|
|
869
|
-
const content = fs.readFileSync(mcpJsonPath, 'utf-8');
|
|
870
|
-
const mcpConfig = JSON.parse(content);
|
|
871
|
-
// Should have mcpServers key
|
|
872
|
-
expect(mcpConfig.mcpServers).toBeDefined();
|
|
873
|
-
// Should have lumenflow server entry
|
|
874
|
-
expect(mcpConfig.mcpServers.lumenflow).toBeDefined();
|
|
875
|
-
// Should use npx command
|
|
876
|
-
expect(mcpConfig.mcpServers.lumenflow.command).toBe('npx');
|
|
877
|
-
// Should reference @lumenflow/mcp package
|
|
878
|
-
expect(mcpConfig.mcpServers.lumenflow.args).toContain('@lumenflow/mcp');
|
|
879
|
-
});
|
|
880
|
-
it('should be valid JSON', async () => {
|
|
881
|
-
const options = {
|
|
882
|
-
force: false,
|
|
883
|
-
full: false,
|
|
884
|
-
client: 'claude',
|
|
885
|
-
};
|
|
886
|
-
await scaffoldProject(tempDir, options);
|
|
887
|
-
const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
|
|
888
|
-
const content = fs.readFileSync(mcpJsonPath, 'utf-8');
|
|
889
|
-
// Should parse without error
|
|
890
|
-
expect(() => JSON.parse(content)).not.toThrow();
|
|
891
|
-
});
|
|
892
|
-
});
|
|
893
|
-
describe('.mcp.json creation with --client all', () => {
|
|
894
|
-
it('should scaffold .mcp.json when --client all is used', async () => {
|
|
895
|
-
const options = {
|
|
896
|
-
force: false,
|
|
897
|
-
full: false,
|
|
898
|
-
client: 'all',
|
|
899
|
-
};
|
|
900
|
-
await scaffoldProject(tempDir, options);
|
|
901
|
-
const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
|
|
902
|
-
expect(fs.existsSync(mcpJsonPath)).toBe(true);
|
|
903
|
-
});
|
|
904
|
-
});
|
|
905
|
-
describe('.mcp.json NOT created with other clients', () => {
|
|
906
|
-
it('should NOT scaffold .mcp.json when --client none is used', async () => {
|
|
907
|
-
const options = {
|
|
908
|
-
force: false,
|
|
909
|
-
full: false,
|
|
910
|
-
client: 'none',
|
|
911
|
-
};
|
|
912
|
-
await scaffoldProject(tempDir, options);
|
|
913
|
-
const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
|
|
914
|
-
expect(fs.existsSync(mcpJsonPath)).toBe(false);
|
|
915
|
-
});
|
|
916
|
-
it('should NOT scaffold .mcp.json when --client cursor is used', async () => {
|
|
917
|
-
const options = {
|
|
918
|
-
force: false,
|
|
919
|
-
full: false,
|
|
920
|
-
client: 'cursor',
|
|
921
|
-
};
|
|
922
|
-
await scaffoldProject(tempDir, options);
|
|
923
|
-
const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
|
|
924
|
-
expect(fs.existsSync(mcpJsonPath)).toBe(false);
|
|
925
|
-
});
|
|
926
|
-
it('should NOT scaffold .mcp.json when no client is specified', async () => {
|
|
927
|
-
const options = {
|
|
928
|
-
force: false,
|
|
929
|
-
full: false,
|
|
930
|
-
};
|
|
931
|
-
await scaffoldProject(tempDir, options);
|
|
932
|
-
const mcpJsonPath = path.join(tempDir, MCP_JSON_FILE);
|
|
933
|
-
expect(fs.existsSync(mcpJsonPath)).toBe(false);
|
|
934
|
-
});
|
|
935
|
-
});
|
|
936
|
-
describe('.mcp.json file modes', () => {
|
|
937
|
-
it('should skip .mcp.json if it already exists (skip mode)', async () => {
|
|
938
|
-
// Create existing .mcp.json
|
|
939
|
-
const existingContent = '{"mcpServers":{"custom":{}}}';
|
|
940
|
-
fs.writeFileSync(path.join(tempDir, MCP_JSON_FILE), existingContent);
|
|
941
|
-
const options = {
|
|
942
|
-
force: false,
|
|
943
|
-
full: false,
|
|
944
|
-
client: 'claude',
|
|
945
|
-
};
|
|
946
|
-
const result = await scaffoldProject(tempDir, options);
|
|
947
|
-
expect(result.skipped).toContain(MCP_JSON_FILE);
|
|
948
|
-
// Content should not be changed
|
|
949
|
-
const content = fs.readFileSync(path.join(tempDir, MCP_JSON_FILE), 'utf-8');
|
|
950
|
-
expect(content).toBe(existingContent);
|
|
951
|
-
});
|
|
952
|
-
it('should overwrite .mcp.json in force mode', async () => {
|
|
953
|
-
// Create existing .mcp.json
|
|
954
|
-
fs.writeFileSync(path.join(tempDir, MCP_JSON_FILE), '{"custom":true}');
|
|
955
|
-
const options = {
|
|
956
|
-
force: true,
|
|
957
|
-
full: false,
|
|
958
|
-
client: 'claude',
|
|
959
|
-
};
|
|
960
|
-
const result = await scaffoldProject(tempDir, options);
|
|
961
|
-
expect(result.created).toContain(MCP_JSON_FILE);
|
|
962
|
-
const content = fs.readFileSync(path.join(tempDir, MCP_JSON_FILE), 'utf-8');
|
|
963
|
-
const mcpConfig = JSON.parse(content);
|
|
964
|
-
expect(mcpConfig.mcpServers.lumenflow).toBeDefined();
|
|
965
|
-
});
|
|
966
|
-
});
|
|
967
|
-
});
|
|
968
|
-
});
|