@app-connect/core 1.7.30 → 1.7.33
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/connector/developerPortal.js +21 -1
- package/docs/connectors.md +1 -1
- package/docs/handlers.md +11 -8
- package/docs/libraries.md +0 -3
- package/docs/models.md +5 -5
- package/docs/routes.md +5 -1
- package/handlers/log.js +431 -167
- package/handlers/plugin.js +0 -24
- package/handlers/user.js +96 -9
- package/index.js +58 -46
- package/mcp/README.md +8 -0
- package/mcp/mcpHandler.js +126 -21
- package/package.json +1 -1
- package/releaseNotes.json +24 -0
- package/test/handlers/log.test.js +225 -0
- package/test/handlers/plugin.test.js +0 -256
- package/test/index.test.js +31 -0
- package/test/mcp/mcpHandler.test.js +73 -0
|
@@ -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';
|
package/test/index.test.js
CHANGED
|
@@ -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
|
+
});
|