data-primals-engine 1.7.1 → 1.7.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.
Files changed (99) hide show
  1. package/README.md +9 -9
  2. package/client/package-lock.json +8430 -8121
  3. package/client/package.json +7 -4
  4. package/client/src/APIInfo.jsx +1 -1
  5. package/client/src/App.jsx +2 -1
  6. package/client/src/App.scss +1635 -1626
  7. package/client/src/AssistantChat.jsx +2 -3
  8. package/client/src/CalendarView.jsx +1 -0
  9. package/client/src/ContentView.jsx +3 -3
  10. package/client/src/Dashboard.jsx +5 -2
  11. package/client/src/DashboardChart.jsx +1 -0
  12. package/client/src/DashboardFlexViewItem.jsx +1 -0
  13. package/client/src/DashboardHtmlViewItem.jsx +1 -0
  14. package/client/src/DashboardView.jsx +2 -0
  15. package/client/src/DataEditor.jsx +0 -1
  16. package/client/src/DataImporter.jsx +489 -468
  17. package/client/src/DataLayout.jsx +25 -23
  18. package/client/src/DataTable.jsx +6 -5
  19. package/client/src/Dialog.jsx +92 -90
  20. package/client/src/Dialog.scss +122 -116
  21. package/client/src/DisplayFlexNodeRenderer.jsx +1 -0
  22. package/client/src/DocumentationPageLayout.scss +1 -1
  23. package/client/src/FlexBuilderControls.jsx +1 -0
  24. package/client/src/FlexBuilderPreview.jsx +1 -1
  25. package/client/src/FlexNode.jsx +1 -1
  26. package/client/src/HistoryDialog.jsx +3 -2
  27. package/client/src/KPIWidget.jsx +1 -1
  28. package/client/src/KanbanView.jsx +1 -0
  29. package/client/src/ModelCreator.jsx +4 -0
  30. package/client/src/ModelList.jsx +1 -0
  31. package/client/src/PackGallery.jsx +5 -4
  32. package/client/src/RTETrans.jsx +1 -1
  33. package/client/src/RelationField.jsx +2 -0
  34. package/client/src/RelationSelectorWidget.jsx +2 -0
  35. package/client/src/RelationValue.jsx +1 -0
  36. package/client/src/RestoreDialog.jsx +1 -0
  37. package/client/src/ViewSwitcher.jsx +1 -1
  38. package/client/src/WorkflowEditor.jsx +3 -0
  39. package/client/src/contexts/CommandContext.jsx +2 -1
  40. package/client/src/contexts/ModelContext.jsx +3 -3
  41. package/client/src/hooks/data.js +1 -0
  42. package/client/src/hooks/useValidation.js +1 -0
  43. package/client/src/translations.js +24 -24
  44. package/client/vite.config.js +31 -30
  45. package/doc/AI-assistance.md +87 -63
  46. package/doc/Concepts.md +122 -0
  47. package/doc/Custom-Endpoints.md +31 -0
  48. package/doc/Event-system.md +13 -14
  49. package/doc/Home.md +33 -0
  50. package/doc/Modules.md +83 -0
  51. package/doc/Packs-gallery.md +8 -23
  52. package/doc/Workflows.md +32 -0
  53. package/doc/automation-workflows.md +141 -102
  54. package/doc/dashboards-kpis-charts.md +47 -49
  55. package/doc/data-management.md +126 -120
  56. package/doc/data-models.md +68 -75
  57. package/doc/roles-permissions.md +144 -43
  58. package/doc/sharding-replication.md +158 -0
  59. package/doc/users.md +54 -30
  60. package/package.json +27 -17
  61. package/server.js +37 -37
  62. package/src/ai.jobs.js +135 -0
  63. package/src/constants.js +560 -545
  64. package/src/data.js +521 -518
  65. package/src/email.js +157 -154
  66. package/src/engine.js +167 -49
  67. package/src/gameObject.js +6 -0
  68. package/src/i18n.js +0 -1
  69. package/src/modules/assistant/assistant.js +782 -763
  70. package/src/modules/assistant/constants.js +23 -16
  71. package/src/modules/assistant/providers.js +77 -37
  72. package/src/modules/auth-google/index.js +53 -50
  73. package/src/modules/bucket.js +346 -335
  74. package/src/modules/data/data.backup.js +400 -376
  75. package/src/modules/data/data.cluster.js +559 -0
  76. package/src/modules/data/data.core.js +11 -8
  77. package/src/modules/data/data.js +311 -311
  78. package/src/modules/data/data.operations.js +3666 -3555
  79. package/src/modules/data/data.relations.js +1 -0
  80. package/src/modules/data/data.replication.js +125 -0
  81. package/src/modules/data/data.routes.js +2206 -1879
  82. package/src/modules/data/data.scheduling.js +2 -1
  83. package/src/modules/file.js +248 -247
  84. package/src/modules/mongodb.js +76 -73
  85. package/src/modules/user.js +18 -2
  86. package/src/modules/worker-script-runner.js +97 -0
  87. package/src/modules/workflow.js +177 -52
  88. package/src/packs.js +5701 -5701
  89. package/src/providers.js +298 -297
  90. package/src/sso.js +91 -25
  91. package/test/assistant.test.js +207 -206
  92. package/test/cluster.test.js +221 -0
  93. package/test/core.test.js +0 -2
  94. package/test/data.integration.test.js +1425 -1416
  95. package/test/import_export.integration.test.js +210 -210
  96. package/test/replication.test.js +163 -0
  97. package/test/workflow.actions.integration.test.js +487 -475
  98. package/test/workflow.integration.test.js +332 -329
  99. package/doc/core-concepts.md +0 -33
