@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/components/beads/beads-instruction-generator.d.ts +4 -8
- package/dist/components/beads/beads-instruction-generator.d.ts.map +1 -1
- package/dist/components/beads/beads-instruction-generator.js +28 -51
- package/dist/components/beads/beads-instruction-generator.js.map +1 -1
- package/dist/components/beads/beads-task-backend-client.d.ts.map +1 -1
- package/dist/components/beads/beads-task-backend-client.js +1 -4
- package/dist/components/beads/beads-task-backend-client.js.map +1 -1
- package/dist/plugin-system/beads-plugin.d.ts +70 -0
- package/dist/plugin-system/beads-plugin.d.ts.map +1 -0
- package/dist/plugin-system/beads-plugin.js +459 -0
- package/dist/plugin-system/beads-plugin.js.map +1 -0
- package/dist/plugin-system/index.d.ts +9 -0
- package/dist/plugin-system/index.d.ts.map +1 -0
- package/dist/plugin-system/index.js +9 -0
- package/dist/plugin-system/index.js.map +1 -0
- package/dist/plugin-system/plugin-interfaces.d.ts +99 -0
- package/dist/plugin-system/plugin-interfaces.d.ts.map +1 -0
- package/dist/plugin-system/plugin-interfaces.js +9 -0
- package/dist/plugin-system/plugin-interfaces.js.map +1 -0
- package/dist/plugin-system/plugin-registry.d.ts +44 -0
- package/dist/plugin-system/plugin-registry.d.ts.map +1 -0
- package/dist/plugin-system/plugin-registry.js +132 -0
- package/dist/plugin-system/plugin-registry.js.map +1 -0
- package/dist/server-config.d.ts.map +1 -1
- package/dist/server-config.js +28 -8
- package/dist/server-config.js.map +1 -1
- package/dist/tool-handlers/conduct-review.d.ts.map +1 -1
- package/dist/tool-handlers/conduct-review.js +1 -2
- package/dist/tool-handlers/conduct-review.js.map +1 -1
- package/dist/tool-handlers/get-tool-info.d.ts +0 -1
- package/dist/tool-handlers/get-tool-info.d.ts.map +1 -1
- package/dist/tool-handlers/get-tool-info.js +0 -1
- package/dist/tool-handlers/get-tool-info.js.map +1 -1
- package/dist/tool-handlers/proceed-to-phase.d.ts +0 -7
- package/dist/tool-handlers/proceed-to-phase.d.ts.map +1 -1
- package/dist/tool-handlers/proceed-to-phase.js +15 -95
- package/dist/tool-handlers/proceed-to-phase.js.map +1 -1
- package/dist/tool-handlers/resume-workflow.d.ts +0 -1
- package/dist/tool-handlers/resume-workflow.d.ts.map +1 -1
- package/dist/tool-handlers/resume-workflow.js +0 -1
- package/dist/tool-handlers/resume-workflow.js.map +1 -1
- package/dist/tool-handlers/start-development.d.ts +0 -16
- package/dist/tool-handlers/start-development.d.ts.map +1 -1
- package/dist/tool-handlers/start-development.js +29 -130
- package/dist/tool-handlers/start-development.js.map +1 -1
- package/dist/tool-handlers/whats-next.d.ts +0 -2
- package/dist/tool-handlers/whats-next.d.ts.map +1 -1
- package/dist/tool-handlers/whats-next.js +1 -2
- package/dist/tool-handlers/whats-next.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/beads/beads-instruction-generator.ts +32 -64
- package/src/components/beads/beads-task-backend-client.ts +1 -4
- package/src/plugin-system/beads-plugin.ts +641 -0
- package/src/plugin-system/index.ts +20 -0
- package/src/plugin-system/plugin-interfaces.ts +154 -0
- package/src/plugin-system/plugin-registry.ts +190 -0
- package/src/server-config.ts +30 -8
- package/src/tool-handlers/conduct-review.ts +1 -2
- package/src/tool-handlers/get-tool-info.ts +0 -2
- package/src/tool-handlers/proceed-to-phase.ts +19 -139
- package/src/tool-handlers/resume-workflow.ts +0 -2
- package/src/tool-handlers/start-development.ts +35 -213
- package/src/tool-handlers/whats-next.ts +1 -4
- package/src/types.ts +2 -0
- package/test/e2e/beads-plugin-integration.test.ts +1594 -0
- package/test/e2e/core-functionality.test.ts +3 -12
- package/test/e2e/mcp-contract.test.ts +0 -31
- package/test/e2e/plugin-system-integration.test.ts +1421 -0
- package/test/e2e/state-management.test.ts +1 -5
- package/test/e2e/workflow-integration.test.ts +2 -11
- package/test/unit/beads-instruction-generator.test.ts +235 -103
- package/test/unit/beads-phase-task-id-integration.test.ts +7 -29
- package/test/unit/beads-plugin-behavioral.test.ts +512 -0
- package/test/unit/beads-plugin.test.ts +94 -0
- package/test/unit/plugin-error-handling.test.ts +240 -0
- package/test/unit/proceed-to-phase-plugin-integration.test.ts +150 -0
- package/test/unit/resume-workflow.test.ts +0 -1
- package/test/unit/server-config-plugin-registry.test.ts +81 -0
- package/test/unit/server-tools.test.ts +0 -1
- package/test/unit/start-development-goal-extraction.test.ts +22 -16
- package/test/utils/test-helpers.ts +3 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/components/server-components-factory.d.ts +0 -39
- package/dist/components/server-components-factory.d.ts.map +0 -1
- package/dist/components/server-components-factory.js +0 -62
- package/dist/components/server-components-factory.js.map +0 -1
- package/src/components/server-components-factory.ts +0 -86
- package/test/e2e/component-substitution.test.ts +0 -208
- package/test/unit/beads-integration-filename.test.ts +0 -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
|
+
});
|