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.
- package/README.md +9 -9
- package/client/package-lock.json +8430 -8121
- package/client/package.json +7 -4
- package/client/src/APIInfo.jsx +1 -1
- package/client/src/App.jsx +2 -1
- package/client/src/App.scss +1635 -1626
- package/client/src/AssistantChat.jsx +2 -3
- package/client/src/CalendarView.jsx +1 -0
- package/client/src/ContentView.jsx +3 -3
- package/client/src/Dashboard.jsx +5 -2
- package/client/src/DashboardChart.jsx +1 -0
- package/client/src/DashboardFlexViewItem.jsx +1 -0
- package/client/src/DashboardHtmlViewItem.jsx +1 -0
- package/client/src/DashboardView.jsx +2 -0
- package/client/src/DataEditor.jsx +0 -1
- package/client/src/DataImporter.jsx +489 -468
- package/client/src/DataLayout.jsx +25 -23
- package/client/src/DataTable.jsx +6 -5
- package/client/src/Dialog.jsx +92 -90
- package/client/src/Dialog.scss +122 -116
- package/client/src/DisplayFlexNodeRenderer.jsx +1 -0
- package/client/src/DocumentationPageLayout.scss +1 -1
- package/client/src/FlexBuilderControls.jsx +1 -0
- package/client/src/FlexBuilderPreview.jsx +1 -1
- package/client/src/FlexNode.jsx +1 -1
- package/client/src/HistoryDialog.jsx +3 -2
- package/client/src/KPIWidget.jsx +1 -1
- package/client/src/KanbanView.jsx +1 -0
- package/client/src/ModelCreator.jsx +4 -0
- package/client/src/ModelList.jsx +1 -0
- package/client/src/PackGallery.jsx +5 -4
- package/client/src/RTETrans.jsx +1 -1
- package/client/src/RelationField.jsx +2 -0
- package/client/src/RelationSelectorWidget.jsx +2 -0
- package/client/src/RelationValue.jsx +1 -0
- package/client/src/RestoreDialog.jsx +1 -0
- package/client/src/ViewSwitcher.jsx +1 -1
- package/client/src/WorkflowEditor.jsx +3 -0
- package/client/src/contexts/CommandContext.jsx +2 -1
- package/client/src/contexts/ModelContext.jsx +3 -3
- package/client/src/hooks/data.js +1 -0
- package/client/src/hooks/useValidation.js +1 -0
- package/client/src/translations.js +24 -24
- package/client/vite.config.js +31 -30
- package/doc/AI-assistance.md +87 -63
- package/doc/Concepts.md +122 -0
- package/doc/Custom-Endpoints.md +31 -0
- package/doc/Event-system.md +13 -14
- package/doc/Home.md +33 -0
- package/doc/Modules.md +83 -0
- package/doc/Packs-gallery.md +8 -23
- package/doc/Workflows.md +32 -0
- package/doc/automation-workflows.md +141 -102
- package/doc/dashboards-kpis-charts.md +47 -49
- package/doc/data-management.md +126 -120
- package/doc/data-models.md +68 -75
- package/doc/roles-permissions.md +144 -43
- package/doc/sharding-replication.md +158 -0
- package/doc/users.md +54 -30
- package/package.json +27 -17
- package/server.js +37 -37
- package/src/ai.jobs.js +135 -0
- package/src/constants.js +560 -545
- package/src/data.js +521 -518
- package/src/email.js +157 -154
- package/src/engine.js +167 -49
- package/src/gameObject.js +6 -0
- package/src/i18n.js +0 -1
- package/src/modules/assistant/assistant.js +782 -763
- package/src/modules/assistant/constants.js +23 -16
- package/src/modules/assistant/providers.js +77 -37
- package/src/modules/auth-google/index.js +53 -50
- package/src/modules/bucket.js +346 -335
- package/src/modules/data/data.backup.js +400 -376
- package/src/modules/data/data.cluster.js +559 -0
- package/src/modules/data/data.core.js +11 -8
- package/src/modules/data/data.js +311 -311
- package/src/modules/data/data.operations.js +3666 -3555
- package/src/modules/data/data.relations.js +1 -0
- package/src/modules/data/data.replication.js +125 -0
- package/src/modules/data/data.routes.js +2206 -1879
- package/src/modules/data/data.scheduling.js +2 -1
- package/src/modules/file.js +248 -247
- package/src/modules/mongodb.js +76 -73
- package/src/modules/user.js +18 -2
- package/src/modules/worker-script-runner.js +97 -0
- package/src/modules/workflow.js +177 -52
- package/src/packs.js +5701 -5701
- package/src/providers.js +298 -297
- package/src/sso.js +91 -25
- package/test/assistant.test.js +207 -206
- package/test/cluster.test.js +221 -0
- package/test/core.test.js +0 -2
- package/test/data.integration.test.js +1425 -1416
- package/test/import_export.integration.test.js +210 -210
- package/test/replication.test.js +163 -0
- package/test/workflow.actions.integration.test.js +487 -475
- package/test/workflow.integration.test.js +332 -329
- 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
|
|
57
|
-
const
|
|
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 (
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
77
|
-
|
|
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) {
|
|
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
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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}`);
|
package/test/assistant.test.js
CHANGED
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
let
|
|
22
|
-
let
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
{ name: '
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
expect(
|
|
78
|
-
expect(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
{ action: '
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
expect(result.
|
|
105
|
-
expect(result.dataResult
|
|
106
|
-
expect(result.dataResult.
|
|
107
|
-
expect(result.dataResult.data
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
expect(result.
|
|
128
|
-
expect(result.confirmationRequest
|
|
129
|
-
expect(result.confirmationRequest.
|
|
130
|
-
expect(result.confirmationRequest.params.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
expect(dataInDb
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
expect(result.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
filter:
|
|
177
|
-
|
|
178
|
-
"$
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
{ "$
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
});
|