@lumenflow/cli 2.8.0 → 2.9.0
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 +3 -2
- package/dist/__tests__/commands.test.js +75 -0
- package/dist/__tests__/doctor.test.js +510 -0
- package/dist/__tests__/init.test.js +222 -0
- package/dist/commands.js +171 -0
- package/dist/doctor.js +479 -8
- package/dist/init.js +248 -6
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -147,10 +147,11 @@ This package provides CLI commands for the LumenFlow workflow framework, includi
|
|
|
147
147
|
| `guard-main-branch` | Guard Main Branch CLI Tool |
|
|
148
148
|
| `guard-worktree-commit` | |
|
|
149
149
|
| `init-plan` | Link a plan file to an initiative |
|
|
150
|
-
| `lumenflow` | Initialize LumenFlow in a project
|
|
150
|
+
| `lumenflow` | Initialize LumenFlow in a project\n\n |
|
|
151
|
+
| `lumenflow-commands` | List all available LumenFlow CLI commands |
|
|
151
152
|
| `lumenflow-docs-sync` | Sync agent onboarding docs to existing projects (skips existing files by default) |
|
|
152
153
|
| `lumenflow-doctor` | Check LumenFlow safety components and configuration |
|
|
153
|
-
| `lumenflow-init` | Initialize LumenFlow in a project
|
|
154
|
+
| `lumenflow-init` | Initialize LumenFlow in a project\n\n |
|
|
154
155
|
| `lumenflow-integrate` | Integrate LumenFlow enforcement with AI client tools |
|
|
155
156
|
| `lumenflow-metrics` | Unified Metrics CLI with subcommands (WU-1110) |
|
|
156
157
|
| `lumenflow-release` | Release Command |
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file commands.test.ts
|
|
3
|
+
* Tests for lumenflow commands discovery feature (WU-1378)
|
|
4
|
+
*
|
|
5
|
+
* Tests the new commands subcommand that lists all available CLI commands
|
|
6
|
+
* grouped by category with brief descriptions.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import { getCommandsRegistry, formatCommandsOutput } from '../commands.js';
|
|
10
|
+
describe('lumenflow commands', () => {
|
|
11
|
+
describe('getCommandsRegistry', () => {
|
|
12
|
+
it('should return command categories', () => {
|
|
13
|
+
const registry = getCommandsRegistry();
|
|
14
|
+
expect(registry).toBeDefined();
|
|
15
|
+
expect(Array.isArray(registry)).toBe(true);
|
|
16
|
+
expect(registry.length).toBeGreaterThan(0);
|
|
17
|
+
});
|
|
18
|
+
it('should include WU Lifecycle category with key commands', () => {
|
|
19
|
+
const registry = getCommandsRegistry();
|
|
20
|
+
const wuLifecycle = registry.find((cat) => cat.name === 'WU Lifecycle');
|
|
21
|
+
expect(wuLifecycle).toBeDefined();
|
|
22
|
+
expect(wuLifecycle.commands).toBeDefined();
|
|
23
|
+
const commandNames = wuLifecycle.commands.map((cmd) => cmd.name);
|
|
24
|
+
expect(commandNames).toContain('wu:create');
|
|
25
|
+
expect(commandNames).toContain('wu:claim');
|
|
26
|
+
});
|
|
27
|
+
it('should include Initiatives category', () => {
|
|
28
|
+
const registry = getCommandsRegistry();
|
|
29
|
+
const initiatives = registry.find((cat) => cat.name === 'Initiatives');
|
|
30
|
+
expect(initiatives).toBeDefined();
|
|
31
|
+
expect(initiatives.commands.some((cmd) => cmd.name === 'initiative:create')).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
it('should include Gates & Quality category with gates command', () => {
|
|
34
|
+
const registry = getCommandsRegistry();
|
|
35
|
+
const gatesCategory = registry.find((cat) => cat.name === 'Gates & Quality');
|
|
36
|
+
expect(gatesCategory).toBeDefined();
|
|
37
|
+
expect(gatesCategory.commands.some((cmd) => cmd.name === 'gates')).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
it('should have description for each command', () => {
|
|
40
|
+
const registry = getCommandsRegistry();
|
|
41
|
+
for (const category of registry) {
|
|
42
|
+
for (const cmd of category.commands) {
|
|
43
|
+
expect(cmd.description).toBeDefined();
|
|
44
|
+
expect(cmd.description.length).toBeGreaterThan(0);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('formatCommandsOutput', () => {
|
|
50
|
+
it('should include category headers', () => {
|
|
51
|
+
const output = formatCommandsOutput();
|
|
52
|
+
expect(output).toContain('WU Lifecycle');
|
|
53
|
+
expect(output).toContain('Initiatives');
|
|
54
|
+
expect(output).toContain('Gates & Quality');
|
|
55
|
+
});
|
|
56
|
+
it('should include command names and descriptions', () => {
|
|
57
|
+
const output = formatCommandsOutput();
|
|
58
|
+
expect(output).toContain('wu:create');
|
|
59
|
+
expect(output).toContain('wu:claim');
|
|
60
|
+
expect(output).toContain('initiative:create');
|
|
61
|
+
expect(output).toContain('gates');
|
|
62
|
+
});
|
|
63
|
+
it('should include hint to run --help for details', () => {
|
|
64
|
+
const output = formatCommandsOutput();
|
|
65
|
+
expect(output).toMatch(/--help/i);
|
|
66
|
+
});
|
|
67
|
+
it('should format output with clear grouping', () => {
|
|
68
|
+
const output = formatCommandsOutput();
|
|
69
|
+
const lines = output.split('\n');
|
|
70
|
+
// Should have multiple non-empty lines
|
|
71
|
+
const nonEmptyLines = lines.filter((line) => line.trim().length > 0);
|
|
72
|
+
expect(nonEmptyLines.length).toBeGreaterThan(10);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor CLI Tests (WU-1386)
|
|
3
|
+
*
|
|
4
|
+
* Tests for the agent-friction checks extension to lumenflow doctor:
|
|
5
|
+
* - Managed-file dirty checks (uncommitted changes to managed files)
|
|
6
|
+
* - WU validity check (--deep flag runs wu:validate --all)
|
|
7
|
+
* - Worktree sanity check (orphan detection from wu:prune)
|
|
8
|
+
* - Exit codes: 0=healthy, 1=warnings, 2=errors
|
|
9
|
+
* - Auto-run after init (non-blocking)
|
|
10
|
+
*
|
|
11
|
+
* Note: These tests use real git operations (not mocked) to verify
|
|
12
|
+
* the actual implementation behavior.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
17
|
+
import { tmpdir } from 'node:os';
|
|
18
|
+
import { execFileSync } from 'node:child_process';
|
|
19
|
+
/**
|
|
20
|
+
* Import the module under test
|
|
21
|
+
*/
|
|
22
|
+
import { runDoctor, runDoctorForInit } from '../doctor.js';
|
|
23
|
+
/**
|
|
24
|
+
* Test constants
|
|
25
|
+
*/
|
|
26
|
+
const HUSKY_DIR = '.husky';
|
|
27
|
+
const SCRIPTS_DIR = 'scripts';
|
|
28
|
+
const DOCS_TASKS_DIR = 'docs/04-operations/tasks';
|
|
29
|
+
/**
|
|
30
|
+
* Test directory path
|
|
31
|
+
*/
|
|
32
|
+
let testDir;
|
|
33
|
+
/**
|
|
34
|
+
* Helper to create a minimal valid project structure
|
|
35
|
+
*/
|
|
36
|
+
function setupMinimalProject(baseDir) {
|
|
37
|
+
// Create husky
|
|
38
|
+
mkdirSync(join(baseDir, HUSKY_DIR), { recursive: true });
|
|
39
|
+
writeFileSync(join(baseDir, HUSKY_DIR, 'pre-commit'), '#!/bin/sh\n', 'utf-8');
|
|
40
|
+
// Create safe-git
|
|
41
|
+
mkdirSync(join(baseDir, SCRIPTS_DIR), { recursive: true });
|
|
42
|
+
writeFileSync(join(baseDir, SCRIPTS_DIR, 'safe-git'), '#!/bin/sh\n', 'utf-8');
|
|
43
|
+
// Create AGENTS.md
|
|
44
|
+
writeFileSync(join(baseDir, 'AGENTS.md'), '# Agents\n', 'utf-8');
|
|
45
|
+
// Create .lumenflow.config.yaml
|
|
46
|
+
writeFileSync(join(baseDir, '.lumenflow.config.yaml'), 'lanes: []\n', 'utf-8');
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Helper to initialize git in test directory
|
|
50
|
+
*/
|
|
51
|
+
function initGit(baseDir) {
|
|
52
|
+
execFileSync('git', ['init', '-b', 'main'], { cwd: baseDir, stdio: 'pipe' });
|
|
53
|
+
execFileSync('git', ['config', 'user.email', 'test@test.com'], {
|
|
54
|
+
cwd: baseDir,
|
|
55
|
+
stdio: 'pipe',
|
|
56
|
+
});
|
|
57
|
+
execFileSync('git', ['config', 'user.name', 'Test'], { cwd: baseDir, stdio: 'pipe' });
|
|
58
|
+
}
|
|
59
|
+
describe('doctor CLI (WU-1386) - Agent Friction Checks', () => {
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
testDir = mkdtempSync(join(tmpdir(), 'doctor-test-'));
|
|
62
|
+
});
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
try {
|
|
65
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Ignore cleanup errors
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
describe('managed-file dirty check', () => {
|
|
72
|
+
it('should detect uncommitted changes to .lumenflow.config.yaml', async () => {
|
|
73
|
+
setupMinimalProject(testDir);
|
|
74
|
+
initGit(testDir);
|
|
75
|
+
// Commit initial state
|
|
76
|
+
execFileSync('git', ['add', '.'], { cwd: testDir, stdio: 'pipe' });
|
|
77
|
+
execFileSync('git', ['commit', '-m', 'initial'], { cwd: testDir, stdio: 'pipe' });
|
|
78
|
+
// Modify managed file
|
|
79
|
+
writeFileSync(join(testDir, '.lumenflow.config.yaml'), 'lanes: []\nmodified: true\n', 'utf-8');
|
|
80
|
+
const result = await runDoctor(testDir);
|
|
81
|
+
expect(result.workflowHealth).toBeDefined();
|
|
82
|
+
expect(result.workflowHealth?.managedFilesDirty.passed).toBe(false);
|
|
83
|
+
expect(result.workflowHealth?.managedFilesDirty.files).toContain('.lumenflow.config.yaml');
|
|
84
|
+
});
|
|
85
|
+
it('should detect uncommitted changes to docs/04-operations/tasks/**', async () => {
|
|
86
|
+
setupMinimalProject(testDir);
|
|
87
|
+
initGit(testDir);
|
|
88
|
+
// Create and commit a placeholder file in the tasks directory first
|
|
89
|
+
// This is needed because git shows untracked directories as just "?? docs/"
|
|
90
|
+
// but shows files in tracked directories with full paths
|
|
91
|
+
mkdirSync(join(testDir, DOCS_TASKS_DIR, 'wu'), { recursive: true });
|
|
92
|
+
writeFileSync(join(testDir, DOCS_TASKS_DIR, 'wu', 'WU-000.yaml'), 'id: WU-000\nstatus: done\nlane: Framework: CLI\n', 'utf-8');
|
|
93
|
+
// Commit initial state including the placeholder
|
|
94
|
+
execFileSync('git', ['add', '.'], { cwd: testDir, stdio: 'pipe' });
|
|
95
|
+
execFileSync('git', ['commit', '-m', 'initial'], { cwd: testDir, stdio: 'pipe' });
|
|
96
|
+
// Now add a new WU file (uncommitted) - git will show full path
|
|
97
|
+
writeFileSync(join(testDir, DOCS_TASKS_DIR, 'wu', 'WU-001.yaml'), 'id: WU-001\nstatus: ready\nlane: Framework: CLI\n', 'utf-8');
|
|
98
|
+
const result = await runDoctor(testDir);
|
|
99
|
+
expect(result.workflowHealth?.managedFilesDirty.passed).toBe(false);
|
|
100
|
+
expect(result.workflowHealth?.managedFilesDirty.files).toContain('docs/04-operations/tasks/wu/WU-001.yaml');
|
|
101
|
+
});
|
|
102
|
+
it('should pass when no managed files have uncommitted changes', async () => {
|
|
103
|
+
setupMinimalProject(testDir);
|
|
104
|
+
initGit(testDir);
|
|
105
|
+
// Commit all files
|
|
106
|
+
execFileSync('git', ['add', '.'], { cwd: testDir, stdio: 'pipe' });
|
|
107
|
+
execFileSync('git', ['commit', '-m', 'initial'], { cwd: testDir, stdio: 'pipe' });
|
|
108
|
+
const result = await runDoctor(testDir);
|
|
109
|
+
expect(result.workflowHealth?.managedFilesDirty.passed).toBe(true);
|
|
110
|
+
expect(result.workflowHealth?.managedFilesDirty.files).toHaveLength(0);
|
|
111
|
+
});
|
|
112
|
+
it('should detect changes to AGENTS.md and CLAUDE.md', async () => {
|
|
113
|
+
setupMinimalProject(testDir);
|
|
114
|
+
writeFileSync(join(testDir, 'CLAUDE.md'), '# Claude\n', 'utf-8');
|
|
115
|
+
initGit(testDir);
|
|
116
|
+
// Commit initial state
|
|
117
|
+
execFileSync('git', ['add', '.'], { cwd: testDir, stdio: 'pipe' });
|
|
118
|
+
execFileSync('git', ['commit', '-m', 'initial'], { cwd: testDir, stdio: 'pipe' });
|
|
119
|
+
// Modify both files
|
|
120
|
+
writeFileSync(join(testDir, 'AGENTS.md'), '# Agents\nmodified\n', 'utf-8');
|
|
121
|
+
writeFileSync(join(testDir, 'CLAUDE.md'), '# Claude\nmodified\n', 'utf-8');
|
|
122
|
+
const result = await runDoctor(testDir);
|
|
123
|
+
expect(result.workflowHealth?.managedFilesDirty.passed).toBe(false);
|
|
124
|
+
expect(result.workflowHealth?.managedFilesDirty.files).toContain('AGENTS.md');
|
|
125
|
+
expect(result.workflowHealth?.managedFilesDirty.files).toContain('CLAUDE.md');
|
|
126
|
+
});
|
|
127
|
+
it('should gracefully handle non-git directories', async () => {
|
|
128
|
+
setupMinimalProject(testDir);
|
|
129
|
+
// Don't init git
|
|
130
|
+
const result = await runDoctor(testDir);
|
|
131
|
+
// Should pass with graceful degradation message
|
|
132
|
+
expect(result.workflowHealth?.managedFilesDirty.passed).toBe(true);
|
|
133
|
+
expect(result.workflowHealth?.managedFilesDirty.message).toContain('skipped');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe('worktree sanity check', () => {
|
|
137
|
+
it('should pass with graceful degradation in isolated test environment', async () => {
|
|
138
|
+
setupMinimalProject(testDir);
|
|
139
|
+
initGit(testDir);
|
|
140
|
+
execFileSync('git', ['add', '.'], { cwd: testDir, stdio: 'pipe' });
|
|
141
|
+
execFileSync('git', ['commit', '-m', 'initial'], { cwd: testDir, stdio: 'pipe' });
|
|
142
|
+
const result = await runDoctor(testDir);
|
|
143
|
+
// In isolated test env, wu:prune isn't available so it gracefully degrades
|
|
144
|
+
// The check passes with a skip message or runs successfully
|
|
145
|
+
expect(result.workflowHealth?.worktreeSanity.passed).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
it('should gracefully handle non-git directories', async () => {
|
|
148
|
+
setupMinimalProject(testDir);
|
|
149
|
+
// Don't init git
|
|
150
|
+
const result = await runDoctor(testDir);
|
|
151
|
+
// Should pass with graceful degradation
|
|
152
|
+
expect(result.workflowHealth?.worktreeSanity.passed).toBe(true);
|
|
153
|
+
expect(result.workflowHealth?.worktreeSanity.message).toContain('skipped');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
describe('--deep flag (WU validity check)', () => {
|
|
157
|
+
it('should skip WU validation by default (fast mode)', async () => {
|
|
158
|
+
setupMinimalProject(testDir);
|
|
159
|
+
const result = await runDoctor(testDir);
|
|
160
|
+
// WU validation should be undefined in default mode
|
|
161
|
+
expect(result.workflowHealth?.wuValidity).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
it('should include wuValidity in --deep mode', async () => {
|
|
164
|
+
setupMinimalProject(testDir);
|
|
165
|
+
mkdirSync(join(testDir, DOCS_TASKS_DIR, 'wu'), { recursive: true });
|
|
166
|
+
writeFileSync(join(testDir, DOCS_TASKS_DIR, 'wu', 'WU-001.yaml'), 'id: WU-001\nstatus: ready\nlane: Framework: CLI\n', 'utf-8');
|
|
167
|
+
const result = await runDoctor(testDir, { deep: true });
|
|
168
|
+
// WU validation should be included in deep mode
|
|
169
|
+
expect(result.workflowHealth?.wuValidity).toBeDefined();
|
|
170
|
+
// WU-1387: In isolated test env without wu:validate CLI, should report failure
|
|
171
|
+
// (previously this would silently pass, now it correctly reports the CLI failure)
|
|
172
|
+
expect(typeof result.workflowHealth?.wuValidity?.passed).toBe('boolean');
|
|
173
|
+
// The message should indicate the validation ran or explain why it couldn't
|
|
174
|
+
expect(result.workflowHealth?.wuValidity?.message).toBeTruthy();
|
|
175
|
+
});
|
|
176
|
+
it('should gracefully handle missing wu:validate CLI', async () => {
|
|
177
|
+
setupMinimalProject(testDir);
|
|
178
|
+
// No WU directory - should still handle gracefully
|
|
179
|
+
const result = await runDoctor(testDir, { deep: true });
|
|
180
|
+
// No WU directory means passed=true with "No WU directory found" message
|
|
181
|
+
expect(result.workflowHealth?.wuValidity).toBeDefined();
|
|
182
|
+
expect(result.workflowHealth?.wuValidity?.passed).toBe(true);
|
|
183
|
+
expect(result.workflowHealth?.wuValidity?.message).toContain('No WU');
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
describe('exit codes', () => {
|
|
187
|
+
it('should return exitCode 0 when all checks pass', async () => {
|
|
188
|
+
setupMinimalProject(testDir);
|
|
189
|
+
initGit(testDir);
|
|
190
|
+
execFileSync('git', ['add', '.'], { cwd: testDir, stdio: 'pipe' });
|
|
191
|
+
execFileSync('git', ['commit', '-m', 'initial'], { cwd: testDir, stdio: 'pipe' });
|
|
192
|
+
const result = await runDoctor(testDir);
|
|
193
|
+
expect(result.exitCode).toBe(0);
|
|
194
|
+
});
|
|
195
|
+
it('should return exitCode 1 when warnings are present', async () => {
|
|
196
|
+
setupMinimalProject(testDir);
|
|
197
|
+
initGit(testDir);
|
|
198
|
+
execFileSync('git', ['add', '.'], { cwd: testDir, stdio: 'pipe' });
|
|
199
|
+
execFileSync('git', ['commit', '-m', 'initial'], { cwd: testDir, stdio: 'pipe' });
|
|
200
|
+
// Modify managed file to create warning
|
|
201
|
+
writeFileSync(join(testDir, '.lumenflow.config.yaml'), 'lanes: []\nmodified: true\n', 'utf-8');
|
|
202
|
+
const result = await runDoctor(testDir);
|
|
203
|
+
expect(result.exitCode).toBe(1);
|
|
204
|
+
});
|
|
205
|
+
it('should return exitCode 2 when critical errors are present', async () => {
|
|
206
|
+
// No husky hooks - critical error
|
|
207
|
+
mkdirSync(join(testDir, SCRIPTS_DIR), { recursive: true });
|
|
208
|
+
writeFileSync(join(testDir, SCRIPTS_DIR, 'safe-git'), '#!/bin/sh\n', 'utf-8');
|
|
209
|
+
writeFileSync(join(testDir, 'AGENTS.md'), '# Agents\n', 'utf-8');
|
|
210
|
+
writeFileSync(join(testDir, '.lumenflow.config.yaml'), 'lanes: []\n', 'utf-8');
|
|
211
|
+
const result = await runDoctor(testDir);
|
|
212
|
+
expect(result.exitCode).toBe(2);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
describe('doctor result structure', () => {
|
|
216
|
+
it('should include workflowHealth section in result', async () => {
|
|
217
|
+
setupMinimalProject(testDir);
|
|
218
|
+
const result = await runDoctor(testDir);
|
|
219
|
+
// New workflowHealth section should be present
|
|
220
|
+
expect(result.workflowHealth).toBeDefined();
|
|
221
|
+
expect(result.workflowHealth?.managedFilesDirty).toBeDefined();
|
|
222
|
+
expect(result.workflowHealth?.worktreeSanity).toBeDefined();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
describe('doctor auto-run after init (WU-1386)', () => {
|
|
227
|
+
beforeEach(() => {
|
|
228
|
+
testDir = mkdtempSync(join(tmpdir(), 'doctor-init-test-'));
|
|
229
|
+
});
|
|
230
|
+
afterEach(() => {
|
|
231
|
+
try {
|
|
232
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// Ignore cleanup errors
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
it('should never block init even with warnings', async () => {
|
|
239
|
+
setupMinimalProject(testDir);
|
|
240
|
+
initGit(testDir);
|
|
241
|
+
execFileSync('git', ['add', '.'], { cwd: testDir, stdio: 'pipe' });
|
|
242
|
+
execFileSync('git', ['commit', '-m', 'initial'], { cwd: testDir, stdio: 'pipe' });
|
|
243
|
+
// Modify managed file to create warning
|
|
244
|
+
writeFileSync(join(testDir, '.lumenflow.config.yaml'), 'lanes: []\nmodified: true\n', 'utf-8');
|
|
245
|
+
const result = await runDoctorForInit(testDir);
|
|
246
|
+
// Should return warnings but not block
|
|
247
|
+
expect(result.blocked).toBe(false);
|
|
248
|
+
expect(result.warnings).toBeGreaterThan(0);
|
|
249
|
+
});
|
|
250
|
+
it('should print warnings but return success', async () => {
|
|
251
|
+
setupMinimalProject(testDir);
|
|
252
|
+
const result = await runDoctorForInit(testDir);
|
|
253
|
+
// Non-blocking mode should always indicate success
|
|
254
|
+
expect(result.blocked).toBe(false);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
/**
|
|
258
|
+
* WU-1387 Edge Case Tests
|
|
259
|
+
*
|
|
260
|
+
* Tests for the specific edge cases identified in WU-1386 review:
|
|
261
|
+
* - AC1: Worktree sanity parsing for orphan, missing, stale, blocked, unclaimed worktrees
|
|
262
|
+
* - AC2: WU validity passes=false when CLI errors
|
|
263
|
+
* - AC3: runDoctorForInit shows accurate status including lane health and prereqs
|
|
264
|
+
* - AC4: Managed-file detection from git repo root in subdirectories
|
|
265
|
+
* - AC5: Real output parsing (not just graceful degradation)
|
|
266
|
+
*/
|
|
267
|
+
describe('WU-1387 Edge Cases - Worktree Sanity Parsing', () => {
|
|
268
|
+
/**
|
|
269
|
+
* These tests use a mock wu:prune module to test parsing of various output formats.
|
|
270
|
+
* The real wu:prune produces these outputs, we need to verify doctor parses them.
|
|
271
|
+
*/
|
|
272
|
+
let testDir;
|
|
273
|
+
beforeEach(() => {
|
|
274
|
+
testDir = mkdtempSync(join(tmpdir(), 'doctor-1387-'));
|
|
275
|
+
});
|
|
276
|
+
afterEach(() => {
|
|
277
|
+
try {
|
|
278
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
// Ignore cleanup errors
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
describe('AC1: parseWorktreePruneOutput helper', () => {
|
|
285
|
+
/**
|
|
286
|
+
* Import the parsing helper directly for unit testing
|
|
287
|
+
*/
|
|
288
|
+
it('should parse orphan directory counts from summary', async () => {
|
|
289
|
+
// This test validates that the parser can extract counts from wu:prune summary
|
|
290
|
+
const sampleOutput = `[wu-prune] Summary
|
|
291
|
+
[wu-prune] ========
|
|
292
|
+
[wu-prune] Tracked worktrees: 3
|
|
293
|
+
[wu-prune] Orphan directories: 2
|
|
294
|
+
[wu-prune] Warnings: 1
|
|
295
|
+
[wu-prune] Errors: 0`;
|
|
296
|
+
// Call the parsing helper (we'll need to export it or test via integration)
|
|
297
|
+
// For now, test via runDoctor which internally uses the parser
|
|
298
|
+
setupMinimalProject(testDir);
|
|
299
|
+
initGit(testDir);
|
|
300
|
+
execFileSync('git', ['add', '.'], { cwd: testDir, stdio: 'pipe' });
|
|
301
|
+
execFileSync('git', ['commit', '-m', 'initial'], { cwd: testDir, stdio: 'pipe' });
|
|
302
|
+
// The result should handle parsing when wu:prune is available
|
|
303
|
+
const result = await runDoctor(testDir);
|
|
304
|
+
expect(result.workflowHealth).toBeDefined();
|
|
305
|
+
expect(result.workflowHealth?.worktreeSanity).toBeDefined();
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
describe('WU-1387 Edge Cases - WU Validity Error Handling', () => {
|
|
310
|
+
let testDir;
|
|
311
|
+
beforeEach(() => {
|
|
312
|
+
testDir = mkdtempSync(join(tmpdir(), 'doctor-wuvalid-'));
|
|
313
|
+
});
|
|
314
|
+
afterEach(() => {
|
|
315
|
+
try {
|
|
316
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
// Ignore cleanup errors
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
describe('AC2: CLI error handling', () => {
|
|
323
|
+
it('should handle validation gracefully when wu directory exists but validate fails', async () => {
|
|
324
|
+
setupMinimalProject(testDir);
|
|
325
|
+
mkdirSync(join(testDir, DOCS_TASKS_DIR, 'wu'), { recursive: true });
|
|
326
|
+
// Create a malformed WU file that will cause validation issues
|
|
327
|
+
writeFileSync(join(testDir, DOCS_TASKS_DIR, 'wu', 'WU-TEST.yaml'), 'id: WU-TEST\nstatus: invalid_status\nlane: Framework: CLI\n', 'utf-8');
|
|
328
|
+
const result = await runDoctor(testDir, { deep: true });
|
|
329
|
+
// Should still have a result, either gracefully degraded or actually validated
|
|
330
|
+
expect(result.workflowHealth?.wuValidity).toBeDefined();
|
|
331
|
+
// In isolated env, this will gracefully skip
|
|
332
|
+
expect(typeof result.workflowHealth?.wuValidity?.passed).toBe('boolean');
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
describe('WU-1387 Edge Cases - runDoctorForInit Accuracy', () => {
|
|
337
|
+
let testDir;
|
|
338
|
+
beforeEach(() => {
|
|
339
|
+
testDir = mkdtempSync(join(tmpdir(), 'doctor-init-'));
|
|
340
|
+
});
|
|
341
|
+
afterEach(() => {
|
|
342
|
+
try {
|
|
343
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// Ignore cleanup errors
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
describe('AC3: Accurate status reporting', () => {
|
|
350
|
+
it('should report all critical errors in output', async () => {
|
|
351
|
+
// Create directory without any required files
|
|
352
|
+
mkdirSync(testDir, { recursive: true });
|
|
353
|
+
const result = await runDoctorForInit(testDir);
|
|
354
|
+
// Should report errors for missing critical components
|
|
355
|
+
expect(result.errors).toBeGreaterThan(0);
|
|
356
|
+
// Output should contain specific error descriptions
|
|
357
|
+
expect(result.output).toMatch(/husky|hook/i);
|
|
358
|
+
expect(result.output).toMatch(/safe-git|script/i);
|
|
359
|
+
expect(result.output).toMatch(/AGENTS|agent/i);
|
|
360
|
+
});
|
|
361
|
+
it('should count workflow health warnings separately from errors', async () => {
|
|
362
|
+
setupMinimalProject(testDir);
|
|
363
|
+
initGit(testDir);
|
|
364
|
+
execFileSync('git', ['add', '.'], { cwd: testDir, stdio: 'pipe' });
|
|
365
|
+
execFileSync('git', ['commit', '-m', 'initial'], { cwd: testDir, stdio: 'pipe' });
|
|
366
|
+
// Modify managed file to create workflow warning
|
|
367
|
+
writeFileSync(join(testDir, '.lumenflow.config.yaml'), 'lanes: []\nmodified: true\n', 'utf-8');
|
|
368
|
+
const result = await runDoctorForInit(testDir);
|
|
369
|
+
// Should have warnings but no errors (all critical checks pass)
|
|
370
|
+
expect(result.errors).toBe(0);
|
|
371
|
+
expect(result.warnings).toBeGreaterThan(0);
|
|
372
|
+
expect(result.output).toMatch(/uncommitted|managed/i);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
describe('WU-1387 Edge Cases - Managed File Detection', () => {
|
|
377
|
+
let testDir;
|
|
378
|
+
beforeEach(() => {
|
|
379
|
+
testDir = mkdtempSync(join(tmpdir(), 'doctor-managed-'));
|
|
380
|
+
});
|
|
381
|
+
afterEach(() => {
|
|
382
|
+
try {
|
|
383
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
// Ignore cleanup errors
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
describe('AC4: Git repo root path resolution', () => {
|
|
390
|
+
it('should detect managed files when running from subdirectory', async () => {
|
|
391
|
+
// Setup project structure
|
|
392
|
+
setupMinimalProject(testDir);
|
|
393
|
+
initGit(testDir);
|
|
394
|
+
// Create subdirectory structure
|
|
395
|
+
const subDir = join(testDir, 'packages', 'cli');
|
|
396
|
+
mkdirSync(subDir, { recursive: true });
|
|
397
|
+
// Commit initial state
|
|
398
|
+
execFileSync('git', ['add', '.'], { cwd: testDir, stdio: 'pipe' });
|
|
399
|
+
execFileSync('git', ['commit', '-m', 'initial'], { cwd: testDir, stdio: 'pipe' });
|
|
400
|
+
// Modify managed file at repo root
|
|
401
|
+
writeFileSync(join(testDir, '.lumenflow.config.yaml'), 'lanes: []\nmodified: true\n', 'utf-8');
|
|
402
|
+
// Run doctor from subdirectory
|
|
403
|
+
const result = await runDoctor(subDir);
|
|
404
|
+
// Should still detect the modified managed file at repo root
|
|
405
|
+
expect(result.workflowHealth?.managedFilesDirty.passed).toBe(false);
|
|
406
|
+
expect(result.workflowHealth?.managedFilesDirty.files).toContain('.lumenflow.config.yaml');
|
|
407
|
+
});
|
|
408
|
+
it('should use git repo root for all path comparisons', async () => {
|
|
409
|
+
setupMinimalProject(testDir);
|
|
410
|
+
initGit(testDir);
|
|
411
|
+
// Create deeply nested subdirectory
|
|
412
|
+
const deepSubDir = join(testDir, 'packages', 'core', 'src', 'lib');
|
|
413
|
+
mkdirSync(deepSubDir, { recursive: true });
|
|
414
|
+
mkdirSync(join(testDir, DOCS_TASKS_DIR, 'wu'), { recursive: true });
|
|
415
|
+
// Create a tracked file in the managed directory
|
|
416
|
+
writeFileSync(join(testDir, DOCS_TASKS_DIR, 'wu', 'WU-TRACK.yaml'), 'id: WU-TRACK\n', 'utf-8');
|
|
417
|
+
// Commit initial state
|
|
418
|
+
execFileSync('git', ['add', '.'], { cwd: testDir, stdio: 'pipe' });
|
|
419
|
+
execFileSync('git', ['commit', '-m', 'initial'], { cwd: testDir, stdio: 'pipe' });
|
|
420
|
+
// Modify the WU file
|
|
421
|
+
writeFileSync(join(testDir, DOCS_TASKS_DIR, 'wu', 'WU-TRACK.yaml'), 'id: WU-TRACK\nmodified: true\n', 'utf-8');
|
|
422
|
+
// Run doctor from deeply nested subdirectory
|
|
423
|
+
const result = await runDoctor(deepSubDir);
|
|
424
|
+
// Should detect modified file using paths relative to repo root
|
|
425
|
+
expect(result.workflowHealth?.managedFilesDirty.passed).toBe(false);
|
|
426
|
+
expect(result.workflowHealth?.managedFilesDirty.files.some((f) => f.includes('WU-TRACK'))).toBe(true);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
/**
|
|
431
|
+
* WU-1387 AC5: Real output parsing tests
|
|
432
|
+
* These unit tests verify the parsing logic directly with sample outputs
|
|
433
|
+
*/
|
|
434
|
+
describe('WU-1387 AC5: Real Output Parsing', () => {
|
|
435
|
+
// Import the parsing helper for direct testing
|
|
436
|
+
// Note: We test via integration since the helper is not exported
|
|
437
|
+
describe('worktree sanity parsing via integration', () => {
|
|
438
|
+
let testDir;
|
|
439
|
+
beforeEach(() => {
|
|
440
|
+
testDir = mkdtempSync(join(tmpdir(), 'doctor-parsing-'));
|
|
441
|
+
});
|
|
442
|
+
afterEach(() => {
|
|
443
|
+
try {
|
|
444
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
// Ignore cleanup errors
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
it('should correctly interpret valid worktree output', async () => {
|
|
451
|
+
setupMinimalProject(testDir);
|
|
452
|
+
initGit(testDir);
|
|
453
|
+
execFileSync('git', ['add', '.'], { cwd: testDir, stdio: 'pipe' });
|
|
454
|
+
execFileSync('git', ['commit', '-m', 'initial'], { cwd: testDir, stdio: 'pipe' });
|
|
455
|
+
const result = await runDoctor(testDir);
|
|
456
|
+
// In a clean project, worktree sanity should pass
|
|
457
|
+
// Either it runs and finds no issues, or gracefully degrades
|
|
458
|
+
expect(result.workflowHealth?.worktreeSanity).toBeDefined();
|
|
459
|
+
expect(typeof result.workflowHealth?.worktreeSanity.passed).toBe('boolean');
|
|
460
|
+
expect(typeof result.workflowHealth?.worktreeSanity.orphans).toBe('number');
|
|
461
|
+
expect(typeof result.workflowHealth?.worktreeSanity.stale).toBe('number');
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
describe('WU validity parsing via integration', () => {
|
|
465
|
+
let testDir;
|
|
466
|
+
beforeEach(() => {
|
|
467
|
+
testDir = mkdtempSync(join(tmpdir(), 'doctor-wuparse-'));
|
|
468
|
+
});
|
|
469
|
+
afterEach(() => {
|
|
470
|
+
try {
|
|
471
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
// Ignore cleanup errors
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
it('should return structured result with all expected fields', async () => {
|
|
478
|
+
setupMinimalProject(testDir);
|
|
479
|
+
mkdirSync(join(testDir, DOCS_TASKS_DIR, 'wu'), { recursive: true });
|
|
480
|
+
writeFileSync(join(testDir, DOCS_TASKS_DIR, 'wu', 'WU-TEST.yaml'), 'id: WU-TEST\nstatus: ready\nlane: Test\n', 'utf-8');
|
|
481
|
+
const result = await runDoctor(testDir, { deep: true });
|
|
482
|
+
// WU validity should have all expected fields
|
|
483
|
+
expect(result.workflowHealth?.wuValidity).toBeDefined();
|
|
484
|
+
const wuValidity = result.workflowHealth?.wuValidity;
|
|
485
|
+
expect(typeof wuValidity?.passed).toBe('boolean');
|
|
486
|
+
expect(typeof wuValidity?.total).toBe('number');
|
|
487
|
+
expect(typeof wuValidity?.valid).toBe('number');
|
|
488
|
+
expect(typeof wuValidity?.invalid).toBe('number');
|
|
489
|
+
expect(typeof wuValidity?.warnings).toBe('number');
|
|
490
|
+
expect(typeof wuValidity?.message).toBe('string');
|
|
491
|
+
expect(wuValidity?.message.length).toBeGreaterThan(0);
|
|
492
|
+
});
|
|
493
|
+
it('should set passed=false with clear message when CLI unavailable', async () => {
|
|
494
|
+
setupMinimalProject(testDir);
|
|
495
|
+
mkdirSync(join(testDir, DOCS_TASKS_DIR, 'wu'), { recursive: true });
|
|
496
|
+
writeFileSync(join(testDir, DOCS_TASKS_DIR, 'wu', 'WU-TEST.yaml'), 'id: WU-TEST\nstatus: ready\n', 'utf-8');
|
|
497
|
+
// In isolated test env without pnpm scripts, CLI will fail
|
|
498
|
+
const result = await runDoctor(testDir, { deep: true });
|
|
499
|
+
// WU-1387: Should report failure, not silently pass
|
|
500
|
+
expect(result.workflowHealth?.wuValidity).toBeDefined();
|
|
501
|
+
const wuValidity = result.workflowHealth?.wuValidity;
|
|
502
|
+
// Message should indicate failure reason
|
|
503
|
+
expect(wuValidity?.message).toBeTruthy();
|
|
504
|
+
// If CLI couldn't run, message should explain why
|
|
505
|
+
if (!wuValidity?.passed) {
|
|
506
|
+
expect(wuValidity?.message).toMatch(/failed|error|unavailable|not found|could not/i);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
});
|