data-primals-engine 1.7.0 → 1.7.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/README.md +160 -160
- package/client/package-lock.json +1080 -212
- package/client/package.json +12 -6
- package/client/src/AssistantChat.jsx +1 -3
- package/client/src/DataLayout.jsx +19 -20
- package/client/src/DataTable.jsx +2 -2
- package/client/src/DocumentationPageLayout.scss +1 -1
- package/client/src/ViewSwitcher.jsx +1 -1
- package/client/vite.config.js +31 -30
- package/package.json +39 -23
- package/src/ai.jobs.js +135 -0
- package/src/constants.js +561 -545
- package/src/core.js +487 -477
- package/src/data.js +2 -0
- package/src/email.js +0 -2
- package/src/engine.js +50 -42
- package/src/filter.js +348 -343
- package/src/modules/assistant/assistant.js +782 -763
- package/src/modules/assistant/constants.js +23 -16
- package/src/modules/assistant/providers.js +77 -37
- package/src/modules/bucket.js +4 -0
- package/src/modules/data/data.cluster.js +191 -0
- package/src/modules/data/data.core.js +11 -8
- package/src/modules/data/data.js +13 -4
- package/src/modules/data/data.operations.js +186 -106
- package/src/modules/data/data.relations.js +1 -0
- package/src/modules/data/data.replication.js +83 -0
- package/src/modules/data/data.routes.js +2183 -1879
- package/src/modules/mongodb.js +76 -73
- package/src/modules/user.js +7 -1
- package/src/modules/worker-script-runner.js +97 -0
- package/src/modules/workflow.js +1953 -1815
- package/src/packs.js +5701 -5697
- package/src/providers.js +298 -297
- package/test/assistant.test.js +207 -206
- package/test/data.integration.test.js +1425 -1416
- package/test/import_export.integration.test.js +210 -210
- package/test/workflow.actions.integration.test.js +487 -475
- package/test/workflow.integration.test.js +332 -329
|
@@ -1,476 +1,488 @@
|
|
|
1
|
-
import { expect, describe, it, beforeEach, afterEach, beforeAll, afterAll, vi } from 'vitest';
|
|
2
|
-
import { Config } from "../src/config.js";
|
|
3
|
-
import { insertData, editData } from '../src/index.js';
|
|
4
|
-
import {
|
|
5
|
-
modelsCollection as getAppModelsCollection,
|
|
6
|
-
getCollectionForUser,
|
|
7
|
-
getCollection
|
|
8
|
-
} from '../src/modules/mongodb.js';
|
|
9
|
-
import * as workflowModule from '../src/modules/workflow.js';
|
|
10
|
-
import { initEngine } from "../src/setenv.js";
|
|
11
|
-
import * as emailModule from '../src/email.js';
|
|
12
|
-
import { ChatOpenAI } from "@langchain/openai";
|
|
13
|
-
import {ObjectId} from "mongodb";
|
|
14
|
-
import {purgeData} from "../src/modules/data/data.history.js";
|
|
15
|
-
|
|
16
|
-
let testModelsColInstance;
|
|
17
|
-
let testDatasColInstance;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// --- Mocks ---
|
|
21
|
-
// On mock le module email pour ne pas envoyer de vrais emails
|
|
22
|
-
vi.mock('../src/email.js', () => ({
|
|
23
|
-
sendEmail: vi.fn().mockResolvedValue({ success: true })
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
// On mock LangChain pour ne pas faire de vrais appels aux API d'IA
|
|
27
|
-
const mockInvoke = vi.fn().mockResolvedValue({
|
|
28
|
-
content: "Ceci est une réponse IA simulée."
|
|
29
|
-
});
|
|
30
|
-
vi.mock('@langchain/openai', () => ({
|
|
31
|
-
ChatOpenAI: vi.fn(() => mockInvoke)
|
|
32
|
-
}));
|
|
33
|
-
vi.mock('@langchain/google-genai', () => ({
|
|
34
|
-
ChatGoogleGenerativeAI: vi.fn(() => mockInvoke)
|
|
35
|
-
}));
|
|
36
|
-
vi.mock('@langchain/deepseek', () => ({
|
|
37
|
-
ChatDeepSeek: vi.fn(() => mockInvoke)
|
|
38
|
-
}));
|
|
39
|
-
|
|
40
|
-
// On mock le fetch global pour les tests de webhook
|
|
41
|
-
global.fetch = vi.fn();
|
|
42
|
-
|
|
43
|
-
// --- Configuration des Tests ---
|
|
44
|
-
beforeAll(async () => {
|
|
45
|
-
Config.Set('defaultModels', []);
|
|
46
|
-
Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow", "user", "assistant"]);
|
|
47
|
-
await initEngine();
|
|
48
|
-
|
|
49
|
-
testModelsColInstance = getAppModelsCollection
|
|
50
|
-
await testModelsColInstance.deleteMany({_user: mockUser.username});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const mockUser = {
|
|
54
|
-
username: 'testuserWorkflowActions',
|
|
55
|
-
_user: 'testuserWorkflowActions',
|
|
56
|
-
email: 'actions@test.com',
|
|
57
|
-
userPlan: 'premium'
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
// --- Définitions des modèles ---
|
|
61
|
-
const targetDataModel = {
|
|
62
|
-
name: 'task',
|
|
63
|
-
description: "",
|
|
64
|
-
_user: mockUser.username,
|
|
65
|
-
fields: [
|
|
66
|
-
{ name: 'title', type: 'string', required: true },
|
|
67
|
-
{ name: 'status', type: 'string' }, // ex: 'todo', 'done'
|
|
68
|
-
{ name: 'assignee', type: 'string' }
|
|
69
|
-
]
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const logDataModel = {
|
|
73
|
-
name: 'log',
|
|
74
|
-
description: "",
|
|
75
|
-
_user: mockUser.username,
|
|
76
|
-
fields: [
|
|
77
|
-
{ name: 'message', type: 'string' },
|
|
78
|
-
{ name: 'level', type: 'string' }
|
|
79
|
-
]
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const workflowMetaModels = [
|
|
83
|
-
{
|
|
84
|
-
name: "env",
|
|
85
|
-
"description": "",
|
|
86
|
-
_user: mockUser.username,
|
|
87
|
-
fields: [
|
|
88
|
-
{ name: "name", type: "string", required: true, unique: true, asMain: true },
|
|
89
|
-
{ name: "value", type: "string", anonymized: true, hiddenable: true }
|
|
90
|
-
]
|
|
91
|
-
},
|
|
92
|
-
{ name: 'workflow', "description": "", _user: mockUser.username, fields: [{ name: 'name', type: 'string' }, { name: 'startStep', type: 'relation', relation: 'workflowStep' }] },
|
|
93
|
-
{ name: 'workflowStep', "description": "",_user: mockUser.username, fields: [{ name: 'name', type: 'string' }, { name: 'onSuccessStep', type: 'relation', relation: 'workflowStep' }, { name: 'onFailureStep', type: 'relation', relation: 'workflowStep' }, { name: 'isTerminal', type: 'boolean' }, { name: 'actions', type: 'array', itemsType: 'relation', relation: 'workflowAction' }, { name: 'conditions', type: 'object' }] },
|
|
94
|
-
{ name: 'workflowTrigger', "description": "",_user: mockUser.username, fields: [{ name: 'name', type: 'string' }, { name: 'targetModel', type: 'model' }, { name: 'onEvent', type: 'enum', items: ['DataAdded', 'DataEdited', 'DataDeleted'] }, { name: 'isActive', type: 'boolean' }, { name: 'dataFilter', type: 'object' }, { name: 'workflow', type: 'relation', relation: 'workflow' }] },
|
|
95
|
-
{ name: 'workflowRun', "description": "",_user: mockUser.username, fields: [{ name: 'status', type: 'enum', items: ['pending', 'running', 'completed', 'failed', 'cancelled', 'paused'] }, { name: 'workflow', type: 'relation', relation: 'workflow' }, { name: 'contextData', type: 'object' }, { name: 'currentStep', type: 'relation', relation: 'workflowStep' }, { name: 'error', type: 'string' }, { name: 'resumeAt', type: 'datetime' }] },
|
|
96
|
-
{ name: 'workflowAction', "description": "d", _user: mockUser.username, fields: [
|
|
97
|
-
{ name: 'name', type: 'string' },
|
|
98
|
-
{ name: 'type', type: 'enum', items: ['Webhook', 'CreateData', 'UpdateData', 'DeleteData', 'GenerateAIContent', 'SendEmail', 'ExecuteScript', 'Wait'] },
|
|
99
|
-
// Webhook
|
|
100
|
-
{ name: 'url', type: 'url' }, { name: 'method', type: 'enum', items: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] }, { name: 'headers', type: 'code', language: 'json' }, { name: 'body', type: 'code', language: 'json' },
|
|
101
|
-
// Data
|
|
102
|
-
{ name: 'targetModel', type: 'model' }, { name: 'dataToCreate', type: 'code', language: 'json' }, { name: 'targetSelector', type: 'code', language: 'json' }, { name: 'fieldsToUpdate', type: 'code', language: 'json' },
|
|
103
|
-
// AI
|
|
104
|
-
{ name: 'aiProvider', type: 'enum', items: ['OpenAI', 'Google', 'DeepSeek'] }, { name: 'aiModel', type: 'string' }, { name: 'prompt', type: 'richtext' },
|
|
105
|
-
// Email
|
|
106
|
-
{ name: 'emailRecipients', type: 'array', itemsType: 'string' }, { name: 'emailSubject', type: 'string' }, { name: 'emailContent', type: 'richtext' },
|
|
107
|
-
// Script
|
|
108
|
-
{ name: 'script', type: 'code', language: 'javascript' },
|
|
109
|
-
// Wait
|
|
110
|
-
{ name: 'duration', type: 'number' }, { name: 'durationUnit', type: 'enum', items: ['seconds', 'minutes', 'hours', 'days'] }
|
|
111
|
-
] }
|
|
112
|
-
];
|
|
113
|
-
|
|
114
|
-
beforeEach(async () => {
|
|
115
|
-
testDatasColInstance = await getCollectionForUser(mockUser);
|
|
116
|
-
|
|
117
|
-
await testDatasColInstance.deleteMany({_user: mockUser.username});
|
|
118
|
-
await testModelsColInstance.deleteMany({_user: mockUser.username});
|
|
119
|
-
|
|
120
|
-
// Réinitialiser les mocks
|
|
121
|
-
vi.clearAllMocks();
|
|
122
|
-
global.fetch.mockClear();
|
|
123
|
-
mockInvoke.mockClear();
|
|
124
|
-
emailModule.sendEmail.mockClear();
|
|
125
|
-
|
|
126
|
-
// Insérer les modèles si nécessaire
|
|
127
|
-
await testModelsColInstance.insertMany([
|
|
128
|
-
{ ...targetDataModel },
|
|
129
|
-
{ ...logDataModel },
|
|
130
|
-
...workflowMetaModels
|
|
131
|
-
]);
|
|
132
|
-
|
|
133
|
-
// Nettoyer les données de test
|
|
134
|
-
await testDatasColInstance.deleteMany({ _user: mockUser.username });
|
|
135
|
-
|
|
136
|
-
// Utiliser des timers simulés
|
|
137
|
-
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
afterEach(() => {
|
|
141
|
-
vi.runOnlyPendingTimers();
|
|
142
|
-
vi.useRealTimers();
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
afterAll(async () => {
|
|
146
|
-
await purgeData(mockUser);
|
|
147
|
-
const coll = await getCollectionForUser(mockUser);
|
|
148
|
-
await coll.drop();
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
describe('Intégration des Actions de Workflow', () => {
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Helper pour créer une structure de workflow de base pour un test.
|
|
155
|
-
* @param {object} actionDef - La définition de l'action à tester.
|
|
156
|
-
* @returns {Promise<{workflowId: ObjectId, stepId: ObjectId, actionId: ObjectId}>}
|
|
157
|
-
*/
|
|
158
|
-
const setupWorkflow = async (actionDef) => {
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
targetModel: 'task',
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
*
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
await
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
expect(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
expect(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
expect(workflowRun.
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
//
|
|
396
|
-
await
|
|
397
|
-
|
|
398
|
-
//
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
//
|
|
405
|
-
await
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
//
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
}, {}, mockUser, false);
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
1
|
+
import { expect, describe, it, beforeEach, afterEach, beforeAll, afterAll, vi } from 'vitest';
|
|
2
|
+
import { Config } from "../src/config.js";
|
|
3
|
+
import { insertData, editData } from '../src/index.js';
|
|
4
|
+
import {
|
|
5
|
+
modelsCollection as getAppModelsCollection,
|
|
6
|
+
getCollectionForUser,
|
|
7
|
+
getCollection
|
|
8
|
+
} from '../src/modules/mongodb.js';
|
|
9
|
+
import * as workflowModule from '../src/modules/workflow.js';
|
|
10
|
+
import { initEngine } from "../src/setenv.js";
|
|
11
|
+
import * as emailModule from '../src/email.js';
|
|
12
|
+
import { ChatOpenAI } from "@langchain/openai";
|
|
13
|
+
import {ObjectId} from "mongodb";
|
|
14
|
+
import {purgeData} from "../src/modules/data/data.history.js";
|
|
15
|
+
|
|
16
|
+
let testModelsColInstance;
|
|
17
|
+
let testDatasColInstance;
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
// --- Mocks ---
|
|
21
|
+
// On mock le module email pour ne pas envoyer de vrais emails
|
|
22
|
+
vi.mock('../src/email.js', () => ({
|
|
23
|
+
sendEmail: vi.fn().mockResolvedValue({ success: true })
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
// On mock LangChain pour ne pas faire de vrais appels aux API d'IA
|
|
27
|
+
const mockInvoke = vi.fn().mockResolvedValue({
|
|
28
|
+
content: "Ceci est une réponse IA simulée."
|
|
29
|
+
});
|
|
30
|
+
vi.mock('@langchain/openai', () => ({
|
|
31
|
+
ChatOpenAI: vi.fn(() => mockInvoke)
|
|
32
|
+
}));
|
|
33
|
+
vi.mock('@langchain/google-genai', () => ({
|
|
34
|
+
ChatGoogleGenerativeAI: vi.fn(() => mockInvoke)
|
|
35
|
+
}));
|
|
36
|
+
vi.mock('@langchain/deepseek', () => ({
|
|
37
|
+
ChatDeepSeek: vi.fn(() => mockInvoke)
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
// On mock le fetch global pour les tests de webhook
|
|
41
|
+
global.fetch = vi.fn();
|
|
42
|
+
|
|
43
|
+
// --- Configuration des Tests ---
|
|
44
|
+
beforeAll(async () => {
|
|
45
|
+
Config.Set('defaultModels', []);
|
|
46
|
+
Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow", "user", "assistant"]);
|
|
47
|
+
await initEngine();
|
|
48
|
+
|
|
49
|
+
testModelsColInstance = getAppModelsCollection
|
|
50
|
+
await testModelsColInstance.deleteMany({_user: mockUser.username});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const mockUser = {
|
|
54
|
+
username: 'testuserWorkflowActions',
|
|
55
|
+
_user: 'testuserWorkflowActions',
|
|
56
|
+
email: 'actions@test.com',
|
|
57
|
+
userPlan: 'premium'
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// --- Définitions des modèles ---
|
|
61
|
+
const targetDataModel = {
|
|
62
|
+
name: 'task',
|
|
63
|
+
description: "",
|
|
64
|
+
_user: mockUser.username,
|
|
65
|
+
fields: [
|
|
66
|
+
{ name: 'title', type: 'string', required: true },
|
|
67
|
+
{ name: 'status', type: 'string' }, // ex: 'todo', 'done'
|
|
68
|
+
{ name: 'assignee', type: 'string' }
|
|
69
|
+
]
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const logDataModel = {
|
|
73
|
+
name: 'log',
|
|
74
|
+
description: "",
|
|
75
|
+
_user: mockUser.username,
|
|
76
|
+
fields: [
|
|
77
|
+
{ name: 'message', type: 'string', required: true },
|
|
78
|
+
{ name: 'level', type: 'string' },
|
|
79
|
+
]
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const workflowMetaModels = [
|
|
83
|
+
{
|
|
84
|
+
name: "env",
|
|
85
|
+
"description": "",
|
|
86
|
+
_user: mockUser.username,
|
|
87
|
+
fields: [
|
|
88
|
+
{ name: "name", type: "string", required: true, unique: true, asMain: true },
|
|
89
|
+
{ name: "value", type: "string", anonymized: true, hiddenable: true }
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
{ name: 'workflow', "description": "", _user: mockUser.username, fields: [{ name: 'name', type: 'string' }, { name: 'startStep', type: 'relation', relation: 'workflowStep' }] },
|
|
93
|
+
{ name: 'workflowStep', "description": "",_user: mockUser.username, fields: [{ name: 'name', type: 'string' }, { name: 'onSuccessStep', type: 'relation', relation: 'workflowStep' }, { name: 'onFailureStep', type: 'relation', relation: 'workflowStep' }, { name: 'isTerminal', type: 'boolean' }, { name: 'actions', type: 'array', itemsType: 'relation', relation: 'workflowAction' }, { name: 'conditions', type: 'object' }] },
|
|
94
|
+
{ name: 'workflowTrigger', "description": "",_user: mockUser.username, fields: [{ name: 'name', type: 'string' }, { name: 'targetModel', type: 'model' }, { name: 'onEvent', type: 'enum', items: ['DataAdded', 'DataEdited', 'DataDeleted'] }, { name: 'isActive', type: 'boolean' }, { name: 'dataFilter', type: 'object' }, { name: 'workflow', type: 'relation', relation: 'workflow' }] },
|
|
95
|
+
{ name: 'workflowRun', "description": "",_user: mockUser.username, fields: [{ name: 'status', type: 'enum', items: ['pending', 'running', 'completed', 'failed', 'cancelled', 'paused'] }, { name: 'workflow', type: 'relation', relation: 'workflow' }, { name: 'contextData', type: 'object' }, { name: 'currentStep', type: 'relation', relation: 'workflowStep' }, { name: 'error', type: 'string' }, { name: 'resumeAt', type: 'datetime' }] },
|
|
96
|
+
{ name: 'workflowAction', "description": "d", _user: mockUser.username, fields: [
|
|
97
|
+
{ name: 'name', type: 'string' },
|
|
98
|
+
{ name: 'type', type: 'enum', items: ['Webhook', 'CreateData', 'UpdateData', 'DeleteData', 'GenerateAIContent', 'SendEmail', 'ExecuteScript', 'Wait'] },
|
|
99
|
+
// Webhook
|
|
100
|
+
{ name: 'url', type: 'url' }, { name: 'method', type: 'enum', items: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] }, { name: 'headers', type: 'code', language: 'json' }, { name: 'body', type: 'code', language: 'json' },
|
|
101
|
+
// Data
|
|
102
|
+
{ name: 'targetModel', type: 'model' }, { name: 'dataToCreate', type: 'code', language: 'json' }, { name: 'targetSelector', type: 'code', language: 'json' }, { name: 'fieldsToUpdate', type: 'code', language: 'json' },
|
|
103
|
+
// AI
|
|
104
|
+
{ name: 'aiProvider', type: 'enum', items: ['OpenAI', 'Google', 'DeepSeek'] }, { name: 'aiModel', type: 'string' }, { name: 'prompt', type: 'richtext' },
|
|
105
|
+
// Email
|
|
106
|
+
{ name: 'emailRecipients', type: 'array', itemsType: 'string' }, { name: 'emailSubject', type: 'string' }, { name: 'emailContent', type: 'richtext' },
|
|
107
|
+
// Script
|
|
108
|
+
{ name: 'script', type: 'code', language: 'javascript' },
|
|
109
|
+
// Wait
|
|
110
|
+
{ name: 'duration', type: 'number' }, { name: 'durationUnit', type: 'enum', items: ['seconds', 'minutes', 'hours', 'days'] }
|
|
111
|
+
] }
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
beforeEach(async () => {
|
|
115
|
+
testDatasColInstance = await getCollectionForUser(mockUser);
|
|
116
|
+
|
|
117
|
+
await testDatasColInstance.deleteMany({_user: mockUser.username});
|
|
118
|
+
await testModelsColInstance.deleteMany({_user: mockUser.username});
|
|
119
|
+
|
|
120
|
+
// Réinitialiser les mocks
|
|
121
|
+
vi.clearAllMocks();
|
|
122
|
+
global.fetch.mockClear();
|
|
123
|
+
mockInvoke.mockClear();
|
|
124
|
+
emailModule.sendEmail.mockClear();
|
|
125
|
+
|
|
126
|
+
// Insérer les modèles si nécessaire
|
|
127
|
+
await testModelsColInstance.insertMany([
|
|
128
|
+
{ ...targetDataModel },
|
|
129
|
+
{ ...logDataModel },
|
|
130
|
+
...workflowMetaModels
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
// Nettoyer les données de test
|
|
134
|
+
await testDatasColInstance.deleteMany({ _user: mockUser.username });
|
|
135
|
+
|
|
136
|
+
// Utiliser des timers simulés
|
|
137
|
+
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
afterEach(() => {
|
|
141
|
+
vi.runOnlyPendingTimers();
|
|
142
|
+
vi.useRealTimers();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
afterAll(async () => {
|
|
146
|
+
await purgeData(mockUser);
|
|
147
|
+
const coll = await getCollectionForUser(mockUser);
|
|
148
|
+
await coll.drop();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('Intégration des Actions de Workflow', () => {
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Helper pour créer une structure de workflow de base pour un test.
|
|
155
|
+
* @param {object} actionDef - La définition de l'action à tester.
|
|
156
|
+
* @returns {Promise<{workflowId: ObjectId, stepId: ObjectId, actionId: ObjectId}>}
|
|
157
|
+
*/
|
|
158
|
+
const setupWorkflow = async (actionDef) => {
|
|
159
|
+
// If the actionDef provides a startStep, it's a complex setup, and we don't create a trigger automatically.
|
|
160
|
+
const isComplexSetup = !!actionDef.startStep;
|
|
161
|
+
|
|
162
|
+
const actionRes = await insertData('workflowAction', actionDef, {}, mockUser, false);
|
|
163
|
+
const actionId = actionRes.insertedIds[0];
|
|
164
|
+
|
|
165
|
+
const stepId = isComplexSetup ? actionDef.startStep : (await insertData('workflowStep', { name: 'Test Step', actions: [actionId.toString()], isTerminal: true }, {}, mockUser, false)).insertedIds[0];
|
|
166
|
+
|
|
167
|
+
const workflowRes = await insertData('workflow', { name: 'Test Workflow', startStep: stepId.toString() }, {}, mockUser, false);
|
|
168
|
+
const workflowId = workflowRes.insertedIds[0];
|
|
169
|
+
|
|
170
|
+
// For simple setups, create the trigger that links the event to the workflow.
|
|
171
|
+
if (!isComplexSetup) {
|
|
172
|
+
await insertData('workflowTrigger', { name: `Trigger for ${actionDef.name}`, targetModel: 'task', onEvent: 'DataAdded', isActive: true, workflow: workflowId.toString() }, {}, mockUser, false);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { workflowId, stepId, actionId };
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Helper pour lancer un workflow et attendre sa complétion.
|
|
180
|
+
* @param {ObjectId} workflowId - L'ID du workflow à lancer.
|
|
181
|
+
* @param {object} triggerData - Les données de déclenchement.
|
|
182
|
+
* @returns {Promise<object>} Le document workflowRun final.
|
|
183
|
+
*/
|
|
184
|
+
const runWorkflowAndWait = async (workflowId, triggerData) => {
|
|
185
|
+
await workflowModule.triggerWorkflows(triggerData, mockUser, 'DataAdded');
|
|
186
|
+
await vi.runAllTimersAsync(); // Exécute les timers (setTimeout(0) dans triggerWorkflows)
|
|
187
|
+
|
|
188
|
+
// Attendre que le workflowRun soit complété
|
|
189
|
+
let workflowRun = await testDatasColInstance.findOne({ _model: 'workflowRun' });
|
|
190
|
+
return workflowRun;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
it('Action CreateData: devrait créer un document avec des données du contexte', async () => {
|
|
194
|
+
const { workflowId } = await setupWorkflow({
|
|
195
|
+
name: 'Create Log Entry',
|
|
196
|
+
type: 'CreateData',
|
|
197
|
+
targetModel: 'log',
|
|
198
|
+
dataToCreate: {
|
|
199
|
+
"message": "New task created: {triggerData.title}",
|
|
200
|
+
"level": "info"
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const triggerTask = { _model: 'task', title: 'Implement Tests', status: 'todo' };
|
|
205
|
+
const workflowRun = await runWorkflowAndWait(workflowId, triggerTask);
|
|
206
|
+
|
|
207
|
+
expect(workflowRun.status).toBe('completed');
|
|
208
|
+
const newLog = await testDatasColInstance.findOne({ _model: 'log' });
|
|
209
|
+
expect(newLog).not.toBeNull();
|
|
210
|
+
expect(newLog.message).toBe('New task created: Implement Tests');
|
|
211
|
+
expect(newLog.level).toBe('info');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('Action UpdateData: devrait mettre à jour un document en utilisant un sélecteur et des données du contexte', async () => {
|
|
215
|
+
const taskRes = await insertData('task', { title: 'Initial Task', status: 'todo' }, {}, mockUser, false);
|
|
216
|
+
const taskId = taskRes.insertedIds[0];
|
|
217
|
+
|
|
218
|
+
const { workflowId } = await setupWorkflow({
|
|
219
|
+
name: 'Update Task Status',
|
|
220
|
+
type: 'UpdateData',
|
|
221
|
+
targetModel: 'task',
|
|
222
|
+
targetSelector: { "_id": "{triggerData._id}" },
|
|
223
|
+
fieldsToUpdate: { "status": "done", "assignee": "{triggerData.assignee}" }
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const triggerData = { _id: taskId, _model: 'task', assignee: 'testuserWorkflowActions' };
|
|
227
|
+
const workflowRun = await runWorkflowAndWait(workflowId, triggerData);
|
|
228
|
+
|
|
229
|
+
expect(workflowRun.status).toBe('completed');
|
|
230
|
+
const updatedTask = await testDatasColInstance.findOne({ _id: new ObjectId(taskId) });
|
|
231
|
+
expect(updatedTask.status).toBe('done');
|
|
232
|
+
expect(updatedTask.assignee).toBe('testuserWorkflowActions');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('Action DeleteData: devrait supprimer un document basé sur un sélecteur', async () => {
|
|
236
|
+
const taskRes = await insertData('task', { title: 'Task to be deleted', status: 'temp' }, {}, mockUser, false);
|
|
237
|
+
const taskId = taskRes.insertedIds[0];
|
|
238
|
+
|
|
239
|
+
const { workflowId } = await setupWorkflow({
|
|
240
|
+
name: 'Delete Temp Task',
|
|
241
|
+
type: 'DeleteData',
|
|
242
|
+
targetModel: 'task',
|
|
243
|
+
targetSelector: { "_id": "{triggerData._id}" }
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const workflowRun = await runWorkflowAndWait(workflowId, { _id: taskId, _model: 'task' });
|
|
247
|
+
|
|
248
|
+
expect(workflowRun.status).toBe('completed');
|
|
249
|
+
const deletedTask = await testDatasColInstance.findOne({ _id: taskId });
|
|
250
|
+
expect(deletedTask).toBeNull();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('Action HttpRequest: devrait appeler fetch avec les bonnes informations substituées', async () => {
|
|
254
|
+
global.fetch.mockResolvedValue({
|
|
255
|
+
ok: true,
|
|
256
|
+
status: 200,
|
|
257
|
+
json: async () => ({ success: true, message: 'Webhook received' }),
|
|
258
|
+
headers: new Map([['content-type', 'application/json']])
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const { workflowId } = await setupWorkflow({
|
|
262
|
+
name: 'Notify External System',
|
|
263
|
+
type: 'HttpRequest',
|
|
264
|
+
method: 'POST',
|
|
265
|
+
url: 'https://api.example.com/notify/{triggerData.status}',
|
|
266
|
+
headers: { "Authorization": "Bearer {env.API_KEY}" },
|
|
267
|
+
body: { "taskId": "{triggerData._id}", "title": "{triggerData.title}" }
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Simuler une variable d'environnement
|
|
271
|
+
await insertData('env', { name: 'API_KEY', value: 'secret123' }, {}, mockUser, false);
|
|
272
|
+
|
|
273
|
+
const triggerData = { _id: 'task_123', _model: 'task', title: 'My Webhook Task', status: 'done' };
|
|
274
|
+
const workflowRun = await runWorkflowAndWait(workflowId, triggerData);
|
|
275
|
+
|
|
276
|
+
expect(workflowRun.status).toBe('completed');
|
|
277
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
278
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
279
|
+
'https://api.example.com/notify/done',
|
|
280
|
+
expect.objectContaining({
|
|
281
|
+
method: 'POST',
|
|
282
|
+
headers: {
|
|
283
|
+
'Authorization': 'Bearer secret123',
|
|
284
|
+
'Content-Type': 'application/json'
|
|
285
|
+
},
|
|
286
|
+
body: JSON.stringify({ taskId: 'task_123', title: 'My Webhook Task' })
|
|
287
|
+
})
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('Action SendEmail: devrait appeler le service email avec un contenu personnalisé', async () => {
|
|
292
|
+
const { workflowId } = await setupWorkflow({
|
|
293
|
+
name: 'Send Task Completion Email',
|
|
294
|
+
type: 'SendEmail',
|
|
295
|
+
emailRecipients: ["{triggerData.assigneeEmail}"],
|
|
296
|
+
emailSubject: "Task Completed: {triggerData.title}",
|
|
297
|
+
emailContent: "<h1>Done!</h1><p>The task '{triggerData.title}' is now complete.</p>"
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const triggerData = { _model: 'task', title: 'Finish Email Action Test', assigneeEmail: 'dev@example.com' };
|
|
301
|
+
const workflowRun = await runWorkflowAndWait(workflowId, triggerData);
|
|
302
|
+
|
|
303
|
+
expect(workflowRun.status).toBe('completed');
|
|
304
|
+
expect(emailModule.sendEmail).toHaveBeenCalledTimes(1);
|
|
305
|
+
expect(emailModule.sendEmail).toHaveBeenCalledWith(
|
|
306
|
+
['dev@example.com'],
|
|
307
|
+
expect.objectContaining({
|
|
308
|
+
title: "Task Completed: Finish Email Action Test",
|
|
309
|
+
content: "<h1>Done!</h1><p>The task 'Finish Email Action Test' is now complete.</p>"
|
|
310
|
+
}),
|
|
311
|
+
expect.any(Object), // smtpConfig
|
|
312
|
+
undefined // lang
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('Action GenerateAIContent: devrait appeler le client IA et ajouter le résultat au contexte', async () => {
|
|
317
|
+
const { workflowId } = await setupWorkflow({
|
|
318
|
+
name: 'Summarize Task',
|
|
319
|
+
type: 'GenerateAIContent',
|
|
320
|
+
aiProvider: 'OpenAI',
|
|
321
|
+
aiModel: 'gpt-4o-mini',
|
|
322
|
+
prompt: "Summarize this task title: {triggerData.title}"
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Simuler une clé API utilisateur
|
|
326
|
+
await insertData('env', { name: 'OPENAI_API_KEY', value: 'user_api_key' }, {}, mockUser, false);
|
|
327
|
+
|
|
328
|
+
const triggerData = { _model: 'task', title: 'A very long and detailed task title that needs summarization' };
|
|
329
|
+
const workflowRun = await runWorkflowAndWait(workflowId, triggerData);
|
|
330
|
+
|
|
331
|
+
expect(workflowRun.status).toBe('completed');
|
|
332
|
+
expect(mockInvoke).toHaveBeenCalledTimes(1);
|
|
333
|
+
expect(workflowRun.contextData.aiContent).toBe("Ceci est une réponse IA simulée.");
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('Action ExecuteScript: devrait exécuter un script et mettre à jour le contexte', async () => {
|
|
337
|
+
const { workflowId } = await setupWorkflow({
|
|
338
|
+
name: 'Process Data with Script',
|
|
339
|
+
type: 'ExecuteScript',
|
|
340
|
+
script: `
|
|
341
|
+
const title = context.triggerData.title.toUpperCase();
|
|
342
|
+
const status = 'processed';
|
|
343
|
+
|
|
344
|
+
// Créer un log
|
|
345
|
+
await db.create('log', { "message": "Processing "+title});
|
|
346
|
+
|
|
347
|
+
// Retourner des données à ajouter au contexte
|
|
348
|
+
return { processedTitle: title, newStatus: status };
|
|
349
|
+
`
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const triggerData = { _model: 'task', title: 'script test' };
|
|
353
|
+
const workflowRun = await runWorkflowAndWait(workflowId, triggerData);
|
|
354
|
+
|
|
355
|
+
expect(workflowRun.status).toBe('completed');
|
|
356
|
+
// Vérifier que le contexte a été mis à jour par le `return` du script
|
|
357
|
+
expect(workflowRun.contextData.result.processedTitle).toBe('SCRIPT TEST');
|
|
358
|
+
expect(workflowRun.contextData.result.newStatus).toBe('processed');
|
|
359
|
+
|
|
360
|
+
// Vérifier que l'action `db.create` dans le script a fonctionné
|
|
361
|
+
const logEntry = await testDatasColInstance.findOne({ _model: 'log' });
|
|
362
|
+
expect(logEntry).not.toBeNull();
|
|
363
|
+
expect(logEntry.message).toBe('Processing SCRIPT TEST');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('Action Wait: devrait mettre le workflow en pause puis le reprendre', async () => {
|
|
367
|
+
// Création d'une étape de fin pour vérifier la reprise
|
|
368
|
+
vi.useFakeTimers();
|
|
369
|
+
|
|
370
|
+
const finalStepRes = await insertData('workflowStep', { name: 'Final Step', isTerminal: true }, {}, mockUser, false);
|
|
371
|
+
const finalStepId = finalStepRes.insertedIds[0];
|
|
372
|
+
|
|
373
|
+
const actionRes = await insertData('workflowAction', { name: 'Wait Action', type: 'Wait', duration: 2, durationUnit: 'seconds' }, {}, mockUser, false);
|
|
374
|
+
const actionId = actionRes.insertedIds[0];
|
|
375
|
+
|
|
376
|
+
const waitStepRes = await insertData('workflowStep', { name: 'Wait Step', actions: [actionId.toString()], onSuccessStep: finalStepId.toString() }, {}, mockUser, false);
|
|
377
|
+
const waitStepId = waitStepRes.insertedIds[0];
|
|
378
|
+
|
|
379
|
+
const workflowRes = await insertData('workflow', { name: 'Wait Workflow', startStep: waitStepId.toString() }, {}, mockUser, false);
|
|
380
|
+
const workflowId = workflowRes.insertedIds[0];
|
|
381
|
+
|
|
382
|
+
// Le trigger était manquant pour ce test spécifique.
|
|
383
|
+
// On l'ajoute ici, comme le fait la fonction `setupWorkflow`.
|
|
384
|
+
await insertData('workflowTrigger', {
|
|
385
|
+
name: 'Trigger for Wait Test',
|
|
386
|
+
targetModel: 'task',
|
|
387
|
+
onEvent: 'DataAdded',
|
|
388
|
+
isActive: true,
|
|
389
|
+
workflow: workflowId.toString()
|
|
390
|
+
}, {}, mockUser, false);
|
|
391
|
+
|
|
392
|
+
// Lancement du workflow
|
|
393
|
+
await workflowModule.triggerWorkflows({ _model: 'task', title: 'wait test' }, mockUser, 'DataAdded');
|
|
394
|
+
|
|
395
|
+
// 1. Vérifier que le workflow est en pause
|
|
396
|
+
let workflowRun = await testDatasColInstance.findOne({ _model: 'workflowRun' });
|
|
397
|
+
expect(workflowRun.status).toBe('paused');
|
|
398
|
+
expect(workflowRun.currentStep.toString()).toBe(finalStepId.toString()); // Il est prêt pour la prochaine étape
|
|
399
|
+
|
|
400
|
+
vi.advanceTimersByTime(4000);
|
|
401
|
+
// 2. Simuler manuellement la reprise du workflow
|
|
402
|
+
await workflowModule.processWorkflowRun(workflowRun._id, mockUser);
|
|
403
|
+
|
|
404
|
+
// 3. Vérifier que le workflow s'est terminé
|
|
405
|
+
workflowRun = await testDatasColInstance.findOne({ _id: workflowRun._id });
|
|
406
|
+
expect(workflowRun.status).toBe('completed');
|
|
407
|
+
expect(workflowRun.currentStep).toBeNull();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('Chemin d\'échec (onFailureStep): devrait suivre la branche d\'échec si une action échoue', async () => {
|
|
411
|
+
// Créer deux étapes terminales: une pour le succès, une pour l'échec
|
|
412
|
+
const successStepRes = await insertData('workflowStep', { name: 'Success Step', isTerminal: true }, {}, mockUser, false);
|
|
413
|
+
const failureStepRes = await insertData('workflowStep', { name: 'Failure Step', isTerminal: true }, {}, mockUser, false);
|
|
414
|
+
|
|
415
|
+
// Créer une action qui va échouer (CreateData sans champ requis 'message')
|
|
416
|
+
const failingActionRes = await insertData('workflowAction', {
|
|
417
|
+
name: 'Failing Create Log',
|
|
418
|
+
type: 'CreateData',
|
|
419
|
+
targetModel: 'log',
|
|
420
|
+
dataToCreate: { "level": "error" } // 'message' est requis dans le modèle 'log' et est manquant ici
|
|
421
|
+
}, {}, mockUser, false);
|
|
422
|
+
|
|
423
|
+
// Créer l'étape principale qui utilise cette action et les branches de succès/échec
|
|
424
|
+
const mainStepRes = await insertData('workflowStep', {
|
|
425
|
+
name: 'Main Step',
|
|
426
|
+
actions: [failingActionRes.insertedIds[0].toString()],
|
|
427
|
+
onSuccessStep: successStepRes.insertedIds[0].toString(),
|
|
428
|
+
onFailureStep: failureStepRes.insertedIds[0].toString()
|
|
429
|
+
}, {}, mockUser, false);
|
|
430
|
+
|
|
431
|
+
const { workflowId } = await setupWorkflow({
|
|
432
|
+
name: 'Workflow with Failure Path',
|
|
433
|
+
startStep: mainStepRes.insertedIds[0].toString()
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// The setupWorkflow helper skips trigger creation for complex setups (when startStep is provided).
|
|
437
|
+
// We must create the trigger manually for this test.
|
|
438
|
+
await insertData('workflowTrigger', {
|
|
439
|
+
name: 'Trigger for Failure Path Test',
|
|
440
|
+
targetModel: 'task',
|
|
441
|
+
onEvent: 'DataAdded',
|
|
442
|
+
isActive: true,
|
|
443
|
+
workflow: workflowId.toString()
|
|
444
|
+
}, {}, mockUser, false);
|
|
445
|
+
|
|
446
|
+
const workflowRun = await runWorkflowAndWait(workflowId, { _model: 'task', title: 'Test failure path' });
|
|
447
|
+
// The workflow should follow the failure path, which leads to a terminal step.
|
|
448
|
+
// Because the action failed and there is a failure path, the overall run status is 'completed'
|
|
449
|
+
// because it successfully reached the terminal "Failure Step".
|
|
450
|
+
expect(workflowRun.status).toBe('completed'); // The workflow itself completed its run.
|
|
451
|
+
expect(workflowRun.currentStep).toBeNull(); // It correctly ended on a terminal step.
|
|
452
|
+
expect(workflowRun.history.some(h => h.stepName === 'Failure Step')).toBe(true);
|
|
453
|
+
expect(workflowRun.history.some(h => h.stepName === 'Success Step')).toBe(false);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('Trigger dataFilter: ne devrait lancer le workflow que si le filtre correspond', async () => {
|
|
457
|
+
// Création d'une étape de fin pour vérifier la reprise
|
|
458
|
+
vi.useFakeTimers();
|
|
459
|
+
|
|
460
|
+
// Créer une action et une étape simples
|
|
461
|
+
const actionRes = await insertData('workflowAction', { name: 'Create Log', type: 'CreateData', targetModel: 'log', dataToCreate: { message: 'Filtered task processed' } }, {}, mockUser, false);
|
|
462
|
+
const stepRes = await insertData('workflowStep', { name: 'Step', actions: [actionRes.insertedIds[0].toString()], isTerminal: true }, {}, mockUser, false);
|
|
463
|
+
const workflowRes = await insertData('workflow', { name: 'Filtered Workflow', startStep: stepRes.insertedIds[0].toString() }, {}, mockUser, false);
|
|
464
|
+
|
|
465
|
+
// Créer un trigger avec un dataFilter
|
|
466
|
+
await insertData('workflowTrigger', {
|
|
467
|
+
name: 'Trigger only for "done" tasks',
|
|
468
|
+
targetModel: 'task',
|
|
469
|
+
onEvent: 'DataAdded',
|
|
470
|
+
isActive: true,
|
|
471
|
+
workflow: workflowRes.insertedIds[0].toString(),
|
|
472
|
+
dataFilter: { "$eq": ["$status","done"] } // Le filtre crucial
|
|
473
|
+
}, {}, mockUser, false);
|
|
474
|
+
|
|
475
|
+
// 1. Déclencher avec une donnée qui NE correspond PAS au filtre
|
|
476
|
+
await workflowModule.triggerWorkflows({ _model: 'task', title: 'A task not done', status: 'todo' }, mockUser, 'DataAdded');
|
|
477
|
+
let runs = await testDatasColInstance.find({ _model: 'workflowRun' }).toArray();
|
|
478
|
+
expect(runs.length).toBe(0); // Aucun workflow ne doit avoir été lancé
|
|
479
|
+
|
|
480
|
+
vi.advanceTimersByTime(2000);
|
|
481
|
+
|
|
482
|
+
// 2. Déclencher avec une donnée qui correspond au filtre
|
|
483
|
+
await workflowModule.triggerWorkflows({ _model: 'task', title: 'A task that is done', status: 'done' }, mockUser, 'DataAdded');
|
|
484
|
+
runs = await testDatasColInstance.find({ _model: 'workflowRun' }).toArray();
|
|
485
|
+
expect(runs.length).toBe(1); // Un seul workflow doit avoir été lancé
|
|
486
|
+
expect(runs[0].status).toBe('completed');
|
|
487
|
+
});
|
|
476
488
|
});
|