@yrpri/api 9.0.231 → 9.0.232

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 (68) hide show
  1. package/controllers/allOurIdeas.js +1 -1
  2. package/controllers/communities.cjs +282 -91
  3. package/controllers/domains.cjs +54 -8
  4. package/controllers/groups.cjs +51 -6
  5. package/controllers/points.cjs +4 -9
  6. package/controllers/posts.cjs +7 -9
  7. package/controllers/ratings.cjs +3 -6
  8. package/models/point.cjs +31 -3
  9. package/models/post.cjs +2 -3
  10. package/package.json +2 -2
  11. package/services/engine/allOurIdeas/aiHelper.d.ts +7 -4
  12. package/services/engine/allOurIdeas/aiHelper.js +34 -19
  13. package/services/engine/allOurIdeas/explainAnswersAssistant.d.ts +1 -1
  14. package/services/engine/allOurIdeas/explainAnswersAssistant.js +3 -9
  15. package/services/engine/moderation/fraud/CreateFraudAuditReport.cjs +35 -11
  16. package/services/engine/moderation/fraud/CreateFraudAuditReport.d.cts +21 -0
  17. package/services/engine/moderation/fraud/FraudBase.cjs +38 -18
  18. package/services/engine/moderation/fraud/FraudBase.d.cts +2 -0
  19. package/services/engine/moderation/fraud/FraudDeleteBase.cjs +48 -29
  20. package/services/engine/moderation/fraud/FraudDeleteBase.d.cts +8 -6
  21. package/services/engine/moderation/fraud/FraudDeleteEndorsements.cjs +5 -4
  22. package/services/engine/moderation/fraud/FraudDeleteEndorsements.d.cts +2 -2
  23. package/services/engine/moderation/fraud/FraudDeletePointQualities.cjs +3 -2
  24. package/services/engine/moderation/fraud/FraudDeletePointQualities.d.cts +1 -1
  25. package/services/engine/moderation/fraud/FraudDeletePoints.cjs +3 -2
  26. package/services/engine/moderation/fraud/FraudDeletePoints.d.cts +1 -1
  27. package/services/engine/moderation/fraud/FraudDeletePosts.cjs +3 -2
  28. package/services/engine/moderation/fraud/FraudDeleteRatings.cjs +61 -4
  29. package/services/engine/moderation/fraud/FraudGetBase.cjs +44 -20
  30. package/services/engine/moderation/fraud/FraudGetBase.d.cts +5 -0
  31. package/services/engine/moderation/fraud/FraudGetEndorsements.cjs +4 -13
  32. package/services/engine/moderation/fraud/FraudGetEndorsements.d.cts +1 -1
  33. package/services/engine/moderation/fraud/FraudGetPointQualities.cjs +3 -0
  34. package/services/engine/moderation/fraud/FraudGetPointQualities.d.cts +1 -1
  35. package/services/engine/moderation/fraud/FraudGetPoints.cjs +3 -0
  36. package/services/engine/moderation/fraud/FraudGetPoints.d.cts +1 -1
  37. package/services/engine/moderation/fraud/FraudGetPosts.cjs +17 -16
  38. package/services/engine/moderation/fraud/FraudGetPosts.d.cts +3 -3
  39. package/services/engine/moderation/fraud/FraudGetRatings.cjs +62 -30
  40. package/services/engine/moderation/fraud/FraudGetRatings.d.cts +4 -1
  41. package/services/engine/moderation/fraud/FraudRequestValidation.cjs +143 -0
  42. package/services/engine/moderation/fraud/FraudRequestValidation.d.cts +21 -0
  43. package/services/engine/moderation/fraud/FraudScannerNotifier.cjs +59 -35
  44. package/services/engine/moderation/fraud/FraudScannerNotifier.d.cts +20 -1
  45. package/services/llms/baseChatBot.d.ts +2 -0
  46. package/services/llms/baseChatBot.js +25 -9
  47. package/services/llms/imageGeneration/chatGptImageGenerator.d.ts +2 -2
  48. package/services/llms/imageGeneration/chatGptImageGenerator.js +13 -10
  49. package/services/llms/imageGeneration/collectionImageGenerator.js +31 -13
  50. package/services/llms/imageGeneration/dalleImageGenerator.d.ts +2 -2
  51. package/services/llms/imageGeneration/dalleImageGenerator.js +28 -16
  52. package/services/llms/imageGeneration/fluxImageGenerator.d.ts +2 -2
  53. package/services/llms/imageGeneration/fluxImageGenerator.js +9 -3
  54. package/services/llms/imageGeneration/iImageGenerator.d.ts +8 -1
  55. package/services/llms/imageGeneration/imageModelConfig.cjs +319 -0
  56. package/services/llms/imageGeneration/imageModelConfig.d.cts +79 -0
  57. package/services/llms/imageGeneration/imagenImageGenerator.d.ts +2 -3
  58. package/services/llms/imageGeneration/imagenImageGenerator.js +10 -10
  59. package/tests/fraudManagement.test.cjs +470 -0
  60. package/tests/fraudManagement.test.d.cts +1 -0
  61. package/tests/imageModelConfig.test.cjs +144 -0
  62. package/tests/imageModelConfig.test.d.cts +1 -0
  63. package/utils/ai_image_generation_guard.cjs +268 -0
  64. package/utils/ai_image_generation_guard.d.cts +34 -0
  65. package/utils/fingerprint_data.cjs +32 -0
  66. package/utils/fingerprint_data.d.cts +6 -0
  67. package/utils/recount_utils.cjs +53 -37
  68. package/utils/recount_utils.d.cts +7 -7
