@lumenflow/cli 2.20.1 → 2.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/dist/hooks/enforcement-checks.js +120 -0
- package/dist/hooks/enforcement-checks.js.map +1 -1
- package/dist/init-lane-validation.js +141 -0
- package/dist/init-lane-validation.js.map +1 -0
- package/dist/init-templates.js +36 -8
- package/dist/init-templates.js.map +1 -1
- package/dist/init.js +27 -58
- package/dist/init.js.map +1 -1
- package/dist/initiative-create.js +35 -4
- package/dist/initiative-create.js.map +1 -1
- package/dist/lane-lifecycle-process.js +364 -0
- package/dist/lane-lifecycle-process.js.map +1 -0
- package/dist/lane-lock.js +41 -0
- package/dist/lane-lock.js.map +1 -0
- package/dist/lane-setup.js +55 -0
- package/dist/lane-setup.js.map +1 -0
- package/dist/lane-status.js +38 -0
- package/dist/lane-status.js.map +1 -0
- package/dist/lane-validate.js +43 -0
- package/dist/lane-validate.js.map +1 -0
- package/dist/onboarding-smoke-test.js +17 -0
- package/dist/onboarding-smoke-test.js.map +1 -1
- package/dist/public-manifest.js +28 -0
- package/dist/public-manifest.js.map +1 -1
- package/dist/wu-claim-cloud.js +16 -0
- package/dist/wu-claim-cloud.js.map +1 -1
- package/dist/wu-claim.js +12 -2
- package/dist/wu-claim.js.map +1 -1
- package/dist/wu-create-content.js +8 -2
- package/dist/wu-create-content.js.map +1 -1
- package/dist/wu-create-validation.js +5 -3
- package/dist/wu-create-validation.js.map +1 -1
- package/dist/wu-create.js +21 -1
- package/dist/wu-create.js.map +1 -1
- package/dist/wu-done.js +57 -8
- package/dist/wu-done.js.map +1 -1
- package/dist/wu-prep.js +22 -0
- package/dist/wu-prep.js.map +1 -1
- package/package.json +15 -11
- package/dist/__tests__/agent-log-issue.test.js +0 -56
- package/dist/__tests__/agent-spawn-coordination.test.js +0 -451
- package/dist/__tests__/backlog-prune.test.js +0 -478
- package/dist/__tests__/cli-entry-point.test.js +0 -160
- package/dist/__tests__/cli-subprocess.test.js +0 -89
- package/dist/__tests__/commands/integrate.test.js +0 -165
- package/dist/__tests__/commands.test.js +0 -271
- package/dist/__tests__/deps-operations.test.js +0 -206
- package/dist/__tests__/doctor.test.js +0 -510
- package/dist/__tests__/file-operations.test.js +0 -906
- package/dist/__tests__/flow-report.test.js +0 -24
- package/dist/__tests__/gates-config.test.js +0 -303
- package/dist/__tests__/gates-integration-tests.test.js +0 -112
- package/dist/__tests__/git-operations.test.js +0 -668
- package/dist/__tests__/guard-main-branch.test.js +0 -79
- package/dist/__tests__/guards-validation.test.js +0 -416
- package/dist/__tests__/hooks/enforcement.test.js +0 -279
- package/dist/__tests__/init-config-lanes.test.js +0 -131
- package/dist/__tests__/init-docs-structure.test.js +0 -152
- package/dist/__tests__/init-greenfield.test.js +0 -247
- package/dist/__tests__/init-lane-inference.test.js +0 -125
- package/dist/__tests__/init-onboarding-docs.test.js +0 -132
- package/dist/__tests__/init-quick-ref.test.js +0 -144
- package/dist/__tests__/init-scripts.test.js +0 -207
- package/dist/__tests__/init-template-portability.test.js +0 -96
- package/dist/__tests__/init.test.js +0 -968
- package/dist/__tests__/initiative-add-wu.test.js +0 -490
- package/dist/__tests__/initiative-e2e.test.js +0 -442
- package/dist/__tests__/initiative-plan-replacement.test.js +0 -161
- package/dist/__tests__/initiative-plan.test.js +0 -340
- package/dist/__tests__/initiative-remove-wu.test.js +0 -458
- package/dist/__tests__/lumenflow-upgrade.test.js +0 -260
- package/dist/__tests__/mem-cleanup-execution.test.js +0 -19
- package/dist/__tests__/memory-integration.test.js +0 -333
- package/dist/__tests__/merge-block.test.js +0 -220
- package/dist/__tests__/metrics-cli.test.js +0 -619
- package/dist/__tests__/metrics-snapshot.test.js +0 -24
- package/dist/__tests__/no-beacon-references-docs.test.js +0 -30
- package/dist/__tests__/no-beacon-references.test.js +0 -39
- package/dist/__tests__/onboarding-smoke-test.test.js +0 -211
- package/dist/__tests__/path-centralization-cli.test.js +0 -234
- package/dist/__tests__/plan-create.test.js +0 -126
- package/dist/__tests__/plan-edit.test.js +0 -157
- package/dist/__tests__/plan-link.test.js +0 -239
- package/dist/__tests__/plan-promote.test.js +0 -181
- package/dist/__tests__/release.test.js +0 -372
- package/dist/__tests__/rotate-progress.test.js +0 -127
- package/dist/__tests__/safe-git.test.js +0 -190
- package/dist/__tests__/session-coordinator.test.js +0 -109
- package/dist/__tests__/state-bootstrap.test.js +0 -432
- package/dist/__tests__/state-doctor.test.js +0 -328
- package/dist/__tests__/sync-templates.test.js +0 -255
- package/dist/__tests__/templates-sync.test.js +0 -219
- package/dist/__tests__/trace-gen.test.js +0 -115
- package/dist/__tests__/wu-create-required-fields.test.js +0 -143
- package/dist/__tests__/wu-create-strict.test.js +0 -118
- package/dist/__tests__/wu-create.test.js +0 -121
- package/dist/__tests__/wu-done-auto-cleanup.test.js +0 -135
- package/dist/__tests__/wu-done-docs-only-policy.test.js +0 -20
- package/dist/__tests__/wu-done-staging-whitelist.test.js +0 -35
- package/dist/__tests__/wu-done.test.js +0 -36
- package/dist/__tests__/wu-edit-strict.test.js +0 -109
- package/dist/__tests__/wu-edit.test.js +0 -119
- package/dist/__tests__/wu-lifecycle-integration.test.js +0 -388
- package/dist/__tests__/wu-prep-default-exec.test.js +0 -35
- package/dist/__tests__/wu-prep.test.js +0 -140
- package/dist/__tests__/wu-proto.test.js +0 -97
- package/dist/__tests__/wu-validate-strict.test.js +0 -113
- package/dist/__tests__/wu-validate.test.js +0 -36
- package/dist/spawn-list.js +0 -143
- package/dist/spawn-list.js.map +0 -1
|
@@ -1,478 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file backlog-prune.test.ts
|
|
3
|
-
* @description Tests for backlog-prune CLI command (WU-1106)
|
|
4
|
-
*
|
|
5
|
-
* backlog-prune maintains backlog hygiene by:
|
|
6
|
-
* - Auto-tagging stale WUs (in_progress/ready too long without activity)
|
|
7
|
-
* - Archiving old completed WUs (done for > N days)
|
|
8
|
-
*
|
|
9
|
-
* TDD: RED phase - these tests define expected behavior before implementation
|
|
10
|
-
*/
|
|
11
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
12
|
-
import { existsSync } from 'node:fs';
|
|
13
|
-
import { join } from 'node:path';
|
|
14
|
-
import { fileURLToPath } from 'node:url';
|
|
15
|
-
import { calculateStaleDays, isWuStale, isWuArchivable, categorizeWus, parseBacklogPruneArgs, BACKLOG_PRUNE_DEFAULTS, loadAllWus, tagStaleWu, printHelp, } from '../backlog-prune.js';
|
|
16
|
-
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
17
|
-
describe('backlog-prune CLI', () => {
|
|
18
|
-
describe('source file existence', () => {
|
|
19
|
-
it('should have the CLI source file', () => {
|
|
20
|
-
const srcPath = join(__dirname, '../backlog-prune.ts');
|
|
21
|
-
expect(existsSync(srcPath)).toBe(true);
|
|
22
|
-
});
|
|
23
|
-
it('should be buildable (dist file exists after build)', () => {
|
|
24
|
-
// This test verifies that tsc compiled the file successfully
|
|
25
|
-
const distPath = join(__dirname, '../../dist/backlog-prune.js');
|
|
26
|
-
expect(existsSync(distPath)).toBe(true);
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
describe('BACKLOG_PRUNE_DEFAULTS', () => {
|
|
30
|
-
it('should have default stale days for in_progress WUs', () => {
|
|
31
|
-
expect(BACKLOG_PRUNE_DEFAULTS.staleDaysInProgress).toBeTypeOf('number');
|
|
32
|
-
expect(BACKLOG_PRUNE_DEFAULTS.staleDaysInProgress).toBeGreaterThan(0);
|
|
33
|
-
});
|
|
34
|
-
it('should have default stale days for ready WUs', () => {
|
|
35
|
-
expect(BACKLOG_PRUNE_DEFAULTS.staleDaysReady).toBeTypeOf('number');
|
|
36
|
-
expect(BACKLOG_PRUNE_DEFAULTS.staleDaysReady).toBeGreaterThan(0);
|
|
37
|
-
});
|
|
38
|
-
it('should have default archive days for done WUs', () => {
|
|
39
|
-
expect(BACKLOG_PRUNE_DEFAULTS.archiveDaysDone).toBeTypeOf('number');
|
|
40
|
-
expect(BACKLOG_PRUNE_DEFAULTS.archiveDaysDone).toBeGreaterThan(0);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
describe('parseBacklogPruneArgs', () => {
|
|
44
|
-
it('should parse --dry-run flag (default)', () => {
|
|
45
|
-
const args = parseBacklogPruneArgs(['node', 'backlog-prune']);
|
|
46
|
-
expect(args.dryRun).toBe(true);
|
|
47
|
-
});
|
|
48
|
-
it('should parse --execute flag', () => {
|
|
49
|
-
const args = parseBacklogPruneArgs(['node', 'backlog-prune', '--execute']);
|
|
50
|
-
expect(args.dryRun).toBe(false);
|
|
51
|
-
});
|
|
52
|
-
it('should parse --stale-days-in-progress option', () => {
|
|
53
|
-
const args = parseBacklogPruneArgs([
|
|
54
|
-
'node',
|
|
55
|
-
'backlog-prune',
|
|
56
|
-
'--stale-days-in-progress',
|
|
57
|
-
'5',
|
|
58
|
-
]);
|
|
59
|
-
expect(args.staleDaysInProgress).toBe(5);
|
|
60
|
-
});
|
|
61
|
-
it('should parse --stale-days-ready option', () => {
|
|
62
|
-
const args = parseBacklogPruneArgs(['node', 'backlog-prune', '--stale-days-ready', '14']);
|
|
63
|
-
expect(args.staleDaysReady).toBe(14);
|
|
64
|
-
});
|
|
65
|
-
it('should parse --archive-days option', () => {
|
|
66
|
-
const args = parseBacklogPruneArgs(['node', 'backlog-prune', '--archive-days', '60']);
|
|
67
|
-
expect(args.archiveDaysDone).toBe(60);
|
|
68
|
-
});
|
|
69
|
-
it('should use defaults when options not provided', () => {
|
|
70
|
-
const args = parseBacklogPruneArgs(['node', 'backlog-prune']);
|
|
71
|
-
expect(args.staleDaysInProgress).toBe(BACKLOG_PRUNE_DEFAULTS.staleDaysInProgress);
|
|
72
|
-
expect(args.staleDaysReady).toBe(BACKLOG_PRUNE_DEFAULTS.staleDaysReady);
|
|
73
|
-
expect(args.archiveDaysDone).toBe(BACKLOG_PRUNE_DEFAULTS.archiveDaysDone);
|
|
74
|
-
});
|
|
75
|
-
it('should parse --help flag', () => {
|
|
76
|
-
const args = parseBacklogPruneArgs(['node', 'backlog-prune', '--help']);
|
|
77
|
-
expect(args.help).toBe(true);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
describe('calculateStaleDays', () => {
|
|
81
|
-
beforeEach(() => {
|
|
82
|
-
vi.useFakeTimers();
|
|
83
|
-
// Set current time to 2026-01-25
|
|
84
|
-
vi.setSystemTime(new Date('2026-01-25T12:00:00Z'));
|
|
85
|
-
});
|
|
86
|
-
afterEach(() => {
|
|
87
|
-
vi.useRealTimers();
|
|
88
|
-
});
|
|
89
|
-
it('should return days since date string', () => {
|
|
90
|
-
const result = calculateStaleDays('2026-01-20');
|
|
91
|
-
expect(result).toBe(5);
|
|
92
|
-
});
|
|
93
|
-
it('should return days since ISO date string', () => {
|
|
94
|
-
const result = calculateStaleDays('2026-01-23T10:00:00Z');
|
|
95
|
-
expect(result).toBe(2);
|
|
96
|
-
});
|
|
97
|
-
it('should return 0 for same day', () => {
|
|
98
|
-
const result = calculateStaleDays('2026-01-25');
|
|
99
|
-
expect(result).toBe(0);
|
|
100
|
-
});
|
|
101
|
-
it('should return null for invalid date', () => {
|
|
102
|
-
const result = calculateStaleDays('not-a-date');
|
|
103
|
-
expect(result).toBeNull();
|
|
104
|
-
});
|
|
105
|
-
it('should return null for undefined', () => {
|
|
106
|
-
const result = calculateStaleDays(undefined);
|
|
107
|
-
expect(result).toBeNull();
|
|
108
|
-
});
|
|
109
|
-
it('should return null for null', () => {
|
|
110
|
-
const result = calculateStaleDays(null);
|
|
111
|
-
expect(result).toBeNull();
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
describe('isWuStale', () => {
|
|
115
|
-
beforeEach(() => {
|
|
116
|
-
vi.useFakeTimers();
|
|
117
|
-
vi.setSystemTime(new Date('2026-01-25T12:00:00Z'));
|
|
118
|
-
});
|
|
119
|
-
afterEach(() => {
|
|
120
|
-
vi.useRealTimers();
|
|
121
|
-
});
|
|
122
|
-
it('should return true for in_progress WU older than threshold', () => {
|
|
123
|
-
const wu = {
|
|
124
|
-
id: 'WU-100',
|
|
125
|
-
status: 'in_progress',
|
|
126
|
-
created: '2026-01-10',
|
|
127
|
-
updated: '2026-01-15', // 10 days old
|
|
128
|
-
};
|
|
129
|
-
expect(isWuStale(wu, { staleDaysInProgress: 7 })).toBe(true);
|
|
130
|
-
});
|
|
131
|
-
it('should return false for in_progress WU within threshold', () => {
|
|
132
|
-
const wu = {
|
|
133
|
-
id: 'WU-100',
|
|
134
|
-
status: 'in_progress',
|
|
135
|
-
created: '2026-01-20',
|
|
136
|
-
updated: '2026-01-24', // 1 day old
|
|
137
|
-
};
|
|
138
|
-
expect(isWuStale(wu, { staleDaysInProgress: 7 })).toBe(false);
|
|
139
|
-
});
|
|
140
|
-
it('should return true for ready WU older than threshold', () => {
|
|
141
|
-
const wu = {
|
|
142
|
-
id: 'WU-100',
|
|
143
|
-
status: 'ready',
|
|
144
|
-
created: '2025-12-20', // 36 days old
|
|
145
|
-
};
|
|
146
|
-
expect(isWuStale(wu, { staleDaysReady: 30 })).toBe(true);
|
|
147
|
-
});
|
|
148
|
-
it('should return false for ready WU within threshold', () => {
|
|
149
|
-
const wu = {
|
|
150
|
-
id: 'WU-100',
|
|
151
|
-
status: 'ready',
|
|
152
|
-
created: '2026-01-10', // 15 days old
|
|
153
|
-
};
|
|
154
|
-
expect(isWuStale(wu, { staleDaysReady: 30 })).toBe(false);
|
|
155
|
-
});
|
|
156
|
-
it('should return false for done WU (done WUs are not stale, they may be archivable)', () => {
|
|
157
|
-
const wu = {
|
|
158
|
-
id: 'WU-100',
|
|
159
|
-
status: 'done',
|
|
160
|
-
created: '2025-01-01',
|
|
161
|
-
completed: '2025-01-10',
|
|
162
|
-
};
|
|
163
|
-
expect(isWuStale(wu, { staleDaysInProgress: 7, staleDaysReady: 30 })).toBe(false);
|
|
164
|
-
});
|
|
165
|
-
it('should use updated date if available for staleness check', () => {
|
|
166
|
-
const wu = {
|
|
167
|
-
id: 'WU-100',
|
|
168
|
-
status: 'in_progress',
|
|
169
|
-
created: '2025-01-01', // Very old
|
|
170
|
-
updated: '2026-01-24', // Recently updated (1 day ago)
|
|
171
|
-
};
|
|
172
|
-
expect(isWuStale(wu, { staleDaysInProgress: 7 })).toBe(false);
|
|
173
|
-
});
|
|
174
|
-
it('should fall back to created date if updated not available', () => {
|
|
175
|
-
const wu = {
|
|
176
|
-
id: 'WU-100',
|
|
177
|
-
status: 'in_progress',
|
|
178
|
-
created: '2026-01-10', // 15 days old
|
|
179
|
-
};
|
|
180
|
-
expect(isWuStale(wu, { staleDaysInProgress: 7 })).toBe(true);
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
describe('isWuArchivable', () => {
|
|
184
|
-
beforeEach(() => {
|
|
185
|
-
vi.useFakeTimers();
|
|
186
|
-
vi.setSystemTime(new Date('2026-01-25T12:00:00Z'));
|
|
187
|
-
});
|
|
188
|
-
afterEach(() => {
|
|
189
|
-
vi.useRealTimers();
|
|
190
|
-
});
|
|
191
|
-
it('should return true for done WU completed older than threshold', () => {
|
|
192
|
-
const wu = {
|
|
193
|
-
id: 'WU-100',
|
|
194
|
-
status: 'done',
|
|
195
|
-
created: '2025-01-01',
|
|
196
|
-
completed: '2025-11-25', // 61 days ago
|
|
197
|
-
};
|
|
198
|
-
expect(isWuArchivable(wu, { archiveDaysDone: 60 })).toBe(true);
|
|
199
|
-
});
|
|
200
|
-
it('should return false for done WU completed within threshold', () => {
|
|
201
|
-
const wu = {
|
|
202
|
-
id: 'WU-100',
|
|
203
|
-
status: 'done',
|
|
204
|
-
created: '2025-12-01',
|
|
205
|
-
completed: '2026-01-10', // 15 days ago
|
|
206
|
-
};
|
|
207
|
-
expect(isWuArchivable(wu, { archiveDaysDone: 60 })).toBe(false);
|
|
208
|
-
});
|
|
209
|
-
it('should return false for non-done WU', () => {
|
|
210
|
-
const wu = {
|
|
211
|
-
id: 'WU-100',
|
|
212
|
-
status: 'in_progress',
|
|
213
|
-
created: '2025-01-01',
|
|
214
|
-
};
|
|
215
|
-
expect(isWuArchivable(wu, { archiveDaysDone: 60 })).toBe(false);
|
|
216
|
-
});
|
|
217
|
-
it('should return false for done WU without completed date', () => {
|
|
218
|
-
const wu = {
|
|
219
|
-
id: 'WU-100',
|
|
220
|
-
status: 'done',
|
|
221
|
-
created: '2025-01-01',
|
|
222
|
-
};
|
|
223
|
-
expect(isWuArchivable(wu, { archiveDaysDone: 60 })).toBe(false);
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
describe('categorizeWus', () => {
|
|
227
|
-
beforeEach(() => {
|
|
228
|
-
vi.useFakeTimers();
|
|
229
|
-
vi.setSystemTime(new Date('2026-01-25T12:00:00Z'));
|
|
230
|
-
});
|
|
231
|
-
afterEach(() => {
|
|
232
|
-
vi.useRealTimers();
|
|
233
|
-
});
|
|
234
|
-
it('should categorize stale, archivable, and healthy WUs', () => {
|
|
235
|
-
const wus = [
|
|
236
|
-
// Stale in_progress (updated 10 days ago)
|
|
237
|
-
{ id: 'WU-101', status: 'in_progress', created: '2026-01-01', updated: '2026-01-15' },
|
|
238
|
-
// Healthy in_progress (updated 2 days ago)
|
|
239
|
-
{ id: 'WU-102', status: 'in_progress', created: '2026-01-01', updated: '2026-01-23' },
|
|
240
|
-
// Stale ready (created 40 days ago)
|
|
241
|
-
{ id: 'WU-103', status: 'ready', created: '2025-12-16' },
|
|
242
|
-
// Healthy ready (created 10 days ago)
|
|
243
|
-
{ id: 'WU-104', status: 'ready', created: '2026-01-15' },
|
|
244
|
-
// Archivable done (completed 70 days ago)
|
|
245
|
-
{ id: 'WU-105', status: 'done', created: '2025-10-01', completed: '2025-11-16' },
|
|
246
|
-
// Healthy done (completed 30 days ago)
|
|
247
|
-
{ id: 'WU-106', status: 'done', created: '2025-12-01', completed: '2025-12-26' },
|
|
248
|
-
// Blocked WU (not considered stale/archivable)
|
|
249
|
-
{ id: 'WU-107', status: 'blocked', created: '2025-01-01' },
|
|
250
|
-
];
|
|
251
|
-
const result = categorizeWus(wus, {
|
|
252
|
-
staleDaysInProgress: 7,
|
|
253
|
-
staleDaysReady: 30,
|
|
254
|
-
archiveDaysDone: 60,
|
|
255
|
-
});
|
|
256
|
-
expect(result.stale).toHaveLength(2);
|
|
257
|
-
expect(result.stale.map((w) => w.id)).toContain('WU-101');
|
|
258
|
-
expect(result.stale.map((w) => w.id)).toContain('WU-103');
|
|
259
|
-
expect(result.archivable).toHaveLength(1);
|
|
260
|
-
expect(result.archivable[0].id).toBe('WU-105');
|
|
261
|
-
expect(result.healthy).toHaveLength(4);
|
|
262
|
-
expect(result.healthy.map((w) => w.id)).toContain('WU-102');
|
|
263
|
-
expect(result.healthy.map((w) => w.id)).toContain('WU-104');
|
|
264
|
-
expect(result.healthy.map((w) => w.id)).toContain('WU-106');
|
|
265
|
-
expect(result.healthy.map((w) => w.id)).toContain('WU-107');
|
|
266
|
-
});
|
|
267
|
-
it('should return empty arrays when no WUs provided', () => {
|
|
268
|
-
const result = categorizeWus([], {
|
|
269
|
-
staleDaysInProgress: 7,
|
|
270
|
-
staleDaysReady: 30,
|
|
271
|
-
archiveDaysDone: 60,
|
|
272
|
-
});
|
|
273
|
-
expect(result.stale).toHaveLength(0);
|
|
274
|
-
expect(result.archivable).toHaveLength(0);
|
|
275
|
-
expect(result.healthy).toHaveLength(0);
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
describe('isWuStale - additional edge cases', () => {
|
|
279
|
-
beforeEach(() => {
|
|
280
|
-
vi.useFakeTimers();
|
|
281
|
-
vi.setSystemTime(new Date('2026-01-25T12:00:00Z'));
|
|
282
|
-
});
|
|
283
|
-
afterEach(() => {
|
|
284
|
-
vi.useRealTimers();
|
|
285
|
-
});
|
|
286
|
-
it('should return false for blocked WU regardless of age', () => {
|
|
287
|
-
const wu = {
|
|
288
|
-
id: 'WU-100',
|
|
289
|
-
status: 'blocked',
|
|
290
|
-
created: '2025-01-01', // Very old
|
|
291
|
-
};
|
|
292
|
-
expect(isWuStale(wu, { staleDaysInProgress: 7, staleDaysReady: 30 })).toBe(false);
|
|
293
|
-
});
|
|
294
|
-
it('should return false for completed WU (legacy status)', () => {
|
|
295
|
-
const wu = {
|
|
296
|
-
id: 'WU-100',
|
|
297
|
-
status: 'completed',
|
|
298
|
-
created: '2025-01-01',
|
|
299
|
-
completed: '2025-01-10',
|
|
300
|
-
};
|
|
301
|
-
expect(isWuStale(wu, { staleDaysInProgress: 7, staleDaysReady: 30 })).toBe(false);
|
|
302
|
-
});
|
|
303
|
-
it('should handle backlog status (legacy ready)', () => {
|
|
304
|
-
const wu = {
|
|
305
|
-
id: 'WU-100',
|
|
306
|
-
status: 'backlog',
|
|
307
|
-
created: '2025-12-20', // 36 days old
|
|
308
|
-
};
|
|
309
|
-
expect(isWuStale(wu, { staleDaysReady: 30 })).toBe(true);
|
|
310
|
-
});
|
|
311
|
-
it('should handle todo status (legacy ready)', () => {
|
|
312
|
-
const wu = {
|
|
313
|
-
id: 'WU-100',
|
|
314
|
-
status: 'todo',
|
|
315
|
-
created: '2025-12-20', // 36 days old
|
|
316
|
-
};
|
|
317
|
-
expect(isWuStale(wu, { staleDaysReady: 30 })).toBe(true);
|
|
318
|
-
});
|
|
319
|
-
it('should return false when no date is available', () => {
|
|
320
|
-
const wu = {
|
|
321
|
-
id: 'WU-100',
|
|
322
|
-
status: 'in_progress',
|
|
323
|
-
};
|
|
324
|
-
expect(isWuStale(wu, { staleDaysInProgress: 7 })).toBe(false);
|
|
325
|
-
});
|
|
326
|
-
it('should return false for unknown status', () => {
|
|
327
|
-
const wu = {
|
|
328
|
-
id: 'WU-100',
|
|
329
|
-
status: 'unknown',
|
|
330
|
-
created: '2025-01-01', // Very old
|
|
331
|
-
};
|
|
332
|
-
expect(isWuStale(wu, { staleDaysInProgress: 7, staleDaysReady: 30 })).toBe(false);
|
|
333
|
-
});
|
|
334
|
-
it('should use default threshold when not specified', () => {
|
|
335
|
-
const wu = {
|
|
336
|
-
id: 'WU-100',
|
|
337
|
-
status: 'in_progress',
|
|
338
|
-
created: '2026-01-10', // 15 days old
|
|
339
|
-
};
|
|
340
|
-
// Default is 7 days, so 15 days is stale
|
|
341
|
-
expect(isWuStale(wu, {})).toBe(true);
|
|
342
|
-
});
|
|
343
|
-
});
|
|
344
|
-
describe('isWuArchivable - additional edge cases', () => {
|
|
345
|
-
beforeEach(() => {
|
|
346
|
-
vi.useFakeTimers();
|
|
347
|
-
vi.setSystemTime(new Date('2026-01-25T12:00:00Z'));
|
|
348
|
-
});
|
|
349
|
-
afterEach(() => {
|
|
350
|
-
vi.useRealTimers();
|
|
351
|
-
});
|
|
352
|
-
it('should return true for completed WU (legacy done status)', () => {
|
|
353
|
-
const wu = {
|
|
354
|
-
id: 'WU-100',
|
|
355
|
-
status: 'completed',
|
|
356
|
-
created: '2025-01-01',
|
|
357
|
-
completed: '2025-11-20', // 66 days ago
|
|
358
|
-
};
|
|
359
|
-
expect(isWuArchivable(wu, { archiveDaysDone: 60 })).toBe(true);
|
|
360
|
-
});
|
|
361
|
-
it('should use default threshold when not specified', () => {
|
|
362
|
-
const wu = {
|
|
363
|
-
id: 'WU-100',
|
|
364
|
-
status: 'done',
|
|
365
|
-
created: '2025-01-01',
|
|
366
|
-
completed: '2025-10-20', // 97 days ago
|
|
367
|
-
};
|
|
368
|
-
// Default is 90 days, so 97 days is archivable
|
|
369
|
-
expect(isWuArchivable(wu, {})).toBe(true);
|
|
370
|
-
});
|
|
371
|
-
it('should return false when completed date is invalid', () => {
|
|
372
|
-
const wu = {
|
|
373
|
-
id: 'WU-100',
|
|
374
|
-
status: 'done',
|
|
375
|
-
created: '2025-01-01',
|
|
376
|
-
completed: 'invalid-date',
|
|
377
|
-
};
|
|
378
|
-
expect(isWuArchivable(wu, { archiveDaysDone: 60 })).toBe(false);
|
|
379
|
-
});
|
|
380
|
-
});
|
|
381
|
-
describe('parseBacklogPruneArgs - additional edge cases', () => {
|
|
382
|
-
it('should handle -h flag as help', () => {
|
|
383
|
-
const args = parseBacklogPruneArgs(['node', 'backlog-prune', '-h']);
|
|
384
|
-
expect(args.help).toBe(true);
|
|
385
|
-
});
|
|
386
|
-
it('should handle missing value for --stale-days-in-progress', () => {
|
|
387
|
-
const args = parseBacklogPruneArgs(['node', 'backlog-prune', '--stale-days-in-progress']);
|
|
388
|
-
// Should use default when no value follows
|
|
389
|
-
expect(args.staleDaysInProgress).toBe(BACKLOG_PRUNE_DEFAULTS.staleDaysInProgress);
|
|
390
|
-
});
|
|
391
|
-
it('should handle missing value for --stale-days-ready', () => {
|
|
392
|
-
const args = parseBacklogPruneArgs(['node', 'backlog-prune', '--stale-days-ready']);
|
|
393
|
-
expect(args.staleDaysReady).toBe(BACKLOG_PRUNE_DEFAULTS.staleDaysReady);
|
|
394
|
-
});
|
|
395
|
-
it('should handle missing value for --archive-days', () => {
|
|
396
|
-
const args = parseBacklogPruneArgs(['node', 'backlog-prune', '--archive-days']);
|
|
397
|
-
expect(args.archiveDaysDone).toBe(BACKLOG_PRUNE_DEFAULTS.archiveDaysDone);
|
|
398
|
-
});
|
|
399
|
-
it('should handle multiple flags combined', () => {
|
|
400
|
-
const args = parseBacklogPruneArgs([
|
|
401
|
-
'node',
|
|
402
|
-
'backlog-prune',
|
|
403
|
-
'--execute',
|
|
404
|
-
'--stale-days-in-progress',
|
|
405
|
-
'10',
|
|
406
|
-
'--stale-days-ready',
|
|
407
|
-
'20',
|
|
408
|
-
'--archive-days',
|
|
409
|
-
'45',
|
|
410
|
-
]);
|
|
411
|
-
expect(args.dryRun).toBe(false);
|
|
412
|
-
expect(args.staleDaysInProgress).toBe(10);
|
|
413
|
-
expect(args.staleDaysReady).toBe(20);
|
|
414
|
-
expect(args.archiveDaysDone).toBe(45);
|
|
415
|
-
});
|
|
416
|
-
it('should override with --dry-run after --execute', () => {
|
|
417
|
-
const args = parseBacklogPruneArgs(['node', 'backlog-prune', '--execute', '--dry-run']);
|
|
418
|
-
expect(args.dryRun).toBe(true);
|
|
419
|
-
});
|
|
420
|
-
});
|
|
421
|
-
describe('loadAllWus', () => {
|
|
422
|
-
it('should be a function', () => {
|
|
423
|
-
expect(typeof loadAllWus).toBe('function');
|
|
424
|
-
});
|
|
425
|
-
it('should return an array', () => {
|
|
426
|
-
// This will load from the actual WU directory in the test environment
|
|
427
|
-
const result = loadAllWus();
|
|
428
|
-
expect(Array.isArray(result)).toBe(true);
|
|
429
|
-
});
|
|
430
|
-
it('should return WuPruneInfo objects with required fields', () => {
|
|
431
|
-
const result = loadAllWus();
|
|
432
|
-
// All returned items should have at least id and status
|
|
433
|
-
for (const wu of result) {
|
|
434
|
-
expect(wu).toHaveProperty('id');
|
|
435
|
-
expect(wu).toHaveProperty('status');
|
|
436
|
-
}
|
|
437
|
-
});
|
|
438
|
-
});
|
|
439
|
-
describe('tagStaleWu', () => {
|
|
440
|
-
it('should be a function', () => {
|
|
441
|
-
expect(typeof tagStaleWu).toBe('function');
|
|
442
|
-
});
|
|
443
|
-
it('should log in dry-run mode without modifying files', () => {
|
|
444
|
-
const wu = {
|
|
445
|
-
id: 'WU-TEST-999',
|
|
446
|
-
status: 'in_progress',
|
|
447
|
-
created: '2026-01-01',
|
|
448
|
-
};
|
|
449
|
-
// In dry-run mode, it should just log
|
|
450
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
451
|
-
tagStaleWu(wu, true);
|
|
452
|
-
expect(consoleSpy).toHaveBeenCalled();
|
|
453
|
-
consoleSpy.mockRestore();
|
|
454
|
-
});
|
|
455
|
-
});
|
|
456
|
-
describe('printHelp', () => {
|
|
457
|
-
it('should be a function', () => {
|
|
458
|
-
expect(typeof printHelp).toBe('function');
|
|
459
|
-
});
|
|
460
|
-
it('should print help text to console', () => {
|
|
461
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
462
|
-
printHelp();
|
|
463
|
-
expect(consoleSpy).toHaveBeenCalled();
|
|
464
|
-
consoleSpy.mockRestore();
|
|
465
|
-
});
|
|
466
|
-
it('should include usage information', () => {
|
|
467
|
-
let output = '';
|
|
468
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation((msg) => {
|
|
469
|
-
output += msg;
|
|
470
|
-
});
|
|
471
|
-
printHelp();
|
|
472
|
-
expect(output).toContain('backlog:prune');
|
|
473
|
-
expect(output).toContain('--execute');
|
|
474
|
-
expect(output).toContain('--dry-run');
|
|
475
|
-
consoleSpy.mockRestore();
|
|
476
|
-
});
|
|
477
|
-
});
|
|
478
|
-
});
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for CLI entry point error handling
|
|
3
|
-
*
|
|
4
|
-
* Verifies that runCLI wrapper properly:
|
|
5
|
-
* - Catches async errors from main()
|
|
6
|
-
* - Logs error messages to stderr
|
|
7
|
-
* - Exits with EXIT_CODES.ERROR on failure
|
|
8
|
-
*
|
|
9
|
-
* WU-1071: Also verifies that CLI entry points use import.meta.main pattern
|
|
10
|
-
* instead of the broken process.argv[1] === fileURLToPath(import.meta.url) pattern.
|
|
11
|
-
*/
|
|
12
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
13
|
-
import { readFileSync, readdirSync } from 'node:fs';
|
|
14
|
-
import path from 'node:path';
|
|
15
|
-
import { runCLI } from '../cli-entry-point.js';
|
|
16
|
-
import { EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
|
|
17
|
-
describe('runCLI', () => {
|
|
18
|
-
let mockExit;
|
|
19
|
-
let mockConsoleError;
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
mockExit = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
22
|
-
mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
23
|
-
});
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
mockExit.mockRestore();
|
|
26
|
-
mockConsoleError.mockRestore();
|
|
27
|
-
});
|
|
28
|
-
it('should call main() and do nothing on success', async () => {
|
|
29
|
-
const main = vi.fn().mockResolvedValue(undefined);
|
|
30
|
-
await runCLI(main);
|
|
31
|
-
expect(main).toHaveBeenCalledOnce();
|
|
32
|
-
expect(mockExit).not.toHaveBeenCalled();
|
|
33
|
-
expect(mockConsoleError).not.toHaveBeenCalled();
|
|
34
|
-
});
|
|
35
|
-
it('should catch errors and exit with ERROR code', async () => {
|
|
36
|
-
const error = new Error('Test error message');
|
|
37
|
-
const main = vi.fn().mockRejectedValue(error);
|
|
38
|
-
await runCLI(main);
|
|
39
|
-
expect(main).toHaveBeenCalledOnce();
|
|
40
|
-
expect(mockConsoleError).toHaveBeenCalledWith('Test error message');
|
|
41
|
-
expect(mockExit).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
42
|
-
});
|
|
43
|
-
it('should handle errors without message property', async () => {
|
|
44
|
-
const main = vi.fn().mockRejectedValue('string error');
|
|
45
|
-
await runCLI(main);
|
|
46
|
-
expect(mockConsoleError).toHaveBeenCalledWith('string error');
|
|
47
|
-
expect(mockExit).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
48
|
-
});
|
|
49
|
-
it('should handle null/undefined errors', async () => {
|
|
50
|
-
const main = vi.fn().mockRejectedValue(null);
|
|
51
|
-
await runCLI(main);
|
|
52
|
-
expect(mockConsoleError).toHaveBeenCalledWith('Unknown error');
|
|
53
|
-
expect(mockExit).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
/**
|
|
57
|
-
* WU-1071/WU-1181: Verify CLI entry points use import.meta.main pattern
|
|
58
|
-
*
|
|
59
|
-
* The old pattern `process.argv[1] === fileURLToPath(import.meta.url)` fails with
|
|
60
|
-
* pnpm symlinks because process.argv[1] is the symlink path but import.meta.url
|
|
61
|
-
* resolves to the real path - they never match so main() is never called.
|
|
62
|
-
*
|
|
63
|
-
* The fix is to use `import.meta.main` (Node.js 22.16.0+ built-in) which correctly
|
|
64
|
-
* handles symlinks.
|
|
65
|
-
*
|
|
66
|
-
* WU-1181: Extended to validate ALL CLI files with entry guards, not just a subset.
|
|
67
|
-
*/
|
|
68
|
-
describe('WU-1071/WU-1181: CLI entry point patterns', () => {
|
|
69
|
-
// Files that should NOT be checked for entry guards
|
|
70
|
-
// (helper modules, index files, tests, or files without CLI entry points)
|
|
71
|
-
const EXCLUDED_FILES = new Set([
|
|
72
|
-
'cli-entry-point.ts', // Helper module, not a CLI entry point itself
|
|
73
|
-
'index.ts', // Re-exports only
|
|
74
|
-
'merge-block.ts', // Not a CLI entry point (no main guard needed)
|
|
75
|
-
'wu-done-check.ts', // Not a CLI entry point (no main guard needed)
|
|
76
|
-
'wu-spawn-completion.ts', // Not a CLI entry point (helper module)
|
|
77
|
-
'agent-session.ts', // Not a CLI entry point (helper module)
|
|
78
|
-
'agent-session-end.ts', // Not a CLI entry point (helper module)
|
|
79
|
-
'agent-log-issue.ts', // Not a CLI entry point (helper module)
|
|
80
|
-
'orchestrate-init-status.ts', // Not a CLI entry point (helper module)
|
|
81
|
-
'orchestrate-initiative.ts', // Not a CLI entry point (helper module)
|
|
82
|
-
'orchestrate-monitor.ts', // Not a CLI entry point (helper module)
|
|
83
|
-
'initiative-edit.ts', // Not a CLI entry point (no main guard)
|
|
84
|
-
'wu-block.ts', // Not a CLI entry point (no main guard)
|
|
85
|
-
'wu-unblock.ts', // Not a CLI entry point (no main guard)
|
|
86
|
-
'wu-release.ts', // Not a CLI entry point (no main guard)
|
|
87
|
-
'wu-delete.ts', // Not a CLI entry point (no main guard)
|
|
88
|
-
'init.ts', // Not a CLI entry point (no main guard)
|
|
89
|
-
]);
|
|
90
|
-
// Old broken pattern that fails with pnpm symlinks
|
|
91
|
-
const OLD_BROKEN_PATTERN = /if\s*\(\s*process\.argv\[1\]\s*===\s*fileURLToPath\(import\.meta\.url\)\s*\)/;
|
|
92
|
-
// New working pattern using import.meta.main
|
|
93
|
-
const NEW_WORKING_PATTERN = /if\s*\(\s*import\.meta\.main\s*\)/;
|
|
94
|
-
/**
|
|
95
|
-
* Discovers all CLI files with entry guards by scanning the src directory.
|
|
96
|
-
* A file is considered to have an entry guard if it contains either:
|
|
97
|
-
* - The old broken pattern: if (process.argv[1] === fileURLToPath(import.meta.url))
|
|
98
|
-
* - The new working pattern: if (import.meta.main)
|
|
99
|
-
*/
|
|
100
|
-
function discoverCLIFilesWithEntryGuards() {
|
|
101
|
-
const srcDir = path.resolve(__dirname, '..');
|
|
102
|
-
const files = readdirSync(srcDir).filter((f) => f.endsWith('.ts') && !f.endsWith('.test.ts') && !EXCLUDED_FILES.has(f));
|
|
103
|
-
return files.filter((file) => {
|
|
104
|
-
const content = readFileSync(path.join(srcDir, file), 'utf-8');
|
|
105
|
-
return OLD_BROKEN_PATTERN.test(content) || NEW_WORKING_PATTERN.test(content);
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
it('should discover all CLI files with entry guards', () => {
|
|
109
|
-
const cliFiles = discoverCLIFilesWithEntryGuards();
|
|
110
|
-
// WU-1181: There should be a significant number of CLI files with entry guards
|
|
111
|
-
// This test ensures we're actually discovering files, not returning an empty list
|
|
112
|
-
expect(cliFiles.length).toBeGreaterThan(40);
|
|
113
|
-
});
|
|
114
|
-
it('should use import.meta.main instead of process.argv[1] comparison in ALL CLI files', () => {
|
|
115
|
-
const srcDir = path.resolve(__dirname, '..');
|
|
116
|
-
const cliFiles = discoverCLIFilesWithEntryGuards();
|
|
117
|
-
const errors = [];
|
|
118
|
-
for (const file of cliFiles) {
|
|
119
|
-
const filePath = path.join(srcDir, file);
|
|
120
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
121
|
-
// Should NOT have old broken pattern
|
|
122
|
-
if (OLD_BROKEN_PATTERN.test(content)) {
|
|
123
|
-
errors.push(`${file} uses the old broken pattern (process.argv[1] === fileURLToPath)`);
|
|
124
|
-
}
|
|
125
|
-
// Should have new working pattern
|
|
126
|
-
if (!NEW_WORKING_PATTERN.test(content)) {
|
|
127
|
-
errors.push(`${file} does not use import.meta.main pattern`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
if (errors.length > 0) {
|
|
131
|
-
expect.fail(`Entry point pattern violations:\n${errors.join('\n')}`);
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
it('should not have unused fileURLToPath imports in CLI files with entry guards', () => {
|
|
135
|
-
const srcDir = path.resolve(__dirname, '..');
|
|
136
|
-
const cliFiles = discoverCLIFilesWithEntryGuards();
|
|
137
|
-
const errors = [];
|
|
138
|
-
for (const file of cliFiles) {
|
|
139
|
-
const filePath = path.join(srcDir, file);
|
|
140
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
141
|
-
// If the file imports fileURLToPath, it should actually use it somewhere
|
|
142
|
-
// (not just for the now-removed entry guard pattern)
|
|
143
|
-
const hasFileURLToPathImport = /import\s*{[^}]*fileURLToPath[^}]*}\s*from\s*['"]node:url['"]/.test(content);
|
|
144
|
-
const usesFileURLToPath = /fileURLToPath\(/.test(content);
|
|
145
|
-
if (hasFileURLToPathImport && !usesFileURLToPath) {
|
|
146
|
-
errors.push(`${file} imports fileURLToPath but does not use it - remove unused import`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
if (errors.length > 0) {
|
|
150
|
-
expect.fail(`Unused fileURLToPath imports:\n${errors.join('\n')}`);
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
it('cli-entry-point.ts JSDoc should document import.meta.main pattern', () => {
|
|
154
|
-
const srcDir = path.resolve(__dirname, '..');
|
|
155
|
-
const cliEntryPointPath = path.join(srcDir, 'cli-entry-point.ts');
|
|
156
|
-
const content = readFileSync(cliEntryPointPath, 'utf-8');
|
|
157
|
-
// JSDoc example should show import.meta.main pattern, not the old one
|
|
158
|
-
expect(content.includes('import.meta.main'), 'cli-entry-point.ts JSDoc should mention import.meta.main').toBe(true);
|
|
159
|
-
});
|
|
160
|
-
});
|