package/src/sso.js CHANGED
@@ -5,6 +5,11 @@ import { cookiesSecret } from "./constants.js";
5
5
  import {UserProvider} from "./providers.js";
6
6
  import {getCollection} from "./modules/mongodb.js";
7
7
  import process from "process";
8
+ import {Event} from "./events.js"
9
+ import { randomBytes } from 'crypto';
10
+ import bcrypt from 'bcrypt';
11
+ import { sendEmail } from './email.js';
12
+ import {getInternalSmtpConfig, getSmtpConfig} from "./modules/user.js";
8
13
 
9
14
  /**
10
15
  * @class SSOUserProvider
@@ -53,35 +58,70 @@ export class SSOUserProvider extends UserProvider {
53
58
 
54
59
  const email = profile.emails[0].value;
55
60
 
56
- // 1. Chercher un utilisateur existant avec cet email.
57
- const existingUser = await this.usersCollection.findOne({ email: email });
61
+ // 1. Chercher un contact existant avec cet email.
62
+ const contactCollection = getCollection("contact");
63
+ const existingContact = await contactCollection.findOne({ email: email });
58
64
 
59
- if (existingUser) {
60
- // TODO: Mettre à jour les informations de l'utilisateur si nécessaire (ex: nom, photo de profil)
61
- // await this.usersCollection.updateOne({ _id: existingUser._id }, { $set: { lastLogin: new Date() } });
62
- return existingUser;
65
+ if (existingContact) {
66
+ // 2. Si le contact existe, chercher l'utilisateur qui lui est lié.
67
+ const existingUser = await this.usersCollection.findOne({ contact: existingContact._id.toString() });
68
+ if (existingUser) {
69
+ // TODO: Mettre à jour les informations de l'utilisateur/contact si nécessaire (ex: nom, photo de profil)
70
+ return existingUser;
71
+ }
63
72
  }
64
73
 
65
- // 2. Si aucun utilisateur n'est trouvé, en créer un nouveau.
74
+ // 3. Si aucun utilisateur n'est trouvé, en créer un nouveau avec son contact.
75
+ const [firstName, ...lastNameParts] = (profile.displayName || email).split(' ');
76
+ const lastName = lastNameParts.join(' ');
77
+
78
+ const newContact = {
79
+ _model: 'contact',
80
+ firstName: firstName,
81
+ lastName: lastName,
82
+ email: email,
83
+ // On pourrait ajouter le _user ici si on a un utilisateur "système" pour les créations SSO
84
+ };
85
+ const contactResult = await contactCollection.insertOne(newContact);
86
+
87
+ // Générer un mot de passe aléatoire et sécurisé que l'utilisateur ne connaîtra pas.
88
+ // Il pourra utiliser le flux "mot de passe oublié" s'il souhaite se connecter localement.
89
+ const randomPassword = randomBytes(32).toString('hex');
90
+ const saltRounds = 10;
91
+ const hashedPassword = await bcrypt.hash(randomPassword, saltRounds);
92
+
66
93
  const newUser = {
94
+ _model: 'user',
67
95
  username: email, // Utiliser l'email comme nom d'utilisateur par défaut
68
- email: email,
69
- name: profile.displayName || email,
96
+ contact: contactResult.insertedId.toString(),
70
97
  provider: profile.provider, // ex: 'google', 'saml'
71
98
  providerId: profile.id,
99
+ hash: hashedPassword, // Mot de passe haché pour une éventuelle connexion locale
72
100
  userPlan: 'free', // Assigner un plan par défaut
73
101
  createdAt: new Date()
74
102
  };
75
103
 
76
- const result = await this.usersCollection.insertOne(newUser);
77
- return await this.findUserById(result.insertedId);
104
+ const userResult = await this.usersCollection.insertOne(newUser);
105
+ const finalUser = await this.findUserById(userResult.insertedId);
106
+
107
+ // Envoyer un e-mail de bienvenue au nouvel utilisateur
108
+ const smtpConfig = getInternalSmtpConfig(finalUser);
109
+ sendEmail(email, { title: "Bienvenue !", content: "Votre compte a été créé avec succès." }, smtpConfig).catch(console.error);
110
+
111
+ return finalUser;
78
112
  }
79
113
 
80
114
  // Les autres méthodes comme validatePassword ne sont pas pertinentes pour le SSO.
81
115
  // On peut les laisser vides ou les faire lever une erreur.
82
116
  async validatePassword(user, password) { return false; }
83
117
  async findUserByUsername(username) { return await this.usersCollection.findOne({ username }); }
84
- async initiateUser(req) { /* Géré par Passport */ }
118
+ async initiateUser(req) {
119
+ const username = req.query._user || req.headers._user || req.cookies.username;
120
+ const user = await this.findUserByUsername(username);
121
+ if (user) {
122
+ req.me = user; // Attache l'utilisateur à la requête
123
+ }
124
+ }
85
125
  }
