@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,416 @@
1
+ /**
2
+ * @file guards-validation.test.ts
3
+ * @description Tests for guard and validation CLI tools (WU-1111)
4
+ *
5
+ * Tests cover:
6
+ * - guard-worktree-commit: Prevents WU commits from main checkout
7
+ * - guard-locked: Prevents changes to locked WUs
8
+ * - validate: Main WU YAML validator
9
+ * - validate-agent-skills: Validates agent skill definitions
10
+ * - validate-agent-sync: Validates agent sync state
11
+ * - validate-backlog-sync: Validates backlog.md is in sync with WU YAML files
12
+ * - validate-skills-spec: Validates skills spec format
13
+ */
14
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
15
+ import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
16
+ import path from 'node:path';
17
+ import os from 'node:os';
18
+ // Test utilities
19
+ function createTempDir() {
20
+ const tmpDir = path.join(os.tmpdir(), `guards-test-${Date.now()}`);
21
+ mkdirSync(tmpDir, { recursive: true });
22
+ return tmpDir;
23
+ }
24
+ function cleanupTempDir(dir) {
25
+ try {
26
+ rmSync(dir, { recursive: true, force: true });
27
+ }
28
+ catch {
29
+ // Ignore cleanup errors
30
+ }
31
+ }
32
+ // ============================================================================
33
+ // guard-locked tests
34
+ // ============================================================================
35
+ describe('guard-locked', () => {
36
+ let tmpDir;
37
+ beforeEach(() => {
38
+ tmpDir = createTempDir();
39
+ });
40
+ afterEach(() => {
41
+ cleanupTempDir(tmpDir);
42
+ });
43
+ describe('isWULocked', () => {
44
+ it('should return true when WU has locked: true', async () => {
45
+ const { isWULocked } = await import('../guard-locked.js');
46
+ const wuYaml = `
47
+ id: WU-123
48
+ title: Test WU
49
+ status: done
50
+ locked: true
51
+ `;
52
+ const wuPath = path.join(tmpDir, 'WU-123.yaml');
53
+ writeFileSync(wuPath, wuYaml);
54
+ const result = isWULocked(wuPath);
55
+ expect(result).toBe(true);
56
+ });
57
+ it('should return false when WU has locked: false', async () => {
58
+ const { isWULocked } = await import('../guard-locked.js');
59
+ const wuYaml = `
60
+ id: WU-456
61
+ title: Test WU
62
+ status: in_progress
63
+ locked: false
64
+ `;
65
+ const wuPath = path.join(tmpDir, 'WU-456.yaml');
66
+ writeFileSync(wuPath, wuYaml);
67
+ const result = isWULocked(wuPath);
68
+ expect(result).toBe(false);
69
+ });
70
+ it('should return false when WU has no locked field', async () => {
71
+ const { isWULocked } = await import('../guard-locked.js');
72
+ const wuYaml = `
73
+ id: WU-789
74
+ title: Test WU
75
+ status: ready
76
+ `;
77
+ const wuPath = path.join(tmpDir, 'WU-789.yaml');
78
+ writeFileSync(wuPath, wuYaml);
79
+ const result = isWULocked(wuPath);
80
+ expect(result).toBe(false);
81
+ });
82
+ it('should throw when WU file does not exist', async () => {
83
+ const { isWULocked } = await import('../guard-locked.js');
84
+ const wuPath = path.join(tmpDir, 'WU-999.yaml');
85
+ expect(() => isWULocked(wuPath)).toThrow(/not found/);
86
+ });
87
+ });
88
+ describe('assertWUNotLocked', () => {
89
+ it('should not throw when WU is not locked', async () => {
90
+ const { assertWUNotLocked } = await import('../guard-locked.js');
91
+ const wuYaml = `
92
+ id: WU-100
93
+ title: Unlocked WU
94
+ status: in_progress
95
+ locked: false
96
+ `;
97
+ const wuPath = path.join(tmpDir, 'WU-100.yaml');
98
+ writeFileSync(wuPath, wuYaml);
99
+ expect(() => assertWUNotLocked(wuPath)).not.toThrow();
100
+ });
101
+ it('should throw with descriptive message when WU is locked', async () => {
102
+ const { assertWUNotLocked } = await import('../guard-locked.js');
103
+ const wuYaml = `
104
+ id: WU-200
105
+ title: Locked WU
106
+ status: done
107
+ locked: true
108
+ `;
109
+ const wuPath = path.join(tmpDir, 'WU-200.yaml');
110
+ writeFileSync(wuPath, wuYaml);
111
+ expect(() => assertWUNotLocked(wuPath)).toThrow(/WU-200.*locked/i);
112
+ });
113
+ it('should include wu:unlock suggestion in error message', async () => {
114
+ const { assertWUNotLocked } = await import('../guard-locked.js');
115
+ const wuYaml = `
116
+ id: WU-300
117
+ title: Locked WU
118
+ status: done
119
+ locked: true
120
+ `;
121
+ const wuPath = path.join(tmpDir, 'WU-300.yaml');
122
+ writeFileSync(wuPath, wuYaml);
123
+ expect(() => assertWUNotLocked(wuPath)).toThrow(/wu:unlock/);
124
+ });
125
+ });
126
+ });
127
+ // ============================================================================
128
+ // guard-worktree-commit tests
129
+ // ============================================================================
130
+ describe('guard-worktree-commit', () => {
131
+ describe('shouldBlockCommit', () => {
132
+ it('should block commits with WU prefix from main checkout', async () => {
133
+ const { shouldBlockCommit } = await import('../guard-worktree-commit.js');
134
+ const result = shouldBlockCommit({
135
+ commitMessage: 'wu(WU-123): add feature',
136
+ isMainCheckout: true,
137
+ isInWorktree: false,
138
+ });
139
+ expect(result.blocked).toBe(true);
140
+ expect(result.reason).toMatch(/worktree/i);
141
+ });
142
+ it('should allow commits with WU prefix from worktree', async () => {
143
+ const { shouldBlockCommit } = await import('../guard-worktree-commit.js');
144
+ const result = shouldBlockCommit({
145
+ commitMessage: 'wu(WU-123): add feature',
146
+ isMainCheckout: false,
147
+ isInWorktree: true,
148
+ });
149
+ expect(result.blocked).toBe(false);
150
+ });
151
+ it('should allow non-WU commits from main checkout', async () => {
152
+ const { shouldBlockCommit } = await import('../guard-worktree-commit.js');
153
+ const result = shouldBlockCommit({
154
+ commitMessage: 'chore: update dependencies',
155
+ isMainCheckout: true,
156
+ isInWorktree: false,
157
+ });
158
+ expect(result.blocked).toBe(false);
159
+ });
160
+ it('should detect WU prefix case-insensitively', async () => {
161
+ const { shouldBlockCommit } = await import('../guard-worktree-commit.js');
162
+ const result = shouldBlockCommit({
163
+ commitMessage: 'WU(wu-456): something',
164
+ isMainCheckout: true,
165
+ isInWorktree: false,
166
+ });
167
+ expect(result.blocked).toBe(true);
168
+ });
169
+ it('should block feat(WU-xxx) pattern from main', async () => {
170
+ const { shouldBlockCommit } = await import('../guard-worktree-commit.js');
171
+ const result = shouldBlockCommit({
172
+ commitMessage: 'feat(WU-789): new feature',
173
+ isMainCheckout: true,
174
+ isInWorktree: false,
175
+ });
176
+ expect(result.blocked).toBe(true);
177
+ });
178
+ });
179
+ });
180
+ // ============================================================================
181
+ // validate-agent-skills tests
182
+ // ============================================================================
183
+ describe('validate-agent-skills', () => {
184
+ let tmpDir;
185
+ beforeEach(() => {
186
+ tmpDir = createTempDir();
187
+ mkdirSync(path.join(tmpDir, '.claude', 'skills'), { recursive: true });
188
+ });
189
+ afterEach(() => {
190
+ cleanupTempDir(tmpDir);
191
+ });
192
+ describe('validateSkillFile', () => {
193
+ it('should pass for valid skill with all required fields', async () => {
194
+ const { validateSkillFile } = await import('../validate-agent-skills.js');
195
+ const skillContent = `# My Skill
196
+
197
+ **Source**: some-doc.md
198
+
199
+ ## When to Use
200
+ Use this skill when you need to do something.
201
+
202
+ ## Core Concepts
203
+ Important concepts here.
204
+ `;
205
+ const skillPath = path.join(tmpDir, '.claude', 'skills', 'my-skill', 'SKILL.md');
206
+ mkdirSync(path.dirname(skillPath), { recursive: true });
207
+ writeFileSync(skillPath, skillContent);
208
+ const result = validateSkillFile(skillPath);
209
+ expect(result.valid).toBe(true);
210
+ expect(result.errors).toHaveLength(0);
211
+ });
212
+ it('should fail for skill missing "When to Use" section', async () => {
213
+ const { validateSkillFile } = await import('../validate-agent-skills.js');
214
+ const skillContent = `# My Skill
215
+
216
+ **Source**: some-doc.md
217
+
218
+ ## Core Concepts
219
+ Concepts only, no when-to-use.
220
+ `;
221
+ const skillPath = path.join(tmpDir, '.claude', 'skills', 'bad-skill', 'SKILL.md');
222
+ mkdirSync(path.dirname(skillPath), { recursive: true });
223
+ writeFileSync(skillPath, skillContent);
224
+ const result = validateSkillFile(skillPath);
225
+ expect(result.valid).toBe(false);
226
+ expect(result.errors.some((e) => /When to Use/i.test(e))).toBe(true);
227
+ });
228
+ it('should fail for skill missing title heading', async () => {
229
+ const { validateSkillFile } = await import('../validate-agent-skills.js');
230
+ const skillContent = `No title heading here
231
+
232
+ ## When to Use
233
+ Some content.
234
+ `;
235
+ const skillPath = path.join(tmpDir, '.claude', 'skills', 'no-title', 'SKILL.md');
236
+ mkdirSync(path.dirname(skillPath), { recursive: true });
237
+ writeFileSync(skillPath, skillContent);
238
+ const result = validateSkillFile(skillPath);
239
+ expect(result.valid).toBe(false);
240
+ expect(result.errors.some((e) => /title|heading/i.test(e))).toBe(true);
241
+ });
242
+ });
243
+ describe('validateAllSkills', () => {
244
+ it('should validate all skills in directory', async () => {
245
+ const { validateAllSkills } = await import('../validate-agent-skills.js');
246
+ // Create two valid skills
247
+ const skill1Path = path.join(tmpDir, '.claude', 'skills', 'skill-one', 'SKILL.md');
248
+ const skill2Path = path.join(tmpDir, '.claude', 'skills', 'skill-two', 'SKILL.md');
249
+ mkdirSync(path.dirname(skill1Path), { recursive: true });
250
+ mkdirSync(path.dirname(skill2Path), { recursive: true });
251
+ const validContent = `# Valid Skill
252
+
253
+ ## When to Use
254
+ Use this.
255
+ `;
256
+ writeFileSync(skill1Path, validContent);
257
+ writeFileSync(skill2Path, validContent);
258
+ const results = validateAllSkills(path.join(tmpDir, '.claude', 'skills'));
259
+ expect(results.totalValid).toBe(2);
260
+ expect(results.totalInvalid).toBe(0);
261
+ });
262
+ });
263
+ });
264
+ // ============================================================================
265
+ // validate-agent-sync tests
266
+ // ============================================================================
267
+ describe('validate-agent-sync', () => {
268
+ let tmpDir;
269
+ beforeEach(() => {
270
+ tmpDir = createTempDir();
271
+ });
272
+ afterEach(() => {
273
+ cleanupTempDir(tmpDir);
274
+ });
275
+ describe('validateAgentSync', () => {
276
+ it('should pass when agent files exist and match expected structure', async () => {
277
+ const { validateAgentSync } = await import('../validate-agent-sync.js');
278
+ // Create expected agent directory structure
279
+ const agentDir = path.join(tmpDir, '.claude', 'agents');
280
+ mkdirSync(agentDir, { recursive: true });
281
+ const agentDef = `{
282
+ "name": "test-agent",
283
+ "description": "A test agent"
284
+ }`;
285
+ writeFileSync(path.join(agentDir, 'test-agent.json'), agentDef);
286
+ const result = await validateAgentSync({ cwd: tmpDir });
287
+ expect(result.valid).toBe(true);
288
+ });
289
+ it('should fail when agent directory is missing', async () => {
290
+ const { validateAgentSync } = await import('../validate-agent-sync.js');
291
+ // No agent directory created
292
+ const result = await validateAgentSync({ cwd: tmpDir });
293
+ expect(result.valid).toBe(false);
294
+ expect(result.errors.some((e) => /agents.*not found|missing/i.test(e))).toBe(true);
295
+ });
296
+ it('should warn when agent definitions have no description', async () => {
297
+ const { validateAgentSync } = await import('../validate-agent-sync.js');
298
+ const agentDir = path.join(tmpDir, '.claude', 'agents');
299
+ mkdirSync(agentDir, { recursive: true });
300
+ const agentDef = `{
301
+ "name": "no-desc-agent"
302
+ }`;
303
+ writeFileSync(path.join(agentDir, 'no-desc.json'), agentDef);
304
+ const result = await validateAgentSync({ cwd: tmpDir });
305
+ expect(result.warnings.some((w) => /description/i.test(w))).toBe(true);
306
+ });
307
+ });
308
+ });
309
+ // ============================================================================
310
+ // validate-backlog-sync tests
311
+ // ============================================================================
312
+ describe('validate-backlog-sync', () => {
313
+ let tmpDir;
314
+ beforeEach(() => {
315
+ tmpDir = createTempDir();
316
+ mkdirSync(path.join(tmpDir, 'docs', '04-operations', 'tasks', 'wu'), { recursive: true });
317
+ });
318
+ afterEach(() => {
319
+ cleanupTempDir(tmpDir);
320
+ });
321
+ describe('validateBacklogSync', () => {
322
+ it('should pass when backlog.md lists all WU YAML files', async () => {
323
+ const { validateBacklogSync } = await import('../validate-backlog-sync.js');
324
+ // Create WU files
325
+ writeFileSync(path.join(tmpDir, 'docs', '04-operations', 'tasks', 'wu', 'WU-001.yaml'), 'id: WU-001\ntitle: First WU\nstatus: ready');
326
+ writeFileSync(path.join(tmpDir, 'docs', '04-operations', 'tasks', 'wu', 'WU-002.yaml'), 'id: WU-002\ntitle: Second WU\nstatus: done');
327
+ // Create backlog.md that references both
328
+ const backlogContent = `# Backlog
329
+
330
+ ## Ready
331
+ - WU-001: First WU
332
+
333
+ ## Done
334
+ - WU-002: Second WU
335
+ `;
336
+ writeFileSync(path.join(tmpDir, 'docs', '04-operations', 'tasks', 'backlog.md'), backlogContent);
337
+ const result = await validateBacklogSync({ cwd: tmpDir });
338
+ expect(result.valid).toBe(true);
339
+ });
340
+ it('should fail when WU exists but not in backlog.md', async () => {
341
+ const { validateBacklogSync } = await import('../validate-backlog-sync.js');
342
+ // Create WU file
343
+ writeFileSync(path.join(tmpDir, 'docs', '04-operations', 'tasks', 'wu', 'WU-001.yaml'), 'id: WU-001\ntitle: Missing WU\nstatus: ready');
344
+ // Create backlog.md that doesn't reference the WU
345
+ const backlogContent = `# Backlog
346
+
347
+ ## Ready
348
+ (empty)
349
+ `;
350
+ writeFileSync(path.join(tmpDir, 'docs', '04-operations', 'tasks', 'backlog.md'), backlogContent);
351
+ const result = await validateBacklogSync({ cwd: tmpDir });
352
+ expect(result.valid).toBe(false);
353
+ expect(result.errors.some((e) => /WU-001.*not found.*backlog/i.test(e))).toBe(true);
354
+ });
355
+ });
356
+ });
357
+ // ============================================================================
358
+ // validate-skills-spec tests
359
+ // ============================================================================
360
+ describe('validate-skills-spec', () => {
361
+ let tmpDir;
362
+ beforeEach(() => {
363
+ tmpDir = createTempDir();
364
+ });
365
+ afterEach(() => {
366
+ cleanupTempDir(tmpDir);
367
+ });
368
+ describe('validateSkillsSpec', () => {
369
+ it('should pass for skill with required sections', async () => {
370
+ const { validateSkillsSpec } = await import('../validate-skills-spec.js');
371
+ const skillSpec = `# Skill Name
372
+
373
+ ## When to Use
374
+ Describe when to activate this skill.
375
+
376
+ ## Key Concepts
377
+ Important concepts.
378
+
379
+ ## Examples
380
+ Show examples.
381
+ `;
382
+ const skillPath = path.join(tmpDir, 'SKILL.md');
383
+ writeFileSync(skillPath, skillSpec);
384
+ const result = validateSkillsSpec(skillPath);
385
+ expect(result.valid).toBe(true);
386
+ });
387
+ it('should fail for skill missing required "When to Use" section', async () => {
388
+ const { validateSkillsSpec } = await import('../validate-skills-spec.js');
389
+ const skillSpec = `# Skill Name
390
+
391
+ ## Key Concepts
392
+ Concepts but no when-to-use.
393
+ `;
394
+ const skillPath = path.join(tmpDir, 'SKILL.md');
395
+ writeFileSync(skillPath, skillSpec);
396
+ const result = validateSkillsSpec(skillPath);
397
+ expect(result.valid).toBe(false);
398
+ expect(result.errors.some((e) => /When to Use/i.test(e))).toBe(true);
399
+ });
400
+ it('should warn for skill without examples section', async () => {
401
+ const { validateSkillsSpec } = await import('../validate-skills-spec.js');
402
+ const skillSpec = `# Skill Name
403
+
404
+ ## When to Use
405
+ When needed.
406
+
407
+ ## Key Concepts
408
+ Concepts only.
409
+ `;
410
+ const skillPath = path.join(tmpDir, 'SKILL.md');
411
+ writeFileSync(skillPath, skillSpec);
412
+ const result = validateSkillsSpec(skillPath);
413
+ expect(result.warnings.some((w) => /Examples/i.test(w))).toBe(true);
414
+ });
415
+ });
416
+ });