@lumenflow/cli 1.5.0 → 2.0.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.
Files changed (41) hide show
  1. package/dist/__tests__/backlog-prune.test.js +478 -0
  2. package/dist/__tests__/deps-operations.test.js +206 -0
  3. package/dist/__tests__/file-operations.test.js +906 -0
  4. package/dist/__tests__/git-operations.test.js +668 -0
  5. package/dist/__tests__/guards-validation.test.js +416 -0
  6. package/dist/__tests__/init-plan.test.js +340 -0
  7. package/dist/__tests__/lumenflow-upgrade.test.js +107 -0
  8. package/dist/__tests__/metrics-cli.test.js +619 -0
  9. package/dist/__tests__/rotate-progress.test.js +127 -0
  10. package/dist/__tests__/session-coordinator.test.js +109 -0
  11. package/dist/__tests__/state-bootstrap.test.js +432 -0
  12. package/dist/__tests__/trace-gen.test.js +115 -0
  13. package/dist/backlog-prune.js +299 -0
  14. package/dist/deps-add.js +215 -0
  15. package/dist/deps-remove.js +94 -0
  16. package/dist/file-delete.js +236 -0
  17. package/dist/file-edit.js +247 -0
  18. package/dist/file-read.js +197 -0
  19. package/dist/file-write.js +220 -0
  20. package/dist/git-branch.js +187 -0
  21. package/dist/git-diff.js +177 -0
  22. package/dist/git-log.js +230 -0
  23. package/dist/git-status.js +208 -0
  24. package/dist/guard-locked.js +169 -0
  25. package/dist/guard-main-branch.js +202 -0
  26. package/dist/guard-worktree-commit.js +160 -0
  27. package/dist/init-plan.js +337 -0
  28. package/dist/lumenflow-upgrade.js +178 -0
  29. package/dist/metrics-cli.js +433 -0
  30. package/dist/rotate-progress.js +247 -0
  31. package/dist/session-coordinator.js +300 -0
  32. package/dist/state-bootstrap.js +307 -0
  33. package/dist/trace-gen.js +331 -0
  34. package/dist/validate-agent-skills.js +218 -0
  35. package/dist/validate-agent-sync.js +148 -0
  36. package/dist/validate-backlog-sync.js +152 -0
  37. package/dist/validate-skills-spec.js +206 -0
  38. package/dist/validate.js +230 -0
  39. package/dist/wu-recover.js +329 -0
  40. package/dist/wu-status.js +188 -0
  41. package/package.json +37 -7
