@onlineapps/conn-orch-orchestrator 1.0.63 → 1.0.64

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.
@@ -1,125 +1,47 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Component tests for Orchestrator
5
- * Tests orchestrator with real internal components but mocked external services
4
+ * Component tests for WorkflowOrchestrator (V2.1 unified cookbook message format)
5
+ *
6
+ * Goal: verify orchestration behavior (task execution + routing) without external MQ/registry.
6
7
  */
7
8
 
9
+ jest.mock('@onlineapps/mq-client-core', () => ({
10
+ monitoring: {
11
+ publishToMonitoringWorkflow: jest.fn(async () => true)
12
+ }
13
+ }));
14
+
8
15
  const WorkflowOrchestrator = require('../../src/WorkflowOrchestrator');
9
16
 
10
- describe('Orchestrator - Component Tests @component', () => {
17
+ describe('WorkflowOrchestrator - Component Tests @component', () => {
11
18
  let orchestrator;
12
19
  let mqClientStub;
13
20
  let registryClientStub;
14
21
  let apiMapperStub;
15
22
 
16
23
  beforeEach(() => {
17
- // Create stubbed versions of external dependencies
18
- // that simulate real behavior
19
24
  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
25
+ publish: jest.fn(async () => true)
40
26
  };
41
27
 
42
28
  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
- })
29
+ getService: jest.fn(async () => null),
30
+ getAllServices: jest.fn(async () => [])
79
31
  };
80
32
 
81
33
  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
- })
34
+ callOperation: jest.fn(async (operationId, input) => ({ operationId, input }))
110
35
  };
111
36
 
112
- // Create orchestrator with component stubs
113
37
  orchestrator = new WorkflowOrchestrator({
114
38
  mqClient: mqClientStub,
115
39
  registryClient: registryClientStub,
116
40
  apiMapper: apiMapperStub,
117
41
  cookbook: {
118
42
  validateCookbook: jest.fn(),
119
- CookbookExecutor: class {
120
- execute() {
121
- return Promise.resolve({ results: {} });
122
- }
43
+ templateHelpers: {
44
+ normalizeString: (v) => String(v || '').trim()
123
45
  }
124
46
  },
125
47
  logger: {
@@ -131,248 +53,68 @@ describe('Orchestrator - Component Tests @component', () => {
131
53
  });
132
54
  });
133
55
 
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');
56
+ it('executes task and routes next task to next service queue', async () => {
57
+ const cookbook = {
58
+ version: '2.1.0',
59
+ api_input: { x: 1 },
60
+ steps: [
61
+ { step_id: 's1', type: 'task', service: 'svc-a', operation: 'op1', input: { x: '{{api_input.x}}' } },
62
+ { step_id: 's2', type: 'task', service: 'svc-b', operation: 'op2', input: { y: 2 } }
63
+ ],
64
+ delivery: { handler: 'none' }
65
+ };
249
66
 
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
- });
67
+ const msg = { workflow_id: 'wf-1', cookbook, current_step: 's1' };
68
+ await orchestrator.processWorkflowMessage(msg, 'svc-a');
69
+
70
+ expect(apiMapperStub.callOperation).toHaveBeenCalledWith(
71
+ 'op1',
72
+ { x: 1 },
73
+ expect.any(Object)
74
+ );
75
+
76
+ expect(mqClientStub.publish).toHaveBeenCalledWith(
77
+ 'svc-b.workflow',
78
+ expect.objectContaining({
79
+ workflow_id: 'wf-1',
80
+ current_step: 's2',
81
+ cookbook: expect.objectContaining({
82
+ _pointer: { next: 's2' }
83
+ })
84
+ })
85
+ );
260
86
  });
261
87
 
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'
88
+ it('routes next pinned control-flow step to pinned service queue (not workflow.init)', async () => {
89
+ const cookbook = {
90
+ version: '2.1.0',
91
+ api_input: { items: ['a'] },
92
+ steps: [
93
+ { step_id: 's1', type: 'task', service: 'svc-a', operation: 'op1', input: { x: 1 } },
94
+ {
95
+ step_id: 'loop',
96
+ type: 'foreach',
97
+ service: 'hello-service',
98
+ iterator: '{{api_input.items}}',
99
+ body: [
100
+ { step_id: 'inner', type: 'task', service: 'hello-service', operation: 'good-day', input: { name: '{{current}}' } }
101
+ ]
289
102
  }
290
- };
291
-
292
- await expect(
293
- orchestrator.processWorkflowMessage(workflow, 'test-service')
294
- ).rejects.toThrow('Connection refused');
295
- });
103
+ ],
104
+ delivery: { handler: 'none' }
105
+ };
296
106
 
297
- it('should handle malformed workflows', async () => {
298
- const malformedWorkflow = {
299
- // Missing required fields
300
- workflow_id: 'wf-malformed-123'
301
- };
107
+ const msg = { workflow_id: 'wf-2', cookbook, current_step: 's1' };
108
+ await orchestrator.processWorkflowMessage(msg, 'svc-a');
302
109
 
303
- await expect(
304
- orchestrator.processWorkflowMessage(malformedWorkflow, 'test-service')
305
- ).rejects.toThrow();
306
- });
110
+ expect(mqClientStub.publish).toHaveBeenCalledWith(
111
+ 'hello-service.workflow',
112
+ expect.objectContaining({
113
+ workflow_id: 'wf-2',
114
+ current_step: 'loop'
115
+ })
116
+ );
307
117
  });
118
+ });
308
119
 
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
120
 
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
- });
@@ -13,14 +13,18 @@ const CookbookConnector = require('@onlineapps/conn-orch-cookbook');
13
13
 
14
14
  // Skip integration tests if external services are not available
15
15
  const SKIP_INTEGRATION = process.env.SKIP_INTEGRATION === 'true';
16
- const { requireEnv } = require('../../../../../tests/_helpers/test-config');
17
- const RABBITMQ_URL = requireEnv('RABBITMQ_URL', 'RabbitMQ connection URL');
18
- const REGISTRY_URL = requireEnv('REGISTRY_URL', 'Registry service URL');
16
+ const RABBITMQ_URL = process.env.RABBITMQ_URL || null;
17
+ const REGISTRY_URL = process.env.REGISTRY_URL || null;
18
+ const HAS_ENV = !!(RABBITMQ_URL && REGISTRY_URL);
19
19
 
20
- describe('Orchestrator - Integration Tests @integration', () => {
20
+ const describeMaybe = (!SKIP_INTEGRATION && HAS_ENV) ? describe : describe.skip;
21
+
22
+ describeMaybe('Orchestrator - Integration Tests @integration', () => {
21
23
  if (SKIP_INTEGRATION) {
22
- it.skip('Skipping integration tests (SKIP_INTEGRATION=true)', () => {});
23
- return;
24
+ it('Skipping integration tests (SKIP_INTEGRATION=true)', () => {});
25
+ }
26
+ if (!HAS_ENV) {
27
+ it('Skipping integration tests (missing RABBITMQ_URL/REGISTRY_URL)', () => {});
24
28
  }
25
29
 
26
30
  let orchestrator;