@lumenflow/cli 2.4.0 → 2.5.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/dist/__tests__/init-config-lanes.test.js +131 -0
- package/dist/__tests__/init-docs-structure.test.js +119 -0
- package/dist/__tests__/init-lane-inference.test.js +125 -0
- package/dist/__tests__/init-onboarding-docs.test.js +132 -0
- package/dist/__tests__/init-quick-ref.test.js +145 -0
- package/dist/__tests__/init-scripts.test.js +96 -0
- package/dist/__tests__/init-template-portability.test.js +97 -0
- package/dist/__tests__/init.test.js +7 -2
- package/dist/__tests__/initiative-add-wu.test.js +420 -0
- package/dist/__tests__/initiative-plan-replacement.test.js +162 -0
- package/dist/__tests__/initiative-remove-wu.test.js +458 -0
- package/dist/__tests__/onboarding-smoke-test.test.js +211 -0
- package/dist/__tests__/path-centralization-cli.test.js +234 -0
- package/dist/__tests__/plan-create.test.js +126 -0
- package/dist/__tests__/plan-edit.test.js +157 -0
- package/dist/__tests__/plan-link.test.js +239 -0
- package/dist/__tests__/plan-promote.test.js +181 -0
- package/dist/__tests__/wu-create-strict.test.js +118 -0
- package/dist/__tests__/wu-edit-strict.test.js +109 -0
- package/dist/__tests__/wu-validate-strict.test.js +113 -0
- package/dist/flow-bottlenecks.js +4 -2
- package/dist/gates.js +22 -0
- package/dist/init.js +580 -87
- package/dist/initiative-add-wu.js +112 -16
- package/dist/initiative-remove-wu.js +248 -0
- package/dist/onboarding-smoke-test.js +400 -0
- package/dist/plan-create.js +199 -0
- package/dist/plan-edit.js +235 -0
- package/dist/plan-link.js +233 -0
- package/dist/plan-promote.js +231 -0
- package/dist/wu-block.js +16 -5
- package/dist/wu-claim.js +15 -9
- package/dist/wu-create.js +50 -2
- package/dist/wu-deps.js +3 -1
- package/dist/wu-done.js +14 -5
- package/dist/wu-edit.js +35 -0
- package/dist/wu-spawn.js +8 -0
- package/dist/wu-unblock.js +34 -2
- package/dist/wu-validate.js +25 -17
- package/package.json +10 -6
- package/templates/core/AGENTS.md.template +2 -2
- package/dist/__tests__/init-plan.test.js +0 -340
- package/dist/agent-issues-query.d.ts +0 -16
- package/dist/agent-log-issue.d.ts +0 -10
- package/dist/agent-session-end.d.ts +0 -10
- package/dist/agent-session.d.ts +0 -10
- package/dist/backlog-prune.d.ts +0 -84
- package/dist/cli-entry-point.d.ts +0 -8
- package/dist/deps-add.d.ts +0 -91
- package/dist/deps-remove.d.ts +0 -17
- package/dist/docs-sync.d.ts +0 -50
- package/dist/file-delete.d.ts +0 -84
- package/dist/file-edit.d.ts +0 -82
- package/dist/file-read.d.ts +0 -92
- package/dist/file-write.d.ts +0 -90
- package/dist/flow-bottlenecks.d.ts +0 -16
- package/dist/flow-report.d.ts +0 -16
- package/dist/gates.d.ts +0 -94
- package/dist/git-branch.d.ts +0 -65
- package/dist/git-diff.d.ts +0 -58
- package/dist/git-log.d.ts +0 -69
- package/dist/git-status.d.ts +0 -58
- package/dist/guard-locked.d.ts +0 -62
- package/dist/guard-main-branch.d.ts +0 -50
- package/dist/guard-worktree-commit.d.ts +0 -59
- package/dist/index.d.ts +0 -10
- package/dist/init-plan.d.ts +0 -80
- package/dist/init-plan.js +0 -337
- package/dist/init.d.ts +0 -46
- package/dist/initiative-add-wu.d.ts +0 -22
- package/dist/initiative-bulk-assign-wus.d.ts +0 -16
- package/dist/initiative-create.d.ts +0 -28
- package/dist/initiative-edit.d.ts +0 -34
- package/dist/initiative-list.d.ts +0 -12
- package/dist/initiative-status.d.ts +0 -11
- package/dist/lumenflow-upgrade.d.ts +0 -103
- package/dist/mem-checkpoint.d.ts +0 -16
- package/dist/mem-cleanup.d.ts +0 -29
- package/dist/mem-create.d.ts +0 -17
- package/dist/mem-export.d.ts +0 -10
- package/dist/mem-inbox.d.ts +0 -35
- package/dist/mem-init.d.ts +0 -15
- package/dist/mem-ready.d.ts +0 -16
- package/dist/mem-signal.d.ts +0 -16
- package/dist/mem-start.d.ts +0 -16
- package/dist/mem-summarize.d.ts +0 -22
- package/dist/mem-triage.d.ts +0 -22
- package/dist/metrics-cli.d.ts +0 -90
- package/dist/metrics-snapshot.d.ts +0 -18
- package/dist/orchestrate-init-status.d.ts +0 -11
- package/dist/orchestrate-initiative.d.ts +0 -12
- package/dist/orchestrate-monitor.d.ts +0 -11
- package/dist/release.d.ts +0 -117
- package/dist/rotate-progress.d.ts +0 -48
- package/dist/session-coordinator.d.ts +0 -74
- package/dist/spawn-list.d.ts +0 -16
- package/dist/state-bootstrap.d.ts +0 -92
- package/dist/sync-templates.d.ts +0 -52
- package/dist/trace-gen.d.ts +0 -84
- package/dist/validate-agent-skills.d.ts +0 -50
- package/dist/validate-agent-sync.d.ts +0 -36
- package/dist/validate-backlog-sync.d.ts +0 -37
- package/dist/validate-skills-spec.d.ts +0 -40
- package/dist/validate.d.ts +0 -60
- package/dist/wu-block.d.ts +0 -16
- package/dist/wu-claim.d.ts +0 -74
- package/dist/wu-cleanup.d.ts +0 -35
- package/dist/wu-create.d.ts +0 -69
- package/dist/wu-delete.d.ts +0 -21
- package/dist/wu-deps.d.ts +0 -13
- package/dist/wu-done.d.ts +0 -225
- package/dist/wu-edit.d.ts +0 -63
- package/dist/wu-infer-lane.d.ts +0 -17
- package/dist/wu-preflight.d.ts +0 -47
- package/dist/wu-prune.d.ts +0 -16
- package/dist/wu-recover.d.ts +0 -37
- package/dist/wu-release.d.ts +0 -19
- package/dist/wu-repair.d.ts +0 -60
- package/dist/wu-spawn-completion.d.ts +0 -10
- package/dist/wu-spawn.d.ts +0 -192
- package/dist/wu-status.d.ts +0 -25
- package/dist/wu-unblock.d.ts +0 -16
- package/dist/wu-unlock-lane.d.ts +0 -19
- package/dist/wu-validate.d.ts +0 -16
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file onboarding-smoke-test.test.ts
|
|
3
|
+
* Tests for onboarding smoke-test gate (WU-1315)
|
|
4
|
+
*
|
|
5
|
+
* This gate verifies the lumenflow init + wu:create flows work correctly
|
|
6
|
+
* by running them in an isolated temp directory.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import * as os from 'node:os';
|
|
12
|
+
// Import the smoke-test module
|
|
13
|
+
import { runOnboardingSmokeTest, validateInitScripts, validateLaneInferenceFormat, } from '../onboarding-smoke-test.js';
|
|
14
|
+
/** Constants for test files to avoid duplicate string literals */
|
|
15
|
+
const PACKAGE_JSON_FILE = 'package.json';
|
|
16
|
+
const LANE_INFERENCE_FILE = '.lumenflow.lane-inference.yaml';
|
|
17
|
+
const TEST_PROJECT_NAME = 'test-project';
|
|
18
|
+
describe('onboarding smoke-test gate (WU-1315)', () => {
|
|
19
|
+
let tempDir;
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
// Create a temporary directory for each test
|
|
22
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-smoke-test-'));
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
// Clean up temporary directory
|
|
26
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
27
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
describe('runOnboardingSmokeTest', () => {
|
|
31
|
+
it('should return success when all validations pass', async () => {
|
|
32
|
+
// This is an integration test - it runs the full smoke test
|
|
33
|
+
const result = await runOnboardingSmokeTest({ tempDir });
|
|
34
|
+
expect(result.success).toBe(true);
|
|
35
|
+
expect(result.errors).toHaveLength(0);
|
|
36
|
+
});
|
|
37
|
+
it('should clean up temp directory after test', async () => {
|
|
38
|
+
// Run with a specific temp dir
|
|
39
|
+
const testTempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-smoke-cleanup-'));
|
|
40
|
+
await runOnboardingSmokeTest({ tempDir: testTempDir, cleanup: true });
|
|
41
|
+
// Temp dir should be cleaned up
|
|
42
|
+
expect(fs.existsSync(testTempDir)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
it('should preserve temp directory when cleanup is false', async () => {
|
|
45
|
+
const testTempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-smoke-preserve-'));
|
|
46
|
+
try {
|
|
47
|
+
await runOnboardingSmokeTest({ tempDir: testTempDir, cleanup: false });
|
|
48
|
+
// Temp dir should still exist
|
|
49
|
+
expect(fs.existsSync(testTempDir)).toBe(true);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
// Manual cleanup
|
|
53
|
+
if (fs.existsSync(testTempDir)) {
|
|
54
|
+
fs.rmSync(testTempDir, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
it('should report errors when validation fails', async () => {
|
|
59
|
+
// Create a directory without proper package.json scripts (init doesn't create them by default without --full)
|
|
60
|
+
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-smoke-invalid-'));
|
|
61
|
+
try {
|
|
62
|
+
// Create an empty package.json (missing required scripts)
|
|
63
|
+
fs.writeFileSync(path.join(testDir, PACKAGE_JSON_FILE), JSON.stringify({ name: 'test', scripts: {} }, null, 2));
|
|
64
|
+
// Run smoke test - should skip scaffolding and validate existing state
|
|
65
|
+
const result = validateInitScripts({ projectDir: testDir });
|
|
66
|
+
expect(result.valid).toBe(false);
|
|
67
|
+
expect(result.missingScripts.length).toBeGreaterThan(0);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
if (fs.existsSync(testDir)) {
|
|
71
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe('validateInitScripts', () => {
|
|
77
|
+
it('should pass when all required scripts are present', () => {
|
|
78
|
+
// Create package.json with required scripts
|
|
79
|
+
const packageJson = {
|
|
80
|
+
name: TEST_PROJECT_NAME,
|
|
81
|
+
scripts: {
|
|
82
|
+
'wu:claim': 'wu-claim',
|
|
83
|
+
'wu:done': 'wu-done',
|
|
84
|
+
'wu:create': 'wu-create',
|
|
85
|
+
gates: 'gates',
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
fs.writeFileSync(path.join(tempDir, PACKAGE_JSON_FILE), JSON.stringify(packageJson, null, 2));
|
|
89
|
+
const result = validateInitScripts({ projectDir: tempDir });
|
|
90
|
+
expect(result.valid).toBe(true);
|
|
91
|
+
expect(result.missingScripts).toHaveLength(0);
|
|
92
|
+
});
|
|
93
|
+
it('should fail when required scripts are missing', () => {
|
|
94
|
+
// Create package.json without LumenFlow scripts
|
|
95
|
+
const packageJson = {
|
|
96
|
+
name: TEST_PROJECT_NAME,
|
|
97
|
+
scripts: {
|
|
98
|
+
test: 'vitest',
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
fs.writeFileSync(path.join(tempDir, PACKAGE_JSON_FILE), JSON.stringify(packageJson, null, 2));
|
|
102
|
+
const result = validateInitScripts({ projectDir: tempDir });
|
|
103
|
+
expect(result.valid).toBe(false);
|
|
104
|
+
expect(result.missingScripts).toContain('wu:claim');
|
|
105
|
+
expect(result.missingScripts).toContain('wu:done');
|
|
106
|
+
expect(result.missingScripts).toContain('gates');
|
|
107
|
+
});
|
|
108
|
+
it('should fail when package.json does not exist', () => {
|
|
109
|
+
const result = validateInitScripts({ projectDir: tempDir });
|
|
110
|
+
expect(result.valid).toBe(false);
|
|
111
|
+
expect(result.error).toContain('package.json');
|
|
112
|
+
});
|
|
113
|
+
it('should verify scripts use standalone binary format', () => {
|
|
114
|
+
// Scripts should be 'wu-claim' not 'pnpm exec lumenflow wu:claim'
|
|
115
|
+
const packageJson = {
|
|
116
|
+
name: TEST_PROJECT_NAME,
|
|
117
|
+
scripts: {
|
|
118
|
+
'wu:claim': 'pnpm exec lumenflow wu:claim', // Wrong format
|
|
119
|
+
'wu:done': 'wu-done',
|
|
120
|
+
'wu:create': 'wu-create',
|
|
121
|
+
gates: 'gates',
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
fs.writeFileSync(path.join(tempDir, PACKAGE_JSON_FILE), JSON.stringify(packageJson, null, 2));
|
|
125
|
+
const result = validateInitScripts({ projectDir: tempDir });
|
|
126
|
+
expect(result.valid).toBe(false);
|
|
127
|
+
expect(result.invalidScripts).toContain('wu:claim');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe('validateLaneInferenceFormat', () => {
|
|
131
|
+
it('should pass when lane-inference.yaml has correct hierarchical format', () => {
|
|
132
|
+
// Create lane-inference.yaml with correct format
|
|
133
|
+
const laneInference = `# Lane Inference Configuration
|
|
134
|
+
Framework:
|
|
135
|
+
Core:
|
|
136
|
+
description: 'Core library'
|
|
137
|
+
code_paths:
|
|
138
|
+
- 'packages/core/**'
|
|
139
|
+
keywords:
|
|
140
|
+
- 'core'
|
|
141
|
+
|
|
142
|
+
Content:
|
|
143
|
+
Documentation:
|
|
144
|
+
description: 'Documentation'
|
|
145
|
+
code_paths:
|
|
146
|
+
- 'docs/**'
|
|
147
|
+
keywords:
|
|
148
|
+
- 'docs'
|
|
149
|
+
`;
|
|
150
|
+
fs.writeFileSync(path.join(tempDir, LANE_INFERENCE_FILE), laneInference);
|
|
151
|
+
const result = validateLaneInferenceFormat({ projectDir: tempDir });
|
|
152
|
+
expect(result.valid).toBe(true);
|
|
153
|
+
expect(result.errors).toHaveLength(0);
|
|
154
|
+
});
|
|
155
|
+
it('should fail when lane-inference.yaml uses flat lanes array', () => {
|
|
156
|
+
// Create lane-inference.yaml with old flat format
|
|
157
|
+
const laneInference = `# Lane Inference Configuration
|
|
158
|
+
lanes:
|
|
159
|
+
- name: Framework
|
|
160
|
+
code_paths:
|
|
161
|
+
- 'packages/**'
|
|
162
|
+
`;
|
|
163
|
+
fs.writeFileSync(path.join(tempDir, LANE_INFERENCE_FILE), laneInference);
|
|
164
|
+
const result = validateLaneInferenceFormat({ projectDir: tempDir });
|
|
165
|
+
expect(result.valid).toBe(false);
|
|
166
|
+
expect(result.errors.some((e) => e.includes('lanes'))).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
it('should fail when lane-inference.yaml does not exist', () => {
|
|
169
|
+
const result = validateLaneInferenceFormat({ projectDir: tempDir });
|
|
170
|
+
expect(result.valid).toBe(false);
|
|
171
|
+
expect(result.error).toContain('.lumenflow.lane-inference.yaml');
|
|
172
|
+
});
|
|
173
|
+
it('should validate parent lane names are capitalized', () => {
|
|
174
|
+
const laneInference = `# Lane Inference Configuration
|
|
175
|
+
framework: # Should be 'Framework'
|
|
176
|
+
core:
|
|
177
|
+
description: 'Core library'
|
|
178
|
+
code_paths:
|
|
179
|
+
- 'packages/core/**'
|
|
180
|
+
`;
|
|
181
|
+
fs.writeFileSync(path.join(tempDir, LANE_INFERENCE_FILE), laneInference);
|
|
182
|
+
const result = validateLaneInferenceFormat({ projectDir: tempDir });
|
|
183
|
+
expect(result.valid).toBe(false);
|
|
184
|
+
expect(result.errors.some((e) => e.includes('capitalized'))).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
it('should validate sub-lanes have required fields', () => {
|
|
187
|
+
const laneInference = `# Lane Inference Configuration
|
|
188
|
+
Framework:
|
|
189
|
+
Core:
|
|
190
|
+
# Missing description and code_paths
|
|
191
|
+
keywords:
|
|
192
|
+
- 'core'
|
|
193
|
+
`;
|
|
194
|
+
fs.writeFileSync(path.join(tempDir, LANE_INFERENCE_FILE), laneInference);
|
|
195
|
+
const result = validateLaneInferenceFormat({ projectDir: tempDir });
|
|
196
|
+
expect(result.valid).toBe(false);
|
|
197
|
+
expect(result.errors.some((e) => e.includes('description') || e.includes('code_paths'))).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
describe('wu:create with requireRemote=false', () => {
|
|
201
|
+
it('should run smoke test with requireRemote=false config', async () => {
|
|
202
|
+
const result = await runOnboardingSmokeTest({
|
|
203
|
+
tempDir,
|
|
204
|
+
skipWuCreate: false,
|
|
205
|
+
});
|
|
206
|
+
// The test should have validated wu:create works without a remote
|
|
207
|
+
expect(result.wuCreateValidation).toBeDefined();
|
|
208
|
+
expect(result.wuCreateValidation?.success).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for WU-1311: CLI path centralization
|
|
3
|
+
*
|
|
4
|
+
* Tests that CLI commands use WU_PATHS/getResolvedPaths/getConfig
|
|
5
|
+
* instead of hardcoded 'docs/04-operations' paths.
|
|
6
|
+
*
|
|
7
|
+
* WU-1311 Acceptance Criteria:
|
|
8
|
+
* - No hardcoded docs/04-operations paths remain in CLI commands (use WU_PATHS/getResolvedPaths)
|
|
9
|
+
* - state-doctor warns when configured paths are missing
|
|
10
|
+
* - Config overrides are respected across wu-* commands and diagnostics
|
|
11
|
+
* - Unit tests cover CLI path usage and warnings
|
|
12
|
+
*
|
|
13
|
+
* @module __tests__/path-centralization-cli.test
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
16
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
17
|
+
import { tmpdir } from 'node:os';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import * as yaml from 'yaml';
|
|
20
|
+
import { clearConfigCache, getConfig } from '@lumenflow/core/dist/lumenflow-config.js';
|
|
21
|
+
import { WU_PATHS, createWuPaths } from '@lumenflow/core/dist/wu-paths.js';
|
|
22
|
+
/** Config file name constant */
|
|
23
|
+
const CONFIG_FILE = '.lumenflow.config.yaml';
|
|
24
|
+
describe('WU-1311: CLI path centralization', () => {
|
|
25
|
+
let tempDir;
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
tempDir = await mkdtemp(path.join(tmpdir(), 'cli-path-test-'));
|
|
28
|
+
clearConfigCache();
|
|
29
|
+
});
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
32
|
+
clearConfigCache();
|
|
33
|
+
});
|
|
34
|
+
describe('AC1: No hardcoded docs/04-operations paths in CLI commands', () => {
|
|
35
|
+
it('should use config-based paths for WU file path generation', () => {
|
|
36
|
+
const paths = createWuPaths({ projectRoot: tempDir });
|
|
37
|
+
// Paths should be from config, not hardcoded
|
|
38
|
+
expect(paths.WU('WU-1311')).toBe('docs/04-operations/tasks/wu/WU-1311.yaml');
|
|
39
|
+
expect(paths.STATUS()).toBe('docs/04-operations/tasks/status.md');
|
|
40
|
+
expect(paths.BACKLOG()).toBe('docs/04-operations/tasks/backlog.md');
|
|
41
|
+
});
|
|
42
|
+
it('should respect custom config paths for WU operations', async () => {
|
|
43
|
+
const customConfig = {
|
|
44
|
+
version: '1.0.0',
|
|
45
|
+
directories: {
|
|
46
|
+
wuDir: 'custom/wu',
|
|
47
|
+
backlogPath: 'custom/backlog.md',
|
|
48
|
+
statusPath: 'custom/status.md',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
await writeFile(path.join(tempDir, CONFIG_FILE), yaml.stringify(customConfig), 'utf-8');
|
|
52
|
+
clearConfigCache();
|
|
53
|
+
const paths = createWuPaths({ projectRoot: tempDir });
|
|
54
|
+
expect(paths.WU('WU-1311')).toBe('custom/wu/WU-1311.yaml');
|
|
55
|
+
expect(paths.STATUS()).toBe('custom/status.md');
|
|
56
|
+
expect(paths.BACKLOG()).toBe('custom/backlog.md');
|
|
57
|
+
});
|
|
58
|
+
it('should use WU_PATHS for stamp file generation', () => {
|
|
59
|
+
const stampPath = WU_PATHS.STAMP('WU-1311');
|
|
60
|
+
expect(stampPath).toContain('WU-1311.done');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('AC2: state-doctor warns when configured paths are missing', () => {
|
|
64
|
+
it('should detect missing WU directory', async () => {
|
|
65
|
+
// Create a config pointing to non-existent paths
|
|
66
|
+
const customConfig = {
|
|
67
|
+
version: '1.0.0',
|
|
68
|
+
directories: {
|
|
69
|
+
wuDir: 'nonexistent/wu',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
await writeFile(path.join(tempDir, CONFIG_FILE), yaml.stringify(customConfig), 'utf-8');
|
|
73
|
+
clearConfigCache();
|
|
74
|
+
// getResolvedPaths returns paths even if they don't exist
|
|
75
|
+
// state-doctor should check existence and warn
|
|
76
|
+
const { getResolvedPaths } = await import('@lumenflow/core/dist/lumenflow-config.js');
|
|
77
|
+
const paths = getResolvedPaths({ projectRoot: tempDir });
|
|
78
|
+
expect(paths.wuDir).toBe(path.join(tempDir, 'nonexistent/wu'));
|
|
79
|
+
// The warnMissingPaths function in state-doctor.ts checks existsSync
|
|
80
|
+
// This verifies the path is properly resolved for checking
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe('AC3: Config overrides are respected across wu-* commands', () => {
|
|
84
|
+
it('should use custom initiatives directory from config', async () => {
|
|
85
|
+
const customConfig = {
|
|
86
|
+
version: '1.0.0',
|
|
87
|
+
directories: {
|
|
88
|
+
initiativesDir: 'custom/initiatives',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
await writeFile(path.join(tempDir, CONFIG_FILE), yaml.stringify(customConfig), 'utf-8');
|
|
92
|
+
clearConfigCache();
|
|
93
|
+
const paths = createWuPaths({ projectRoot: tempDir });
|
|
94
|
+
expect(paths.INITIATIVES_DIR()).toBe('custom/initiatives');
|
|
95
|
+
});
|
|
96
|
+
it('should use custom plans directory from config', async () => {
|
|
97
|
+
const customConfig = {
|
|
98
|
+
version: '1.0.0',
|
|
99
|
+
directories: {
|
|
100
|
+
plansDir: 'custom/plans',
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
await writeFile(path.join(tempDir, CONFIG_FILE), yaml.stringify(customConfig), 'utf-8');
|
|
104
|
+
clearConfigCache();
|
|
105
|
+
const paths = createWuPaths({ projectRoot: tempDir });
|
|
106
|
+
expect(paths.PLANS_DIR()).toBe('custom/plans');
|
|
107
|
+
});
|
|
108
|
+
it('should use custom templates directory from config', async () => {
|
|
109
|
+
const customConfig = {
|
|
110
|
+
version: '1.0.0',
|
|
111
|
+
directories: {
|
|
112
|
+
templatesDir: 'custom/templates',
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
await writeFile(path.join(tempDir, CONFIG_FILE), yaml.stringify(customConfig), 'utf-8');
|
|
116
|
+
clearConfigCache();
|
|
117
|
+
const paths = createWuPaths({ projectRoot: tempDir });
|
|
118
|
+
expect(paths.TEMPLATES_DIR()).toBe('custom/templates');
|
|
119
|
+
});
|
|
120
|
+
it('should use custom onboarding directory from config', async () => {
|
|
121
|
+
const customConfig = {
|
|
122
|
+
version: '1.0.0',
|
|
123
|
+
directories: {
|
|
124
|
+
onboardingDir: 'custom/onboarding',
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
await writeFile(path.join(tempDir, CONFIG_FILE), yaml.stringify(customConfig), 'utf-8');
|
|
128
|
+
clearConfigCache();
|
|
129
|
+
const paths = createWuPaths({ projectRoot: tempDir });
|
|
130
|
+
expect(paths.ONBOARDING_DIR()).toBe('custom/onboarding');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe('AC4: Whitelist paths use config-based values', () => {
|
|
134
|
+
it('should generate correct whitelist paths for wu:done staged file validation', async () => {
|
|
135
|
+
const customConfig = {
|
|
136
|
+
version: '1.0.0',
|
|
137
|
+
directories: {
|
|
138
|
+
wuDir: 'tasks/wu',
|
|
139
|
+
backlogPath: 'tasks/backlog.md',
|
|
140
|
+
statusPath: 'tasks/status.md',
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
await writeFile(path.join(tempDir, CONFIG_FILE), yaml.stringify(customConfig), 'utf-8');
|
|
144
|
+
clearConfigCache();
|
|
145
|
+
const config = getConfig({ projectRoot: tempDir });
|
|
146
|
+
const wuId = 'WU-1311';
|
|
147
|
+
// The whitelist should use config paths, not hardcoded ones
|
|
148
|
+
const expectedWuPath = path.join(config.directories.wuDir, `${wuId}.yaml`);
|
|
149
|
+
const expectedBacklogPath = config.directories.backlogPath;
|
|
150
|
+
const expectedStatusPath = config.directories.statusPath;
|
|
151
|
+
expect(expectedWuPath).toBe('tasks/wu/WU-1311.yaml');
|
|
152
|
+
expect(expectedBacklogPath).toBe('tasks/backlog.md');
|
|
153
|
+
expect(expectedStatusPath).toBe('tasks/status.md');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
describe('AC5: Error messages use config-based paths', () => {
|
|
157
|
+
it('should provide config-aware error messages for missing WU', async () => {
|
|
158
|
+
const customConfig = {
|
|
159
|
+
version: '1.0.0',
|
|
160
|
+
directories: {
|
|
161
|
+
wuDir: 'custom/wu',
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
await writeFile(path.join(tempDir, CONFIG_FILE), yaml.stringify(customConfig), 'utf-8');
|
|
165
|
+
clearConfigCache();
|
|
166
|
+
const config = getConfig({ projectRoot: tempDir });
|
|
167
|
+
// Error messages should reference the configured path, not hardcoded
|
|
168
|
+
const expectedDir = config.directories.wuDir;
|
|
169
|
+
expect(expectedDir).toBe('custom/wu');
|
|
170
|
+
// CLI commands should use this in error messages like:
|
|
171
|
+
// "WU not found in ${config.directories.wuDir}/"
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
describe('WU-1311: validateStagedFiles whitelist paths', () => {
|
|
176
|
+
let tempDir;
|
|
177
|
+
beforeEach(async () => {
|
|
178
|
+
tempDir = await mkdtemp(path.join(tmpdir(), 'whitelist-test-'));
|
|
179
|
+
clearConfigCache();
|
|
180
|
+
});
|
|
181
|
+
afterEach(async () => {
|
|
182
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
183
|
+
clearConfigCache();
|
|
184
|
+
});
|
|
185
|
+
it('should generate whitelist paths from config', async () => {
|
|
186
|
+
const customConfig = {
|
|
187
|
+
version: '1.0.0',
|
|
188
|
+
directories: {
|
|
189
|
+
wuDir: 'my/tasks/wu',
|
|
190
|
+
backlogPath: 'my/tasks/backlog.md',
|
|
191
|
+
statusPath: 'my/tasks/status.md',
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
await writeFile(path.join(tempDir, CONFIG_FILE), yaml.stringify(customConfig), 'utf-8');
|
|
195
|
+
clearConfigCache();
|
|
196
|
+
const config = getConfig({ projectRoot: tempDir });
|
|
197
|
+
// Helper function that CLI should use to generate whitelist
|
|
198
|
+
const generateWhitelist = (id) => [
|
|
199
|
+
path.join(config.directories.wuDir, `${id}.yaml`),
|
|
200
|
+
config.directories.statusPath,
|
|
201
|
+
config.directories.backlogPath,
|
|
202
|
+
];
|
|
203
|
+
const whitelist = generateWhitelist('WU-1311');
|
|
204
|
+
expect(whitelist).toContain('my/tasks/wu/WU-1311.yaml');
|
|
205
|
+
expect(whitelist).toContain('my/tasks/backlog.md');
|
|
206
|
+
expect(whitelist).toContain('my/tasks/status.md');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
describe('WU-1311: getWorktreeCommitFiles config paths', () => {
|
|
210
|
+
let tempDir;
|
|
211
|
+
beforeEach(async () => {
|
|
212
|
+
tempDir = await mkdtemp(path.join(tmpdir(), 'commit-files-test-'));
|
|
213
|
+
clearConfigCache();
|
|
214
|
+
});
|
|
215
|
+
afterEach(async () => {
|
|
216
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
217
|
+
clearConfigCache();
|
|
218
|
+
});
|
|
219
|
+
it('should use config-based WU path in commit file list', async () => {
|
|
220
|
+
const customConfig = {
|
|
221
|
+
version: '1.0.0',
|
|
222
|
+
directories: {
|
|
223
|
+
wuDir: 'custom/wu',
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
await writeFile(path.join(tempDir, CONFIG_FILE), yaml.stringify(customConfig), 'utf-8');
|
|
227
|
+
clearConfigCache();
|
|
228
|
+
const config = getConfig({ projectRoot: tempDir });
|
|
229
|
+
// The getWorktreeCommitFiles function should use config paths
|
|
230
|
+
const wuId = 'WU-1311';
|
|
231
|
+
const expectedWuPath = path.join(config.directories.wuDir, `${wuId}.yaml`);
|
|
232
|
+
expect(expectedWuPath).toBe('custom/wu/WU-1311.yaml');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for plan:create command (WU-1313)
|
|
3
|
+
*
|
|
4
|
+
* The plan:create command creates plan files in the repo-native plansDir.
|
|
5
|
+
* Plans can be linked to WUs (via spec_refs) or initiatives (via related_plan).
|
|
6
|
+
*
|
|
7
|
+
* TDD: These tests are written BEFORE the implementation.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, vi, beforeEach, afterEach } 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
|
+
// Mock modules before importing
|
|
14
|
+
vi.mock('@lumenflow/core/dist/git-adapter.js', () => ({
|
|
15
|
+
getGitForCwd: vi.fn(() => ({
|
|
16
|
+
branch: vi.fn().mockResolvedValue({ current: 'main' }),
|
|
17
|
+
status: vi.fn().mockResolvedValue({ isClean: () => true }),
|
|
18
|
+
})),
|
|
19
|
+
}));
|
|
20
|
+
vi.mock('@lumenflow/core/dist/wu-helpers.js', () => ({
|
|
21
|
+
ensureOnMain: vi.fn().mockResolvedValue(undefined),
|
|
22
|
+
}));
|
|
23
|
+
vi.mock('@lumenflow/core/dist/micro-worktree.js', () => ({
|
|
24
|
+
withMicroWorktree: vi.fn(async ({ execute }) => {
|
|
25
|
+
const tempDir = join(tmpdir(), `plan-create-test-${Date.now()}`);
|
|
26
|
+
mkdirSync(tempDir, { recursive: true });
|
|
27
|
+
return execute({ worktreePath: tempDir });
|
|
28
|
+
}),
|
|
29
|
+
}));
|
|
30
|
+
describe('plan:create command', () => {
|
|
31
|
+
let tempDir;
|
|
32
|
+
let originalCwd;
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
tempDir = join(tmpdir(), `plan-create-test-${Date.now()}`);
|
|
35
|
+
mkdirSync(tempDir, { recursive: true });
|
|
36
|
+
originalCwd = process.cwd();
|
|
37
|
+
});
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
process.chdir(originalCwd);
|
|
40
|
+
if (existsSync(tempDir)) {
|
|
41
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
vi.clearAllMocks();
|
|
44
|
+
});
|
|
45
|
+
describe('createPlan', () => {
|
|
46
|
+
it('should create a plan file in repo plansDir', async () => {
|
|
47
|
+
const { createPlan } = await import('../plan-create.js');
|
|
48
|
+
const plansDir = join(tempDir, 'docs', '04-operations', 'plans');
|
|
49
|
+
mkdirSync(plansDir, { recursive: true });
|
|
50
|
+
const planPath = createPlan(tempDir, 'WU-1313', 'Implement plan tooling');
|
|
51
|
+
expect(existsSync(planPath)).toBe(true);
|
|
52
|
+
const content = readFileSync(planPath, 'utf-8');
|
|
53
|
+
expect(content).toContain('# WU-1313');
|
|
54
|
+
expect(content).toContain('Implement plan tooling');
|
|
55
|
+
expect(content).toContain('## Goal');
|
|
56
|
+
expect(content).toContain('## Scope');
|
|
57
|
+
expect(content).toContain('## Approach');
|
|
58
|
+
});
|
|
59
|
+
it('should create plans directory if it does not exist', async () => {
|
|
60
|
+
const { createPlan } = await import('../plan-create.js');
|
|
61
|
+
// Do NOT pre-create the plans directory
|
|
62
|
+
const planPath = createPlan(tempDir, 'WU-1313', 'Test Plan');
|
|
63
|
+
expect(existsSync(planPath)).toBe(true);
|
|
64
|
+
expect(planPath).toContain('docs/04-operations/plans');
|
|
65
|
+
});
|
|
66
|
+
it('should not overwrite existing plan file', async () => {
|
|
67
|
+
const { createPlan } = await import('../plan-create.js');
|
|
68
|
+
const plansDir = join(tempDir, 'docs', '04-operations', 'plans');
|
|
69
|
+
mkdirSync(plansDir, { recursive: true });
|
|
70
|
+
// Create existing file
|
|
71
|
+
const existingPath = join(plansDir, 'WU-1313-plan.md');
|
|
72
|
+
writeFileSync(existingPath, '# Existing Content');
|
|
73
|
+
expect(() => createPlan(tempDir, 'WU-1313', 'New Title')).toThrow();
|
|
74
|
+
});
|
|
75
|
+
it('should support initiative ID format', async () => {
|
|
76
|
+
const { createPlan } = await import('../plan-create.js');
|
|
77
|
+
const planPath = createPlan(tempDir, 'INIT-001', 'Initiative Plan');
|
|
78
|
+
expect(existsSync(planPath)).toBe(true);
|
|
79
|
+
const content = readFileSync(planPath, 'utf-8');
|
|
80
|
+
expect(content).toContain('# INIT-001');
|
|
81
|
+
expect(content).toContain('Initiative Plan');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe('getPlanUri', () => {
|
|
85
|
+
it('should return lumenflow:// URI for plan', async () => {
|
|
86
|
+
const { getPlanUri } = await import('../plan-create.js');
|
|
87
|
+
expect(getPlanUri('WU-1313')).toBe('lumenflow://plans/WU-1313-plan.md');
|
|
88
|
+
expect(getPlanUri('INIT-001')).toBe('lumenflow://plans/INIT-001-plan.md');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe('validatePlanId', () => {
|
|
92
|
+
it('should accept valid WU and INIT IDs', async () => {
|
|
93
|
+
const { validatePlanId } = await import('../plan-create.js');
|
|
94
|
+
expect(() => validatePlanId('WU-1313')).not.toThrow();
|
|
95
|
+
expect(() => validatePlanId('INIT-001')).not.toThrow();
|
|
96
|
+
expect(() => validatePlanId('INIT-TOOLING')).not.toThrow();
|
|
97
|
+
});
|
|
98
|
+
it('should reject invalid IDs', async () => {
|
|
99
|
+
const { validatePlanId } = await import('../plan-create.js');
|
|
100
|
+
expect(() => validatePlanId('invalid')).toThrow();
|
|
101
|
+
expect(() => validatePlanId('')).toThrow();
|
|
102
|
+
expect(() => validatePlanId('WU1313')).toThrow();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
describe('getCommitMessage', () => {
|
|
106
|
+
it('should generate correct commit message', async () => {
|
|
107
|
+
const { getCommitMessage } = await import('../plan-create.js');
|
|
108
|
+
expect(getCommitMessage('WU-1313', 'Feature Plan')).toBe('docs: create plan for wu-1313 - Feature Plan');
|
|
109
|
+
expect(getCommitMessage('INIT-001', 'Auth System')).toBe('docs: create plan for init-001 - Auth System');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe('plan:create CLI exports', () => {
|
|
114
|
+
it('should export main function for CLI entry', async () => {
|
|
115
|
+
const planCreate = await import('../plan-create.js');
|
|
116
|
+
expect(typeof planCreate.main).toBe('function');
|
|
117
|
+
});
|
|
118
|
+
it('should export all required functions', async () => {
|
|
119
|
+
const planCreate = await import('../plan-create.js');
|
|
120
|
+
expect(typeof planCreate.createPlan).toBe('function');
|
|
121
|
+
expect(typeof planCreate.getPlanUri).toBe('function');
|
|
122
|
+
expect(typeof planCreate.validatePlanId).toBe('function');
|
|
123
|
+
expect(typeof planCreate.getCommitMessage).toBe('function');
|
|
124
|
+
expect(typeof planCreate.LOG_PREFIX).toBe('string');
|
|
125
|
+
});
|
|
126
|
+
});
|