data-primals-engine 1.0.8 → 1.0.9

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,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
+ });