data-primals-engine 1.6.2-rc1 → 1.6.2
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/README.md +969 -968
- package/package.json +145 -142
- package/src/client.js +4 -0
- package/src/modules/assistant/assistant.js +38 -56
- package/src/modules/assistant/providers.js +38 -0
- package/src/modules/data/data.history.js +2 -5
- package/src/modules/data/data.operations.js +11 -2
- package/src/modules/workflow.js +2 -2
- package/test/assistant.test.js +207 -0
- package/test/config.test.js +41 -0
- package/test/data.history.integration.test.js +4 -4
|
@@ -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
|
+
});
|
|
@@ -88,7 +88,7 @@ describe('Data History Module Integration Tests', () => {
|
|
|
88
88
|
};
|
|
89
89
|
const insertResult = await insertData(modelName, initialData, {}, testUser);
|
|
90
90
|
expect(insertResult.success).toBe(true);
|
|
91
|
-
const docId = new ObjectId(insertResult.
|
|
91
|
+
const docId = new ObjectId(insertResult.data._id);
|
|
92
92
|
|
|
93
93
|
// 3. Verify the history record
|
|
94
94
|
const historyRecord = await historyCollection.findOne({ documentId: new ObjectId(docId) });
|
|
@@ -136,7 +136,7 @@ describe('Data History Module Integration Tests', () => {
|
|
|
136
136
|
// 2. Insert the initial document
|
|
137
137
|
const initialData = { name: 'Selective Widget', price: 50, stock: 200, description: 'Initial description.' };
|
|
138
138
|
const insertResult = await insertData(modelName, initialData, {}, testUser);
|
|
139
|
-
const docId = new ObjectId(insertResult.
|
|
139
|
+
const docId = new ObjectId(insertResult.data._id);
|
|
140
140
|
|
|
141
141
|
// 3. Edit the document: change one historized field and one non-historized field
|
|
142
142
|
const updateData = { price: 55.5, description: 'Updated description.' };
|
|
@@ -179,7 +179,7 @@ describe('Data History Module Integration Tests', () => {
|
|
|
179
179
|
// 2. Insert initial document
|
|
180
180
|
const initialData = { name: 'No-Op Widget', price: 10, description: 'Initial.' };
|
|
181
181
|
const insertResult = await insertData(modelName, initialData, {}, testUser);
|
|
182
|
-
const docId = new ObjectId(insertResult.
|
|
182
|
+
const docId = new ObjectId(insertResult.data._id);
|
|
183
183
|
|
|
184
184
|
// 3. Edit ONLY a non-historized field
|
|
185
185
|
const updateData = { description: 'This change should not be recorded.' };
|
|
@@ -206,7 +206,7 @@ describe('Data History Module Integration Tests', () => {
|
|
|
206
206
|
|
|
207
207
|
// 2. Insert initial document
|
|
208
208
|
const insertResult = await insertData(modelName, { name: 'Time-traveling Widget' }, {}, testUser);
|
|
209
|
-
const docId = new ObjectId(insertResult.
|
|
209
|
+
const docId = new ObjectId(insertResult.data._id);
|
|
210
210
|
|
|
211
211
|
// 3. Create history entries at different times
|
|
212
212
|
// To simulate different timestamps, we'll manually insert history records
|