data-primals-engine 1.2.6-rc2 → 1.2.6

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