@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,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
+ });