@codemcp/workflows 4.10.1 → 4.10.3

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 (93) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/components/beads/beads-instruction-generator.d.ts +4 -8
  3. package/dist/components/beads/beads-instruction-generator.d.ts.map +1 -1
  4. package/dist/components/beads/beads-instruction-generator.js +28 -51
  5. package/dist/components/beads/beads-instruction-generator.js.map +1 -1
  6. package/dist/components/beads/beads-task-backend-client.d.ts.map +1 -1
  7. package/dist/components/beads/beads-task-backend-client.js +1 -4
  8. package/dist/components/beads/beads-task-backend-client.js.map +1 -1
  9. package/dist/plugin-system/beads-plugin.d.ts +70 -0
  10. package/dist/plugin-system/beads-plugin.d.ts.map +1 -0
  11. package/dist/plugin-system/beads-plugin.js +459 -0
  12. package/dist/plugin-system/beads-plugin.js.map +1 -0
  13. package/dist/plugin-system/index.d.ts +9 -0
  14. package/dist/plugin-system/index.d.ts.map +1 -0
  15. package/dist/plugin-system/index.js +9 -0
  16. package/dist/plugin-system/index.js.map +1 -0
  17. package/dist/plugin-system/plugin-interfaces.d.ts +99 -0
  18. package/dist/plugin-system/plugin-interfaces.d.ts.map +1 -0
  19. package/dist/plugin-system/plugin-interfaces.js +9 -0
  20. package/dist/plugin-system/plugin-interfaces.js.map +1 -0
  21. package/dist/plugin-system/plugin-registry.d.ts +44 -0
  22. package/dist/plugin-system/plugin-registry.d.ts.map +1 -0
  23. package/dist/plugin-system/plugin-registry.js +132 -0
  24. package/dist/plugin-system/plugin-registry.js.map +1 -0
  25. package/dist/server-config.d.ts.map +1 -1
  26. package/dist/server-config.js +28 -8
  27. package/dist/server-config.js.map +1 -1
  28. package/dist/tool-handlers/conduct-review.d.ts.map +1 -1
  29. package/dist/tool-handlers/conduct-review.js +1 -2
  30. package/dist/tool-handlers/conduct-review.js.map +1 -1
  31. package/dist/tool-handlers/get-tool-info.d.ts +0 -1
  32. package/dist/tool-handlers/get-tool-info.d.ts.map +1 -1
  33. package/dist/tool-handlers/get-tool-info.js +0 -1
  34. package/dist/tool-handlers/get-tool-info.js.map +1 -1
  35. package/dist/tool-handlers/proceed-to-phase.d.ts +0 -7
  36. package/dist/tool-handlers/proceed-to-phase.d.ts.map +1 -1
  37. package/dist/tool-handlers/proceed-to-phase.js +15 -95
  38. package/dist/tool-handlers/proceed-to-phase.js.map +1 -1
  39. package/dist/tool-handlers/resume-workflow.d.ts +0 -1
  40. package/dist/tool-handlers/resume-workflow.d.ts.map +1 -1
  41. package/dist/tool-handlers/resume-workflow.js +0 -1
  42. package/dist/tool-handlers/resume-workflow.js.map +1 -1
  43. package/dist/tool-handlers/start-development.d.ts +0 -16
  44. package/dist/tool-handlers/start-development.d.ts.map +1 -1
  45. package/dist/tool-handlers/start-development.js +29 -130
  46. package/dist/tool-handlers/start-development.js.map +1 -1
  47. package/dist/tool-handlers/whats-next.d.ts +0 -2
  48. package/dist/tool-handlers/whats-next.d.ts.map +1 -1
  49. package/dist/tool-handlers/whats-next.js +1 -2
  50. package/dist/tool-handlers/whats-next.js.map +1 -1
  51. package/dist/types.d.ts +2 -0
  52. package/dist/types.d.ts.map +1 -1
  53. package/package.json +2 -2
  54. package/src/components/beads/beads-instruction-generator.ts +32 -64
  55. package/src/components/beads/beads-task-backend-client.ts +1 -4
  56. package/src/plugin-system/beads-plugin.ts +641 -0
  57. package/src/plugin-system/index.ts +20 -0
  58. package/src/plugin-system/plugin-interfaces.ts +154 -0
  59. package/src/plugin-system/plugin-registry.ts +190 -0
  60. package/src/server-config.ts +30 -8
  61. package/src/tool-handlers/conduct-review.ts +1 -2
  62. package/src/tool-handlers/get-tool-info.ts +0 -2
  63. package/src/tool-handlers/proceed-to-phase.ts +19 -139
  64. package/src/tool-handlers/resume-workflow.ts +0 -2
  65. package/src/tool-handlers/start-development.ts +35 -213
  66. package/src/tool-handlers/whats-next.ts +1 -4
  67. package/src/types.ts +2 -0
  68. package/test/e2e/beads-plugin-integration.test.ts +1594 -0
  69. package/test/e2e/core-functionality.test.ts +3 -12
  70. package/test/e2e/mcp-contract.test.ts +0 -31
  71. package/test/e2e/plugin-system-integration.test.ts +1421 -0
  72. package/test/e2e/state-management.test.ts +1 -5
  73. package/test/e2e/workflow-integration.test.ts +2 -11
  74. package/test/unit/beads-instruction-generator.test.ts +235 -103
  75. package/test/unit/beads-phase-task-id-integration.test.ts +7 -29
  76. package/test/unit/beads-plugin-behavioral.test.ts +512 -0
  77. package/test/unit/beads-plugin.test.ts +94 -0
  78. package/test/unit/plugin-error-handling.test.ts +240 -0
  79. package/test/unit/proceed-to-phase-plugin-integration.test.ts +150 -0
  80. package/test/unit/resume-workflow.test.ts +0 -1
  81. package/test/unit/server-config-plugin-registry.test.ts +81 -0
  82. package/test/unit/server-tools.test.ts +0 -1
  83. package/test/unit/start-development-goal-extraction.test.ts +22 -16
  84. package/test/utils/test-helpers.ts +3 -1
  85. package/tsconfig.build.tsbuildinfo +1 -1
  86. package/dist/components/server-components-factory.d.ts +0 -39
  87. package/dist/components/server-components-factory.d.ts.map +0 -1
  88. package/dist/components/server-components-factory.js +0 -62
  89. package/dist/components/server-components-factory.js.map +0 -1
  90. package/src/components/server-components-factory.ts +0 -86
  91. package/test/e2e/component-substitution.test.ts +0 -208
  92. package/test/unit/beads-integration-filename.test.ts +0 -93
  93. package/test/unit/server-components-factory.test.ts +0 -279
