data-primals-engine 1.0.7 → 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.
- package/.github/workflows/node.js.yml +59 -0
- package/.github/workflows/npm-publish.yml +23 -0
- package/Dockerfile +15 -0
- package/DockerfileTest +15 -0
- package/package.json +4 -7
- package/server.js +11 -2
- package/src/data.js +15 -5
- package/src/email.js +2 -0
- package/src/engine.js +42 -40
- package/src/gameObject.js +5 -7
- package/src/migrate.js +1 -1
- package/src/modules/data.js +87 -61
- package/src/modules/mongodb.js +1 -8
- package/src/modules/workflow.js +190 -58
- package/src/packs.js +26 -10
- package/src/setenv.js +39 -0
- package/test/data.backup.integration.test.js +141 -0
- package/test/data.integration.test.js +796 -0
- package/test/globalSetup.js +15 -0
- package/test/globalTeardown.js +8 -0
- package/test/import_export.integration.test.js +200 -0
- package/test/workflow.integration.test.js +314 -0
- package/test/workflow.robustness.test.js +195 -0
- package/vitest.config.js +16 -0
|
@@ -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
|
+
});
|
package/vitest.config.js
ADDED
|
@@ -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
|
+
});
|