@lumenflow/cli 2.2.1 → 2.3.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 +147 -57
- package/dist/__tests__/agent-log-issue.test.js +56 -0
- package/dist/__tests__/cli-entry-point.test.js +66 -17
- package/dist/__tests__/cli-subprocess.test.js +25 -0
- package/dist/__tests__/init.test.js +298 -0
- package/dist/__tests__/initiative-plan.test.js +340 -0
- package/dist/__tests__/mem-cleanup-execution.test.js +19 -0
- package/dist/__tests__/merge-block.test.js +220 -0
- package/dist/__tests__/release.test.js +28 -0
- package/dist/__tests__/safe-git.test.js +191 -0
- package/dist/__tests__/state-doctor.test.js +274 -0
- package/dist/__tests__/wu-done.test.js +36 -0
- package/dist/__tests__/wu-edit.test.js +119 -0
- package/dist/__tests__/wu-prep.test.js +108 -0
- package/dist/agent-issues-query.js +4 -3
- package/dist/agent-log-issue.js +25 -4
- package/dist/backlog-prune.js +5 -4
- package/dist/cli-entry-point.js +11 -1
- package/dist/doctor.js +368 -0
- package/dist/flow-bottlenecks.js +6 -5
- package/dist/flow-report.js +4 -3
- package/dist/gates.js +468 -116
- package/dist/guard-locked.js +4 -3
- package/dist/guard-worktree-commit.js +4 -3
- package/dist/init.js +508 -86
- package/dist/initiative-add-wu.js +4 -3
- package/dist/initiative-bulk-assign-wus.js +8 -5
- package/dist/initiative-create.js +73 -37
- package/dist/initiative-edit.js +37 -21
- package/dist/initiative-list.js +4 -3
- package/dist/initiative-plan.js +337 -0
- package/dist/initiative-status.js +4 -3
- package/dist/lane-health.js +377 -0
- package/dist/lane-suggest.js +382 -0
- package/dist/mem-checkpoint.js +2 -2
- package/dist/mem-cleanup.js +2 -2
- package/dist/mem-context.js +306 -0
- package/dist/mem-create.js +2 -2
- package/dist/mem-delete.js +293 -0
- package/dist/mem-inbox.js +2 -2
- package/dist/mem-index.js +211 -0
- package/dist/mem-init.js +1 -1
- package/dist/mem-profile.js +207 -0
- package/dist/mem-promote.js +254 -0
- package/dist/mem-ready.js +2 -2
- package/dist/mem-signal.js +2 -2
- package/dist/mem-start.js +2 -2
- package/dist/mem-summarize.js +2 -2
- package/dist/mem-triage.js +2 -2
- package/dist/merge-block.js +222 -0
- package/dist/metrics-cli.js +7 -4
- package/dist/metrics-snapshot.js +4 -3
- package/dist/orchestrate-initiative.js +10 -4
- package/dist/orchestrate-monitor.js +379 -31
- package/dist/signal-cleanup.js +296 -0
- package/dist/spawn-list.js +6 -5
- package/dist/state-bootstrap.js +5 -4
- package/dist/state-cleanup.js +360 -0
- package/dist/state-doctor-fix.js +196 -0
- package/dist/state-doctor.js +501 -0
- package/dist/validate-agent-skills.js +4 -3
- package/dist/validate-agent-sync.js +4 -3
- package/dist/validate-backlog-sync.js +7 -84
- package/dist/validate-skills-spec.js +4 -3
- package/dist/validate.js +7 -107
- package/dist/wu-block.js +3 -3
- package/dist/wu-claim.js +208 -98
- package/dist/wu-cleanup.js +5 -4
- package/dist/wu-create.js +71 -46
- package/dist/wu-delete.js +88 -60
- package/dist/wu-deps.js +6 -5
- package/dist/wu-done-check.js +34 -0
- package/dist/wu-done.js +60 -24
- package/dist/wu-edit.js +63 -28
- package/dist/wu-infer-lane.js +7 -6
- package/dist/wu-preflight.js +23 -81
- package/dist/wu-prep.js +125 -0
- package/dist/wu-prune.js +4 -3
- package/dist/wu-recover.js +88 -22
- package/dist/wu-repair.js +7 -6
- package/dist/wu-spawn.js +226 -270
- package/dist/wu-status.js +4 -3
- package/dist/wu-unblock.js +5 -5
- package/dist/wu-unlock-lane.js +4 -3
- package/dist/wu-validate.js +5 -4
- package/package.json +16 -7
- package/templates/core/.lumenflow/constraints.md.template +192 -0
- package/templates/core/.lumenflow/rules/git-safety.md.template +27 -0
- package/templates/core/.lumenflow/rules/wu-workflow.md.template +48 -0
- package/templates/core/AGENTS.md.template +60 -0
- package/templates/core/LUMENFLOW.md.template +255 -0
- package/templates/core/UPGRADING.md.template +121 -0
- package/templates/core/ai/onboarding/agent-safety-card.md.template +106 -0
- package/templates/core/ai/onboarding/first-wu-mistakes.md.template +198 -0
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +186 -0
- package/templates/core/ai/onboarding/release-process.md.template +362 -0
- package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +159 -0
- package/templates/core/ai/onboarding/wu-create-checklist.md.template +117 -0
- package/templates/vendors/aider/.aider.conf.yml.template +27 -0
- package/templates/vendors/claude/.claude/CLAUDE.md.template +52 -0
- package/templates/vendors/claude/.claude/settings.json.template +49 -0
- package/templates/vendors/claude/.claude/skills/bug-classification/SKILL.md.template +192 -0
- package/templates/vendors/claude/.claude/skills/code-quality/SKILL.md.template +152 -0
- package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +155 -0
- package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +304 -0
- package/templates/vendors/claude/.claude/skills/frontend-design/SKILL.md.template +131 -0
- package/templates/vendors/claude/.claude/skills/initiative-management/SKILL.md.template +164 -0
- package/templates/vendors/claude/.claude/skills/library-first/SKILL.md.template +98 -0
- package/templates/vendors/claude/.claude/skills/lumenflow-gates/SKILL.md.template +87 -0
- package/templates/vendors/claude/.claude/skills/multi-agent-coordination/SKILL.md.template +84 -0
- package/templates/vendors/claude/.claude/skills/ops-maintenance/SKILL.md.template +254 -0
- package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +189 -0
- package/templates/vendors/claude/.claude/skills/tdd-workflow/SKILL.md.template +139 -0
- package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +138 -0
- package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +106 -0
- package/templates/vendors/cline/.clinerules.template +53 -0
- package/templates/vendors/cursor/.cursor/rules/lumenflow.md.template +34 -0
- package/templates/vendors/cursor/.cursor/rules.md.template +28 -0
- package/templates/vendors/windsurf/.windsurf/rules/lumenflow.md.template +34 -0
|
@@ -0,0 +1,298 @@
|
|
|
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
|
+
describe('lumenflow init', () => {
|
|
17
|
+
let tempDir;
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-init-test-'));
|
|
20
|
+
});
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
describe('AGENTS.md creation', () => {
|
|
25
|
+
it('should create AGENTS.md by default', async () => {
|
|
26
|
+
const options = {
|
|
27
|
+
force: false,
|
|
28
|
+
full: false,
|
|
29
|
+
};
|
|
30
|
+
await scaffoldProject(tempDir, options);
|
|
31
|
+
const agentsPath = path.join(tempDir, 'AGENTS.md');
|
|
32
|
+
expect(fs.existsSync(agentsPath)).toBe(true);
|
|
33
|
+
const content = fs.readFileSync(agentsPath, 'utf-8');
|
|
34
|
+
expect(content).toContain(LUMENFLOW_MD);
|
|
35
|
+
expect(content).toContain('universal');
|
|
36
|
+
});
|
|
37
|
+
it('should link AGENTS.md to LUMENFLOW.md', async () => {
|
|
38
|
+
const options = {
|
|
39
|
+
force: false,
|
|
40
|
+
full: false,
|
|
41
|
+
};
|
|
42
|
+
await scaffoldProject(tempDir, options);
|
|
43
|
+
const agentsContent = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
44
|
+
expect(agentsContent).toContain(`[${LUMENFLOW_MD}]`);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('--client flag', () => {
|
|
48
|
+
it('should accept --client claude', async () => {
|
|
49
|
+
const options = {
|
|
50
|
+
force: false,
|
|
51
|
+
full: false,
|
|
52
|
+
client: 'claude',
|
|
53
|
+
};
|
|
54
|
+
const result = await scaffoldProject(tempDir, options);
|
|
55
|
+
expect(fs.existsSync(path.join(tempDir, 'CLAUDE.md'))).toBe(true);
|
|
56
|
+
expect(result.created).toContain('CLAUDE.md');
|
|
57
|
+
});
|
|
58
|
+
it('should accept --client cursor', async () => {
|
|
59
|
+
const options = {
|
|
60
|
+
force: false,
|
|
61
|
+
full: false,
|
|
62
|
+
client: 'cursor',
|
|
63
|
+
};
|
|
64
|
+
await scaffoldProject(tempDir, options);
|
|
65
|
+
// Cursor uses .cursor/rules/lumenflow.md (not .cursor/rules.md)
|
|
66
|
+
expect(fs.existsSync(path.join(tempDir, '.cursor', 'rules', VENDOR_RULES_FILE))).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
it('should accept --client windsurf', async () => {
|
|
69
|
+
const options = {
|
|
70
|
+
force: false,
|
|
71
|
+
full: false,
|
|
72
|
+
client: 'windsurf',
|
|
73
|
+
};
|
|
74
|
+
await scaffoldProject(tempDir, options);
|
|
75
|
+
// Windsurf uses .windsurf/rules/lumenflow.md (not .windsurfrules)
|
|
76
|
+
expect(fs.existsSync(path.join(tempDir, '.windsurf', 'rules', VENDOR_RULES_FILE))).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
it('should accept --client codex', async () => {
|
|
79
|
+
const options = {
|
|
80
|
+
force: false,
|
|
81
|
+
full: false,
|
|
82
|
+
client: 'codex',
|
|
83
|
+
};
|
|
84
|
+
await scaffoldProject(tempDir, options);
|
|
85
|
+
// Codex reads AGENTS.md directly, minimal extra config
|
|
86
|
+
expect(fs.existsSync(path.join(tempDir, 'AGENTS.md'))).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
it('should accept --client all', async () => {
|
|
89
|
+
const options = {
|
|
90
|
+
force: false,
|
|
91
|
+
full: false,
|
|
92
|
+
client: 'all',
|
|
93
|
+
};
|
|
94
|
+
await scaffoldProject(tempDir, options);
|
|
95
|
+
// Should create all vendor files
|
|
96
|
+
expect(fs.existsSync(path.join(tempDir, 'AGENTS.md'))).toBe(true);
|
|
97
|
+
expect(fs.existsSync(path.join(tempDir, 'CLAUDE.md'))).toBe(true);
|
|
98
|
+
expect(fs.existsSync(path.join(tempDir, '.cursor', 'rules', VENDOR_RULES_FILE))).toBe(true);
|
|
99
|
+
expect(fs.existsSync(path.join(tempDir, '.windsurf', 'rules', VENDOR_RULES_FILE))).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
it('should treat --vendor as alias for --client (backwards compatibility)', async () => {
|
|
102
|
+
const options = {
|
|
103
|
+
force: false,
|
|
104
|
+
full: false,
|
|
105
|
+
vendor: 'claude', // Using old --vendor flag
|
|
106
|
+
};
|
|
107
|
+
await scaffoldProject(tempDir, options);
|
|
108
|
+
expect(fs.existsSync(path.join(tempDir, 'CLAUDE.md'))).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('--merge mode', () => {
|
|
112
|
+
it('should insert LUMENFLOW block into existing file', async () => {
|
|
113
|
+
// Create existing AGENTS.md
|
|
114
|
+
const existingContent = '# My Project Agents\n\nCustom content here.\n';
|
|
115
|
+
fs.writeFileSync(path.join(tempDir, 'AGENTS.md'), existingContent);
|
|
116
|
+
const options = {
|
|
117
|
+
force: false,
|
|
118
|
+
full: false,
|
|
119
|
+
merge: true,
|
|
120
|
+
};
|
|
121
|
+
await scaffoldProject(tempDir, options);
|
|
122
|
+
const content = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
123
|
+
// Should preserve original content
|
|
124
|
+
expect(content).toContain('# My Project Agents');
|
|
125
|
+
expect(content).toContain('Custom content here.');
|
|
126
|
+
// Should add LumenFlow block
|
|
127
|
+
expect(content).toContain('<!-- LUMENFLOW:START -->');
|
|
128
|
+
expect(content).toContain('<!-- LUMENFLOW:END -->');
|
|
129
|
+
expect(content).toContain(LUMENFLOW_MD);
|
|
130
|
+
});
|
|
131
|
+
it('should be idempotent (running twice produces no diff)', async () => {
|
|
132
|
+
// Create existing file
|
|
133
|
+
fs.writeFileSync(path.join(tempDir, 'AGENTS.md'), '# My Project\n');
|
|
134
|
+
const options = {
|
|
135
|
+
force: false,
|
|
136
|
+
full: false,
|
|
137
|
+
merge: true,
|
|
138
|
+
};
|
|
139
|
+
// First run
|
|
140
|
+
await scaffoldProject(tempDir, options);
|
|
141
|
+
const firstContent = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
142
|
+
// Second run
|
|
143
|
+
await scaffoldProject(tempDir, options);
|
|
144
|
+
const secondContent = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
145
|
+
expect(firstContent).toBe(secondContent);
|
|
146
|
+
});
|
|
147
|
+
it('should preserve CRLF line endings', async () => {
|
|
148
|
+
// Create existing file with CRLF
|
|
149
|
+
const existingContent = '# My Project\r\n\r\nWindows style.\r\n';
|
|
150
|
+
fs.writeFileSync(path.join(tempDir, 'AGENTS.md'), existingContent);
|
|
151
|
+
const options = {
|
|
152
|
+
force: false,
|
|
153
|
+
full: false,
|
|
154
|
+
merge: true,
|
|
155
|
+
};
|
|
156
|
+
await scaffoldProject(tempDir, options);
|
|
157
|
+
const content = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
158
|
+
// Should preserve CRLF
|
|
159
|
+
expect(content).toContain('\r\n');
|
|
160
|
+
// Should not have mixed line endings (standalone LF without preceding CR)
|
|
161
|
+
const lfCount = (content.match(/(?<!\r)\n/g) || []).length;
|
|
162
|
+
expect(lfCount).toBe(0);
|
|
163
|
+
});
|
|
164
|
+
it('should warn on malformed markers and append fresh block', async () => {
|
|
165
|
+
// Create file with only START marker (malformed)
|
|
166
|
+
const malformedContent = '# My Project\n\n<!-- LUMENFLOW:START -->\nOrphan block\n';
|
|
167
|
+
fs.writeFileSync(path.join(tempDir, 'AGENTS.md'), malformedContent);
|
|
168
|
+
const options = {
|
|
169
|
+
force: false,
|
|
170
|
+
full: false,
|
|
171
|
+
merge: true,
|
|
172
|
+
};
|
|
173
|
+
const result = await scaffoldProject(tempDir, options);
|
|
174
|
+
// Should have warnings about malformed markers
|
|
175
|
+
expect(result.warnings).toBeDefined();
|
|
176
|
+
expect(result.warnings?.some((w) => w.includes('malformed'))).toBe(true);
|
|
177
|
+
const content = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
178
|
+
// Should have complete block
|
|
179
|
+
expect(content).toContain('<!-- LUMENFLOW:END -->');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
describe('createFile mode option', () => {
|
|
183
|
+
it('should skip existing files in skip mode (default)', async () => {
|
|
184
|
+
const existingContent = 'Original content';
|
|
185
|
+
fs.writeFileSync(path.join(tempDir, 'AGENTS.md'), existingContent);
|
|
186
|
+
const options = {
|
|
187
|
+
force: false,
|
|
188
|
+
full: false,
|
|
189
|
+
// Default mode is 'skip'
|
|
190
|
+
};
|
|
191
|
+
const result = await scaffoldProject(tempDir, options);
|
|
192
|
+
expect(result.skipped).toContain('AGENTS.md');
|
|
193
|
+
const content = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
194
|
+
expect(content).toBe(existingContent);
|
|
195
|
+
});
|
|
196
|
+
it('should overwrite in force mode', async () => {
|
|
197
|
+
fs.writeFileSync(path.join(tempDir, 'AGENTS.md'), 'Original');
|
|
198
|
+
const options = {
|
|
199
|
+
force: true,
|
|
200
|
+
full: false,
|
|
201
|
+
};
|
|
202
|
+
const result = await scaffoldProject(tempDir, options);
|
|
203
|
+
expect(result.created).toContain('AGENTS.md');
|
|
204
|
+
const content = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
205
|
+
expect(content).not.toBe('Original');
|
|
206
|
+
});
|
|
207
|
+
it('should merge in merge mode', async () => {
|
|
208
|
+
fs.writeFileSync(path.join(tempDir, 'AGENTS.md'), '# Custom Header\n');
|
|
209
|
+
const options = {
|
|
210
|
+
force: false,
|
|
211
|
+
full: false,
|
|
212
|
+
merge: true,
|
|
213
|
+
};
|
|
214
|
+
const result = await scaffoldProject(tempDir, options);
|
|
215
|
+
expect(result.merged).toContain('AGENTS.md');
|
|
216
|
+
const content = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
|
|
217
|
+
expect(content).toContain('# Custom Header');
|
|
218
|
+
expect(content).toContain('<!-- LUMENFLOW:START -->');
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
describe('vendor overlay paths', () => {
|
|
222
|
+
it('should use .cursor/rules/lumenflow.md for Cursor', async () => {
|
|
223
|
+
const options = {
|
|
224
|
+
force: false,
|
|
225
|
+
full: false,
|
|
226
|
+
client: 'cursor',
|
|
227
|
+
};
|
|
228
|
+
await scaffoldProject(tempDir, options);
|
|
229
|
+
// Should NOT create old path
|
|
230
|
+
expect(fs.existsSync(path.join(tempDir, '.cursor', 'rules.md'))).toBe(false);
|
|
231
|
+
// Should create new path
|
|
232
|
+
expect(fs.existsSync(path.join(tempDir, '.cursor', 'rules', VENDOR_RULES_FILE))).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
it('should use .windsurf/rules/lumenflow.md for Windsurf', async () => {
|
|
235
|
+
const options = {
|
|
236
|
+
force: false,
|
|
237
|
+
full: false,
|
|
238
|
+
client: 'windsurf',
|
|
239
|
+
};
|
|
240
|
+
await scaffoldProject(tempDir, options);
|
|
241
|
+
// Should NOT create old path
|
|
242
|
+
expect(fs.existsSync(path.join(tempDir, '.windsurfrules'))).toBe(false);
|
|
243
|
+
// Should create new path
|
|
244
|
+
expect(fs.existsSync(path.join(tempDir, '.windsurf', 'rules', VENDOR_RULES_FILE))).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
describe('CLAUDE.md location', () => {
|
|
248
|
+
it('should create single CLAUDE.md at root only', async () => {
|
|
249
|
+
const options = {
|
|
250
|
+
force: false,
|
|
251
|
+
full: false,
|
|
252
|
+
client: 'claude',
|
|
253
|
+
};
|
|
254
|
+
await scaffoldProject(tempDir, options);
|
|
255
|
+
// Should create root CLAUDE.md
|
|
256
|
+
expect(fs.existsSync(path.join(tempDir, 'CLAUDE.md'))).toBe(true);
|
|
257
|
+
// Should NOT create .claude/CLAUDE.md (no duplication)
|
|
258
|
+
expect(fs.existsSync(path.join(tempDir, '.claude', 'CLAUDE.md'))).toBe(false);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
// WU-1286: --full is now the default
|
|
262
|
+
describe('--full default and --minimal flag', () => {
|
|
263
|
+
it('should scaffold agent onboarding docs by default (full=true)', async () => {
|
|
264
|
+
const options = {
|
|
265
|
+
force: false,
|
|
266
|
+
full: true, // This is now the default when parsed
|
|
267
|
+
};
|
|
268
|
+
await scaffoldProject(tempDir, options);
|
|
269
|
+
// Should create agent onboarding docs
|
|
270
|
+
const onboardingDir = path.join(tempDir, 'docs/04-operations/_frameworks/lumenflow/agent/onboarding');
|
|
271
|
+
expect(fs.existsSync(path.join(onboardingDir, 'quick-ref-commands.md'))).toBe(true);
|
|
272
|
+
expect(fs.existsSync(path.join(onboardingDir, 'first-wu-mistakes.md'))).toBe(true);
|
|
273
|
+
expect(fs.existsSync(path.join(onboardingDir, 'troubleshooting-wu-done.md'))).toBe(true);
|
|
274
|
+
});
|
|
275
|
+
it('should skip agent onboarding docs when full=false (minimal mode)', async () => {
|
|
276
|
+
const options = {
|
|
277
|
+
force: false,
|
|
278
|
+
full: false, // Explicitly minimal
|
|
279
|
+
};
|
|
280
|
+
await scaffoldProject(tempDir, options);
|
|
281
|
+
// Should NOT create agent onboarding docs
|
|
282
|
+
const onboardingDir = path.join(tempDir, 'docs/04-operations/_frameworks/lumenflow/agent/onboarding');
|
|
283
|
+
expect(fs.existsSync(path.join(onboardingDir, 'quick-ref-commands.md'))).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
it('should still create core files in minimal mode', async () => {
|
|
286
|
+
const options = {
|
|
287
|
+
force: false,
|
|
288
|
+
full: false,
|
|
289
|
+
};
|
|
290
|
+
await scaffoldProject(tempDir, options);
|
|
291
|
+
// Core files should always be created
|
|
292
|
+
expect(fs.existsSync(path.join(tempDir, 'AGENTS.md'))).toBe(true);
|
|
293
|
+
expect(fs.existsSync(path.join(tempDir, 'LUMENFLOW.md'))).toBe(true);
|
|
294
|
+
expect(fs.existsSync(path.join(tempDir, '.lumenflow.config.yaml'))).toBe(true);
|
|
295
|
+
expect(fs.existsSync(path.join(tempDir, '.lumenflow', 'constraints.md'))).toBe(true);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
});
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for initiative:plan command (WU-1105, renamed in WU-1193)
|
|
3
|
+
*
|
|
4
|
+
* The initiative:plan command links plan files to initiatives by setting
|
|
5
|
+
* the `related_plan` field in the initiative YAML.
|
|
6
|
+
*
|
|
7
|
+
* TDD: These tests are written BEFORE the implementation.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll } from 'vitest';
|
|
10
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { parseYAML, stringifyYAML } from '@lumenflow/core/dist/wu-yaml.js';
|
|
14
|
+
// Pre-import the module to ensure coverage tracking includes the module itself
|
|
15
|
+
let initPlanModule;
|
|
16
|
+
beforeAll(async () => {
|
|
17
|
+
initPlanModule = await import('../initiative-plan.js');
|
|
18
|
+
});
|
|
19
|
+
// Mock modules before importing the module under test
|
|
20
|
+
const mockGit = {
|
|
21
|
+
branch: vi.fn().mockResolvedValue({ current: 'main' }),
|
|
22
|
+
status: vi.fn().mockResolvedValue({ isClean: () => true }),
|
|
23
|
+
};
|
|
24
|
+
vi.mock('@lumenflow/core/dist/git-adapter.js', () => ({
|
|
25
|
+
getGitForCwd: vi.fn(() => mockGit),
|
|
26
|
+
}));
|
|
27
|
+
vi.mock('@lumenflow/core/dist/wu-helpers.js', () => ({
|
|
28
|
+
ensureOnMain: vi.fn().mockResolvedValue(undefined),
|
|
29
|
+
}));
|
|
30
|
+
vi.mock('@lumenflow/core/dist/micro-worktree.js', () => ({
|
|
31
|
+
withMicroWorktree: vi.fn(async ({ execute }) => {
|
|
32
|
+
// Simulate micro-worktree by executing in temp dir
|
|
33
|
+
const tempDir = join(tmpdir(), `init-plan-test-${Date.now()}`);
|
|
34
|
+
mkdirSync(tempDir, { recursive: true });
|
|
35
|
+
try {
|
|
36
|
+
await execute({ worktreePath: tempDir });
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
// Cleanup handled by test
|
|
40
|
+
}
|
|
41
|
+
}),
|
|
42
|
+
}));
|
|
43
|
+
describe('init:plan command', () => {
|
|
44
|
+
let tempDir;
|
|
45
|
+
let originalCwd;
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
tempDir = join(tmpdir(), `init-plan-test-${Date.now()}`);
|
|
48
|
+
mkdirSync(tempDir, { recursive: true });
|
|
49
|
+
originalCwd = process.cwd();
|
|
50
|
+
});
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
process.chdir(originalCwd);
|
|
53
|
+
if (existsSync(tempDir)) {
|
|
54
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
vi.clearAllMocks();
|
|
57
|
+
});
|
|
58
|
+
describe('validateInitIdFormat', () => {
|
|
59
|
+
it('should accept valid INIT-NNN format', async () => {
|
|
60
|
+
const { validateInitIdFormat } = await import('../initiative-plan.js');
|
|
61
|
+
// Should not throw
|
|
62
|
+
expect(() => validateInitIdFormat('INIT-001')).not.toThrow();
|
|
63
|
+
expect(() => validateInitIdFormat('INIT-123')).not.toThrow();
|
|
64
|
+
});
|
|
65
|
+
it('should accept valid INIT-NAME format', async () => {
|
|
66
|
+
const { validateInitIdFormat } = await import('../initiative-plan.js');
|
|
67
|
+
expect(() => validateInitIdFormat('INIT-TOOLING')).not.toThrow();
|
|
68
|
+
expect(() => validateInitIdFormat('INIT-A1')).not.toThrow();
|
|
69
|
+
});
|
|
70
|
+
it('should reject invalid formats', async () => {
|
|
71
|
+
const { validateInitIdFormat } = await import('../initiative-plan.js');
|
|
72
|
+
expect(() => validateInitIdFormat('init-001')).toThrow();
|
|
73
|
+
expect(() => validateInitIdFormat('INIT001')).toThrow();
|
|
74
|
+
expect(() => validateInitIdFormat('WU-001')).toThrow();
|
|
75
|
+
expect(() => validateInitIdFormat('')).toThrow();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('validatePlanPath', () => {
|
|
79
|
+
it('should accept existing markdown files', async () => {
|
|
80
|
+
const { validatePlanPath } = await import('../initiative-plan.js');
|
|
81
|
+
const planPath = join(tempDir, 'test-plan.md');
|
|
82
|
+
writeFileSync(planPath, '# Test Plan');
|
|
83
|
+
// Should not throw
|
|
84
|
+
expect(() => validatePlanPath(planPath)).not.toThrow();
|
|
85
|
+
});
|
|
86
|
+
it('should reject non-existent files when not creating', async () => {
|
|
87
|
+
const { validatePlanPath } = await import('../initiative-plan.js');
|
|
88
|
+
const planPath = join(tempDir, 'nonexistent.md');
|
|
89
|
+
expect(() => validatePlanPath(planPath)).toThrow();
|
|
90
|
+
});
|
|
91
|
+
it('should reject non-markdown files', async () => {
|
|
92
|
+
const { validatePlanPath } = await import('../initiative-plan.js');
|
|
93
|
+
const planPath = join(tempDir, 'test-plan.txt');
|
|
94
|
+
writeFileSync(planPath, 'Test Plan');
|
|
95
|
+
expect(() => validatePlanPath(planPath)).toThrow();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('formatPlanUri', () => {
|
|
99
|
+
it('should format plan path as lumenflow:// URI', async () => {
|
|
100
|
+
const { formatPlanUri } = await import('../initiative-plan.js');
|
|
101
|
+
expect(formatPlanUri('docs/04-operations/plans/my-plan.md')).toBe('lumenflow://plans/my-plan.md');
|
|
102
|
+
});
|
|
103
|
+
it('should handle nested paths', async () => {
|
|
104
|
+
const { formatPlanUri } = await import('../initiative-plan.js');
|
|
105
|
+
expect(formatPlanUri('docs/04-operations/plans/subdir/nested-plan.md')).toBe('lumenflow://plans/subdir/nested-plan.md');
|
|
106
|
+
});
|
|
107
|
+
it('should handle paths not in standard location', async () => {
|
|
108
|
+
const { formatPlanUri } = await import('../initiative-plan.js');
|
|
109
|
+
// Should still create a URI even for non-standard paths
|
|
110
|
+
expect(formatPlanUri('/absolute/path/custom-plan.md')).toBe('lumenflow://plans/custom-plan.md');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe('checkInitiativeExists', () => {
|
|
114
|
+
it('should return initiative doc if found', async () => {
|
|
115
|
+
const { checkInitiativeExists } = await import('../initiative-plan.js');
|
|
116
|
+
// Create a mock initiative file
|
|
117
|
+
const initDir = join(tempDir, 'docs', '04-operations', 'tasks', 'initiatives');
|
|
118
|
+
mkdirSync(initDir, { recursive: true });
|
|
119
|
+
const initPath = join(initDir, 'INIT-001.yaml');
|
|
120
|
+
const initDoc = {
|
|
121
|
+
id: 'INIT-001',
|
|
122
|
+
slug: 'test-initiative',
|
|
123
|
+
title: 'Test Initiative',
|
|
124
|
+
status: 'open',
|
|
125
|
+
created: '2026-01-25',
|
|
126
|
+
};
|
|
127
|
+
writeFileSync(initPath, stringifyYAML(initDoc));
|
|
128
|
+
process.chdir(tempDir);
|
|
129
|
+
const result = checkInitiativeExists('INIT-001');
|
|
130
|
+
expect(result.id).toBe('INIT-001');
|
|
131
|
+
});
|
|
132
|
+
it('should throw if initiative not found', async () => {
|
|
133
|
+
const { checkInitiativeExists } = await import('../initiative-plan.js');
|
|
134
|
+
process.chdir(tempDir);
|
|
135
|
+
expect(() => checkInitiativeExists('INIT-999')).toThrow();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('updateInitiativeWithPlan', () => {
|
|
139
|
+
it('should add related_plan field to initiative', async () => {
|
|
140
|
+
const { updateInitiativeWithPlan } = await import('../initiative-plan.js');
|
|
141
|
+
// Setup mock initiative
|
|
142
|
+
const initDir = join(tempDir, 'docs', '04-operations', 'tasks', 'initiatives');
|
|
143
|
+
mkdirSync(initDir, { recursive: true });
|
|
144
|
+
const initPath = join(initDir, 'INIT-001.yaml');
|
|
145
|
+
const initDoc = {
|
|
146
|
+
id: 'INIT-001',
|
|
147
|
+
slug: 'test-initiative',
|
|
148
|
+
title: 'Test Initiative',
|
|
149
|
+
status: 'open',
|
|
150
|
+
created: '2026-01-25',
|
|
151
|
+
};
|
|
152
|
+
writeFileSync(initPath, stringifyYAML(initDoc));
|
|
153
|
+
// Update initiative
|
|
154
|
+
const changed = updateInitiativeWithPlan(tempDir, 'INIT-001', 'lumenflow://plans/test-plan.md');
|
|
155
|
+
expect(changed).toBe(true);
|
|
156
|
+
// Verify the file was updated
|
|
157
|
+
const updated = parseYAML(readFileSync(initPath, 'utf-8'));
|
|
158
|
+
expect(updated.related_plan).toBe('lumenflow://plans/test-plan.md');
|
|
159
|
+
});
|
|
160
|
+
it('should return false if plan already linked (idempotent)', async () => {
|
|
161
|
+
const { updateInitiativeWithPlan } = await import('../initiative-plan.js');
|
|
162
|
+
// Setup mock initiative with existing plan
|
|
163
|
+
const initDir = join(tempDir, 'docs', '04-operations', 'tasks', 'initiatives');
|
|
164
|
+
mkdirSync(initDir, { recursive: true });
|
|
165
|
+
const initPath = join(initDir, 'INIT-001.yaml');
|
|
166
|
+
const initDoc = {
|
|
167
|
+
id: 'INIT-001',
|
|
168
|
+
slug: 'test-initiative',
|
|
169
|
+
title: 'Test Initiative',
|
|
170
|
+
status: 'open',
|
|
171
|
+
created: '2026-01-25',
|
|
172
|
+
related_plan: 'lumenflow://plans/test-plan.md',
|
|
173
|
+
};
|
|
174
|
+
writeFileSync(initPath, stringifyYAML(initDoc));
|
|
175
|
+
// Update initiative with same plan
|
|
176
|
+
const changed = updateInitiativeWithPlan(tempDir, 'INIT-001', 'lumenflow://plans/test-plan.md');
|
|
177
|
+
expect(changed).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
it('should warn but proceed if different plan already linked', async () => {
|
|
180
|
+
const { updateInitiativeWithPlan } = await import('../initiative-plan.js');
|
|
181
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
182
|
+
// Setup mock initiative with different plan
|
|
183
|
+
const initDir = join(tempDir, 'docs', '04-operations', 'tasks', 'initiatives');
|
|
184
|
+
mkdirSync(initDir, { recursive: true });
|
|
185
|
+
const initPath = join(initDir, 'INIT-001.yaml');
|
|
186
|
+
const initDoc = {
|
|
187
|
+
id: 'INIT-001',
|
|
188
|
+
slug: 'test-initiative',
|
|
189
|
+
title: 'Test Initiative',
|
|
190
|
+
status: 'open',
|
|
191
|
+
created: '2026-01-25',
|
|
192
|
+
related_plan: 'lumenflow://plans/old-plan.md',
|
|
193
|
+
};
|
|
194
|
+
writeFileSync(initPath, stringifyYAML(initDoc));
|
|
195
|
+
// Update initiative with new plan
|
|
196
|
+
const changed = updateInitiativeWithPlan(tempDir, 'INIT-001', 'lumenflow://plans/new-plan.md');
|
|
197
|
+
expect(changed).toBe(true);
|
|
198
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Replacing existing related_plan'));
|
|
199
|
+
consoleSpy.mockRestore();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
describe('createPlanTemplate', () => {
|
|
203
|
+
it('should create a plan template file', async () => {
|
|
204
|
+
const { createPlanTemplate } = await import('../initiative-plan.js');
|
|
205
|
+
const plansDir = join(tempDir, 'docs', '04-operations', 'plans');
|
|
206
|
+
mkdirSync(plansDir, { recursive: true });
|
|
207
|
+
const planPath = createPlanTemplate(tempDir, 'INIT-001', 'Test Initiative');
|
|
208
|
+
expect(existsSync(planPath)).toBe(true);
|
|
209
|
+
const content = readFileSync(planPath, 'utf-8');
|
|
210
|
+
expect(content).toContain('# INIT-001');
|
|
211
|
+
expect(content).toContain('Test Initiative');
|
|
212
|
+
expect(content).toContain('## Goal');
|
|
213
|
+
expect(content).toContain('## Scope');
|
|
214
|
+
});
|
|
215
|
+
it('should not overwrite existing plan file', async () => {
|
|
216
|
+
const { createPlanTemplate } = await import('../initiative-plan.js');
|
|
217
|
+
const plansDir = join(tempDir, 'docs', '04-operations', 'plans');
|
|
218
|
+
mkdirSync(plansDir, { recursive: true });
|
|
219
|
+
// Create existing file
|
|
220
|
+
const existingPath = join(plansDir, 'INIT-001-test-initiative.md');
|
|
221
|
+
writeFileSync(existingPath, '# Existing Content');
|
|
222
|
+
expect(() => createPlanTemplate(tempDir, 'INIT-001', 'Test Initiative')).toThrow();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
describe('LOG_PREFIX', () => {
|
|
226
|
+
it('should use correct log prefix', async () => {
|
|
227
|
+
const { LOG_PREFIX } = await import('../initiative-plan.js');
|
|
228
|
+
expect(LOG_PREFIX).toBe('[initiative:plan]');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
describe('getCommitMessage', () => {
|
|
232
|
+
it('should generate correct commit message', async () => {
|
|
233
|
+
const { getCommitMessage } = await import('../initiative-plan.js');
|
|
234
|
+
expect(getCommitMessage('INIT-001', 'lumenflow://plans/my-plan.md')).toBe('docs: link plan my-plan.md to init-001');
|
|
235
|
+
});
|
|
236
|
+
it('should handle nested plan paths', async () => {
|
|
237
|
+
const { getCommitMessage } = await import('../initiative-plan.js');
|
|
238
|
+
expect(getCommitMessage('INIT-TOOLING', 'lumenflow://plans/subdir/nested-plan.md')).toBe('docs: link plan subdir/nested-plan.md to init-tooling');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
describe('updateInitiativeWithPlan ID mismatch', () => {
|
|
242
|
+
it('should throw if initiative ID does not match', async () => {
|
|
243
|
+
const { updateInitiativeWithPlan } = await import('../initiative-plan.js');
|
|
244
|
+
// Setup mock initiative with different ID
|
|
245
|
+
const initDir = join(tempDir, 'docs', '04-operations', 'tasks', 'initiatives');
|
|
246
|
+
mkdirSync(initDir, { recursive: true });
|
|
247
|
+
const initPath = join(initDir, 'INIT-001.yaml');
|
|
248
|
+
const initDoc = {
|
|
249
|
+
id: 'INIT-002', // Wrong ID
|
|
250
|
+
slug: 'test-initiative',
|
|
251
|
+
title: 'Test Initiative',
|
|
252
|
+
status: 'open',
|
|
253
|
+
created: '2026-01-25',
|
|
254
|
+
};
|
|
255
|
+
writeFileSync(initPath, stringifyYAML(initDoc));
|
|
256
|
+
expect(() => updateInitiativeWithPlan(tempDir, 'INIT-001', 'lumenflow://plans/test-plan.md')).toThrow();
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
describe('init:plan CLI integration', () => {
|
|
261
|
+
it('should require --initiative flag', async () => {
|
|
262
|
+
// This test verifies that the CLI requires the initiative flag
|
|
263
|
+
// The actual CLI integration is tested via subprocess
|
|
264
|
+
const { WU_OPTIONS } = await import('@lumenflow/core/dist/arg-parser.js');
|
|
265
|
+
expect(WU_OPTIONS.initiative).toBeDefined();
|
|
266
|
+
expect(WU_OPTIONS.initiative.flags).toContain('--initiative');
|
|
267
|
+
});
|
|
268
|
+
it('should export main function for CLI entry', async () => {
|
|
269
|
+
const initPlan = await import('../initiative-plan.js');
|
|
270
|
+
expect(typeof initPlan.main).toBe('function');
|
|
271
|
+
});
|
|
272
|
+
it('should export all required functions', async () => {
|
|
273
|
+
const initPlan = await import('../initiative-plan.js');
|
|
274
|
+
expect(typeof initPlan.validateInitIdFormat).toBe('function');
|
|
275
|
+
expect(typeof initPlan.validatePlanPath).toBe('function');
|
|
276
|
+
expect(typeof initPlan.formatPlanUri).toBe('function');
|
|
277
|
+
expect(typeof initPlan.checkInitiativeExists).toBe('function');
|
|
278
|
+
expect(typeof initPlan.updateInitiativeWithPlan).toBe('function');
|
|
279
|
+
expect(typeof initPlan.createPlanTemplate).toBe('function');
|
|
280
|
+
expect(typeof initPlan.getCommitMessage).toBe('function');
|
|
281
|
+
expect(typeof initPlan.LOG_PREFIX).toBe('string');
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
describe('createPlanTemplate edge cases', () => {
|
|
285
|
+
let tempDir;
|
|
286
|
+
let originalCwd;
|
|
287
|
+
beforeEach(() => {
|
|
288
|
+
tempDir = join(tmpdir(), `init-plan-test-${Date.now()}`);
|
|
289
|
+
mkdirSync(tempDir, { recursive: true });
|
|
290
|
+
originalCwd = process.cwd();
|
|
291
|
+
});
|
|
292
|
+
afterEach(() => {
|
|
293
|
+
process.chdir(originalCwd);
|
|
294
|
+
if (existsSync(tempDir)) {
|
|
295
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
296
|
+
}
|
|
297
|
+
vi.clearAllMocks();
|
|
298
|
+
});
|
|
299
|
+
it('should create plans directory if it does not exist', async () => {
|
|
300
|
+
const { createPlanTemplate } = await import('../initiative-plan.js');
|
|
301
|
+
// Do NOT pre-create the plans directory
|
|
302
|
+
const planPath = createPlanTemplate(tempDir, 'INIT-001', 'Test Initiative');
|
|
303
|
+
expect(existsSync(planPath)).toBe(true);
|
|
304
|
+
expect(planPath).toContain('docs/04-operations/plans');
|
|
305
|
+
});
|
|
306
|
+
it('should truncate long titles in filename', async () => {
|
|
307
|
+
const { createPlanTemplate } = await import('../initiative-plan.js');
|
|
308
|
+
const longTitle = 'This is an extremely long initiative title that should be truncated in the filename';
|
|
309
|
+
const planPath = createPlanTemplate(tempDir, 'INIT-001', longTitle);
|
|
310
|
+
expect(existsSync(planPath)).toBe(true);
|
|
311
|
+
// Filename should be truncated
|
|
312
|
+
const filename = planPath.split('/').pop() || '';
|
|
313
|
+
// INIT-001- is 9 chars, .md is 3 chars, slug should be max 30 chars
|
|
314
|
+
expect(filename.length).toBeLessThanOrEqual(9 + 30 + 3);
|
|
315
|
+
});
|
|
316
|
+
it('should handle special characters in title', async () => {
|
|
317
|
+
const { createPlanTemplate } = await import('../initiative-plan.js');
|
|
318
|
+
const specialTitle = "Test's Initiative: (Special) Chars! @#$%";
|
|
319
|
+
const planPath = createPlanTemplate(tempDir, 'INIT-001', specialTitle);
|
|
320
|
+
expect(existsSync(planPath)).toBe(true);
|
|
321
|
+
// Filename should only have kebab-case characters
|
|
322
|
+
expect(planPath).toMatch(/INIT-001-[a-z0-9-]+\.md$/);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
/**
|
|
326
|
+
* Note on main() function testing:
|
|
327
|
+
*
|
|
328
|
+
* The main() function is intentionally not unit-tested because:
|
|
329
|
+
* 1. It calls die() which invokes process.exit() - difficult to mock without complex test infrastructure
|
|
330
|
+
* 2. It involves micro-worktree operations with git
|
|
331
|
+
* 3. All business logic functions it calls ARE thoroughly tested above
|
|
332
|
+
*
|
|
333
|
+
* The main() function is integration/orchestration code that composes the tested helper functions.
|
|
334
|
+
* Integration testing via subprocess (pnpm init:plan) is the appropriate testing strategy for main().
|
|
335
|
+
*
|
|
336
|
+
* Coverage statistics:
|
|
337
|
+
* - All exported helper functions: ~100% coverage
|
|
338
|
+
* - main() function: Not unit tested (orchestration code)
|
|
339
|
+
* - Overall file coverage: ~50% (acceptable for CLI commands)
|
|
340
|
+
*/
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
const CLI_DIST_PATH = path.resolve(__dirname, '../../dist/mem-cleanup.js');
|
|
7
|
+
describe('mem:cleanup CLI execution', () => {
|
|
8
|
+
it('should run --help without crashing', async () => {
|
|
9
|
+
try {
|
|
10
|
+
// We run the actual built JS file to catch ESM/CJS compatibility issues
|
|
11
|
+
const { stdout } = await execAsync(`node ${CLI_DIST_PATH} --help`);
|
|
12
|
+
expect(stdout).toContain('Usage: mem-cleanup');
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
// If it fails, we want to see why (expecting ReferenceError: require is not defined)
|
|
16
|
+
throw new Error(`Command failed: ${error.message}\nStderr: ${error.stderr}`);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
});
|