@@ -2,7 +2,7 @@
2
2
  * Phase-Specific Task ID Integration Tests for BeadsInstructionGenerator
3
3
  *
4
4
  * Tests that validate BeadsInstructionGenerator's ability to extract phase task IDs
5
- * from plan files and integrate them properly into BD CLI commands.
5
+ * from plan files and integrate them properly into bd commands.
6
6
  */
7
7
 
8
8
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
@@ -43,6 +43,7 @@ describe('Phase-Specific Task ID Integration Tests', () => {
43
43
  mockInstructionContext = {
44
44
  phase: 'design',
45
45
  conversationContext: mockConversationContext,
46
+ instructionSource: 'whats_next',
46
47
  transitionReason: 'test transition',
47
48
  isModeled: false,
48
49
  planFileExists: true,
@@ -89,10 +90,7 @@ Some implementation tasks here.
89
90
  expect(result.instructions).toContain(
90
91
  "bd create 'Task description' --parent project-epic-1.2"
91
92
  );
92
- expect(result.instructions).toContain('bd show project-epic-1.2');
93
- expect(result.instructions).toContain(
94
- 'All work items should be created as children of project-epic-1.2'
95
- );
93
+ // Removed 'bd show' expectation - no longer part of minimal output
96
94
  });
97
95
 
98
96
  it('should handle phase task IDs with various formats', async () => {
@@ -161,10 +159,6 @@ Some implementation tasks here.
161
159
  ).toContain(
162
160
  `bd list --parent phase-${mapping.phase}-123 --status open`
163
161
  );
164
- expect(
165
- result.instructions,
166
- `Should capitalize phase name correctly: ${mapping.phase}`
167
- ).toContain(`You are currently in the ${mapping.header} phase`);
168
162
  }
169
163
  });
170
164
 
@@ -354,8 +348,8 @@ ${edgeCase.comment}
354
348
  });
355
349
  });
356
350
 
