@onlineapps/conn-orch-orchestrator 1.0.0
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/API.md +195 -0
- package/README.md +585 -0
- package/jest.config.js +29 -0
- package/jest.setup.js +38 -0
- package/package.json +41 -0
- package/src/WorkflowOrchestrator.js +341 -0
- package/src/index.js +43 -0
- package/test/component/orchestrator.component.test.js +378 -0
- package/test/integration/orchestrator.integration.test.js +313 -0
- package/test/unit/WorkflowOrchestrator.test.js +253 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Component tests for Orchestrator
|
|
5
|
+
* Tests orchestrator with real internal components but mocked external services
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const WorkflowOrchestrator = require('../../src/WorkflowOrchestrator');
|
|
9
|
+
|
|
10
|
+
describe('Orchestrator - Component Tests', () => {
|
|
11
|
+
let orchestrator;
|
|
12
|
+
let mqClientStub;
|
|
13
|
+
let registryClientStub;
|
|
14
|
+
let apiMapperStub;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// Create stubbed versions of external dependencies
|
|
18
|
+
// that simulate real behavior
|
|
19
|
+
mqClientStub = {
|
|
20
|
+
connected: true,
|
|
21
|
+
queues: new Map(),
|
|
22
|
+
|
|
23
|
+
publish: jest.fn(async (message, options) => {
|
|
24
|
+
// Simulate message publishing
|
|
25
|
+
const queueName = options.queue || 'default';
|
|
26
|
+
if (!mqClientStub.queues.has(queueName)) {
|
|
27
|
+
mqClientStub.queues.set(queueName, []);
|
|
28
|
+
}
|
|
29
|
+
mqClientStub.queues.get(queueName).push(message);
|
|
30
|
+
return { published: true };
|
|
31
|
+
}),
|
|
32
|
+
|
|
33
|
+
consume: jest.fn(async (handler, options) => {
|
|
34
|
+
// Store handler for later invocation
|
|
35
|
+
mqClientStub.handler = handler;
|
|
36
|
+
return { consuming: true };
|
|
37
|
+
}),
|
|
38
|
+
|
|
39
|
+
isConnected: () => mqClientStub.connected
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
registryClientStub = {
|
|
43
|
+
services: new Map([
|
|
44
|
+
['test-service', {
|
|
45
|
+
name: 'test-service',
|
|
46
|
+
url: 'http://localhost:3000',
|
|
47
|
+
openapi: {
|
|
48
|
+
openapi: '3.0.0',
|
|
49
|
+
paths: {
|
|
50
|
+
'/test': {
|
|
51
|
+
post: {
|
|
52
|
+
operationId: 'testOp',
|
|
53
|
+
requestBody: {
|
|
54
|
+
content: {
|
|
55
|
+
'application/json': {
|
|
56
|
+
schema: { type: 'object' }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}],
|
|
65
|
+
['invoice-service', {
|
|
66
|
+
name: 'invoice-service',
|
|
67
|
+
url: 'http://localhost:3001',
|
|
68
|
+
openapi: {}
|
|
69
|
+
}]
|
|
70
|
+
]),
|
|
71
|
+
|
|
72
|
+
getService: jest.fn(async (serviceName) => {
|
|
73
|
+
return registryClientStub.services.get(serviceName);
|
|
74
|
+
}),
|
|
75
|
+
|
|
76
|
+
getAllServices: jest.fn(async () => {
|
|
77
|
+
return Array.from(registryClientStub.services.values());
|
|
78
|
+
})
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
apiMapperStub = {
|
|
82
|
+
operations: new Map(),
|
|
83
|
+
|
|
84
|
+
callOperation: jest.fn(async (operationId, input, context) => {
|
|
85
|
+
// Simulate successful API call
|
|
86
|
+
return {
|
|
87
|
+
status: 200,
|
|
88
|
+
data: {
|
|
89
|
+
operationId,
|
|
90
|
+
input,
|
|
91
|
+
result: `Processed ${operationId}`
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}),
|
|
95
|
+
|
|
96
|
+
parseOpenApi: jest.fn((spec) => {
|
|
97
|
+
// Extract operations from OpenAPI spec
|
|
98
|
+
const operations = {};
|
|
99
|
+
if (spec.paths) {
|
|
100
|
+
Object.entries(spec.paths).forEach(([path, methods]) => {
|
|
101
|
+
Object.entries(methods).forEach(([method, op]) => {
|
|
102
|
+
if (op.operationId) {
|
|
103
|
+
operations[op.operationId] = { path, method, ...op };
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return operations;
|
|
109
|
+
})
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Create orchestrator with component stubs
|
|
113
|
+
orchestrator = new WorkflowOrchestrator({
|
|
114
|
+
mqClient: mqClientStub,
|
|
115
|
+
registryClient: registryClientStub,
|
|
116
|
+
apiMapper: apiMapperStub,
|
|
117
|
+
cookbook: {
|
|
118
|
+
validateCookbook: jest.fn(),
|
|
119
|
+
CookbookExecutor: class {
|
|
120
|
+
execute() {
|
|
121
|
+
return Promise.resolve({ results: {} });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
logger: {
|
|
126
|
+
info: jest.fn(),
|
|
127
|
+
error: jest.fn(),
|
|
128
|
+
warn: jest.fn(),
|
|
129
|
+
debug: jest.fn()
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('Workflow Processing', () => {
|
|
135
|
+
it('should process a complete workflow with multiple steps', async () => {
|
|
136
|
+
const workflow = {
|
|
137
|
+
workflow_id: 'wf-test-123',
|
|
138
|
+
current_step: {
|
|
139
|
+
id: 'step1',
|
|
140
|
+
service: 'test-service',
|
|
141
|
+
operation: 'testOp',
|
|
142
|
+
input: { value: 100 },
|
|
143
|
+
output: 'step1Result'
|
|
144
|
+
},
|
|
145
|
+
next_steps: [
|
|
146
|
+
{
|
|
147
|
+
id: 'step2',
|
|
148
|
+
service: 'invoice-service',
|
|
149
|
+
operation: 'createInvoice',
|
|
150
|
+
input: {
|
|
151
|
+
amount: '{{step1Result.value}}'
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
],
|
|
155
|
+
context: {
|
|
156
|
+
variables: { customerId: 'cust-123' },
|
|
157
|
+
results: {}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Process first step
|
|
162
|
+
const result = await orchestrator.processWorkflowMessage(workflow, 'test-service');
|
|
163
|
+
|
|
164
|
+
expect(result).toBeDefined();
|
|
165
|
+
expect(apiMapperStub.callOperation).toHaveBeenCalledWith(
|
|
166
|
+
'testOp',
|
|
167
|
+
{ value: 100 },
|
|
168
|
+
expect.any(Object)
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Verify message was routed to next service
|
|
172
|
+
if (workflow.next_steps && workflow.next_steps.length > 0) {
|
|
173
|
+
expect(mqClientStub.publish).toHaveBeenCalledWith(
|
|
174
|
+
expect.objectContaining({
|
|
175
|
+
workflow_id: 'wf-test-123',
|
|
176
|
+
current_step: expect.objectContaining({
|
|
177
|
+
service: 'invoice-service'
|
|
178
|
+
})
|
|
179
|
+
}),
|
|
180
|
+
expect.objectContaining({
|
|
181
|
+
queue: 'invoice-service.workflow'
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should handle parallel step execution', async () => {
|
|
188
|
+
const workflow = {
|
|
189
|
+
workflow_id: 'wf-parallel-123',
|
|
190
|
+
current_step: {
|
|
191
|
+
id: 'fork',
|
|
192
|
+
type: 'parallel',
|
|
193
|
+
steps: [
|
|
194
|
+
{
|
|
195
|
+
id: 'parallel1',
|
|
196
|
+
service: 'test-service',
|
|
197
|
+
operation: 'op1'
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: 'parallel2',
|
|
201
|
+
service: 'test-service',
|
|
202
|
+
operation: 'op2'
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
},
|
|
206
|
+
context: {}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
await orchestrator.processWorkflowMessage(workflow, 'test-service');
|
|
210
|
+
|
|
211
|
+
// Both operations should be called
|
|
212
|
+
expect(apiMapperStub.callOperation).toHaveBeenCalledTimes(2);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should handle conditional routing', async () => {
|
|
216
|
+
const workflow = {
|
|
217
|
+
workflow_id: 'wf-conditional-123',
|
|
218
|
+
current_step: {
|
|
219
|
+
id: 'decision',
|
|
220
|
+
service: 'test-service',
|
|
221
|
+
operation: 'checkCondition',
|
|
222
|
+
input: { threshold: 50 },
|
|
223
|
+
output: 'decision'
|
|
224
|
+
},
|
|
225
|
+
next_steps: [
|
|
226
|
+
{
|
|
227
|
+
id: 'highPath',
|
|
228
|
+
condition: '{{decision.value}} > 50',
|
|
229
|
+
service: 'invoice-service',
|
|
230
|
+
operation: 'highValue'
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: 'lowPath',
|
|
234
|
+
condition: '{{decision.value}} <= 50',
|
|
235
|
+
service: 'invoice-service',
|
|
236
|
+
operation: 'lowValue'
|
|
237
|
+
}
|
|
238
|
+
],
|
|
239
|
+
context: {}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Mock API response
|
|
243
|
+
apiMapperStub.callOperation.mockResolvedValueOnce({
|
|
244
|
+
status: 200,
|
|
245
|
+
data: { value: 75 }
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
await orchestrator.processWorkflowMessage(workflow, 'test-service');
|
|
249
|
+
|
|
250
|
+
// Should route to high value path
|
|
251
|
+
expect(mqClientStub.publish).toHaveBeenCalledWith(
|
|
252
|
+
expect.objectContaining({
|
|
253
|
+
current_step: expect.objectContaining({
|
|
254
|
+
operation: 'highValue'
|
|
255
|
+
})
|
|
256
|
+
}),
|
|
257
|
+
expect.any(Object)
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('Error Handling', () => {
|
|
263
|
+
it('should handle service unavailable gracefully', async () => {
|
|
264
|
+
registryClientStub.getService.mockResolvedValueOnce(null);
|
|
265
|
+
|
|
266
|
+
const workflow = {
|
|
267
|
+
workflow_id: 'wf-error-123',
|
|
268
|
+
current_step: {
|
|
269
|
+
service: 'unknown-service',
|
|
270
|
+
operation: 'test'
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
await expect(
|
|
275
|
+
orchestrator.processWorkflowMessage(workflow, 'unknown-service')
|
|
276
|
+
).rejects.toThrow();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should handle API call failures', async () => {
|
|
280
|
+
apiMapperStub.callOperation.mockRejectedValueOnce(
|
|
281
|
+
new Error('Connection refused')
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
const workflow = {
|
|
285
|
+
workflow_id: 'wf-api-error-123',
|
|
286
|
+
current_step: {
|
|
287
|
+
service: 'test-service',
|
|
288
|
+
operation: 'failingOp'
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
await expect(
|
|
293
|
+
orchestrator.processWorkflowMessage(workflow, 'test-service')
|
|
294
|
+
).rejects.toThrow('Connection refused');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should handle malformed workflows', async () => {
|
|
298
|
+
const malformedWorkflow = {
|
|
299
|
+
// Missing required fields
|
|
300
|
+
workflow_id: 'wf-malformed-123'
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
await expect(
|
|
304
|
+
orchestrator.processWorkflowMessage(malformedWorkflow, 'test-service')
|
|
305
|
+
).rejects.toThrow();
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe('Context Management', () => {
|
|
310
|
+
it('should maintain context across steps', async () => {
|
|
311
|
+
const workflow = {
|
|
312
|
+
workflow_id: 'wf-context-123',
|
|
313
|
+
current_step: {
|
|
314
|
+
id: 'step1',
|
|
315
|
+
service: 'test-service',
|
|
316
|
+
operation: 'addContext',
|
|
317
|
+
input: { newData: 'test' },
|
|
318
|
+
output: 'contextData'
|
|
319
|
+
},
|
|
320
|
+
context: {
|
|
321
|
+
variables: { existing: 'data' },
|
|
322
|
+
results: {}
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
await orchestrator.processWorkflowMessage(workflow, 'test-service');
|
|
327
|
+
|
|
328
|
+
// Context should be preserved in routed message
|
|
329
|
+
expect(mqClientStub.publish).toHaveBeenCalledWith(
|
|
330
|
+
expect.objectContaining({
|
|
331
|
+
context: expect.objectContaining({
|
|
332
|
+
variables: expect.objectContaining({
|
|
333
|
+
existing: 'data'
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
}),
|
|
337
|
+
expect.any(Object)
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('should accumulate results in context', async () => {
|
|
342
|
+
const workflow = {
|
|
343
|
+
workflow_id: 'wf-results-123',
|
|
344
|
+
current_step: {
|
|
345
|
+
id: 'step2',
|
|
346
|
+
service: 'test-service',
|
|
347
|
+
operation: 'process',
|
|
348
|
+
output: 'step2Result'
|
|
349
|
+
},
|
|
350
|
+
context: {
|
|
351
|
+
results: {
|
|
352
|
+
step1Result: { previous: 'data' }
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
apiMapperStub.callOperation.mockResolvedValueOnce({
|
|
358
|
+
status: 200,
|
|
359
|
+
data: { new: 'result' }
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
await orchestrator.processWorkflowMessage(workflow, 'test-service');
|
|
363
|
+
|
|
364
|
+
// Results should accumulate
|
|
365
|
+
expect(mqClientStub.publish).toHaveBeenCalledWith(
|
|
366
|
+
expect.objectContaining({
|
|
367
|
+
context: expect.objectContaining({
|
|
368
|
+
results: expect.objectContaining({
|
|
369
|
+
step1Result: { previous: 'data' },
|
|
370
|
+
step2Result: { new: 'result' }
|
|
371
|
+
})
|
|
372
|
+
})
|
|
373
|
+
}),
|
|
374
|
+
expect.any(Object)
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
});
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Integration tests for Orchestrator
|
|
5
|
+
* Tests orchestrator with real external services (when available)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const WorkflowOrchestrator = require('../../src');
|
|
9
|
+
const MQConnector = require('@onlineapps/conn-infra-mq');
|
|
10
|
+
const RegistryConnector = require('@onlineapps/conn-orch-registry');
|
|
11
|
+
const ApiMapperConnector = require('@onlineapps/conn-orch-api-mapper');
|
|
12
|
+
const CookbookConnector = require('@onlineapps/conn-orch-cookbook');
|
|
13
|
+
|
|
14
|
+
// Skip integration tests if external services are not available
|
|
15
|
+
const SKIP_INTEGRATION = process.env.SKIP_INTEGRATION === 'true';
|
|
16
|
+
const RABBITMQ_URL = process.env.RABBITMQ_URL || 'amqp://localhost:5672';
|
|
17
|
+
const REGISTRY_URL = process.env.REGISTRY_URL || 'http://localhost:4000';
|
|
18
|
+
|
|
19
|
+
describe('Orchestrator - Integration Tests', () => {
|
|
20
|
+
if (SKIP_INTEGRATION) {
|
|
21
|
+
it.skip('Skipping integration tests (SKIP_INTEGRATION=true)', () => {});
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let orchestrator;
|
|
26
|
+
let mqClient;
|
|
27
|
+
let registryClient;
|
|
28
|
+
|
|
29
|
+
beforeAll(async () => {
|
|
30
|
+
// Setup real connections
|
|
31
|
+
try {
|
|
32
|
+
mqClient = new MQConnector({
|
|
33
|
+
url: RABBITMQ_URL,
|
|
34
|
+
serviceName: 'test-orchestrator'
|
|
35
|
+
});
|
|
36
|
+
await mqClient.connect();
|
|
37
|
+
|
|
38
|
+
registryClient = new RegistryConnector.ServiceRegistryClient({
|
|
39
|
+
registryUrl: REGISTRY_URL
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Register test service
|
|
43
|
+
await registryClient.register({
|
|
44
|
+
name: 'test-orchestrator',
|
|
45
|
+
url: 'http://localhost:9999',
|
|
46
|
+
openapi: {
|
|
47
|
+
openapi: '3.0.0',
|
|
48
|
+
info: { title: 'Test Service', version: '1.0.0' },
|
|
49
|
+
paths: {
|
|
50
|
+
'/echo': {
|
|
51
|
+
post: {
|
|
52
|
+
operationId: 'echo',
|
|
53
|
+
requestBody: {
|
|
54
|
+
content: {
|
|
55
|
+
'application/json': {
|
|
56
|
+
schema: { type: 'object' }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.warn('Failed to setup integration test environment:', error.message);
|
|
67
|
+
console.warn('Skipping integration tests');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterAll(async () => {
|
|
73
|
+
// Cleanup
|
|
74
|
+
if (orchestrator) {
|
|
75
|
+
await orchestrator.shutdown();
|
|
76
|
+
}
|
|
77
|
+
if (registryClient) {
|
|
78
|
+
await registryClient.unregister('test-orchestrator');
|
|
79
|
+
}
|
|
80
|
+
if (mqClient) {
|
|
81
|
+
await mqClient.disconnect();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
const apiMapper = ApiMapperConnector.create({
|
|
87
|
+
openApiSpec: {
|
|
88
|
+
openapi: '3.0.0',
|
|
89
|
+
paths: {
|
|
90
|
+
'/echo': {
|
|
91
|
+
post: {
|
|
92
|
+
operationId: 'echo'
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
serviceUrl: 'http://localhost:9999'
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
orchestrator = WorkflowOrchestrator.create({
|
|
101
|
+
mqClient,
|
|
102
|
+
registryClient,
|
|
103
|
+
apiMapper,
|
|
104
|
+
cookbook: CookbookConnector,
|
|
105
|
+
logger: {
|
|
106
|
+
info: jest.fn(),
|
|
107
|
+
error: jest.fn(),
|
|
108
|
+
warn: jest.fn(),
|
|
109
|
+
debug: jest.fn()
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('End-to-End Workflow Execution', () => {
|
|
115
|
+
it('should execute a simple workflow end-to-end', async () => {
|
|
116
|
+
const workflow = {
|
|
117
|
+
workflow_id: 'int-test-' + Date.now(),
|
|
118
|
+
current_step: {
|
|
119
|
+
id: 'step1',
|
|
120
|
+
service: 'test-orchestrator',
|
|
121
|
+
operation: 'echo',
|
|
122
|
+
input: { message: 'Hello Integration Test' },
|
|
123
|
+
output: 'echoResult'
|
|
124
|
+
},
|
|
125
|
+
context: {
|
|
126
|
+
variables: {},
|
|
127
|
+
results: {}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Mock the actual HTTP call since we don't have a real service
|
|
132
|
+
orchestrator.apiMapper.callOperation = jest.fn().mockResolvedValue({
|
|
133
|
+
status: 200,
|
|
134
|
+
data: { echoed: 'Hello Integration Test' }
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const result = await orchestrator.processWorkflowMessage(
|
|
138
|
+
workflow,
|
|
139
|
+
'test-orchestrator'
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
expect(result).toBeDefined();
|
|
143
|
+
expect(result.data).toHaveProperty('echoed');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should handle message routing through RabbitMQ', async () => {
|
|
147
|
+
const testQueue = 'test.integration.queue';
|
|
148
|
+
|
|
149
|
+
// Setup consumer
|
|
150
|
+
let receivedMessage = null;
|
|
151
|
+
await mqClient.consume(
|
|
152
|
+
(message) => {
|
|
153
|
+
receivedMessage = message;
|
|
154
|
+
return Promise.resolve();
|
|
155
|
+
},
|
|
156
|
+
{ queue: testQueue }
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Send message through orchestrator
|
|
160
|
+
const workflow = {
|
|
161
|
+
workflow_id: 'route-test-' + Date.now(),
|
|
162
|
+
current_step: {
|
|
163
|
+
id: 'route',
|
|
164
|
+
service: 'test-orchestrator',
|
|
165
|
+
operation: 'echo',
|
|
166
|
+
input: { test: 'data' }
|
|
167
|
+
},
|
|
168
|
+
next_steps: [
|
|
169
|
+
{
|
|
170
|
+
id: 'next',
|
|
171
|
+
service: 'next-service',
|
|
172
|
+
operation: 'process'
|
|
173
|
+
}
|
|
174
|
+
],
|
|
175
|
+
context: {}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Mock the API call
|
|
179
|
+
orchestrator.apiMapper.callOperation = jest.fn().mockResolvedValue({
|
|
180
|
+
status: 200,
|
|
181
|
+
data: { processed: true }
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Override routing to use test queue
|
|
185
|
+
orchestrator.routeToNextStep = async (workflowId, step, context) => {
|
|
186
|
+
await mqClient.publish(
|
|
187
|
+
{ workflow_id: workflowId, current_step: step, context },
|
|
188
|
+
{ queue: testQueue }
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
await orchestrator.processWorkflowMessage(workflow, 'test-orchestrator');
|
|
193
|
+
|
|
194
|
+
// Wait for message to be received
|
|
195
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
196
|
+
|
|
197
|
+
expect(receivedMessage).toBeTruthy();
|
|
198
|
+
expect(receivedMessage.workflow_id).toBe(workflow.workflow_id);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should interact with registry for service discovery', async () => {
|
|
202
|
+
// Get service info from registry
|
|
203
|
+
const serviceInfo = await registryClient.getService('test-orchestrator');
|
|
204
|
+
|
|
205
|
+
expect(serviceInfo).toBeDefined();
|
|
206
|
+
expect(serviceInfo.name).toBe('test-orchestrator');
|
|
207
|
+
expect(serviceInfo.openapi).toBeDefined();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('Fault Tolerance', () => {
|
|
212
|
+
it('should handle temporary network failures', async () => {
|
|
213
|
+
// Simulate network failure
|
|
214
|
+
orchestrator.apiMapper.callOperation = jest.fn()
|
|
215
|
+
.mockRejectedValueOnce(new Error('ECONNREFUSED'))
|
|
216
|
+
.mockResolvedValueOnce({ status: 200, data: { ok: true } });
|
|
217
|
+
|
|
218
|
+
const workflow = {
|
|
219
|
+
workflow_id: 'retry-test-' + Date.now(),
|
|
220
|
+
current_step: {
|
|
221
|
+
service: 'test-orchestrator',
|
|
222
|
+
operation: 'echo',
|
|
223
|
+
retry: { max: 3, delay: 100 }
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// With retry logic (if implemented)
|
|
228
|
+
const result = await orchestrator.processWorkflowMessage(
|
|
229
|
+
workflow,
|
|
230
|
+
'test-orchestrator'
|
|
231
|
+
).catch(e => e);
|
|
232
|
+
|
|
233
|
+
// Should eventually succeed or fail after retries
|
|
234
|
+
expect(orchestrator.apiMapper.callOperation).toHaveBeenCalled();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should handle message acknowledgment correctly', async () => {
|
|
238
|
+
const workflow = {
|
|
239
|
+
workflow_id: 'ack-test-' + Date.now(),
|
|
240
|
+
current_step: {
|
|
241
|
+
service: 'test-orchestrator',
|
|
242
|
+
operation: 'echo'
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
orchestrator.apiMapper.callOperation = jest.fn()
|
|
247
|
+
.mockResolvedValue({ status: 200, data: {} });
|
|
248
|
+
|
|
249
|
+
await orchestrator.processWorkflowMessage(workflow, 'test-orchestrator');
|
|
250
|
+
|
|
251
|
+
// Message should be processed without errors
|
|
252
|
+
expect(orchestrator.apiMapper.callOperation).toHaveBeenCalled();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('Performance', () => {
|
|
257
|
+
it('should handle multiple concurrent workflows', async () => {
|
|
258
|
+
const workflows = Array.from({ length: 10 }, (_, i) => ({
|
|
259
|
+
workflow_id: `perf-test-${Date.now()}-${i}`,
|
|
260
|
+
current_step: {
|
|
261
|
+
service: 'test-orchestrator',
|
|
262
|
+
operation: 'echo',
|
|
263
|
+
input: { index: i }
|
|
264
|
+
}
|
|
265
|
+
}));
|
|
266
|
+
|
|
267
|
+
orchestrator.apiMapper.callOperation = jest.fn()
|
|
268
|
+
.mockResolvedValue({ status: 200, data: {} });
|
|
269
|
+
|
|
270
|
+
const startTime = Date.now();
|
|
271
|
+
|
|
272
|
+
await Promise.all(
|
|
273
|
+
workflows.map(w =>
|
|
274
|
+
orchestrator.processWorkflowMessage(w, 'test-orchestrator')
|
|
275
|
+
)
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const duration = Date.now() - startTime;
|
|
279
|
+
|
|
280
|
+
expect(orchestrator.apiMapper.callOperation).toHaveBeenCalledTimes(10);
|
|
281
|
+
expect(duration).toBeLessThan(5000); // Should complete within 5 seconds
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should not leak memory during long-running operations', async () => {
|
|
285
|
+
const initialMemory = process.memoryUsage().heapUsed;
|
|
286
|
+
|
|
287
|
+
// Process many workflows
|
|
288
|
+
for (let i = 0; i < 100; i++) {
|
|
289
|
+
const workflow = {
|
|
290
|
+
workflow_id: `mem-test-${i}`,
|
|
291
|
+
current_step: {
|
|
292
|
+
service: 'test-orchestrator',
|
|
293
|
+
operation: 'echo',
|
|
294
|
+
input: { data: 'x'.repeat(1000) } // 1KB of data
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
orchestrator.apiMapper.callOperation = jest.fn()
|
|
299
|
+
.mockResolvedValue({ status: 200, data: {} });
|
|
300
|
+
|
|
301
|
+
await orchestrator.processWorkflowMessage(workflow, 'test-orchestrator');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
global.gc && global.gc(); // Force garbage collection if available
|
|
305
|
+
|
|
306
|
+
const finalMemory = process.memoryUsage().heapUsed;
|
|
307
|
+
const memoryGrowth = finalMemory - initialMemory;
|
|
308
|
+
|
|
309
|
+
// Memory growth should be reasonable (< 50MB)
|
|
310
|
+
expect(memoryGrowth).toBeLessThan(50 * 1024 * 1024);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
});
|