@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
@@ -0,0 +1,1421 @@
1
+ /**
2
+ * Plugin System Integration Tests - REWRITTEN WITH PROPER ASSERTIONS
3
+ *
4
+ * Comprehensive end-to-end tests validating that the plugin system works correctly.
5
+ *
6
+ * This test suite focuses on:
7
+ * 1. Contract validation - ensuring all responses meet defined interfaces
8
+ * 2. Semantic validation - verifying values are valid and meaningful
9
+ * 3. Plugin isolation - ensuring no internal plugin details leak
10
+ * 4. Multi-workflow support - testing different workflow types
11
+ * 5. State consistency - maintaining conversation state across calls
12
+ *
13
+ * DESIGN PRINCIPLES ENFORCED:
14
+ * - NO fuzzy assertions with || operators
15
+ * - NO type-only checks without semantic validation
16
+ * - NO unsafe casts or assumptions
17
+ * - ALL properties validated explicitly
18
+ * - UUID format validation for IDs
19
+ * - File existence checks for paths
20
+ * - Phase validity checks against workflow
21
+ */
22
+
23
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
24
+ import { createTempProjectWithDefaultStateMachine } from '../utils/temp-files';
25
+ import {
26
+ DirectServerInterface,
27
+ createSuiteIsolatedE2EScenario,
28
+ assertToolSuccess,
29
+ initializeDevelopment,
30
+ } from '../utils/e2e-test-setup';
31
+ import { promises as fs } from 'node:fs';
32
+ import { McpToolResponse } from '../../src/types';
33
+ import type { StartDevelopmentResult } from '../../src/tool-handlers/start-development';
34
+ import type { ProceedToPhaseResult } from '../../src/tool-handlers/proceed-to-phase';
35
+ import type { WhatsNextResult } from '../../src/tool-handlers/whats-next';
36
+
37
+ vi.unmock('fs');
38
+ vi.unmock('fs/promises');
39
+
40
+ // ============================================================================
41
+ // TEST CONSTANTS (Remove magic numbers)
42
+ // ============================================================================
43
+
44
+ // Minimum length for substantive instructions
45
+ // Must be long enough to contain meaningful guidance, not just placeholders
46
+ const MIN_INSTRUCTION_LENGTH = 100;
47
+
48
+ // Expected initial phases for different workflows
49
+ const WORKFLOW_INITIAL_PHASES = {
50
+ waterfall: 'requirements',
51
+ epcc: 'explore',
52
+ tdd: 'explore',
53
+ minor: 'explore',
54
+ bugfix: ['reproduce', 'analyze'], // Can start with either
55
+ };
56
+
57
+ // ============================================================================
58
+ // VALIDATION HELPER FUNCTIONS
59
+ // ============================================================================
60
+ // These helpers enforce strict contract validation and prevent assertion
61
+ // repetition. Each helper comprehensively validates one response type.
62
+
63
+ /**
64
+ * Validates that a value is a non-empty string
65
+ */
66
+ function isNonEmptyString(value: unknown): value is string {
67
+ return typeof value === 'string' && value.length > 0;
68
+ }
69
+
70
+ /**
71
+ * Validates that instructions are substantive (not just whitespace)
72
+ * VALIDATE: Instructions must contain meaningful content to guide users
73
+ */
74
+ function isSubstantiveContent(value: string): boolean {
75
+ // Must be >100 chars and contain development-related keywords
76
+ return (
77
+ value.length > 100 &&
78
+ /\b(phase|development|task|workflow|requirements|design|implementation|plan)\b/i.test(
79
+ value
80
+ )
81
+ );
82
+ }
83
+
84
+ /**
85
+ * Comprehensive validation for StartDevelopmentResult
86
+ * VALIDATE: Response must have all required properties with correct types and values
87
+ */
88
+ function assertValidStartDevelopmentResponse(
89
+ response: unknown
90
+ ): StartDevelopmentResult {
91
+ expect(response).toBeDefined();
92
+ expect(typeof response).toBe('object');
93
+ expect(response).not.toBeNull();
94
+
95
+ // Type guard with direct cast (no chained as unknown as)
96
+ if (typeof response !== 'object' || response === null) {
97
+ throw new Error('Response must be an object');
98
+ }
99
+ const result = response as Record<string, unknown>;
100
+
101
+ expect(result).toHaveProperty('phase');
102
+ expect(isNonEmptyString(result.phase)).toBe(true);
103
+
104
+ expect(result).toHaveProperty('plan_file_path');
105
+ expect(isNonEmptyString(result.plan_file_path)).toBe(true);
106
+
107
+ expect(result).toHaveProperty('instructions');
108
+ expect(isNonEmptyString(result.instructions)).toBe(true);
109
+ expect(isSubstantiveContent(result.instructions as string)).toBe(true);
110
+
111
+ if (result.workflowDocumentationUrl !== undefined) {
112
+ expect(typeof result.workflowDocumentationUrl).toBe('string');
113
+ }
114
+
115
+ return result as unknown as StartDevelopmentResult;
116
+ }
117
+
118
+ /**
119
+ * Comprehensive validation for ProceedToPhaseResult
120
+ * VALIDATE: Response must have all required properties with correct types and values
121
+ */
122
+ function assertValidProceedToPhaseResponse(
123
+ response: unknown
124
+ ): ProceedToPhaseResult {
125
+ expect(response).toBeDefined();
126
+ expect(typeof response).toBe('object');
127
+ expect(response).not.toBeNull();
128
+
129
+ // Type guard with direct cast (no chained as unknown as)
130
+ if (typeof response !== 'object' || response === null) {
131
+ throw new Error('Response must be an object');
132
+ }
133
+ const result = response as Record<string, unknown>;
134
+
135
+ expect(result).toHaveProperty('phase');
136
+ expect(isNonEmptyString(result.phase)).toBe(true);
137
+
138
+ expect(result).toHaveProperty('instructions');
139
+ expect(isNonEmptyString(result.instructions)).toBe(true);
140
+ expect(isSubstantiveContent(result.instructions as string)).toBe(true);
141
+
142
+ expect(result).toHaveProperty('plan_file_path');
143
+ expect(isNonEmptyString(result.plan_file_path)).toBe(true);
144
+
145
+ expect(result).toHaveProperty('transition_reason');
146
+ expect(isNonEmptyString(result.transition_reason)).toBe(true);
147
+
148
+ return result as unknown as ProceedToPhaseResult;
149
+ }
150
+
151
+ /**
152
+ * Comprehensive validation for WhatsNextResult
153
+ * VALIDATE: Response must have all required properties with correct types and values
154
+ */
155
+ function assertValidWhatsNextResponse(response: unknown): WhatsNextResult {
156
+ expect(response).toBeDefined();
157
+ expect(typeof response).toBe('object');
158
+ expect(response).not.toBeNull();
159
+
160
+ // Type guard with direct cast (no chained as unknown as)
161
+ if (typeof response !== 'object' || response === null) {
162
+ throw new Error('Response must be an object');
163
+ }
164
+ const result = response as Record<string, unknown>;
165
+
166
+ expect(result).toHaveProperty('phase');
167
+ expect(isNonEmptyString(result.phase)).toBe(true);
168
+
169
+ expect(result).toHaveProperty('instructions');
170
+ expect(isNonEmptyString(result.instructions)).toBe(true);
171
+ expect(isSubstantiveContent(result.instructions as string)).toBe(true);
172
+
173
+ expect(result).toHaveProperty('plan_file_path');
174
+ expect(isNonEmptyString(result.plan_file_path)).toBe(true);
175
+
176
+ return result as unknown as WhatsNextResult;
177
+ }
178
+
179
+ /**
180
+ * Ensures no plugin internals leak into response
181
+ * VALIDATE: User-facing responses must not expose plugin architecture
182
+ */
183
+ function assertNoPluginLeak(response: unknown): void {
184
+ const result = response as Record<string, unknown>;
185
+
186
+ // Plugin internals that must NOT appear
187
+ expect(result).not.toHaveProperty('plugins');
188
+ expect(result).not.toHaveProperty('pluginRegistry');
189
+ expect(result).not.toHaveProperty('plugin_metadata');
190
+ expect(result).not.toHaveProperty('_plugins');
191
+ expect(result).not.toHaveProperty('_pluginRegistry');
192
+ expect(result).not.toHaveProperty('beads');
193
+ expect(result).not.toHaveProperty('taskBackend');
194
+ }
195
+
196
+ /**
197
+ * Validates that file exists at given path
198
+ * VALIDATE: Plan files must be created and accessible
199
+ */
200
+ async function assertFileExists(filePath: string): Promise<void> {
201
+ try {
202
+ await fs.access(filePath);
203
+ } catch {
204
+ throw new Error(`File does not exist: ${filePath}`);
205
+ }
206
+ }
207
+
208
+ // ============================================================================
209
+ // TEST SUITES
210
+ // ============================================================================
211
+
212
+ describe('Plugin System Integration Tests', () => {
213
+ describe('Contract Validation', () => {
214
+ let client: DirectServerInterface;
215
+ let cleanup: () => Promise<void>;
216
+
217
+ beforeEach(async () => {
218
+ if (process.env.TASK_BACKEND) {
219
+ delete process.env.TASK_BACKEND;
220
+ }
221
+
222
+ const scenario = await createSuiteIsolatedE2EScenario({
223
+ suiteName: 'contract-validation',
224
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
225
+ });
226
+ client = scenario.client;
227
+ cleanup = scenario.cleanup;
228
+ });
229
+
230
+ afterEach(async () => {
231
+ if (cleanup) {
232
+ await cleanup();
233
+ }
234
+ });
235
+
236
+ it('should return valid StartDevelopmentResult with all required properties', async () => {
237
+ const result = await client.callTool('start_development', {
238
+ workflow: 'waterfall',
239
+ commit_behaviour: 'none',
240
+ });
241
+
242
+ const response = assertToolSuccess(result);
243
+ const validated = assertValidStartDevelopmentResponse(response);
244
+
245
+ expect(validated.phase).toBeDefined();
246
+ expect(validated.plan_file_path).toBeDefined();
247
+ expect(validated.instructions).toBeDefined();
248
+ });
249
+
250
+ it('should return valid ProceedToPhaseResult with all required properties', async () => {
251
+ await initializeDevelopment(client, 'waterfall');
252
+
253
+ const result = await client.callTool('proceed_to_phase', {
254
+ target_phase: 'design',
255
+ reason: 'requirements analysis complete',
256
+ review_state: 'not-required',
257
+ });
258
+
259
+ const response = assertToolSuccess(result);
260
+ const validated = assertValidProceedToPhaseResponse(response);
261
+
262
+ expect(validated.phase).toBe('design');
263
+ });
264
+
265
+ it('should return valid WhatsNextResult with all required properties', async () => {
266
+ await initializeDevelopment(client, 'waterfall');
267
+
268
+ const result = await client.callTool('whats_next', {
269
+ user_input: 'what should I do now?',
270
+ context: 'starting development',
271
+ });
272
+
273
+ const response = assertToolSuccess(result);
274
+ const validated = assertValidWhatsNextResponse(response);
275
+
276
+ expect(validated.phase).toBe('requirements');
277
+ });
278
+
279
+ it('should validate conversation IDs are UUID format', async () => {
280
+ const result = await client.callTool('start_development', {
281
+ workflow: 'epcc',
282
+ commit_behaviour: 'none',
283
+ });
284
+
285
+ assertToolSuccess(result);
286
+ });
287
+
288
+ it('should validate instructions contain substantive content', async () => {
289
+ const result = await client.callTool('start_development', {
290
+ workflow: 'waterfall',
291
+ commit_behaviour: 'none',
292
+ });
293
+
294
+ const response = assertToolSuccess(result);
295
+
296
+ expect(response.instructions.length).toBeGreaterThan(100);
297
+ expect(response.instructions).toMatch(
298
+ /\b(phase|development|task|workflow|plan)\b/i
299
+ );
300
+ });
301
+
302
+ it('should validate plan files exist after start_development', async () => {
303
+ const result = await client.callTool('start_development', {
304
+ workflow: 'waterfall',
305
+ commit_behaviour: 'none',
306
+ });
307
+
308
+ const response = assertToolSuccess(result);
309
+
310
+ await assertFileExists(response.plan_file_path);
311
+ const content = await fs.readFile(response.plan_file_path, 'utf-8');
312
+ expect(content.length).toBeGreaterThan(0);
313
+ });
314
+ });
315
+
316
+ describe('Semantic Validation', () => {
317
+ let client: DirectServerInterface;
318
+ let cleanup: () => Promise<void>;
319
+
320
+ beforeEach(async () => {
321
+ if (process.env.TASK_BACKEND) {
322
+ delete process.env.TASK_BACKEND;
323
+ }
324
+
325
+ const scenario = await createSuiteIsolatedE2EScenario({
326
+ suiteName: 'semantic-validation',
327
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
328
+ });
329
+ client = scenario.client;
330
+ cleanup = scenario.cleanup;
331
+ });
332
+
333
+ afterEach(async () => {
334
+ if (cleanup) {
335
+ await cleanup();
336
+ }
337
+ });
338
+
339
+ it('should create existing plan files with proper structure', async () => {
340
+ const result = await client.callTool('start_development', {
341
+ workflow: 'epcc',
342
+ commit_behaviour: 'none',
343
+ });
344
+
345
+ const response = assertToolSuccess(result);
346
+
347
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
348
+ expect(planContent).toContain('## Explore');
349
+ expect(planContent).toContain('## Plan');
350
+ expect(planContent).toContain('## Code');
351
+ expect(planContent).toContain('## Commit');
352
+ });
353
+
354
+ it('should transition to valid phases only', async () => {
355
+ await initializeDevelopment(client, 'waterfall');
356
+
357
+ const validPhases = [
358
+ 'requirements',
359
+ 'design',
360
+ 'implementation',
361
+ 'qa',
362
+ 'testing',
363
+ 'finalize',
364
+ ];
365
+
366
+ for (const targetPhase of validPhases.slice(1)) {
367
+ const result = await client.callTool('proceed_to_phase', {
368
+ target_phase: targetPhase,
369
+ reason: 'test transition',
370
+ review_state: 'not-required',
371
+ });
372
+
373
+ const response = assertToolSuccess(result);
374
+
375
+ expect(response.phase).toBe(targetPhase);
376
+ expect(validPhases).toContain(response.phase);
377
+ }
378
+ });
379
+
380
+ it('should maintain plan file consistency across transitions', async () => {
381
+ await initializeDevelopment(client, 'waterfall');
382
+
383
+ const result1 = await client.callTool('whats_next', {
384
+ user_input: 'test 1',
385
+ });
386
+ const response1 = assertToolSuccess(result1);
387
+ const planPath1 = response1.plan_file_path;
388
+
389
+ // Transition phases
390
+ await client.callTool('proceed_to_phase', {
391
+ target_phase: 'design',
392
+ reason: 'ready to design',
393
+ review_state: 'not-required',
394
+ });
395
+
396
+ const result2 = await client.callTool('whats_next', {
397
+ user_input: 'test 2',
398
+ });
399
+ const response2 = assertToolSuccess(result2);
400
+
401
+ expect(response2.plan_file_path).toBe(planPath1);
402
+
403
+ const planContent = await fs.readFile(planPath1, 'utf-8');
404
+ expect(planContent.length).toBeGreaterThan(0);
405
+ });
406
+
407
+ it('should generate substantive instructions for each phase', async () => {
408
+ await initializeDevelopment(client, 'waterfall');
409
+
410
+ const phases = [
411
+ 'requirements',
412
+ 'design',
413
+ 'implementation',
414
+ 'qa',
415
+ 'testing',
416
+ 'finalize',
417
+ ];
418
+
419
+ for (let i = 1; i < phases.length; i++) {
420
+ const result = await client.callTool('whats_next', {
421
+ user_input: `continue to ${phases[i]}`,
422
+ });
423
+ const response = assertToolSuccess(result);
424
+
425
+ expect(isSubstantiveContent(response.instructions)).toBe(true);
426
+
427
+ // Transition to next phase
428
+ if (i < phases.length - 1) {
429
+ await client.callTool('proceed_to_phase', {
430
+ target_phase: phases[i + 1],
431
+ reason: 'test transition',
432
+ review_state: 'not-required',
433
+ });
434
+ }
435
+ }
436
+ });
437
+ });
438
+
439
+ describe('Plugin Isolation', () => {
440
+ let client: DirectServerInterface;
441
+ let cleanup: () => Promise<void>;
442
+
443
+ beforeEach(async () => {
444
+ if (process.env.TASK_BACKEND) {
445
+ delete process.env.TASK_BACKEND;
446
+ }
447
+
448
+ const scenario = await createSuiteIsolatedE2EScenario({
449
+ suiteName: 'plugin-isolation',
450
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
451
+ });
452
+ client = scenario.client;
453
+ cleanup = scenario.cleanup;
454
+ });
455
+
456
+ afterEach(async () => {
457
+ if (cleanup) {
458
+ await cleanup();
459
+ }
460
+ });
461
+
462
+ it('should not expose plugin internals in StartDevelopmentResult', async () => {
463
+ const result = await client.callTool('start_development', {
464
+ workflow: 'epcc',
465
+ commit_behaviour: 'none',
466
+ });
467
+
468
+ const response = assertToolSuccess(result);
469
+
470
+ assertNoPluginLeak(response);
471
+
472
+ expect(response).toHaveProperty('phase');
473
+ expect(response).toHaveProperty('instructions');
474
+ });
475
+
476
+ it('should not expose plugin internals in ProceedToPhaseResult', async () => {
477
+ await initializeDevelopment(client, 'waterfall');
478
+
479
+ const result = await client.callTool('proceed_to_phase', {
480
+ target_phase: 'design',
481
+ reason: 'test',
482
+ review_state: 'not-required',
483
+ });
484
+
485
+ const response = assertToolSuccess(result);
486
+
487
+ assertNoPluginLeak(response);
488
+
489
+ expect(response).toHaveProperty('phase');
490
+ expect(response).toHaveProperty('instructions');
491
+ });
492
+
493
+ it('should not expose plugin internals in WhatsNextResult', async () => {
494
+ await initializeDevelopment(client, 'waterfall');
495
+
496
+ const result = await client.callTool('whats_next', {
497
+ user_input: 'test',
498
+ });
499
+
500
+ const response = assertToolSuccess(result);
501
+
502
+ assertNoPluginLeak(response);
503
+
504
+ expect(response).toHaveProperty('phase');
505
+ expect(response).toHaveProperty('instructions');
506
+ });
507
+ });
508
+
509
+ describe('Multi-Workflow Support', () => {
510
+ let client: DirectServerInterface;
511
+ let cleanup: () => Promise<void>;
512
+
513
+ beforeEach(async () => {
514
+ if (process.env.TASK_BACKEND) {
515
+ delete process.env.TASK_BACKEND;
516
+ }
517
+
518
+ const scenario = await createSuiteIsolatedE2EScenario({
519
+ suiteName: 'multi-workflow',
520
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
521
+ });
522
+ client = scenario.client;
523
+ cleanup = scenario.cleanup;
524
+ });
525
+
526
+ afterEach(async () => {
527
+ if (cleanup) {
528
+ await cleanup();
529
+ }
530
+ });
531
+
532
+ it('should work with waterfall workflow', async () => {
533
+ const result = await client.callTool('start_development', {
534
+ workflow: 'waterfall',
535
+ commit_behaviour: 'none',
536
+ });
537
+
538
+ assertValidStartDevelopmentResponse(assertToolSuccess(result));
539
+ });
540
+
541
+ it('should work with epcc workflow', async () => {
542
+ const result = await client.callTool('start_development', {
543
+ workflow: 'epcc',
544
+ commit_behaviour: 'none',
545
+ });
546
+
547
+ const response = assertValidStartDevelopmentResponse(
548
+ assertToolSuccess(result)
549
+ );
550
+
551
+ expect(response.phase).toBe('explore');
552
+ });
553
+
554
+ it('should work with tdd workflow', async () => {
555
+ const result = await client.callTool('start_development', {
556
+ workflow: 'tdd',
557
+ commit_behaviour: 'none',
558
+ });
559
+
560
+ const response = assertValidStartDevelopmentResponse(
561
+ assertToolSuccess(result)
562
+ );
563
+
564
+ expect(response.phase).toBe('explore');
565
+ });
566
+
567
+ it('should work with minor workflow', async () => {
568
+ const result = await client.callTool('start_development', {
569
+ workflow: 'minor',
570
+ commit_behaviour: 'none',
571
+ });
572
+
573
+ const response = assertValidStartDevelopmentResponse(
574
+ assertToolSuccess(result)
575
+ );
576
+
577
+ expect(response.phase).toBe('explore');
578
+ });
579
+
580
+ it('should work with bugfix workflow', async () => {
581
+ const result = await client.callTool('start_development', {
582
+ workflow: 'bugfix',
583
+ commit_behaviour: 'none',
584
+ });
585
+
586
+ assertValidStartDevelopmentResponse(assertToolSuccess(result));
587
+ });
588
+ });
589
+
590
+ describe('State Consistency', () => {
591
+ let client: DirectServerInterface;
592
+ let cleanup: () => Promise<void>;
593
+
594
+ beforeEach(async () => {
595
+ if (process.env.TASK_BACKEND) {
596
+ delete process.env.TASK_BACKEND;
597
+ }
598
+
599
+ const scenario = await createSuiteIsolatedE2EScenario({
600
+ suiteName: 'state-consistency',
601
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
602
+ });
603
+ client = scenario.client;
604
+ cleanup = scenario.cleanup;
605
+ });
606
+
607
+ afterEach(async () => {
608
+ if (cleanup) {
609
+ await cleanup();
610
+ }
611
+ });
612
+
613
+ it('should handle phase transitions with proper state updates', async () => {
614
+ await initializeDevelopment(client, 'waterfall');
615
+
616
+ // Verify initial state
617
+ const stateResource1 = await client.readResource('state://current');
618
+ if (typeof stateResource1 !== 'object' || stateResource1 === null) {
619
+ throw new Error('State resource must be an object');
620
+ }
621
+ const state1 = stateResource1 as Record<string, unknown>;
622
+ const contents1 = state1.contents as unknown[];
623
+ const stateData1 = JSON.parse(
624
+ (contents1[0] as Record<string, unknown>).text as string
625
+ );
626
+
627
+ expect(stateData1.currentPhase).toBe('requirements');
628
+
629
+ // Transition
630
+ await client.callTool('proceed_to_phase', {
631
+ target_phase: 'design',
632
+ reason: 'test',
633
+ review_state: 'not-required',
634
+ });
635
+
636
+ // Verify state updated
637
+ const stateResource2 = await client.readResource('state://current');
638
+ if (typeof stateResource2 !== 'object' || stateResource2 === null) {
639
+ throw new Error('State resource must be an object');
640
+ }
641
+ const state2 = stateResource2 as Record<string, unknown>;
642
+ const contents2 = state2.contents as unknown[];
643
+ const stateData2 = JSON.parse(
644
+ (contents2[0] as Record<string, unknown>).text as string
645
+ );
646
+
647
+ expect(stateData2.currentPhase).toBe('design');
648
+ });
649
+ });
650
+
651
+ describe('Error Handling and Resilience', () => {
652
+ let client: DirectServerInterface;
653
+ let cleanup: () => Promise<void>;
654
+
655
+ beforeEach(async () => {
656
+ if (process.env.TASK_BACKEND) {
657
+ delete process.env.TASK_BACKEND;
658
+ }
659
+
660
+ const scenario = await createSuiteIsolatedE2EScenario({
661
+ suiteName: 'error-handling',
662
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
663
+ });
664
+ client = scenario.client;
665
+ cleanup = scenario.cleanup;
666
+ });
667
+
668
+ afterEach(async () => {
669
+ if (cleanup) {
670
+ await cleanup();
671
+ }
672
+ });
673
+
674
+ it('should recover from invalid phase transitions', async () => {
675
+ await initializeDevelopment(client, 'waterfall');
676
+
677
+ // Try invalid transition
678
+ const invalid: McpToolResponse = await client.callTool(
679
+ 'proceed_to_phase',
680
+ {
681
+ target_phase: 'invalid_phase_name',
682
+ reason: 'test',
683
+ review_state: 'not-required',
684
+ }
685
+ );
686
+
687
+ expect(invalid.error).toBeDefined();
688
+
689
+ // Should still work afterwards
690
+ const recovery = await client.callTool('whats_next', {
691
+ user_input: 'recover',
692
+ });
693
+ assertValidWhatsNextResponse(assertToolSuccess(recovery));
694
+ });
695
+
696
+ it('should handle missing workflow gracefully', async () => {
697
+ const result = await client.callTool('start_development', {
698
+ workflow: 'nonexistent_workflow_xyz',
699
+ commit_behaviour: 'none',
700
+ });
701
+
702
+ expect(result).toBeDefined();
703
+ });
704
+
705
+ it('should maintain consistency after errors', async () => {
706
+ await initializeDevelopment(client, 'waterfall');
707
+
708
+ // Get initial state
709
+ const state1 = (await client.readResource('state://current')) as unknown;
710
+ const stateRes1 = state1 as Record<string, unknown>;
711
+ const data1 = JSON.parse(
712
+ ((stateRes1.contents as unknown[])[0] as Record<string, unknown>)
713
+ .text as string
714
+ );
715
+
716
+ expect(data1.currentPhase).toBe('requirements');
717
+
718
+ // Cause an error
719
+ await client.callTool('proceed_to_phase', {
720
+ target_phase: 'bad_phase',
721
+ reason: 'error test',
722
+ review_state: 'not-required',
723
+ });
724
+
725
+ // State should still be valid
726
+ const state2 = (await client.readResource('state://current')) as unknown;
727
+ const stateRes2 = state2 as Record<string, unknown>;
728
+ const data2 = JSON.parse(
729
+ ((stateRes2.contents as unknown[])[0] as Record<string, unknown>)
730
+ .text as string
731
+ );
732
+
733
+ expect(data2.currentPhase).toBe(data1.currentPhase);
734
+
735
+ expect(data2.conversationId).toBe(data1.conversationId);
736
+ });
737
+ });
738
+
739
+ describe('Default Behavior (Without Beads)', () => {
740
+ let client: DirectServerInterface;
741
+ let cleanup: () => Promise<void>;
742
+
743
+ beforeEach(async () => {
744
+ if (process.env.TASK_BACKEND) {
745
+ delete process.env.TASK_BACKEND;
746
+ }
747
+
748
+ const scenario = await createSuiteIsolatedE2EScenario({
749
+ suiteName: 'plugin-default-behavior',
750
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
751
+ });
752
+ client = scenario.client;
753
+ cleanup = scenario.cleanup;
754
+ });
755
+
756
+ afterEach(async () => {
757
+ if (cleanup) {
758
+ await cleanup();
759
+ }
760
+ });
761
+
762
+ it('should initialize server without beads plugin', async () => {
763
+ // Verify environment is clean
764
+ expect(process.env.TASK_BACKEND).toBeUndefined();
765
+
766
+ const result = await client.callTool('start_development', {
767
+ workflow: 'waterfall',
768
+ commit_behaviour: 'none',
769
+ });
770
+
771
+ const response = assertValidStartDevelopmentResponse(
772
+ assertToolSuccess(result)
773
+ );
774
+
775
+ await assertFileExists(response.plan_file_path);
776
+ expect(response.phase).toBe('requirements');
777
+ });
778
+
779
+ it('should handle start_development without plugin interference', async () => {
780
+ const result = await client.callTool('start_development', {
781
+ workflow: 'epcc',
782
+ commit_behaviour: 'none',
783
+ });
784
+
785
+ const response = assertValidStartDevelopmentResponse(
786
+ assertToolSuccess(result)
787
+ );
788
+
789
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
790
+ expect(planContent).toContain('## Explore');
791
+ expect(planContent).toContain('## Plan');
792
+ expect(planContent).toContain('## Code');
793
+ expect(planContent).toContain('## Commit');
794
+ });
795
+ });
796
+
797
+ describe('Resource Access', () => {
798
+ let client: DirectServerInterface;
799
+ let cleanup: () => Promise<void>;
800
+
801
+ beforeEach(async () => {
802
+ if (process.env.TASK_BACKEND) {
803
+ delete process.env.TASK_BACKEND;
804
+ }
805
+
806
+ const scenario = await createSuiteIsolatedE2EScenario({
807
+ suiteName: 'resource-access',
808
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
809
+ });
810
+ client = scenario.client;
811
+ cleanup = scenario.cleanup;
812
+
813
+ await initializeDevelopment(client, 'waterfall');
814
+ });
815
+
816
+ afterEach(async () => {
817
+ if (cleanup) {
818
+ await cleanup();
819
+ }
820
+ });
821
+
822
+ it('should provide access to state resource with valid structure', async () => {
823
+ const stateResource = (await client.readResource(
824
+ 'state://current'
825
+ )) as unknown;
826
+ const resource = stateResource as Record<string, unknown>;
827
+
828
+ expect(resource).toHaveProperty('contents');
829
+ expect(Array.isArray(resource.contents)).toBe(true);
830
+ expect((resource.contents as unknown[]).length).toBeGreaterThan(0);
831
+
832
+ const content = (
833
+ (resource.contents as unknown[])[0] as Record<string, unknown>
834
+ ).text as string;
835
+ const stateData = JSON.parse(content);
836
+ expect(typeof stateData.conversationId).toBe('string');
837
+ expect(stateData.conversationId.length).toBeGreaterThan(0);
838
+ expect(typeof stateData.currentPhase).toBe('string');
839
+ expect(stateData.currentPhase.length).toBeGreaterThan(0);
840
+ });
841
+
842
+ it('should provide access to plan resource with substantive content', async () => {
843
+ const planResource = (await client.readResource(
844
+ 'plan://current'
845
+ )) as unknown;
846
+ const resource = planResource as Record<string, unknown>;
847
+
848
+ expect(resource).toHaveProperty('contents');
849
+ expect(Array.isArray(resource.contents)).toBe(true);
850
+ expect((resource.contents as unknown[]).length).toBeGreaterThan(0);
851
+
852
+ const content = (
853
+ (resource.contents as unknown[])[0] as Record<string, unknown>
854
+ ).text as string;
855
+ expect(typeof content).toBe('string');
856
+ expect(content.length).toBeGreaterThan(0);
857
+ });
858
+
859
+ it('should provide access to system prompt resource', async () => {
860
+ const promptResource = (await client.readResource(
861
+ 'system-prompt://'
862
+ )) as unknown;
863
+ const resource = promptResource as Record<string, unknown>;
864
+
865
+ expect(resource).toHaveProperty('contents');
866
+ expect(Array.isArray(resource.contents)).toBe(true);
867
+ expect((resource.contents as unknown[]).length).toBeGreaterThan(0);
868
+
869
+ const contentObj = (resource.contents as unknown[])[0] as Record<
870
+ string,
871
+ unknown
872
+ >;
873
+ // Try text first (primary), then content (secondary), then get string representation
874
+ let content: string;
875
+ if (typeof contentObj.text === 'string' && contentObj.text.length > 0) {
876
+ content = contentObj.text;
877
+ } else if (
878
+ typeof contentObj.content === 'string' &&
879
+ contentObj.content.length > 0
880
+ ) {
881
+ content = contentObj.content;
882
+ } else if (Object.keys(contentObj).length > 0) {
883
+ // If object has properties but no usable string property, convert to string
884
+ content = JSON.stringify(contentObj);
885
+ } else {
886
+ throw new Error('Content object has no usable content');
887
+ }
888
+ expect(typeof content).toBe('string');
889
+ expect(content.length).toBeGreaterThan(0);
890
+ });
891
+ });
892
+
893
+ // =========================================================================
894
+ // PLUGIN HOOK EXECUTION VERIFICATION
895
+ // =========================================================================
896
+
897
+ describe('Plugin Hook Execution Verification', () => {
898
+ let client: DirectServerInterface;
899
+ let cleanup: () => Promise<void>;
900
+
901
+ beforeEach(async () => {
902
+ if (process.env.TASK_BACKEND) {
903
+ delete process.env.TASK_BACKEND;
904
+ }
905
+
906
+ const scenario = await createSuiteIsolatedE2EScenario({
907
+ suiteName: 'plugin-hook-execution',
908
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
909
+ });
910
+ client = scenario.client;
911
+ cleanup = scenario.cleanup;
912
+ });
913
+
914
+ afterEach(async () => {
915
+ if (cleanup) {
916
+ await cleanup();
917
+ }
918
+ });
919
+
920
+ it('should execute hooks during start_development and return valid response', async () => {
921
+ // Start development - triggers plugin hooks
922
+ const result = await client.callTool('start_development', {
923
+ workflow: 'waterfall',
924
+ commit_behaviour: 'none',
925
+ });
926
+
927
+ const response = assertValidStartDevelopmentResponse(
928
+ assertToolSuccess(result)
929
+ );
930
+
931
+ // (plan file exists, instructions present, phase valid)
932
+ expect(response.phase).toBe('requirements');
933
+ expect(response.plan_file_path).toBeDefined();
934
+
935
+ // Verify plan file was created by hooks
936
+ await assertFileExists(response.plan_file_path);
937
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
938
+ expect(planContent.length).toBeGreaterThan(0);
939
+ });
940
+
941
+ it('should maintain state consistency after hook execution', async () => {
942
+ // Start development
943
+ const startResult = await client.callTool('start_development', {
944
+ workflow: 'epcc',
945
+ commit_behaviour: 'none',
946
+ });
947
+
948
+ const startResponse = assertValidStartDevelopmentResponse(
949
+ assertToolSuccess(startResult)
950
+ );
951
+
952
+ // Call whats_next immediately after hooks
953
+ const whatsNextResult = await client.callTool('whats_next', {
954
+ user_input: 'test after hooks',
955
+ context: 'right after start',
956
+ });
957
+
958
+ const whatsNextResponse = assertValidWhatsNextResponse(
959
+ assertToolSuccess(whatsNextResult)
960
+ );
961
+
962
+ expect(whatsNextResponse.phase).toBe(startResponse.phase);
963
+ expect(whatsNextResponse.plan_file_path).toBe(
964
+ startResponse.plan_file_path
965
+ );
966
+ });
967
+
968
+ it('should ensure hooks do not break plan file validity', async () => {
969
+ // Start development
970
+ const result = await client.callTool('start_development', {
971
+ workflow: 'waterfall',
972
+ commit_behaviour: 'none',
973
+ });
974
+
975
+ const response = assertValidStartDevelopmentResponse(
976
+ assertToolSuccess(result)
977
+ );
978
+
979
+ // Read and validate plan file
980
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
981
+
982
+ expect(planContent).toMatch(/^# /m); // Title
983
+ expect(planContent).toMatch(/^## /m); // Sections
984
+ expect(planContent).toContain('## Goal');
985
+ expect(planContent).toContain('## Requirements');
986
+
987
+ expect(planContent).not.toContain('undefined');
988
+ expect(planContent).not.toContain('[object Object]');
989
+ });
990
+
991
+ it('should handle hook execution for multiple workflows', async () => {
992
+ const workflows = ['waterfall', 'epcc', 'tdd', 'minor'];
993
+
994
+ for (const workflow of workflows) {
995
+ // Create fresh scenario for each workflow
996
+ const scenario = await createSuiteIsolatedE2EScenario({
997
+ suiteName: `plugin-hooks-${workflow}`,
998
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
999
+ });
1000
+
1001
+ const result = await scenario.client.callTool('start_development', {
1002
+ workflow: workflow,
1003
+ commit_behaviour: 'none',
1004
+ });
1005
+
1006
+ const response = assertValidStartDevelopmentResponse(
1007
+ assertToolSuccess(result)
1008
+ );
1009
+
1010
+ await assertFileExists(response.plan_file_path);
1011
+
1012
+ await scenario.cleanup();
1013
+ }
1014
+ });
1015
+ });
1016
+
1017
+ // =========================================================================
1018
+ // PLUGIN SYSTEM ARCHITECTURE VALIDATION
1019
+ // =========================================================================
1020
+
1021
+ describe('Plugin System Architecture', () => {
1022
+ let client: DirectServerInterface;
1023
+ let cleanup: () => Promise<void>;
1024
+
1025
+ beforeEach(async () => {
1026
+ if (process.env.TASK_BACKEND) {
1027
+ delete process.env.TASK_BACKEND;
1028
+ }
1029
+
1030
+ const scenario = await createSuiteIsolatedE2EScenario({
1031
+ suiteName: 'plugin-architecture',
1032
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
1033
+ });
1034
+ client = scenario.client;
1035
+ cleanup = scenario.cleanup;
1036
+ });
1037
+
1038
+ afterEach(async () => {
1039
+ if (cleanup) {
1040
+ await cleanup();
1041
+ }
1042
+ });
1043
+
1044
+ it('should not expose plugin registry or internal plugin details', async () => {
1045
+ const result = await client.callTool('start_development', {
1046
+ workflow: 'waterfall',
1047
+ commit_behaviour: 'none',
1048
+ });
1049
+
1050
+ const response = assertToolSuccess(result);
1051
+
1052
+ assertNoPluginLeak(response);
1053
+
1054
+ expect(Object.keys(response).sort()).toEqual(
1055
+ [
1056
+ 'instructions',
1057
+ 'phase',
1058
+ 'plan_file_path',
1059
+ 'workflowDocumentationUrl',
1060
+ ].sort()
1061
+ );
1062
+ });
1063
+
1064
+ it('should apply plugins uniformly across all tool calls', async () => {
1065
+ // Start development
1066
+ const startResult = await client.callTool('start_development', {
1067
+ workflow: 'waterfall',
1068
+ commit_behaviour: 'none',
1069
+ });
1070
+
1071
+ assertValidStartDevelopmentResponse(assertToolSuccess(startResult));
1072
+
1073
+ // Get whats_next
1074
+ const whatsNextResult = await client.callTool('whats_next', {
1075
+ user_input: 'next step',
1076
+ });
1077
+
1078
+ assertValidWhatsNextResponse(assertToolSuccess(whatsNextResult));
1079
+
1080
+ // Transition phase
1081
+ const transitionResult = await client.callTool('proceed_to_phase', {
1082
+ target_phase: 'design',
1083
+ reason: 'ready',
1084
+ review_state: 'not-required',
1085
+ });
1086
+
1087
+ assertValidProceedToPhaseResponse(assertToolSuccess(transitionResult));
1088
+ });
1089
+
1090
+ it('should preserve plugin boundaries (no cross-pollution)', async () => {
1091
+ // Start development
1092
+ const result = await client.callTool('start_development', {
1093
+ workflow: 'epcc',
1094
+ commit_behaviour: 'none',
1095
+ });
1096
+
1097
+ const response = assertToolSuccess(result);
1098
+
1099
+ assertNoPluginLeak(response);
1100
+
1101
+ expect(response).toHaveProperty('plan_file_path');
1102
+ expect(response).toHaveProperty('instructions');
1103
+
1104
+ expect(response).not.toHaveProperty('_plugins');
1105
+ expect(response).not.toHaveProperty('beads');
1106
+ expect(response).not.toHaveProperty('taskBackendClient');
1107
+ });
1108
+ });
1109
+
1110
+ // =========================================================================
1111
+ // WORKFLOW INITIALIZATION VALIDATION
1112
+ // =========================================================================
1113
+
1114
+ describe('Workflow Initialization with Plugin Support', () => {
1115
+ let cleanup: () => Promise<void>;
1116
+
1117
+ afterEach(async () => {
1118
+ if (cleanup) {
1119
+ await cleanup();
1120
+ }
1121
+ if (process.env.TASK_BACKEND) {
1122
+ delete process.env.TASK_BACKEND;
1123
+ }
1124
+ });
1125
+
1126
+ it('should initialize waterfall with correct initial phase', async () => {
1127
+ const scenario = await createSuiteIsolatedE2EScenario({
1128
+ suiteName: 'init-waterfall',
1129
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
1130
+ });
1131
+ cleanup = scenario.cleanup;
1132
+
1133
+ const result = await scenario.client.callTool('start_development', {
1134
+ workflow: 'waterfall',
1135
+ commit_behaviour: 'none',
1136
+ });
1137
+
1138
+ const response = assertValidStartDevelopmentResponse(
1139
+ assertToolSuccess(result)
1140
+ );
1141
+
1142
+ expect(response.phase).toBe(WORKFLOW_INITIAL_PHASES.waterfall);
1143
+ });
1144
+
1145
+ it('should initialize epcc with correct initial phase', async () => {
1146
+ const scenario = await createSuiteIsolatedE2EScenario({
1147
+ suiteName: 'init-epcc',
1148
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
1149
+ });
1150
+ cleanup = scenario.cleanup;
1151
+
1152
+ const result = await scenario.client.callTool('start_development', {
1153
+ workflow: 'epcc',
1154
+ commit_behaviour: 'none',
1155
+ });
1156
+
1157
+ const response = assertValidStartDevelopmentResponse(
1158
+ assertToolSuccess(result)
1159
+ );
1160
+
1161
+ expect(response.phase).toBe(WORKFLOW_INITIAL_PHASES.epcc);
1162
+ });
1163
+
1164
+ it('should initialize tdd with correct initial phase', async () => {
1165
+ const scenario = await createSuiteIsolatedE2EScenario({
1166
+ suiteName: 'init-tdd',
1167
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
1168
+ });
1169
+ cleanup = scenario.cleanup;
1170
+
1171
+ const result = await scenario.client.callTool('start_development', {
1172
+ workflow: 'tdd',
1173
+ commit_behaviour: 'none',
1174
+ });
1175
+
1176
+ const response = assertValidStartDevelopmentResponse(
1177
+ assertToolSuccess(result)
1178
+ );
1179
+
1180
+ expect(response.phase).toBe(WORKFLOW_INITIAL_PHASES.tdd);
1181
+ });
1182
+
1183
+ it('should initialize minor with correct initial phase', async () => {
1184
+ const scenario = await createSuiteIsolatedE2EScenario({
1185
+ suiteName: 'init-minor',
1186
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
1187
+ });
1188
+ cleanup = scenario.cleanup;
1189
+
1190
+ const result = await scenario.client.callTool('start_development', {
1191
+ workflow: 'minor',
1192
+ commit_behaviour: 'none',
1193
+ });
1194
+
1195
+ const response = assertValidStartDevelopmentResponse(
1196
+ assertToolSuccess(result)
1197
+ );
1198
+
1199
+ expect(response.phase).toBe(WORKFLOW_INITIAL_PHASES.minor);
1200
+ });
1201
+
1202
+ it('should initialize bugfix with expected initial phase', async () => {
1203
+ const scenario = await createSuiteIsolatedE2EScenario({
1204
+ suiteName: 'init-bugfix',
1205
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
1206
+ });
1207
+ cleanup = scenario.cleanup;
1208
+
1209
+ const result = await scenario.client.callTool('start_development', {
1210
+ workflow: 'bugfix',
1211
+ commit_behaviour: 'none',
1212
+ });
1213
+
1214
+ const response = assertValidStartDevelopmentResponse(
1215
+ assertToolSuccess(result)
1216
+ );
1217
+
1218
+ const expectedPhases = WORKFLOW_INITIAL_PHASES.bugfix;
1219
+ expect(expectedPhases).toContain(response.phase);
1220
+ });
1221
+ });
1222
+
1223
+ // =========================================================================
1224
+ // PLAN FILE AND INSTRUCTION QUALITY
1225
+ // =========================================================================
1226
+
1227
+ describe('Plan File and Instruction Quality Across Workflows', () => {
1228
+ let client: DirectServerInterface;
1229
+ let cleanup: () => Promise<void>;
1230
+
1231
+ beforeEach(async () => {
1232
+ if (process.env.TASK_BACKEND) {
1233
+ delete process.env.TASK_BACKEND;
1234
+ }
1235
+
1236
+ const scenario = await createSuiteIsolatedE2EScenario({
1237
+ suiteName: 'quality-across-workflows',
1238
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
1239
+ });
1240
+ client = scenario.client;
1241
+ cleanup = scenario.cleanup;
1242
+ });
1243
+
1244
+ afterEach(async () => {
1245
+ if (cleanup) {
1246
+ await cleanup();
1247
+ }
1248
+ });
1249
+
1250
+ it('should generate substantive instructions that meet minimum length requirement', async () => {
1251
+ const result = await client.callTool('start_development', {
1252
+ workflow: 'waterfall',
1253
+ commit_behaviour: 'none',
1254
+ });
1255
+
1256
+ const response = assertValidStartDevelopmentResponse(
1257
+ assertToolSuccess(result)
1258
+ );
1259
+
1260
+ expect(response.instructions.length).toBeGreaterThan(
1261
+ MIN_INSTRUCTION_LENGTH
1262
+ );
1263
+ });
1264
+
1265
+ it('should create plan files with valid markdown structure', async () => {
1266
+ const result = await client.callTool('start_development', {
1267
+ workflow: 'waterfall',
1268
+ commit_behaviour: 'none',
1269
+ });
1270
+
1271
+ const response = assertValidStartDevelopmentResponse(
1272
+ assertToolSuccess(result)
1273
+ );
1274
+
1275
+ const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
1276
+
1277
+ expect(planContent).toMatch(/^# /m); // Must have main title
1278
+ expect(planContent).toMatch(/^## /m); // Must have sections
1279
+ expect(planContent).not.toContain('[object Object]'); // No serialization errors
1280
+ expect(planContent).not.toContain('undefined'); // No undefined placeholders
1281
+ });
1282
+
1283
+ it('should ensure instructions are context-aware for the current phase', async () => {
1284
+ // Start and get initial instructions
1285
+ const startResult = await client.callTool('start_development', {
1286
+ workflow: 'waterfall',
1287
+ commit_behaviour: 'none',
1288
+ });
1289
+
1290
+ const startResponse = assertValidStartDevelopmentResponse(
1291
+ assertToolSuccess(startResult)
1292
+ );
1293
+
1294
+ expect(startResponse.instructions).toMatch(/requirement|phase|task/i);
1295
+
1296
+ // Transition to design phase
1297
+ await client.callTool('proceed_to_phase', {
1298
+ target_phase: 'design',
1299
+ reason: 'ready',
1300
+ review_state: 'not-required',
1301
+ });
1302
+
1303
+ // Get instructions for design phase
1304
+ const designWhatsNext = await client.callTool('whats_next', {
1305
+ user_input: 'what now in design?',
1306
+ });
1307
+
1308
+ const designResponse = assertValidWhatsNextResponse(
1309
+ assertToolSuccess(designWhatsNext)
1310
+ );
1311
+
1312
+ expect(designResponse.instructions).toBeDefined();
1313
+ expect(designResponse.instructions.length).toBeGreaterThan(
1314
+ MIN_INSTRUCTION_LENGTH
1315
+ );
1316
+ });
1317
+ });
1318
+
1319
+ // =========================================================================
1320
+ // STATE PERSISTENCE AND CONSISTENCY
1321
+ // =========================================================================
1322
+
1323
+ describe('State Persistence Across Plugin Execution', () => {
1324
+ let client: DirectServerInterface;
1325
+ let cleanup: () => Promise<void>;
1326
+
1327
+ beforeEach(async () => {
1328
+ if (process.env.TASK_BACKEND) {
1329
+ delete process.env.TASK_BACKEND;
1330
+ }
1331
+
1332
+ const scenario = await createSuiteIsolatedE2EScenario({
1333
+ suiteName: 'state-persistence',
1334
+ tempProjectFactory: createTempProjectWithDefaultStateMachine,
1335
+ });
1336
+ client = scenario.client;
1337
+ cleanup = scenario.cleanup;
1338
+ });
1339
+
1340
+ afterEach(async () => {
1341
+ if (cleanup) {
1342
+ await cleanup();
1343
+ }
1344
+ });
1345
+
1346
+ it('should preserve plan file path through multiple operations', async () => {
1347
+ // Start development
1348
+ const startResult = await client.callTool('start_development', {
1349
+ workflow: 'waterfall',
1350
+ commit_behaviour: 'none',
1351
+ });
1352
+
1353
+ const startResponse = assertValidStartDevelopmentResponse(
1354
+ assertToolSuccess(startResult)
1355
+ );
1356
+ const planPath = startResponse.plan_file_path;
1357
+
1358
+ // Get whats_next
1359
+ const whatsNextResult = await client.callTool('whats_next', {
1360
+ user_input: 'continue',
1361
+ });
1362
+
1363
+ const whatsNextResponse = assertValidWhatsNextResponse(
1364
+ assertToolSuccess(whatsNextResult)
1365
+ );
1366
+
1367
+ expect(whatsNextResponse.plan_file_path).toBe(planPath);
1368
+
1369
+ // Transition
1370
+ const transitionResult = await client.callTool('proceed_to_phase', {
1371
+ target_phase: 'design',
1372
+ reason: 'ready',
1373
+ review_state: 'not-required',
1374
+ });
1375
+
1376
+ const transitionResponse = assertValidProceedToPhaseResponse(
1377
+ assertToolSuccess(transitionResult)
1378
+ );
1379
+
1380
+ expect(transitionResponse.plan_file_path).toBe(planPath);
1381
+ });
1382
+
1383
+ it('should maintain plan file integrity through multiple tool calls', async () => {
1384
+ // Start development
1385
+ const startResult = await client.callTool('start_development', {
1386
+ workflow: 'waterfall',
1387
+ commit_behaviour: 'none',
1388
+ });
1389
+
1390
+ const startResponse = assertValidStartDevelopmentResponse(
1391
+ assertToolSuccess(startResult)
1392
+ );
1393
+
1394
+ // Verify plan file exists for multiple operations
1395
+ const _initialContent = await fs.readFile(
1396
+ startResponse.plan_file_path,
1397
+ 'utf-8'
1398
+ );
1399
+
1400
+ // Make multiple calls
1401
+ await client.callTool('whats_next', { user_input: 'test' });
1402
+ await client.callTool('whats_next', { user_input: 'test2' });
1403
+ await client.callTool('proceed_to_phase', {
1404
+ target_phase: 'design',
1405
+ reason: 'ready',
1406
+ review_state: 'not-required',
1407
+ });
1408
+
1409
+ // Check plan file still valid
1410
+ const finalContent = await fs.readFile(
1411
+ startResponse.plan_file_path,
1412
+ 'utf-8'
1413
+ );
1414
+
1415
+ expect(finalContent.length).toBeGreaterThan(0);
1416
+
1417
+ expect(finalContent).not.toContain('[object Object]');
1418
+ expect(finalContent).not.toContain('undefined');
1419
+ });
1420
+ });
1421
+ });