357
- describe('BD CLI Command Integration', () => {
358
- it('should integrate extracted phase task ID into all relevant BD CLI commands', async () => {
351
+ describe('bd Command Integration', () => {
352
+ it('should integrate extracted phase task ID into all relevant bd commands', async () => {
359
353
  const planContent = `# Project Plan
360
354
 
361
355
  ## Design
@@ -370,24 +364,8 @@ ${edgeCase.comment}
370
364
  mockInstructionContext
371
365
  );
372
366
 
373
- // Check all BD CLI commands contain the extracted ID
374
- const expectedCommands = [
375
- 'bd list --parent design-epic-789 --status open',
376
- "bd create 'Task description' --parent design-epic-789",
377
- 'bd show design-epic-789',
378
- ];
379
-
380
- for (const command of expectedCommands) {
381
- expect(
382
- result.instructions,
383
- `Should contain command: ${command}`
384
- ).toContain(command);
385
- }
386
-
387
367
  // Should mention the specific task ID in context
388
- expect(result.instructions).toContain(
389
- 'All work items should be created as children of design-epic-789'
390
- );
368
+ expect(result.instructions).toContain('design-epic-789');
391
369
  expect(result.instructions).toContain('subtasks of `design-epic-789`');
392
370
  });
393
371
 
@@ -408,7 +386,7 @@ ${edgeCase.comment}
408
386
 
409
387
  // Should provide specific immediate action
410
388
  expect(result.instructions).toContain(
411
- 'Run `bd list --parent feature-impl-999 --status open` to see ready tasks'
389
+ '--parent feature-impl-999 --status open'
412
390
  );
413
391
  });
414
392
 
@@ -0,0 +1,512 @@
1
+ /**
2
+ * Comprehensive Behavioral Tests for BeadsPlugin
3
+ *
4
+ * Tests validate:
5
+ * - Actual beads task creation and management
6
+ * - User experience preservation (same inputs → same outputs)
7
+ * - Plan file enhancement with task IDs
8
+ * - Error handling and graceful degradation
9
+ * - Integration between plugin hooks and beads backend
10
+ */
11
+
12
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
13
+ import { mkdir, writeFile, readFile, rm } from 'node:fs/promises';
14
+ import { existsSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ import { tmpdir } from 'node:os';
17
+ import { execSync } from 'node:child_process';
18
+ import type { PluginHookContext } from '../../src/plugin-system/plugin-interfaces.js';
19
+
20
+ // Mock child_process to intercept beads commands
21
+ vi.mock('node:child_process', () => ({
22
+ execSync: vi.fn(),
23
+ }));
24
+
25
+ import { BeadsPlugin } from '../../src/plugin-system/beads-plugin.js';
26
+
27
+ describe('BeadsPlugin - Comprehensive Behavioral Tests', () => {
28
+ let testProjectPath: string;
29
+ let testPlanFilePath: string;
30
+
31
+ const createPlanFileContent = () => `# Development Plan
32
+
33
+ ## Goal
34
+ Build a comprehensive feature for task management with beads integration
35
+
36
+ ## Explore
37
+ <!-- beads-phase-id: TBD -->
38
+ Research existing implementation
39
+
40
+ ## Plan
41
+ <!-- beads-phase-id: TBD -->
42
+ Design the solution
43
+
44
+ ## Code
45
+ <!-- beads-phase-id: TBD -->
46
+ Implement the feature
47
+
48
+ ## Test
49
+ <!-- beads-phase-id: TBD -->
50
+ Test all functionality`;
51
+
52
+ const createMockContext = (overrides: Record<string, unknown> = {}) =>
53
+ ({
54
+ conversationId: 'test-conversation-123',
55
+ planFilePath: testPlanFilePath,
56
+ currentPhase: 'explore',
57
+ workflow: 'epcc',
58
+ projectPath: testProjectPath,
59
+ gitBranch: 'feature/test-branch',
60
+ stateMachine: {
61
+ name: 'epcc',
62
+ description: 'Explore Plan Code Commit workflow',
63
+ initial_state: 'explore',
64
+ states: {
65
+ explore: {
66
+ description: 'Exploration phase',
67
+ default_instructions: 'Explore the codebase',
68
+ transitions: [],
69
+ },
70
+ plan: {
71
+ description: 'Planning phase',
72
+ default_instructions: 'Plan the feature',
73
+ transitions: [],
74
+ },
75
+ code: {
76
+ description: 'Coding phase',
77
+ default_instructions: 'Code the feature',
78
+ transitions: [],
79
+ },
80
+ test: {
81
+ description: 'Testing phase',
82
+ default_instructions: 'Test the feature',
83
+ transitions: [],
84
+ },
85
+ },
86
+ },
87
+ ...overrides,
88
+ }) as unknown as PluginHookContext;
89
+
90
+ beforeEach(async () => {
91
+ testProjectPath = join(tmpdir(), `beads-plugin-test-${Date.now()}`);
92
+ testPlanFilePath = join(testProjectPath, '.vibe', 'plan.md');
93
+
94
+ await mkdir(join(testProjectPath, '.vibe'), { recursive: true });
95
+ await writeFile(testPlanFilePath, createPlanFileContent());
96
+
97
+ vi.stubEnv('TASK_BACKEND', 'beads');
98
+ vi.clearAllMocks();
99
+ });
100
+
101
+ afterEach(async () => {
102
+ if (existsSync(testProjectPath)) {
103
+ await rm(testProjectPath, { recursive: true, force: true });
104
+ }
105
+ vi.unstubAllEnvs();
106
+ vi.clearAllMocks();
107
+ });
108
+
109
+ // ============================================================================
110
+ // Test Suite A: Plugin Interface and Metadata
111
+ // ============================================================================
112
+
113
+ describe('Test Suite A: Plugin Interface and Metadata', () => {
114
+ it('A1: should implement complete IPlugin interface', () => {
115
+ const plugin = new BeadsPlugin({ projectPath: testProjectPath });
116
+
117
+ expect(typeof plugin.getName).toBe('function');
118
+ expect(typeof plugin.getSequence).toBe('function');
119
+ expect(typeof plugin.isEnabled).toBe('function');
120
+ expect(typeof plugin.getHooks).toBe('function');
121
+
122
+ expect(typeof plugin.getName()).toBe('string');
123
+ expect(typeof plugin.getSequence()).toBe('number');
124
+ expect(typeof plugin.isEnabled()).toBe('boolean');
125
+ expect(typeof plugin.getHooks()).toBe('object');
126
+ });
127
+
128
+ it('A2: should provide all required hooks', () => {
129
+ const plugin = new BeadsPlugin({ projectPath: testProjectPath });
130
+ const hooks = plugin.getHooks();
131
+
132
+ expect(hooks.afterStartDevelopment).toBeDefined();
133
+ expect(hooks.beforePhaseTransition).toBeDefined();
134
+ expect(hooks.afterPlanFileCreated).toBeDefined();
135
+
136
+ expect(typeof hooks.afterStartDevelopment).toBe('function');
137
+ expect(typeof hooks.beforePhaseTransition).toBe('function');
138
+ expect(typeof hooks.afterPlanFileCreated).toBe('function');
139
+ });
140
+
141
+ it('A3: should have correct plugin metadata', () => {
142
+ const plugin = new BeadsPlugin({ projectPath: testProjectPath });
143
+
144
+ expect(plugin.getName()).toBe('BeadsPlugin');
145
+ expect(plugin.getSequence()).toBe(100);
146
+ });
147
+
148
+ it('A4: should be enabled when TASK_BACKEND is beads', () => {
149
+ vi.stubEnv('TASK_BACKEND', 'beads');
150
+ const plugin = new BeadsPlugin({ projectPath: testProjectPath });
151
+ expect(plugin.isEnabled()).toBe(true);
152
+ });
153
+
154
+ it('A5: should not be enabled when TASK_BACKEND is not beads', () => {
155
+ vi.stubEnv('TASK_BACKEND', 'none');
156
+ const plugin = new BeadsPlugin({ projectPath: testProjectPath });
157
+ expect(plugin.isEnabled()).toBe(false);
158
+ });
159
+
160
+ it('A6: should not crash when plugin not enabled', () => {
161
+ vi.stubEnv('TASK_BACKEND', 'none');
162
+ const plugin = new BeadsPlugin({ projectPath: testProjectPath });
163
+ const isEnabled = plugin.isEnabled();
164
+ expect(isEnabled).toBe(false);
165
+ });
166
+ });
167
+
168
+ // ============================================================================
169
+ // Test Suite B: Hook Basic Functionality
170
+ // ============================================================================
171
+
172
+ describe('Test Suite B: Hook Basic Functionality', () => {
173
+ it('B1: should handle afterPlanFileCreated without modifications', async () => {
174
+ const plugin = new BeadsPlugin({ projectPath: testProjectPath });
175
+ const context = createMockContext();
176
+ const planContent = 'test plan content';
177
+
178
+ const hooks = plugin.getHooks();
179
+ const result = await hooks.afterPlanFileCreated?.(
180
+ context,
181
+ testPlanFilePath,
182
+ planContent
183
+ );
184
+
185
+ expect(result).toBe(planContent);
186
+ });
187
+ });
188
+
189
+ // ============================================================================
190
+ // Test Suite C: Plan File Enhancement
191
+ // ============================================================================
192
+
193
+ describe('Test Suite C: Plan File Enhancement', () => {
194
+ it('C1: should gracefully handle missing plan file', async () => {
195
+ // Remove the plan file to simulate read error
196
+ await rm(testPlanFilePath);
197
+
198
+ const plugin = new BeadsPlugin({ projectPath: testProjectPath });
199
+ const context = createMockContext();
200
+ const args = { workflow: 'epcc', commit_behaviour: 'end' as const };
201
+
202
+ // Setup mocks for execSync
203
+ vi.mocked(execSync).mockImplementation((command: string) => {
204
+ if (command === 'bd list --limit 1') {
205
+ return 'No issues found\n';
206
+ }
207
+ throw new Error(`Unexpected command: ${command}`);
208
+ });
209
+
210
+ const hooks = plugin.getHooks();
211
+
212
+ // Plugin handles missing plan file gracefully in goal extraction
213
+ // It continues without a goal description
214
+ const promise = hooks.afterStartDevelopment?.(context, args, {
215
+ conversationId: context.conversationId,
216
+ planFilePath: context.planFilePath,
217
+ phase: context.currentPhase,
218
+ workflow: args.workflow,
219
+ });
220
+
221
+ // Goal extraction error should not crash the system
222
+ // Result depends on whether execSync supports the command
223
+ if (promise) {
224
+ await expect(promise).resolves.not.toThrow('Goal extraction');
225
+ }
226
+ });
227
+
228
+ it('C2: should update plan file with beads task IDs when successful', async () => {
229
+ const plugin = new BeadsPlugin({ projectPath: testProjectPath });
230
+ const context = createMockContext();
231
+ const args = { workflow: 'epcc', commit_behaviour: 'end' as const };
232
+
233
+ // Mock execSync to simulate beads commands
234
+ let callCount = 0;
235
+ vi.mocked(execSync).mockImplementation((command: string) => {
236
+ callCount++;
237
+
238
+ if (command === 'bd list --limit 1') {
239
+ return 'No issues found\n';
240
+ }
241
+
242
+ // Return different task IDs for each phase task creation
243
+ if (command.includes('bd create')) {
244
+ if (callCount === 2) return '✓ Created issue: epic-1\n'; // main epic
245
+ if (callCount === 3) return '✓ Created issue: epic-1.1\n'; // explore
246
+ if (callCount === 4) return '✓ Created issue: epic-1.2\n'; // plan
247
+ if (callCount === 5) return '✓ Created issue: epic-1.3\n'; // code
248
+ if (callCount === 6) return '✓ Created issue: epic-1.4\n'; // test
249
+ if (callCount === 7) return '✓ Dependency created\n'; // dependency
250
+ if (callCount === 8) return '✓ Dependency created\n';
251
+ if (callCount === 9) return '✓ Dependency created\n';
252
+ }
253
+
254
+ throw new Error(`Unexpected command: ${command}`);
255
+ });
256
+
257
+ const hooks = plugin.getHooks();
258
+ await hooks.afterStartDevelopment?.(context, args, {
259
+ conversationId: context.conversationId,
260
+ planFilePath: context.planFilePath,
261
+ phase: context.currentPhase,
262
+ workflow: args.workflow,
263
+ });
264
+
265
+ // Verify plan file was updated
266
+ const updatedContent = await readFile(testPlanFilePath, 'utf-8');
267
+
268
+ // Should have replaced all TBD placeholders
269
+ expect(updatedContent).not.toMatch(/<!-- beads-phase-id: TBD -->/);
270
+
271
+ // Should have actual task IDs
272
+ expect(updatedContent).toContain('beads-phase-id: epic-1');
273
+ });
274
+ });
275
+
276
+ // ============================================================================
277
+ // Test Suite D: User Experience Preservation
278
+ // ============================================================================
279
+
280
+ describe('Test Suite D: User Experience Preservation', () => {
281
+ it('D1: should handle beads backend unavailability gracefully', async () => {
282
+ const plugin = new BeadsPlugin({ projectPath: testProjectPath });
283
+ const context = createMockContext();
284
+
285
+ // Mock the backend client to return unavailable
286
+ vi.mocked(execSync).mockImplementation((command: string) => {
287
+ if (command.includes('--version')) {
288
+ throw new Error('beads CLI not found');
289
+ }
290
+ throw new Error(`Unexpected command: ${command}`);
291
+ });
292
+
293
+ const hooks = plugin.getHooks();
294
+
295
+ // Should not throw when backend unavailable
296
+ await expect(
297
+ hooks.beforePhaseTransition?.(context, 'explore', 'plan')
298
+ ).resolves.not.toThrow();
299
+ });
300
+
301
+ it('D2: should allow phase transitions without beads tasks present', async () => {
302
+ const plugin = new BeadsPlugin({ projectPath: testProjectPath });
303
+ const context = createMockContext();
304
+
305
+ vi.mocked(execSync).mockImplementation((command: string) => {
306
+ if (command === 'bd --version') {
307
+ return 'beads v1.0.0\n';
308
+ }
309
+ // Simulate no beads state found
310
+ throw new Error('No beads state');
311
+ });
312
+
313
+ const hooks = plugin.getHooks();
314
+
315
+ // Should not throw even if beads state not found
316
+ await expect(
317
+ hooks.beforePhaseTransition?.(context, 'explore', 'plan')
318
+ ).resolves.not.toThrow();
319
+ });
320
+
321
+ it('D3: should preserve identical interface with and without beads', () => {
322
+ vi.stubEnv('TASK_BACKEND', 'beads');
323
+ const pluginWithBeads = new BeadsPlugin({ projectPath: testProjectPath });
324
+
325
+ vi.stubEnv('TASK_BACKEND', 'none');
326
+ const pluginWithoutBeads = new BeadsPlugin({
327
+ projectPath: testProjectPath,
328
+ });
329
+
330
+ // Both should have same interface
331
+ expect(pluginWithBeads.getName()).toBe(pluginWithoutBeads.getName());
332
+ expect(pluginWithBeads.getSequence()).toBe(
333
+ pluginWithoutBeads.getSequence()
334
+ );
335
+
336
+ // Hooks should exist for both
337
+ const beadsHooks = pluginWithBeads.getHooks();
338
+ const nonBeadsHooks = pluginWithoutBeads.getHooks();
339
+
340
+ expect(Object.keys(beadsHooks)).toEqual(Object.keys(nonBeadsHooks));
341
+ });
342
+
343
+ it('D4: should provide meaningful error messages', async () => {
344
+ const plugin = new BeadsPlugin({ projectPath: testProjectPath });
345
+ const context = createMockContext();
346
+
347
+ vi.mocked(execSync).mockImplementation((_command: string) => {
348
+ throw new Error('beads CLI not found or not in PATH');
349
+ });
350
+
351
+ const hooks = plugin.getHooks();
352
+
353
+ try {
354
+ await hooks.afterStartDevelopment?.(
355
+ context,
356
+ {
357
+ workflow: 'epcc',
358
+ commit_behaviour: 'end' as const,
359
+ } as unknown,
360
+ {
361
+ conversationId: context.conversationId,
362
+ planFilePath: context.planFilePath,
363
+ phase: context.currentPhase,
364
+ workflow: 'epcc',
365
+ }
366
+ );
367
+ } catch (error) {
368
+ const message = error instanceof Error ? error.message : String(error);
369
+ // Error should be clear and actionable
370
+ expect(message).toContain('BeadsPlugin');
371
+ }
372
+ });
373
+ });
374
+
375
+ // ============================================================================
376
+ // Test Suite E: Goal Extraction
377
+ // ============================================================================
378
+
379
+ describe('Test Suite E: Goal Extraction', () => {
380
+ it('E1: should handle missing goal section gracefully', async () => {
381
+ // Create plan without goal section
382
+ const planWithoutGoal = `# Development Plan
383
+
384
+ ## Explore
385
+ <!-- beads-phase-id: TBD -->`;
386
+
387
+ await writeFile(testPlanFilePath, planWithoutGoal);
388
+
389
+ const plugin = new BeadsPlugin({ projectPath: testProjectPath });
390
+ const context = createMockContext();
391
+ const args = { workflow: 'epcc', commit_behaviour: 'end' as const };
392
+
393
+ let _epicCreateCmd = '';
394
+ vi.mocked(execSync).mockImplementation((command: string) => {
395
+ if (command === 'bd list --limit 1') {
396
+ return 'No issues found\n';
397
+ }
398
+ if (command.includes('bd create') && callCount === 1) {
399
+ _epicCreateCmd = command;
400
+ }
401
+ if (command.includes('bd create')) {
402
+ return '✓ Created issue: epic-1\n';
403
+ }
404
+ if (command.includes('bd') && command.includes('--parent')) {
405
+ return '✓ Created issue: epic-1.1\n';
406
+ }
407
+ throw new Error(`Unexpected command: ${command}`);
408
+ });
409
+
410
+ let callCount = 0;
411
+
412
+ const hooks = plugin.getHooks();
413
+ await hooks.afterStartDevelopment?.(context, args, {
414
+ conversationId: context.conversationId,
415
+ planFilePath: context.planFilePath,
416
+ phase: context.currentPhase,
417
+ workflow: args.workflow,
418
+ });
419
+
420
+ // Should have called create without goal description being undefined
421
+ // The goal extraction should fail gracefully
422
+ expect(vi.mocked(execSync)).toHaveBeenCalled();
423
+ });
424
+
425
+ it('E2: should reject placeholder goals', async () => {
426
+ const planWithPlaceholder = `# Development Plan
427
+
428
+ ## Goal
429
+ *Define what you're building...*
430
+
431
+ ## Explore
432
+ <!-- beads-phase-id: TBD -->`;
433
+
434
+ await writeFile(testPlanFilePath, planWithPlaceholder);
435
+
436
+ const plugin = new BeadsPlugin({ projectPath: testProjectPath });
437
+ const context = createMockContext();
438
+ const args = { workflow: 'epcc', commit_behaviour: 'end' as const };
439
+
440
+ vi.mocked(execSync).mockImplementation((command: string) => {
441
+ if (command === 'bd list --limit 1') {
442
+ return 'No issues found\n';
443
+ }
444
+ if (command.includes('bd create')) {
445
+ return '✓ Created issue: epic-1\n';
446
+ }
447
+ if (command.includes('bd') && command.includes('--parent')) {
448
+ return '✓ Created issue: epic-1.1\n';
449
+ }
450
+ throw new Error(`Unexpected command: ${command}`);
451
+ });
452
+
453
+ const hooks = plugin.getHooks();
454
+ await hooks.afterStartDevelopment?.(context, args, {
455
+ conversationId: context.conversationId,
456
+ planFilePath: context.planFilePath,
457
+ phase: context.currentPhase,
458
+ workflow: args.workflow,
459
+ });
460
+
461
+ // Should complete without throwing
462
+ expect(vi.mocked(execSync)).toHaveBeenCalled();
463
+ });
464
+ });
465
+
466
+ // ============================================================================
467
+ // Test Suite F: Error Recovery
468
+ // ============================================================================
469
+
470
+ describe('Test Suite F: Error Recovery', () => {
471
+ it('F2: should handle plan file write errors gracefully', async () => {
472
+ const plugin = new BeadsPlugin({ projectPath: testProjectPath });
473
+ const context = createMockContext();
474
+ const args = { workflow: 'epcc', commit_behaviour: 'end' as const };
475
+
476
+ // Remove write permissions on plan file by replacing with directory
477
+ await rm(testPlanFilePath);
478
+ await mkdir(testPlanFilePath);
479
+
480
+ vi.mocked(execSync).mockImplementation((command: string) => {
481
+ if (command === 'bd list --limit 1') {
482
+ return 'No issues found\n';
483
+ }
484
+ if (command.includes('bd create')) {
485
+ return '✓ Created issue: epic-1\n';
486
+ }
487
+ if (command.includes('bd')) {
488
+ return '✓ Created issue: epic-1.1\n';
489
+ }
490
+ throw new Error(`Unexpected command: ${command}`);
491
+ });
492
+
493
+ const hooks = plugin.getHooks();
494
+
495
+ try {
496
+ await hooks.afterStartDevelopment?.(context, args, {
497
+ conversationId: context.conversationId,
498
+ planFilePath: testPlanFilePath,
499
+ phase: context.currentPhase,
500
+ workflow: args.workflow,
501
+ });
502
+ } catch (error) {
503
+ // Expected to fail when writing plan file
504
+ expect(error instanceof Error).toBe(true);
505
+ return;
506
+ }
507
+
508
+ // If it gets here, the write might have succeeded despite the directory
509
+ // which is fine for this test
510
+ });
511
+ });
512
+ });
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Basic tests for BeadsPlugin implementation
3
+ */
4
+
5
+ import { BeadsPlugin } from '../../src/plugin-system/beads-plugin.js';
6
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
7
+
8
+ describe('BeadsPlugin', () => {
9
+ let plugin: BeadsPlugin;
10
+ const mockProjectPath = '/test/project/path';
11
+
12
+ beforeEach(() => {
13
+ // Mock environment variable
14
+ vi.stubEnv('TASK_BACKEND', 'beads');
15
+ plugin = new BeadsPlugin({ projectPath: mockProjectPath });
16
+ });
17
+
18
+ describe('Basic Interface Implementation', () => {
19
+ it('should return correct name', () => {
20
+ expect(plugin.getName()).toBe('BeadsPlugin');
21
+ });
22
+
23
+ it('should return correct sequence', () => {
24
+ expect(plugin.getSequence()).toBe(100);
25
+ });
26
+
27
+ it('should be enabled when TASK_BACKEND is beads', () => {
28
+ expect(plugin.isEnabled()).toBe(true);
29
+ });
30
+
31
+ it('should not be enabled when TASK_BACKEND is not beads', () => {
32
+ vi.stubEnv('TASK_BACKEND', 'none');
33
+ const testPlugin = new BeadsPlugin({ projectPath: mockProjectPath });
34
+ expect(testPlugin.isEnabled()).toBe(false);
35
+ });
36
+
37
+ it('should provide required hooks', () => {
38
+ const hooks = plugin.getHooks();
39
+ expect(hooks.afterStartDevelopment).toBeDefined();
40
+ expect(hooks.beforePhaseTransition).toBeDefined();
41
+ expect(hooks.afterPlanFileCreated).toBeDefined();
42
+ });
43
+ });
44
+
45
+ describe('Hook Implementation', () => {
46
+ const mockContext = {
47
+ conversationId: 'test-conversation',
48
+ planFilePath: '/test/plan.md',
49
+ currentPhase: 'test-phase',
50
+ workflow: 'test-workflow',
51
+ projectPath: mockProjectPath,
52
+ gitBranch: 'test-branch',
53
+ };
54
+
55
+ it('should handle afterStartDevelopment hook without errors', async () => {
56
+ const hooks = plugin.getHooks();
57
+ const result = hooks.afterStartDevelopment;
58
+ expect(result).toBeDefined();
59
+
60
+ // This should not throw because it's just logging a warning
61
+ // about architectural limitations
62
+ if (result) {
63
+ await expect(
64
+ result(
65
+ mockContext,
66
+ { workflow: 'test-workflow', commit_behaviour: 'end' },
67
+ {
68
+ conversationId: 'test',
69
+ planFilePath: '/test/plan.md',
70
+ phase: 'test-phase',
71
+ workflow: 'test-workflow',
72
+ }
73
+ )
74
+ ).resolves.not.toThrow();
75
+ }
76
+ });
77
+
78
+ it('should handle afterPlanFileCreated hook', async () => {
79
+ const hooks = plugin.getHooks();
80
+ const result = hooks.afterPlanFileCreated;
81
+ expect(result).toBeDefined();
82
+
83
+ if (result) {
84
+ const content = 'test plan content';
85
+ const processedContent = await result(
86
+ mockContext,
87
+ '/test/plan.md',
88
+ content
89
+ );
90
+ expect(processedContent).toBe(content); // Should return unchanged
91
+ }
92
+ });
93
+ });
94
+ });