data-primals-engine 1.6.2-rc1 → 1.6.3

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,207 @@
1
+ // __tests__/assistant.test.js
2
+ import { vi, expect, describe, it, beforeAll, afterAll, beforeEach } from 'vitest';
3
+ import { Config } from '../src/config.js';
4
+ import { getCollection, getCollectionForUser } from '../src/modules/mongodb.js';
5
+ import { generateUniqueName, initEngine } from "../src/setenv.js";
6
+ import { purgeData } from "../src/modules/data/data.history.js";
7
+ import { handleChatRequest } from '../src/modules/assistant/assistant.js';
8
+ import { insertData } from '../src/index.js';
9
+ import * as dataOperations from '../src/modules/data/data.operations.js';
10
+
11
+ // --- CORRECTION ---
12
+ // On mock le nouveau module 'providers.js' qui contient la fonction que l'on veut surcharger.
13
+ vi.mock('../src/modules/assistant/providers.js', () => ({
14
+ getAIProvider: vi.fn(),
15
+ }));
16
+
17
+ // On importe la version mockée de getAIProvider
18
+ import { getAIProvider } from '../src/modules/assistant/providers.js';
19
+
20
+ let testUser;
21
+ let modelsCollection;
22
+ let datasCollection;
23
+
24
+ const modelName = generateUniqueName('productAssistant');
25
+
26
+ describe('Assistant Module Unit Tests', () => {
27
+
28
+ beforeAll(async () => {
29
+ Config.Set("modules", ["mongodb", "data", "user", "assistant"]);
30
+ await initEngine(); // Cette ligne ne devrait plus planter.
31
+ testUser = {
32
+ username: generateUniqueName('testUserAssistant'),
33
+ email: generateUniqueName('test') + '@example.com',
34
+ lang: 'fr'
35
+ };
36
+
37
+ modelsCollection = getCollection('models');
38
+ datasCollection = await getCollectionForUser(testUser);
39
+
40
+ // Create a model for testing
41
+ const productModelDef = {
42
+ name: modelName,
43
+ description: 'A model for testing',
44
+ _user: testUser.username,
45
+ fields: [
46
+ { name: 'name', type: 'string', required: true },
47
+ { name: 'price', type: 'number' }
48
+ ]
49
+ };
50
+ await modelsCollection.insertOne(productModelDef);
51
+ });
52
+
53
+ beforeEach(async () => {
54
+ // Clear mocks before each test
55
+ vi.clearAllMocks();
56
+ // Clean data, but keep the model
57
+ await datasCollection.deleteMany({ _user: testUser.username });
58
+ });
59
+
60
+ // Helper to create a mock LLM stream
61
+ const createMockStream = (content) => {
62
+ return (async function* () {
63
+ yield { content };
64
+ })();
65
+ };
66
+
67
+ it('should return a simple text message from the AI', async () => {
68
+ const mockLLM = {
69
+ stream: vi.fn().mockReturnValue(createMockStream('Bonjour, ceci est un test.')),
70
+ };
71
+ getAIProvider.mockResolvedValue(mockLLM);
72
+
73
+ const params = { message: 'Dis bonjour', history: [] };
74
+ const result = await handleChatRequest(params, testUser);
75
+
76
+ expect(getAIProvider).toHaveBeenCalled();
77
+ expect(mockLLM.stream).toHaveBeenCalled();
78
+ expect(result).toEqual({
79
+ success: true,
80
+ displayMessage: 'Bonjour, ceci est un test.'
81
+ });
82
+ });
83
+
84
+ it('should handle a search action', async () => {
85
+
86
+ // Configure le retour du mock pour ce test spécifique
87
+ const searchDataSpy = vi.spyOn(dataOperations, 'searchData').mockResolvedValue({ data: [{ name: 'Test Widget', price: 100 }], count: 1 });
88
+
89
+ const aiResponse = JSON.stringify([
90
+ { action: 'search_models', params: { query: modelName } },
91
+ { action: 'search', params: { model: modelName, filter: {} } }
92
+ ]);
93
+ const mockLLM = {
94
+ stream: vi.fn().mockReturnValue(createMockStream(aiResponse))
95
+ };
96
+ getAIProvider.mockResolvedValue(mockLLM);
97
+
98
+ const params = { message: `cherche les produits`, history: [] };
99
+ const result = await handleChatRequest(params, testUser);
100
+
101
+ expect(searchDataSpy).toHaveBeenCalled();
102
+
103
+ expect(result.success).toBe(true);
104
+ expect(result.dataResult).toBeDefined();
105
+ expect(result.dataResult.model).toBe(modelName);
106
+ expect(result.dataResult.data).toHaveLength(1);
107
+ expect(result.dataResult.data[0].name).toBe('Test Widget');
108
+ });
109
+
110
+ it('should request confirmation for a "post" action', async () => {
111
+ const aiResponse = JSON.stringify({
112
+ action: 'post',
113
+ params: {
114
+ model: modelName,
115
+ data: { name: 'New Gadget', price: 19.99 }
116
+ }
117
+ });
118
+ const mockLLM = {
119
+ stream: vi.fn().mockReturnValue(createMockStream(aiResponse)),
120
+ };
121
+ getAIProvider.mockResolvedValue(mockLLM);
122
+
123
+ const params = { message: 'Crée un produit', history: [] };
124
+ const result = await handleChatRequest(params, testUser);
125
+
126
+ expect(result.success).toBe(true);
127
+ expect(result.confirmationRequest).toBeDefined();
128
+ expect(result.confirmationRequest.action).toBe('post');
129
+ expect(result.confirmationRequest.params.model).toBe(modelName);
130
+ expect(result.confirmationRequest.params.data.name).toBe('New Gadget');
131
+ });
132
+
133
+ it('should execute a confirmed "post" action', async () => {
134
+ const confirmedAction = {
135
+ action: 'post',
136
+ params: {
137
+ model: modelName,
138
+ data: { name: 'Confirmed Gadget', price: 50 }
139
+ }
140
+ };
141
+
142
+ const params = { message: '', confirmedAction };
143
+ const result = await handleChatRequest(params, testUser);
144
+
145
+ expect(result.success).toBe(true);
146
+ // --- CORRECTION ---
147
+ // On vérifie le message de succès spécifique à la création, qui est plus informatif.
148
+ expect(result.displayMessage).toContain("Élément créé avec l'ID:");
149
+
150
+ // Verify the data was actually inserted
151
+ const dataInDb = await datasCollection.findOne({ name: 'Confirmed Gadget' });
152
+ expect(dataInDb).not.toBeNull();
153
+ expect(dataInDb.price).toBe(50);
154
+ });
155
+
156
+ it('should handle malformed JSON from AI by returning it as a text message', async () => {
157
+ const aiResponse = `{ "action": "search", "params": { "model": "${modelName}" }`; // Missing closing brace
158
+ const mockLLM = {
159
+ stream: vi.fn().mockReturnValue(createMockStream(aiResponse)),
160
+ };
161
+ getAIProvider.mockResolvedValue(mockLLM);
162
+
163
+ const params = { message: 'test', history: [] };
164
+ const result = await handleChatRequest(params, testUser);
165
+
166
+ expect(result.success).toBe(true);
167
+ expect(result.displayMessage).toBe(aiResponse);
168
+ });
169
+
170
+ it('should correct a malformed filter from the AI', async () => {
171
+ const aiResponse = JSON.stringify({
172
+ action: 'search',
173
+ params: {
174
+ model: modelName,
175
+ // This filter is malformed: multiple operators at the root level
176
+ filter: {
177
+ "$gt": ["$price", 50],
178
+ "$lt": ["$price", 200]
179
+ }
180
+ }
181
+ });
182
+ const mockLLM = {
183
+ stream: vi.fn().mockReturnValue(createMockStream(aiResponse)),
184
+ };
185
+ getAIProvider.mockResolvedValue(mockLLM);
186
+
187
+ // Configure le retour du mock pour ce test spécifique
188
+ const searchDataSpy = vi.spyOn(dataOperations, 'searchData').mockResolvedValue({ data: [], count: 0 });
189
+
190
+ const params = { message: 'cherche produits entre 50 et 200', history: [] };
191
+ // On peut maintenant utiliser la fonction handleChatRequest importée normalement
192
+ await handleChatRequest(params, testUser);
193
+
194
+ // On s'attend à ce que le filtre ait été encapsulé dans un $and
195
+ const expectedFilter = {
196
+ "$and": [
197
+ { "$gt": ["$price", 50] },
198
+ { "$lt": ["$price", 200] }
199
+ ]
200
+ };
201
+
202
+ expect(searchDataSpy).toHaveBeenCalledWith(expect.objectContaining({ filter: expectedFilter }), expect.anything());
203
+
204
+ // On nettoie l'espion après le test
205
+ searchDataSpy.mockRestore();
206
+ });
207
+ });
@@ -0,0 +1,41 @@
1
+ // __tests__/config.test.js
2
+ import { describe, it, expect, beforeAll } from 'vitest';
3
+ import { Config } from '../src/config.js';
4
+ import { initEngine } from '../src/setenv.js';
5
+
6
+ describe('Configuration Module Tests', () => {
7
+
8
+ it('should correctly get and set a simple value', () => {
9
+ const key = 'testKey';
10
+ const value = 'testValue';
11
+
12
+ Config.Set(key, value);
13
+ const retrievedValue = Config.Get(key);
14
+
15
+ expect(retrievedValue).toBe(value);
16
+ });
17
+
18
+ it('should return the default value if a key is not set', () => {
19
+ const defaultValue = 'default';
20
+ const retrievedValue = Config.Get('nonExistentKey', defaultValue);
21
+
22
+ expect(retrievedValue).toBe(defaultValue);
23
+ });
24
+
25
+ it('should initialize the engine with only the modules specified in the config', async () => {
26
+ // 1. Définir une configuration de modules minimale.
27
+ const targetModules = ['mongodb', 'user'];
28
+ Config.Set("modules", targetModules);
29
+
30
+ // 2. Initialiser le moteur. initEngine devrait lire cette configuration.
31
+ const engine = await initEngine();
32
+
33
+ // 3. Vérifier que le moteur a chargé UNIQUEMENT les modules spécifiés.
34
+ // La propriété _modules de l'engine contient les instances des modules chargés.
35
+ const loadedModuleNames = engine._modules.map(m => m.module);
36
+
37
+ expect(engine._modules).toBeInstanceOf(Array);
38
+ expect(loadedModuleNames).toHaveLength(targetModules.length);
39
+ expect(loadedModuleNames).toEqual(expect.arrayContaining(targetModules));
40
+ });
41
+ });
@@ -1,10 +1,10 @@
1
1
  // __tests__/data.history.integration.test.js
