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.
Files changed (39) hide show
  1. package/README.md +160 -160
  2. package/client/package-lock.json +1080 -212
  3. package/client/package.json +12 -6
  4. package/client/src/AssistantChat.jsx +1 -3
  5. package/client/src/DataLayout.jsx +19 -20
  6. package/client/src/DataTable.jsx +2 -2
  7. package/client/src/DocumentationPageLayout.scss +1 -1
  8. package/client/src/ViewSwitcher.jsx +1 -1
  9. package/client/vite.config.js +31 -30
  10. package/package.json +39 -23
  11. package/src/ai.jobs.js +135 -0
  12. package/src/constants.js +561 -545
  13. package/src/core.js +487 -477
  14. package/src/data.js +2 -0
  15. package/src/email.js +0 -2
  16. package/src/engine.js +50 -42
  17. package/src/filter.js +348 -343
  18. package/src/modules/assistant/assistant.js +782 -763
  19. package/src/modules/assistant/constants.js +23 -16
  20. package/src/modules/assistant/providers.js +77 -37
  21. package/src/modules/bucket.js +4 -0
  22. package/src/modules/data/data.cluster.js +191 -0
  23. package/src/modules/data/data.core.js +11 -8
  24. package/src/modules/data/data.js +13 -4
  25. package/src/modules/data/data.operations.js +186 -106
  26. package/src/modules/data/data.relations.js +1 -0
  27. package/src/modules/data/data.replication.js +83 -0
  28. package/src/modules/data/data.routes.js +2183 -1879
  29. package/src/modules/mongodb.js +76 -73
  30. package/src/modules/user.js +7 -1
  31. package/src/modules/worker-script-runner.js +97 -0
  32. package/src/modules/workflow.js +1953 -1815
  33. package/src/packs.js +5701 -5697
  34. package/src/providers.js +298 -297
  35. package/test/assistant.test.js +207 -206
  36. package/test/data.integration.test.js +1425 -1416
  37. package/test/import_export.integration.test.js +210 -210
  38. package/test/workflow.actions.integration.test.js +487 -475
  39. 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