@@ -0,0 +1,470 @@
1
+ "use strict";
2
+ const test = require("node:test");
3
+ const assert = require("node:assert/strict");
4
+ const Module = require("node:module");
5
+ const path = require("node:path");
6
+ const modelsPath = path.resolve(__dirname, "../models/index.cjs");
7
+ const recountUtilsPath = require.resolve("../utils/recount_utils.cjs");
8
+ const loggerPath = require.resolve("../utils/logger.cjs");
9
+ const fraudDeleteBasePath = require.resolve("../services/engine/moderation/fraud/FraudDeleteBase.cjs");
10
+ const fraudDeleteEndorsementsPath = require.resolve("../services/engine/moderation/fraud/FraudDeleteEndorsements.cjs");
11
+ const fraudDeleteRatingsPath = require.resolve("../services/engine/moderation/fraud/FraudDeleteRatings.cjs");
12
+ const fraudGetBasePath = require.resolve("../services/engine/moderation/fraud/FraudGetBase.cjs");
13
+ const fraudGetPostsPath = require.resolve("../services/engine/moderation/fraud/FraudGetPosts.cjs");
14
+ const fraudBasePath = require.resolve("../services/engine/moderation/fraud/FraudBase.cjs");
15
+ const fraudScannerNotifierPath = require.resolve("../services/engine/moderation/fraud/FraudScannerNotifier.cjs");
16
+ const validationPath = require.resolve("../services/engine/moderation/fraud/FraudRequestValidation.cjs");
17
+ const fraudAuditReportPath = require.resolve("../services/engine/moderation/fraud/CreateFraudAuditReport.cjs");
18
+ const queuePath = require.resolve("../services/workers/queue.cjs");
19
+ const injectMockModule = (modulePath, moduleExports) => {
20
+ const mockModule = new Module(modulePath);
21
+ mockModule.filename = modulePath;
22
+ mockModule.loaded = true;
23
+ mockModule.exports = moduleExports;
24
+ require.cache[modulePath] = mockModule;
25
+ };
26
+ const loadFraudModules = (fakeModels = {}, fakeRecountUtils) => {
27
+ const originalResolveFilename = Module._resolveFilename;
28
+ const pathsToRestore = [
29
+ modelsPath,
30
+ recountUtilsPath,
31
+ loggerPath,
32
+ fraudDeleteBasePath,
33
+ fraudDeleteEndorsementsPath,
34
+ fraudDeleteRatingsPath,
35
+ fraudGetBasePath,
36
+ fraudGetPostsPath,
37
+ fraudBasePath,
38
+ fraudScannerNotifierPath,
39
+ fraudAuditReportPath,
40
+ queuePath,
41
+ ];
42
+ const originals = new Map(pathsToRestore.map(path => [path, require.cache[path]]));
43
+ injectMockModule(modelsPath, fakeModels);
44
+ injectMockModule(recountUtilsPath, fakeRecountUtils || {
45
+ recountCommunity: (_communityId, done) => done(),
46
+ recountPoints: (_pointIds, done) => done(),
47
+ recountPosts: (_postIds, done) => done(),
48
+ });
49
+ injectMockModule(loggerPath, {
50
+ error() { },
51
+ info() { },
52
+ warn() { },
53
+ });
54
+ injectMockModule(queuePath, {
55
+ add() { },
56
+ });
57
+ Module._resolveFilename = function (request, parent, isMain, options) {
58
+ if (request.endsWith("models/index.cjs")) {
59
+ return modelsPath;
60
+ }
61
+ return originalResolveFilename.call(this, request, parent, isMain, options);
62
+ };
63
+ for (const modulePath of [
64
+ fraudDeleteBasePath,
65
+ fraudDeleteEndorsementsPath,
66
+ fraudDeleteRatingsPath,
67
+ fraudGetBasePath,
68
+ fraudGetPostsPath,
69
+ fraudBasePath,
70
+ fraudScannerNotifierPath,
71
+ fraudAuditReportPath,
72
+ ]) {
73
+ delete require.cache[modulePath];
74
+ }
75
+ const scannerModule = require(fraudScannerNotifierPath);
76
+ return {
77
+ FraudDeleteBase: require(fraudDeleteBasePath),
78
+ FraudDeleteRatings: require(fraudDeleteRatingsPath),
79
+ FraudGetBase: require(fraudGetBasePath),
80
+ FraudGetPosts: require(fraudGetPostsPath),
81
+ FraudScannerNotifier: scannerModule.FraudScannerNotifier,
82
+ FraudAuditReport: require(fraudAuditReportPath).FraudAuditReport,
83
+ runFraudScannerNotifier: scannerModule.runFraudScannerNotifier,
84
+ sanitizeWorksheetName: require(fraudAuditReportPath).sanitizeWorksheetName,
85
+ restore() {
86
+ Module._resolveFilename = originalResolveFilename;
87
+ for (const [modulePath, originalModule] of originals) {
88
+ if (originalModule) {
89
+ require.cache[modulePath] = originalModule;
90
+ }
91
+ else {
92
+ delete require.cache[modulePath];
93
+ }
94
+ }
95
+ },
96
+ };
97
+ };
98
+ const makeFraudItem = (overrides = {}) => {
99
+ return {
100
+ id: 1,
101
+ created_at: "2026-01-01T00:00:00.000Z",
102
+ value: 1,
103
+ post_id: 10,
104
+ user_id: 20,
105
+ user_agent: "test-agent",
106
+ ip_address: "127.0.0.1",
107
+ data: {},
108
+ dataValues: {
109
+ backgroundColor: "#ffffff",
110
+ confidenceScore: "80%",
111
+ },
112
+ User: {
113
+ id: 20,
114
+ email: "user@example.com",
115
+ name: "User",
116
+ },
117
+ Post: {
118
+ id: 10,
119
+ name: "Post",
120
+ },
121
+ ...overrides,
122
+ };
123
+ };
124
+ test("fraud scan compression tolerates missing user associations", (t) => {
125
+ const { FraudGetBase, restore } = loadFraudModules();
126
+ t.after(restore);
127
+ const engine = new FraudGetBase({ collectionType: "ratings" });
128
+ engine.dataToProcess = [
129
+ {
130
+ key: "127.0.0.1:fingerprint",
131
+ items: [
132
+ makeFraudItem({
133
+ User: null,
134
+ }),
135
+ ],
136
+ },
137
+ ];
138
+ assert.doesNotThrow(() => engine.customCompress());
139
+ assert.deepEqual(engine.dataToProcess.cEmails, [""]);
140
+ assert.deepEqual(engine.dataToProcess.cNames, [""]);
141
+ assert.equal(engine.dataToProcess.items[0].User.email, 0);
142
+ assert.equal(engine.dataToProcess.items[0].User.name, 0);
143
+ });
144
+ test("fraud scan compression tolerates missing parent associations", (t) => {
145
+ const { FraudGetBase, restore } = loadFraudModules();
146
+ t.after(restore);
147
+ const ratingsEngine = new FraudGetBase({ collectionType: "ratings" });
148
+ ratingsEngine.dataToProcess = [
149
+ {
150
+ key: "127.0.0.1:fingerprint",
151
+ items: [
152
+ makeFraudItem({
153
+ Post: null,
154
+ }),
155
+ ],
156
+ },
157
+ ];
158
+ assert.doesNotThrow(() => ratingsEngine.customCompress());
159
+ assert.deepEqual(ratingsEngine.dataToProcess.cPostNames, [""]);
160
+ assert.equal(ratingsEngine.dataToProcess.items[0].Post.name, 0);
161
+ const pointQualityEngine = new FraudGetBase({ collectionType: "pointQualities" });
162
+ pointQualityEngine.dataToProcess = [
163
+ {
164
+ key: "127.0.0.1:fingerprint",
165
+ items: [
166
+ makeFraudItem({
167
+ Point: null,
168
+ }),
169
+ ],
170
+ },
171
+ ];
172
+ assert.doesNotThrow(() => pointQualityEngine.customCompress());
173
+ assert.deepEqual(pointQualityEngine.dataToProcess.cPostNames, [""]);
174
+ assert.equal(pointQualityEngine.dataToProcess.items[0].Point.Post.name, 0);
175
+ });
176
+ test("post fraud detection counts post rows by id", (t) => {
177
+ const { FraudGetPosts, restore } = loadFraudModules();
178
+ t.after(restore);
179
+ const postItems = Array.from({ length: 20 }, (_value, index) => {
180
+ return makeFraudItem({
181
+ id: index + 1,
182
+ post_id: undefined,
183
+ created_at: `2026-01-01T00:${String(index).padStart(2, "0")}:00.000Z`,
184
+ data: {
185
+ browserFingerprint: "fingerprint",
186
+ },
187
+ });
188
+ });
189
+ const groupedItems = {
190
+ "127.0.0.1:fingerprint": postItems,
191
+ };
192
+ const engine = new FraudGetPosts({ collectionType: "posts" });
193
+ assert.deepEqual(engine.getTopItems(groupedItems, "byIpFingerprint"), []);
194
+ assert.deepEqual(engine.getTopItems(groupedItems, "byIpAddress"), []);
195
+ });
196
+ test("fraud scanner notifier is safe to import", (t) => {
197
+ const originalExit = process.exit;
198
+ let exitCalled = false;
199
+ process.exit = ((code) => {
200
+ exitCalled = true;
201
+ throw new Error(`Unexpected process.exit(${code})`);
202
+ });
203
+ const { FraudScannerNotifier, runFraudScannerNotifier, restore, } = loadFraudModules();
204
+ t.after(() => {
205
+ process.exit = originalExit;
206
+ restore();
207
+ });
208
+ assert.equal(typeof FraudScannerNotifier, "function");
209
+ assert.equal(typeof runFraudScannerNotifier, "function");
210
+ assert.equal(exitCalled, false);
211
+ });
212
+ test("fraud delete grouping tolerates missing user associations", (t) => {
213
+ const { FraudDeleteBase, restore } = loadFraudModules();
214
+ t.after(restore);
215
+ const engine = new FraudDeleteBase({ type: "delete-items" });
216
+ const itemsToDelete = engine.getAllItemsExceptOne([
217
+ makeFraudItem({
218
+ id: 1,
219
+ created_at: "2026-01-01T00:00:00.000Z",
220
+ User: null,
221
+ }),
222
+ makeFraudItem({
223
+ id: 2,
224
+ created_at: "2026-01-02T00:00:00.000Z",
225
+ User: undefined,
226
+ }),
227
+ ]);
228
+ assert.equal(itemsToDelete.length, 1);
229
+ assert.equal(itemsToDelete[0].id, 1);
230
+ });
231
+ test("rating fraud deletion preserves one row per custom rating dimension", (t) => {
232
+ const { FraudDeleteRatings, restore } = loadFraudModules();
233
+ t.after(restore);
234
+ const engine = new FraudDeleteRatings({ type: "delete-items" });
235
+ const itemsToDelete = engine.getAllItemsExceptOne([
236
+ makeFraudItem({
237
+ id: 1,
238
+ type_index: 0,
239
+ created_at: "2026-01-01T00:00:00.000Z",
240
+ }),
241
+ makeFraudItem({
242
+ id: 2,
243
+ type_index: 0,
244
+ created_at: "2026-01-02T00:00:00.000Z",
245
+ }),
246
+ makeFraudItem({
247
+ id: 3,
248
+ type_index: 1,
249
+ created_at: "2026-01-01T00:00:00.000Z",
250
+ }),
251
+ makeFraudItem({
252
+ id: 4,
253
+ type_index: 1,
254
+ created_at: "2026-01-02T00:00:00.000Z",
255
+ }),
256
+ ]);
257
+ assert.deepEqual(itemsToDelete.map(item => item.id), [2, 4]);
258
+ });
259
+ test("fraud deletion and audit log share one transaction", async (t) => {
260
+ const transaction = { id: "tx" };
261
+ const seenTransactions = [];
262
+ let auditLogData;
263
+ const fakeModels = {
264
+ sequelize: {
265
+ async transaction(callback) {
266
+ return callback(transaction);
267
+ },
268
+ },
269
+ AcBackgroundJob: {
270
+ async findOne() {
271
+ return {
272
+ internal_data: {
273
+ idsToDelete: [1, 2],
274
+ },
275
+ };
276
+ },
277
+ async updateProgressAsync() { },
278
+ async updateErrorAsync() { },
279
+ },
280
+ User: {
281
+ async findOne(options) {
282
+ seenTransactions.push(options.transaction);
283
+ return { name: "Admin" };
284
+ },
285
+ },
286
+ GeneralDataStore: {
287
+ async create(data, options) {
288
+ seenTransactions.push(options.transaction);
289
+ auditLogData = data.data;
290
+ return {
291
+ id: 123,
292
+ data: data.data,
293
+ };
294
+ },
295
+ },
296
+ Community: {
297
+ async findOne(options) {
298
+ seenTransactions.push(options.transaction);
299
+ return {
300
+ data: {},
301
+ set(_path, value) {
302
+ this.data.fraudDeletionsAuditLogs = value;
303
+ },
304
+ changed() { },
305
+ async save(options) {
306
+ seenTransactions.push(options.transaction);
307
+ },
308
+ };
309
+ },
310
+ },
311
+ };
312
+ const fakeRecountUtils = {
313
+ recountCommunity: (_communityId, done, transactionArg) => {
314
+ seenTransactions.push(transactionArg);
315
+ done();
316
+ },
317
+ recountPoints: (_pointIds, done, transactionArg) => {
318
+ seenTransactions.push(transactionArg);
319
+ done();
320
+ },
321
+ recountPosts: (_postIds, done, transactionArg) => {
322
+ seenTransactions.push(transactionArg);
323
+ done();
324
+ },
325
+ };
326
+ const { FraudDeleteBase, restore } = loadFraudModules(fakeModels, fakeRecountUtils);
327
+ t.after(restore);
328
+ class TestFraudDelete extends FraudDeleteBase {
329
+ async getItemsById() {
330
+ return [
331
+ makeFraudItem({ id: 1, created_at: "2026-01-01T00:00:00.000Z" }),
332
+ makeFraudItem({ id: 2, created_at: "2026-01-02T00:00:00.000Z" }),
333
+ ];
334
+ }
335
+ setupDataToProcess() {
336
+ this.dataToProcess = {
337
+ group: {
338
+ items: this.items,
339
+ },
340
+ };
341
+ }
342
+ async destroyChunkItems(_items, transactionArg) {
343
+ seenTransactions.push(transactionArg);
344
+ }
345
+ }
346
+ const engine = new TestFraudDelete({
347
+ type: "delete-items",
348
+ jobId: 1,
349
+ userId: 2,
350
+ communityId: 3,
351
+ });
352
+ await engine.deleteItems();
353
+ assert.equal(seenTransactions.length, 6);
354
+ assert.ok(seenTransactions.every(item => item === transaction));
355
+ assert.deepEqual(auditLogData.deleteData.idsToDelete, [2]);
356
+ assert.deepEqual(auditLogData.deleteData.requestedIdsToDelete, [1, 2]);
357
+ });
358
+ test("fraud action request validation rejects unsafe and invalid inputs", () => {
359
+ const { MAX_FRAUD_IDS_TO_DELETE, validateFraudActionRequest, } = require(validationPath);
360
+ assert.equal(validateFraudActionRequest({
361
+ type: "explode",
362
+ selectedMethod: "byIpAddress",
363
+ collectionType: "ratings",
364
+ }).error, "invalid_fraud_action_type");
365
+ assert.equal(validateFraudActionRequest({
366
+ type: "get-items",
367
+ selectedMethod: "byIpFingerprintPointId",
368
+ collectionType: "ratings",
369
+ }).error, "invalid_fraud_detection_method");
370
+ assert.equal(validateFraudActionRequest({
371
+ type: "get-items",
372
+ selectedMethod: "byIpFingerprintPostId",
373
+ collectionType: "posts",
374
+ }).error, "invalid_fraud_detection_method");
375
+ assert.equal(validateFraudActionRequest({
376
+ type: "delete-one-item",
377
+ selectedMethod: "byIpAddress",
378
+ collectionType: "ratings",
379
+ idsToDelete: [1, 2],
380
+ }).error, "single_delete_requires_one_id");
381
+ assert.equal(validateFraudActionRequest({
382
+ type: "delete-items",
383
+ selectedMethod: "byIpAddress",
384
+ collectionType: "ratings",
385
+ idsToDelete: [],
386
+ }).error, "delete_requires_ids");
387
+ assert.equal(validateFraudActionRequest({
388
+ type: "delete-items",
389
+ selectedMethod: "byIpAddress",
390
+ collectionType: "ratings",
391
+ idsToDelete: "1",
392
+ }).error, "invalid_ids_to_delete");
393
+ assert.equal(validateFraudActionRequest({
394
+ type: "delete-items",
395
+ selectedMethod: "byIpAddress",
396
+ collectionType: "ratings",
397
+ idsToDelete: [1, 0],
398
+ }).error, "invalid_ids_to_delete");
399
+ assert.equal(validateFraudActionRequest({
400
+ type: "delete-items",
401
+ selectedMethod: "byIpAddress",
402
+ collectionType: "ratings",
403
+ idsToDelete: [1, "bad"],
404
+ }).error, "invalid_ids_to_delete");
405
+ assert.equal(validateFraudActionRequest({
406
+ type: "delete-items",
407
+ selectedMethod: "byIpAddress",
408
+ collectionType: "ratings",
409
+ idsToDelete: Array.from({ length: MAX_FRAUD_IDS_TO_DELETE + 1 }, (_value, index) => index + 1),
410
+ }).error, "too_many_ids_to_delete");
411
+ assert.equal(validateFraudActionRequest({
412
+ type: "delete-items",
413
+ selectedMethod: "byMissingBrowserFingerprint",
414
+ collectionType: "ratings",
415
+ idsToDelete: [1],
416
+ }).error, "bulk_delete_missing_fingerprint_disabled");
417
+ assert.deepEqual(validateFraudActionRequest({
418
+ type: "delete-items",
419
+ selectedMethod: "byIpAddress",
420
+ collectionType: "ratings",
421
+ idsToDelete: [3, "2", 3, "2"],
422
+ }), { idsToDelete: [3, 2] });
423
+ assert.deepEqual(validateFraudActionRequest({
424
+ type: "get-items",
425
+ selectedMethod: "byIpFingerprintPointId",
426
+ collectionType: "pointQualities",
427
+ idsToDelete: [1],
428
+ }), { idsToDelete: [1] });
429
+ });
430
+ test("fraud audit report validates community ownership and xlsx metadata", (t) => {
431
+ const { FraudAuditReport, sanitizeWorksheetName, restore } = loadFraudModules();
432
+ t.after(restore);
433
+ const report = new FraudAuditReport({
434
+ communityId: 10,
435
+ community: {
436
+ id: 10,
437
+ name: "Community / With * Invalid : Characters",
438
+ },
439
+ userName: "Admin [With] A Very Very Very Long Name",
440
+ auditReportData: {
441
+ workPackage: {
442
+ collectionType: "posts",
443
+ },
444
+ },
445
+ });
446
+ assert.doesNotThrow(() => report.validateAuditReportCommunity({
447
+ data: {
448
+ workPackage: {
449
+ communityId: "10",
450
+ },
451
+ },
452
+ }));
453
+ assert.throws(() => report.validateAuditReportCommunity({
454
+ data: {
455
+ workPackage: {
456
+ communityId: 11,
457
+ },
458
+ },
459
+ }), /does not belong/);
460
+ report.setupFilename();
461
+ assert.ok(report.workPackage.filename.endsWith(".xlsx"));
462
+ assert.equal(report.workPackage.fileEnding, "xlsx");
463
+ const worksheetName = sanitizeWorksheetName("Community Users 10 Admin [With] / Invalid * Long Long Long Long");
464
+ assert.ok(worksheetName.length <= 31);
465
+ assert.equal(/[\\/\*\?:\[\]]/.test(worksheetName), false);
466
+ const apostropheBoundaryName = sanitizeWorksheetName("Community Users 10 ABCDEFGHIJK'LMNO");
467
+ assert.ok(apostropheBoundaryName.length <= 31);
468
+ assert.equal(apostropheBoundaryName.endsWith("'"), false);
469
+ assert.equal(apostropheBoundaryName.startsWith("'"), false);
470
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ const test = require("node:test");
3
+ const assert = require("node:assert/strict");
4
+ const { getAspectRatioForImageSize, getDefaultImageSizeForOptions, normalizeImageGenerationProfileOptions, normalizeImageGenerationOptions, } = require("../services/llms/imageGeneration/imageModelConfig.cjs");
5
+ const withEnv = (updates, fn) => {
6
+ const originals = {};
7
+ for (const key of Object.keys(updates)) {
8
+ originals[key] = process.env[key];
9
+ if (updates[key] === undefined) {
10
+ delete process.env[key];
11
+ }
12
+ else {
13
+ process.env[key] = updates[key];
14
+ }
15
+ }
16
+ try {
17
+ fn();
18
+ }
19
+ finally {
20
+ for (const key of Object.keys(updates)) {
21
+ if (originals[key] === undefined) {
22
+ delete process.env[key];
23
+ }
24
+ else {
25
+ process.env[key] = originals[key];
26
+ }
27
+ }
28
+ }
29
+ };
30
+ test("image generation options default to OpenAI gpt-image-2", () => {
31
+ const options = normalizeImageGenerationOptions(undefined, undefined);
32
+ assert.deepEqual(options, {
33
+ imageProvider: "openai",
34
+ imageModel: "gpt-image-2",
35
+ });
36
+ });
37
+ test("image generation options default Imagen to Imagen 4 GA", () => {
38
+ const options = normalizeImageGenerationOptions("imagen", undefined);
39
+ assert.deepEqual(options, {
40
+ imageProvider: "imagen",
41
+ imageModel: "imagen-4.0-generate-001",
42
+ });
43
+ });
44
+ test("image generation options allow Imagen 4 variants", () => {
45
+ const options = normalizeImageGenerationOptions("imagen", "imagen-4.0-ultra-generate-001", "16:9");
46
+ assert.deepEqual(options, {
47
+ imageProvider: "imagen",
48
+ imageModel: "imagen-4.0-ultra-generate-001",
49
+ imageSize: "16:9",
50
+ });
51
+ });
52
+ test("image generation options map Imagen 4 listed resolutions to aspect ratios", () => {
53
+ assert.equal(getAspectRatioForImageSize("2816x1536"), "16:9");
54
+ assert.equal(getAspectRatioForImageSize("1536x2816"), "9:16");
55
+ });
56
+ test("image generation options reject unsupported Imagen aspect ratios", () => {
57
+ const options = normalizeImageGenerationOptions("imagen", "imagen-4.0-generate-001", "21:9");
58
+ assert.equal(options.error, "Unsupported image size for imagen: 21:9");
59
+ });
60
+ test("gpt-image-2 defaults to 16:9 landscape size outside icon generation", () => {
61
+ assert.equal(getDefaultImageSizeForOptions("openai", "gpt-image-2", "logo"), "2048x1152");
62
+ assert.equal(getDefaultImageSizeForOptions("openai", "gpt-image-2", "other"), "2048x1152");
63
+ assert.equal(getDefaultImageSizeForOptions("openai", "gpt-image-2", "icon"), "1024x1024");
64
+ });
65
+ test("image generation options allow gpt-image-2 custom size and quality", () => {
66
+ const options = normalizeImageGenerationOptions("openai", "gpt-image-2", "816x816", "low");
67
+ assert.deepEqual(options, {
68
+ imageProvider: "openai",
69
+ imageModel: "gpt-image-2",
70
+ imageSize: "816x816",
71
+ imageQuality: "low",
72
+ });
73
+ });
74
+ test("image generation profile maps AOI icons to low quality GPT Image 2", () => {
75
+ const options = normalizeImageGenerationProfileOptions("aoiIcon", "aoiIconAdmin", "icon");
76
+ assert.deepEqual(options, {
77
+ imageProvider: "openai",
78
+ imageModel: "gpt-image-2",
79
+ imageSize: "1024x1024",
80
+ imageQuality: "low",
81
+ imageGenerationProfile: "aoiIcon",
82
+ });
83
+ });
84
+ test("image generation profile maps regular images to medium landscape GPT Image 2", () => {
85
+ const options = normalizeImageGenerationProfileOptions("regularAiImage", "regularAiImage", "logo");
86
+ assert.deepEqual(options, {
87
+ imageProvider: "openai",
88
+ imageModel: "gpt-image-2",
89
+ imageSize: "2048x1152",
90
+ imageQuality: "medium",
91
+ imageGenerationProfile: "regularAiImage",
92
+ });
93
+ });
94
+ test("image generation profiles reject context and type mismatches", () => {
95
+ assert.equal(normalizeImageGenerationProfileOptions("regularAiImage", "aoiIconAdmin", "icon").error, "Image generation profile regularAiImage is not allowed for aoiIconAdmin");
96
+ assert.equal(normalizeImageGenerationProfileOptions("aoiIcon", "aoiIconAdmin", "logo").error, "Image generation profile aoiIcon only supports icon images");
97
+ });
98
+ test("image generation options reject invalid gpt-image-2 custom sizes", () => {
99
+ const nonMultipleSizeOptions = normalizeImageGenerationOptions("openai", "gpt-image-2", "2049x1152", "high");
100
+ assert.equal(nonMultipleSizeOptions.error, "Unsupported image size for gpt-image-2: 2049x1152");
101
+ const tooSmallOptions = normalizeImageGenerationOptions("openai", "gpt-image-2", "512x512", "low");
102
+ assert.equal(tooSmallOptions.error, "Unsupported image size for gpt-image-2: 512x512");
103
+ const tooLargeOptions = normalizeImageGenerationOptions("openai", "gpt-image-2", "3840x3840", "high");
104
+ assert.equal(tooLargeOptions.error, "Unsupported image size for gpt-image-2: 3840x3840");
105
+ });
106
+ test("image generation options still allow DALL-E 3 through OpenAI", () => {
107
+ const options = normalizeImageGenerationOptions("openai", "dall-e-3");
108
+ assert.deepEqual(options, {
109
+ imageProvider: "openai",
110
+ imageModel: "dall-e-3",
111
+ });
112
+ });
113
+ test("image generation options allow DALL-E 3 size and quality", () => {
114
+ const options = normalizeImageGenerationOptions("openai", "dall-e-3", "1792x1024", "hd");
115
+ assert.deepEqual(options, {
116
+ imageProvider: "openai",
117
+ imageModel: "dall-e-3",
118
+ imageSize: "1792x1024",
119
+ imageQuality: "hd",
120
+ });
121
+ });
122
+ test("image generation options reject GPT quality for DALL-E 3", () => {
123
+ const options = normalizeImageGenerationOptions("openai", "dall-e-3", "1792x1024", "high");
124
+ assert.equal(options.error, "Unsupported image quality for dall-e-3: high");
125
+ });
126
+ test("image generation options reject unsupported OpenAI image models", () => {
127
+ const options = normalizeImageGenerationOptions("openai", "not-a-model");
128
+ assert.equal(options.error, "Unsupported image model for openai: not-a-model");
129
+ });
130
+ test("image generation options allow configured Azure OpenAI deployment", () => {
131
+ withEnv({ AZURE_OPENAI_API_DALLE_DEPLOYMENT_NAME: "dalle-deployment" }, () => {
132
+ const options = normalizeImageGenerationOptions("azureOpenai", "dalle-deployment");
133
+ assert.deepEqual(options, {
134
+ imageProvider: "azureOpenai",
135
+ imageModel: "dalle-deployment",
136
+ });
137
+ });
138
+ });
139
+ test("image generation options reject unconfigured Flux models", () => {
140
+ withEnv({ FLUX_PRO_MODEL_NAME: "configured/flux-model" }, () => {
141
+ const options = normalizeImageGenerationOptions("flux", "other/flux-model");
142
+ assert.equal(options.error, "Unsupported image model for flux: other/flux-model");
143
+ });
144
+ });
@@ -0,0 +1 @@
1
+ export {};