@app-connect/core 1.7.32 → 1.7.34

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.
@@ -31,6 +31,7 @@ const { CallLogModel } = require('../../models/callLogModel');
31
31
  const { MessageLogModel } = require('../../models/messageLogModel');
32
32
  const { UserModel } = require('../../models/userModel');
33
33
  const { AccountDataModel } = require('../../models/accountDataModel');
34
+ const { CacheModel } = require('../../models/cacheModel');
34
35
  const connectorRegistry = require('../../connector/registry');
35
36
  const oauth = require('../../lib/oauth');
36
37
  const { composeCallLog } = require('../../lib/callLogComposer');
@@ -44,6 +45,7 @@ describe('Log Handler', () => {
44
45
  await MessageLogModel.sync({ force: true });
45
46
  await UserModel.sync({ force: true });
46
47
  await AccountDataModel.sync({ force: true });
48
+ await CacheModel.sync({ force: true });
47
49
  });
48
50
 
49
51
  afterEach(async () => {
@@ -51,6 +53,7 @@ describe('Log Handler', () => {
51
53
  await MessageLogModel.destroy({ where: {} });
52
54
  await UserModel.destroy({ where: {} });
53
55
  await AccountDataModel.destroy({ where: {} });
56
+ await CacheModel.destroy({ where: {} });
54
57
  jest.clearAllMocks();
55
58
  });
56
59
 
@@ -330,6 +333,91 @@ describe('Log Handler', () => {
330
333
  );
331
334
  });
332
335
 
336
+ test('should create async task cache and pass task details to async call plugin after log creation', async () => {
337
+ const originalAppServer = process.env.APP_SERVER;
338
+ process.env.APP_SERVER = 'https://app.example.com';
339
+ try {
340
+ await UserModel.create(mockUser);
341
+ await AccountDataModel.create({
342
+ rcAccountId: mockUser.rcAccountId,
343
+ platformName: 'asyncPlugin',
344
+ dataKey: 'pluginData',
345
+ data: {
346
+ name: 'plugin.async',
347
+ supportedLogTypes: ['call'],
348
+ isAsync: true,
349
+ endpointUrl: 'https://plugins.example.com/plugin/asyncPlugin',
350
+ tokenSyncUrl: 'https://plugins.example.com/plugin/asyncPlugin/token',
351
+ jwtToken: 'plugin-jwt-token'
352
+ }
353
+ });
354
+
355
+ const mockConnector = {
356
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
357
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
358
+ getLogFormatType: jest.fn().mockReturnValue('text/plain'),
359
+ createCallLog: jest.fn().mockResolvedValue({
360
+ logId: 'new-log-async',
361
+ returnMessage: { message: 'Call logged', messageType: 'success', ttl: 2000 }
362
+ })
363
+ };
364
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
365
+ composeCallLog.mockReturnValue('Composed log details');
366
+ axios.post.mockImplementation((url) => {
367
+ if (url === 'https://plugins.example.com/plugin/asyncPlugin/token') {
368
+ return Promise.resolve({
369
+ headers: {
370
+ 'x-refreshed-jwt-token': 'synced-plugin-jwt'
371
+ }
372
+ });
373
+ }
374
+ return Promise.resolve({ data: { accepted: true }, headers: {} });
375
+ });
376
+
377
+ const result = await logHandler.createCallLog({
378
+ platform: 'testCRM',
379
+ userId: 'test-user-id',
380
+ incomingData: mockIncomingData,
381
+ hashedAccountId: 'hashed-123',
382
+ isFromSSCL: false
383
+ });
384
+
385
+ expect(result.successful).toBe(true);
386
+ const cache = await CacheModel.findOne({
387
+ where: {
388
+ cacheKey: 'asyncPluginTask-asyncPlugin'
389
+ }
390
+ });
391
+ expect(cache).not.toBeNull();
392
+ expect(cache.status).toBe('pending');
393
+ expect(cache.userId).toBe('test-user-id');
394
+ expect(cache.data.pluginId).toBe('asyncPlugin');
395
+ expect(cache.data.sessionId).toBe('session-123');
396
+ expect(cache.data.thirdPartyLogId).toBe('new-log-async');
397
+ expect(cache.expiry.getTime() - Date.now()).toBeGreaterThan(6 * 24 * 60 * 60 * 1000);
398
+
399
+ const pluginCall = axios.post.mock.calls.find(([url]) => url === 'https://plugins.example.com/plugin/asyncPlugin');
400
+ expect(pluginCall).toBeTruthy();
401
+ expect(pluginCall[1]).toEqual({
402
+ data: mockIncomingData,
403
+ config: null,
404
+ asyncTaskId: cache.id,
405
+ callbackUrl: `https://app.example.com/plugin/async-callback/${cache.id}`
406
+ });
407
+ expect(pluginCall[2]).toEqual({
408
+ headers: {
409
+ Authorization: 'Bearer synced-plugin-jwt'
410
+ }
411
+ });
412
+ } finally {
413
+ if (originalAppServer) {
414
+ process.env.APP_SERVER = originalAppServer;
415
+ } else {
416
+ delete process.env.APP_SERVER;
417
+ }
418
+ }
419
+ });
420
+
333
421
  test('should successfully create call log with oauth auth', async () => {
334
422
  // Arrange
335
423
  const oauthUser = { ...mockUser };
@@ -1201,6 +1289,143 @@ describe('Log Handler', () => {
1201
1289
  });
1202
1290
  });
1203
1291
 
1292
+ describe('handleAsyncPluginCallback', () => {
1293
+ test('should append callback note to call log and remove task cache on success', async () => {
1294
+ await UserModel.create({
1295
+ id: 'test-user-id',
1296
+ platform: 'testCRM',
1297
+ accessToken: 'test-token',
1298
+ platformAdditionalInfo: {}
1299
+ });
1300
+ await CallLogModel.create({
1301
+ id: 'call-1',
1302
+ sessionId: 'session-1',
1303
+ extensionNumber: '',
1304
+ platform: 'testCRM',
1305
+ thirdPartyLogId: 'log-1',
1306
+ userId: 'test-user-id',
1307
+ contactId: 'contact-1'
1308
+ });
1309
+ await CacheModel.create({
1310
+ id: 'task-1',
1311
+ status: 'pending',
1312
+ userId: 'test-user-id',
1313
+ cacheKey: 'asyncPluginTask-testPlugin',
1314
+ expiry: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
1315
+ data: {
1316
+ pluginId: 'testPlugin',
1317
+ platform: 'testCRM',
1318
+ userId: 'test-user-id',
1319
+ sessionId: 'session-1',
1320
+ extensionNumber: '',
1321
+ incomingData: {
1322
+ logInfo: {
1323
+ sessionId: 'session-1',
1324
+ startTime: '2026-06-12T10:00:00.000Z',
1325
+ duration: 120,
1326
+ result: 'Completed',
1327
+ direction: 'Outbound',
1328
+ from: { phoneNumber: '+1234567890' },
1329
+ to: { phoneNumber: '+1987654321' }
1330
+ },
1331
+ note: 'Original note'
1332
+ },
1333
+ hashedAccountId: 'hashed-123',
1334
+ isFromSSCL: false
1335
+ }
1336
+ });
1337
+
1338
+ const mockConnector = {
1339
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
1340
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
1341
+ getLogFormatType: jest.fn().mockReturnValue('text/plain'),
1342
+ getCallLog: jest.fn().mockResolvedValue({
1343
+ callLogInfo: {
1344
+ fullBody: '- Note: Existing note\n- Summary: Existing summary',
1345
+ note: 'Existing note',
1346
+ fullLogResponse: { id: 'log-1' }
1347
+ }
1348
+ }),
1349
+ updateCallLog: jest.fn().mockResolvedValue({
1350
+ updatedNote: 'Existing note\n\nCallback note'
1351
+ })
1352
+ };
1353
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
1354
+ composeCallLog.mockReturnValue('Updated composed log');
1355
+
1356
+ const result = await logHandler.handleAsyncPluginCallback({
1357
+ taskId: 'task-1',
1358
+ body: {
1359
+ successful: true,
1360
+ message: 'Done',
1361
+ note: 'Callback note'
1362
+ }
1363
+ });
1364
+
1365
+ expect(result).toEqual({
1366
+ statusCode: 200,
1367
+ body: { successful: true }
1368
+ });
1369
+ expect(composeCallLog).toHaveBeenCalledWith(expect.objectContaining({
1370
+ existingBody: '- Note: Existing note\n- Summary: Existing summary',
1371
+ note: 'Existing note\n\nCallback note'
1372
+ }));
1373
+ expect(mockConnector.updateCallLog).toHaveBeenCalledWith(expect.objectContaining({
1374
+ note: 'Existing note\n\nCallback note',
1375
+ composedLogDetails: 'Updated composed log',
1376
+ existingCallLogDetails: { id: 'log-1' }
1377
+ }));
1378
+ expect(await CacheModel.findByPk('task-1')).toBeNull();
1379
+ });
1380
+
1381
+ test('should mark task cache failed when callback reports failure', async () => {
1382
+ await CacheModel.create({
1383
+ id: 'task-failed',
1384
+ status: 'pending',
1385
+ userId: 'test-user-id',
1386
+ cacheKey: 'asyncPluginTask-testPlugin',
1387
+ expiry: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
1388
+ data: {
1389
+ pluginId: 'testPlugin',
1390
+ platform: 'testCRM',
1391
+ userId: 'test-user-id',
1392
+ sessionId: 'session-1'
1393
+ }
1394
+ });
1395
+
1396
+ const result = await logHandler.handleAsyncPluginCallback({
1397
+ taskId: 'task-failed',
1398
+ body: {
1399
+ successful: false,
1400
+ message: 'Plugin failed',
1401
+ note: 'Ignored note'
1402
+ }
1403
+ });
1404
+
1405
+ expect(result).toEqual({
1406
+ statusCode: 200,
1407
+ body: { successful: true }
1408
+ });
1409
+ const cache = await CacheModel.findByPk('task-failed');
1410
+ expect(cache.status).toBe('failed');
1411
+ expect(cache.data.message).toBe('Plugin failed');
1412
+ });
1413
+
1414
+ test('should reject callback without successful boolean', async () => {
1415
+ const result = await logHandler.handleAsyncPluginCallback({
1416
+ taskId: 'task-1',
1417
+ body: {
1418
+ message: 'Missing status'
1419
+ }
1420
+ });
1421
+
1422
+ expect(result).toEqual({
1423
+ statusCode: 400,
1424
+ body: { successful: false, message: 'successful is required' }
1425
+ });
1426
+ });
1427
+ });
1428
+
1204
1429
  describe('Error Handling', () => {
1205
1430
  test('should handle 429 rate limit error in createCallLog', async () => {
1206
1431
  // Arrange
@@ -34,262 +34,6 @@ describe('Plugin Handler', () => {
34
34
  await sequelize.close();
35
35
  });
36
36
 
37
- describe('getPluginAsyncTasks', () => {
38
- test('should retrieve async task status by IDs from CacheModel', async () => {
39
- // Arrange
40
- await CacheModel.create({
41
- id: 'user-123-task-1',
42
- status: 'processing',
43
- userId: 'user-123',
44
- cacheKey: 'pluginTask-googleDrive'
45
- });
46
- await CacheModel.create({
47
- id: 'user-123-task-2',
48
- status: 'completed',
49
- userId: 'user-123',
50
- cacheKey: 'pluginTask-piiRedaction'
51
- });
52
-
53
- // Act
54
- const result = await pluginHandler.getPluginAsyncTasks({
55
- asyncTaskIds: ['user-123-task-1', 'user-123-task-2']
56
- });
57
-
58
- // Assert
59
- expect(result).toHaveLength(2);
60
- expect(result).toContainEqual({ cacheKey: 'pluginTask-googleDrive', status: 'processing' });
61
- expect(result).toContainEqual({ cacheKey: 'pluginTask-piiRedaction', status: 'completed' });
62
- });
63
-
64
- test('should return empty array when no matching tasks found', async () => {
65
- // Arrange - no tasks created
66
-
67
- // Act
68
- const result = await pluginHandler.getPluginAsyncTasks({
69
- asyncTaskIds: ['non-existent-task-1', 'non-existent-task-2']
70
- });
71
-
72
- // Assert
73
- expect(result).toEqual([]);
74
- });
75
-
76
- test('should filter and return only tasks with matching IDs', async () => {
77
- // Arrange
78
- await CacheModel.create({
79
- id: 'user-123-task-1',
80
- status: 'processing',
81
- userId: 'user-123',
82
- cacheKey: 'pluginTask-googleDrive'
83
- });
84
- await CacheModel.create({
85
- id: 'user-456-task-2',
86
- status: 'completed',
87
- userId: 'user-456',
88
- cacheKey: 'pluginTask-piiRedaction'
89
- });
90
- await CacheModel.create({
91
- id: 'user-789-task-3',
92
- status: 'failed',
93
- userId: 'user-789',
94
- cacheKey: 'pluginTask-other'
95
- });
96
-
97
- // Act - only request tasks for user-123 and user-789
98
- const result = await pluginHandler.getPluginAsyncTasks({
99
- asyncTaskIds: ['user-123-task-1', 'user-789-task-3']
100
- });
101
-
102
- // Assert
103
- expect(result).toHaveLength(2);
104
- expect(result).toContainEqual({ cacheKey: 'pluginTask-googleDrive', status: 'processing' });
105
- expect(result).toContainEqual({ cacheKey: 'pluginTask-other', status: 'failed' });
106
- expect(result).not.toContainEqual(expect.objectContaining({ cacheKey: 'pluginTask-piiRedaction' }));
107
- });
108
-
109
- test('should automatically remove completed tasks from cache after retrieval', async () => {
110
- // Arrange
111
- await CacheModel.create({
112
- id: 'user-123-completed-task',
113
- status: 'completed',
114
- userId: 'user-123',
115
- cacheKey: 'pluginTask-googleDrive'
116
- });
117
-
118
- // Act
119
- const result = await pluginHandler.getPluginAsyncTasks({
120
- asyncTaskIds: ['user-123-completed-task']
121
- });
122
-
123
- // Assert - result should contain the task
124
- expect(result).toHaveLength(1);
125
- expect(result[0]).toEqual({ cacheKey: 'pluginTask-googleDrive', status: 'completed' });
126
-
127
- // Verify task was removed from cache
128
- const remainingTask = await CacheModel.findByPk('user-123-completed-task');
129
- expect(remainingTask).toBeNull();
130
- });
131
-
132
- test('should automatically remove failed tasks from cache after retrieval', async () => {
133
- // Arrange
134
- await CacheModel.create({
135
- id: 'user-123-failed-task',
136
- status: 'failed',
137
- userId: 'user-123',
138
- cacheKey: 'pluginTask-piiRedaction'
139
- });
140
-
141
- // Act
142
- const result = await pluginHandler.getPluginAsyncTasks({
143
- asyncTaskIds: ['user-123-failed-task']
144
- });
145
-
146
- // Assert - result should contain the task
147
- expect(result).toHaveLength(1);
148
- expect(result[0]).toEqual({ cacheKey: 'pluginTask-piiRedaction', status: 'failed' });
149
-
150
- // Verify task was removed from cache
151
- const remainingTask = await CacheModel.findByPk('user-123-failed-task');
152
- expect(remainingTask).toBeNull();
153
- });
154
-
155
- test('should preserve pending tasks in cache after retrieval', async () => {
156
- // Arrange
157
- await CacheModel.create({
158
- id: 'user-123-pending-task',
159
- status: 'pending',
160
- userId: 'user-123',
161
- cacheKey: 'pluginTask-googleDrive'
162
- });
163
-
164
- // Act
165
- const result = await pluginHandler.getPluginAsyncTasks({
166
- asyncTaskIds: ['user-123-pending-task']
167
- });
168
-
169
- // Assert - result should contain the task
170
- expect(result).toHaveLength(1);
171
- expect(result[0]).toEqual({ cacheKey: 'pluginTask-googleDrive', status: 'pending' });
172
-
173
- // Verify task was NOT removed from cache
174
- const remainingTask = await CacheModel.findByPk('user-123-pending-task');
175
- expect(remainingTask).not.toBeNull();
176
- expect(remainingTask.status).toBe('pending');
177
- });
178
-
179
- test('should preserve processing tasks in cache after retrieval', async () => {
180
- // Arrange
181
- await CacheModel.create({
182
- id: 'user-123-processing-task',
183
- status: 'processing',
184
- userId: 'user-123',
185
- cacheKey: 'pluginTask-piiRedaction'
186
- });
187
-
188
- // Act
189
- const result = await pluginHandler.getPluginAsyncTasks({
190
- asyncTaskIds: ['user-123-processing-task']
191
- });
192
-
193
- // Assert - result should contain the task
194
- expect(result).toHaveLength(1);
195
- expect(result[0]).toEqual({ cacheKey: 'pluginTask-piiRedaction', status: 'processing' });
196
-
197
- // Verify task was NOT removed from cache
198
- const remainingTask = await CacheModel.findByPk('user-123-processing-task');
199
- expect(remainingTask).not.toBeNull();
200
- expect(remainingTask.status).toBe('processing');
201
- });
202
-
203
- test('should handle mixed task statuses - remove completed/failed but preserve pending/processing', async () => {
204
- // Arrange
205
- await CacheModel.create({
206
- id: 'task-completed',
207
- status: 'completed',
208
- userId: 'user-123',
209
- cacheKey: 'pluginTask-1'
210
- });
211
- await CacheModel.create({
212
- id: 'task-failed',
213
- status: 'failed',
214
- userId: 'user-123',
215
- cacheKey: 'pluginTask-2'
216
- });
217
- await CacheModel.create({
218
- id: 'task-pending',
219
- status: 'pending',
220
- userId: 'user-123',
221
- cacheKey: 'pluginTask-3'
222
- });
223
- await CacheModel.create({
224
- id: 'task-processing',
225
- status: 'processing',
226
- userId: 'user-123',
227
- cacheKey: 'pluginTask-4'
228
- });
229
-
230
- // Act
231
- const result = await pluginHandler.getPluginAsyncTasks({
232
- asyncTaskIds: ['task-completed', 'task-failed', 'task-pending', 'task-processing']
233
- });
234
-
235
- // Assert - all tasks should be in result
236
- expect(result).toHaveLength(4);
237
-
238
- // Verify completed and failed tasks were removed
239
- expect(await CacheModel.findByPk('task-completed')).toBeNull();
240
- expect(await CacheModel.findByPk('task-failed')).toBeNull();
241
-
242
- // Verify pending and processing tasks were preserved
243
- expect(await CacheModel.findByPk('task-pending')).not.toBeNull();
244
- expect(await CacheModel.findByPk('task-processing')).not.toBeNull();
245
- });
246
-
247
- test('should handle empty asyncTaskIds array', async () => {
248
- // Arrange
249
- await CacheModel.create({
250
- id: 'some-task',
251
- status: 'completed',
252
- userId: 'user-123',
253
- cacheKey: 'pluginTask-test'
254
- });
255
-
256
- // Act
257
- const result = await pluginHandler.getPluginAsyncTasks({
258
- asyncTaskIds: []
259
- });
260
-
261
- // Assert
262
- expect(result).toEqual([]);
263
-
264
- // Verify existing task was not touched
265
- const existingTask = await CacheModel.findByPk('some-task');
266
- expect(existingTask).not.toBeNull();
267
- });
268
-
269
- test('should preserve initialized status tasks in cache', async () => {
270
- // Arrange
271
- await CacheModel.create({
272
- id: 'user-123-initialized-task',
273
- status: 'initialized',
274
- userId: 'user-123',
275
- cacheKey: 'pluginTask-googleDrive'
276
- });
277
-
278
- // Act
279
- const result = await pluginHandler.getPluginAsyncTasks({
280
- asyncTaskIds: ['user-123-initialized-task']
281
- });
282
-
283
- // Assert
284
- expect(result).toHaveLength(1);
285
- expect(result[0]).toEqual({ cacheKey: 'pluginTask-googleDrive', status: 'initialized' });
286
-
287
- // Verify task was NOT removed from cache
288
- const remainingTask = await CacheModel.findByPk('user-123-initialized-task');
289
- expect(remainingTask).not.toBeNull();
290
- });
291
- });
292
-
293
37
  describe('registerPluginAccount', () => {
294
38
  test('should register plugin account and persist plugin jwt token in account data', async () => {
295
39
  const rcAccountId = '12345';
@@ -8,6 +8,9 @@ jest.mock('../lib/jwt', () => ({
8
8
  jest.mock('../handlers/auth', () => ({
9
9
  authValidation: jest.fn(),
10
10
  }));
11
+ jest.mock('../handlers/log', () => ({
12
+ handleAsyncPluginCallback: jest.fn(),
13
+ }));
11
14
  jest.mock('../lib/analytics', () => ({
12
15
  init: jest.fn(),
13
16
  track: jest.fn(),
@@ -15,6 +18,7 @@ jest.mock('../lib/analytics', () => ({
15
18
 
16
19
  const jwt = require('../lib/jwt');
17
20
  const authCore = require('../handlers/auth');
21
+ const logCore = require('../handlers/log');
18
22
  const { createCoreRouter, createCoreMiddleware } = require('../index');
19
23
 
20
24
  function buildApp() {
@@ -101,5 +105,32 @@ describe('Core Router JWT normalization', () => {
101
105
  expect(response.status).toBe(404);
102
106
  expect(jwt.decodeJwt).not.toHaveBeenCalled();
103
107
  });
108
+
109
+ test('should route plugin async callbacks by task id', async () => {
110
+ logCore.handleAsyncPluginCallback.mockResolvedValue({
111
+ statusCode: 200,
112
+ body: { successful: true },
113
+ });
114
+ const app = buildApp();
115
+
116
+ const response = await request(app)
117
+ .post('/plugin/async-callback/task-123')
118
+ .send({
119
+ successful: true,
120
+ message: 'Done',
121
+ note: 'Callback note',
122
+ });
123
+
124
+ expect(response.status).toBe(200);
125
+ expect(response.body).toEqual({ successful: true });
126
+ expect(logCore.handleAsyncPluginCallback).toHaveBeenCalledWith({
127
+ taskId: 'task-123',
128
+ body: {
129
+ successful: true,
130
+ message: 'Done',
131
+ note: 'Callback note',
132
+ },
133
+ });
134
+ });
104
135
  });
105
136
 
@@ -0,0 +1,73 @@
1
+ const { handleMcpRequest } = require('../../mcp/mcpHandler');
2
+
3
+ function mockResponse() {
4
+ return {
5
+ status: jest.fn().mockReturnThis(),
6
+ json: jest.fn(),
7
+ end: jest.fn(),
8
+ };
9
+ }
10
+
11
+ describe('MCP Handler', () => {
12
+ beforeEach(() => {
13
+ jest.clearAllMocks();
14
+ });
15
+
16
+ test('tools/list includes output schemas for registered tools', async () => {
17
+ const req = {
18
+ body: {
19
+ jsonrpc: '2.0',
20
+ id: 1,
21
+ method: 'tools/list',
22
+ },
23
+ headers: {},
24
+ };
25
+ const res = mockResponse();
26
+
27
+ await handleMcpRequest(req, res);
28
+
29
+ expect(res.status).toHaveBeenCalledWith(200);
30
+ const response = res.json.mock.calls[0][0];
31
+ const getHelp = response.result.tools.find(tool => tool.name === 'getHelp');
32
+ const getPublicConnectors = response.result.tools.find(tool => tool.name === 'getPublicConnectors');
33
+
34
+ expect(getHelp.outputSchema).toEqual(expect.objectContaining({
35
+ type: 'object',
36
+ required: ['success', 'data'],
37
+ }));
38
+ expect(getPublicConnectors.outputSchema.properties).toHaveProperty('serverUrl');
39
+ });
40
+
41
+ test('tools/call returns structuredContent matching the tool output', async () => {
42
+ const req = {
43
+ body: {
44
+ jsonrpc: '2.0',
45
+ id: 2,
46
+ method: 'tools/call',
47
+ params: {
48
+ name: 'getHelp',
49
+ arguments: {},
50
+ },
51
+ },
52
+ headers: {},
53
+ };
54
+ const res = mockResponse();
55
+
56
+ await handleMcpRequest(req, res);
57
+
58
+ expect(res.status).toHaveBeenCalledWith(200);
59
+ const response = res.json.mock.calls[0][0];
60
+
61
+ expect(response.result.structuredContent).toEqual(expect.objectContaining({
62
+ success: true,
63
+ data: expect.objectContaining({
64
+ overview: expect.any(String),
65
+ steps: expect.any(Array),
66
+ }),
67
+ }));
68
+ expect(response.result.content[0]).toEqual(expect.objectContaining({
69
+ type: 'text',
70
+ text: expect.stringContaining('"success": true'),
71
+ }));
72
+ });
73
+ });