data-primals-engine 1.0.8 → 1.0.10

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,314 @@
1
+ // __tests__/workflow.integration.test.js
2
+ import { expect, describe, it, beforeEach,afterEach, beforeAll, afterAll, vi } from 'vitest';
3
+ import { Config } from "data-primals-engine/config";
4
+
5
+ import {insertData, editData, deleteData, patchData} from 'data-primals-engine/modules/data';
6
+ import { modelsCollection as getAppModelsCollection, getCollectionForUser } from 'data-primals-engine/modules/mongodb';
7
+ import * as workflowModule from 'data-primals-engine/modules/workflow';
8
+ import {getUniquePort, initEngine} from "../src/setenv.js";
9
+ import process from "process";
10
+
11
+
12
+ beforeAll(async () =>{
13
+ Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow","user", "assistant"]);
14
+ await initEngine();
15
+ })
16
+ vi.mock('data-primals-engine/modules/workflow', { spy: true })
17
+ // --- Données Mock pour les tests ---
18
+ const mockUser = {
19
+ username: 'testuserWorkflow',
20
+ _user: 'testuserWorkflow',
21
+ userPlan: 'free',
22
+ email: 'testWorkflow@example.com'
23
+ };
24
+
25
+ // --- Définitions des modèles ---
26
+
27
+ // Le modèle de données qui déclenchera les workflows
28
+ const targetDataModel = {
29
+ name: 'project',
30
+ _user: mockUser.username,
31
+ description: 'Un modèle pour les projets qui déclencheront des workflows',
32
+ fields: [
33
+ { name: 'projectName', type: 'string', required: true },
34
+ { name: 'status', type: 'string', required: true }, // ex: 'new', 'active', 'archived'
35
+ { name: 'budget', type: 'number' }
36
+ ],
37
+ };
38
+
39
+ // Les modèles qui définissent le système de workflow lui-même.
40
+ // Pour les tests, nous devons insérer leurs définitions dans la collection 'models',
41
+ // puis leurs instances (ex: un trigger spécifique) dans la collection 'datas' de l'utilisateur.
42
+ // Les modèles qui définissent le système de workflow lui-me.
43
+ // Pour les tests, nous devons insérer leurs définitions dans la collection 'models',
44
+ // puis leurs instances (ex: un trigger spécifique) dans la collection 'datas' de l'utilisateur.
45
+ const workflowMetaModels = [
46
+ {
47
+ name: 'workflow',
48
+ _user: mockUser.username,
49
+ description:'',
50
+ fields: [
51
+ { name: 'name', type: 'string' },
52
+ { name: 'startStep', type: 'relation', relation: 'workflowStep' }
53
+ ]
54
+ },
55
+ {
56
+ name: 'workflowStep',
57
+ _user: mockUser.username,
58
+ description:'',
59
+ fields: [
60
+ { name: 'name', type: 'string' },
61
+ { name: 'onSuccessStep', type: 'relation', relation: 'workflowStep' },
62
+ { name: 'onFailureStep', type: 'relation', relation: 'workflowStep' },
63
+ { name: 'isTerminal', type: 'boolean' },
64
+ { name: 'actions', type: 'array', itemsType: 'relation', relation: 'workflowAction' },
65
+ { name: 'conditions', type: 'object' }
66
+ ]
67
+ },
68
+ {
69
+ name: 'workflowTrigger',
70
+ _user: mockUser.username,
71
+ description:'',
72
+ fields: [
73
+ { name: 'name', type: 'string' },
74
+ { name: 'targetModel', type: 'model' },
75
+ { name: 'onEvent', type: 'enum', items: ['DataAdded', 'DataEdited', 'DataDeleted', 'ModelAdded', 'ModelEdited', 'ModelDeleted'] },
76
+ { name: 'isActive', type: 'boolean' },
77
+ { name: 'dataFilter', type: 'object' },
78
+ { name: 'workflow', type: 'relation', relation: 'workflow' },
79
+ { name: 'cronExpression', type: 'cronSchedule' },
80
+ { name: 'lockDurationMinutes', type: 'number' }
81
+ ]
82
+ },
83
+ {
84
+ name: 'workflowRun',
85
+ _user: mockUser.username,
86
+ description:'',
87
+ fields: [
88
+ { name: 'status', type: 'enum', items: ['pending', 'running', 'completed', 'failed', 'cancelled'] },
89
+ { name: 'workflow', type: 'relation', relation: 'workflow' },
90
+ { name: 'contextData', type: 'object' },
91
+ { name: 'currentStep', type: 'relation', relation: 'workflowStep' },
92
+ { name: 'error', type: 'string' },
93
+ { name: 'startedAt', type: 'datetime' },
94
+ { name: 'completedAt', type: 'datetime' },
95
+ { name: 'stepExecutionsCount', type: 'object' }
96
+ ]
97
+ },
98
+ {
99
+ name: 'workflowAction',
100
+ _user: mockUser.username,
101
+ description:'',
102
+ fields: [
103
+ // Common
104
+ { name: 'name', type: 'string' },
105
+ { name: 'type', type: 'enum', items: ['Webhook', 'CreateData', 'UpdateData', 'DeleteData', 'GenerateAIContent', 'SendEmail'] },
106
+ // Webhook
107
+ { name: 'url', type: 'url' },
108
+ { name: 'method', type: 'enum', items: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] },
109
+ { name: 'headers', type: 'code', language: 'json' },
110
+ { name: 'body', type: 'code', language: 'json' },
111
+ // Create/Update/Delete
112
+ { name: 'targetModel', type: 'model' },
113
+ { name: 'dataToCreate', type: 'code', language: 'json' },
114
+ { name: 'targetSelector', type: 'code', language: 'json' },
115
+ { name: 'fieldsToUpdate', type: 'code', language: 'json' },
116
+ { name: 'updateMultiple', type: 'boolean' },
117
+ { name: 'deleteMultiple', type: 'boolean' },
118
+ // GenerateAIContent
119
+ { name: 'aiProvider', type: 'enum', items: ['OpenAI', 'GoogleGemini'] },
120
+ { name: 'aiModel', type: 'string' },
121
+ { name: 'prompt', type: 'richtext' },
122
+ // SendEmail
123
+ { name: 'emailRecipients', type: 'array', itemsType: 'string' },
124
+ { name: 'emailSubject', type: 'string' },
125
+ { name: 'emailContent', type: 'richtext' }
126
+ ]
127
+ }
128
+ ];
129
+
130
+
131
+ let testModelsColInstance;
132
+ let testDatasColInstance;
133
+ let engineInstance;
134
+
135
+ const port = process.env.PORT || getUniquePort(); // Port différent
136
+
137
+ // On "espionne" `c` pour vérifier qu'elle est appelée sans l'exécuter réellement.
138
+ // Cela nous permet de tester uniquement la logique de déclenchement.
139
+ const processWorkflowRunSpy = vi.spyOn(workflowModule, 'processWorkflowRun');
140
+
141
+ beforeEach(async () => {
142
+ testModelsColInstance = getAppModelsCollection;
143
+ testDatasColInstance = getCollectionForUser(mockUser);
144
+ // Nettoyer les données avant chaque test
145
+ await testDatasColInstance.deleteMany({_user: "testuserWorkflow"});
146
+
147
+ // Réinitialiser l'espion
148
+ processWorkflowRunSpy.mockClear();
149
+
150
+ const mods = await testModelsColInstance.find({ $and: [{ _user: mockUser.username}, { $or: [{name: targetDataModel.name}, ...workflowMetaModels.map(m =>({name: m.name}))]}]}).toArray();
151
+ if( mods.length === 0 ) {
152
+ // Insérer les définitions de modèles nécessaires
153
+ await testModelsColInstance.insertMany([
154
+ {...targetDataModel},
155
+ ...workflowMetaModels.map(m => ({...m})) // Copie pour éviter les mutations
156
+ ]);
157
+ }
158
+ console.log({mods})
159
+ });
160
+
161
+ describe('Intégration des Workflows - triggerWorkflows', () => {
162
+
163
+ let testWorkflow;
164
+ let testStep;
165
+
166
+ // Avant chaque test de ce bloc, on crée un workflow et une étape de base
167
+ const initTest = async () => {
168
+ const workflowInsertResult = await insertData('workflow', { name: 'Test Workflow' }, {}, mockUser, false);
169
+ testWorkflow = { _id: workflowInsertResult.insertedIds[0] };
170
+
171
+ const stepInsertResult = await insertData('workflowStep', { name: 'Start Step', isTerminal: true }, {}, mockUser, false);
172
+ testStep = { _id: stepInsertResult.insertedIds[0] };
173
+
174
+ // Lier l'étape au workflow
175
+ await editData('workflow', testWorkflow._id, { startStep: testStep._id.toString() }, {}, mockUser, false);
176
+ };
177
+
178
+ it('devrait créer un workflowRun lors de l\'ajout de données correspondant à un trigger "DataAdded"', async () => {
179
+ await initTest();
180
+ // 1. Arrange: Créer un déclencheur actif pour 'DataAdded' sur le modèle 'project'
181
+ await insertData('workflowTrigger', {
182
+ name: 'Trigger on Project Add',
183
+ targetModel: targetDataModel.name,
184
+ onEvent: 'DataAdded',
185
+ isActive: true,
186
+ workflow: testWorkflow._id.toString()
187
+ }, {}, mockUser, false);
188
+
189
+ // 2. Act: Insérer une donnée qui doit déclencher le workflow
190
+ const projectData = { projectName: 'New Corp Website', status: 'new', budget: 50000 };
191
+ const insertResult = await insertData(targetDataModel.name, projectData, {}, mockUser, true, true);
192
+
193
+ expect(insertResult.success).toBe(true);
194
+
195
+ const workflowRun = await testDatasColInstance.findOne({ _model: 'workflowRun' });
196
+
197
+ expect(workflowRun).not.toBeNull();
198
+ expect(workflowRun.status).toBe('completed');
199
+ expect(workflowRun.workflow.toString()).toBe(testWorkflow._id.toString());
200
+ expect(workflowRun.contextData.triggerData._id.toString()).toBe(insertResult.insertedIds[0].toString());
201
+ expect(workflowRun.contextData.triggerData.projectName).toBe('New Corp Website');
202
+
203
+ });
204
+
205
+ it('ne devrait PAS créer de workflowRun si le trigger est inactif', async () => {
206
+ await initTest();
207
+ // 1. Arrange: Créer un déclencheur INACTIF
208
+ await insertData('workflowTrigger', {
209
+ name: 'Inactive Trigger',
210
+ targetModel: targetDataModel.name,
211
+ onEvent: 'DataAdded',
212
+ isActive: false, // Inactif
213
+ workflow: testWorkflow._id.toString()
214
+ }, {}, mockUser, false);
215
+
216
+ // 2. Act: Insérer des données
217
+ await insertData(targetDataModel.name, { projectName: 'Secret Project', status: 'new' }, {}, mockUser, true);
218
+
219
+ // 3. Assert: Aucun `workflowRun` ne doit être créé
220
+ const workflowRunCount = await testDatasColInstance.countDocuments({ _model: 'workflowRun' });
221
+ expect(workflowRunCount).toBe(0);
222
+ });
223
+
224
+ it('devrait créer un workflowRun si le dataFilter est satisfait', async () => {
225
+ await initTest();
226
+ // 1. Arrange: Créer un déclencheur avec un `dataFilter`
227
+ await insertData('workflowTrigger', {
228
+ name: 'Trigger for Active Projects',
229
+ targetModel: targetDataModel.name,
230
+ onEvent: 'DataAdded',
231
+ isActive: true,
232
+ dataFilter: { $eq: ['$status', 'active'] }, // Filtre pour status = 'active'
233
+ workflow: testWorkflow._id.toString()
234
+ }, {}, mockUser, false);
235
+
236
+ // 2. Act: Insérer des données qui correspondent au filtre
237
+ await insertData(targetDataModel.name, { projectName: 'Go-Live Project', status: 'active' }, {}, mockUser, true, true);
238
+
239
+ // 3. Assert: Un `workflowRun` doit être créé
240
+ const workflowRunCount = await testDatasColInstance.countDocuments({ _model: 'workflowRun' });
241
+ expect(workflowRunCount).toBe(1);
242
+ });
243
+
244
+ it('ne devrait PAS créer de workflowRun si le dataFilter n\'est PAS satisfait', async () => {
245
+ await initTest();
246
+ // 1. Arrange: Créer un déclencheur avec un `dataFilter`
247
+ await insertData('workflowTrigger', {
248
+ name: 'Trigger for Archived Projects',
249
+ targetModel: targetDataModel.name,
250
+ onEvent: 'DataAdded',
251
+ isActive: true,
252
+ dataFilter: { $eq: ['$status', 'archived'] }, // Filtre pour status = 'archived'
253
+ workflow: testWorkflow._id.toString()
254
+ }, {}, mockUser, false);
255
+
256
+ // 2. Act: Insérer des données qui NE correspondent PAS au filtre
257
+ await insertData(targetDataModel.name, { projectName: 'Ongoing Project', status: 'active' }, {}, mockUser, true, true);
258
+
259
+ // 3. Assert: Aucun `workflowRun` ne doit être créé
260
+ const workflowRunCount = await testDatasColInstance.countDocuments({ _model: 'workflowRun' });
261
+ expect(workflowRunCount).toBe(0);
262
+ });
263
+
264
+ it('devrait déclencher un workflow "DataEdited" lors de la modification de données', async () => {
265
+ await initTest();
266
+ // 1. Arrange: Insérer une donnée initiale et un déclencheur pour 'DataEdited'
267
+ const insertResult = await insertData(targetDataModel.name, { projectName: 'To Edit', status: 'initial' }, {}, mockUser, false);
268
+ const projectId = insertResult.insertedIds[0];
269
+
270
+ await insertData('workflowTrigger', {
271
+ name: 'Trigger on Project Edit',
272
+ targetModel: targetDataModel.name,
273
+ onEvent: 'DataEdited',
274
+ isActive: true,
275
+ workflow: testWorkflow._id.toString()
276
+ }, {}, mockUser, false);
277
+
278
+ // 2. Act: Modifier la donnée
279
+ const editResult = await patchData(targetDataModel.name, projectId, { status: 'edited' }, {}, mockUser, true, true);
280
+ expect(editResult.success).toBe(true);
281
+
282
+ // 2. Act: Modifier la donnée
283
+ const editResult2 = await editData(targetDataModel.name, projectId, { projectName: 'test', status: 'edited' }, {}, mockUser, true, true);
284
+ expect(editResult2.success).toBe(true);
285
+
286
+ const workflows = await testDatasColInstance.countDocuments({ _model: 'workflowRun' });
287
+ expect(workflows).toBe(2);
288
+ });
289
+
290
+ it('devrait déclencher un workflow "DataDeleted" lors de la suppression de données', async () => {
291
+ await initTest();
292
+ // 1. Arrange: Insérer une donnée et un déclencheur pour 'DataDeleted'
293
+ const insertResult = await insertData(targetDataModel.name, { projectName: 'To Delete', status: 'temp' }, {}, mockUser, false);
294
+ const projectId = insertResult.insertedIds[0];
295
+
296
+ await insertData('workflowTrigger', {
297
+ name: 'Trigger on Project Delete',
298
+ targetModel: targetDataModel.name,
299
+ onEvent: 'DataDeleted',
300
+ isActive: true,
301
+ workflow: testWorkflow._id.toString()
302
+ }, {}, mockUser, false);
303
+
304
+ // 2. Act: Supprimer la donnée
305
+ const deleteResult = await deleteData(targetDataModel.name, [projectId], {}, mockUser, true, true);
306
+ expect(deleteResult.success).toBe(true);
307
+
308
+ // 3. Assert: Un `workflowRun` doit être créé
309
+ const workflowRun = await testDatasColInstance.findOne({ _model: 'workflowRun' });
310
+ expect(workflowRun).not.toBeNull();
311
+ // Le `triggerData` est le document *avant* sa suppression
312
+ expect(workflowRun.contextData.triggerData.projectName).toBe('To Delete');
313
+ });
314
+ });
@@ -0,0 +1,195 @@
1
+ import { ObjectId } from 'mongodb';
2
+ import { expect, describe, it, beforeEach, afterAll, beforeAll, vi } from 'vitest';
3
+ import { Config } from "data-primals-engine/config";
4
+
5
+ // --- Configuration initiale (similaire à workflow.integration.test.js) ---
6
+ let mongod;
7
+ let testDbUri;
8
+ const testDbName = 'testRobustnessDbHO_Workflow';
9
+
10
+ // --- Importations des modules de l'application ---
11
+ import { Engine } from "data-primals-engine/engine";
12
+ import { insertData, editData } from 'data-primals-engine/modules/data';
13
+ import { modelsCollection as getAppModelsCollection, getCollectionForUser, getCollection } from 'data-primals-engine/modules/mongodb';
14
+ import * as workflowModule from 'data-primals-engine/modules/workflow';
15
+ import {getUniquePort, initEngine} from "../src/setenv.js";
16
+ import {maxExecutionsByStep} from "data-primals-engine/constants";
17
+
18
+ vi.mock('data-primals-engine/modules/workflow', { spy: true })
19
+
20
+ const mockUser = {
21
+ username: 'robustnessUser',
22
+ _user: 'robustnessUser',
23
+ email: 'robustness@example.com'
24
+ };
25
+
26
+ // Recopie des méta-modèles pour la lisibilité
27
+ const workflowMetaModels = [
28
+ { name: 'workflow', 'description': '', _user: mockUser.username, fields: [{ name: 'name', type: 'string' }, { name: 'startStep', type: 'relation', relation: 'workflowStep' }] },
29
+ { 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' }] },
30
+ { name: 'workflowTrigger', 'description': '', _user: mockUser.username, fields: [{ name: 'name', type: 'string' }, { name: 'targetModel', type: 'model' }, { name: 'onEvent', type: 'enum', items: ['DataAdded'] }, { name: 'isActive', type: 'boolean' }, { name: 'dataFilter', type: 'object' }, { name: 'workflow', type: 'relation', relation: 'workflow' }] },
31
+ { name: 'workflowRun', 'description': '',_user: mockUser.username, fields: [{ name: 'status', type: 'enum', items: ['pending', 'running', 'completed', 'failed', 'cancelled'] }, { name: 'workflow', type: 'relation', relation: 'workflow' }, { name: 'contextData', type: 'object' }, { name: 'currentStep', type: 'relation', relation: 'workflowStep' }, { name: 'error', type: 'string' }, { name: 'completedAt', type: 'datetime' }, { name: 'stepExecutionsCount', type: 'object' }] },
32
+ { name: 'workflowAction', 'description': '', _user: mockUser.username, fields: [{ name: 'name', type: 'string' }, { name: 'type', type: 'enum', items: ['Log'] }] }
33
+ ];
34
+ const targetDataModel = { name: 'project', 'description': '',_user: mockUser.username, fields: [{ name: 'projectName', type: 'string' }, { name: 'status', type: 'string' }] };
35
+
36
+
37
+ let testModelsColInstance;
38
+ let testDatasColInstance;
39
+
40
+ beforeAll(async () =>{
41
+ Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow","user", "assistant"]);
42
+ await initEngine();
43
+ })
44
+
45
+ beforeEach(async () => {
46
+ testModelsColInstance = getAppModelsCollection;
47
+ testDatasColInstance = getCollectionForUser(mockUser);
48
+ await testDatasColInstance.deleteMany({ _user: mockUser.username });
49
+ await getCollection('job_locks').deleteMany({}); // Nettoyer les verrous
50
+ const mods = await testModelsColInstance.find({ $and: [{_user: mockUser.username}, {$or: [{name: targetDataModel.name}, ...workflowMetaModels.map(m =>({name: m.name}))] }]}).toArray();
51
+ console.log({mods})
52
+ if( mods.length === 0){
53
+ await testModelsColInstance.insertMany([targetDataModel, ...workflowMetaModels]);
54
+ }
55
+ });
56
+
57
+ // ====================================================================================
58
+ // =================== DÉBUT DES TESTS DE ROBUSTESSE ==================================
59
+ // ====================================================================================
60
+
61
+ describe('Tests de robustesse et des cas limites du module Workflow', () => {
62
+
63
+ describe('Verrouillage distribué (runScheduledJobWithDbLock)', () => {
64
+
65
+ it('ne devrait pas exécuter la fonction si un verrou est déjà actif', async () => {
66
+ const jobsCollection = getCollection('job_locks');
67
+ const jobId = 'concurrent-job-1';
68
+ const jobFunctionSpy = vi.fn();
69
+
70
+ // 1. Arrange: Insérer manuellement un verrou actif dans la DB
71
+ const now = new Date();
72
+ await jobsCollection.insertOne({
73
+ jobId: jobId,
74
+ lockedUntil: new Date(now.getTime() + 10 * 60 * 1000), // Verrouillé pour 10 min
75
+ });
76
+
77
+ // 2. Act: Tenter de lancer le job
78
+ await workflowModule.runScheduledJobWithDbLock(jobId, jobFunctionSpy);
79
+
80
+ // 3. Assert: La fonction ne doit JAMAIS avoir été appelée
81
+ expect(jobFunctionSpy).not.toHaveBeenCalled();
82
+ });
83
+
84
+ it('devrait acquérir un verrou expiré et exécuter la fonction', async () => {
85
+ const jobsCollection = getCollection('job_locks');
86
+ const jobId = 'expired-lock-job-1';
87
+ const jobFunctionSpy = vi.fn();
88
+
89
+ // 1. Arrange: Insérer un verrou qui a déjà expiré (simule un crash)
90
+ await jobsCollection.insertOne({
91
+ jobId: jobId,
92
+ lockedUntil: new Date(Date.now() - 1000), // Expiré depuis 1 seconde
93
+ });
94
+
95
+ // 2. Act: Lancer le job
96
+ await workflowModule.runScheduledJobWithDbLock(jobId, jobFunctionSpy);
97
+
98
+ // 3. Assert: La fonction a bien été exécutée car le verrou a été repris
99
+ expect(jobFunctionSpy).toHaveBeenCalledTimes(1);
100
+ });
101
+
102
+ it('devrait libérer le verrou même si la jobFunction lve une exception', async () => {
103
+ const jobsCollection = getCollection('job_locks');
104
+ const jobId = 'job-with-exception';
105
+ // 1. Arrange: Créer une fonction qui échoue systématiquement
106
+ const failingJobFunction = vi.fn().mockRejectedValue(new Error('Erreur de traitement interne'));
107
+
108
+ // 2. Act: Exécuter la fonction. On s'attend à ce qu'elle gère l'erreur en interne.
109
+ await expect(workflowModule.runScheduledJobWithDbLock(jobId, failingJobFunction)).resolves.not.toThrow();
110
+
111
+ // 3. Assert:
112
+ // - La fonction a bien été tentée
113
+ expect(failingJobFunction).toHaveBeenCalledTimes(1);
114
+ // - Le verrou a quand même été libéré (c'est le test le plus important ici)
115
+ const lockInDb = await jobsCollection.findOne({ jobId: jobId });
116
+ expect(lockInDb).not.toBeNull();
117
+ // La date d'expiration est mise à une date passée (ici, l'époque UNIX 0)
118
+ expect(lockInDb.lockedUntil.getTime()).toBe(new Date(0).getTime());
119
+ });
120
+ });
121
+
122
+ describe('Moteur de Workflow (processWorkflowRun)', () => {
123
+
124
+ it('devrait arrêter un workflow en boucle et le marquer comme "failed"', async () => {
125
+ // 1. Arrange: Créer un workflow qui boucle sur lui-même
126
+ const action = (await insertData('workflowAction', { name: 'Log Action', type: 'Log' }, {}, mockUser)).insertedIds[0];
127
+ const step1 = (await insertData('workflowStep', { name: 'Step 1', actions: [action] }, {}, mockUser)).insertedIds[0];
128
+ const step2 = (await insertData('workflowStep', { name: 'Step 2', onSuccessStep: step1.toString() }, {}, mockUser)).insertedIds[0];
129
+ await editData('workflowStep', step1, { onSuccessStep: step2.toString() }, {}, mockUser);
130
+
131
+ const workflow = (await insertData('workflow', { name: 'Looping Workflow', startStep: step1.toString() }, {}, mockUser)).insertedIds[0];
132
+ const run = (await insertData('workflowRun', { workflow: workflow, status: 'pending', contextData: {} }, {}, mockUser)).insertedIds[0];
133
+
134
+ // 2. Act: Lancer le traitement du workflow
135
+ await workflowModule.processWorkflowRun(run, mockUser);
136
+
137
+ // 3. Assert: Le workflow a été arrêté et marqué comme échoué
138
+ const finalRun = await testDatasColInstance.findOne({ _id: new ObjectId(run) });
139
+ expect(finalRun.status).toBe('failed');
140
+ expect(finalRun.error).toContain("Maximum executions ("+maxExecutionsByStep+") exceed");
141
+ // Vérifier que le compteur de steps a bien augmenté
142
+ expect(Object.keys(finalRun.stepExecutionsCount).length).toBeGreaterThan(1);
143
+ }, 10000); // Timeout un peu plus long pour ce test
144
+
145
+ it('devrait suivre le chemin "onFailureStep" si les conditions ne sont pas remplies', async () => {
146
+ // 1. Arrange:
147
+ const successAction = (await insertData('workflowAction', { name: 'Success Action', type: 'Log' }, {}, mockUser)).insertedIds[0];
148
+ const failureAction = (await insertData('workflowAction', { name: 'Failure Action', type: 'Log' }, {}, mockUser)).insertedIds[0];
149
+
150
+ const successStep = (await insertData('workflowStep', { name: 'Success Step', actions: [successAction], isTerminal: true }, {}, mockUser)).insertedIds[0];
151
+ const failureStep = (await insertData('workflowStep', { name: 'Failure Step', actions: [failureAction], isTerminal: true }, {}, mockUser)).insertedIds[0];
152
+
153
+ const startStepDef = {
154
+ name: 'Conditional Step',
155
+ // Cette condition est conçue pour échouer car la donnée n'existera pas
156
+ conditions: { status: 'must-be-this-to-succeed' },
157
+ onSuccessStep: successStep.toString(),
158
+ onFailureStep: failureStep.toString(),
159
+ };
160
+ const startStep = (await insertData('workflowStep', startStepDef, {}, mockUser)).insertedIds[0];
161
+ const workflow = (await insertData('workflow', { name: 'Failure Path Workflow', startStep: startStep.toString() }, {}, mockUser)).insertedIds[0];
162
+ const run = (await insertData('workflowRun', { workflow: workflow, status: 'pending', contextData: { triggerDataModel: 'project' } }, {}, mockUser)).insertedIds[0];
163
+
164
+ // 2. Act
165
+ await workflowModule.processWorkflowRun(run, mockUser);
166
+
167
+ // 3. Assert
168
+ const finalRun = await testDatasColInstance.findOne({ _id: new ObjectId(run) });
169
+
170
+ // Le workflow doit se terminer avec le statut 'completed', car une condition
171
+ // non remplie est une branche valide, pas une erreur système.
172
+ expect(finalRun.status).toBe('completed');
173
+ expect(finalRun.error).toBeNull(); // Aucune erreur ne doit être enregistrée
174
+ expect(finalRun.stepExecutionsCount[startStep]).toBe(1); // Aucune erreur ne doit être enregistrée
175
+ expect(finalRun.stepExecutionsCount[failureStep]).toBe(1); // Aucune erreur ne doit être enregistrée
176
+
177
+ }, 10000);
178
+
179
+ it('devrait échouer proprement si une définition d\'étape est introuvable', async () => {
180
+ // 1. Arrange: Créer un workflow qui pointe vers un step ID invalide
181
+ const nonExistentStepId = new ObjectId().toString();
182
+ const workflow = (await insertData('workflow', { name: 'Broken Workflow', startStep: nonExistentStepId }, {}, mockUser)).insertedIds[0];
183
+ const run = (await insertData('workflowRun', { workflow: workflow, status: 'pending' }, {}, mockUser)).insertedIds[0];
184
+
185
+ // 2. Act
186
+ await workflowModule.processWorkflowRun(run, mockUser);
187
+
188
+ // 3. Assert
189
+ const finalRun = await testDatasColInstance.findOne({ _id: new ObjectId(run) });
190
+ expect(finalRun.status).toBe('failed');
191
+ expect(finalRun.error).toContain(`Step definition ID: ${nonExistentStepId} not found`);
192
+ });
193
+ });
194
+
195
+ });
@@ -0,0 +1,16 @@
1
+ // vitest.config.js
2
+ import { defineConfig } from 'vitest/config';
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ // Set a generous timeout for the global setup/teardown hooks themselves.
7
+ // This is important because starting a server can be slow.
8
+ hookTimeout: 60000, // 60 seconds
9
+
10
+ // A default timeout for individual tests
11
+ testTimeout: 25000, // 20 seconds
12
+
13
+ // Tell Vitest where to find your global setup and teardown files
14
+ globalTeardown: './test/globalTeardown.js',
15
+ },
16
+ });