@lumenflow/cli 2.7.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 +121 -105
- package/dist/__tests__/agent-spawn-coordination.test.js +451 -0
- package/dist/__tests__/commands/integrate.test.js +165 -0
- package/dist/__tests__/commands.test.js +75 -0
- package/dist/__tests__/doctor.test.js +510 -0
- package/dist/__tests__/gates-config.test.js +0 -1
- package/dist/__tests__/hooks/enforcement.test.js +279 -0
- package/dist/__tests__/init-greenfield.test.js +247 -0
- package/dist/__tests__/init-quick-ref.test.js +0 -1
- package/dist/__tests__/init-template-portability.test.js +0 -1
- package/dist/__tests__/init.test.js +249 -0
- package/dist/__tests__/initiative-e2e.test.js +442 -0
- package/dist/__tests__/initiative-plan-replacement.test.js +0 -1
- package/dist/__tests__/memory-integration.test.js +333 -0
- package/dist/__tests__/release.test.js +1 -1
- package/dist/__tests__/safe-git.test.js +0 -1
- package/dist/__tests__/state-doctor.test.js +54 -0
- package/dist/__tests__/sync-templates.test.js +255 -0
- package/dist/__tests__/wu-create-required-fields.test.js +121 -0
- package/dist/__tests__/wu-done-auto-cleanup.test.js +135 -0
- package/dist/__tests__/wu-lifecycle-integration.test.js +388 -0
- package/dist/backlog-prune.js +0 -1
- package/dist/cli-entry-point.js +0 -1
- package/dist/commands/integrate.js +229 -0
- package/dist/commands.js +171 -0
- package/dist/docs-sync.js +46 -0
- package/dist/doctor.js +479 -10
- package/dist/gates.js +0 -7
- package/dist/hooks/enforcement-checks.js +209 -0
- package/dist/hooks/enforcement-generator.js +365 -0
- package/dist/hooks/enforcement-sync.js +243 -0
- package/dist/hooks/index.js +7 -0
- package/dist/init.js +502 -17
- package/dist/initiative-add-wu.js +0 -2
- package/dist/initiative-create.js +0 -3
- package/dist/initiative-edit.js +0 -5
- package/dist/initiative-plan.js +0 -1
- package/dist/initiative-remove-wu.js +0 -2
- package/dist/lane-health.js +0 -2
- package/dist/lane-suggest.js +0 -1
- package/dist/mem-checkpoint.js +0 -2
- package/dist/mem-cleanup.js +0 -2
- package/dist/mem-context.js +0 -3
- package/dist/mem-create.js +0 -2
- package/dist/mem-delete.js +0 -3
- package/dist/mem-inbox.js +0 -2
- package/dist/mem-index.js +0 -1
- package/dist/mem-init.js +0 -2
- package/dist/mem-profile.js +0 -1
- package/dist/mem-promote.js +0 -1
- package/dist/mem-ready.js +0 -2
- package/dist/mem-signal.js +0 -2
- package/dist/mem-start.js +0 -2
- package/dist/mem-summarize.js +0 -2
- package/dist/metrics-cli.js +1 -1
- package/dist/metrics-snapshot.js +1 -1
- package/dist/onboarding-smoke-test.js +0 -5
- package/dist/orchestrate-init-status.js +0 -1
- package/dist/orchestrate-initiative.js +0 -1
- package/dist/orchestrate-monitor.js +0 -1
- package/dist/plan-create.js +0 -2
- package/dist/plan-edit.js +0 -2
- package/dist/plan-link.js +0 -2
- package/dist/plan-promote.js +0 -2
- package/dist/signal-cleanup.js +0 -4
- package/dist/state-bootstrap.js +0 -1
- package/dist/state-cleanup.js +0 -4
- package/dist/state-doctor-fix.js +5 -8
- package/dist/state-doctor.js +0 -11
- package/dist/sync-templates.js +188 -34
- package/dist/wu-block.js +100 -48
- package/dist/wu-claim.js +1 -22
- package/dist/wu-cleanup.js +0 -1
- package/dist/wu-create.js +0 -2
- package/dist/wu-done-auto-cleanup.js +139 -0
- package/dist/wu-done.js +11 -4
- package/dist/wu-edit.js +0 -12
- package/dist/wu-preflight.js +0 -1
- package/dist/wu-prep.js +0 -1
- package/dist/wu-proto.js +0 -1
- package/dist/wu-spawn.js +0 -3
- package/dist/wu-unblock.js +0 -2
- package/dist/wu-validate.js +0 -1
- package/package.json +9 -7
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file wu-create-required-fields.test.ts
|
|
3
|
+
* Test suite for wu:create required field aggregation (WU-1366)
|
|
4
|
+
*
|
|
5
|
+
* WU-1366: Missing required fields in wu:create should be reported together
|
|
6
|
+
* in a single error block.
|
|
7
|
+
*
|
|
8
|
+
* Tests:
|
|
9
|
+
* - Multiple missing required fields are collected and reported together
|
|
10
|
+
* - Error block formatting includes all missing fields
|
|
11
|
+
* - Single missing field still reports correctly
|
|
12
|
+
*/
|
|
13
|
+
import { describe, it, expect } from 'vitest';
|
|
14
|
+
import { validateCreateSpec } from '../wu-create.js';
|
|
15
|
+
/** Default lane for test cases */
|
|
16
|
+
const TEST_LANE = 'Framework: CLI';
|
|
17
|
+
/** Default test WU ID */
|
|
18
|
+
const TEST_WU_ID = 'WU-9999';
|
|
19
|
+
/** Minimum valid description with required sections */
|
|
20
|
+
const VALID_DESCRIPTION = 'Context: test context.\nProblem: test problem.\nSolution: test solution that exceeds minimum.';
|
|
21
|
+
/** Default acceptance criteria for tests */
|
|
22
|
+
const TEST_ACCEPTANCE = ['Acceptance criterion'];
|
|
23
|
+
describe('wu:create required field aggregation (WU-1366)', () => {
|
|
24
|
+
describe('validateCreateSpec error aggregation', () => {
|
|
25
|
+
it('should aggregate multiple missing required fields into a single error block', () => {
|
|
26
|
+
// Missing: description, acceptance, exposure, code-paths, test-paths
|
|
27
|
+
const result = validateCreateSpec({
|
|
28
|
+
id: TEST_WU_ID,
|
|
29
|
+
lane: TEST_LANE,
|
|
30
|
+
title: 'Test WU',
|
|
31
|
+
priority: 'P2',
|
|
32
|
+
type: 'feature',
|
|
33
|
+
opts: {
|
|
34
|
+
// All required fields missing
|
|
35
|
+
strict: false, // Skip path existence checks
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
expect(result.valid).toBe(false);
|
|
39
|
+
// Should have multiple errors collected
|
|
40
|
+
expect(result.errors.length).toBeGreaterThan(1);
|
|
41
|
+
// Verify specific missing fields are included
|
|
42
|
+
expect(result.errors.some((e) => e.includes('--description'))).toBe(true);
|
|
43
|
+
expect(result.errors.some((e) => e.includes('--acceptance'))).toBe(true);
|
|
44
|
+
expect(result.errors.some((e) => e.includes('--exposure'))).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
it('should report code-paths and test-paths as missing for non-documentation WUs', () => {
|
|
47
|
+
const result = validateCreateSpec({
|
|
48
|
+
id: TEST_WU_ID,
|
|
49
|
+
lane: TEST_LANE,
|
|
50
|
+
title: 'Test WU',
|
|
51
|
+
priority: 'P2',
|
|
52
|
+
type: 'feature',
|
|
53
|
+
opts: {
|
|
54
|
+
description: VALID_DESCRIPTION,
|
|
55
|
+
acceptance: TEST_ACCEPTANCE,
|
|
56
|
+
exposure: 'backend-only',
|
|
57
|
+
// Missing: codePaths, testPaths, specRefs
|
|
58
|
+
strict: false,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
expect(result.valid).toBe(false);
|
|
62
|
+
expect(result.errors.some((e) => e.includes('--code-paths'))).toBe(true);
|
|
63
|
+
expect(result.errors.some((e) => e.includes('test path'))).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
it('should report spec-refs as missing for feature WUs', () => {
|
|
66
|
+
const result = validateCreateSpec({
|
|
67
|
+
id: TEST_WU_ID,
|
|
68
|
+
lane: TEST_LANE,
|
|
69
|
+
title: 'Test WU',
|
|
70
|
+
priority: 'P2',
|
|
71
|
+
type: 'feature',
|
|
72
|
+
opts: {
|
|
73
|
+
description: VALID_DESCRIPTION,
|
|
74
|
+
acceptance: TEST_ACCEPTANCE,
|
|
75
|
+
exposure: 'backend-only',
|
|
76
|
+
codePaths: ['packages/@lumenflow/cli/src/wu-create.ts'],
|
|
77
|
+
testPathsUnit: ['packages/@lumenflow/cli/src/__tests__/wu-create.test.ts'],
|
|
78
|
+
// Missing: specRefs (required for feature type)
|
|
79
|
+
strict: false,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
expect(result.valid).toBe(false);
|
|
83
|
+
expect(result.errors.some((e) => e.includes('--spec-refs'))).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
it('should return all errors at once, not fail on first error', () => {
|
|
86
|
+
const result = validateCreateSpec({
|
|
87
|
+
id: TEST_WU_ID,
|
|
88
|
+
lane: TEST_LANE,
|
|
89
|
+
title: 'Test WU',
|
|
90
|
+
priority: 'P2',
|
|
91
|
+
type: 'feature',
|
|
92
|
+
opts: {
|
|
93
|
+
// All required fields missing to ensure multiple errors
|
|
94
|
+
strict: false,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
expect(result.valid).toBe(false);
|
|
98
|
+
// Verify multiple errors are present (at least 3: description, acceptance, exposure)
|
|
99
|
+
expect(result.errors.length).toBeGreaterThanOrEqual(3);
|
|
100
|
+
});
|
|
101
|
+
it('should validate documentation WUs without requiring code-paths', () => {
|
|
102
|
+
const result = validateCreateSpec({
|
|
103
|
+
id: TEST_WU_ID,
|
|
104
|
+
lane: 'Content: Documentation',
|
|
105
|
+
title: 'Test Docs WU',
|
|
106
|
+
priority: 'P2',
|
|
107
|
+
type: 'documentation',
|
|
108
|
+
opts: {
|
|
109
|
+
description: VALID_DESCRIPTION,
|
|
110
|
+
acceptance: TEST_ACCEPTANCE,
|
|
111
|
+
exposure: 'documentation',
|
|
112
|
+
testPathsManual: ['Verify docs render correctly'],
|
|
113
|
+
strict: false,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
// Documentation WUs should not require code-paths
|
|
117
|
+
const hasCodePathsError = result.errors.some((e) => e.includes('--code-paths'));
|
|
118
|
+
expect(hasCodePathsError).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file wu-done-auto-cleanup.test.ts
|
|
3
|
+
* Test suite for wu:done auto cleanup on success (WU-1366)
|
|
4
|
+
*
|
|
5
|
+
* WU-1366: State cleanup runs automatically after wu:done success (non-fatal)
|
|
6
|
+
*
|
|
7
|
+
* Tests:
|
|
8
|
+
* - shouldRunAutoCleanup respects config.cleanup.trigger setting
|
|
9
|
+
* - runAutoCleanupAfterDone is non-fatal (logs errors but doesn't throw)
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
12
|
+
/** Common mock path for lumenflow config module */
|
|
13
|
+
const CONFIG_MODULE_PATH = '@lumenflow/core/dist/lumenflow-config.js';
|
|
14
|
+
// Test the exported functions directly with minimal mocking
|
|
15
|
+
describe('wu:done auto cleanup (WU-1366)', () => {
|
|
16
|
+
let consoleLogSpy;
|
|
17
|
+
let consoleWarnSpy;
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
20
|
+
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
21
|
+
});
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
consoleLogSpy.mockRestore();
|
|
24
|
+
consoleWarnSpy.mockRestore();
|
|
25
|
+
vi.resetModules();
|
|
26
|
+
});
|
|
27
|
+
describe('shouldRunAutoCleanup', () => {
|
|
28
|
+
it('should return true when config.cleanup.trigger is on_done', async () => {
|
|
29
|
+
// Mock getConfig to return on_done trigger
|
|
30
|
+
vi.doMock(CONFIG_MODULE_PATH, () => ({
|
|
31
|
+
getConfig: vi.fn().mockReturnValue({
|
|
32
|
+
cleanup: { trigger: 'on_done' },
|
|
33
|
+
}),
|
|
34
|
+
}));
|
|
35
|
+
const { shouldRunAutoCleanup } = await import('../wu-done-auto-cleanup.js');
|
|
36
|
+
const result = shouldRunAutoCleanup();
|
|
37
|
+
expect(result).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
it('should return false when config.cleanup.trigger is manual', async () => {
|
|
40
|
+
vi.doMock(CONFIG_MODULE_PATH, () => ({
|
|
41
|
+
getConfig: vi.fn().mockReturnValue({
|
|
42
|
+
cleanup: { trigger: 'manual' },
|
|
43
|
+
}),
|
|
44
|
+
}));
|
|
45
|
+
const { shouldRunAutoCleanup } = await import('../wu-done-auto-cleanup.js');
|
|
46
|
+
const result = shouldRunAutoCleanup();
|
|
47
|
+
expect(result).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
it('should return false when config.cleanup.trigger is on_init', async () => {
|
|
50
|
+
vi.doMock(CONFIG_MODULE_PATH, () => ({
|
|
51
|
+
getConfig: vi.fn().mockReturnValue({
|
|
52
|
+
cleanup: { trigger: 'on_init' },
|
|
53
|
+
}),
|
|
54
|
+
}));
|
|
55
|
+
const { shouldRunAutoCleanup } = await import('../wu-done-auto-cleanup.js');
|
|
56
|
+
const result = shouldRunAutoCleanup();
|
|
57
|
+
expect(result).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
it('should return true when cleanup config is missing (default behavior)', async () => {
|
|
60
|
+
vi.doMock(CONFIG_MODULE_PATH, () => ({
|
|
61
|
+
getConfig: vi.fn().mockReturnValue({}),
|
|
62
|
+
}));
|
|
63
|
+
const { shouldRunAutoCleanup } = await import('../wu-done-auto-cleanup.js');
|
|
64
|
+
const result = shouldRunAutoCleanup();
|
|
65
|
+
expect(result).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('runAutoCleanupAfterDone non-fatal behavior', () => {
|
|
69
|
+
it('should not throw when cleanup throws an error', async () => {
|
|
70
|
+
// Mock config to enable cleanup
|
|
71
|
+
vi.doMock(CONFIG_MODULE_PATH, () => ({
|
|
72
|
+
getConfig: vi.fn().mockReturnValue({
|
|
73
|
+
cleanup: { trigger: 'on_done' },
|
|
74
|
+
directories: { wuDir: 'docs/tasks/wu' },
|
|
75
|
+
}),
|
|
76
|
+
}));
|
|
77
|
+
// Mock cleanupState to throw
|
|
78
|
+
vi.doMock('@lumenflow/core/dist/state-cleanup-core.js', () => ({
|
|
79
|
+
cleanupState: vi.fn().mockRejectedValue(new Error('Cleanup failed')),
|
|
80
|
+
}));
|
|
81
|
+
// Mock the memory functions to avoid actual file operations
|
|
82
|
+
vi.doMock('@lumenflow/memory/dist/signal-cleanup-core.js', () => ({
|
|
83
|
+
cleanupSignals: vi.fn().mockResolvedValue({
|
|
84
|
+
success: true,
|
|
85
|
+
removedIds: [],
|
|
86
|
+
retainedIds: [],
|
|
87
|
+
bytesFreed: 0,
|
|
88
|
+
compactionRatio: 0,
|
|
89
|
+
breakdown: {},
|
|
90
|
+
}),
|
|
91
|
+
}));
|
|
92
|
+
vi.doMock('@lumenflow/memory/dist/mem-cleanup-core.js', () => ({
|
|
93
|
+
cleanupMemory: vi.fn().mockResolvedValue({
|
|
94
|
+
success: true,
|
|
95
|
+
removedIds: [],
|
|
96
|
+
retainedIds: [],
|
|
97
|
+
bytesFreed: 0,
|
|
98
|
+
compactionRatio: 0,
|
|
99
|
+
breakdown: {},
|
|
100
|
+
}),
|
|
101
|
+
}));
|
|
102
|
+
vi.doMock('@lumenflow/core/dist/wu-events-cleanup.js', () => ({
|
|
103
|
+
archiveWuEvents: vi.fn().mockResolvedValue({
|
|
104
|
+
success: true,
|
|
105
|
+
archivedWuIds: [],
|
|
106
|
+
retainedWuIds: [],
|
|
107
|
+
bytesArchived: 0,
|
|
108
|
+
archivedEventCount: 0,
|
|
109
|
+
retainedEventCount: 0,
|
|
110
|
+
breakdown: {},
|
|
111
|
+
}),
|
|
112
|
+
}));
|
|
113
|
+
const { runAutoCleanupAfterDone } = await import('../wu-done-auto-cleanup.js');
|
|
114
|
+
// Should not throw - cleanup errors are non-fatal
|
|
115
|
+
await expect(runAutoCleanupAfterDone('/test/dir')).resolves.not.toThrow();
|
|
116
|
+
// Should log warning about the error
|
|
117
|
+
expect(consoleWarnSpy).toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
it('should skip cleanup when trigger is manual', async () => {
|
|
120
|
+
vi.doMock(CONFIG_MODULE_PATH, () => ({
|
|
121
|
+
getConfig: vi.fn().mockReturnValue({
|
|
122
|
+
cleanup: { trigger: 'manual' },
|
|
123
|
+
}),
|
|
124
|
+
}));
|
|
125
|
+
const mockCleanupState = vi.fn();
|
|
126
|
+
vi.doMock('@lumenflow/core/dist/state-cleanup-core.js', () => ({
|
|
127
|
+
cleanupState: mockCleanupState,
|
|
128
|
+
}));
|
|
129
|
+
const { runAutoCleanupAfterDone } = await import('../wu-done-auto-cleanup.js');
|
|
130
|
+
await runAutoCleanupAfterDone('/test/dir');
|
|
131
|
+
// Cleanup should not be called when trigger is manual
|
|
132
|
+
expect(mockCleanupState).not.toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WU Lifecycle Integration Tests (WU-1363)
|
|
3
|
+
*
|
|
4
|
+
* Integration tests covering the full WU lifecycle:
|
|
5
|
+
* - AC1: wu:create, wu:claim, wu:status
|
|
6
|
+
* - AC2: wu:prep, wu:done workflow
|
|
7
|
+
*
|
|
8
|
+
* These tests validate the end-to-end behavior of WU lifecycle commands
|
|
9
|
+
* by running them in isolated temporary directories with proper git setup.
|
|
10
|
+
*
|
|
11
|
+
* TDD: Tests written BEFORE implementation verification.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
14
|
+
import { existsSync, mkdirSync, rmSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import { execFileSync } from 'node:child_process';
|
|
18
|
+
import { parseYAML, stringifyYAML } from '@lumenflow/core/dist/wu-yaml.js';
|
|
19
|
+
import { WU_STATUS } from '@lumenflow/core/dist/wu-constants.js';
|
|
20
|
+
// Test constants
|
|
21
|
+
const TEST_WU_ID = 'WU-9901';
|
|
22
|
+
const TEST_LANE = 'Framework: CLI';
|
|
23
|
+
const TEST_TITLE = 'Integration test WU';
|
|
24
|
+
const TEST_DESCRIPTION = 'Context: Integration test. Problem: Need to test lifecycle. Solution: Run integration tests.';
|
|
25
|
+
/**
|
|
26
|
+
* Helper to create a minimal LumenFlow project structure
|
|
27
|
+
*/
|
|
28
|
+
function createTestProject(baseDir) {
|
|
29
|
+
// Create directory structure
|
|
30
|
+
const dirs = [
|
|
31
|
+
'docs/04-operations/tasks/wu',
|
|
32
|
+
'docs/04-operations/tasks/initiatives',
|
|
33
|
+
'.lumenflow/state',
|
|
34
|
+
'.lumenflow/stamps',
|
|
35
|
+
'packages/@lumenflow/cli/src/__tests__',
|
|
36
|
+
];
|
|
37
|
+
for (const dir of dirs) {
|
|
38
|
+
mkdirSync(join(baseDir, dir), { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
// Create minimal .lumenflow.config.yaml
|
|
41
|
+
const configContent = `
|
|
42
|
+
version: 1
|
|
43
|
+
lanes:
|
|
44
|
+
definitions:
|
|
45
|
+
- name: 'Framework: CLI'
|
|
46
|
+
wip_limit: 1
|
|
47
|
+
code_paths:
|
|
48
|
+
- 'packages/@lumenflow/cli/**'
|
|
49
|
+
git:
|
|
50
|
+
requireRemote: false
|
|
51
|
+
experimental:
|
|
52
|
+
context_validation: false
|
|
53
|
+
`;
|
|
54
|
+
writeFileSync(join(baseDir, '.lumenflow.config.yaml'), configContent);
|
|
55
|
+
// Create minimal package.json
|
|
56
|
+
const packageJson = {
|
|
57
|
+
name: 'test-project',
|
|
58
|
+
version: '1.0.0',
|
|
59
|
+
type: 'module',
|
|
60
|
+
};
|
|
61
|
+
writeFileSync(join(baseDir, 'package.json'), JSON.stringify(packageJson, null, 2));
|
|
62
|
+
// Initialize git repo using execFileSync (safer than execSync)
|
|
63
|
+
execFileSync('git', ['init'], { cwd: baseDir, stdio: 'pipe' });
|
|
64
|
+
execFileSync('git', ['config', 'user.email', 'test@test.com'], { cwd: baseDir, stdio: 'pipe' });
|
|
65
|
+
execFileSync('git', ['config', 'user.name', 'Test'], { cwd: baseDir, stdio: 'pipe' });
|
|
66
|
+
writeFileSync(join(baseDir, 'README.md'), '# Test Project\n');
|
|
67
|
+
execFileSync('git', ['add', '.'], { cwd: baseDir, stdio: 'pipe' });
|
|
68
|
+
execFileSync('git', ['commit', '-m', 'Initial commit'], { cwd: baseDir, stdio: 'pipe' });
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Helper to create a WU YAML file directly
|
|
72
|
+
*/
|
|
73
|
+
function createWUFile(baseDir, id, options = {}) {
|
|
74
|
+
const wuDir = join(baseDir, 'docs/04-operations/tasks/wu');
|
|
75
|
+
const wuPath = join(wuDir, `${id}.yaml`);
|
|
76
|
+
const doc = {
|
|
77
|
+
id,
|
|
78
|
+
title: options.title || TEST_TITLE,
|
|
79
|
+
lane: options.lane || TEST_LANE,
|
|
80
|
+
status: options.status || WU_STATUS.READY,
|
|
81
|
+
type: 'feature',
|
|
82
|
+
priority: 'P2',
|
|
83
|
+
created: '2026-02-03',
|
|
84
|
+
description: options.description || TEST_DESCRIPTION,
|
|
85
|
+
acceptance: options.acceptance || ['Test criterion 1', 'Test criterion 2'],
|
|
86
|
+
code_paths: options.codePaths || ['packages/@lumenflow/cli/src/__tests__'],
|
|
87
|
+
tests: {
|
|
88
|
+
unit: ['packages/@lumenflow/cli/src/__tests__/wu-lifecycle-integration.test.ts'],
|
|
89
|
+
},
|
|
90
|
+
exposure: 'backend-only',
|
|
91
|
+
};
|
|
92
|
+
writeFileSync(wuPath, stringifyYAML(doc));
|
|
93
|
+
return wuPath;
|
|
94
|
+
}
|
|
95
|
+
describe('WU Lifecycle Integration Tests (WU-1363)', () => {
|
|
96
|
+
let tempDir;
|
|
97
|
+
let originalCwd;
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
tempDir = join(tmpdir(), `wu-lifecycle-integration-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
100
|
+
mkdirSync(tempDir, { recursive: true });
|
|
101
|
+
originalCwd = process.cwd();
|
|
102
|
+
createTestProject(tempDir);
|
|
103
|
+
vi.resetModules(); // Reset module cache for fresh imports
|
|
104
|
+
});
|
|
105
|
+
afterEach(() => {
|
|
106
|
+
process.chdir(originalCwd);
|
|
107
|
+
if (existsSync(tempDir)) {
|
|
108
|
+
try {
|
|
109
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Ignore cleanup errors
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
vi.clearAllMocks();
|
|
116
|
+
});
|
|
117
|
+
describe('AC1: Integration tests for wu:create, wu:claim, wu:status', () => {
|
|
118
|
+
describe('wu:create core functionality', () => {
|
|
119
|
+
it('should create a WU YAML file with correct structure', async () => {
|
|
120
|
+
// Arrange
|
|
121
|
+
process.chdir(tempDir);
|
|
122
|
+
// Act - Create WU file directly (simulating wu:create core behavior)
|
|
123
|
+
const wuPath = createWUFile(tempDir, TEST_WU_ID, {
|
|
124
|
+
status: WU_STATUS.READY,
|
|
125
|
+
lane: TEST_LANE,
|
|
126
|
+
title: TEST_TITLE,
|
|
127
|
+
description: TEST_DESCRIPTION,
|
|
128
|
+
acceptance: ['Criterion 1', 'Criterion 2'],
|
|
129
|
+
codePaths: ['packages/@lumenflow/cli/src'],
|
|
130
|
+
});
|
|
131
|
+
// Assert
|
|
132
|
+
expect(existsSync(wuPath)).toBe(true);
|
|
133
|
+
const content = readFileSync(wuPath, 'utf-8');
|
|
134
|
+
const doc = parseYAML(content);
|
|
135
|
+
expect(doc.id).toBe(TEST_WU_ID);
|
|
136
|
+
expect(doc.lane).toBe(TEST_LANE);
|
|
137
|
+
expect(doc.status).toBe(WU_STATUS.READY);
|
|
138
|
+
expect(doc.title).toBe(TEST_TITLE);
|
|
139
|
+
expect(doc.acceptance).toHaveLength(2);
|
|
140
|
+
});
|
|
141
|
+
it('should validate required fields are present', () => {
|
|
142
|
+
// Arrange
|
|
143
|
+
process.chdir(tempDir);
|
|
144
|
+
// Create a minimal WU and verify required fields
|
|
145
|
+
const wuPath = createWUFile(tempDir, TEST_WU_ID);
|
|
146
|
+
const content = readFileSync(wuPath, 'utf-8');
|
|
147
|
+
const doc = parseYAML(content);
|
|
148
|
+
// Assert required fields exist
|
|
149
|
+
expect(doc.id).toBeDefined();
|
|
150
|
+
expect(doc.title).toBeDefined();
|
|
151
|
+
expect(doc.lane).toBeDefined();
|
|
152
|
+
expect(doc.status).toBeDefined();
|
|
153
|
+
expect(doc.description).toBeDefined();
|
|
154
|
+
expect(doc.acceptance).toBeDefined();
|
|
155
|
+
expect(doc.code_paths).toBeDefined();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
describe('wu:claim core functionality', () => {
|
|
159
|
+
it('should update WU status to in_progress when claimed', async () => {
|
|
160
|
+
// Arrange
|
|
161
|
+
process.chdir(tempDir);
|
|
162
|
+
const wuPath = createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.READY });
|
|
163
|
+
// Act - Simulate claim by updating status
|
|
164
|
+
const content = readFileSync(wuPath, 'utf-8');
|
|
165
|
+
const doc = parseYAML(content);
|
|
166
|
+
doc.status = WU_STATUS.IN_PROGRESS;
|
|
167
|
+
doc.claimed_at = new Date().toISOString();
|
|
168
|
+
doc.worktree_path = `worktrees/framework-cli-${TEST_WU_ID.toLowerCase()}`;
|
|
169
|
+
writeFileSync(wuPath, stringifyYAML(doc));
|
|
170
|
+
// Assert
|
|
171
|
+
const updatedContent = readFileSync(wuPath, 'utf-8');
|
|
172
|
+
const updatedDoc = parseYAML(updatedContent);
|
|
173
|
+
expect(updatedDoc.status).toBe(WU_STATUS.IN_PROGRESS);
|
|
174
|
+
expect(updatedDoc.claimed_at).toBeDefined();
|
|
175
|
+
expect(updatedDoc.worktree_path).toContain('worktrees');
|
|
176
|
+
});
|
|
177
|
+
it('should reject claim when WU does not exist', () => {
|
|
178
|
+
// Arrange
|
|
179
|
+
process.chdir(tempDir);
|
|
180
|
+
const nonExistentPath = join(tempDir, 'docs/04-operations/tasks/wu', 'WU-9999.yaml');
|
|
181
|
+
// Assert
|
|
182
|
+
expect(existsSync(nonExistentPath)).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
it('should reject claim when WU is not in ready status', () => {
|
|
185
|
+
// Arrange
|
|
186
|
+
process.chdir(tempDir);
|
|
187
|
+
createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.DONE });
|
|
188
|
+
// Read and verify status prevents claiming
|
|
189
|
+
const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
|
|
190
|
+
const content = readFileSync(wuPath, 'utf-8');
|
|
191
|
+
const doc = parseYAML(content);
|
|
192
|
+
// Assert
|
|
193
|
+
expect(doc.status).toBe(WU_STATUS.DONE);
|
|
194
|
+
expect(doc.status).not.toBe(WU_STATUS.READY);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
describe('wu:status core functionality', () => {
|
|
198
|
+
it('should return WU details correctly', () => {
|
|
199
|
+
// Arrange
|
|
200
|
+
process.chdir(tempDir);
|
|
201
|
+
createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.READY });
|
|
202
|
+
// Act - Read WU status
|
|
203
|
+
const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
|
|
204
|
+
const content = readFileSync(wuPath, 'utf-8');
|
|
205
|
+
const doc = parseYAML(content);
|
|
206
|
+
// Assert
|
|
207
|
+
expect(doc.id).toBe(TEST_WU_ID);
|
|
208
|
+
expect(doc.status).toBe(WU_STATUS.READY);
|
|
209
|
+
expect(doc.lane).toBe(TEST_LANE);
|
|
210
|
+
});
|
|
211
|
+
it('should return valid commands for ready status', () => {
|
|
212
|
+
// Arrange
|
|
213
|
+
process.chdir(tempDir);
|
|
214
|
+
createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.READY });
|
|
215
|
+
// Act
|
|
216
|
+
const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
|
|
217
|
+
const content = readFileSync(wuPath, 'utf-8');
|
|
218
|
+
const doc = parseYAML(content);
|
|
219
|
+
// Compute valid commands based on status
|
|
220
|
+
const validCommands = doc.status === WU_STATUS.READY
|
|
221
|
+
? ['wu:claim', 'wu:edit', 'wu:delete']
|
|
222
|
+
: ['wu:prep', 'wu:block'];
|
|
223
|
+
// Assert
|
|
224
|
+
expect(validCommands).toContain('wu:claim');
|
|
225
|
+
});
|
|
226
|
+
it('should return valid commands for in_progress status', () => {
|
|
227
|
+
// Arrange
|
|
228
|
+
process.chdir(tempDir);
|
|
229
|
+
createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.IN_PROGRESS });
|
|
230
|
+
// Act
|
|
231
|
+
const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
|
|
232
|
+
const content = readFileSync(wuPath, 'utf-8');
|
|
233
|
+
const doc = parseYAML(content);
|
|
234
|
+
// Compute valid commands based on status
|
|
235
|
+
const validCommands = doc.status === WU_STATUS.IN_PROGRESS ? ['wu:prep', 'wu:block'] : ['wu:claim'];
|
|
236
|
+
// Assert
|
|
237
|
+
expect(validCommands).toContain('wu:prep');
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
describe('AC2: Integration tests for wu:prep, wu:done workflow', () => {
|
|
242
|
+
describe('wu:prep core functionality', () => {
|
|
243
|
+
it('should validate WU is in in_progress status', () => {
|
|
244
|
+
// Arrange
|
|
245
|
+
process.chdir(tempDir);
|
|
246
|
+
createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.IN_PROGRESS });
|
|
247
|
+
// Act
|
|
248
|
+
const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
|
|
249
|
+
const content = readFileSync(wuPath, 'utf-8');
|
|
250
|
+
const doc = parseYAML(content);
|
|
251
|
+
// Assert
|
|
252
|
+
expect(doc.status).toBe(WU_STATUS.IN_PROGRESS);
|
|
253
|
+
});
|
|
254
|
+
it('should generate next command pointing to wu:done', () => {
|
|
255
|
+
// Arrange
|
|
256
|
+
process.chdir(tempDir);
|
|
257
|
+
createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.IN_PROGRESS });
|
|
258
|
+
// Act - Generate next command
|
|
259
|
+
const mainCheckoutPath = tempDir;
|
|
260
|
+
const nextCommand = `cd ${mainCheckoutPath} && pnpm wu:done --id ${TEST_WU_ID}`;
|
|
261
|
+
// Assert
|
|
262
|
+
expect(nextCommand).toContain('wu:done');
|
|
263
|
+
expect(nextCommand).toContain(TEST_WU_ID);
|
|
264
|
+
expect(nextCommand).toContain(mainCheckoutPath);
|
|
265
|
+
});
|
|
266
|
+
it('should reject prep when WU is not in_progress', () => {
|
|
267
|
+
// Arrange
|
|
268
|
+
process.chdir(tempDir);
|
|
269
|
+
createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.READY });
|
|
270
|
+
// Act
|
|
271
|
+
const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
|
|
272
|
+
const content = readFileSync(wuPath, 'utf-8');
|
|
273
|
+
const doc = parseYAML(content);
|
|
274
|
+
// Assert
|
|
275
|
+
expect(doc.status).not.toBe(WU_STATUS.IN_PROGRESS);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
describe('wu:done core functionality', () => {
|
|
279
|
+
it('should create stamp file on completion', () => {
|
|
280
|
+
// Arrange
|
|
281
|
+
process.chdir(tempDir);
|
|
282
|
+
createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.IN_PROGRESS });
|
|
283
|
+
// Act - Create stamp file
|
|
284
|
+
const stampDir = join(tempDir, '.lumenflow/stamps');
|
|
285
|
+
mkdirSync(stampDir, { recursive: true });
|
|
286
|
+
const stampPath = join(stampDir, `${TEST_WU_ID}.done`);
|
|
287
|
+
const stampContent = {
|
|
288
|
+
completed_at: new Date().toISOString(),
|
|
289
|
+
wu_id: TEST_WU_ID,
|
|
290
|
+
};
|
|
291
|
+
writeFileSync(stampPath, JSON.stringify(stampContent, null, 2));
|
|
292
|
+
// Assert
|
|
293
|
+
expect(existsSync(stampPath)).toBe(true);
|
|
294
|
+
const savedStamp = JSON.parse(readFileSync(stampPath, 'utf-8'));
|
|
295
|
+
expect(savedStamp.wu_id).toBe(TEST_WU_ID);
|
|
296
|
+
});
|
|
297
|
+
it('should update WU status to done', () => {
|
|
298
|
+
// Arrange
|
|
299
|
+
process.chdir(tempDir);
|
|
300
|
+
const wuPath = createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.IN_PROGRESS });
|
|
301
|
+
// Act - Update status to done
|
|
302
|
+
const content = readFileSync(wuPath, 'utf-8');
|
|
303
|
+
const doc = parseYAML(content);
|
|
304
|
+
doc.status = WU_STATUS.DONE;
|
|
305
|
+
doc.completed_at = new Date().toISOString();
|
|
306
|
+
writeFileSync(wuPath, stringifyYAML(doc));
|
|
307
|
+
// Assert
|
|
308
|
+
const updatedContent = readFileSync(wuPath, 'utf-8');
|
|
309
|
+
const updatedDoc = parseYAML(updatedContent);
|
|
310
|
+
expect(updatedDoc.status).toBe(WU_STATUS.DONE);
|
|
311
|
+
expect(updatedDoc.completed_at).toBeDefined();
|
|
312
|
+
});
|
|
313
|
+
it('should reject done when WU is not in_progress', () => {
|
|
314
|
+
// Arrange
|
|
315
|
+
process.chdir(tempDir);
|
|
316
|
+
createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.READY });
|
|
317
|
+
// Act
|
|
318
|
+
const wuPath = join(tempDir, 'docs/04-operations/tasks/wu', `${TEST_WU_ID}.yaml`);
|
|
319
|
+
const content = readFileSync(wuPath, 'utf-8');
|
|
320
|
+
const doc = parseYAML(content);
|
|
321
|
+
// Assert - Cannot complete a WU that isn't in_progress
|
|
322
|
+
expect(doc.status).not.toBe(WU_STATUS.IN_PROGRESS);
|
|
323
|
+
});
|
|
324
|
+
it('should support skip-gates flag with reason and fix-wu', () => {
|
|
325
|
+
// Arrange
|
|
326
|
+
process.chdir(tempDir);
|
|
327
|
+
createWUFile(tempDir, TEST_WU_ID, { status: WU_STATUS.IN_PROGRESS });
|
|
328
|
+
// Act - Simulate skip-gates audit log
|
|
329
|
+
const skipGatesLog = {
|
|
330
|
+
wu_id: TEST_WU_ID,
|
|
331
|
+
skipped_at: new Date().toISOString(),
|
|
332
|
+
reason: 'pre-existing on main',
|
|
333
|
+
fix_wu: 'WU-1234',
|
|
334
|
+
};
|
|
335
|
+
const auditPath = join(tempDir, '.lumenflow/skip-gates-audit.log');
|
|
336
|
+
writeFileSync(auditPath, JSON.stringify(skipGatesLog) + '\n');
|
|
337
|
+
// Assert
|
|
338
|
+
expect(existsSync(auditPath)).toBe(true);
|
|
339
|
+
const logContent = readFileSync(auditPath, 'utf-8');
|
|
340
|
+
expect(logContent).toContain('pre-existing on main');
|
|
341
|
+
expect(logContent).toContain('WU-1234');
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
describe('wu:prep + wu:done complete workflow', () => {
|
|
345
|
+
it('should complete full lifecycle from create to done', () => {
|
|
346
|
+
// This test validates the complete workflow state transitions:
|
|
347
|
+
// 1. Create WU (status: ready)
|
|
348
|
+
// 2. Claim WU (status: in_progress, worktree created)
|
|
349
|
+
// 3. Prep WU (gates run, provides next command)
|
|
350
|
+
// 4. Done WU (status: done, stamp created)
|
|
351
|
+
// Arrange
|
|
352
|
+
process.chdir(tempDir);
|
|
353
|
+
// Step 1: Create WU
|
|
354
|
+
const wuPath = createWUFile(tempDir, TEST_WU_ID, {
|
|
355
|
+
status: WU_STATUS.READY,
|
|
356
|
+
lane: TEST_LANE,
|
|
357
|
+
title: TEST_TITLE,
|
|
358
|
+
description: TEST_DESCRIPTION,
|
|
359
|
+
acceptance: ['Full lifecycle test'],
|
|
360
|
+
});
|
|
361
|
+
expect(existsSync(wuPath)).toBe(true);
|
|
362
|
+
// Step 2: Simulate Claim WU
|
|
363
|
+
let doc = parseYAML(readFileSync(wuPath, 'utf-8'));
|
|
364
|
+
expect(doc.status).toBe(WU_STATUS.READY);
|
|
365
|
+
doc.status = WU_STATUS.IN_PROGRESS;
|
|
366
|
+
doc.claimed_at = new Date().toISOString();
|
|
367
|
+
doc.worktree_path = `worktrees/framework-cli-${TEST_WU_ID.toLowerCase()}`;
|
|
368
|
+
writeFileSync(wuPath, stringifyYAML(doc));
|
|
369
|
+
// Step 3: Verify Prep is valid (status check)
|
|
370
|
+
doc = parseYAML(readFileSync(wuPath, 'utf-8'));
|
|
371
|
+
expect(doc.status).toBe(WU_STATUS.IN_PROGRESS);
|
|
372
|
+
const nextCommand = `cd ${tempDir} && pnpm wu:done --id ${TEST_WU_ID}`;
|
|
373
|
+
expect(nextCommand).toContain('wu:done');
|
|
374
|
+
// Step 4: Complete WU
|
|
375
|
+
doc.status = WU_STATUS.DONE;
|
|
376
|
+
doc.completed_at = new Date().toISOString();
|
|
377
|
+
writeFileSync(wuPath, stringifyYAML(doc));
|
|
378
|
+
// Create stamp
|
|
379
|
+
const stampPath = join(tempDir, '.lumenflow/stamps', `${TEST_WU_ID}.done`);
|
|
380
|
+
writeFileSync(stampPath, JSON.stringify({ completed_at: new Date().toISOString() }));
|
|
381
|
+
// Verify final state
|
|
382
|
+
expect(existsSync(stampPath)).toBe(true);
|
|
383
|
+
doc = parseYAML(readFileSync(wuPath, 'utf-8'));
|
|
384
|
+
expect(doc.status).toBe(WU_STATUS.DONE);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
});
|
package/dist/backlog-prune.js
CHANGED
|
@@ -17,7 +17,6 @@ import path from 'node:path';
|
|
|
17
17
|
import { readWURaw, writeWU, appendNote } from '@lumenflow/core/dist/wu-yaml.js';
|
|
18
18
|
import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
|
|
19
19
|
import { CLI_FLAGS, EXIT_CODES, EMOJI, WU_STATUS, STRING_LITERALS, } from '@lumenflow/core/dist/wu-constants.js';
|
|
20
|
-
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
21
20
|
/** Log prefix for consistent output */
|
|
22
21
|
const LOG_PREFIX = '[backlog-prune]';
|
|
23
22
|
/**
|
package/dist/cli-entry-point.js
CHANGED