@@ -0,0 +1,619 @@
1
+ /**
2
+ * @file metrics-cli.test.ts
3
+ * @description Tests for unified metrics CLI with subcommands (WU-1110)
4
+ *
5
+ * TDD: RED phase - Write tests before implementation.
6
+ *
7
+ * Acceptance criteria:
8
+ * - metrics-cli.ts exists with subcommands (lanes, dora, flow)
9
+ * - All metrics/application modules migrated
10
+ * - Existing tests ported to Vitest
11
+ * - >80% coverage
12
+ */
13
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
14
+ import { existsSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
18
+ describe('metrics-cli module', () => {
19
+ it('should have the CLI source file', () => {
20
+ const srcPath = join(__dirname, '../metrics-cli.ts');
21
+ expect(existsSync(srcPath)).toBe(true);
22
+ });
23
+ it('should be buildable (dist file exists after build)', () => {
24
+ const distPath = join(__dirname, '../../dist/metrics-cli.js');
25
+ expect(existsSync(distPath)).toBe(true);
26
+ });
27
+ });
28
+ describe('metrics-cli subcommands', () => {
29
+ // Mock console output
30
+ let consoleSpy;
31
+ let consoleErrorSpy;
32
+ beforeEach(() => {
33
+ consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
34
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
35
+ });
36
+ afterEach(() => {
37
+ consoleSpy.mockRestore();
38
+ consoleErrorSpy.mockRestore();
39
+ vi.resetModules();
40
+ });
41
+ describe('parseCommand', () => {
42
+ it('should export parseCommand function', async () => {
43
+ const { parseCommand } = await import('../metrics-cli.js');
44
+ expect(typeof parseCommand).toBe('function');
45
+ });
46
+ it('should parse "lanes" subcommand', async () => {
47
+ const { parseCommand } = await import('../metrics-cli.js');
48
+ const result = parseCommand(['node', 'metrics', 'lanes']);
49
+ expect(result.subcommand).toBe('lanes');
50
+ });
51
+ it('should parse "dora" subcommand', async () => {
52
+ const { parseCommand } = await import('../metrics-cli.js');
53
+ const result = parseCommand(['node', 'metrics', 'dora']);
54
+ expect(result.subcommand).toBe('dora');
55
+ });
56
+ it('should parse "flow" subcommand', async () => {
57
+ const { parseCommand } = await import('../metrics-cli.js');
58
+ const result = parseCommand(['node', 'metrics', 'flow']);
59
+ expect(result.subcommand).toBe('flow');
60
+ });
61
+ it('should default to "all" when no subcommand given', async () => {
62
+ const { parseCommand } = await import('../metrics-cli.js');
63
+ const result = parseCommand(['node', 'metrics']);
64
+ expect(result.subcommand).toBe('all');
65
+ });
66
+ it('should parse --days option', async () => {
67
+ const { parseCommand } = await import('../metrics-cli.js');
68
+ const result = parseCommand(['node', 'metrics', 'dora', '--days', '30']);
69
+ expect(result.days).toBe(30);
70
+ });
71
+ it('should parse --format option', async () => {
72
+ const { parseCommand } = await import('../metrics-cli.js');
73
+ const result = parseCommand(['node', 'metrics', 'lanes', '--format', 'table']);
74
+ expect(result.format).toBe('table');
75
+ });
76
+ it('should parse --output option', async () => {
77
+ const { parseCommand } = await import('../metrics-cli.js');
78
+ const result = parseCommand(['node', 'metrics', '--output', 'custom.json']);
79
+ expect(result.output).toBe('custom.json');
80
+ });
81
+ it('should parse --dry-run flag', async () => {
82
+ const { parseCommand } = await import('../metrics-cli.js');
83
+ const result = parseCommand(['node', 'metrics', '--dry-run']);
84
+ expect(result.dryRun).toBe(true);
85
+ });
86
+ });
87
+ describe('MetricsCommandResult type', () => {
88
+ it('should have proper subcommand type', async () => {
89
+ const { parseCommand } = await import('../metrics-cli.js');
90
+ const result = parseCommand(['node', 'metrics', 'dora']);
91
+ // Type check: subcommand should be one of 'lanes' | 'dora' | 'flow' | 'all'
92
+ expect(['lanes', 'dora', 'flow', 'all']).toContain(result.subcommand);
93
+ });
94
+ });
95
+ describe('runLanesSubcommand', () => {
96
+ it('should export runLanesSubcommand function', async () => {
97
+ const { runLanesSubcommand } = await import('../metrics-cli.js');
98
+ expect(typeof runLanesSubcommand).toBe('function');
99
+ });
100
+ });
101
+ describe('runDoraSubcommand', () => {
102
+ it('should export runDoraSubcommand function', async () => {
103
+ const { runDoraSubcommand } = await import('../metrics-cli.js');
104
+ expect(typeof runDoraSubcommand).toBe('function');
105
+ });
106
+ });
107
+ describe('runFlowSubcommand', () => {
108
+ it('should export runFlowSubcommand function', async () => {
109
+ const { runFlowSubcommand } = await import('../metrics-cli.js');
110
+ expect(typeof runFlowSubcommand).toBe('function');
111
+ });
112
+ });
113
+ describe('runAllSubcommand', () => {
114
+ it('should export runAllSubcommand function', async () => {
115
+ const { runAllSubcommand } = await import('../metrics-cli.js');
116
+ expect(typeof runAllSubcommand).toBe('function');
117
+ });
118
+ });
119
+ });
120
+ describe('metrics-cli integration', () => {
121
+ describe('lanes subcommand', () => {
122
+ it('should calculate lane health from WU data', async () => {
123
+ const { calculateLaneHealthFromWUs } = await import('../metrics-cli.js');
124
+ const wuMetrics = [
125
+ { id: 'WU-1', title: 'A', lane: 'Framework: Core', status: 'done' },
126
+ { id: 'WU-2', title: 'B', lane: 'Framework: Core', status: 'in_progress' },
127
+ { id: 'WU-3', title: 'C', lane: 'Framework: CLI', status: 'blocked' },
128
+ ];
129
+ const result = calculateLaneHealthFromWUs(wuMetrics);
130
+ expect(result.lanes).toBeDefined();
131
+ expect(result.lanes.length).toBeGreaterThan(0);
132
+ expect(result.totalActive).toBeGreaterThanOrEqual(0);
133
+ expect(result.totalBlocked).toBe(1);
134
+ });
135
+ });
136
+ describe('dora subcommand', () => {
137
+ it('should calculate DORA metrics from commits and WUs', async () => {
138
+ const { calculateDoraFromData } = await import('../metrics-cli.js');
139
+ const commits = [
140
+ { hash: 'a1', timestamp: new Date('2026-01-02'), message: 'feat: add feature' },
141
+ { hash: 'a2', timestamp: new Date('2026-01-03'), message: 'fix: bug fix' },
142
+ ];
143
+ const wuMetrics = [
144
+ {
145
+ id: 'WU-1',
146
+ title: 'A',
147
+ lane: 'Ops',
148
+ status: 'done',
149
+ cycleTimeHours: 12,
150
+ },
151
+ ];
152
+ const skipGatesEntries = [];
153
+ const weekStart = new Date('2026-01-01');
154
+ const weekEnd = new Date('2026-01-07');
155
+ const result = calculateDoraFromData({
156
+ commits,
157
+ wuMetrics,
158
+ skipGatesEntries,
159
+ weekStart,
160
+ weekEnd,
161
+ });
162
+ expect(result.deploymentFrequency).toBeDefined();
163
+ expect(result.leadTimeForChanges).toBeDefined();
164
+ expect(result.changeFailureRate).toBeDefined();
165
+ expect(result.meanTimeToRecovery).toBeDefined();
166
+ });
167
+ });
168
+ describe('flow subcommand', () => {
169
+ it('should calculate flow state from WU data', async () => {
170
+ const { calculateFlowFromWUs } = await import('../metrics-cli.js');
171
+ const wuMetrics = [
172
+ { id: 'WU-1', title: 'A', lane: 'Ops', status: 'ready' },
173
+ { id: 'WU-2', title: 'B', lane: 'Ops', status: 'in_progress' },
174
+ { id: 'WU-3', title: 'C', lane: 'Ops', status: 'blocked' },
175
+ { id: 'WU-4', title: 'D', lane: 'Ops', status: 'done' },
176
+ ];
177
+ const result = calculateFlowFromWUs(wuMetrics);
178
+ expect(result.ready).toBe(1);
179
+ expect(result.inProgress).toBe(1);
180
+ expect(result.blocked).toBe(1);
181
+ expect(result.done).toBe(1);
182
+ expect(result.totalActive).toBe(3); // ready + in_progress + blocked
183
+ });
184
+ });
185
+ });
186
+ describe('metrics-cli formatters', () => {
187
+ describe('formatLanesOutput', () => {
188
+ it('should export formatLanesOutput function', async () => {
189
+ const { formatLanesOutput } = await import('../metrics-cli.js');
190
+ expect(typeof formatLanesOutput).toBe('function');
191
+ });
192
+ it('should format lanes as JSON by default', async () => {
193
+ const { formatLanesOutput } = await import('../metrics-cli.js');
194
+ const lanes = {
195
+ lanes: [
196
+ {
197
+ lane: 'Framework: Core',
198
+ wusCompleted: 5,
199
+ wusInProgress: 2,
200
+ wusBlocked: 0,
201
+ averageCycleTimeHours: 24,
202
+ medianCycleTimeHours: 20,
203
+ status: 'healthy',
204
+ },
205
+ ],
206
+ totalActive: 7,
207
+ totalBlocked: 0,
208
+ totalCompleted: 5,
209
+ };
210
+ const result = formatLanesOutput(lanes, 'json');
211
+ expect(() => JSON.parse(result)).not.toThrow();
212
+ });
213
+ it('should format lanes as table', async () => {
214
+ const { formatLanesOutput } = await import('../metrics-cli.js');
215
+ const lanes = {
216
+ lanes: [
217
+ {
218
+ lane: 'Framework: Core',
219
+ wusCompleted: 5,
220
+ wusInProgress: 2,
221
+ wusBlocked: 0,
222
+ averageCycleTimeHours: 24,
223
+ medianCycleTimeHours: 20,
224
+ status: 'healthy',
225
+ },
226
+ ],
227
+ totalActive: 7,
228
+ totalBlocked: 0,
229
+ totalCompleted: 5,
230
+ };
231
+ const result = formatLanesOutput(lanes, 'table');
232
+ expect(result).toContain('Framework: Core');
233
+ expect(result).toContain('[ok]'); // [ok] = healthy, [!] = at-risk, [x] = blocked
234
+ });
235
+ });
236
+ describe('formatDoraOutput', () => {
237
+ it('should export formatDoraOutput function', async () => {
238
+ const { formatDoraOutput } = await import('../metrics-cli.js');
239
+ expect(typeof formatDoraOutput).toBe('function');
240
+ });
241
+ it('should format DORA as JSON', async () => {
242
+ const { formatDoraOutput } = await import('../metrics-cli.js');
243
+ const dora = {
244
+ deploymentFrequency: { deploysPerWeek: 5, status: 'elite' },
245
+ leadTimeForChanges: {
246
+ averageHours: 12,
247
+ medianHours: 10,
248
+ p90Hours: 20,
249
+ status: 'elite',
250
+ },
251
+ changeFailureRate: {
252
+ failurePercentage: 5,
253
+ totalDeployments: 100,
254
+ failures: 5,
255
+ status: 'elite',
256
+ },
257
+ meanTimeToRecovery: { averageHours: 1, incidents: 2, status: 'elite' },
258
+ };
259
+ const result = formatDoraOutput(dora, 'json');
260
+ expect(() => JSON.parse(result)).not.toThrow();
261
+ const parsed = JSON.parse(result);
262
+ expect(parsed.deploymentFrequency.deploysPerWeek).toBe(5);
263
+ });
264
+ it('should format DORA as table', async () => {
265
+ const { formatDoraOutput } = await import('../metrics-cli.js');
266
+ const dora = {
267
+ deploymentFrequency: { deploysPerWeek: 5, status: 'elite' },
268
+ leadTimeForChanges: {
269
+ averageHours: 12,
270
+ medianHours: 10,
271
+ p90Hours: 20,
272
+ status: 'elite',
273
+ },
274
+ changeFailureRate: {
275
+ failurePercentage: 5,
276
+ totalDeployments: 100,
277
+ failures: 5,
278
+ status: 'elite',
279
+ },
280
+ meanTimeToRecovery: { averageHours: 1, incidents: 2, status: 'elite' },
281
+ };
282
+ const result = formatDoraOutput(dora, 'table');
283
+ expect(result).toContain('DORA METRICS');
284
+ expect(result).toContain('Deployment Frequency');
285
+ expect(result).toContain('elite');
286
+ expect(result).toContain('Lead Time');
287
+ expect(result).toContain('MTTR');
288
+ });
289
+ });
290
+ describe('formatFlowOutput', () => {
291
+ it('should export formatFlowOutput function', async () => {
292
+ const { formatFlowOutput } = await import('../metrics-cli.js');
293
+ expect(typeof formatFlowOutput).toBe('function');
294
+ });
295
+ it('should format flow as JSON', async () => {
296
+ const { formatFlowOutput } = await import('../metrics-cli.js');
297
+ const flow = {
298
+ ready: 5,
299
+ inProgress: 3,
300
+ blocked: 1,
301
+ waiting: 0,
302
+ done: 10,
303
+ totalActive: 9,
304
+ };
305
+ const result = formatFlowOutput(flow, 'json');
306
+ expect(() => JSON.parse(result)).not.toThrow();
307
+ const parsed = JSON.parse(result);
308
+ expect(parsed.ready).toBe(5);
309
+ expect(parsed.totalActive).toBe(9);
310
+ });
311
+ it('should format flow as table', async () => {
312
+ const { formatFlowOutput } = await import('../metrics-cli.js');
313
+ const flow = {
314
+ ready: 5,
315
+ inProgress: 3,
316
+ blocked: 1,
317
+ waiting: 0,
318
+ done: 10,
319
+ totalActive: 9,
320
+ };
321
+ const result = formatFlowOutput(flow, 'table');
322
+ expect(result).toContain('FLOW STATE');
323
+ expect(result).toContain('Ready: 5');
324
+ expect(result).toContain('In Progress: 3');
325
+ expect(result).toContain('Blocked: 1');
326
+ expect(result).toContain('Done: 10');
327
+ });
328
+ });
329
+ describe('formatLanesOutput edge cases', () => {
330
+ it('should handle at-risk status', async () => {
331
+ const { formatLanesOutput } = await import('../metrics-cli.js');
332
+ const lanes = {
333
+ lanes: [
334
+ {
335
+ lane: 'Framework: CLI',
336
+ wusCompleted: 2,
337
+ wusInProgress: 1,
338
+ wusBlocked: 1,
339
+ averageCycleTimeHours: 48,
340
+ medianCycleTimeHours: 40,
341
+ status: 'at-risk',
342
+ },
343
+ ],
344
+ totalActive: 4,
345
+ totalBlocked: 1,
346
+ totalCompleted: 2,
347
+ };
348
+ const result = formatLanesOutput(lanes, 'table');
349
+ expect(result).toContain('[!]'); // at-risk indicator
350
+ });
351
+ it('should handle blocked status', async () => {
352
+ const { formatLanesOutput } = await import('../metrics-cli.js');
353
+ const lanes = {
354
+ lanes: [
355
+ {
356
+ lane: 'Operations',
357
+ wusCompleted: 0,
358
+ wusInProgress: 0,
359
+ wusBlocked: 3,
360
+ averageCycleTimeHours: 0,
361
+ medianCycleTimeHours: 0,
362
+ status: 'blocked',
363
+ },
364
+ ],
365
+ totalActive: 3,
366
+ totalBlocked: 3,
367
+ totalCompleted: 0,
368
+ };
369
+ const result = formatLanesOutput(lanes, 'table');
370
+ expect(result).toContain('[x]'); // blocked indicator
371
+ });
372
+ it('should handle empty lanes array', async () => {
373
+ const { formatLanesOutput } = await import('../metrics-cli.js');
374
+ const lanes = {
375
+ lanes: [],
376
+ totalActive: 0,
377
+ totalBlocked: 0,
378
+ totalCompleted: 0,
379
+ };
380
+ const result = formatLanesOutput(lanes, 'table');
381
+ expect(result).toContain('LANE HEALTH');
382
+ expect(result).toContain('Total Active: 0');
383
+ });
384
+ });
385
+ });
386
+ describe('metrics-cli parseCommand edge cases', () => {
387
+ it('should handle invalid subcommand gracefully', async () => {
388
+ const { parseCommand } = await import('../metrics-cli.js');
389
+ const result = parseCommand(['node', 'metrics', 'invalid']);
390
+ // Invalid subcommand should default to 'all'
391
+ expect(result.subcommand).toBe('all');
392
+ });
393
+ it('should handle explicit "all" subcommand', async () => {
394
+ const { parseCommand } = await import('../metrics-cli.js');
395
+ const result = parseCommand(['node', 'metrics', 'all']);
396
+ expect(result.subcommand).toBe('all');
397
+ });
398
+ it('should use default days when not specified', async () => {
399
+ const { parseCommand } = await import('../metrics-cli.js');
400
+ const result = parseCommand(['node', 'metrics']);
401
+ expect(result.days).toBe(7);
402
+ });
403
+ it('should use default format when not specified', async () => {
404
+ const { parseCommand } = await import('../metrics-cli.js');
405
+ const result = parseCommand(['node', 'metrics']);
406
+ expect(result.format).toBe('json');
407
+ });
408
+ it('should use default output path when not specified', async () => {
409
+ const { parseCommand } = await import('../metrics-cli.js');
410
+ const result = parseCommand(['node', 'metrics']);
411
+ expect(result.output).toContain('.lumenflow/snapshots/metrics-latest.json');
412
+ });
413
+ it('should handle dryRun false by default', async () => {
414
+ const { parseCommand } = await import('../metrics-cli.js');
415
+ const result = parseCommand(['node', 'metrics']);
416
+ expect(result.dryRun).toBe(false);
417
+ });
418
+ });
419
+ describe('metrics-cli calculation functions', () => {
420
+ describe('calculateLaneHealthFromWUs', () => {
421
+ it('should handle empty WU list', async () => {
422
+ const { calculateLaneHealthFromWUs } = await import('../metrics-cli.js');
423
+ const result = calculateLaneHealthFromWUs([]);
424
+ expect(result.lanes).toEqual([]);
425
+ expect(result.totalActive).toBe(0);
426
+ });
427
+ it('should correctly count ready WUs as active', async () => {
428
+ const { calculateLaneHealthFromWUs } = await import('../metrics-cli.js');
429
+ const wuMetrics = [{ id: 'WU-1', title: 'A', lane: 'Test', status: 'ready' }];
430
+ const result = calculateLaneHealthFromWUs(wuMetrics);
431
+ expect(result.totalActive).toBe(1);
432
+ });
433
+ it('should correctly count waiting WUs as active', async () => {
434
+ const { calculateLaneHealthFromWUs } = await import('../metrics-cli.js');
435
+ const wuMetrics = [{ id: 'WU-1', title: 'A', lane: 'Test', status: 'waiting' }];
436
+ const result = calculateLaneHealthFromWUs(wuMetrics);
437
+ expect(result.totalActive).toBe(1);
438
+ });
439
+ });
440
+ describe('calculateDoraFromData', () => {
441
+ it('should handle empty commit list', async () => {
442
+ const { calculateDoraFromData } = await import('../metrics-cli.js');
443
+ const result = calculateDoraFromData({
444
+ commits: [],
445
+ wuMetrics: [],
446
+ skipGatesEntries: [],
447
+ weekStart: new Date('2026-01-01'),
448
+ weekEnd: new Date('2026-01-07'),
449
+ });
450
+ expect(result.deploymentFrequency.deploysPerWeek).toBe(0);
451
+ });
452
+ it('should handle skip gates entries', async () => {
453
+ const { calculateDoraFromData } = await import('../metrics-cli.js');
454
+ const commits = [
455
+ { hash: 'a1', timestamp: new Date('2026-01-02'), message: 'feat: a' },
456
+ { hash: 'a2', timestamp: new Date('2026-01-03'), message: 'feat: b' },
457
+ ];
458
+ const skipGatesEntries = [
459
+ { timestamp: new Date(), wuId: 'WU-1', reason: 'test', gate: 'lint' },
460
+ ];
461
+ const result = calculateDoraFromData({
462
+ commits,
463
+ wuMetrics: [],
464
+ skipGatesEntries,
465
+ weekStart: new Date('2026-01-01'),
466
+ weekEnd: new Date('2026-01-07'),
467
+ });
468
+ expect(result.changeFailureRate.failures).toBe(1);
469
+ });
470
+ });
471
+ describe('calculateFlowFromWUs', () => {
472
+ it('should handle all WU statuses', async () => {
473
+ const { calculateFlowFromWUs } = await import('../metrics-cli.js');
474
+ const wuMetrics = [
475
+ { id: 'WU-1', title: 'A', lane: 'Ops', status: 'ready' },
476
+ { id: 'WU-2', title: 'B', lane: 'Ops', status: 'in_progress' },
477
+ { id: 'WU-3', title: 'C', lane: 'Ops', status: 'blocked' },
478
+ { id: 'WU-4', title: 'D', lane: 'Ops', status: 'waiting' },
479
+ { id: 'WU-5', title: 'E', lane: 'Ops', status: 'done' },
480
+ ];
481
+ const result = calculateFlowFromWUs(wuMetrics);
482
+ expect(result.ready).toBe(1);
483
+ expect(result.inProgress).toBe(1);
484
+ expect(result.blocked).toBe(1);
485
+ expect(result.waiting).toBe(1);
486
+ expect(result.done).toBe(1);
487
+ expect(result.totalActive).toBe(4); // ready + in_progress + blocked + waiting
488
+ });
489
+ it('should handle empty WU list', async () => {
490
+ const { calculateFlowFromWUs } = await import('../metrics-cli.js');
491
+ const result = calculateFlowFromWUs([]);
492
+ expect(result.ready).toBe(0);
493
+ expect(result.totalActive).toBe(0);
494
+ });
495
+ });
496
+ });
497
+ describe('metrics-cli run* subcommand functions', () => {
498
+ let consoleSpy;
499
+ let consoleWarnSpy;
500
+ beforeEach(() => {
501
+ consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
502
+ consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
503
+ });
504
+ afterEach(() => {
505
+ consoleSpy.mockRestore();
506
+ consoleWarnSpy.mockRestore();
507
+ });
508
+ describe('runLanesSubcommand', () => {
509
+ it('should run lanes subcommand with dry-run', async () => {
510
+ const { runLanesSubcommand } = await import('../metrics-cli.js');
511
+ // Run with dry-run to avoid file writes
512
+ await runLanesSubcommand({
513
+ subcommand: 'lanes',
514
+ days: 7,
515
+ format: 'json',
516
+ output: '.lumenflow/snapshots/test.json',
517
+ dryRun: true,
518
+ });
519
+ // Verify console output was called
520
+ expect(consoleSpy).toHaveBeenCalled();
521
+ });
522
+ it('should run lanes subcommand with table format', async () => {
523
+ const { runLanesSubcommand } = await import('../metrics-cli.js');
524
+ await runLanesSubcommand({
525
+ subcommand: 'lanes',
526
+ days: 7,
527
+ format: 'table',
528
+ output: '.lumenflow/snapshots/test.json',
529
+ dryRun: true,
530
+ });
531
+ // Verify table format was used (contains LANE HEALTH header)
532
+ const calls = consoleSpy.mock.calls;
533
+ const output = calls.map((c) => String(c[0])).join('\n');
534
+ expect(output).toContain('LANE HEALTH');
535
+ });
536
+ });
537
+ describe('runDoraSubcommand', () => {
538
+ it('should run dora subcommand with dry-run', async () => {
539
+ const { runDoraSubcommand } = await import('../metrics-cli.js');
540
+ await runDoraSubcommand({
541
+ subcommand: 'dora',
542
+ days: 7,
543
+ format: 'json',
544
+ output: '.lumenflow/snapshots/test.json',
545
+ dryRun: true,
546
+ });
547
+ expect(consoleSpy).toHaveBeenCalled();
548
+ });
549
+ it('should run dora subcommand with custom days', async () => {
550
+ const { runDoraSubcommand } = await import('../metrics-cli.js');
551
+ await runDoraSubcommand({
552
+ subcommand: 'dora',
553
+ days: 30,
554
+ format: 'table',
555
+ output: '.lumenflow/snapshots/test.json',
556
+ dryRun: true,
557
+ });
558
+ // Verify output includes DORA METRICS header
559
+ const calls = consoleSpy.mock.calls;
560
+ const output = calls.map((c) => String(c[0])).join('\n');
561
+ expect(output).toContain('DORA METRICS');
562
+ });
563
+ });
564
+ describe('runFlowSubcommand', () => {
565
+ it('should run flow subcommand with dry-run', async () => {
566
+ const { runFlowSubcommand } = await import('../metrics-cli.js');
567
+ await runFlowSubcommand({
568
+ subcommand: 'flow',
569
+ days: 7,
570
+ format: 'json',
571
+ output: '.lumenflow/snapshots/test.json',
572
+ dryRun: true,
573
+ });
574
+ expect(consoleSpy).toHaveBeenCalled();
575
+ });
576
+ it('should run flow subcommand with table format', async () => {
577
+ const { runFlowSubcommand } = await import('../metrics-cli.js');
578
+ await runFlowSubcommand({
579
+ subcommand: 'flow',
580
+ days: 7,
581
+ format: 'table',
582
+ output: '.lumenflow/snapshots/test.json',
583
+ dryRun: true,
584
+ });
585
+ const calls = consoleSpy.mock.calls;
586
+ const output = calls.map((c) => String(c[0])).join('\n');
587
+ expect(output).toContain('FLOW STATE');
588
+ });
589
+ });
590
+ describe('runAllSubcommand', () => {
591
+ it('should run all subcommand with dry-run', async () => {
592
+ const { runAllSubcommand } = await import('../metrics-cli.js');
593
+ await runAllSubcommand({
594
+ subcommand: 'all',
595
+ days: 7,
596
+ format: 'json',
597
+ output: '.lumenflow/snapshots/test.json',
598
+ dryRun: true,
599
+ });
600
+ expect(consoleSpy).toHaveBeenCalled();
601
+ });
602
+ it('should run all subcommand with table format', async () => {
603
+ const { runAllSubcommand } = await import('../metrics-cli.js');
604
+ await runAllSubcommand({
605
+ subcommand: 'all',
606
+ days: 7,
607
+ format: 'table',
608
+ output: '.lumenflow/snapshots/test.json',
609
+ dryRun: true,
610
+ });
611
+ const calls = consoleSpy.mock.calls;
612
+ const output = calls.map((c) => String(c[0])).join('\n');
613
+ // Should contain all three sections
614
+ expect(output).toContain('DORA METRICS');
615
+ expect(output).toContain('LANE HEALTH');
616
+ expect(output).toContain('FLOW STATE');
617
+ });
618
+ });
619
+ });