@lumenflow/cli 2.4.0 → 2.5.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 (124) hide show
  1. package/dist/__tests__/init-config-lanes.test.js +131 -0
  2. package/dist/__tests__/init-docs-structure.test.js +119 -0
  3. package/dist/__tests__/init-lane-inference.test.js +125 -0
  4. package/dist/__tests__/init-onboarding-docs.test.js +132 -0
  5. package/dist/__tests__/init-quick-ref.test.js +145 -0
  6. package/dist/__tests__/init-scripts.test.js +96 -0
  7. package/dist/__tests__/init-template-portability.test.js +97 -0
  8. package/dist/__tests__/init.test.js +7 -2
  9. package/dist/__tests__/initiative-add-wu.test.js +420 -0
  10. package/dist/__tests__/initiative-plan-replacement.test.js +162 -0
  11. package/dist/__tests__/initiative-remove-wu.test.js +458 -0
  12. package/dist/__tests__/onboarding-smoke-test.test.js +211 -0
  13. package/dist/__tests__/path-centralization-cli.test.js +234 -0
  14. package/dist/__tests__/plan-create.test.js +126 -0
  15. package/dist/__tests__/plan-edit.test.js +157 -0
  16. package/dist/__tests__/plan-link.test.js +239 -0
  17. package/dist/__tests__/plan-promote.test.js +181 -0
  18. package/dist/__tests__/wu-create-strict.test.js +118 -0
  19. package/dist/__tests__/wu-edit-strict.test.js +109 -0
  20. package/dist/__tests__/wu-validate-strict.test.js +113 -0
  21. package/dist/flow-bottlenecks.js +4 -2
  22. package/dist/gates.js +22 -0
  23. package/dist/init.js +580 -87
  24. package/dist/initiative-add-wu.js +112 -16
  25. package/dist/initiative-remove-wu.js +248 -0
  26. package/dist/onboarding-smoke-test.js +400 -0
  27. package/dist/plan-create.js +199 -0
  28. package/dist/plan-edit.js +235 -0
  29. package/dist/plan-link.js +233 -0
  30. package/dist/plan-promote.js +231 -0
  31. package/dist/wu-block.js +16 -5
  32. package/dist/wu-claim.js +15 -9
  33. package/dist/wu-create.js +50 -2
  34. package/dist/wu-deps.js +3 -1
  35. package/dist/wu-done.js +14 -5
  36. package/dist/wu-edit.js +35 -0
  37. package/dist/wu-spawn.js +8 -0
  38. package/dist/wu-unblock.js +34 -2
  39. package/dist/wu-validate.js +25 -17
  40. package/package.json +10 -6
  41. package/templates/core/AGENTS.md.template +2 -2
  42. package/dist/__tests__/init-plan.test.js +0 -340
  43. package/dist/agent-issues-query.d.ts +0 -16
  44. package/dist/agent-log-issue.d.ts +0 -10
  45. package/dist/agent-session-end.d.ts +0 -10
  46. package/dist/agent-session.d.ts +0 -10
  47. package/dist/backlog-prune.d.ts +0 -84
  48. package/dist/cli-entry-point.d.ts +0 -8
  49. package/dist/deps-add.d.ts +0 -91
  50. package/dist/deps-remove.d.ts +0 -17
  51. package/dist/docs-sync.d.ts +0 -50
  52. package/dist/file-delete.d.ts +0 -84
  53. package/dist/file-edit.d.ts +0 -82
  54. package/dist/file-read.d.ts +0 -92
  55. package/dist/file-write.d.ts +0 -90
  56. package/dist/flow-bottlenecks.d.ts +0 -16
  57. package/dist/flow-report.d.ts +0 -16
  58. package/dist/gates.d.ts +0 -94
  59. package/dist/git-branch.d.ts +0 -65
  60. package/dist/git-diff.d.ts +0 -58
  61. package/dist/git-log.d.ts +0 -69
  62. package/dist/git-status.d.ts +0 -58
  63. package/dist/guard-locked.d.ts +0 -62
  64. package/dist/guard-main-branch.d.ts +0 -50
  65. package/dist/guard-worktree-commit.d.ts +0 -59
  66. package/dist/index.d.ts +0 -10
  67. package/dist/init-plan.d.ts +0 -80
  68. package/dist/init-plan.js +0 -337
  69. package/dist/init.d.ts +0 -46
  70. package/dist/initiative-add-wu.d.ts +0 -22
  71. package/dist/initiative-bulk-assign-wus.d.ts +0 -16
  72. package/dist/initiative-create.d.ts +0 -28
  73. package/dist/initiative-edit.d.ts +0 -34
  74. package/dist/initiative-list.d.ts +0 -12
  75. package/dist/initiative-status.d.ts +0 -11
  76. package/dist/lumenflow-upgrade.d.ts +0 -103
  77. package/dist/mem-checkpoint.d.ts +0 -16
  78. package/dist/mem-cleanup.d.ts +0 -29
  79. package/dist/mem-create.d.ts +0 -17
  80. package/dist/mem-export.d.ts +0 -10
  81. package/dist/mem-inbox.d.ts +0 -35
  82. package/dist/mem-init.d.ts +0 -15
  83. package/dist/mem-ready.d.ts +0 -16
  84. package/dist/mem-signal.d.ts +0 -16
  85. package/dist/mem-start.d.ts +0 -16
  86. package/dist/mem-summarize.d.ts +0 -22
  87. package/dist/mem-triage.d.ts +0 -22
  88. package/dist/metrics-cli.d.ts +0 -90
  89. package/dist/metrics-snapshot.d.ts +0 -18
  90. package/dist/orchestrate-init-status.d.ts +0 -11
  91. package/dist/orchestrate-initiative.d.ts +0 -12
  92. package/dist/orchestrate-monitor.d.ts +0 -11
  93. package/dist/release.d.ts +0 -117
  94. package/dist/rotate-progress.d.ts +0 -48
  95. package/dist/session-coordinator.d.ts +0 -74
  96. package/dist/spawn-list.d.ts +0 -16
  97. package/dist/state-bootstrap.d.ts +0 -92
  98. package/dist/sync-templates.d.ts +0 -52
  99. package/dist/trace-gen.d.ts +0 -84
  100. package/dist/validate-agent-skills.d.ts +0 -50
  101. package/dist/validate-agent-sync.d.ts +0 -36
  102. package/dist/validate-backlog-sync.d.ts +0 -37
  103. package/dist/validate-skills-spec.d.ts +0 -40
  104. package/dist/validate.d.ts +0 -60
  105. package/dist/wu-block.d.ts +0 -16
  106. package/dist/wu-claim.d.ts +0 -74
  107. package/dist/wu-cleanup.d.ts +0 -35
  108. package/dist/wu-create.d.ts +0 -69
  109. package/dist/wu-delete.d.ts +0 -21
  110. package/dist/wu-deps.d.ts +0 -13
  111. package/dist/wu-done.d.ts +0 -225
  112. package/dist/wu-edit.d.ts +0 -63
  113. package/dist/wu-infer-lane.d.ts +0 -17
  114. package/dist/wu-preflight.d.ts +0 -47
  115. package/dist/wu-prune.d.ts +0 -16
  116. package/dist/wu-recover.d.ts +0 -37
  117. package/dist/wu-release.d.ts +0 -19
  118. package/dist/wu-repair.d.ts +0 -60
  119. package/dist/wu-spawn-completion.d.ts +0 -10
  120. package/dist/wu-spawn.d.ts +0 -192
  121. package/dist/wu-status.d.ts +0 -25
  122. package/dist/wu-unblock.d.ts +0 -16
  123. package/dist/wu-unlock-lane.d.ts +0 -19
  124. package/dist/wu-validate.d.ts +0 -16
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @file init-scripts.test.ts
3
+ * Test: Generated package.json scripts use correct format (wu-create, wu-claim, wu-done, gates)
4
+ *
5
+ * WU-1307: Fix lumenflow-init scaffolding
6
+ *
7
+ * The init command should inject standalone binary scripts that work
8
+ * in consumer projects without requiring the full @lumenflow/cli path.
9
+ */
10
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import * as os from 'node:os';
14
+ import { scaffoldProject } from '../init.js';
15
+ /** Package.json file name - extracted to avoid duplicate string lint errors */
16
+ const PACKAGE_JSON_FILE_NAME = 'package.json';
17
+ describe('init scripts generation (WU-1307)', () => {
18
+ let tempDir;
19
+ beforeEach(() => {
20
+ // Create a temporary directory for each test
21
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-init-scripts-'));
22
+ });
23
+ afterEach(() => {
24
+ // Clean up temporary directory
25
+ if (tempDir && fs.existsSync(tempDir)) {
26
+ fs.rmSync(tempDir, { recursive: true, force: true });
27
+ }
28
+ });
29
+ /** Helper to read and parse package.json from temp directory */
30
+ function readPackageJson() {
31
+ const packageJsonPath = path.join(tempDir, PACKAGE_JSON_FILE_NAME);
32
+ return JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
33
+ }
34
+ it('should generate package.json scripts using standalone binaries', async () => {
35
+ // Arrange
36
+ const packageJsonPath = path.join(tempDir, PACKAGE_JSON_FILE_NAME);
37
+ // Act
38
+ await scaffoldProject(tempDir, { force: true, full: true });
39
+ // Assert
40
+ expect(fs.existsSync(packageJsonPath)).toBe(true);
41
+ const packageJson = readPackageJson();
42
+ expect(packageJson.scripts).toBeDefined();
43
+ // Scripts should use standalone binary format (wu-create, wu-claim, etc.)
44
+ // NOT 'pnpm exec lumenflow' format
45
+ expect(packageJson.scripts?.['wu:claim']).toBe('wu-claim');
46
+ expect(packageJson.scripts?.['wu:done']).toBe('wu-done');
47
+ expect(packageJson.scripts?.['wu:create']).toBe('wu-create');
48
+ expect(packageJson.scripts?.gates).toBe('gates');
49
+ });
50
+ it('should NOT use pnpm exec lumenflow format', async () => {
51
+ // Act
52
+ await scaffoldProject(tempDir, { force: true, full: true });
53
+ // Assert
54
+ const packageJson = readPackageJson();
55
+ // Ensure scripts do NOT use the old 'pnpm exec lumenflow' format
56
+ const scriptValues = Object.values(packageJson.scripts ?? {});
57
+ const hasOldFormat = scriptValues.some((script) => script.includes('pnpm exec lumenflow'));
58
+ expect(hasOldFormat).toBe(false);
59
+ });
60
+ it('should include all essential WU lifecycle scripts', async () => {
61
+ // Act
62
+ await scaffoldProject(tempDir, { force: true, full: true });
63
+ // Assert
64
+ const packageJson = readPackageJson();
65
+ // Essential scripts that must be present
66
+ const essentialScripts = ['wu:claim', 'wu:done', 'wu:create', 'wu:status', 'gates'];
67
+ for (const scriptName of essentialScripts) {
68
+ expect(packageJson.scripts?.[scriptName]).toBeDefined();
69
+ }
70
+ });
71
+ it('should preserve existing scripts when updating package.json', async () => {
72
+ // Arrange
73
+ const packageJsonPath = path.join(tempDir, PACKAGE_JSON_FILE_NAME);
74
+ const existingPackageJson = {
75
+ name: 'test-project',
76
+ version: '1.0.0',
77
+ scripts: {
78
+ test: 'vitest',
79
+ build: 'tsc',
80
+ custom: 'echo hello',
81
+ },
82
+ };
83
+ fs.writeFileSync(packageJsonPath, JSON.stringify(existingPackageJson, null, 2));
84
+ // Act
85
+ await scaffoldProject(tempDir, { force: false, full: true });
86
+ // Assert
87
+ const packageJson = readPackageJson();
88
+ // Existing scripts should be preserved
89
+ expect(packageJson.scripts?.test).toBe('vitest');
90
+ expect(packageJson.scripts?.build).toBe('tsc');
91
+ expect(packageJson.scripts?.custom).toBe('echo hello');
92
+ // LumenFlow scripts should be added
93
+ expect(packageJson.scripts?.['wu:claim']).toBeDefined();
94
+ expect(packageJson.scripts?.gates).toBeDefined();
95
+ });
96
+ });
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @file init-template-portability.test.ts
3
+ * Tests for template portability - no absolute paths (WU-1309)
4
+ */
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
+ import * as os from 'node:os';
9
+ import { scaffoldProject } from '../init.js';
10
+ // Constants to avoid duplicate strings (sonarjs/no-duplicate-string)
11
+ const ARC42_DOCS_STRUCTURE = 'arc42';
12
+ const PROJECT_ROOT_PLACEHOLDER = '<project-root>';
13
+ describe('template portability', () => {
14
+ let tempDir;
15
+ beforeEach(() => {
16
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-portability-test-'));
17
+ });
18
+ afterEach(() => {
19
+ fs.rmSync(tempDir, { recursive: true, force: true });
20
+ });
21
+ describe('no absolute paths in templates', () => {
22
+ it('should use <project-root> placeholder instead of absolute paths', async () => {
23
+ const options = {
24
+ force: false,
25
+ full: true,
26
+ client: 'claude',
27
+ };
28
+ await scaffoldProject(tempDir, options);
29
+ // Check LUMENFLOW.md for absolute paths
30
+ const lumenflowContent = fs.readFileSync(path.join(tempDir, 'LUMENFLOW.md'), 'utf-8');
31
+ expect(lumenflowContent).not.toMatch(/\/home\//);
32
+ expect(lumenflowContent).not.toMatch(/\/Users\//);
33
+ expect(lumenflowContent).not.toMatch(/C:\\/);
34
+ // Should contain <project-root> placeholder for portable references
35
+ expect(lumenflowContent).toContain(PROJECT_ROOT_PLACEHOLDER);
36
+ });
37
+ it('should not contain hardcoded user paths in AGENTS.md', async () => {
38
+ const options = {
39
+ force: false,
40
+ full: true,
41
+ };
42
+ await scaffoldProject(tempDir, options);
43
+ const agentsContent = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
44
+ expect(agentsContent).not.toMatch(/\/home\/[a-zA-Z0-9_-]+\//);
45
+ expect(agentsContent).not.toMatch(/\/Users\/[a-zA-Z0-9_-]+\//);
46
+ expect(agentsContent).not.toMatch(/C:\\Users\\[a-zA-Z0-9_-]+\\/);
47
+ });
48
+ it('should not contain hardcoded paths in quick-ref-commands.md', async () => {
49
+ const options = {
50
+ force: false,
51
+ full: true,
52
+ docsStructure: ARC42_DOCS_STRUCTURE,
53
+ };
54
+ await scaffoldProject(tempDir, options);
55
+ // Find the quick-ref-commands.md based on docs structure (arc42)
56
+ const quickRefPath = path.join(tempDir, 'docs', '04-operations', '_frameworks', 'lumenflow', 'agent', 'onboarding', 'quick-ref-commands.md');
57
+ if (fs.existsSync(quickRefPath)) {
58
+ const quickRefContent = fs.readFileSync(quickRefPath, 'utf-8');
59
+ expect(quickRefContent).not.toMatch(/\/home\/[a-zA-Z0-9_-]+\//);
60
+ expect(quickRefContent).not.toMatch(/\/Users\/[a-zA-Z0-9_-]+\//);
61
+ expect(quickRefContent).toContain(PROJECT_ROOT_PLACEHOLDER);
62
+ }
63
+ });
64
+ it('should use relative paths for docs cross-references', async () => {
65
+ const options = {
66
+ force: false,
67
+ full: true,
68
+ client: 'claude',
69
+ docsStructure: ARC42_DOCS_STRUCTURE,
70
+ };
71
+ await scaffoldProject(tempDir, options);
72
+ // Starting prompt should have relative paths to other docs
73
+ const onboardingDir = path.join(tempDir, 'docs', '04-operations', '_frameworks', 'lumenflow', 'agent', 'onboarding');
74
+ const startingPromptPath = path.join(onboardingDir, 'starting-prompt.md');
75
+ if (fs.existsSync(startingPromptPath)) {
76
+ const content = fs.readFileSync(startingPromptPath, 'utf-8');
77
+ // Should use relative paths like ../../../../../../LUMENFLOW.md
78
+ // eslint-disable-next-line sonarjs/slow-regex -- Simple path pattern, no backtracking risk
79
+ expect(content).toMatch(/\[.*?\]\([./]+.*?\.md\)/);
80
+ }
81
+ });
82
+ });
83
+ describe('PROJECT_ROOT token replacement', () => {
84
+ it('should replace {{PROJECT_ROOT}} with <project-root> placeholder', async () => {
85
+ const options = {
86
+ force: false,
87
+ full: true,
88
+ };
89
+ await scaffoldProject(tempDir, options);
90
+ const lumenflowContent = fs.readFileSync(path.join(tempDir, 'LUMENFLOW.md'), 'utf-8');
91
+ // Should not have unreplaced {{PROJECT_ROOT}} tokens
92
+ expect(lumenflowContent).not.toContain('{{PROJECT_ROOT}}');
93
+ // Should have the portable placeholder
94
+ expect(lumenflowContent).toContain('<project-root>');
95
+ });
96
+ });
97
+ });
@@ -268,6 +268,7 @@ describe('lumenflow init', () => {
268
268
  const options = {
269
269
  force: false,
270
270
  full: true, // This is now the default when parsed
271
+ docsStructure: 'arc42', // WU-1309: Explicitly request arc42 for legacy test
271
272
  };
272
273
  await scaffoldProject(tempDir, options);
273
274
  // Should create agent onboarding docs
@@ -311,8 +312,10 @@ describe('lumenflow init', () => {
311
312
  const laneInferencePath = path.join(tempDir, '.lumenflow.lane-inference.yaml');
312
313
  expect(fs.existsSync(laneInferencePath)).toBe(true);
313
314
  const content = fs.readFileSync(laneInferencePath, 'utf-8');
314
- // Should have lane definitions
315
- expect(content).toContain('lanes:');
315
+ // WU-1307: Should have hierarchical lane definitions (not flat lanes: array)
316
+ expect(content).toContain('Framework:');
317
+ expect(content).toContain('Content:');
318
+ expect(content).toContain('Operations:');
316
319
  });
317
320
  it('should scaffold lane-inference with framework-specific lanes when --framework is provided', async () => {
318
321
  const options = {
@@ -330,6 +333,7 @@ describe('lumenflow init', () => {
330
333
  const options = {
331
334
  force: false,
332
335
  full: true,
336
+ docsStructure: 'arc42', // WU-1309: Explicitly request arc42 for legacy test
333
337
  };
334
338
  await scaffoldProject(tempDir, options);
335
339
  const onboardingDir = path.join(tempDir, ONBOARDING_DOCS_PATH);
@@ -381,6 +385,7 @@ describe('lumenflow init', () => {
381
385
  const options = {
382
386
  force: false,
383
387
  full: true,
388
+ docsStructure: 'arc42', // WU-1309: Explicitly request arc42 for legacy test
384
389
  };
385
390
  await scaffoldProject(tempDir, options);
386
391
  const agentsContent = fs.readFileSync(path.join(tempDir, 'AGENTS.md'), 'utf-8');
@@ -0,0 +1,420 @@
1
+ /**
2
+ * Tests for initiative:add-wu command validation (WU-1330)
3
+ *
4
+ * The initiative:add-wu command now validates WU specs before linking.
5
+ * This ensures only valid, complete WUs can be linked to initiatives.
6
+ *
7
+ * TDD: These tests are written BEFORE the implementation.
8
+ */
9
+ import { describe, it, expect, vi, beforeEach, afterEach, beforeAll } from 'vitest';
10
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { tmpdir } from 'node:os';
13
+ import { stringifyYAML } from '@lumenflow/core/dist/wu-yaml.js';
14
+ // Test constants to avoid lint warnings about duplicate strings
15
+ const TEST_WU_ID = 'WU-123';
16
+ const TEST_INIT_ID = 'INIT-001';
17
+ const TEST_LANE = 'Framework: CLI';
18
+ const WU_REL_PATH = 'docs/04-operations/tasks/wu';
19
+ const INIT_REL_PATH = 'docs/04-operations/tasks/initiatives';
20
+ const TEST_INIT_SLUG = 'test-initiative';
21
+ const TEST_INIT_TITLE = 'Test Initiative';
22
+ const TEST_INIT_STATUS = 'open';
23
+ const TEST_DATE = '2026-01-25';
24
+ const MIN_DESCRIPTION_LENGTH = 50;
25
+ // Valid WU document template
26
+ const createValidWUDoc = (overrides = {}) => ({
27
+ id: TEST_WU_ID,
28
+ title: 'Test Work Unit Title',
29
+ lane: TEST_LANE,
30
+ status: 'ready',
31
+ type: 'feature',
32
+ priority: 'P2',
33
+ created: TEST_DATE,
34
+ description: 'Context: Testing WU validation. Problem: No validation on add-wu. Solution: Add strict validation before linking.',
35
+ acceptance: ['WU validates schema', 'Invalid WUs rejected', 'Valid WUs linked bidirectionally'],
36
+ code_paths: ['packages/@lumenflow/cli/src/initiative-add-wu.ts'],
37
+ tests: { unit: ['packages/@lumenflow/cli/src/__tests__/initiative-add-wu.test.ts'] },
38
+ exposure: 'backend-only',
39
+ ...overrides,
40
+ });
41
+ // Valid initiative document template
42
+ const createValidInitDoc = (overrides = {}) => ({
43
+ id: TEST_INIT_ID,
44
+ slug: TEST_INIT_SLUG,
45
+ title: TEST_INIT_TITLE,
46
+ status: TEST_INIT_STATUS,
47
+ created: TEST_DATE,
48
+ wus: [],
49
+ ...overrides,
50
+ });
51
+ // Pre-import the module to ensure coverage tracking includes the module itself
52
+ beforeAll(async () => {
53
+ await import('../initiative-add-wu.js');
54
+ });
55
+ // Mock modules before importing the module under test
56
+ const mockGit = {
57
+ branch: vi.fn().mockResolvedValue({ current: 'main' }),
58
+ status: vi.fn().mockResolvedValue({ isClean: () => true }),
59
+ };
60
+ vi.mock('@lumenflow/core/dist/git-adapter.js', () => ({
61
+ getGitForCwd: vi.fn(() => mockGit),
62
+ }));
63
+ vi.mock('@lumenflow/core/dist/wu-helpers.js', () => ({
64
+ ensureOnMain: vi.fn().mockResolvedValue(undefined),
65
+ }));
66
+ vi.mock('@lumenflow/core/dist/micro-worktree.js', async (importOriginal) => {
67
+ const actual = await importOriginal();
68
+ return {
69
+ ...actual,
70
+ withMicroWorktree: vi.fn(async ({ execute }) => {
71
+ // Simulate micro-worktree by executing in temp dir
72
+ const tempDir = join(tmpdir(), `init-add-wu-test-${Date.now()}`);
73
+ mkdirSync(tempDir, { recursive: true });
74
+ try {
75
+ await execute({ worktreePath: tempDir });
76
+ }
77
+ finally {
78
+ // Cleanup handled by test
79
+ }
80
+ }),
81
+ };
82
+ });
83
+ describe('initiative:add-wu WU validation (WU-1330)', () => {
84
+ let tempDir;
85
+ let originalCwd;
86
+ beforeEach(() => {
87
+ tempDir = join(tmpdir(), `init-add-wu-validation-test-${Date.now()}`);
88
+ mkdirSync(tempDir, { recursive: true });
89
+ originalCwd = process.cwd();
90
+ });
91
+ afterEach(() => {
92
+ process.chdir(originalCwd);
93
+ if (existsSync(tempDir)) {
94
+ rmSync(tempDir, { recursive: true, force: true });
95
+ }
96
+ vi.clearAllMocks();
97
+ });
98
+ describe('validateWUForLinking', () => {
99
+ it('should return valid for a well-formed WU', async () => {
100
+ const { validateWUForLinking } = await import('../initiative-add-wu.js');
101
+ // Create a valid WU file
102
+ const wuDir = join(tempDir, WU_REL_PATH);
103
+ mkdirSync(wuDir, { recursive: true });
104
+ const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
105
+ writeFileSync(wuPath, stringifyYAML(createValidWUDoc()));
106
+ process.chdir(tempDir);
107
+ const result = validateWUForLinking(TEST_WU_ID);
108
+ expect(result.valid).toBe(true);
109
+ expect(result.errors).toHaveLength(0);
110
+ });
111
+ it('should reject WU with missing required fields', async () => {
112
+ const { validateWUForLinking } = await import('../initiative-add-wu.js');
113
+ // Create a WU missing required fields (no description)
114
+ const wuDir = join(tempDir, WU_REL_PATH);
115
+ mkdirSync(wuDir, { recursive: true });
116
+ const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
117
+ writeFileSync(wuPath, stringifyYAML({
118
+ id: TEST_WU_ID,
119
+ title: 'Test',
120
+ lane: TEST_LANE,
121
+ status: 'ready',
122
+ created: TEST_DATE,
123
+ // Missing: description, acceptance, code_paths
124
+ }));
125
+ process.chdir(tempDir);
126
+ const result = validateWUForLinking(TEST_WU_ID);
127
+ expect(result.valid).toBe(false);
128
+ expect(result.errors.length).toBeGreaterThan(0);
129
+ expect(result.errors.some((e) => e.toLowerCase().includes('description'))).toBe(true);
130
+ });
131
+ it('should reject WU with invalid schema', async () => {
132
+ const { validateWUForLinking } = await import('../initiative-add-wu.js');
133
+ // Create a WU with invalid status
134
+ const wuDir = join(tempDir, WU_REL_PATH);
135
+ mkdirSync(wuDir, { recursive: true });
136
+ const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
137
+ writeFileSync(wuPath, stringifyYAML({
138
+ ...createValidWUDoc(),
139
+ status: 'invalid_status', // Invalid status value
140
+ }));
141
+ process.chdir(tempDir);
142
+ const result = validateWUForLinking(TEST_WU_ID);
143
+ expect(result.valid).toBe(false);
144
+ expect(result.errors.some((e) => e.toLowerCase().includes('status'))).toBe(true);
145
+ });
146
+ it('should reject WU with description containing placeholder marker', async () => {
147
+ const { validateWUForLinking } = await import('../initiative-add-wu.js');
148
+ // Create a WU with placeholder in description
149
+ const wuDir = join(tempDir, WU_REL_PATH);
150
+ mkdirSync(wuDir, { recursive: true });
151
+ const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
152
+ writeFileSync(wuPath, stringifyYAML({
153
+ ...createValidWUDoc(),
154
+ description: '[PLACEHOLDER] This is a placeholder description that is long enough.',
155
+ }));
156
+ process.chdir(tempDir);
157
+ const result = validateWUForLinking(TEST_WU_ID);
158
+ expect(result.valid).toBe(false);
159
+ expect(result.errors.some((e) => e.includes('PLACEHOLDER'))).toBe(true);
160
+ });
161
+ it('should reject WU with too short description', async () => {
162
+ const { validateWUForLinking } = await import('../initiative-add-wu.js');
163
+ // Create a WU with short description
164
+ const wuDir = join(tempDir, WU_REL_PATH);
165
+ mkdirSync(wuDir, { recursive: true });
166
+ const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
167
+ writeFileSync(wuPath, stringifyYAML({
168
+ ...createValidWUDoc(),
169
+ description: 'Too short', // Less than MIN_DESCRIPTION_LENGTH
170
+ }));
171
+ process.chdir(tempDir);
172
+ const result = validateWUForLinking(TEST_WU_ID);
173
+ expect(result.valid).toBe(false);
174
+ expect(result.errors.some((e) => e.includes(`${MIN_DESCRIPTION_LENGTH}`))).toBe(true);
175
+ });
176
+ it('should reject WU with invalid ID format', async () => {
177
+ const { validateWUForLinking } = await import('../initiative-add-wu.js');
178
+ // Create a WU with invalid ID
179
+ const invalidId = 'INVALID-123';
180
+ const wuDir = join(tempDir, WU_REL_PATH);
181
+ mkdirSync(wuDir, { recursive: true });
182
+ const wuPath = join(wuDir, `${invalidId}.yaml`);
183
+ writeFileSync(wuPath, stringifyYAML({
184
+ ...createValidWUDoc(),
185
+ id: invalidId,
186
+ }));
187
+ process.chdir(tempDir);
188
+ const result = validateWUForLinking(invalidId);
189
+ expect(result.valid).toBe(false);
190
+ expect(result.errors.some((e) => e.toLowerCase().includes('id'))).toBe(true);
191
+ });
192
+ it('should reject WU that does not exist', async () => {
193
+ const { validateWUForLinking } = await import('../initiative-add-wu.js');
194
+ process.chdir(tempDir);
195
+ const result = validateWUForLinking('WU-999');
196
+ expect(result.valid).toBe(false);
197
+ expect(result.errors.some((e) => e.toLowerCase().includes('not found'))).toBe(true);
198
+ });
199
+ it('should reject WU with empty acceptance criteria', async () => {
200
+ const { validateWUForLinking } = await import('../initiative-add-wu.js');
201
+ // Create a WU with empty acceptance
202
+ const wuDir = join(tempDir, WU_REL_PATH);
203
+ mkdirSync(wuDir, { recursive: true });
204
+ const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
205
+ writeFileSync(wuPath, stringifyYAML({
206
+ ...createValidWUDoc(),
207
+ acceptance: [], // Empty array
208
+ }));
209
+ process.chdir(tempDir);
210
+ const result = validateWUForLinking(TEST_WU_ID);
211
+ expect(result.valid).toBe(false);
212
+ expect(result.errors.some((e) => e.toLowerCase().includes('acceptance'))).toBe(true);
213
+ });
214
+ it('should aggregate multiple errors', async () => {
215
+ const { validateWUForLinking } = await import('../initiative-add-wu.js');
216
+ // Create a WU with multiple issues
217
+ const wuDir = join(tempDir, WU_REL_PATH);
218
+ mkdirSync(wuDir, { recursive: true });
219
+ const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
220
+ writeFileSync(wuPath, stringifyYAML({
221
+ id: TEST_WU_ID,
222
+ title: '', // Empty title
223
+ lane: TEST_LANE,
224
+ status: 'invalid_status', // Invalid status
225
+ created: TEST_DATE,
226
+ description: 'short', // Too short
227
+ acceptance: [], // Empty
228
+ }));
229
+ process.chdir(tempDir);
230
+ const result = validateWUForLinking(TEST_WU_ID);
231
+ expect(result.valid).toBe(false);
232
+ // Should have multiple errors aggregated
233
+ expect(result.errors.length).toBeGreaterThanOrEqual(2);
234
+ });
235
+ it('should include warnings but still be valid', async () => {
236
+ const { validateWUForLinking } = await import('../initiative-add-wu.js');
237
+ // Create a valid WU that might have warnings (missing optional recommended fields)
238
+ const wuDir = join(tempDir, WU_REL_PATH);
239
+ mkdirSync(wuDir, { recursive: true });
240
+ const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
241
+ writeFileSync(wuPath, stringifyYAML({
242
+ ...createValidWUDoc(),
243
+ notes: '', // Empty notes - should produce warning
244
+ spec_refs: [], // Empty spec_refs for feature - should produce warning
245
+ }));
246
+ process.chdir(tempDir);
247
+ const result = validateWUForLinking(TEST_WU_ID);
248
+ // Should be valid (warnings don't block)
249
+ expect(result.valid).toBe(true);
250
+ // But should have warnings
251
+ expect(result.warnings.length).toBeGreaterThan(0);
252
+ });
253
+ });
254
+ describe('checkWUExists with validation', () => {
255
+ it('should throw for invalid WU when strict validation enabled', async () => {
256
+ const { checkWUExistsAndValidate } = await import('../initiative-add-wu.js');
257
+ // Create an invalid WU file
258
+ const wuDir = join(tempDir, WU_REL_PATH);
259
+ mkdirSync(wuDir, { recursive: true });
260
+ const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
261
+ writeFileSync(wuPath, stringifyYAML({
262
+ id: TEST_WU_ID,
263
+ title: 'Test',
264
+ lane: TEST_LANE,
265
+ status: 'ready',
266
+ created: TEST_DATE,
267
+ description: 'short', // Too short
268
+ }));
269
+ process.chdir(tempDir);
270
+ // Should throw with aggregated validation errors
271
+ expect(() => checkWUExistsAndValidate(TEST_WU_ID)).toThrow();
272
+ });
273
+ it('should return WU doc when validation passes', async () => {
274
+ const { checkWUExistsAndValidate } = await import('../initiative-add-wu.js');
275
+ // Create a valid WU file
276
+ const wuDir = join(tempDir, WU_REL_PATH);
277
+ mkdirSync(wuDir, { recursive: true });
278
+ const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
279
+ writeFileSync(wuPath, stringifyYAML(createValidWUDoc()));
280
+ process.chdir(tempDir);
281
+ const result = checkWUExistsAndValidate(TEST_WU_ID);
282
+ expect(result.id).toBe(TEST_WU_ID);
283
+ });
284
+ });
285
+ describe('initiative:add-wu integration', () => {
286
+ it('should reject linking invalid WU with clear error message', async () => {
287
+ // This is an integration test scenario - main() calls validation before linking
288
+ // The main() function should call validateWUForLinking and die() with aggregated errors
289
+ const { validateWUForLinking } = await import('../initiative-add-wu.js');
290
+ // Setup invalid WU
291
+ const wuDir = join(tempDir, WU_REL_PATH);
292
+ mkdirSync(wuDir, { recursive: true });
293
+ const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
294
+ writeFileSync(wuPath, stringifyYAML({
295
+ id: TEST_WU_ID,
296
+ title: 'Test',
297
+ lane: TEST_LANE,
298
+ status: 'ready',
299
+ created: TEST_DATE,
300
+ description: 'Too short',
301
+ }));
302
+ process.chdir(tempDir);
303
+ const result = validateWUForLinking(TEST_WU_ID);
304
+ // The error message should be suitable for display to user
305
+ expect(result.valid).toBe(false);
306
+ expect(result.errors.join('\n')).toContain('50'); // Should mention minimum length
307
+ });
308
+ it('should successfully link valid WU bidirectionally', async () => {
309
+ // This test verifies that after validation passes, bidirectional linking works
310
+ // The existing functionality should still work for valid WUs
311
+ const { validateWUForLinking } = await import('../initiative-add-wu.js');
312
+ // Setup valid WU and initiative
313
+ const wuDir = join(tempDir, WU_REL_PATH);
314
+ const initDir = join(tempDir, INIT_REL_PATH);
315
+ mkdirSync(wuDir, { recursive: true });
316
+ mkdirSync(initDir, { recursive: true });
317
+ const wuPath = join(wuDir, `${TEST_WU_ID}.yaml`);
318
+ const initPath = join(initDir, `${TEST_INIT_ID}.yaml`);
319
+ writeFileSync(wuPath, stringifyYAML(createValidWUDoc()));
320
+ writeFileSync(initPath, stringifyYAML(createValidInitDoc()));
321
+ process.chdir(tempDir);
322
+ // Validation should pass
323
+ const result = validateWUForLinking(TEST_WU_ID);
324
+ expect(result.valid).toBe(true);
325
+ });
326
+ });
327
+ describe('error formatting', () => {
328
+ it('should format errors in human-readable format', async () => {
329
+ const { formatValidationErrors } = await import('../initiative-add-wu.js');
330
+ const errors = ['description: Description is required', 'acceptance: At least one criterion'];
331
+ const wuId = TEST_WU_ID;
332
+ const formatted = formatValidationErrors(wuId, errors);
333
+ expect(formatted).toContain(wuId);
334
+ expect(formatted).toContain('description');
335
+ expect(formatted).toContain('acceptance');
336
+ });
337
+ });
338
+ describe('exports', () => {
339
+ it('should export validateWUForLinking function', async () => {
340
+ const mod = await import('../initiative-add-wu.js');
341
+ expect(typeof mod.validateWUForLinking).toBe('function');
342
+ });
343
+ it('should export checkWUExistsAndValidate function', async () => {
344
+ const mod = await import('../initiative-add-wu.js');
345
+ expect(typeof mod.checkWUExistsAndValidate).toBe('function');
346
+ });
347
+ it('should export formatValidationErrors function', async () => {
348
+ const mod = await import('../initiative-add-wu.js');
349
+ expect(typeof mod.formatValidationErrors).toBe('function');
350
+ });
351
+ it('should export isRetryExhaustionError function (WU-1333)', async () => {
352
+ const mod = await import('../initiative-add-wu.js');
353
+ expect(typeof mod.isRetryExhaustionError).toBe('function');
354
+ });
355
+ it('should export formatRetryExhaustionError function (WU-1333)', async () => {
356
+ const mod = await import('../initiative-add-wu.js');
357
+ expect(typeof mod.formatRetryExhaustionError).toBe('function');
358
+ });
359
+ });
360
+ });
361
+ /**
362
+ * WU-1333: Retry handling tests for initiative:add-wu
363
+ *
364
+ * When origin/main moves during operation, the micro-worktree layer handles retry.
365
+ * When retries are exhausted, the error message should include actionable next steps.
366
+ */
367
+ describe('initiative:add-wu retry handling (WU-1333)', () => {
368
+ describe('isRetryExhaustionError', () => {
369
+ it('should detect retry exhaustion from error message', async () => {
370
+ const { isRetryExhaustionError } = await import('../initiative-add-wu.js');
371
+ // Should detect retry exhaustion error
372
+ const retryError = new Error('Push failed after 3 attempts. Origin main may have significant traffic.');
373
+ expect(isRetryExhaustionError(retryError)).toBe(true);
374
+ });
375
+ it('should detect retry exhaustion with any attempt count', async () => {
376
+ const { isRetryExhaustionError } = await import('../initiative-add-wu.js');
377
+ // Different attempt counts should still match
378
+ const error5 = new Error('Push failed after 5 attempts. Something.');
379
+ expect(isRetryExhaustionError(error5)).toBe(true);
380
+ const error1 = new Error('Push failed after 1 attempts. Something.');
381
+ expect(isRetryExhaustionError(error1)).toBe(true);
382
+ });
383
+ it('should not match other errors', async () => {
384
+ const { isRetryExhaustionError } = await import('../initiative-add-wu.js');
385
+ const otherError = new Error('Some other error');
386
+ expect(isRetryExhaustionError(otherError)).toBe(false);
387
+ const networkError = new Error('Network unreachable');
388
+ expect(isRetryExhaustionError(networkError)).toBe(false);
389
+ });
390
+ });
391
+ describe('formatRetryExhaustionError', () => {
392
+ it('should include actionable next steps', async () => {
393
+ const { formatRetryExhaustionError } = await import('../initiative-add-wu.js');
394
+ const retryError = new Error('Push failed after 3 attempts. Origin main may have significant traffic.');
395
+ const formatted = formatRetryExhaustionError(retryError, TEST_WU_ID, TEST_INIT_ID);
396
+ // Should include the original error
397
+ expect(formatted).toContain('Push failed after 3 attempts');
398
+ // Should include next steps heading
399
+ expect(formatted).toContain('Next steps:');
400
+ // Should include actionable suggestions
401
+ expect(formatted).toContain('Wait a few seconds and retry');
402
+ expect(formatted).toContain('initiative:add-wu');
403
+ });
404
+ it('should include the retry command', async () => {
405
+ const { formatRetryExhaustionError } = await import('../initiative-add-wu.js');
406
+ const retryError = new Error('Push failed after 3 attempts.');
407
+ const formatted = formatRetryExhaustionError(retryError, TEST_WU_ID, TEST_INIT_ID);
408
+ // Should include command to retry
409
+ expect(formatted).toContain(`--wu ${TEST_WU_ID}`);
410
+ expect(formatted).toContain(`--initiative ${TEST_INIT_ID}`);
411
+ });
412
+ it('should suggest checking for concurrent agents', async () => {
413
+ const { formatRetryExhaustionError } = await import('../initiative-add-wu.js');
414
+ const retryError = new Error('Push failed after 3 attempts.');
415
+ const formatted = formatRetryExhaustionError(retryError, TEST_WU_ID, TEST_INIT_ID);
416
+ // Should mention concurrent agents as possible cause
417
+ expect(formatted).toMatch(/concurrent|agent|traffic/i);
418
+ });
419
+ });
420
+ });