@codemcp/workflows 4.10.0 → 4.10.2
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 +3 -4
- package/dist/components/beads/beads-instruction-generator.d.ts.map +1 -1
- package/dist/components/beads/beads-instruction-generator.js +12 -7
- 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/proceed-to-phase.d.ts +0 -5
- package/dist/tool-handlers/proceed-to-phase.d.ts.map +1 -1
- package/dist/tool-handlers/proceed-to-phase.js +15 -93
- package/dist/tool-handlers/proceed-to-phase.js.map +1 -1
- package/dist/tool-handlers/start-development.d.ts +0 -13
- package/dist/tool-handlers/start-development.d.ts.map +1 -1
- package/dist/tool-handlers/start-development.js +29 -124
- package/dist/tool-handlers/start-development.js.map +1 -1
- package/dist/tool-handlers/whats-next.d.ts.map +1 -1
- package/dist/tool-handlers/whats-next.js +1 -0
- 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 +12 -12
- 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/proceed-to-phase.ts +19 -135
- package/src/tool-handlers/start-development.ts +35 -205
- package/src/tool-handlers/whats-next.ts +1 -0
- package/src/types.ts +2 -0
- package/test/e2e/beads-plugin-integration.test.ts +1609 -0
- package/test/e2e/plugin-system-integration.test.ts +1729 -0
- 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/server-config-plugin-registry.test.ts +81 -0
- 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,1609 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive Beads Plugin Integration Test
|
|
3
|
+
*
|
|
4
|
+
* This single test file validates ALL aspects of beads plugin behavior:
|
|
5
|
+
* 1. Plan file structure with beads markers
|
|
6
|
+
* 2. Beads instruction generation
|
|
7
|
+
* 3. Beads task creation on start
|
|
8
|
+
* 4. Plan file task ID integration
|
|
9
|
+
* 5. Phase transition validation
|
|
10
|
+
* 6. With vs without beads comparison
|
|
11
|
+
* 7. Beads error handling
|
|
12
|
+
* 8. Plugin hook integration
|
|
13
|
+
*
|
|
14
|
+
* Design Principles:
|
|
15
|
+
* - NO fuzzy assertions
|
|
16
|
+
* - EXPLICIT validation of content (not just existence)
|
|
17
|
+
* - COMPREHENSIVE coverage of all beads functionality
|
|
18
|
+
* - PROPER isolation and cleanup between tests
|
|
19
|
+
* - MEANINGFUL test names that describe what is validated
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
23
|
+
import { createTempProjectWithDefaultStateMachine } from '../utils/temp-files';
|
|
24
|
+
import {
|
|
25
|
+
DirectServerInterface,
|
|
26
|
+
createSuiteIsolatedE2EScenario,
|
|
27
|
+
assertToolSuccess,
|
|
28
|
+
} from '../utils/e2e-test-setup';
|
|
29
|
+
import { promises as fs } from 'node:fs';
|
|
30
|
+
import { execSync } from 'node:child_process';
|
|
31
|
+
import type { StartDevelopmentResult } from '../../src/tool-handlers/start-development';
|
|
32
|
+
import type { WhatsNextResult } from '../../src/tool-handlers/whats-next';
|
|
33
|
+
|
|
34
|
+
vi.unmock('fs');
|
|
35
|
+
vi.unmock('fs/promises');
|
|
36
|
+
|
|
37
|
+
// Mock child_process to simulate beads CLI responses
|
|
38
|
+
vi.mock('node:child_process', () => ({
|
|
39
|
+
execSync: vi.fn(),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
// Track created task IDs for consistent mock responses
|
|
43
|
+
let mockTaskIdCounter = 0;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Setup beads CLI mock for tests
|
|
47
|
+
* This simulates the bd CLI responses needed for beads integration
|
|
48
|
+
*/
|
|
49
|
+
function setupBeadsCliMock(): void {
|
|
50
|
+
mockTaskIdCounter = 0;
|
|
51
|
+
|
|
52
|
+
vi.mocked(execSync).mockImplementation((command: string) => {
|
|
53
|
+
// Handle bd --version check (used by TaskBackendManager)
|
|
54
|
+
if (command === 'bd --version') {
|
|
55
|
+
return 'beads v1.0.0\n';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Handle bd list command (used to check if beads is initialized)
|
|
59
|
+
if (command.includes('bd list')) {
|
|
60
|
+
return ''; // Return empty list
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Handle bd init command
|
|
64
|
+
if (command.includes('bd init')) {
|
|
65
|
+
return 'Initialized beads repository\n';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Handle bd create command for epic/phase tasks
|
|
69
|
+
if (command.includes('bd create')) {
|
|
70
|
+
mockTaskIdCounter++;
|
|
71
|
+
const taskId = `mock-task-${mockTaskIdCounter}`;
|
|
72
|
+
return `✓ Created issue: ${taskId}\n`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Handle bd dep command for dependencies
|
|
76
|
+
if (command.includes('bd dep')) {
|
|
77
|
+
return '✓ Dependency created\n';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Handle bd show command
|
|
81
|
+
if (command.includes('bd show')) {
|
|
82
|
+
return 'Title: Mock Task\nStatus: open\n';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Handle bd update command
|
|
86
|
+
if (command.includes('bd update')) {
|
|
87
|
+
return '✓ Updated\n';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Handle bd close command
|
|
91
|
+
if (command.includes('bd close')) {
|
|
92
|
+
return '✓ Closed\n';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Handle git commands (used in some tests)
|
|
96
|
+
if (command === 'git symbolic-ref --short HEAD') {
|
|
97
|
+
return 'feature/test-branch\n';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// For any other command, throw an error (unexpected command)
|
|
101
|
+
throw new Error(`Unexpected command in beads test: ${command}`);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// TEST CONSTANTS (Remove magic numbers)
|
|
107
|
+
// ============================================================================
|
|
108
|
+
|
|
109
|
+
// Minimum number of phases in a workflow that should have beads markers
|
|
110
|
+
const MIN_PHASES_WITH_MARKERS = 4;
|
|
111
|
+
|
|
112
|
+
// Minimum length for substantive instructions
|
|
113
|
+
// Must be long enough to contain meaningful guidance, not just placeholders
|
|
114
|
+
const MIN_INSTRUCTION_LENGTH = 200;
|
|
115
|
+
|
|
116
|
+
// ============================================================================
|
|
117
|
+
// HELPER FUNCTIONS
|
|
118
|
+
// ============================================================================
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Verify beads plan file has the expected structure with markers
|
|
122
|
+
*/
|
|
123
|
+
function validateBeadsPlanFileStructure(content: string): void {
|
|
124
|
+
// Should have beads-phase-id markers for each phase
|
|
125
|
+
expect(content).toContain('<!-- beads-phase-id:');
|
|
126
|
+
|
|
127
|
+
// Should have HTML comment format with either TBD or actual task IDs
|
|
128
|
+
// Task IDs can include alphanumerics, hyphens, and dots
|
|
129
|
+
expect(content).toMatch(/<!-- beads-phase-id:\s*(TBD|[a-zA-Z0-9\-.]+)\s*-->/);
|
|
130
|
+
|
|
131
|
+
// Should have phase headers
|
|
132
|
+
expect(content).toMatch(/^## \w+/m);
|
|
133
|
+
|
|
134
|
+
// Should have "Tasks managed via `bd` CLI" guidance
|
|
135
|
+
expect(content).toContain('Tasks managed via');
|
|
136
|
+
expect(content).toContain('bd');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Verify instructions contain beads CLI references
|
|
141
|
+
*/
|
|
142
|
+
function validateBeadsInstructions(instructions: string): void {
|
|
143
|
+
// Should mention bd CLI tool
|
|
144
|
+
expect(instructions.toLowerCase()).toContain('bd');
|
|
145
|
+
|
|
146
|
+
// Should have bd commands
|
|
147
|
+
const hasCommands = /bd\s+(list|create|update|close|show)/i.test(
|
|
148
|
+
instructions
|
|
149
|
+
);
|
|
150
|
+
expect(hasCommands).toBe(true);
|
|
151
|
+
|
|
152
|
+
// Should mention task management
|
|
153
|
+
expect(instructions.toLowerCase()).toContain('task');
|
|
154
|
+
|
|
155
|
+
// Should have beads-specific guidance
|
|
156
|
+
expect(instructions).toContain('🔧 BD CLI Task Management');
|
|
157
|
+
|
|
158
|
+
// Should mention using ONLY bd CLI
|
|
159
|
+
expect(instructions).toContain('Use ONLY bd CLI tool');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Extract beads phase IDs from plan file content
|
|
164
|
+
* Returns array of task IDs found in the plan
|
|
165
|
+
*/
|
|
166
|
+
function extractBeadsPhaseIds(content: string): string[] {
|
|
167
|
+
const matches =
|
|
168
|
+
content.match(/<!-- beads-phase-id:\s*([a-zA-Z0-9\-.]+)\s*-->/g) || [];
|
|
169
|
+
return matches
|
|
170
|
+
.map(match => {
|
|
171
|
+
const idMatch = match.match(/beads-phase-id:\s*([a-zA-Z0-9\-.]+)\s*-->/);
|
|
172
|
+
return idMatch ? idMatch[1] : '';
|
|
173
|
+
})
|
|
174
|
+
.filter(id => id.length > 0);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ============================================================================
|
|
178
|
+
// TESTS
|
|
179
|
+
// ============================================================================
|
|
180
|
+
|
|
181
|
+
describe('Beads Plugin Comprehensive Integration', () => {
|
|
182
|
+
// =========================================================================
|
|
183
|
+
// 1. PLAN FILE STRUCTURE
|
|
184
|
+
// =========================================================================
|
|
185
|
+
|
|
186
|
+
describe('1. Plan File Structure with Beads Markers', () => {
|
|
187
|
+
let client: DirectServerInterface;
|
|
188
|
+
let cleanup: () => Promise<void>;
|
|
189
|
+
|
|
190
|
+
beforeEach(async () => {
|
|
191
|
+
// CRITICAL: Enable beads backend and mock CLI
|
|
192
|
+
process.env.TASK_BACKEND = 'beads';
|
|
193
|
+
setupBeadsCliMock();
|
|
194
|
+
|
|
195
|
+
const scenario = await createSuiteIsolatedE2EScenario({
|
|
196
|
+
suiteName: 'beads-plan-structure',
|
|
197
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
198
|
+
});
|
|
199
|
+
client = scenario.client;
|
|
200
|
+
cleanup = scenario.cleanup;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
afterEach(async () => {
|
|
204
|
+
if (cleanup) {
|
|
205
|
+
await cleanup();
|
|
206
|
+
}
|
|
207
|
+
delete process.env.TASK_BACKEND;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should create plan file WITH beads-phase-id placeholders when TASK_BACKEND=beads', async () => {
|
|
211
|
+
// Verify environment
|
|
212
|
+
expect(process.env.TASK_BACKEND).toBe('beads');
|
|
213
|
+
|
|
214
|
+
// Start development
|
|
215
|
+
const result = await client.callTool('start_development', {
|
|
216
|
+
workflow: 'epcc',
|
|
217
|
+
commit_behaviour: 'none',
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
221
|
+
const planFilePath = response.plan_file_path;
|
|
222
|
+
|
|
223
|
+
// Read plan file
|
|
224
|
+
const planContent = await fs.readFile(planFilePath, 'utf-8');
|
|
225
|
+
|
|
226
|
+
// VALIDATE: Plan file has beads markers
|
|
227
|
+
validateBeadsPlanFileStructure(planContent);
|
|
228
|
+
|
|
229
|
+
// VALIDATE: Each phase has beads-phase-id placeholder
|
|
230
|
+
expect(planContent).toContain('## Explore');
|
|
231
|
+
expect(planContent).toMatch(/## Explore\n<!-- beads-phase-id:/);
|
|
232
|
+
|
|
233
|
+
expect(planContent).toContain('## Plan');
|
|
234
|
+
expect(planContent).toMatch(/## Plan\n<!-- beads-phase-id:/);
|
|
235
|
+
|
|
236
|
+
expect(planContent).toContain('## Code');
|
|
237
|
+
expect(planContent).toMatch(/## Code\n<!-- beads-phase-id:/);
|
|
238
|
+
|
|
239
|
+
expect(planContent).toContain('## Commit');
|
|
240
|
+
expect(planContent).toMatch(/## Commit\n<!-- beads-phase-id:/);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should have beads phase IDs for each phase (updated by plugin hook)', async () => {
|
|
244
|
+
// Start development
|
|
245
|
+
const result = await client.callTool('start_development', {
|
|
246
|
+
workflow: 'epcc',
|
|
247
|
+
commit_behaviour: 'none',
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
251
|
+
const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
|
|
252
|
+
|
|
253
|
+
// VALIDATE: Plan has beads-phase-id markers (either TBD or actual IDs)
|
|
254
|
+
// The plugin's afterStartDevelopment hook will update TBD placeholders with actual task IDs
|
|
255
|
+
expect(planContent).toContain('<!-- beads-phase-id:');
|
|
256
|
+
|
|
257
|
+
// Count beads-phase-id occurrences (should be one per phase)
|
|
258
|
+
const phaseIdMatches = planContent.match(/<!-- beads-phase-id:/g);
|
|
259
|
+
expect(phaseIdMatches).not.toBeNull();
|
|
260
|
+
expect((phaseIdMatches || []).length).toBeGreaterThanOrEqual(
|
|
261
|
+
MIN_PHASES_WITH_MARKERS
|
|
262
|
+
); // At least 4 phases
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should format beads markers as HTML comments with proper structure', async () => {
|
|
266
|
+
// Start development
|
|
267
|
+
const result = await client.callTool('start_development', {
|
|
268
|
+
workflow: 'epcc',
|
|
269
|
+
commit_behaviour: 'none',
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
273
|
+
const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
|
|
274
|
+
|
|
275
|
+
// VALIDATE: Format matches the pattern (either TBD or actual task IDs with dots)
|
|
276
|
+
expect(planContent).toMatch(
|
|
277
|
+
/## \w+\n<!-- beads-phase-id:\s*(TBD|[a-zA-Z0-9\-.]+)\s*-->\n### Tasks/
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
// VALIDATE: Must be HTML comment format
|
|
281
|
+
expect(planContent).toMatch(/<!-- beads-phase-id:/);
|
|
282
|
+
expect(planContent).not.toMatch(/\/\/ beads-phase-id:/);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// =========================================================================
|
|
287
|
+
// 2. BEADS INSTRUCTION GENERATION
|
|
288
|
+
// =========================================================================
|
|
289
|
+
|
|
290
|
+
describe('2. Beads Instruction Generation', () => {
|
|
291
|
+
let client: DirectServerInterface;
|
|
292
|
+
let cleanup: () => Promise<void>;
|
|
293
|
+
|
|
294
|
+
beforeEach(async () => {
|
|
295
|
+
process.env.TASK_BACKEND = 'beads';
|
|
296
|
+
setupBeadsCliMock();
|
|
297
|
+
|
|
298
|
+
const scenario = await createSuiteIsolatedE2EScenario({
|
|
299
|
+
suiteName: 'beads-instructions',
|
|
300
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
301
|
+
});
|
|
302
|
+
client = scenario.client;
|
|
303
|
+
cleanup = scenario.cleanup;
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
afterEach(async () => {
|
|
307
|
+
if (cleanup) {
|
|
308
|
+
await cleanup();
|
|
309
|
+
}
|
|
310
|
+
delete process.env.TASK_BACKEND;
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should generate beads-specific instructions mentioning bd CLI', async () => {
|
|
314
|
+
// Start development
|
|
315
|
+
const startResult = await client.callTool('start_development', {
|
|
316
|
+
workflow: 'epcc',
|
|
317
|
+
commit_behaviour: 'none',
|
|
318
|
+
});
|
|
319
|
+
assertToolSuccess(startResult);
|
|
320
|
+
|
|
321
|
+
// Get instructions
|
|
322
|
+
const whatsNextResult = await client.callTool('whats_next', {
|
|
323
|
+
context: 'Testing beads instructions',
|
|
324
|
+
user_input: 'What should I do?',
|
|
325
|
+
conversation_summary: 'Started development with beads',
|
|
326
|
+
recent_messages: [],
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const response = assertToolSuccess(whatsNextResult) as WhatsNextResult;
|
|
330
|
+
const instructions = response.instructions;
|
|
331
|
+
|
|
332
|
+
// VALIDATE: Instructions mention beads
|
|
333
|
+
validateBeadsInstructions(instructions);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should include bd CLI commands in instructions', async () => {
|
|
337
|
+
// Start development
|
|
338
|
+
const startResult = await client.callTool('start_development', {
|
|
339
|
+
workflow: 'epcc',
|
|
340
|
+
commit_behaviour: 'none',
|
|
341
|
+
});
|
|
342
|
+
assertToolSuccess(startResult);
|
|
343
|
+
|
|
344
|
+
// Get instructions
|
|
345
|
+
const whatsNextResult = await client.callTool('whats_next', {
|
|
346
|
+
context: 'Testing beads instructions',
|
|
347
|
+
user_input: 'What should I do?',
|
|
348
|
+
conversation_summary: 'Started development with beads',
|
|
349
|
+
recent_messages: [],
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const response = assertToolSuccess(whatsNextResult) as WhatsNextResult;
|
|
353
|
+
const instructions = response.instructions;
|
|
354
|
+
|
|
355
|
+
// VALIDATE: Should have multiple bd commands
|
|
356
|
+
expect(instructions).toMatch(/`bd\s+list/);
|
|
357
|
+
expect(instructions).toMatch(/`bd\s+create/);
|
|
358
|
+
expect(instructions).toMatch(/`bd\s+(update|close)/);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should remind user to use ONLY bd CLI for task management', async () => {
|
|
362
|
+
// Start development
|
|
363
|
+
const startResult = await client.callTool('start_development', {
|
|
364
|
+
workflow: 'epcc',
|
|
365
|
+
commit_behaviour: 'none',
|
|
366
|
+
});
|
|
367
|
+
assertToolSuccess(startResult);
|
|
368
|
+
|
|
369
|
+
// Get instructions
|
|
370
|
+
const whatsNextResult = await client.callTool('whats_next', {
|
|
371
|
+
context: 'Testing beads instructions',
|
|
372
|
+
user_input: 'What should I do?',
|
|
373
|
+
conversation_summary: 'Started development with beads',
|
|
374
|
+
recent_messages: [],
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const response = assertToolSuccess(whatsNextResult) as WhatsNextResult;
|
|
378
|
+
const instructions = response.instructions;
|
|
379
|
+
|
|
380
|
+
// VALIDATE: Clear instruction about bd CLI exclusivity
|
|
381
|
+
expect(instructions).toContain('Use ONLY bd CLI tool');
|
|
382
|
+
expect(instructions).toContain(
|
|
383
|
+
'do not use your own task management tools'
|
|
384
|
+
);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// =========================================================================
|
|
389
|
+
// 3. WITH VS WITHOUT BEADS COMPARISON
|
|
390
|
+
// =========================================================================
|
|
391
|
+
|
|
392
|
+
describe('3. Plan File and Instructions: With vs Without Beads', () => {
|
|
393
|
+
it('should produce DIFFERENT plan files with and without beads', async () => {
|
|
394
|
+
// Create two independent scenarios
|
|
395
|
+
let cleanupWith: () => Promise<void>;
|
|
396
|
+
let cleanupWithout: () => Promise<void>;
|
|
397
|
+
|
|
398
|
+
// WITH BEADS
|
|
399
|
+
process.env.TASK_BACKEND = 'beads';
|
|
400
|
+
setupBeadsCliMock();
|
|
401
|
+
const scenarioWith = await createSuiteIsolatedE2EScenario({
|
|
402
|
+
suiteName: 'beads-comparison-with',
|
|
403
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
404
|
+
});
|
|
405
|
+
const clientWith = scenarioWith.client;
|
|
406
|
+
cleanupWith = scenarioWith.cleanup;
|
|
407
|
+
|
|
408
|
+
const resultWith = await clientWith.callTool('start_development', {
|
|
409
|
+
workflow: 'epcc',
|
|
410
|
+
commit_behaviour: 'none',
|
|
411
|
+
});
|
|
412
|
+
const responseWith = assertToolSuccess(
|
|
413
|
+
resultWith
|
|
414
|
+
) as StartDevelopmentResult;
|
|
415
|
+
const planContentWith = await fs.readFile(
|
|
416
|
+
responseWith.plan_file_path,
|
|
417
|
+
'utf-8'
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
await cleanupWith();
|
|
421
|
+
delete process.env.TASK_BACKEND;
|
|
422
|
+
|
|
423
|
+
// WITHOUT BEADS
|
|
424
|
+
const scenarioWithout = await createSuiteIsolatedE2EScenario({
|
|
425
|
+
suiteName: 'beads-comparison-without',
|
|
426
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
427
|
+
});
|
|
428
|
+
const clientWithout = scenarioWithout.client;
|
|
429
|
+
cleanupWithout = scenarioWithout.cleanup;
|
|
430
|
+
|
|
431
|
+
const resultWithout = await clientWithout.callTool('start_development', {
|
|
432
|
+
workflow: 'epcc',
|
|
433
|
+
commit_behaviour: 'none',
|
|
434
|
+
});
|
|
435
|
+
const responseWithout = assertToolSuccess(
|
|
436
|
+
resultWithout
|
|
437
|
+
) as StartDevelopmentResult;
|
|
438
|
+
const planContentWithout = await fs.readFile(
|
|
439
|
+
responseWithout.plan_file_path,
|
|
440
|
+
'utf-8'
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
await cleanupWithout();
|
|
444
|
+
|
|
445
|
+
// VALIDATE: WITH beads has beads markers
|
|
446
|
+
expect(planContentWith).toContain('<!-- beads-phase-id:');
|
|
447
|
+
|
|
448
|
+
// VALIDATE: WITHOUT beads does NOT have beads markers
|
|
449
|
+
expect(planContentWithout).not.toContain('<!-- beads-phase-id:');
|
|
450
|
+
|
|
451
|
+
// VALIDATE: WITHOUT beads uses checkbox format
|
|
452
|
+
expect(planContentWithout).toContain('- [ ]');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('should generate DIFFERENT instructions with and without beads', async () => {
|
|
456
|
+
// WITH BEADS
|
|
457
|
+
process.env.TASK_BACKEND = 'beads';
|
|
458
|
+
setupBeadsCliMock();
|
|
459
|
+
const scenarioWith = await createSuiteIsolatedE2EScenario({
|
|
460
|
+
suiteName: 'beads-instructions-comparison-with',
|
|
461
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
462
|
+
});
|
|
463
|
+
const clientWith = scenarioWith.client;
|
|
464
|
+
|
|
465
|
+
await clientWith.callTool('start_development', {
|
|
466
|
+
workflow: 'epcc',
|
|
467
|
+
commit_behaviour: 'none',
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const whatsNextWith = await clientWith.callTool('whats_next', {
|
|
471
|
+
context: 'Testing',
|
|
472
|
+
user_input: 'What should I do?',
|
|
473
|
+
conversation_summary: 'Started',
|
|
474
|
+
recent_messages: [],
|
|
475
|
+
});
|
|
476
|
+
const responseWith = assertToolSuccess(whatsNextWith) as WhatsNextResult;
|
|
477
|
+
const instructionsWithBeads = responseWith.instructions;
|
|
478
|
+
|
|
479
|
+
await scenarioWith.cleanup();
|
|
480
|
+
delete process.env.TASK_BACKEND;
|
|
481
|
+
|
|
482
|
+
// WITHOUT BEADS
|
|
483
|
+
const scenarioWithout = await createSuiteIsolatedE2EScenario({
|
|
484
|
+
suiteName: 'beads-instructions-comparison-without',
|
|
485
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
486
|
+
});
|
|
487
|
+
const clientWithout = scenarioWithout.client;
|
|
488
|
+
|
|
489
|
+
await clientWithout.callTool('start_development', {
|
|
490
|
+
workflow: 'epcc',
|
|
491
|
+
commit_behaviour: 'none',
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const whatsNextWithout = await clientWithout.callTool('whats_next', {
|
|
495
|
+
context: 'Testing',
|
|
496
|
+
user_input: 'What should I do?',
|
|
497
|
+
conversation_summary: 'Started',
|
|
498
|
+
recent_messages: [],
|
|
499
|
+
});
|
|
500
|
+
const responseWithout = assertToolSuccess(
|
|
501
|
+
whatsNextWithout
|
|
502
|
+
) as WhatsNextResult;
|
|
503
|
+
const instructionsWithout = responseWithout.instructions;
|
|
504
|
+
|
|
505
|
+
await scenarioWithout.cleanup();
|
|
506
|
+
|
|
507
|
+
// VALIDATE: WITH beads mentions bd CLI
|
|
508
|
+
expect(instructionsWithBeads.toLowerCase()).toContain('bd');
|
|
509
|
+
expect(instructionsWithBeads).toContain('BD CLI Task Management');
|
|
510
|
+
|
|
511
|
+
// VALIDATE: WITHOUT beads does NOT mention bd CLI
|
|
512
|
+
expect(instructionsWithout.toLowerCase()).not.toContain('bd cli');
|
|
513
|
+
expect(instructionsWithout).not.toContain('BD CLI Task Management');
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('should maintain identical response contracts regardless of beads', async () => {
|
|
517
|
+
// WITH BEADS
|
|
518
|
+
process.env.TASK_BACKEND = 'beads';
|
|
519
|
+
setupBeadsCliMock();
|
|
520
|
+
const scenarioWith = await createSuiteIsolatedE2EScenario({
|
|
521
|
+
suiteName: 'beads-contract-with',
|
|
522
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
523
|
+
});
|
|
524
|
+
const clientWith = scenarioWith.client;
|
|
525
|
+
|
|
526
|
+
const resultWith = await clientWith.callTool('start_development', {
|
|
527
|
+
workflow: 'epcc',
|
|
528
|
+
commit_behaviour: 'none',
|
|
529
|
+
});
|
|
530
|
+
const responseWith = assertToolSuccess(resultWith);
|
|
531
|
+
|
|
532
|
+
await scenarioWith.cleanup();
|
|
533
|
+
delete process.env.TASK_BACKEND;
|
|
534
|
+
|
|
535
|
+
// WITHOUT BEADS
|
|
536
|
+
const scenarioWithout = await createSuiteIsolatedE2EScenario({
|
|
537
|
+
suiteName: 'beads-contract-without',
|
|
538
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
539
|
+
});
|
|
540
|
+
const clientWithout = scenarioWithout.client;
|
|
541
|
+
|
|
542
|
+
const resultWithout = await clientWithout.callTool('start_development', {
|
|
543
|
+
workflow: 'epcc',
|
|
544
|
+
commit_behaviour: 'none',
|
|
545
|
+
});
|
|
546
|
+
const responseWithout = assertToolSuccess(resultWithout);
|
|
547
|
+
|
|
548
|
+
await scenarioWithout.cleanup();
|
|
549
|
+
|
|
550
|
+
// VALIDATE: Both responses have identical properties
|
|
551
|
+
expect(responseWith).toHaveProperty('conversation_id');
|
|
552
|
+
expect(responseWith).toHaveProperty('phase');
|
|
553
|
+
expect(responseWith).toHaveProperty('plan_file_path');
|
|
554
|
+
expect(responseWith).toHaveProperty('instructions');
|
|
555
|
+
|
|
556
|
+
expect(responseWithout).toHaveProperty('conversation_id');
|
|
557
|
+
expect(responseWithout).toHaveProperty('phase');
|
|
558
|
+
expect(responseWithout).toHaveProperty('plan_file_path');
|
|
559
|
+
expect(responseWithout).toHaveProperty('instructions');
|
|
560
|
+
|
|
561
|
+
// VALIDATE: Response structure identical
|
|
562
|
+
expect(Object.keys(responseWith).sort()).toEqual(
|
|
563
|
+
Object.keys(responseWithout).sort()
|
|
564
|
+
);
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// =========================================================================
|
|
569
|
+
// 4. PLAN FILE TASK ID INTEGRATION
|
|
570
|
+
// =========================================================================
|
|
571
|
+
|
|
572
|
+
describe('4. Plan File Task ID Integration', () => {
|
|
573
|
+
let client: DirectServerInterface;
|
|
574
|
+
let cleanup: () => Promise<void>;
|
|
575
|
+
|
|
576
|
+
beforeEach(async () => {
|
|
577
|
+
process.env.TASK_BACKEND = 'beads';
|
|
578
|
+
setupBeadsCliMock();
|
|
579
|
+
|
|
580
|
+
const scenario = await createSuiteIsolatedE2EScenario({
|
|
581
|
+
suiteName: 'beads-task-ids',
|
|
582
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
583
|
+
});
|
|
584
|
+
client = scenario.client;
|
|
585
|
+
cleanup = scenario.cleanup;
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
afterEach(async () => {
|
|
589
|
+
if (cleanup) {
|
|
590
|
+
await cleanup();
|
|
591
|
+
}
|
|
592
|
+
delete process.env.TASK_BACKEND;
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it('should have beads phase IDs after task creation by plugin hooks', async () => {
|
|
596
|
+
// Start development
|
|
597
|
+
const result = await client.callTool('start_development', {
|
|
598
|
+
workflow: 'epcc',
|
|
599
|
+
commit_behaviour: 'none',
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
603
|
+
const planFilePath = response.plan_file_path;
|
|
604
|
+
const planContent = await fs.readFile(planFilePath, 'utf-8');
|
|
605
|
+
|
|
606
|
+
// VALIDATE: Plan has beads markers (IDs updated by afterStartDevelopment hook)
|
|
607
|
+
expect(planContent).toContain('<!-- beads-phase-id:');
|
|
608
|
+
|
|
609
|
+
// VALIDATE: Has proper format with TBD or actual task IDs
|
|
610
|
+
expect(planContent).toMatch(
|
|
611
|
+
/## \w+\n<!-- beads-phase-id:\s*(TBD|[a-zA-Z0-9\-.]+)\s*-->\n### Tasks/
|
|
612
|
+
);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it('should have valid beads-phase-id format (not TBD after plugin execution)', async () => {
|
|
616
|
+
// Start development
|
|
617
|
+
const result = await client.callTool('start_development', {
|
|
618
|
+
workflow: 'epcc',
|
|
619
|
+
commit_behaviour: 'none',
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
623
|
+
const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
|
|
624
|
+
|
|
625
|
+
// VALIDATE: Format must match - either TBD or actual task IDs with dots
|
|
626
|
+
const validFormats = planContent.match(
|
|
627
|
+
/<!-- beads-phase-id:\s*(TBD|[a-zA-Z0-9\-.]+)\s*-->/g
|
|
628
|
+
);
|
|
629
|
+
expect(validFormats).not.toBeNull();
|
|
630
|
+
expect((validFormats || []).length).toBeGreaterThanOrEqual(1);
|
|
631
|
+
|
|
632
|
+
// VALIDATE: No malformed placeholders
|
|
633
|
+
expect(planContent).not.toMatch(/<!-- beads-phase-id:\s*-->/);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it('should preserve plan file structure when updating task IDs', async () => {
|
|
637
|
+
// Start development
|
|
638
|
+
const result = await client.callTool('start_development', {
|
|
639
|
+
workflow: 'epcc',
|
|
640
|
+
commit_behaviour: 'none',
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
644
|
+
const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
|
|
645
|
+
|
|
646
|
+
// VALIDATE: Plan structure intact
|
|
647
|
+
expect(planContent).toContain('# Development Plan:');
|
|
648
|
+
expect(planContent).toContain('## Goal');
|
|
649
|
+
expect(planContent).toContain('## Explore');
|
|
650
|
+
expect(planContent).toContain('## Plan');
|
|
651
|
+
expect(planContent).toContain('## Code');
|
|
652
|
+
expect(planContent).toContain('## Commit');
|
|
653
|
+
expect(planContent).toContain('## Key Decisions');
|
|
654
|
+
expect(planContent).toContain('## Notes');
|
|
655
|
+
|
|
656
|
+
// VALIDATE: Markdown is valid
|
|
657
|
+
expect(planContent).toMatch(/^# Development Plan:/m);
|
|
658
|
+
expect(planContent).toMatch(/^## /m);
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
// =========================================================================
|
|
663
|
+
// 5. ERROR HANDLING AND DEGRADATION
|
|
664
|
+
// =========================================================================
|
|
665
|
+
|
|
666
|
+
describe('5. Beads Error Handling and Graceful Degradation', () => {
|
|
667
|
+
let client: DirectServerInterface;
|
|
668
|
+
let cleanup: () => Promise<void>;
|
|
669
|
+
|
|
670
|
+
beforeEach(async () => {
|
|
671
|
+
process.env.TASK_BACKEND = 'beads';
|
|
672
|
+
setupBeadsCliMock();
|
|
673
|
+
|
|
674
|
+
const scenario = await createSuiteIsolatedE2EScenario({
|
|
675
|
+
suiteName: 'beads-error-handling',
|
|
676
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
677
|
+
});
|
|
678
|
+
client = scenario.client;
|
|
679
|
+
cleanup = scenario.cleanup;
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
afterEach(async () => {
|
|
683
|
+
if (cleanup) {
|
|
684
|
+
await cleanup();
|
|
685
|
+
}
|
|
686
|
+
delete process.env.TASK_BACKEND;
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it('should create valid plan file even when beads unavailable', async () => {
|
|
690
|
+
// Start development with beads enabled
|
|
691
|
+
const result = await client.callTool('start_development', {
|
|
692
|
+
workflow: 'epcc',
|
|
693
|
+
commit_behaviour: 'none',
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
// Should NOT return error - graceful degradation
|
|
697
|
+
expect(result).not.toHaveProperty('error');
|
|
698
|
+
|
|
699
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
700
|
+
|
|
701
|
+
// VALIDATE: Plan file created successfully
|
|
702
|
+
expect(response.plan_file_path).toBeDefined();
|
|
703
|
+
expect(response.plan_file_path).toBeTruthy();
|
|
704
|
+
|
|
705
|
+
// VALIDATE: Plan file exists and is readable
|
|
706
|
+
const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
|
|
707
|
+
expect(planContent).toBeTruthy();
|
|
708
|
+
|
|
709
|
+
// VALIDATE: Plan has beads markers even if tasks weren't created
|
|
710
|
+
expect(planContent).toContain('<!-- beads-phase-id:');
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it('should return success response even if beads operations fail', async () => {
|
|
714
|
+
// Start development
|
|
715
|
+
const result = await client.callTool('start_development', {
|
|
716
|
+
workflow: 'epcc',
|
|
717
|
+
commit_behaviour: 'none',
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// VALIDATE: Response is successful (no error field)
|
|
721
|
+
expect(result).not.toHaveProperty('error');
|
|
722
|
+
|
|
723
|
+
// VALIDATE: Has all required response fields
|
|
724
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
725
|
+
expect(response).toHaveProperty('conversation_id');
|
|
726
|
+
expect(response).toHaveProperty('phase');
|
|
727
|
+
expect(response).toHaveProperty('plan_file_path');
|
|
728
|
+
expect(response).toHaveProperty('instructions');
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
// =========================================================================
|
|
733
|
+
// 6. PLUGIN HOOK INTEGRATION
|
|
734
|
+
// =========================================================================
|
|
735
|
+
|
|
736
|
+
describe('6. Plugin Hook Integration', () => {
|
|
737
|
+
let client: DirectServerInterface;
|
|
738
|
+
let cleanup: () => Promise<void>;
|
|
739
|
+
|
|
740
|
+
beforeEach(async () => {
|
|
741
|
+
process.env.TASK_BACKEND = 'beads';
|
|
742
|
+
setupBeadsCliMock();
|
|
743
|
+
|
|
744
|
+
const scenario = await createSuiteIsolatedE2EScenario({
|
|
745
|
+
suiteName: 'beads-plugin-hooks',
|
|
746
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
747
|
+
});
|
|
748
|
+
client = scenario.client;
|
|
749
|
+
cleanup = scenario.cleanup;
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
afterEach(async () => {
|
|
753
|
+
if (cleanup) {
|
|
754
|
+
await cleanup();
|
|
755
|
+
}
|
|
756
|
+
delete process.env.TASK_BACKEND;
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it('should execute plugin hooks during start_development', async () => {
|
|
760
|
+
// Start development - this should execute afterStartDevelopment hook
|
|
761
|
+
const result = await client.callTool('start_development', {
|
|
762
|
+
workflow: 'epcc',
|
|
763
|
+
commit_behaviour: 'none',
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
767
|
+
|
|
768
|
+
// VALIDATE: Plan file was created by plugin
|
|
769
|
+
const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
|
|
770
|
+
expect(planContent).toContain('<!-- beads-phase-id:');
|
|
771
|
+
|
|
772
|
+
// VALIDATE: Response indicates successful hook execution
|
|
773
|
+
expect(response.plan_file_path).toBeTruthy();
|
|
774
|
+
expect(response.instructions).toBeTruthy();
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
it('should coordinate afterStartDevelopment and afterPlanFileCreated hooks', async () => {
|
|
778
|
+
// Start development
|
|
779
|
+
const result = await client.callTool('start_development', {
|
|
780
|
+
workflow: 'epcc',
|
|
781
|
+
commit_behaviour: 'none',
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
785
|
+
const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
|
|
786
|
+
|
|
787
|
+
// VALIDATE: Plan file has beads structure (created by hooks)
|
|
788
|
+
expect(planContent).toContain('<!-- beads-phase-id:');
|
|
789
|
+
|
|
790
|
+
// VALIDATE: Plan has proper content from both hooks
|
|
791
|
+
expect(planContent).toContain('## Explore');
|
|
792
|
+
expect(planContent).toContain('## Plan');
|
|
793
|
+
expect(planContent).toContain('## Code');
|
|
794
|
+
expect(planContent).toContain('## Commit');
|
|
795
|
+
|
|
796
|
+
// VALIDATE: Each phase has a beads marker
|
|
797
|
+
const phaseMatches = planContent.match(/## \w+\n<!-- beads-phase-id:/g);
|
|
798
|
+
expect(phaseMatches).not.toBeNull();
|
|
799
|
+
expect((phaseMatches || []).length).toBeGreaterThanOrEqual(4);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it('should maintain system in consistent state after hook execution', async () => {
|
|
803
|
+
// Start development
|
|
804
|
+
const startResult = await client.callTool('start_development', {
|
|
805
|
+
workflow: 'epcc',
|
|
806
|
+
commit_behaviour: 'none',
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
const startResponse = assertToolSuccess(
|
|
810
|
+
startResult
|
|
811
|
+
) as StartDevelopmentResult;
|
|
812
|
+
|
|
813
|
+
// Get current state
|
|
814
|
+
const whatsNextResult = await client.callTool('whats_next', {
|
|
815
|
+
context: 'Testing hook consistency',
|
|
816
|
+
user_input: 'What should I do?',
|
|
817
|
+
conversation_summary: 'Just started development',
|
|
818
|
+
recent_messages: [],
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
const whatsNextResponse = assertToolSuccess(
|
|
822
|
+
whatsNextResult
|
|
823
|
+
) as WhatsNextResult;
|
|
824
|
+
|
|
825
|
+
// VALIDATE: State is consistent
|
|
826
|
+
expect(whatsNextResponse.phase).toBe('explore');
|
|
827
|
+
expect(whatsNextResponse.plan_file_path).toBe(
|
|
828
|
+
startResponse.plan_file_path
|
|
829
|
+
);
|
|
830
|
+
expect(whatsNextResponse.conversation_id).toBe(
|
|
831
|
+
startResponse.conversation_id
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
// VALIDATE: Plan file is still valid
|
|
835
|
+
const planContent = await fs.readFile(
|
|
836
|
+
whatsNextResponse.plan_file_path,
|
|
837
|
+
'utf-8'
|
|
838
|
+
);
|
|
839
|
+
expect(planContent).toContain('<!-- beads-phase-id:');
|
|
840
|
+
});
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
// =========================================================================
|
|
844
|
+
// 7. BEADS ACTIVATION AND ENVIRONMENT CHECK
|
|
845
|
+
// =========================================================================
|
|
846
|
+
|
|
847
|
+
describe('7. Beads Environment Activation', () => {
|
|
848
|
+
it('should apply beads when TASK_BACKEND=beads is set', async () => {
|
|
849
|
+
process.env.TASK_BACKEND = 'beads';
|
|
850
|
+
setupBeadsCliMock();
|
|
851
|
+
|
|
852
|
+
const scenario = await createSuiteIsolatedE2EScenario({
|
|
853
|
+
suiteName: 'beads-activation-with',
|
|
854
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
const result = await scenario.client.callTool('start_development', {
|
|
858
|
+
workflow: 'epcc',
|
|
859
|
+
commit_behaviour: 'none',
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
863
|
+
const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
|
|
864
|
+
|
|
865
|
+
await scenario.cleanup();
|
|
866
|
+
delete process.env.TASK_BACKEND;
|
|
867
|
+
|
|
868
|
+
// VALIDATE: Beads features enabled
|
|
869
|
+
expect(planContent).toContain('<!-- beads-phase-id:');
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
it('should NOT apply beads when TASK_BACKEND is not set', async () => {
|
|
873
|
+
// Ensure env var is NOT set
|
|
874
|
+
delete process.env.TASK_BACKEND;
|
|
875
|
+
|
|
876
|
+
const scenario = await createSuiteIsolatedE2EScenario({
|
|
877
|
+
suiteName: 'beads-activation-without',
|
|
878
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
const result = await scenario.client.callTool('start_development', {
|
|
882
|
+
workflow: 'epcc',
|
|
883
|
+
commit_behaviour: 'none',
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
887
|
+
const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
|
|
888
|
+
|
|
889
|
+
await scenario.cleanup();
|
|
890
|
+
|
|
891
|
+
// VALIDATE: Beads features NOT enabled
|
|
892
|
+
expect(planContent).not.toContain('<!-- beads-phase-id:');
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it('should NOT apply beads when TASK_BACKEND has different value', async () => {
|
|
896
|
+
process.env.TASK_BACKEND = 'other-backend';
|
|
897
|
+
|
|
898
|
+
const scenario = await createSuiteIsolatedE2EScenario({
|
|
899
|
+
suiteName: 'beads-activation-other',
|
|
900
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
const result = await scenario.client.callTool('start_development', {
|
|
904
|
+
workflow: 'epcc',
|
|
905
|
+
commit_behaviour: 'none',
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
909
|
+
const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
|
|
910
|
+
|
|
911
|
+
await scenario.cleanup();
|
|
912
|
+
delete process.env.TASK_BACKEND;
|
|
913
|
+
|
|
914
|
+
// VALIDATE: Beads features NOT enabled
|
|
915
|
+
expect(planContent).not.toContain('<!-- beads-phase-id:');
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
// =========================================================================
|
|
920
|
+
// 8. CONTENT VALIDATION AND SEMANTIC CHECKS
|
|
921
|
+
// =========================================================================
|
|
922
|
+
|
|
923
|
+
describe('8. Beads Content Validation and Semantic Checks', () => {
|
|
924
|
+
let client: DirectServerInterface;
|
|
925
|
+
let cleanup: () => Promise<void>;
|
|
926
|
+
|
|
927
|
+
beforeEach(async () => {
|
|
928
|
+
process.env.TASK_BACKEND = 'beads';
|
|
929
|
+
setupBeadsCliMock();
|
|
930
|
+
|
|
931
|
+
const scenario = await createSuiteIsolatedE2EScenario({
|
|
932
|
+
suiteName: 'beads-content-validation',
|
|
933
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
934
|
+
});
|
|
935
|
+
client = scenario.client;
|
|
936
|
+
cleanup = scenario.cleanup;
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
afterEach(async () => {
|
|
940
|
+
if (cleanup) {
|
|
941
|
+
await cleanup();
|
|
942
|
+
}
|
|
943
|
+
delete process.env.TASK_BACKEND;
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
it('should generate substantive beads instructions with actual guidance', async () => {
|
|
947
|
+
// Start development
|
|
948
|
+
const startResult = await client.callTool('start_development', {
|
|
949
|
+
workflow: 'epcc',
|
|
950
|
+
commit_behaviour: 'none',
|
|
951
|
+
});
|
|
952
|
+
assertToolSuccess(startResult);
|
|
953
|
+
|
|
954
|
+
// Get instructions
|
|
955
|
+
const whatsNextResult = await client.callTool('whats_next', {
|
|
956
|
+
context: 'Testing instruction quality',
|
|
957
|
+
user_input: 'What should I do?',
|
|
958
|
+
conversation_summary: 'Started development',
|
|
959
|
+
recent_messages: [],
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
const response = assertToolSuccess(whatsNextResult) as WhatsNextResult;
|
|
963
|
+
const instructions = response.instructions;
|
|
964
|
+
|
|
965
|
+
// VALIDATE: Instructions are substantive (not just placeholders)
|
|
966
|
+
expect(instructions.length).toBeGreaterThan(MIN_INSTRUCTION_LENGTH);
|
|
967
|
+
|
|
968
|
+
// VALIDATE: Instructions contain beads-specific guidance
|
|
969
|
+
validateBeadsInstructions(instructions);
|
|
970
|
+
|
|
971
|
+
// VALIDATE: Instructions mention specific phases
|
|
972
|
+
expect(instructions).toMatch(/explore|plan|code|commit/i);
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
it('should create valid markdown plan file structure', async () => {
|
|
976
|
+
// Start development
|
|
977
|
+
const result = await client.callTool('start_development', {
|
|
978
|
+
workflow: 'epcc',
|
|
979
|
+
commit_behaviour: 'none',
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
983
|
+
const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
|
|
984
|
+
|
|
985
|
+
// VALIDATE: Markdown structure
|
|
986
|
+
expect(planContent).toMatch(/^# /m); // Title
|
|
987
|
+
expect(planContent).toMatch(/^## /m); // Sections
|
|
988
|
+
expect(planContent).toMatch(/^### /m); // Subsections
|
|
989
|
+
|
|
990
|
+
// VALIDATE: No malformed headers
|
|
991
|
+
expect(planContent).not.toMatch(/^#$/m); // Empty header
|
|
992
|
+
expect(planContent).not.toMatch(/^## $/m); // Empty section
|
|
993
|
+
|
|
994
|
+
// VALIDATE: Beads markers are valid comments
|
|
995
|
+
expect(planContent).toMatch(/<!-- beads-phase-id:/);
|
|
996
|
+
expect(planContent).not.toMatch(/<!-- -->/); // Empty comment
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
it('should include beads CLI guidance in plan file', async () => {
|
|
1000
|
+
// Start development
|
|
1001
|
+
const result = await client.callTool('start_development', {
|
|
1002
|
+
workflow: 'epcc',
|
|
1003
|
+
commit_behaviour: 'none',
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
1007
|
+
const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
|
|
1008
|
+
|
|
1009
|
+
// VALIDATE: Plan mentions beads CLI
|
|
1010
|
+
expect(planContent).toContain('bd');
|
|
1011
|
+
expect(planContent).toContain('Tasks managed via');
|
|
1012
|
+
expect(planContent).toContain('beads CLI');
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
// =========================================================================
|
|
1017
|
+
// 9. TASK ID EXTRACTION AND VALIDATION
|
|
1018
|
+
// =========================================================================
|
|
1019
|
+
|
|
1020
|
+
describe('9. Task ID Extraction and Validation', () => {
|
|
1021
|
+
let client: DirectServerInterface;
|
|
1022
|
+
let cleanup: () => Promise<void>;
|
|
1023
|
+
|
|
1024
|
+
beforeEach(async () => {
|
|
1025
|
+
process.env.TASK_BACKEND = 'beads';
|
|
1026
|
+
setupBeadsCliMock();
|
|
1027
|
+
|
|
1028
|
+
const scenario = await createSuiteIsolatedE2EScenario({
|
|
1029
|
+
suiteName: 'beads-task-id-extraction',
|
|
1030
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
1031
|
+
});
|
|
1032
|
+
client = scenario.client;
|
|
1033
|
+
cleanup = scenario.cleanup;
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
afterEach(async () => {
|
|
1037
|
+
if (cleanup) {
|
|
1038
|
+
await cleanup();
|
|
1039
|
+
}
|
|
1040
|
+
delete process.env.TASK_BACKEND;
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
it('should extract beads phase IDs from plan file', async () => {
|
|
1044
|
+
// Start development
|
|
1045
|
+
const result = await client.callTool('start_development', {
|
|
1046
|
+
workflow: 'epcc',
|
|
1047
|
+
commit_behaviour: 'none',
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
1051
|
+
const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
|
|
1052
|
+
|
|
1053
|
+
// VALIDATE: Extract IDs and verify format
|
|
1054
|
+
const extractedIds = extractBeadsPhaseIds(planContent);
|
|
1055
|
+
|
|
1056
|
+
// VALIDATE: Should have extracted some IDs
|
|
1057
|
+
expect(extractedIds).toBeDefined();
|
|
1058
|
+
expect(Array.isArray(extractedIds)).toBe(true);
|
|
1059
|
+
|
|
1060
|
+
// VALIDATE: Each ID should be a non-empty string
|
|
1061
|
+
for (const id of extractedIds) {
|
|
1062
|
+
expect(typeof id).toBe('string');
|
|
1063
|
+
expect(id.length).toBeGreaterThan(0);
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
it('should validate beads phase ID format', async () => {
|
|
1068
|
+
// Start development
|
|
1069
|
+
const result = await client.callTool('start_development', {
|
|
1070
|
+
workflow: 'epcc',
|
|
1071
|
+
commit_behaviour: 'none',
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
1075
|
+
const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
|
|
1076
|
+
|
|
1077
|
+
// VALIDATE: IDs must match pattern (alphanumeric, hyphens, dots)
|
|
1078
|
+
const allMatches = planContent.match(
|
|
1079
|
+
/<!-- beads-phase-id:\s*([a-zA-Z0-9\-.]+)\s*-->/g
|
|
1080
|
+
);
|
|
1081
|
+
expect(allMatches).not.toBeNull();
|
|
1082
|
+
expect((allMatches || []).length).toBeGreaterThan(0);
|
|
1083
|
+
|
|
1084
|
+
// VALIDATE: Each match is properly formatted
|
|
1085
|
+
for (const match of allMatches || []) {
|
|
1086
|
+
expect(match).toMatch(/^<!-- beads-phase-id:/);
|
|
1087
|
+
expect(match).toMatch(/-->$/);
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
it('should not have empty beads-phase-id placeholders', async () => {
|
|
1092
|
+
// Start development
|
|
1093
|
+
const result = await client.callTool('start_development', {
|
|
1094
|
+
workflow: 'epcc',
|
|
1095
|
+
commit_behaviour: 'none',
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
1099
|
+
const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
|
|
1100
|
+
|
|
1101
|
+
// VALIDATE: No malformed empty placeholders
|
|
1102
|
+
expect(planContent).not.toMatch(/<!-- beads-phase-id:\s*-->/);
|
|
1103
|
+
expect(planContent).not.toMatch(/<!-- beads-phase-id: -->/);
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
it('should replace TBD placeholders with actual task IDs or keep TBD', async () => {
|
|
1107
|
+
// Start development
|
|
1108
|
+
const result = await client.callTool('start_development', {
|
|
1109
|
+
workflow: 'epcc',
|
|
1110
|
+
commit_behaviour: 'none',
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
1114
|
+
const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
|
|
1115
|
+
|
|
1116
|
+
// VALIDATE: All placeholders are either TBD or actual IDs (not empty)
|
|
1117
|
+
const placeholders = planContent.match(
|
|
1118
|
+
/<!-- beads-phase-id:\s*(TBD|[a-zA-Z0-9\-.]+)\s*-->/g
|
|
1119
|
+
);
|
|
1120
|
+
expect(placeholders).not.toBeNull();
|
|
1121
|
+
|
|
1122
|
+
for (const placeholder of placeholders || []) {
|
|
1123
|
+
// Each must have either TBD or an actual ID
|
|
1124
|
+
expect(placeholder).toMatch(/TBD|[a-zA-Z0-9\-.]+/);
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
// =========================================================================
|
|
1130
|
+
// 10. PHASE TRANSITION AND TASK COMPLETION VALIDATION
|
|
1131
|
+
// =========================================================================
|
|
1132
|
+
|
|
1133
|
+
describe('10. Phase Transition and Task Completion', () => {
|
|
1134
|
+
let client: DirectServerInterface;
|
|
1135
|
+
let cleanup: () => Promise<void>;
|
|
1136
|
+
|
|
1137
|
+
beforeEach(async () => {
|
|
1138
|
+
process.env.TASK_BACKEND = 'beads';
|
|
1139
|
+
setupBeadsCliMock();
|
|
1140
|
+
|
|
1141
|
+
const scenario = await createSuiteIsolatedE2EScenario({
|
|
1142
|
+
suiteName: 'beads-phase-transitions',
|
|
1143
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
1144
|
+
});
|
|
1145
|
+
client = scenario.client;
|
|
1146
|
+
cleanup = scenario.cleanup;
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
afterEach(async () => {
|
|
1150
|
+
if (cleanup) {
|
|
1151
|
+
await cleanup();
|
|
1152
|
+
}
|
|
1153
|
+
delete process.env.TASK_BACKEND;
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
it('should maintain beads markers through phase transitions', async () => {
|
|
1157
|
+
// Start development
|
|
1158
|
+
const startResult = await client.callTool('start_development', {
|
|
1159
|
+
workflow: 'epcc',
|
|
1160
|
+
commit_behaviour: 'none',
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
const startResponse = assertToolSuccess(
|
|
1164
|
+
startResult
|
|
1165
|
+
) as StartDevelopmentResult;
|
|
1166
|
+
let planContent = await fs.readFile(
|
|
1167
|
+
startResponse.plan_file_path,
|
|
1168
|
+
'utf-8'
|
|
1169
|
+
);
|
|
1170
|
+
const initialMarkers = planContent.match(/<!-- beads-phase-id:/g);
|
|
1171
|
+
|
|
1172
|
+
// VALIDATE: Initial markers exist
|
|
1173
|
+
expect(initialMarkers).not.toBeNull();
|
|
1174
|
+
expect((initialMarkers || []).length).toBeGreaterThanOrEqual(
|
|
1175
|
+
MIN_PHASES_WITH_MARKERS
|
|
1176
|
+
);
|
|
1177
|
+
|
|
1178
|
+
// Transition to next phase
|
|
1179
|
+
const transitionResult = await client.callTool('proceed_to_phase', {
|
|
1180
|
+
target_phase: 'plan',
|
|
1181
|
+
reason: 'exploration complete',
|
|
1182
|
+
review_state: 'not-required',
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
expect(transitionResult).not.toHaveProperty('error');
|
|
1186
|
+
|
|
1187
|
+
// VALIDATE: Markers still present after transition
|
|
1188
|
+
planContent = await fs.readFile(startResponse.plan_file_path, 'utf-8');
|
|
1189
|
+
const afterTransitionMarkers = planContent.match(/<!-- beads-phase-id:/g);
|
|
1190
|
+
|
|
1191
|
+
expect(afterTransitionMarkers).not.toBeNull();
|
|
1192
|
+
expect(afterTransitionMarkers).toEqual(initialMarkers);
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
it('should preserve plan file structure across multiple phase transitions', async () => {
|
|
1196
|
+
// Start development
|
|
1197
|
+
const result = await client.callTool('start_development', {
|
|
1198
|
+
workflow: 'epcc',
|
|
1199
|
+
commit_behaviour: 'none',
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
1203
|
+
const initialContent = await fs.readFile(
|
|
1204
|
+
response.plan_file_path,
|
|
1205
|
+
'utf-8'
|
|
1206
|
+
);
|
|
1207
|
+
|
|
1208
|
+
// Verify initial structure
|
|
1209
|
+
expect(initialContent).toContain('## Explore');
|
|
1210
|
+
expect(initialContent).toContain('## Plan');
|
|
1211
|
+
|
|
1212
|
+
// Transition through phases
|
|
1213
|
+
await client.callTool('proceed_to_phase', {
|
|
1214
|
+
target_phase: 'plan',
|
|
1215
|
+
reason: 'exploration complete',
|
|
1216
|
+
review_state: 'not-required',
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
// VALIDATE: Structure preserved
|
|
1220
|
+
const afterFirstTransition = await fs.readFile(
|
|
1221
|
+
response.plan_file_path,
|
|
1222
|
+
'utf-8'
|
|
1223
|
+
);
|
|
1224
|
+
expect(afterFirstTransition).toContain('## Explore');
|
|
1225
|
+
expect(afterFirstTransition).toContain('## Plan');
|
|
1226
|
+
expect(afterFirstTransition).toContain('## Code');
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
it('should keep beads markers consistent with phase structure', async () => {
|
|
1230
|
+
// Start development
|
|
1231
|
+
const result = await client.callTool('start_development', {
|
|
1232
|
+
workflow: 'epcc',
|
|
1233
|
+
commit_behaviour: 'none',
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
1237
|
+
const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
|
|
1238
|
+
|
|
1239
|
+
// VALIDATE: Each phase header has a corresponding beads marker
|
|
1240
|
+
const phasePattern = /## (\w+)\n<!-- beads-phase-id:/g;
|
|
1241
|
+
const phaseMatches: string[] = [];
|
|
1242
|
+
let match: RegExpExecArray | null;
|
|
1243
|
+
while ((match = phasePattern.exec(planContent)) !== null) {
|
|
1244
|
+
if (match[1]) {
|
|
1245
|
+
phaseMatches.push(match[1]);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// VALIDATE: Found multiple phases with markers
|
|
1250
|
+
expect(phaseMatches.length).toBeGreaterThanOrEqual(
|
|
1251
|
+
MIN_PHASES_WITH_MARKERS
|
|
1252
|
+
);
|
|
1253
|
+
|
|
1254
|
+
// VALIDATE: Phases are expected workflow phases
|
|
1255
|
+
const expectedPhases = ['Explore', 'Plan', 'Code', 'Commit'];
|
|
1256
|
+
for (const phase of phaseMatches) {
|
|
1257
|
+
expect(expectedPhases).toContain(phase);
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
// =========================================================================
|
|
1263
|
+
// 11. PLUGIN HOOK EXECUTION AND SIDE EFFECTS
|
|
1264
|
+
// =========================================================================
|
|
1265
|
+
|
|
1266
|
+
describe('11. Plugin Hook Execution and Side Effects', () => {
|
|
1267
|
+
let client: DirectServerInterface;
|
|
1268
|
+
let cleanup: () => Promise<void>;
|
|
1269
|
+
|
|
1270
|
+
beforeEach(async () => {
|
|
1271
|
+
process.env.TASK_BACKEND = 'beads';
|
|
1272
|
+
setupBeadsCliMock();
|
|
1273
|
+
|
|
1274
|
+
const scenario = await createSuiteIsolatedE2EScenario({
|
|
1275
|
+
suiteName: 'beads-hook-execution',
|
|
1276
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
1277
|
+
});
|
|
1278
|
+
client = scenario.client;
|
|
1279
|
+
cleanup = scenario.cleanup;
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
afterEach(async () => {
|
|
1283
|
+
if (cleanup) {
|
|
1284
|
+
await cleanup();
|
|
1285
|
+
}
|
|
1286
|
+
delete process.env.TASK_BACKEND;
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
it('should execute hooks and update plan file with markers', async () => {
|
|
1290
|
+
// Start development - triggers afterStartDevelopment hook
|
|
1291
|
+
const result = await client.callTool('start_development', {
|
|
1292
|
+
workflow: 'epcc',
|
|
1293
|
+
commit_behaviour: 'none',
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
1297
|
+
const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
|
|
1298
|
+
|
|
1299
|
+
// VALIDATE: Hook executed and updated plan file
|
|
1300
|
+
expect(planContent).toContain('<!-- beads-phase-id:');
|
|
1301
|
+
|
|
1302
|
+
// VALIDATE: Plan has beads-specific guidance
|
|
1303
|
+
expect(planContent).toContain('Tasks managed via');
|
|
1304
|
+
expect(planContent).toContain('bd');
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
it('should ensure whats_next respects beads plan markers', async () => {
|
|
1308
|
+
// Start development
|
|
1309
|
+
const startResult = await client.callTool('start_development', {
|
|
1310
|
+
workflow: 'epcc',
|
|
1311
|
+
commit_behaviour: 'none',
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
assertToolSuccess(startResult) as StartDevelopmentResult;
|
|
1315
|
+
|
|
1316
|
+
// Get whats_next guidance
|
|
1317
|
+
const whatsNextResult = await client.callTool('whats_next', {
|
|
1318
|
+
context: 'Continuing development',
|
|
1319
|
+
user_input: 'What should I do?',
|
|
1320
|
+
conversation_summary: 'Started with beads',
|
|
1321
|
+
recent_messages: [],
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
const whatsNextResponse = assertToolSuccess(
|
|
1325
|
+
whatsNextResult
|
|
1326
|
+
) as WhatsNextResult;
|
|
1327
|
+
|
|
1328
|
+
// VALIDATE: Instructions include beads guidance
|
|
1329
|
+
expect(whatsNextResponse.instructions).toContain('bd');
|
|
1330
|
+
expect(whatsNextResponse.instructions).toContain('Use ONLY bd CLI tool');
|
|
1331
|
+
|
|
1332
|
+
// VALIDATE: Plan file still has markers
|
|
1333
|
+
const planContent = await fs.readFile(
|
|
1334
|
+
whatsNextResponse.plan_file_path,
|
|
1335
|
+
'utf-8'
|
|
1336
|
+
);
|
|
1337
|
+
expect(planContent).toContain('<!-- beads-phase-id:');
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
it('should maintain conversation state through plugin hook execution', async () => {
|
|
1341
|
+
// Start development
|
|
1342
|
+
const startResult = await client.callTool('start_development', {
|
|
1343
|
+
workflow: 'epcc',
|
|
1344
|
+
commit_behaviour: 'none',
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
const startResponse = assertToolSuccess(
|
|
1348
|
+
startResult
|
|
1349
|
+
) as StartDevelopmentResult;
|
|
1350
|
+
const conversationId = startResponse.conversation_id;
|
|
1351
|
+
|
|
1352
|
+
// Get whats_next
|
|
1353
|
+
const whatsNextResult = await client.callTool('whats_next', {
|
|
1354
|
+
context: 'Testing conversation preservation',
|
|
1355
|
+
user_input: 'What is the current state?',
|
|
1356
|
+
conversation_summary: 'Started development',
|
|
1357
|
+
recent_messages: [],
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
const whatsNextResponse = assertToolSuccess(
|
|
1361
|
+
whatsNextResult
|
|
1362
|
+
) as WhatsNextResult;
|
|
1363
|
+
|
|
1364
|
+
// VALIDATE: Conversation ID preserved after hook execution
|
|
1365
|
+
expect(whatsNextResponse.conversation_id).toBe(conversationId);
|
|
1366
|
+
|
|
1367
|
+
// VALIDATE: Phase information consistent
|
|
1368
|
+
expect(whatsNextResponse.phase).toBe('explore');
|
|
1369
|
+
});
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
// =========================================================================
|
|
1373
|
+
// 12. MULTI-WORKFLOW BEADS SUPPORT
|
|
1374
|
+
// =========================================================================
|
|
1375
|
+
|
|
1376
|
+
describe('12. Multi-Workflow Beads Support', () => {
|
|
1377
|
+
let cleanup: () => Promise<void>;
|
|
1378
|
+
|
|
1379
|
+
afterEach(async () => {
|
|
1380
|
+
if (cleanup) {
|
|
1381
|
+
await cleanup();
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
it('should apply beads markers to waterfall workflow', async () => {
|
|
1386
|
+
process.env.TASK_BACKEND = 'beads';
|
|
1387
|
+
setupBeadsCliMock();
|
|
1388
|
+
|
|
1389
|
+
const scenario = await createSuiteIsolatedE2EScenario({
|
|
1390
|
+
suiteName: 'beads-waterfall',
|
|
1391
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
1392
|
+
});
|
|
1393
|
+
cleanup = scenario.cleanup;
|
|
1394
|
+
|
|
1395
|
+
const result = await scenario.client.callTool('start_development', {
|
|
1396
|
+
workflow: 'waterfall',
|
|
1397
|
+
commit_behaviour: 'none',
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
1401
|
+
const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
|
|
1402
|
+
|
|
1403
|
+
// VALIDATE: Beads markers present in waterfall
|
|
1404
|
+
expect(planContent).toContain('<!-- beads-phase-id:');
|
|
1405
|
+
|
|
1406
|
+
// VALIDATE: Waterfall phases have markers
|
|
1407
|
+
const phases = ['Requirements', 'Design', 'Implementation'];
|
|
1408
|
+
for (const phase of phases) {
|
|
1409
|
+
expect(planContent).toContain(`## ${phase}`);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
delete process.env.TASK_BACKEND;
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
it('should apply beads markers to tdd workflow', async () => {
|
|
1416
|
+
process.env.TASK_BACKEND = 'beads';
|
|
1417
|
+
setupBeadsCliMock();
|
|
1418
|
+
|
|
1419
|
+
const scenario = await createSuiteIsolatedE2EScenario({
|
|
1420
|
+
suiteName: 'beads-tdd',
|
|
1421
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
1422
|
+
});
|
|
1423
|
+
cleanup = scenario.cleanup;
|
|
1424
|
+
|
|
1425
|
+
const result = await scenario.client.callTool('start_development', {
|
|
1426
|
+
workflow: 'tdd',
|
|
1427
|
+
commit_behaviour: 'none',
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
1431
|
+
const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
|
|
1432
|
+
|
|
1433
|
+
// VALIDATE: Beads markers present in tdd
|
|
1434
|
+
expect(planContent).toContain('<!-- beads-phase-id:');
|
|
1435
|
+
|
|
1436
|
+
// VALIDATE: TDD phases have markers
|
|
1437
|
+
const phaseMatches = planContent.match(/## \w+\n<!-- beads-phase-id:/g);
|
|
1438
|
+
expect(phaseMatches).not.toBeNull();
|
|
1439
|
+
expect((phaseMatches || []).length).toBeGreaterThanOrEqual(3);
|
|
1440
|
+
|
|
1441
|
+
delete process.env.TASK_BACKEND;
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
it('should apply beads markers to bugfix workflow', async () => {
|
|
1445
|
+
process.env.TASK_BACKEND = 'beads';
|
|
1446
|
+
setupBeadsCliMock();
|
|
1447
|
+
|
|
1448
|
+
const scenario = await createSuiteIsolatedE2EScenario({
|
|
1449
|
+
suiteName: 'beads-bugfix',
|
|
1450
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
1451
|
+
});
|
|
1452
|
+
cleanup = scenario.cleanup;
|
|
1453
|
+
|
|
1454
|
+
const result = await scenario.client.callTool('start_development', {
|
|
1455
|
+
workflow: 'bugfix',
|
|
1456
|
+
commit_behaviour: 'none',
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
const response = assertToolSuccess(result) as StartDevelopmentResult;
|
|
1460
|
+
const planContent = await fs.readFile(response.plan_file_path, 'utf-8');
|
|
1461
|
+
|
|
1462
|
+
// VALIDATE: Beads markers present in bugfix
|
|
1463
|
+
expect(planContent).toContain('<!-- beads-phase-id:');
|
|
1464
|
+
|
|
1465
|
+
// VALIDATE: Bugfix phases have markers
|
|
1466
|
+
const phaseMatches = planContent.match(/## \w+\n<!-- beads-phase-id:/g);
|
|
1467
|
+
expect(phaseMatches).not.toBeNull();
|
|
1468
|
+
expect((phaseMatches || []).length).toBeGreaterThanOrEqual(2);
|
|
1469
|
+
|
|
1470
|
+
delete process.env.TASK_BACKEND;
|
|
1471
|
+
});
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
// =========================================================================
|
|
1475
|
+
// 13. BEADS INTEGRATION ROBUSTNESS
|
|
1476
|
+
// =========================================================================
|
|
1477
|
+
|
|
1478
|
+
describe('13. Beads Integration Robustness', () => {
|
|
1479
|
+
let client: DirectServerInterface;
|
|
1480
|
+
let cleanup: () => Promise<void>;
|
|
1481
|
+
|
|
1482
|
+
beforeEach(async () => {
|
|
1483
|
+
process.env.TASK_BACKEND = 'beads';
|
|
1484
|
+
setupBeadsCliMock();
|
|
1485
|
+
|
|
1486
|
+
const scenario = await createSuiteIsolatedE2EScenario({
|
|
1487
|
+
suiteName: 'beads-robustness',
|
|
1488
|
+
tempProjectFactory: createTempProjectWithDefaultStateMachine,
|
|
1489
|
+
});
|
|
1490
|
+
client = scenario.client;
|
|
1491
|
+
cleanup = scenario.cleanup;
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
afterEach(async () => {
|
|
1495
|
+
if (cleanup) {
|
|
1496
|
+
await cleanup();
|
|
1497
|
+
}
|
|
1498
|
+
delete process.env.TASK_BACKEND;
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
it('should not break normal functionality when beads enabled', async () => {
|
|
1502
|
+
// Start development
|
|
1503
|
+
const startResult = await client.callTool('start_development', {
|
|
1504
|
+
workflow: 'epcc',
|
|
1505
|
+
commit_behaviour: 'none',
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
const startResponse = assertToolSuccess(
|
|
1509
|
+
startResult
|
|
1510
|
+
) as StartDevelopmentResult;
|
|
1511
|
+
|
|
1512
|
+
// VALIDATE: All standard response properties present
|
|
1513
|
+
expect(startResponse).toHaveProperty('conversation_id');
|
|
1514
|
+
expect(startResponse).toHaveProperty('phase');
|
|
1515
|
+
expect(startResponse).toHaveProperty('plan_file_path');
|
|
1516
|
+
expect(startResponse).toHaveProperty('instructions');
|
|
1517
|
+
expect(startResponse).toHaveProperty('workflow');
|
|
1518
|
+
|
|
1519
|
+
// VALIDATE: Can transition phases normally
|
|
1520
|
+
const transitionResult = await client.callTool('proceed_to_phase', {
|
|
1521
|
+
target_phase: 'plan',
|
|
1522
|
+
reason: 'exploration complete',
|
|
1523
|
+
review_state: 'not-required',
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
expect(transitionResult).not.toHaveProperty('error');
|
|
1527
|
+
|
|
1528
|
+
// VALIDATE: Can get whats_next normally
|
|
1529
|
+
const whatsNextResult = await client.callTool('whats_next', {
|
|
1530
|
+
context: 'Testing',
|
|
1531
|
+
user_input: 'What now?',
|
|
1532
|
+
conversation_summary: 'Testing beads',
|
|
1533
|
+
recent_messages: [],
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
expect(whatsNextResult).not.toHaveProperty('error');
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
it('should have consistent beads markers across all workflow operations', async () => {
|
|
1540
|
+
// Start
|
|
1541
|
+
const startResult = await client.callTool('start_development', {
|
|
1542
|
+
workflow: 'epcc',
|
|
1543
|
+
commit_behaviour: 'none',
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
const startResponse = assertToolSuccess(
|
|
1547
|
+
startResult
|
|
1548
|
+
) as StartDevelopmentResult;
|
|
1549
|
+
let planContent = await fs.readFile(
|
|
1550
|
+
startResponse.plan_file_path,
|
|
1551
|
+
'utf-8'
|
|
1552
|
+
);
|
|
1553
|
+
const startMarkers = planContent.match(/<!-- beads-phase-id:/g);
|
|
1554
|
+
|
|
1555
|
+
// Get whats_next
|
|
1556
|
+
const whatsNextResult = await client.callTool('whats_next', {
|
|
1557
|
+
context: 'Checking markers',
|
|
1558
|
+
user_input: 'What should I do?',
|
|
1559
|
+
conversation_summary: 'Just started',
|
|
1560
|
+
recent_messages: [],
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
expect(whatsNextResult).not.toHaveProperty('error');
|
|
1564
|
+
|
|
1565
|
+
// Transition
|
|
1566
|
+
await client.callTool('proceed_to_phase', {
|
|
1567
|
+
target_phase: 'plan',
|
|
1568
|
+
reason: 'ready',
|
|
1569
|
+
review_state: 'not-required',
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
planContent = await fs.readFile(startResponse.plan_file_path, 'utf-8');
|
|
1573
|
+
const transitionMarkers = planContent.match(/<!-- beads-phase-id:/g);
|
|
1574
|
+
|
|
1575
|
+
// VALIDATE: Markers consistent
|
|
1576
|
+
expect(startMarkers).toEqual(transitionMarkers);
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
it('should generate consistent beads instructions across operations', async () => {
|
|
1580
|
+
// Start development
|
|
1581
|
+
const startResult = await client.callTool('start_development', {
|
|
1582
|
+
workflow: 'epcc',
|
|
1583
|
+
commit_behaviour: 'none',
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
assertToolSuccess(startResult);
|
|
1587
|
+
|
|
1588
|
+
// Get instructions in whats_next
|
|
1589
|
+
const whatsNextResult = await client.callTool('whats_next', {
|
|
1590
|
+
context: 'Testing instruction consistency',
|
|
1591
|
+
user_input: 'What should I do?',
|
|
1592
|
+
conversation_summary: 'Started',
|
|
1593
|
+
recent_messages: [],
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
const whatsNextResponse = assertToolSuccess(
|
|
1597
|
+
whatsNextResult
|
|
1598
|
+
) as WhatsNextResult;
|
|
1599
|
+
|
|
1600
|
+
// VALIDATE: Instructions have beads content
|
|
1601
|
+
validateBeadsInstructions(whatsNextResponse.instructions);
|
|
1602
|
+
|
|
1603
|
+
// VALIDATE: Instructions contain expected guidance
|
|
1604
|
+
expect(whatsNextResponse.instructions).toContain('bd');
|
|
1605
|
+
expect(whatsNextResponse.instructions).toContain('Task');
|
|
1606
|
+
expect(whatsNextResponse.instructions).toContain('Use ONLY bd CLI tool');
|
|
1607
|
+
});
|
|
1608
|
+
});
|
|
1609
|
+
});
|