@lumenflow/cli 1.6.0 → 2.1.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 +19 -0
- package/dist/__tests__/backlog-prune.test.js +478 -0
- package/dist/__tests__/deps-operations.test.js +206 -0
- package/dist/__tests__/file-operations.test.js +906 -0
- package/dist/__tests__/git-operations.test.js +668 -0
- package/dist/__tests__/guards-validation.test.js +416 -0
- package/dist/__tests__/init-plan.test.js +340 -0
- package/dist/__tests__/lumenflow-upgrade.test.js +107 -0
- package/dist/__tests__/metrics-cli.test.js +619 -0
- package/dist/__tests__/rotate-progress.test.js +127 -0
- package/dist/__tests__/session-coordinator.test.js +109 -0
- package/dist/__tests__/state-bootstrap.test.js +432 -0
- package/dist/__tests__/trace-gen.test.js +115 -0
- package/dist/backlog-prune.js +299 -0
- package/dist/deps-add.js +215 -0
- package/dist/deps-remove.js +94 -0
- package/dist/docs-sync.js +72 -326
- package/dist/file-delete.js +236 -0
- package/dist/file-edit.js +247 -0
- package/dist/file-read.js +197 -0
- package/dist/file-write.js +220 -0
- package/dist/git-branch.js +187 -0
- package/dist/git-diff.js +177 -0
- package/dist/git-log.js +230 -0
- package/dist/git-status.js +208 -0
- package/dist/guard-locked.js +169 -0
- package/dist/guard-main-branch.js +202 -0
- package/dist/guard-worktree-commit.js +160 -0
- package/dist/init-plan.js +337 -0
- package/dist/lumenflow-upgrade.js +178 -0
- package/dist/metrics-cli.js +433 -0
- package/dist/rotate-progress.js +247 -0
- package/dist/session-coordinator.js +300 -0
- package/dist/state-bootstrap.js +307 -0
- package/dist/sync-templates.js +212 -0
- package/dist/trace-gen.js +331 -0
- package/dist/validate-agent-skills.js +218 -0
- package/dist/validate-agent-sync.js +148 -0
- package/dist/validate-backlog-sync.js +152 -0
- package/dist/validate-skills-spec.js +206 -0
- package/dist/validate.js +230 -0
- package/package.json +37 -7
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Tests for rotate-progress CLI command
|
|
4
|
+
*
|
|
5
|
+
* WU-1112: INIT-003 Phase 6 - Migrate remaining Tier 1 tools
|
|
6
|
+
*
|
|
7
|
+
* Rotate progress moves completed WUs from status.md In Progress
|
|
8
|
+
* section to Completed section, keeping the file tidy.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
11
|
+
// Import functions under test
|
|
12
|
+
import { parseRotateArgs, findCompletedWUs, buildRotatedContent, } from '../rotate-progress.js';
|
|
13
|
+
describe('rotate-progress', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
describe('parseRotateArgs', () => {
|
|
18
|
+
it('should parse --dry-run flag', () => {
|
|
19
|
+
const args = parseRotateArgs(['node', 'rotate-progress.js', '--dry-run']);
|
|
20
|
+
expect(args.dryRun).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
it('should parse --help flag', () => {
|
|
23
|
+
const args = parseRotateArgs(['node', 'rotate-progress.js', '--help']);
|
|
24
|
+
expect(args.help).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
it('should parse --limit flag', () => {
|
|
27
|
+
const args = parseRotateArgs(['node', 'rotate-progress.js', '--limit', '10']);
|
|
28
|
+
expect(args.limit).toBe(10);
|
|
29
|
+
});
|
|
30
|
+
it('should default dryRun to false', () => {
|
|
31
|
+
const args = parseRotateArgs(['node', 'rotate-progress.js']);
|
|
32
|
+
expect(args.dryRun).toBeFalsy();
|
|
33
|
+
});
|
|
34
|
+
it('should default limit to undefined', () => {
|
|
35
|
+
const args = parseRotateArgs(['node', 'rotate-progress.js']);
|
|
36
|
+
expect(args.limit).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe('findCompletedWUs', () => {
|
|
40
|
+
it('should find WUs with done status in In Progress section', () => {
|
|
41
|
+
const statusContent = `## In Progress
|
|
42
|
+
- WU-1001 - Feature A
|
|
43
|
+
- WU-1002 - Feature B (done)
|
|
44
|
+
|
|
45
|
+
## Completed
|
|
46
|
+
- WU-1000 - Old feature (2024-01-01)
|
|
47
|
+
`;
|
|
48
|
+
const wuStatuses = new Map([
|
|
49
|
+
['WU-1001', 'in_progress'],
|
|
50
|
+
['WU-1002', 'done'],
|
|
51
|
+
]);
|
|
52
|
+
const completed = findCompletedWUs(statusContent, wuStatuses);
|
|
53
|
+
expect(completed).toContain('WU-1002');
|
|
54
|
+
expect(completed).not.toContain('WU-1001');
|
|
55
|
+
});
|
|
56
|
+
it('should return empty array when no completed WUs found', () => {
|
|
57
|
+
const statusContent = `## In Progress
|
|
58
|
+
- WU-1001 - Feature A
|
|
59
|
+
|
|
60
|
+
## Completed
|
|
61
|
+
`;
|
|
62
|
+
const wuStatuses = new Map([['WU-1001', 'in_progress']]);
|
|
63
|
+
const completed = findCompletedWUs(statusContent, wuStatuses);
|
|
64
|
+
expect(completed).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
it('should handle multiple completed WUs', () => {
|
|
67
|
+
const statusContent = `## In Progress
|
|
68
|
+
- WU-1001 - Feature A
|
|
69
|
+
- WU-1002 - Feature B
|
|
70
|
+
- WU-1003 - Feature C
|
|
71
|
+
|
|
72
|
+
## Completed
|
|
73
|
+
`;
|
|
74
|
+
const wuStatuses = new Map([
|
|
75
|
+
['WU-1001', 'done'],
|
|
76
|
+
['WU-1002', 'done'],
|
|
77
|
+
['WU-1003', 'in_progress'],
|
|
78
|
+
]);
|
|
79
|
+
const completed = findCompletedWUs(statusContent, wuStatuses);
|
|
80
|
+
expect(completed).toContain('WU-1001');
|
|
81
|
+
expect(completed).toContain('WU-1002');
|
|
82
|
+
expect(completed).not.toContain('WU-1003');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('buildRotatedContent', () => {
|
|
86
|
+
it('should move completed WUs to Completed section', () => {
|
|
87
|
+
const statusContent = `## In Progress
|
|
88
|
+
- WU-1001 - Feature A
|
|
89
|
+
- WU-1002 - Feature B
|
|
90
|
+
|
|
91
|
+
## Completed
|
|
92
|
+
- WU-1000 - Old feature (2024-01-01)
|
|
93
|
+
`;
|
|
94
|
+
const completedWUs = ['WU-1002'];
|
|
95
|
+
const result = buildRotatedContent(statusContent, completedWUs);
|
|
96
|
+
// WU-1002 should be removed from In Progress
|
|
97
|
+
expect(result).not.toContain('## In Progress\n- WU-1001 - Feature A\n- WU-1002 - Feature B');
|
|
98
|
+
// WU-1001 should still be in In Progress
|
|
99
|
+
expect(result).toContain('WU-1001 - Feature A');
|
|
100
|
+
// WU-1002 should be in Completed
|
|
101
|
+
expect(result).toContain('WU-1002');
|
|
102
|
+
});
|
|
103
|
+
it('should preserve existing completed entries', () => {
|
|
104
|
+
const statusContent = `## In Progress
|
|
105
|
+
- WU-1001 - Feature A
|
|
106
|
+
|
|
107
|
+
## Completed
|
|
108
|
+
- WU-1000 - Old feature (2024-01-01)
|
|
109
|
+
`;
|
|
110
|
+
const completedWUs = ['WU-1001'];
|
|
111
|
+
const result = buildRotatedContent(statusContent, completedWUs);
|
|
112
|
+
// Existing completed entry should remain
|
|
113
|
+
expect(result).toContain('WU-1000 - Old feature');
|
|
114
|
+
});
|
|
115
|
+
it('should add date stamp to newly completed WUs', () => {
|
|
116
|
+
const statusContent = `## In Progress
|
|
117
|
+
- WU-1001 - Feature A
|
|
118
|
+
|
|
119
|
+
## Completed
|
|
120
|
+
`;
|
|
121
|
+
const completedWUs = ['WU-1001'];
|
|
122
|
+
const result = buildRotatedContent(statusContent, completedWUs);
|
|
123
|
+
// Should have a date stamp (YYYY-MM-DD format)
|
|
124
|
+
expect(result).toMatch(/WU-1001.*\(\d{4}-\d{2}-\d{2}\)/);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Tests for session-coordinator CLI command
|
|
4
|
+
*
|
|
5
|
+
* WU-1112: INIT-003 Phase 6 - Migrate remaining Tier 1 tools
|
|
6
|
+
*
|
|
7
|
+
* Session coordinator manages agent sessions - starting, stopping,
|
|
8
|
+
* and coordinating handoffs between sessions.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
11
|
+
// Import functions under test
|
|
12
|
+
import { parseSessionArgs, SessionCommand, validateSessionCommand, } from '../session-coordinator.js';
|
|
13
|
+
describe('session-coordinator', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
describe('parseSessionArgs', () => {
|
|
18
|
+
it('should parse start subcommand', () => {
|
|
19
|
+
const args = parseSessionArgs(['node', 'session-coordinator.js', 'start']);
|
|
20
|
+
expect(args.command).toBe('start');
|
|
21
|
+
});
|
|
22
|
+
it('should parse stop subcommand', () => {
|
|
23
|
+
const args = parseSessionArgs(['node', 'session-coordinator.js', 'stop']);
|
|
24
|
+
expect(args.command).toBe('stop');
|
|
25
|
+
});
|
|
26
|
+
it('should parse status subcommand', () => {
|
|
27
|
+
const args = parseSessionArgs(['node', 'session-coordinator.js', 'status']);
|
|
28
|
+
expect(args.command).toBe('status');
|
|
29
|
+
});
|
|
30
|
+
it('should parse handoff subcommand', () => {
|
|
31
|
+
const args = parseSessionArgs(['node', 'session-coordinator.js', 'handoff']);
|
|
32
|
+
expect(args.command).toBe('handoff');
|
|
33
|
+
});
|
|
34
|
+
it('should parse --wu option', () => {
|
|
35
|
+
const args = parseSessionArgs(['node', 'session-coordinator.js', 'start', '--wu', 'WU-1112']);
|
|
36
|
+
expect(args.wuId).toBe('WU-1112');
|
|
37
|
+
});
|
|
38
|
+
it('should parse --agent option', () => {
|
|
39
|
+
const args = parseSessionArgs([
|
|
40
|
+
'node',
|
|
41
|
+
'session-coordinator.js',
|
|
42
|
+
'start',
|
|
43
|
+
'--agent',
|
|
44
|
+
'claude-code',
|
|
45
|
+
]);
|
|
46
|
+
expect(args.agent).toBe('claude-code');
|
|
47
|
+
});
|
|
48
|
+
it('should parse --reason option for stop', () => {
|
|
49
|
+
const args = parseSessionArgs([
|
|
50
|
+
'node',
|
|
51
|
+
'session-coordinator.js',
|
|
52
|
+
'stop',
|
|
53
|
+
'--reason',
|
|
54
|
+
'Completed work',
|
|
55
|
+
]);
|
|
56
|
+
expect(args.reason).toBe('Completed work');
|
|
57
|
+
});
|
|
58
|
+
it('should parse --help flag', () => {
|
|
59
|
+
const args = parseSessionArgs(['node', 'session-coordinator.js', '--help']);
|
|
60
|
+
expect(args.help).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
it('should default to status when no subcommand given', () => {
|
|
63
|
+
const args = parseSessionArgs(['node', 'session-coordinator.js']);
|
|
64
|
+
expect(args.command).toBe('status');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe('validateSessionCommand', () => {
|
|
68
|
+
it('should accept valid start command with wu', () => {
|
|
69
|
+
const args = { command: 'start', wuId: 'WU-1112' };
|
|
70
|
+
const result = validateSessionCommand(args);
|
|
71
|
+
expect(result.valid).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it('should reject start without wu', () => {
|
|
74
|
+
const args = { command: 'start' };
|
|
75
|
+
const result = validateSessionCommand(args);
|
|
76
|
+
expect(result.valid).toBe(false);
|
|
77
|
+
expect(result.error).toContain('--wu');
|
|
78
|
+
});
|
|
79
|
+
it('should accept stop command without reason', () => {
|
|
80
|
+
const args = { command: 'stop' };
|
|
81
|
+
const result = validateSessionCommand(args);
|
|
82
|
+
expect(result.valid).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
it('should accept status command', () => {
|
|
85
|
+
const args = { command: 'status' };
|
|
86
|
+
const result = validateSessionCommand(args);
|
|
87
|
+
expect(result.valid).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
it('should accept handoff command with wu', () => {
|
|
90
|
+
const args = { command: 'handoff', wuId: 'WU-1112' };
|
|
91
|
+
const result = validateSessionCommand(args);
|
|
92
|
+
expect(result.valid).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
it('should reject handoff without wu', () => {
|
|
95
|
+
const args = { command: 'handoff' };
|
|
96
|
+
const result = validateSessionCommand(args);
|
|
97
|
+
expect(result.valid).toBe(false);
|
|
98
|
+
expect(result.error).toContain('--wu');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe('SessionCommand enum', () => {
|
|
102
|
+
it('should have all expected commands', () => {
|
|
103
|
+
expect(SessionCommand.START).toBe('start');
|
|
104
|
+
expect(SessionCommand.STOP).toBe('stop');
|
|
105
|
+
expect(SessionCommand.STATUS).toBe('status');
|
|
106
|
+
expect(SessionCommand.HANDOFF).toBe('handoff');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file state-bootstrap.test.ts
|
|
4
|
+
* @description Tests for state-bootstrap CLI command (WU-1107)
|
|
5
|
+
*
|
|
6
|
+
* state-bootstrap is a one-time migration utility that:
|
|
7
|
+
* - Reads all WU YAML files
|
|
8
|
+
* - Generates corresponding events in the state store
|
|
9
|
+
* - Allows migration from YAML-only repos to event-sourced state
|
|
10
|
+
*
|
|
11
|
+
* TDD: RED phase - these tests define expected behavior before implementation
|
|
12
|
+
*/
|
|
13
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
14
|
+
import { existsSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
19
|
+
// These imports will fail until we implement the module (RED phase)
|
|
20
|
+
// We define the expected exports here
|
|
21
|
+
import { parseStateBootstrapArgs, inferEventsFromWu, generateBootstrapEvents, runStateBootstrap, STATE_BOOTSTRAP_DEFAULTS, } from '../state-bootstrap.js';
|
|
22
|
+
describe('state-bootstrap CLI', () => {
|
|
23
|
+
describe('source file existence', () => {
|
|
24
|
+
it('should have the CLI source file', () => {
|
|
25
|
+
const srcPath = join(__dirname, '../state-bootstrap.ts');
|
|
26
|
+
expect(existsSync(srcPath)).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
it('should be buildable (dist file exists after build)', () => {
|
|
29
|
+
// This test verifies that tsc compiled the file successfully
|
|
30
|
+
const distPath = join(__dirname, '../../dist/state-bootstrap.js');
|
|
31
|
+
expect(existsSync(distPath)).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe('STATE_BOOTSTRAP_DEFAULTS', () => {
|
|
35
|
+
it('should have default WU directory path', () => {
|
|
36
|
+
expect(STATE_BOOTSTRAP_DEFAULTS.wuDir).toBeTypeOf('string');
|
|
37
|
+
});
|
|
38
|
+
it('should have default state directory path', () => {
|
|
39
|
+
expect(STATE_BOOTSTRAP_DEFAULTS.stateDir).toBeTypeOf('string');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('parseStateBootstrapArgs', () => {
|
|
43
|
+
it('should parse --dry-run flag (default)', () => {
|
|
44
|
+
const args = parseStateBootstrapArgs(['node', 'state-bootstrap']);
|
|
45
|
+
expect(args.dryRun).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
it('should parse --execute flag', () => {
|
|
48
|
+
const args = parseStateBootstrapArgs(['node', 'state-bootstrap', '--execute']);
|
|
49
|
+
expect(args.dryRun).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
it('should parse --wu-dir option', () => {
|
|
52
|
+
const args = parseStateBootstrapArgs([
|
|
53
|
+
'node',
|
|
54
|
+
'state-bootstrap',
|
|
55
|
+
'--wu-dir',
|
|
56
|
+
'/custom/wu/path',
|
|
57
|
+
]);
|
|
58
|
+
expect(args.wuDir).toBe('/custom/wu/path');
|
|
59
|
+
});
|
|
60
|
+
it('should parse --state-dir option', () => {
|
|
61
|
+
const args = parseStateBootstrapArgs([
|
|
62
|
+
'node',
|
|
63
|
+
'state-bootstrap',
|
|
64
|
+
'--state-dir',
|
|
65
|
+
'/custom/state/path',
|
|
66
|
+
]);
|
|
67
|
+
expect(args.stateDir).toBe('/custom/state/path');
|
|
68
|
+
});
|
|
69
|
+
it('should parse --help flag', () => {
|
|
70
|
+
const args = parseStateBootstrapArgs(['node', 'state-bootstrap', '--help']);
|
|
71
|
+
expect(args.help).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it('should parse -h flag as help', () => {
|
|
74
|
+
const args = parseStateBootstrapArgs(['node', 'state-bootstrap', '-h']);
|
|
75
|
+
expect(args.help).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
it('should parse --force flag to overwrite existing state', () => {
|
|
78
|
+
const args = parseStateBootstrapArgs(['node', 'state-bootstrap', '--force']);
|
|
79
|
+
expect(args.force).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('inferEventsFromWu', () => {
|
|
83
|
+
it('should generate claim event for in_progress WU', () => {
|
|
84
|
+
const wu = {
|
|
85
|
+
id: 'WU-100',
|
|
86
|
+
status: 'in_progress',
|
|
87
|
+
lane: 'Framework: CLI',
|
|
88
|
+
title: 'Test WU',
|
|
89
|
+
created: '2026-01-20',
|
|
90
|
+
claimed_at: '2026-01-20T10:00:00Z',
|
|
91
|
+
};
|
|
92
|
+
const events = inferEventsFromWu(wu);
|
|
93
|
+
expect(events).toHaveLength(1);
|
|
94
|
+
expect(events[0].type).toBe('claim');
|
|
95
|
+
expect(events[0].wuId).toBe('WU-100');
|
|
96
|
+
expect(events[0].lane).toBe('Framework: CLI');
|
|
97
|
+
expect(events[0].title).toBe('Test WU');
|
|
98
|
+
});
|
|
99
|
+
it('should generate claim and complete events for done WU', () => {
|
|
100
|
+
const wu = {
|
|
101
|
+
id: 'WU-100',
|
|
102
|
+
status: 'done',
|
|
103
|
+
lane: 'Framework: CLI',
|
|
104
|
+
title: 'Test WU',
|
|
105
|
+
created: '2026-01-15',
|
|
106
|
+
claimed_at: '2026-01-15T10:00:00Z',
|
|
107
|
+
completed_at: '2026-01-20T15:00:00Z',
|
|
108
|
+
};
|
|
109
|
+
const events = inferEventsFromWu(wu);
|
|
110
|
+
expect(events).toHaveLength(2);
|
|
111
|
+
expect(events[0].type).toBe('claim');
|
|
112
|
+
expect(events[1].type).toBe('complete');
|
|
113
|
+
expect(events[1].wuId).toBe('WU-100');
|
|
114
|
+
});
|
|
115
|
+
it('should generate claim and block events for blocked WU', () => {
|
|
116
|
+
const wu = {
|
|
117
|
+
id: 'WU-100',
|
|
118
|
+
status: 'blocked',
|
|
119
|
+
lane: 'Framework: CLI',
|
|
120
|
+
title: 'Test WU',
|
|
121
|
+
created: '2026-01-15',
|
|
122
|
+
claimed_at: '2026-01-15T10:00:00Z',
|
|
123
|
+
};
|
|
124
|
+
const events = inferEventsFromWu(wu);
|
|
125
|
+
expect(events).toHaveLength(2);
|
|
126
|
+
expect(events[0].type).toBe('claim');
|
|
127
|
+
expect(events[1].type).toBe('block');
|
|
128
|
+
});
|
|
129
|
+
it('should return empty array for ready WU (not yet claimed)', () => {
|
|
130
|
+
const wu = {
|
|
131
|
+
id: 'WU-100',
|
|
132
|
+
status: 'ready',
|
|
133
|
+
lane: 'Framework: CLI',
|
|
134
|
+
title: 'Test WU',
|
|
135
|
+
created: '2026-01-20',
|
|
136
|
+
};
|
|
137
|
+
const events = inferEventsFromWu(wu);
|
|
138
|
+
expect(events).toHaveLength(0);
|
|
139
|
+
});
|
|
140
|
+
it('should use created date as fallback for claim timestamp', () => {
|
|
141
|
+
const wu = {
|
|
142
|
+
id: 'WU-100',
|
|
143
|
+
status: 'in_progress',
|
|
144
|
+
lane: 'Framework: CLI',
|
|
145
|
+
title: 'Test WU',
|
|
146
|
+
created: '2026-01-20',
|
|
147
|
+
// No claimed_at
|
|
148
|
+
};
|
|
149
|
+
const events = inferEventsFromWu(wu);
|
|
150
|
+
expect(events).toHaveLength(1);
|
|
151
|
+
expect(events[0].timestamp).toContain('2026-01-20');
|
|
152
|
+
});
|
|
153
|
+
it('should handle legacy completed status same as done', () => {
|
|
154
|
+
const wu = {
|
|
155
|
+
id: 'WU-100',
|
|
156
|
+
status: 'completed',
|
|
157
|
+
lane: 'Framework: CLI',
|
|
158
|
+
title: 'Test WU',
|
|
159
|
+
created: '2026-01-15',
|
|
160
|
+
claimed_at: '2026-01-15T10:00:00Z',
|
|
161
|
+
completed_at: '2026-01-20T15:00:00Z',
|
|
162
|
+
};
|
|
163
|
+
const events = inferEventsFromWu(wu);
|
|
164
|
+
expect(events).toHaveLength(2);
|
|
165
|
+
expect(events[0].type).toBe('claim');
|
|
166
|
+
expect(events[1].type).toBe('complete');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
describe('generateBootstrapEvents', () => {
|
|
170
|
+
it('should generate events for multiple WUs', () => {
|
|
171
|
+
const wus = [
|
|
172
|
+
{
|
|
173
|
+
id: 'WU-100',
|
|
174
|
+
status: 'in_progress',
|
|
175
|
+
lane: 'Framework: CLI',
|
|
176
|
+
title: 'WU 100',
|
|
177
|
+
created: '2026-01-20',
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'WU-101',
|
|
181
|
+
status: 'done',
|
|
182
|
+
lane: 'Framework: Core',
|
|
183
|
+
title: 'WU 101',
|
|
184
|
+
created: '2026-01-15',
|
|
185
|
+
completed_at: '2026-01-18T10:00:00Z',
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: 'WU-102',
|
|
189
|
+
status: 'ready',
|
|
190
|
+
lane: 'Framework: CLI',
|
|
191
|
+
title: 'WU 102',
|
|
192
|
+
created: '2026-01-22',
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
const events = generateBootstrapEvents(wus);
|
|
196
|
+
// WU-100: 1 claim, WU-101: 1 claim + 1 complete, WU-102: 0 (ready)
|
|
197
|
+
expect(events).toHaveLength(3);
|
|
198
|
+
});
|
|
199
|
+
it('should order events chronologically', () => {
|
|
200
|
+
const wus = [
|
|
201
|
+
{
|
|
202
|
+
id: 'WU-102',
|
|
203
|
+
status: 'done',
|
|
204
|
+
lane: 'Framework: CLI',
|
|
205
|
+
title: 'WU 102',
|
|
206
|
+
created: '2026-01-20',
|
|
207
|
+
claimed_at: '2026-01-20T10:00:00Z',
|
|
208
|
+
completed_at: '2026-01-22T10:00:00Z',
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: 'WU-100',
|
|
212
|
+
status: 'done',
|
|
213
|
+
lane: 'Framework: Core',
|
|
214
|
+
title: 'WU 100',
|
|
215
|
+
created: '2026-01-15',
|
|
216
|
+
claimed_at: '2026-01-15T08:00:00Z',
|
|
217
|
+
completed_at: '2026-01-16T10:00:00Z',
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
const events = generateBootstrapEvents(wus);
|
|
221
|
+
// Events should be ordered: WU-100 claim, WU-100 complete, WU-102 claim, WU-102 complete
|
|
222
|
+
expect(events[0].wuId).toBe('WU-100');
|
|
223
|
+
expect(events[0].type).toBe('claim');
|
|
224
|
+
expect(events[1].wuId).toBe('WU-100');
|
|
225
|
+
expect(events[1].type).toBe('complete');
|
|
226
|
+
expect(events[2].wuId).toBe('WU-102');
|
|
227
|
+
expect(events[2].type).toBe('claim');
|
|
228
|
+
});
|
|
229
|
+
it('should return empty array for empty WU list', () => {
|
|
230
|
+
const events = generateBootstrapEvents([]);
|
|
231
|
+
expect(events).toHaveLength(0);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
describe('runStateBootstrap', () => {
|
|
235
|
+
let tempDir;
|
|
236
|
+
beforeEach(() => {
|
|
237
|
+
// Create a temporary directory for test fixtures
|
|
238
|
+
tempDir = join(tmpdir(), `state-bootstrap-test-${Date.now()}`);
|
|
239
|
+
mkdirSync(join(tempDir, 'wu'), { recursive: true });
|
|
240
|
+
mkdirSync(join(tempDir, 'state'), { recursive: true });
|
|
241
|
+
});
|
|
242
|
+
afterEach(() => {
|
|
243
|
+
// Cleanup temp directory
|
|
244
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
245
|
+
});
|
|
246
|
+
it('should not write events in dry-run mode', async () => {
|
|
247
|
+
// Create a test WU YAML (lane value must be quoted due to colon)
|
|
248
|
+
const wuYaml = `id: WU-100
|
|
249
|
+
title: Test WU
|
|
250
|
+
lane: "Framework: CLI"
|
|
251
|
+
status: in_progress
|
|
252
|
+
created: 2026-01-20
|
|
253
|
+
`;
|
|
254
|
+
writeFileSync(join(tempDir, 'wu', 'WU-100.yaml'), wuYaml);
|
|
255
|
+
const result = await runStateBootstrap({
|
|
256
|
+
dryRun: true,
|
|
257
|
+
wuDir: join(tempDir, 'wu'),
|
|
258
|
+
stateDir: join(tempDir, 'state'),
|
|
259
|
+
force: false,
|
|
260
|
+
help: false,
|
|
261
|
+
});
|
|
262
|
+
expect(result.success).toBe(true);
|
|
263
|
+
expect(result.eventsGenerated).toBe(1);
|
|
264
|
+
expect(result.eventsWritten).toBe(0);
|
|
265
|
+
expect(existsSync(join(tempDir, 'state', 'wu-events.jsonl'))).toBe(false);
|
|
266
|
+
});
|
|
267
|
+
it('should write events in execute mode', async () => {
|
|
268
|
+
// Create a test WU YAML (lane value must be quoted due to colon)
|
|
269
|
+
const wuYaml = `id: WU-100
|
|
270
|
+
title: Test WU
|
|
271
|
+
lane: "Framework: CLI"
|
|
272
|
+
status: in_progress
|
|
273
|
+
created: 2026-01-20
|
|
274
|
+
`;
|
|
275
|
+
writeFileSync(join(tempDir, 'wu', 'WU-100.yaml'), wuYaml);
|
|
276
|
+
const result = await runStateBootstrap({
|
|
277
|
+
dryRun: false,
|
|
278
|
+
wuDir: join(tempDir, 'wu'),
|
|
279
|
+
stateDir: join(tempDir, 'state'),
|
|
280
|
+
force: false,
|
|
281
|
+
help: false,
|
|
282
|
+
});
|
|
283
|
+
expect(result.success).toBe(true);
|
|
284
|
+
expect(result.eventsGenerated).toBe(1);
|
|
285
|
+
expect(result.eventsWritten).toBe(1);
|
|
286
|
+
expect(existsSync(join(tempDir, 'state', 'wu-events.jsonl'))).toBe(true);
|
|
287
|
+
});
|
|
288
|
+
it('should fail if state file already exists without --force', async () => {
|
|
289
|
+
// Create existing state file
|
|
290
|
+
writeFileSync(join(tempDir, 'state', 'wu-events.jsonl'), '{"existing":"data"}\n');
|
|
291
|
+
// Create a test WU YAML (lane value must be quoted due to colon)
|
|
292
|
+
const wuYaml = `id: WU-100
|
|
293
|
+
title: Test WU
|
|
294
|
+
lane: "Framework: CLI"
|
|
295
|
+
status: in_progress
|
|
296
|
+
created: 2026-01-20
|
|
297
|
+
`;
|
|
298
|
+
writeFileSync(join(tempDir, 'wu', 'WU-100.yaml'), wuYaml);
|
|
299
|
+
const result = await runStateBootstrap({
|
|
300
|
+
dryRun: false,
|
|
301
|
+
wuDir: join(tempDir, 'wu'),
|
|
302
|
+
stateDir: join(tempDir, 'state'),
|
|
303
|
+
force: false,
|
|
304
|
+
help: false,
|
|
305
|
+
});
|
|
306
|
+
expect(result.success).toBe(false);
|
|
307
|
+
expect(result.error).toContain('exists');
|
|
308
|
+
});
|
|
309
|
+
it('should overwrite state file with --force', async () => {
|
|
310
|
+
// Create existing state file
|
|
311
|
+
writeFileSync(join(tempDir, 'state', 'wu-events.jsonl'), '{"existing":"data"}\n');
|
|
312
|
+
// Create a test WU YAML (lane value must be quoted due to colon)
|
|
313
|
+
const wuYaml = `id: WU-100
|
|
314
|
+
title: Test WU
|
|
315
|
+
lane: "Framework: CLI"
|
|
316
|
+
status: in_progress
|
|
317
|
+
created: 2026-01-20
|
|
318
|
+
`;
|
|
319
|
+
writeFileSync(join(tempDir, 'wu', 'WU-100.yaml'), wuYaml);
|
|
320
|
+
const result = await runStateBootstrap({
|
|
321
|
+
dryRun: false,
|
|
322
|
+
wuDir: join(tempDir, 'wu'),
|
|
323
|
+
stateDir: join(tempDir, 'state'),
|
|
324
|
+
force: true,
|
|
325
|
+
help: false,
|
|
326
|
+
});
|
|
327
|
+
expect(result.success).toBe(true);
|
|
328
|
+
expect(result.eventsWritten).toBe(1);
|
|
329
|
+
});
|
|
330
|
+
it('should handle missing WU directory gracefully', async () => {
|
|
331
|
+
const result = await runStateBootstrap({
|
|
332
|
+
dryRun: true,
|
|
333
|
+
wuDir: join(tempDir, 'nonexistent'),
|
|
334
|
+
stateDir: join(tempDir, 'state'),
|
|
335
|
+
force: false,
|
|
336
|
+
help: false,
|
|
337
|
+
});
|
|
338
|
+
expect(result.success).toBe(true);
|
|
339
|
+
expect(result.eventsGenerated).toBe(0);
|
|
340
|
+
expect(result.warnings).toContain('WU directory not found');
|
|
341
|
+
});
|
|
342
|
+
it('should skip invalid YAML files', async () => {
|
|
343
|
+
// Create valid WU YAML (lane value must be quoted due to colon)
|
|
344
|
+
const validWuYaml = `id: WU-100
|
|
345
|
+
title: Valid WU
|
|
346
|
+
lane: "Framework: CLI"
|
|
347
|
+
status: in_progress
|
|
348
|
+
created: 2026-01-20
|
|
349
|
+
`;
|
|
350
|
+
writeFileSync(join(tempDir, 'wu', 'WU-100.yaml'), validWuYaml);
|
|
351
|
+
// Create invalid YAML file
|
|
352
|
+
writeFileSync(join(tempDir, 'wu', 'WU-101.yaml'), 'invalid: yaml: content:');
|
|
353
|
+
const result = await runStateBootstrap({
|
|
354
|
+
dryRun: false,
|
|
355
|
+
wuDir: join(tempDir, 'wu'),
|
|
356
|
+
stateDir: join(tempDir, 'state'),
|
|
357
|
+
force: false,
|
|
358
|
+
help: false,
|
|
359
|
+
});
|
|
360
|
+
expect(result.success).toBe(true);
|
|
361
|
+
expect(result.eventsGenerated).toBe(1);
|
|
362
|
+
expect(result.skipped).toBe(1);
|
|
363
|
+
});
|
|
364
|
+
it('should process multiple valid WU files', async () => {
|
|
365
|
+
// Create multiple valid WU YAMLs (lane value must be quoted due to colon)
|
|
366
|
+
for (let i = 100; i <= 105; i++) {
|
|
367
|
+
const wuYaml = `id: WU-${i}
|
|
368
|
+
title: Test WU ${i}
|
|
369
|
+
lane: "Framework: CLI"
|
|
370
|
+
status: ${i <= 102 ? 'done' : 'in_progress'}
|
|
371
|
+
created: 2026-01-${10 + i - 100}
|
|
372
|
+
${i <= 102 ? `completed_at: 2026-01-${15 + i - 100}T10:00:00Z` : ''}
|
|
373
|
+
`;
|
|
374
|
+
writeFileSync(join(tempDir, 'wu', `WU-${i}.yaml`), wuYaml);
|
|
375
|
+
}
|
|
376
|
+
const result = await runStateBootstrap({
|
|
377
|
+
dryRun: false,
|
|
378
|
+
wuDir: join(tempDir, 'wu'),
|
|
379
|
+
stateDir: join(tempDir, 'state'),
|
|
380
|
+
force: false,
|
|
381
|
+
help: false,
|
|
382
|
+
});
|
|
383
|
+
expect(result.success).toBe(true);
|
|
384
|
+
// 3 done WUs = 3 claim + 3 complete = 6 events
|
|
385
|
+
// 3 in_progress WUs = 3 claim = 3 events
|
|
386
|
+
// Total = 9 events
|
|
387
|
+
expect(result.eventsGenerated).toBe(9);
|
|
388
|
+
expect(result.eventsWritten).toBe(9);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
describe('integration: written events are valid', () => {
|
|
392
|
+
let tempDir;
|
|
393
|
+
beforeEach(() => {
|
|
394
|
+
tempDir = join(tmpdir(), `state-bootstrap-integration-${Date.now()}`);
|
|
395
|
+
mkdirSync(join(tempDir, 'wu'), { recursive: true });
|
|
396
|
+
mkdirSync(join(tempDir, 'state'), { recursive: true });
|
|
397
|
+
});
|
|
398
|
+
afterEach(() => {
|
|
399
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
400
|
+
});
|
|
401
|
+
it('should write events that can be loaded by WUStateStore', async () => {
|
|
402
|
+
// lane value must be quoted due to colon
|
|
403
|
+
const wuYaml = `id: WU-100
|
|
404
|
+
title: Test WU
|
|
405
|
+
lane: "Framework: CLI"
|
|
406
|
+
status: done
|
|
407
|
+
created: 2026-01-15
|
|
408
|
+
claimed_at: 2026-01-15T10:00:00Z
|
|
409
|
+
completed_at: 2026-01-20T15:00:00Z
|
|
410
|
+
`;
|
|
411
|
+
writeFileSync(join(tempDir, 'wu', 'WU-100.yaml'), wuYaml);
|
|
412
|
+
await runStateBootstrap({
|
|
413
|
+
dryRun: false,
|
|
414
|
+
wuDir: join(tempDir, 'wu'),
|
|
415
|
+
stateDir: join(tempDir, 'state'),
|
|
416
|
+
force: false,
|
|
417
|
+
help: false,
|
|
418
|
+
});
|
|
419
|
+
// Read the generated file
|
|
420
|
+
const content = readFileSync(join(tempDir, 'state', 'wu-events.jsonl'), 'utf-8');
|
|
421
|
+
const lines = content.trim().split('\n');
|
|
422
|
+
expect(lines.length).toBe(2); // claim + complete
|
|
423
|
+
// Verify each line is valid JSON with required fields
|
|
424
|
+
for (const line of lines) {
|
|
425
|
+
const event = JSON.parse(line);
|
|
426
|
+
expect(event).toHaveProperty('type');
|
|
427
|
+
expect(event).toHaveProperty('wuId');
|
|
428
|
+
expect(event).toHaveProperty('timestamp');
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
});
|