2
2
  import { ObjectId } from 'mongodb';
3
3
  import { expect, describe, it, beforeAll, afterAll, beforeEach } from 'vitest';
4
- import { handleGetHistoryRequest } from '../src/modules/data/data.history.js';
4
+ import { handleGetHistoryRequest, handleGetRevisionRequest, handleRevertToRevisionRequest } from '../src/modules/data/data.history.js';
5
5
  import { Config } from '../src/config.js';
6
6
  import { sleep } from '../src/core.js';
7
- import { insertData, editData, deleteModels } from '../src/index.js';
7
+ import { insertData, editData, deleteData } from '../src/index.js';
8
8
  import { getCollection, getCollectionForUser } from '../src/modules/mongodb.js';
9
9
  import { generateUniqueName, initEngine } from "../src/setenv.js";
10
10
  import {purgeData} from "../src/modules/data/data.history.js";
@@ -16,6 +16,20 @@ let historyCollection;
16
16
  let datasCollection;
17
17
  let modelsCollection;
18
18
 
19
+ // Mock Express req/res objects for testing API handlers
20
+ const mockReq = (params = {}, query = {}, body = {}, user = testUser) => ({
21
+ params,
22
+ query,
23
+ body,
24
+ me: user
25
+ });
26
+ const mockRes = () => {
27
+ const res = {};
28
+ res.status = (code) => { res.statusCode = code; return res; };
29
+ res.json = (data) => { res.body = data; return res; };
30
+ return res;
31
+ };
32
+
19
33
  describe('Data History Module Integration Tests', () => {
20
34
 
21
35
  beforeAll(async () => {
@@ -88,7 +102,7 @@ describe('Data History Module Integration Tests', () => {
88
102
  };
89
103
  const insertResult = await insertData(modelName, initialData, {}, testUser);
90
104
  expect(insertResult.success).toBe(true);
91
- const docId = new ObjectId(insertResult.insertedIds[0]);
105
+ const docId = new ObjectId(insertResult.data._id);
92
106
 
93
107
  // 3. Verify the history record
94
108
  const historyRecord = await historyCollection.findOne({ documentId: new ObjectId(docId) });
@@ -136,7 +150,7 @@ describe('Data History Module Integration Tests', () => {
136
150
  // 2. Insert the initial document
137
151
  const initialData = { name: 'Selective Widget', price: 50, stock: 200, description: 'Initial description.' };
138
152
  const insertResult = await insertData(modelName, initialData, {}, testUser);
139
- const docId = new ObjectId(insertResult.insertedIds[0]);
153
+ const docId = new ObjectId(insertResult.data._id);
140
154
 
141
155
  // 3. Edit the document: change one historized field and one non-historized field
142
156
  const updateData = { price: 55.5, description: 'Updated description.' };
@@ -179,7 +193,7 @@ describe('Data History Module Integration Tests', () => {
179
193
  // 2. Insert initial document
180
194
  const initialData = { name: 'No-Op Widget', price: 10, description: 'Initial.' };
181
195
  const insertResult = await insertData(modelName, initialData, {}, testUser);
182
- const docId = new ObjectId(insertResult.insertedIds[0]);
196
+ const docId = new ObjectId(insertResult.data._id);
183
197
 
184
198
  // 3. Edit ONLY a non-historized field
185
199
  const updateData = { description: 'This change should not be recorded.' };
@@ -192,6 +206,126 @@ describe('Data History Module Integration Tests', () => {
192
206
  expect(historyCount).toBe(1);
193
207
  });
194
208
 
209
+ it('should create a snapshot history record on document deletion', async () => {
210
+ // 1. Setup model
211
+ const modelName = generateUniqueName('productHistoryDelete');
212
+ const productModelDef = {
213
+ name: modelName,
214
+ description:'',
215
+ _user: testUser.username,
216
+ history: { enabled: true },
217
+ fields: [{ name: 'name', type: 'string' }]
218
+ };
219
+ await modelsCollection.insertOne(productModelDef);
220
+
221
+ // 2. Insert a document
222
+ const insertResult = await insertData(modelName, { name: 'Document to be deleted' }, {}, testUser);
223
+ const docId = new ObjectId(insertResult.data._id);
224
+
225
+ // 3. Delete the document
226
+ await deleteData(modelName, [docId.toString()], testUser);
227
+
228
+ // 4. Verify the new history record (v2)
229
+ const historyRecord = await historyCollection.findOne({ documentId: docId, version: 2 });
230
+
231
+ expect(historyRecord).not.toBeNull();
232
+ expect(historyRecord.operation).toBe('delete');
233
+ expect(historyRecord.snapshot).not.toBeNull();
234
+ expect(historyRecord.snapshot.name).toBe('Document to be deleted');
235
+ expect(historyRecord.changes).toBeUndefined();
236
+ });
237
+
238
+ it('should correctly reconstruct a document at a specific version', async () => {
239
+ // 1. Setup model
240
+ const modelName = generateUniqueName('productHistoryReconstruct');
241
+ const productModelDef = {
242
+ name: modelName,
243
+ _user: testUser.username,
244
+ description:'',
245
+ history: { enabled: true },
246
+ fields: [
247
+ { name: 'name', type: 'string' },
248
+ { name: 'status', type: 'string' },
249
+ { name: 'count', type: 'number' }
250
+ ]
251
+ };
252
+ await modelsCollection.insertOne(productModelDef);
253
+
254
+ // 2. Create and update document to generate history
255
+ const insertResult = await insertData(modelName, { name: 'Reconstruct', status: 'initial', count: 0 }, {}, testUser); // v1
256
+ const docId = new ObjectId(insertResult.data._id);
257
+
258
+ await editData(modelName, { _id: docId }, { status: 'updated' }, {}, testUser); // v2
259
+ await editData(modelName, { _id: docId }, { count: 10 }, {}, testUser); // v3
260
+
261
+ // 3. Test reconstruction at each version via the API handler
262
+ // Version 1 (creation)
263
+ let req = mockReq({ modelName, recordId: docId.toString(), version: '1' });
264
+ let res = mockRes();
265
+ await handleGetRevisionRequest(req, res);
266
+ expect(res.body.success).toBe(true);
267
+ expect(res.body.data.name).toBe('Reconstruct');
268
+ expect(res.body.data.status).toBe('initial');
269
+ expect(res.body.data.count).toBe(0);
270
+
271
+ // Version 2 (status updated)
272
+ req = mockReq({ modelName, recordId: docId.toString(), version: '2' });
273
+ res = mockRes();
274
+ await handleGetRevisionRequest(req, res);
275
+ expect(res.body.success).toBe(true);
276
+ expect(res.body.data.status).toBe('updated');
277
+ expect(res.body.data.count).toBe(0); // count is still 0
278
+
279
+ // Version 3 (count updated)
280
+ req = mockReq({ modelName, recordId: docId.toString(), version: '3' });
281
+ res = mockRes();
282
+ await handleGetRevisionRequest(req, res);
283
+ expect(res.body.success).toBe(true);
284
+ expect(res.body.data.status).toBe('updated');
285
+ expect(res.body.data.count).toBe(10);
286
+ });
287
+
288
+ it('should revert a document to a previous version and create a new history entry', async () => {
289
+ // 1. Setup model
290
+ const modelName = generateUniqueName('productHistoryRevert');
291
+ const productModelDef = {
292
+ name: modelName,
293
+ _user: testUser.username,
294
+ description:'',
295
+ history: { enabled: true },
296
+ fields: [{ name: 'name', type: 'string' }, { name: 'status', type: 'string' }]
297
+ };
298
+ await modelsCollection.insertOne(productModelDef);
299
+
300
+ // 2. Create and update document
301
+ const insertResult = await insertData(modelName, { name: 'Revert Test', status: 'v1' }, {}, testUser); // v1
302
+ const docId = new ObjectId(insertResult.data._id);
303
+ await editData(modelName, { _id: docId }, { status: 'v2' }, {}, testUser); // v2
304
+
305
+ // 3. Revert the document to version 1
306
+ const req = mockReq({ modelName, recordId: docId.toString(), version: '1' });
307
+ const res = mockRes();
308
+ await handleRevertToRevisionRequest(req, res);
309
+
310
+ expect(res.body.success).toBe(true);
311
+ expect(res.body.message).toBe("Document successfully reverted.");
312
+
313
+ // 4. Verify the current state of the document
314
+ const revertedDoc = await datasCollection.findOne({ _id: docId });
315
+ expect(revertedDoc.status).toBe('v1');
316
+
317
+ // 5. Verify that a new history entry (v3) was created for the revert action
318
+ const historyRecords = await historyCollection.find({ documentId: docId }).sort({ version: 1 }).toArray();
319
+ expect(historyRecords).toHaveLength(3);
320
+
321
+ const revertHistoryEntry = historyRecords[2]; // v3
322
+ expect(revertHistoryEntry.version).toBe(3);
323
+ expect(revertHistoryEntry.operation).toBe('update'); // A revert is an update
324
+ expect(revertHistoryEntry.changes).not.toBeNull();
325
+ expect(revertHistoryEntry.changes.status.from).toBe('v2');
326
+ expect(revertHistoryEntry.changes.status.to).toBe('v1');
327
+ });
328
+
195
329
  it('should filter history records by date range', async () => {
196
330
  // 1. Setup model
197
331
  const modelName = generateUniqueName('productHistoryDateFilter');
@@ -206,7 +340,7 @@ describe('Data History Module Integration Tests', () => {
206
340
 
207
341
  // 2. Insert initial document
208
342
  const insertResult = await insertData(modelName, { name: 'Time-traveling Widget' }, {}, testUser);
209
- const docId = new ObjectId(insertResult.insertedIds[0]);
343
+ const docId = new ObjectId(insertResult.data._id);
210
344
 
211
345
  // 3. Create history entries at different times
212
346
  // To simulate different timestamps, we'll manually insert history records
@@ -227,21 +361,11 @@ describe('Data History Module Integration Tests', () => {
227
361
  });
228
362
 
229
363
  // Mock Express req/res objects
230
- const mockReq = (query) => ({
231
- params: { modelName, recordId: docId.toString() },
232
- query,
233
- me: testUser
234
- });
235
- const mockRes = () => {
236
- const res = {};
237
- res.status = (code) => { res.statusCode = code; return res; };
238
- res.json = (data) => { res.body = data; return res; };
239
- return res;
240
- };
364
+ // Using the global mockReq and mockRes helpers now
241
365
 
242
366
  // 4. Test cases
243
367
  // Case A: Filter for February
244
- let req = mockReq({ startDate: '2023-02-01', endDate: '2023-02-28' });
368
+ let req = mockReq({ modelName, recordId: docId.toString() }, { startDate: '2023-02-01', endDate: '2023-02-28' });
245
369
  let res = mockRes();
246
370
  await handleGetHistoryRequest(req, res);
247
371
  expect(res.body.success).toBe(true);
@@ -249,14 +373,14 @@ describe('Data History Module Integration Tests', () => {
249
373
  expect(res.body.data.map(d => d._v).sort()).toEqual([2, 3]);
250
374
 
251
375
  // Case B: Filter starting from Feb 15th
252
- req = mockReq({ startDate: '2023-02-15' });
376
+ req = mockReq({ modelName, recordId: docId.toString() }, { startDate: '2023-02-15' });
253
377
  res = mockRes();
254
378
  await handleGetHistoryRequest(req, res);
255
379
  expect(res.body.success).toBe(true);
256
380
  expect(res.body.count).toBe(3); // v2, v3, v4
257
381
 
258
382
  // Case C: Filter up to Feb 15th (inclusive)
259
- req = mockReq({ endDate: '2023-02-15' });
383
+ req = mockReq({ modelName, recordId: docId.toString() }, { endDate: '2023-02-15' });
260
384
  res = mockRes();
261
385
  await handleGetHistoryRequest(req, res);
262
386
  expect(res.body.success).toBe(true);