- const actionRes = await insertData('workflowAction', actionDef, {}, mockUser, false);
160
- const actionId = actionRes.insertedIds[0];
161
-
162
- const stepRes = await insertData('workflowStep', { name: 'Test Step', actions: [actionId.toString()], isTerminal: true }, {}, mockUser, false);
163
- const stepId = stepRes.insertedIds[0];
164
-
165
- const workflowRes = await insertData('workflow', { name: 'Test Workflow', startStep: stepId.toString() }, {}, mockUser, false);
166
- const workflowId = workflowRes.insertedIds[0];
167
-
168
- // AJOUT : Créer le déclencheur qui lie l'événement au workflow.
169
- // C'est l'élément manquant qui empêchait les workflows de se lancer.
170
- await insertData('workflowTrigger', {
171
- name: `Trigger for ${actionDef.name}`,
172
- targetModel: 'task', // Tous les tests se déclenchent sur le modèle 'task'
173
- onEvent: 'DataAdded', // Tous les tests utilisent cet événement
174
- isActive: true,
175
- workflow: workflowId.toString()
176
- }, {}, mockUser, false);
177
-
178
- return { workflowId, stepId, actionId };
179
- };
180
-
181
- /**
182
- * Helper pour lancer un workflow et attendre sa complétion.
183
- * @param {ObjectId} workflowId - L'ID du workflow à lancer.
184
- * @param {object} triggerData - Les données de déclenchement.
185
- * @returns {Promise<object>} Le document workflowRun final.
186
- */
187
- const runWorkflowAndWait = async (workflowId, triggerData) => {
188
- await workflowModule.triggerWorkflows(triggerData, mockUser, 'DataAdded');
189
- await vi.runAllTimersAsync(); // Exécute les timers (setTimeout(0) dans triggerWorkflows)
190
-
191
- // Attendre que le workflowRun soit complété
192
- let workflowRun = await testDatasColInstance.findOne({ _model: 'workflowRun' });
193
- return workflowRun;
194
- };
195
-
196
- it('Action CreateData: devrait créer un document avec des données du contexte', async () => {
197
- const { workflowId } = await setupWorkflow({
198
- name: 'Create Log Entry',
199
- type: 'CreateData',
200
- targetModel: 'log',
201
- dataToCreate: {
202
- "message": "New task created: {triggerData.title}",
203
- "level": "info"
204
- }
205
- });
206
-
207
- const triggerTask = { _model: 'task', title: 'Implement Tests', status: 'todo' };
208
- const workflowRun = await runWorkflowAndWait(workflowId, triggerTask);
209
-
210
- expect(workflowRun.status).toBe('completed');
211
- const newLog = await testDatasColInstance.findOne({ _model: 'log' });
212
- expect(newLog).not.toBeNull();
213
- expect(newLog.message).toBe('New task created: Implement Tests');
214
- expect(newLog.level).toBe('info');
215
- });
216
-
217
- it('Action UpdateData: devrait mettre à jour un document en utilisant un sélecteur et des données du contexte', async () => {
218
- const taskRes = await insertData('task', { title: 'Initial Task', status: 'todo' }, {}, mockUser, false);
219
- const taskId = taskRes.insertedIds[0];
220
-
221
- const { workflowId } = await setupWorkflow({
222
- name: 'Update Task Status',
223
- type: 'UpdateData',
224
- targetModel: 'task',
225
- targetSelector: { "_id": "{triggerData._id}" },
226
- fieldsToUpdate: { "status": "done", "assignee": "{triggerData.assignee}" }
227
- });
228
-
229
- const triggerData = { _id: taskId, _model: 'task', assignee: 'testuserWorkflowActions' };
230
- const workflowRun = await runWorkflowAndWait(workflowId, triggerData);
231
-
232
- expect(workflowRun.status).toBe('completed');
233
- const updatedTask = await testDatasColInstance.findOne({ _id: new ObjectId(taskId) });
234
- expect(updatedTask.status).toBe('done');
235
- expect(updatedTask.assignee).toBe('testuserWorkflowActions');
236
- });
237
-
238
- it('Action DeleteData: devrait supprimer un document basé sur un sélecteur', async () => {
239
- const taskRes = await insertData('task', { title: 'Task to be deleted', status: 'temp' }, {}, mockUser, false);
240
- const taskId = taskRes.insertedIds[0];
241
-
242
- const { workflowId } = await setupWorkflow({
243
- name: 'Delete Temp Task',
244
- type: 'DeleteData',
245
- targetModel: 'task',
246
- targetSelector: { "_id": "{triggerData._id}" }
247
- });
248
-
249
- const workflowRun = await runWorkflowAndWait(workflowId, { _id: taskId, _model: 'task' });
250
-
251
- expect(workflowRun.status).toBe('completed');
252
- const deletedTask = await testDatasColInstance.findOne({ _id: taskId });
253
- expect(deletedTask).toBeNull();
254
- });
255
-
256
- it('Action HttpRequest: devrait appeler fetch avec les bonnes informations substituées', async () => {
257
- global.fetch.mockResolvedValue({
258
- ok: true,
259
- status: 200,
260
- json: async () => ({ success: true, message: 'Webhook received' }),
261
- headers: new Map([['content-type', 'application/json']])
262
- });
263
-
264
- const { workflowId } = await setupWorkflow({
265
- name: 'Notify External System',
266
- type: 'HttpRequest',
267
- method: 'POST',
268
- url: 'https://api.example.com/notify/{triggerData.status}',
269
- headers: { "Authorization": "Bearer {env.API_KEY}" },
270
- body: { "taskId": "{triggerData._id}", "title": "{triggerData.title}" }
271
- });
272
-
273
- // Simuler une variable d'environnement
274
- await insertData('env', { name: 'API_KEY', value: 'secret123' }, {}, mockUser, false);
275
-
276
- const triggerData = { _id: 'task_123', _model: 'task', title: 'My Webhook Task', status: 'done' };
277
- const workflowRun = await runWorkflowAndWait(workflowId, triggerData);
278
-
279
- expect(workflowRun.status).toBe('completed');
280
- expect(global.fetch).toHaveBeenCalledTimes(1);
281
- expect(global.fetch).toHaveBeenCalledWith(
282
- 'https://api.example.com/notify/done',
283
- expect.objectContaining({
284
- method: 'POST',
285
- headers: {
286
- 'Authorization': 'Bearer secret123',
287
- 'Content-Type': 'application/json'
288
- },
289
- body: JSON.stringify({ taskId: 'task_123', title: 'My Webhook Task' })
290
- })
291
- );
292
- });
293
-
294
- it('Action SendEmail: devrait appeler le service email avec un contenu personnalisé', async () => {
295
- const { workflowId } = await setupWorkflow({
296
- name: 'Send Task Completion Email',
297
- type: 'SendEmail',
298
- emailRecipients: ["{triggerData.assigneeEmail}"],
299
- emailSubject: "Task Completed: {triggerData.title}",
300
- emailContent: "<h1>Done!</h1><p>The task '{triggerData.title}' is now complete.</p>"
301
- });
302
-
303
- const triggerData = { _model: 'task', title: 'Finish Email Action Test', assigneeEmail: 'dev@example.com' };
304
- const workflowRun = await runWorkflowAndWait(workflowId, triggerData);
305
-
306
- expect(workflowRun.status).toBe('completed');
307
- expect(emailModule.sendEmail).toHaveBeenCalledTimes(1);
308
- expect(emailModule.sendEmail).toHaveBeenCalledWith(
309
- ['dev@example.com'],
310
- expect.objectContaining({
311
- title: "Task Completed: Finish Email Action Test",
312
- content: "<h1>Done!</h1><p>The task 'Finish Email Action Test' is now complete.</p>"
313
- }),
314
- expect.any(Object), // smtpConfig
315
- undefined // lang
316
- );
317
- });
318
-
319
- it('Action GenerateAIContent: devrait appeler le client IA et ajouter le résultat au contexte', async () => {
320
- const { workflowId } = await setupWorkflow({
321
- name: 'Summarize Task',
322
- type: 'GenerateAIContent',
323
- aiProvider: 'OpenAI',
324
- aiModel: 'gpt-4o-mini',
325
- prompt: "Summarize this task title: {triggerData.title}"
326
- });
327
-
328
- // Simuler une clé API utilisateur
329
- await insertData('env', { name: 'OPENAI_API_KEY', value: 'user_api_key' }, {}, mockUser, false);
330
-
331
- const triggerData = { _model: 'task', title: 'A very long and detailed task title that needs summarization' };
332
- const workflowRun = await runWorkflowAndWait(workflowId, triggerData);
333
-
334
- expect(workflowRun.status).toBe('completed');
335
- expect(mockInvoke).toHaveBeenCalledTimes(1);
336
- expect(workflowRun.contextData.aiContent).toBe("Ceci est une réponse IA simulée.");
337
- });
338
-
339
- it('Action ExecuteScript: devrait exécuter un script et mettre à jour le contexte', async () => {
340
- const { workflowId } = await setupWorkflow({
341
- name: 'Process Data with Script',
342
- type: 'ExecuteScript',
343
- script: `
344
- const title = context.triggerData.title.toUpperCase();
345
- const status = 'processed';
346
-
347
- // Créer un log
348
- await db.create('log', { "message": "Processing "+title});
349
-
350
- // Retourner des données à ajouter au contexte
351
- return { processedTitle: title, newStatus: status };
352
- `
353
- });
354
-
355
- const triggerData = { _model: 'task', title: 'script test' };
356
- const workflowRun = await runWorkflowAndWait(workflowId, triggerData);
357
-
358
- expect(workflowRun.status).toBe('completed');
359
- // Vérifier que le contexte a été mis à jour par le `return` du script
360
- expect(workflowRun.contextData.result.processedTitle).toBe('SCRIPT TEST');
361
- expect(workflowRun.contextData.result.newStatus).toBe('processed');
362
-
363
- // Vérifier que l'action `db.create` dans le script a fonctionné
364
- const logEntry = await testDatasColInstance.findOne({ _model: 'log' });
365
- expect(logEntry).not.toBeNull();
366
- expect(logEntry.message).toBe('Processing SCRIPT TEST');
367
- });
368
-
369
- it('Action Wait: devrait mettre le workflow en pause puis le reprendre', async () => {
370
- // Création d'une étape de fin pour vérifier la reprise
371
- vi.useFakeTimers();
372
-
373
- const finalStepRes = await insertData('workflowStep', { name: 'Final Step', isTerminal: true }, {}, mockUser, false);
374
- const finalStepId = finalStepRes.insertedIds[0];
375
-
376
- const actionRes = await insertData('workflowAction', { name: 'Wait Action', type: 'Wait', duration: 2, durationUnit: 'seconds' }, {}, mockUser, false);
377
- const actionId = actionRes.insertedIds[0];
378
-
379
- const waitStepRes = await insertData('workflowStep', { name: 'Wait Step', actions: [actionId.toString()], onSuccessStep: finalStepId.toString() }, {}, mockUser, false);
380
- const waitStepId = waitStepRes.insertedIds[0];
381
-
382
- const workflowRes = await insertData('workflow', { name: 'Wait Workflow', startStep: waitStepId.toString() }, {}, mockUser, false);
383
- const workflowId = workflowRes.insertedIds[0];
384
-
385
- // Le trigger était manquant pour ce test spécifique.
386
- // On l'ajoute ici, comme le fait la fonction `setupWorkflow`.
387
- await insertData('workflowTrigger', {
388
- name: 'Trigger for Wait Test',
389
- targetModel: 'task',
390
- onEvent: 'DataAdded',
391
- isActive: true,
392
- workflow: workflowId.toString()
393
- }, {}, mockUser, false);
394
-
395
- // Lancement du workflow
396
- await workflowModule.triggerWorkflows({ _model: 'task', title: 'wait test' }, mockUser, 'DataAdded');
397
-
398
- // 1. Vérifier que le workflow est en pause
399
- let workflowRun = await testDatasColInstance.findOne({ _model: 'workflowRun' });
400
- expect(workflowRun.status).toBe('paused');
401
- expect(workflowRun.currentStep.toString()).toBe(finalStepId.toString()); // Il est prêt pour la prochaine étape
402
-
403
- vi.advanceTimersByTime(4000);
404
- // 2. Simuler manuellement la reprise du workflow
405
- await workflowModule.processWorkflowRun(workflowRun._id, mockUser);
406
-
407
- // 3. Vérifier que le workflow s'est terminé
408
- workflowRun = await testDatasColInstance.findOne({ _id: workflowRun._id });
409
- expect(workflowRun.status).toBe('completed');
410
- expect(workflowRun.currentStep).toBeNull();
411
- });
412
-
413
- it('Chemin d\'échec (onFailureStep): devrait suivre la branche d\'échec si une action échoue', async () => {
414
- // Créer deux étapes terminales: une pour le succès, une pour l'échec
415
- const successStepRes = await insertData('workflowStep', { name: 'Success Step', isTerminal: true }, {}, mockUser, false);
416
- const failureStepRes = await insertData('workflowStep', { name: 'Failure Step', isTerminal: true }, {}, mockUser, false);
417
-
418
- // Créer une action qui va échouer (CreateData sans champ requis 'message')
419
- const failingActionRes = await insertData('workflowAction', {
420
- name: 'Failing Create Log',
421
- type: 'CreateData',
422
- targetModel: 'log',
423
- dataToCreate: { "level": "error" } // 'message' est requis dans le modèle 'log' et est manquant ici
424
- }, {}, mockUser, false);
425
-
426
- // Créer l'étape principale qui utilise cette action et les branches de succès/échec
427
- const mainStepRes = await insertData('workflowStep', {
428
- name: 'Main Step',
429
- actions: [failingActionRes.insertedIds[0].toString()],
430
- onSuccessStep: successStepRes.insertedIds[0].toString(),
431
- onFailureStep: failureStepRes.insertedIds[0].toString()
432
- }, {}, mockUser, false);
433
-
434
- const { workflowId } = await setupWorkflow({
435
- name: 'Workflow with Failure Path',
436
- startStep: mainStepRes.insertedIds[0].toString()
437
- });
438
-
439
- const workflowRun = await runWorkflowAndWait(workflowId, { _model: 'task', title: 'Test failure path' });
440
-
441
- expect(workflowRun.status).toBe('failed'); // Le statut final est 'failed' car il n'y a pas d'étape après l'échec
442
- });
443
-
444
- it('Trigger dataFilter: ne devrait lancer le workflow que si le filtre correspond', async () => {
445
- // Création d'une étape de fin pour vérifier la reprise
446
- vi.useFakeTimers();
447
-
448
- // Créer une action et une étape simples
449
- const actionRes = await insertData('workflowAction', { name: 'Create Log', type: 'CreateData', targetModel: 'log', dataToCreate: { message: 'Filtered task processed' } }, {}, mockUser, false);
450
- const stepRes = await insertData('workflowStep', { name: 'Step', actions: [actionRes.insertedIds[0].toString()], isTerminal: true }, {}, mockUser, false);
451
- const workflowRes = await insertData('workflow', { name: 'Filtered Workflow', startStep: stepRes.insertedIds[0].toString() }, {}, mockUser, false);
452
-
453
- // Créer un trigger avec un dataFilter
454
- await insertData('workflowTrigger', {
455
- name: 'Trigger only for "done" tasks',
456
- targetModel: 'task',
457
- onEvent: 'DataAdded',
458
- isActive: true,
459
- workflow: workflowRes.insertedIds[0].toString(),
460
- dataFilter: { "$eq": ["$status","done"] } // Le filtre crucial
461
- }, {}, mockUser, false);
462
-
463
- // 1. Déclencher avec une donnée qui NE correspond PAS au filtre
464
- await workflowModule.triggerWorkflows({ _model: 'task', title: 'A task not done', status: 'todo' }, mockUser, 'DataAdded');
465
- let runs = await testDatasColInstance.find({ _model: 'workflowRun' }).toArray();
466
- expect(runs.length).toBe(0); // Aucun workflow ne doit avoir été lancé
467
-
468
- vi.advanceTimersByTime(2000);
469
-
470
- // 2. Déclencher avec une donnée qui correspond au filtre
471
- await workflowModule.triggerWorkflows({ _model: 'task', title: 'A task that is done', status: 'done' }, mockUser, 'DataAdded');
472
- runs = await testDatasColInstance.find({ _model: 'workflowRun' }).toArray();
473
- expect(runs.length).toBe(1); // Un seul workflow doit avoir été lancé
474
- expect(runs[0].status).toBe('completed');
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
  });