edsger 0.41.0 → 0.41.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/.claude/settings.local.json +23 -3
- package/.env.local +12 -0
- package/dist/api/features/__tests__/regression-prevention.test.d.ts +5 -0
- package/dist/api/features/__tests__/regression-prevention.test.js +338 -0
- package/dist/api/features/__tests__/status-updater.integration.test.d.ts +5 -0
- package/dist/api/features/__tests__/status-updater.integration.test.js +497 -0
- package/dist/commands/workflow/pipeline-runner.d.ts +17 -0
- package/dist/commands/workflow/pipeline-runner.js +393 -0
- package/dist/commands/workflow/runner.d.ts +26 -0
- package/dist/commands/workflow/runner.js +119 -0
- package/dist/commands/workflow/workflow-runner.d.ts +26 -0
- package/dist/commands/workflow/workflow-runner.js +119 -0
- package/dist/index.js +0 -0
- package/dist/phases/code-implementation/analyzer-helpers.d.ts +28 -0
- package/dist/phases/code-implementation/analyzer-helpers.js +177 -0
- package/dist/phases/code-implementation/analyzer.d.ts +32 -0
- package/dist/phases/code-implementation/analyzer.js +629 -0
- package/dist/phases/code-implementation/context-fetcher.d.ts +17 -0
- package/dist/phases/code-implementation/context-fetcher.js +86 -0
- package/dist/phases/code-implementation/mcp-server.d.ts +1 -0
- package/dist/phases/code-implementation/mcp-server.js +93 -0
- package/dist/phases/code-implementation/prompts-improvement.d.ts +5 -0
- package/dist/phases/code-implementation/prompts-improvement.js +108 -0
- package/dist/phases/code-implementation-verification/verifier.d.ts +31 -0
- package/dist/phases/code-implementation-verification/verifier.js +196 -0
- package/dist/phases/code-refine/analyzer.d.ts +41 -0
- package/dist/phases/code-refine/analyzer.js +561 -0
- package/dist/phases/code-refine/context-fetcher.d.ts +94 -0
- package/dist/phases/code-refine/context-fetcher.js +423 -0
- package/dist/phases/code-refine-verification/analysis/llm-analyzer.d.ts +22 -0
- package/dist/phases/code-refine-verification/analysis/llm-analyzer.js +134 -0
- package/dist/phases/code-refine-verification/verifier.d.ts +47 -0
- package/dist/phases/code-refine-verification/verifier.js +597 -0
- package/dist/phases/code-review/analyzer.d.ts +29 -0
- package/dist/phases/code-review/analyzer.js +363 -0
- package/dist/phases/code-review/context-fetcher.d.ts +92 -0
- package/dist/phases/code-review/context-fetcher.js +296 -0
- package/dist/phases/feature-analysis/analyzer-helpers.d.ts +10 -0
- package/dist/phases/feature-analysis/analyzer-helpers.js +47 -0
- package/dist/phases/feature-analysis/analyzer.d.ts +11 -0
- package/dist/phases/feature-analysis/analyzer.js +208 -0
- package/dist/phases/feature-analysis/context-fetcher.d.ts +26 -0
- package/dist/phases/feature-analysis/context-fetcher.js +134 -0
- package/dist/phases/feature-analysis/http-fallback.d.ts +20 -0
- package/dist/phases/feature-analysis/http-fallback.js +95 -0
- package/dist/phases/feature-analysis/mcp-server.d.ts +1 -0
- package/dist/phases/feature-analysis/mcp-server.js +144 -0
- package/dist/phases/feature-analysis/prompts-improvement.d.ts +8 -0
- package/dist/phases/feature-analysis/prompts-improvement.js +109 -0
- package/dist/phases/feature-analysis-verification/verifier.d.ts +37 -0
- package/dist/phases/feature-analysis-verification/verifier.js +147 -0
- package/dist/phases/pr-execution/file-assigner.js +20 -12
- package/dist/phases/technical-design/analyzer-helpers.d.ts +25 -0
- package/dist/phases/technical-design/analyzer-helpers.js +39 -0
- package/dist/phases/technical-design/analyzer.d.ts +21 -0
- package/dist/phases/technical-design/analyzer.js +461 -0
- package/dist/phases/technical-design/context-fetcher.d.ts +12 -0
- package/dist/phases/technical-design/context-fetcher.js +39 -0
- package/dist/phases/technical-design/http-fallback.d.ts +17 -0
- package/dist/phases/technical-design/http-fallback.js +151 -0
- package/dist/phases/technical-design/mcp-server.d.ts +1 -0
- package/dist/phases/technical-design/mcp-server.js +157 -0
- package/dist/phases/technical-design/prompts-improvement.d.ts +5 -0
- package/dist/phases/technical-design/prompts-improvement.js +93 -0
- package/dist/phases/technical-design-verification/verifier.d.ts +53 -0
- package/dist/phases/technical-design-verification/verifier.js +170 -0
- package/dist/services/feature-branches.d.ts +77 -0
- package/dist/services/feature-branches.js +205 -0
- package/dist/workflow-runner/config/phase-configs.d.ts +5 -0
- package/dist/workflow-runner/config/phase-configs.js +120 -0
- package/dist/workflow-runner/core/feature-filter.d.ts +16 -0
- package/dist/workflow-runner/core/feature-filter.js +46 -0
- package/dist/workflow-runner/core/index.d.ts +8 -0
- package/dist/workflow-runner/core/index.js +12 -0
- package/dist/workflow-runner/core/pipeline-evaluator.d.ts +24 -0
- package/dist/workflow-runner/core/pipeline-evaluator.js +32 -0
- package/dist/workflow-runner/core/state-manager.d.ts +24 -0
- package/dist/workflow-runner/core/state-manager.js +42 -0
- package/dist/workflow-runner/core/workflow-logger.d.ts +20 -0
- package/dist/workflow-runner/core/workflow-logger.js +65 -0
- package/dist/workflow-runner/executors/phase-executor.d.ts +8 -0
- package/dist/workflow-runner/executors/phase-executor.js +248 -0
- package/dist/workflow-runner/feature-workflow-runner.d.ts +26 -0
- package/dist/workflow-runner/feature-workflow-runner.js +119 -0
- package/dist/workflow-runner/index.d.ts +2 -0
- package/dist/workflow-runner/index.js +2 -0
- package/dist/workflow-runner/pipeline-runner.d.ts +17 -0
- package/dist/workflow-runner/pipeline-runner.js +393 -0
- package/dist/workflow-runner/workflow-processor.d.ts +54 -0
- package/dist/workflow-runner/workflow-processor.js +170 -0
- package/package.json +1 -1
- package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.d.ts +0 -4
- package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.js +0 -133
- package/dist/services/lifecycle-agent/__tests__/transition-rules.test.d.ts +0 -4
- package/dist/services/lifecycle-agent/__tests__/transition-rules.test.js +0 -336
- package/dist/services/lifecycle-agent/index.d.ts +0 -24
- package/dist/services/lifecycle-agent/index.js +0 -25
- package/dist/services/lifecycle-agent/phase-criteria.d.ts +0 -57
- package/dist/services/lifecycle-agent/phase-criteria.js +0 -335
- package/dist/services/lifecycle-agent/transition-rules.d.ts +0 -60
- package/dist/services/lifecycle-agent/transition-rules.js +0 -184
- package/dist/services/lifecycle-agent/types.d.ts +0 -190
- package/dist/services/lifecycle-agent/types.js +0 -12
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for status updater functionality
|
|
3
|
+
* Tests the complete flow including MCP calls and error handling
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import { updateFeatureStatus, updateFeatureStatusForPhase, getStatusForPhase, } from '../status-updater.js';
|
|
8
|
+
// Mock modules
|
|
9
|
+
let mockCallMcpEndpoint;
|
|
10
|
+
let mockGetFeature;
|
|
11
|
+
let originalCallMcpEndpoint;
|
|
12
|
+
let originalGetFeature;
|
|
13
|
+
// Test data
|
|
14
|
+
const mockFeatureInfo = {
|
|
15
|
+
id: 'test-feature-id',
|
|
16
|
+
name: 'Test Feature',
|
|
17
|
+
description: 'Test feature description',
|
|
18
|
+
status: 'backlog',
|
|
19
|
+
product_id: 'test-product-id',
|
|
20
|
+
execution_mode: 'auto',
|
|
21
|
+
created_at: '2025-11-13T00:00:00Z',
|
|
22
|
+
updated_at: '2025-11-13T00:00:00Z',
|
|
23
|
+
};
|
|
24
|
+
// Setup mocks before tests
|
|
25
|
+
const setupMocks = () => {
|
|
26
|
+
// Mock successful MCP call
|
|
27
|
+
mockCallMcpEndpoint = async (method, params) => {
|
|
28
|
+
if (method === 'features/update') {
|
|
29
|
+
return { success: true };
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`Unexpected method: ${method}`);
|
|
32
|
+
};
|
|
33
|
+
// Mock successful getFeature call
|
|
34
|
+
mockGetFeature = async (featureId, verbose) => {
|
|
35
|
+
return { ...mockFeatureInfo, id: featureId };
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
// Restore original functions
|
|
39
|
+
const restoreMocks = () => {
|
|
40
|
+
if (originalCallMcpEndpoint) {
|
|
41
|
+
// Note: In a real test environment, we would need proper module mocking
|
|
42
|
+
// This is a simplified version for demonstration
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
describe('Status Updater Integration Tests', () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
setupMocks();
|
|
48
|
+
});
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
restoreMocks();
|
|
51
|
+
});
|
|
52
|
+
describe('updateFeatureStatus', () => {
|
|
53
|
+
it('should successfully update feature status when progression is valid', async () => {
|
|
54
|
+
// Set current status to backlog
|
|
55
|
+
mockGetFeature = async (featureId) => ({
|
|
56
|
+
...mockFeatureInfo,
|
|
57
|
+
id: featureId,
|
|
58
|
+
status: 'backlog',
|
|
59
|
+
});
|
|
60
|
+
let mcpCalled = false;
|
|
61
|
+
let updateParams = null;
|
|
62
|
+
mockCallMcpEndpoint = async (method, params) => {
|
|
63
|
+
if (method === 'features/update') {
|
|
64
|
+
mcpCalled = true;
|
|
65
|
+
updateParams = params;
|
|
66
|
+
return { success: true };
|
|
67
|
+
}
|
|
68
|
+
throw new Error(`Unexpected method: ${method}`);
|
|
69
|
+
};
|
|
70
|
+
const result = await updateFeatureStatus({
|
|
71
|
+
featureId: 'test-feature-id',
|
|
72
|
+
status: 'ready_for_dev',
|
|
73
|
+
verbose: true,
|
|
74
|
+
});
|
|
75
|
+
assert.strictEqual(result, true, 'Should return true for successful update');
|
|
76
|
+
assert.strictEqual(mcpCalled, true, 'Should call MCP endpoint');
|
|
77
|
+
assert.deepStrictEqual(updateParams, {
|
|
78
|
+
feature_id: 'test-feature-id',
|
|
79
|
+
status: 'ready_for_dev',
|
|
80
|
+
}, 'Should pass correct parameters to MCP');
|
|
81
|
+
});
|
|
82
|
+
it('should prevent status regression', async () => {
|
|
83
|
+
// Set current status to ready_for_dev
|
|
84
|
+
mockGetFeature = async (featureId) => ({
|
|
85
|
+
...mockFeatureInfo,
|
|
86
|
+
id: featureId,
|
|
87
|
+
status: 'ready_for_dev',
|
|
88
|
+
});
|
|
89
|
+
let mcpCalled = false;
|
|
90
|
+
mockCallMcpEndpoint = async () => {
|
|
91
|
+
mcpCalled = true;
|
|
92
|
+
return { success: true };
|
|
93
|
+
};
|
|
94
|
+
const result = await updateFeatureStatus({
|
|
95
|
+
featureId: 'test-feature-id',
|
|
96
|
+
status: 'backlog', // This is a regression
|
|
97
|
+
verbose: true,
|
|
98
|
+
});
|
|
99
|
+
assert.strictEqual(result, false, 'Should return false for regression attempt');
|
|
100
|
+
assert.strictEqual(mcpCalled, false, 'Should not call MCP endpoint for regression');
|
|
101
|
+
});
|
|
102
|
+
it('should allow staying at the same status', async () => {
|
|
103
|
+
// Set current status to feature_analysis
|
|
104
|
+
mockGetFeature = async (featureId) => ({
|
|
105
|
+
...mockFeatureInfo,
|
|
106
|
+
id: featureId,
|
|
107
|
+
status: 'feature_analysis',
|
|
108
|
+
});
|
|
109
|
+
let mcpCalled = false;
|
|
110
|
+
mockCallMcpEndpoint = async () => {
|
|
111
|
+
mcpCalled = true;
|
|
112
|
+
return { success: true };
|
|
113
|
+
};
|
|
114
|
+
const result = await updateFeatureStatus({
|
|
115
|
+
featureId: 'test-feature-id',
|
|
116
|
+
status: 'feature_analysis', // Same status
|
|
117
|
+
verbose: true,
|
|
118
|
+
});
|
|
119
|
+
assert.strictEqual(result, true, 'Should return true when status is already at target');
|
|
120
|
+
assert.strictEqual(mcpCalled, false, 'Should not call MCP when status is unchanged');
|
|
121
|
+
});
|
|
122
|
+
it('should handle getFeature errors gracefully', async () => {
|
|
123
|
+
// Mock getFeature to fail
|
|
124
|
+
mockGetFeature = async () => {
|
|
125
|
+
throw new Error('Feature not found');
|
|
126
|
+
};
|
|
127
|
+
let mcpCalled = false;
|
|
128
|
+
mockCallMcpEndpoint = async () => {
|
|
129
|
+
mcpCalled = true;
|
|
130
|
+
return { success: true };
|
|
131
|
+
};
|
|
132
|
+
const result = await updateFeatureStatus({
|
|
133
|
+
featureId: 'invalid-feature-id',
|
|
134
|
+
status: 'ready_for_dev',
|
|
135
|
+
verbose: true,
|
|
136
|
+
});
|
|
137
|
+
assert.strictEqual(result, false, 'Should return false when current status cannot be retrieved');
|
|
138
|
+
assert.strictEqual(mcpCalled, false, 'Should not call MCP when getFeature fails');
|
|
139
|
+
});
|
|
140
|
+
it('should handle MCP endpoint errors gracefully', async () => {
|
|
141
|
+
// Set current status to backlog
|
|
142
|
+
mockGetFeature = async (featureId) => ({
|
|
143
|
+
...mockFeatureInfo,
|
|
144
|
+
id: featureId,
|
|
145
|
+
status: 'backlog',
|
|
146
|
+
});
|
|
147
|
+
// Mock MCP call to fail
|
|
148
|
+
mockCallMcpEndpoint = async () => {
|
|
149
|
+
throw new Error('MCP server error');
|
|
150
|
+
};
|
|
151
|
+
const result = await updateFeatureStatus({
|
|
152
|
+
featureId: 'test-feature-id',
|
|
153
|
+
status: 'ready_for_dev',
|
|
154
|
+
verbose: true,
|
|
155
|
+
});
|
|
156
|
+
assert.strictEqual(result, false, 'Should return false when MCP call fails');
|
|
157
|
+
});
|
|
158
|
+
it('should handle complex status progressions correctly', async () => {
|
|
159
|
+
// Test progression from middle of workflow to end
|
|
160
|
+
mockGetFeature = async (featureId) => ({
|
|
161
|
+
...mockFeatureInfo,
|
|
162
|
+
id: featureId,
|
|
163
|
+
status: 'code_implementation',
|
|
164
|
+
});
|
|
165
|
+
let mcpCalled = false;
|
|
166
|
+
let updateParams = null;
|
|
167
|
+
mockCallMcpEndpoint = async (method, params) => {
|
|
168
|
+
mcpCalled = true;
|
|
169
|
+
updateParams = params;
|
|
170
|
+
return { success: true };
|
|
171
|
+
};
|
|
172
|
+
const result = await updateFeatureStatus({
|
|
173
|
+
featureId: 'test-feature-id',
|
|
174
|
+
status: 'shipped',
|
|
175
|
+
verbose: false, // Test non-verbose mode
|
|
176
|
+
});
|
|
177
|
+
assert.strictEqual(result, true, 'Should allow forward progression');
|
|
178
|
+
assert.strictEqual(mcpCalled, true, 'Should call MCP endpoint');
|
|
179
|
+
assert.strictEqual(updateParams.status, 'shipped', 'Should pass correct target status');
|
|
180
|
+
});
|
|
181
|
+
it('should handle testing workflow edge cases', async () => {
|
|
182
|
+
// Test progression from testing_failed back to testing_in_progress (retry)
|
|
183
|
+
mockGetFeature = async (featureId) => ({
|
|
184
|
+
...mockFeatureInfo,
|
|
185
|
+
id: featureId,
|
|
186
|
+
status: 'testing_failed',
|
|
187
|
+
});
|
|
188
|
+
let updateParams = null;
|
|
189
|
+
mockCallMcpEndpoint = async (method, params) => {
|
|
190
|
+
updateParams = params;
|
|
191
|
+
return { success: true };
|
|
192
|
+
};
|
|
193
|
+
const result = await updateFeatureStatus({
|
|
194
|
+
featureId: 'test-feature-id',
|
|
195
|
+
status: 'testing_in_progress',
|
|
196
|
+
verbose: true,
|
|
197
|
+
});
|
|
198
|
+
// This should be prevented because testing_in_progress comes before testing_failed
|
|
199
|
+
// in the progression order
|
|
200
|
+
assert.strictEqual(result, false, 'Should prevent regression from testing_failed to testing_in_progress');
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
describe('updateFeatureStatusForPhase', () => {
|
|
204
|
+
it('should successfully update status for known phases', async () => {
|
|
205
|
+
mockGetFeature = async (featureId) => ({
|
|
206
|
+
...mockFeatureInfo,
|
|
207
|
+
id: featureId,
|
|
208
|
+
status: 'backlog',
|
|
209
|
+
});
|
|
210
|
+
let mcpCalled = false;
|
|
211
|
+
let updateParams = null;
|
|
212
|
+
mockCallMcpEndpoint = async (method, params) => {
|
|
213
|
+
mcpCalled = true;
|
|
214
|
+
updateParams = params;
|
|
215
|
+
return { success: true };
|
|
216
|
+
};
|
|
217
|
+
const result = await updateFeatureStatusForPhase('test-feature-id', 'feature-analysis', true);
|
|
218
|
+
assert.strictEqual(result, true, 'Should return true for known phase');
|
|
219
|
+
assert.strictEqual(mcpCalled, true, 'Should call MCP endpoint');
|
|
220
|
+
assert.strictEqual(updateParams.status, 'feature_analysis', 'Should map phase to correct status');
|
|
221
|
+
});
|
|
222
|
+
it('should reject unknown phases', async () => {
|
|
223
|
+
let mcpCalled = false;
|
|
224
|
+
mockCallMcpEndpoint = async () => {
|
|
225
|
+
mcpCalled = true;
|
|
226
|
+
return { success: true };
|
|
227
|
+
};
|
|
228
|
+
const result = await updateFeatureStatusForPhase('test-feature-id', 'unknown-phase', true);
|
|
229
|
+
assert.strictEqual(result, false, 'Should return false for unknown phase');
|
|
230
|
+
assert.strictEqual(mcpCalled, false, 'Should not call MCP for unknown phase');
|
|
231
|
+
});
|
|
232
|
+
it('should handle all mapped phases correctly', async () => {
|
|
233
|
+
const phaseStatusPairs = [
|
|
234
|
+
['feature-analysis', 'feature_analysis'],
|
|
235
|
+
['feature-analysis-verification', 'feature_analysis_verification'],
|
|
236
|
+
['technical-design', 'technical_design'],
|
|
237
|
+
['technical-design-verification', 'technical_design_verification'],
|
|
238
|
+
['code-implementation', 'code_implementation'],
|
|
239
|
+
['code-implementation-verification', 'code_implementation_verification'],
|
|
240
|
+
['code-refine', 'code_refine'],
|
|
241
|
+
['code-refine-verification', 'code_refine_verification'],
|
|
242
|
+
['bug-fixing', 'bug_fixing'],
|
|
243
|
+
['code-review', 'code_review'],
|
|
244
|
+
['pull-request', 'pull_request'],
|
|
245
|
+
['functional-testing', 'functional_testing'],
|
|
246
|
+
['deployment', 'deployment'],
|
|
247
|
+
['testing-in-progress', 'testing_in_progress'],
|
|
248
|
+
['testing-passed', 'testing_passed'],
|
|
249
|
+
['testing-failed', 'testing_failed'],
|
|
250
|
+
];
|
|
251
|
+
for (const [phase, expectedStatus] of phaseStatusPairs) {
|
|
252
|
+
mockGetFeature = async (featureId) => ({
|
|
253
|
+
...mockFeatureInfo,
|
|
254
|
+
id: featureId,
|
|
255
|
+
status: 'backlog', // Start from backlog each time
|
|
256
|
+
});
|
|
257
|
+
let capturedStatus = null;
|
|
258
|
+
mockCallMcpEndpoint = async (method, params) => {
|
|
259
|
+
if (method === 'features/update') {
|
|
260
|
+
capturedStatus = params.status;
|
|
261
|
+
return { success: true };
|
|
262
|
+
}
|
|
263
|
+
return {};
|
|
264
|
+
};
|
|
265
|
+
const result = await updateFeatureStatusForPhase('test-feature-id', phase, false);
|
|
266
|
+
assert.strictEqual(result, true, `Phase ${phase} should be processed successfully`);
|
|
267
|
+
assert.strictEqual(capturedStatus, expectedStatus, `Phase ${phase} should map to status ${expectedStatus}`);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
it('should propagate errors from updateFeatureStatus', async () => {
|
|
271
|
+
mockGetFeature = async () => {
|
|
272
|
+
throw new Error('Database connection failed');
|
|
273
|
+
};
|
|
274
|
+
const result = await updateFeatureStatusForPhase('test-feature-id', 'feature-analysis', true);
|
|
275
|
+
assert.strictEqual(result, false, 'Should return false when underlying update fails');
|
|
276
|
+
});
|
|
277
|
+
it('should respect regression prevention in phase updates', async () => {
|
|
278
|
+
// Set current status to shipped
|
|
279
|
+
mockGetFeature = async (featureId) => ({
|
|
280
|
+
...mockFeatureInfo,
|
|
281
|
+
id: featureId,
|
|
282
|
+
status: 'shipped',
|
|
283
|
+
});
|
|
284
|
+
let mcpCalled = false;
|
|
285
|
+
mockCallMcpEndpoint = async () => {
|
|
286
|
+
mcpCalled = true;
|
|
287
|
+
return { success: true };
|
|
288
|
+
};
|
|
289
|
+
// Try to update to a phase that would regress the status
|
|
290
|
+
const result = await updateFeatureStatusForPhase('test-feature-id', 'feature-analysis', // This would be regression from shipped
|
|
291
|
+
true);
|
|
292
|
+
assert.strictEqual(result, false, 'Should prevent regression even when phase is valid');
|
|
293
|
+
assert.strictEqual(mcpCalled, false, 'Should not call MCP for regression attempt');
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
describe('getStatusForPhase', () => {
|
|
297
|
+
it('should return correct status for all known phases', () => {
|
|
298
|
+
const testCases = [
|
|
299
|
+
['feature-analysis', 'feature_analysis'],
|
|
300
|
+
['technical-design', 'technical_design'],
|
|
301
|
+
['code-implementation', 'code_implementation'],
|
|
302
|
+
['functional-testing', 'functional_testing'],
|
|
303
|
+
['deployment', 'deployment'],
|
|
304
|
+
['testing-passed', 'testing_passed'],
|
|
305
|
+
];
|
|
306
|
+
for (const [phase, expectedStatus] of testCases) {
|
|
307
|
+
const result = getStatusForPhase(phase);
|
|
308
|
+
assert.strictEqual(result, expectedStatus, `Phase ${phase} should map to ${expectedStatus}`);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
it('should return null for unknown phases', () => {
|
|
312
|
+
const unknownPhases = [
|
|
313
|
+
'unknown-phase',
|
|
314
|
+
'invalid-phase',
|
|
315
|
+
'some-random-string',
|
|
316
|
+
'',
|
|
317
|
+
'feature_analysis', // Wrong format (underscore instead of hyphen)
|
|
318
|
+
'technical_design', // Wrong format
|
|
319
|
+
];
|
|
320
|
+
for (const phase of unknownPhases) {
|
|
321
|
+
const result = getStatusForPhase(phase);
|
|
322
|
+
assert.strictEqual(result, null, `Unknown phase ${phase} should return null`);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
it('should handle edge cases', () => {
|
|
326
|
+
// Test case sensitivity
|
|
327
|
+
assert.strictEqual(getStatusForPhase('Feature-Analysis'), null, 'Should be case sensitive');
|
|
328
|
+
// Test whitespace
|
|
329
|
+
assert.strictEqual(getStatusForPhase(' feature-analysis '), null, 'Should not handle whitespace');
|
|
330
|
+
// Test empty string
|
|
331
|
+
assert.strictEqual(getStatusForPhase(''), null, 'Should return null for empty string');
|
|
332
|
+
});
|
|
333
|
+
it('should maintain consistency with phase naming conventions', () => {
|
|
334
|
+
// All returned statuses should use snake_case
|
|
335
|
+
const phases = [
|
|
336
|
+
'feature-analysis',
|
|
337
|
+
'technical-design',
|
|
338
|
+
'code-implementation',
|
|
339
|
+
'functional-testing',
|
|
340
|
+
];
|
|
341
|
+
for (const phase of phases) {
|
|
342
|
+
const status = getStatusForPhase(phase);
|
|
343
|
+
if (status) {
|
|
344
|
+
assert.ok(!status.includes('-'), `Status ${status} should not contain hyphens`);
|
|
345
|
+
if (status.includes('_')) {
|
|
346
|
+
assert.ok(status.split('_').length > 1, `Multi-word status ${status} should use snake_case`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
describe('Async Behavior and Breaking Changes', () => {
|
|
353
|
+
it('should handle updateFeatureStatusForPhase as async function', async () => {
|
|
354
|
+
mockGetFeature = async (featureId) => ({
|
|
355
|
+
...mockFeatureInfo,
|
|
356
|
+
id: featureId,
|
|
357
|
+
status: 'backlog',
|
|
358
|
+
});
|
|
359
|
+
mockCallMcpEndpoint = async () => ({ success: true });
|
|
360
|
+
// Verify the function returns a Promise
|
|
361
|
+
const resultPromise = updateFeatureStatusForPhase('test-feature-id', 'feature-analysis');
|
|
362
|
+
assert.ok(resultPromise instanceof Promise, 'updateFeatureStatusForPhase should return a Promise');
|
|
363
|
+
const result = await resultPromise;
|
|
364
|
+
assert.strictEqual(typeof result, 'boolean', 'Promise should resolve to boolean');
|
|
365
|
+
});
|
|
366
|
+
it('should handle concurrent status updates correctly', async () => {
|
|
367
|
+
let mcpCallCount = 0;
|
|
368
|
+
const mcpCalls = [];
|
|
369
|
+
mockGetFeature = async (featureId) => ({
|
|
370
|
+
...mockFeatureInfo,
|
|
371
|
+
id: featureId,
|
|
372
|
+
status: 'backlog',
|
|
373
|
+
});
|
|
374
|
+
mockCallMcpEndpoint = async (method, params) => {
|
|
375
|
+
mcpCallCount++;
|
|
376
|
+
mcpCalls.push({ method, params });
|
|
377
|
+
// Simulate some delay
|
|
378
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
379
|
+
return { success: true };
|
|
380
|
+
};
|
|
381
|
+
// Start multiple updates concurrently
|
|
382
|
+
const promises = [
|
|
383
|
+
updateFeatureStatusForPhase('feature-1', 'feature-analysis'),
|
|
384
|
+
updateFeatureStatusForPhase('feature-2', 'technical-design'),
|
|
385
|
+
updateFeatureStatusForPhase('feature-3', 'code-implementation'),
|
|
386
|
+
];
|
|
387
|
+
const results = await Promise.all(promises);
|
|
388
|
+
assert.strictEqual(results.length, 3, 'Should handle all concurrent updates');
|
|
389
|
+
assert.ok(results.every(r => r === true), 'All updates should succeed');
|
|
390
|
+
assert.strictEqual(mcpCallCount, 3, 'Should make correct number of MCP calls');
|
|
391
|
+
});
|
|
392
|
+
it('should maintain proper error isolation in concurrent updates', async () => {
|
|
393
|
+
let callCount = 0;
|
|
394
|
+
mockGetFeature = async (featureId) => {
|
|
395
|
+
callCount++;
|
|
396
|
+
if (featureId === 'failing-feature') {
|
|
397
|
+
throw new Error('Feature not found');
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
...mockFeatureInfo,
|
|
401
|
+
id: featureId,
|
|
402
|
+
status: 'backlog',
|
|
403
|
+
};
|
|
404
|
+
};
|
|
405
|
+
mockCallMcpEndpoint = async () => ({ success: true });
|
|
406
|
+
// Mix successful and failing updates
|
|
407
|
+
const promises = [
|
|
408
|
+
updateFeatureStatusForPhase('feature-1', 'feature-analysis'),
|
|
409
|
+
updateFeatureStatusForPhase('failing-feature', 'technical-design'),
|
|
410
|
+
updateFeatureStatusForPhase('feature-3', 'code-implementation'),
|
|
411
|
+
];
|
|
412
|
+
const results = await Promise.all(promises);
|
|
413
|
+
assert.strictEqual(results[0], true, 'First update should succeed');
|
|
414
|
+
assert.strictEqual(results[1], false, 'Second update should fail');
|
|
415
|
+
assert.strictEqual(results[2], true, 'Third update should succeed');
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
describe('Error Handling and Edge Cases', () => {
|
|
419
|
+
it('should handle malformed feature data gracefully', async () => {
|
|
420
|
+
// Mock getFeature to return malformed data
|
|
421
|
+
mockGetFeature = async () => ({
|
|
422
|
+
id: 'test-feature',
|
|
423
|
+
// Missing required fields
|
|
424
|
+
status: null,
|
|
425
|
+
});
|
|
426
|
+
mockCallMcpEndpoint = async () => ({ success: true });
|
|
427
|
+
const result = await updateFeatureStatus({
|
|
428
|
+
featureId: 'test-feature-id',
|
|
429
|
+
status: 'ready_for_dev',
|
|
430
|
+
verbose: true,
|
|
431
|
+
});
|
|
432
|
+
assert.strictEqual(result, false, 'Should handle malformed feature data gracefully');
|
|
433
|
+
});
|
|
434
|
+
it('should handle network timeouts and retries', async () => {
|
|
435
|
+
let attemptCount = 0;
|
|
436
|
+
mockGetFeature = async (featureId) => ({
|
|
437
|
+
...mockFeatureInfo,
|
|
438
|
+
id: featureId,
|
|
439
|
+
status: 'backlog',
|
|
440
|
+
});
|
|
441
|
+
mockCallMcpEndpoint = async () => {
|
|
442
|
+
attemptCount++;
|
|
443
|
+
if (attemptCount < 2) {
|
|
444
|
+
throw new Error('Network timeout');
|
|
445
|
+
}
|
|
446
|
+
return { success: true };
|
|
447
|
+
};
|
|
448
|
+
const result = await updateFeatureStatus({
|
|
449
|
+
featureId: 'test-feature-id',
|
|
450
|
+
status: 'ready_for_dev',
|
|
451
|
+
});
|
|
452
|
+
// Since we don't implement retry logic, this should fail
|
|
453
|
+
assert.strictEqual(result, false, 'Should handle network errors');
|
|
454
|
+
assert.strictEqual(attemptCount, 1, 'Should only attempt once');
|
|
455
|
+
});
|
|
456
|
+
it('should handle very long feature IDs and status names', async () => {
|
|
457
|
+
const longFeatureId = 'a'.repeat(1000);
|
|
458
|
+
const validStatus = 'feature_analysis';
|
|
459
|
+
mockGetFeature = async (featureId) => ({
|
|
460
|
+
...mockFeatureInfo,
|
|
461
|
+
id: featureId,
|
|
462
|
+
status: 'backlog',
|
|
463
|
+
});
|
|
464
|
+
let receivedParams = null;
|
|
465
|
+
mockCallMcpEndpoint = async (method, params) => {
|
|
466
|
+
receivedParams = params;
|
|
467
|
+
return { success: true };
|
|
468
|
+
};
|
|
469
|
+
const result = await updateFeatureStatus({
|
|
470
|
+
featureId: longFeatureId,
|
|
471
|
+
status: validStatus,
|
|
472
|
+
});
|
|
473
|
+
assert.strictEqual(result, true, 'Should handle long feature IDs');
|
|
474
|
+
assert.strictEqual(receivedParams?.feature_id, longFeatureId, 'Should pass through long feature ID correctly');
|
|
475
|
+
});
|
|
476
|
+
it('should handle status transitions at workflow boundaries', async () => {
|
|
477
|
+
// Test transition from last testing status to deployment
|
|
478
|
+
mockGetFeature = async (featureId) => ({
|
|
479
|
+
...mockFeatureInfo,
|
|
480
|
+
id: featureId,
|
|
481
|
+
status: 'testing_passed',
|
|
482
|
+
});
|
|
483
|
+
let updateParams = null;
|
|
484
|
+
mockCallMcpEndpoint = async (method, params) => {
|
|
485
|
+
updateParams = params;
|
|
486
|
+
return { success: true };
|
|
487
|
+
};
|
|
488
|
+
const result = await updateFeatureStatus({
|
|
489
|
+
featureId: 'test-feature-id',
|
|
490
|
+
status: 'deployment',
|
|
491
|
+
verbose: true,
|
|
492
|
+
});
|
|
493
|
+
assert.strictEqual(result, true, 'Should allow progression from testing to deployment');
|
|
494
|
+
assert.strictEqual(updateParams.status, 'deployment', 'Should update to deployment status');
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline runner for executing development workflow phases
|
|
3
|
+
* Complete pipeline flow:
|
|
4
|
+
* feature-analysis → technical-design → code-implementation →
|
|
5
|
+
* functional-testing → pull-request → code-review → code-refine → code-refine-verification
|
|
6
|
+
* Uses functional programming principles
|
|
7
|
+
*/
|
|
8
|
+
import { EdsgerConfig } from '../../types/index.js';
|
|
9
|
+
import { PipelinePhaseOptions, PipelineResult, ExecutionMode } from '../../types/pipeline.js';
|
|
10
|
+
/**
|
|
11
|
+
* Run pipeline based on execution mode
|
|
12
|
+
*/
|
|
13
|
+
export declare const runPipelineByMode: (options: PipelinePhaseOptions, config: EdsgerConfig, executionMode: ExecutionMode) => Promise<readonly PipelineResult[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Run complete pipeline for a feature using functional composition (legacy)
|
|
16
|
+
*/
|
|
17
|
+
export declare const runCompletePipeline: (options: PipelinePhaseOptions, config: EdsgerConfig) => () => Promise<readonly PipelineResult[]>;
|