86
126
 
87
127
  /**
@@ -162,19 +202,45 @@ export class Sso extends Behaviour {
162
202
 
163
203
  const app = this.gameObject;
164
204
 
165
- // Créer la route d'initiation
166
- app.get(routes.authPath, passport.authenticate(name, options));
167
-
168
- // Créer la route de callback
169
- app.get(
170
- routes.callbackPath,
171
- passport.authenticate(name, { failureRedirect: '/login', failureMessage: true }),
172
- (req, res) => {
173
- // Redirection après une authentification réussie
174
- res.redirect(req.session.returnTo || '/dashboard');
175
- delete req.session.returnTo;
176
- }
177
- );
205
+ // --- ROUTE D'INITIATION AMÉLIORÉE ---
206
+ // Nous utilisons un middleware personnalisé pour préserver l'état (query params).
207
+ app.get(routes.authPath, (req, res, next) => {
208
+ this.#logger.debug(`[SSO] Initiating '${name}' auth. Full returnTo URL stored: '${req.session.returnTo}'`);
209
+
210
+ // On sauvegarde la session manuellement avant de rediriger vers le fournisseur SSO.
211
+ // Ceci garantit que `returnTo` est persisté.
212
+ req.session.save(() => {
213
+ passport.authenticate(name, options)(req, res, next);
214
+ });
215
+ });
216
+
217
+ // --- ROUTE DE CALLBACK AMÉLIORÉE ---
218
+ // Nous utilisons un callback personnalisé pour déclencher des événements.
219
+ app.get(routes.callbackPath, (req, res, next) => {
220
+ passport.authenticate(name, { failureRedirect: '/login', failureMessage: true }, async (err, user, info) => {
221
+ if (err) { return next(err); }
222
+ if (!user) { return res.redirect('/login'); }
223
+
224
+ // Sauvegarder l'URL de retour avant que req.logIn() ne régénère potentiellement la session.
225
+ const returnToUrl = req.session.returnTo;
226
+
227
+ req.logIn(user, async (loginErr) => {
228
+ if (loginErr) { return next(loginErr); }
229
+
230
+ // Restaurer l'URL de retour dans la nouvelle session.
231
+ req.session.returnTo = returnToUrl;
232
+
233
+ // *** DÉCLENCHEMENT DE L'ÉVÉNEMENT OnSSOLogin ***
234
+ await Event.Trigger("OnSsoLogin", "event", "system", { req, res, user, ssoProfile: info?.profile });
235
+
236
+ // Après que l'événement a mis à jour la session (par exemple, avec un nouveau token),
237
+ // nous pouvons rediriger l'utilisateur.
238
+ // On restaure une dernière fois au cas où la session aurait été régénérée.
239
+ const finalReturnTo = req.session.returnTo || '/';
240
+ res.redirect(finalReturnTo);
241
+ });
242
+ })(req, res, next);
243
+ });
178
244
 
179
245
  this.strategies.set(name, { strategy, routes, options });
180
246
  this.#logger.info(`Passport strategy '${name}' registered with routes: ${routes.authPath}, ${routes.callbackPath}`);
@@ -1,207 +1,208 @@
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
- });
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
+ findFirstAvailableProvider: vi.fn().mockResolvedValue({ provider: 'OpenAI', apiKey: 'mock-api-key' }),
16
+ }));
17
+
18
+ // On importe la version mockée de getAIProvider
19
+ import { getAIProvider, findFirstAvailableProvider } from '../src/modules/assistant/providers.js';
20
+
21
+ let testUser;
22
+ let modelsCollection;
23
+ let datasCollection;
24
+
25
+ const modelName = generateUniqueName('productAssistant');
26
+
27
+ describe('Assistant Module Unit Tests', () => {
28
+
29
+ beforeAll(async () => {
30
+ Config.Set("modules", ["mongodb", "data", "user", "assistant"]);
31
+ await initEngine(); // Cette ligne ne devrait plus planter.
32
+ testUser = {
33
+ username: generateUniqueName('testUserAssistant'),
34
+ email: generateUniqueName('test') + '@example.com',
35
+ lang: 'fr'
36
+ };
37
+
38
+ modelsCollection = getCollection('models');
39
+ datasCollection = await getCollectionForUser(testUser);
40
+
41
+ // Create a model for testing
42
+ const productModelDef = {
43
+ name: modelName,
44
+ description: 'A model for testing',
45
+ _user: testUser.username,
46
+ fields: [
47
+ { name: 'name', type: 'string', required: true },
48
+ { name: 'price', type: 'number' }
49
+ ]
50
+ };
51
+ await modelsCollection.insertOne(productModelDef);
52
+ });
53
+
54
+ beforeEach(async () => {
55
+ // Clear mocks before each test
56
+ vi.clearAllMocks();
57
+ // Clean data, but keep the model
58
+ await datasCollection.deleteMany({ _user: testUser.username });
59
+ });
60
+
61
+ // Helper to create a mock LLM stream
62
+ const createMockStream = (content) => {
63
+ return (async function* () {
64
+ yield { content };
65
+ })();
66
+ };
67
+
68
+ it('should return a simple text message from the AI', async () => {
69
+ const mockLLM = {
70
+ stream: vi.fn().mockReturnValue(createMockStream('Bonjour, ceci est un test.')),
71
+ };
72
+ getAIProvider.mockResolvedValue(mockLLM);
73
+
74
+ const params = { message: 'Dis bonjour', history: [] };
75
+ const result = await handleChatRequest(params, testUser);
76
+
77
+ expect(getAIProvider).toHaveBeenCalled();
78
+ expect(mockLLM.stream).toHaveBeenCalled();
79
+ expect(result).toEqual({
80
+ success: true,
81
+ displayMessage: 'Bonjour, ceci est un test.'
82
+ });
83
+ });
84
+
85
+ it('should handle a search action', async () => {
86
+
87
+ // Configure le retour du mock pour ce test spécifique
88
+ const searchDataSpy = vi.spyOn(dataOperations, 'searchData').mockResolvedValue({ data: [{ name: 'Test Widget', price: 100 }], count: 1 });
89
+
90
+ const aiResponse = JSON.stringify([
91
+ { action: 'search_models', params: { query: modelName } },
92
+ { action: 'search', params: { model: modelName, filter: {} } }
93
+ ]);
94
+ const mockLLM = {
95
+ stream: vi.fn().mockReturnValue(createMockStream(aiResponse))
96
+ };
97
+ getAIProvider.mockResolvedValue(mockLLM);
98
+
99
+ const params = { message: `cherche les produits`, history: [] };
100
+ const result = await handleChatRequest(params, testUser);
101
+
102
+ expect(searchDataSpy).toHaveBeenCalled();
103
+
104
+ expect(result.success).toBe(true);
105
+ expect(result.dataResult).toBeDefined();
106
+ expect(result.dataResult.model).toBe(modelName);
107
+ expect(result.dataResult.data).toHaveLength(1);
108
+ expect(result.dataResult.data[0].name).toBe('Test Widget');
109
+ });
110
+
111
+ it('should request confirmation for a "post" action', async () => {
112
+ const aiResponse = JSON.stringify({
113
+ action: 'post',
114
+ params: {
115
+ model: modelName,
116
+ data: { name: 'New Gadget', price: 19.99 }
117
+ }
118
+ });
119
+ const mockLLM = {
120
+ stream: vi.fn().mockReturnValue(createMockStream(aiResponse)),
121
+ };
122
+ getAIProvider.mockResolvedValue(mockLLM);
123
+
124
+ const params = { message: 'Crée un produit', history: [] };
125
+ const result = await handleChatRequest(params, testUser);
126
+
127
+ expect(result.success).toBe(true);
128
+ expect(result.confirmationRequest).toBeDefined();
129
+ expect(result.confirmationRequest.action).toBe('post');
130
+ expect(result.confirmationRequest.params.model).toBe(modelName);
131
+ expect(result.confirmationRequest.params.data.name).toBe('New Gadget');
132
+ });
133
+
134
+ it('should execute a confirmed "post" action', async () => {
135
+ const confirmedAction = {
136
+ action: 'post',
137
+ params: {
138
+ model: modelName,
139
+ data: { name: 'Confirmed Gadget', price: 50 }
140
+ }
141
+ };
142
+
143
+ const params = { message: '', confirmedAction };
144
+ const result = await handleChatRequest(params, testUser);
145
+
146
+ expect(result.success).toBe(true);
147
+ // --- CORRECTION ---
148
+ // On vérifie le message de succès spécifique à la création, qui est plus informatif.
149
+ expect(result.displayMessage).toContain("Élément créé avec l'ID:");
150
+
151
+ // Verify the data was actually inserted
152
+ const dataInDb = await datasCollection.findOne({ name: 'Confirmed Gadget' });
153
+ expect(dataInDb).not.toBeNull();
154
+ expect(dataInDb.price).toBe(50);
155
+ });
156
+
157
+ it('should handle malformed JSON from AI by returning it as a text message', async () => {
158
+ const aiResponse = `{ "action": "search", "params": { "model": "${modelName}" }`; // Missing closing brace
159
+ const mockLLM = {
160
+ stream: vi.fn().mockReturnValue(createMockStream(aiResponse)),
161
+ };
162
+ getAIProvider.mockResolvedValue(mockLLM);
163
+
164
+ const params = { message: 'test', history: [] };
165
+ const result = await handleChatRequest(params, testUser);
166
+
167
+ expect(result.success).toBe(true);
168
+ expect(result.displayMessage).toBe(aiResponse);
169
+ });
170
+
171
+ it('should correct a malformed filter from the AI', async () => {
172
+ const aiResponse = JSON.stringify([{
173
+ action: 'search',
174
+ params: {
175
+ model: modelName,
176
+ // This filter is malformed: multiple operators at the root level
177
+ filter: {
178
+ "$gt": ["$price", 50],
179
+ "$lt": ["$price", 200]
180
+ }
181
+ }
182
+ }]);
183
+ const mockLLM = {
184
+ stream: vi.fn().mockReturnValue(createMockStream(aiResponse)),
185
+ };
186
+ getAIProvider.mockResolvedValue(mockLLM);
187
+
188
+ // Configure le retour du mock pour ce test spécifique
189
+ const searchDataSpy = vi.spyOn(dataOperations, 'searchData').mockResolvedValue({ data: [], count: 0 });
190
+
191
+ const params = { message: 'cherche produits entre 50 et 200', history: [] };
192
+ // On peut maintenant utiliser la fonction handleChatRequest importée normalement
193
+ await handleChatRequest(params, testUser);
194
+
195
+ // On s'attend à ce que le filtre ait été encapsulé dans un $and
196
+ const expectedFilter = {
197
+ "$and": [
198
+ { "$gt": ["$price", 50] },
199
+ { "$lt": ["$price", 200] }
200
+ ]
201
+ };
202
+
203
+ expect(searchDataSpy).toHaveBeenCalledWith(expect.objectContaining({ filter: expectedFilter }), expect.anything());
204
+
205
+ // On nettoie l'espion après le test
206
+ searchDataSpy.mockRestore();
207
+ });
207
208
  });