@lobehub/lobehub 2.0.0-next.190 → 2.0.0-next.191

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 (87) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/package.json +1 -1
  4. package/packages/const/src/utils/merge.test.ts +679 -0
  5. package/packages/context-engine/src/processors/__tests__/AgentCouncilFlatten.test.ts +0 -20
  6. package/packages/context-engine/src/processors/__tests__/MessageContent.test.ts +5 -23
  7. package/packages/context-engine/src/providers/SystemRoleInjector.ts +0 -1
  8. package/packages/conversation-flow/src/__tests__/fixtures/inputs/agentCouncil/simple.json +4 -19
  9. package/packages/conversation-flow/src/__tests__/fixtures/inputs/agentCouncil/with-supervisor-reply.json +4 -23
  10. package/packages/conversation-flow/src/__tests__/fixtures/inputs/agentGroup/speak-different-agent.json +3 -13
  11. package/packages/conversation-flow/src/__tests__/fixtures/inputs/assistant-chain-with-followup.json +3 -8
  12. package/packages/conversation-flow/src/__tests__/fixtures/inputs/assistantGroup/assistant-with-tools.json +21 -17
  13. package/packages/conversation-flow/src/__tests__/fixtures/inputs/assistantGroup/tools-with-branches.json +15 -15
  14. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/active-index-1.json +3 -13
  15. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-branch.json +2 -9
  16. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-group-branches.json +0 -11
  17. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-user-branch.json +5 -15
  18. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/conversation.json +4 -14
  19. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/multi-assistant-group.json +0 -13
  20. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/nested.json +2 -15
  21. package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/simple.json +4 -9
  22. package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/with-tools.json +8 -17
  23. package/packages/conversation-flow/src/__tests__/fixtures/inputs/linear-conversation.json +3 -7
  24. package/packages/conversation-flow/src/__tests__/fixtures/inputs/tasks/simple.json +1 -7
  25. package/packages/conversation-flow/src/__tests__/fixtures/inputs/tasks/with-summary.json +10 -11
  26. package/packages/conversation-flow/src/__tests__/fixtures/outputs/agentCouncil/simple.json +2 -32
  27. package/packages/conversation-flow/src/__tests__/fixtures/outputs/agentCouncil/with-supervisor-reply.json +8 -46
  28. package/packages/conversation-flow/src/__tests__/fixtures/outputs/agentGroup/speak-different-agent.json +5 -24
  29. package/packages/conversation-flow/src/__tests__/fixtures/outputs/assistant-chain-with-followup.json +5 -13
  30. package/packages/conversation-flow/src/__tests__/fixtures/outputs/assistantGroup/assistant-with-tools.json +6 -16
  31. package/packages/conversation-flow/src/__tests__/fixtures/outputs/assistantGroup/tools-with-branches.json +6 -17
  32. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/active-index-1.json +4 -18
  33. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-branch.json +4 -16
  34. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-group-branches.json +0 -19
  35. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-user-branch.json +8 -24
  36. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/conversation.json +7 -23
  37. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/multi-assistant-group.json +0 -15
  38. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/nested.json +4 -25
  39. package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/simple.json +2 -13
  40. package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/with-tools.json +4 -20
  41. package/packages/conversation-flow/src/__tests__/fixtures/outputs/linear-conversation.json +4 -12
  42. package/packages/conversation-flow/src/__tests__/fixtures/outputs/tasks/simple.json +2 -14
  43. package/packages/conversation-flow/src/__tests__/fixtures/outputs/tasks/with-summary.json +20 -22
  44. package/packages/conversation-flow/src/__tests__/indexing.test.ts +0 -35
  45. package/packages/conversation-flow/src/__tests__/structuring.test.ts +0 -41
  46. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +0 -4
  47. package/packages/conversation-flow/src/transformation/__tests__/BranchResolver.test.ts +0 -10
  48. package/packages/conversation-flow/src/transformation/__tests__/ContextTreeBuilder.test.ts +0 -19
  49. package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +0 -37
  50. package/packages/conversation-flow/src/transformation/__tests__/MessageCollector.test.ts +0 -12
  51. package/packages/conversation-flow/src/transformation/__tests__/MessageTransformer.test.ts +0 -2
  52. package/packages/database/src/models/message.ts +0 -1
  53. package/packages/prompts/src/prompts/chatMessages/index.test.ts +0 -1
  54. package/packages/prompts/src/prompts/groupChat/__snapshots__/index.test.ts.snap +0 -21
  55. package/packages/prompts/src/prompts/groupChat/index.test.ts +0 -3
  56. package/packages/types/src/message/ui/chat.ts +0 -2
  57. package/src/features/Conversation/store/slices/data/action.test.ts +0 -14
  58. package/src/features/Conversation/store/slices/data/reducer.test.ts +0 -21
  59. package/src/features/Conversation/store/slices/data/reducer.ts +1 -1
  60. package/src/features/Conversation/store/slices/message/action/crud.test.ts +0 -10
  61. package/src/server/modules/Mecha/ContextEngineering/__tests__/serverMessagesEngine.test.ts +3 -5
  62. package/src/server/routers/lambda/__tests__/message.test.ts +1 -2
  63. package/src/server/services/agentRuntime/AgentRuntimeService.ts +109 -109
  64. package/src/server/services/agentRuntime/types.ts +8 -8
  65. package/src/server/services/doc/index.tsx +2 -2
  66. package/src/server/services/generation/index.ts +2 -2
  67. package/src/server/services/message/index.ts +3 -3
  68. package/src/server/services/usage/index.ts +4 -4
  69. package/src/services/chat/chat.test.ts +0 -6
  70. package/src/services/chat/mecha/contextEngineering.test.ts +3 -29
  71. package/src/services/chat/mecha/modelParamsResolver.test.ts +803 -0
  72. package/src/store/chat/agents/GroupOrchestration/createGroupOrchestrationExecutors.ts +0 -2
  73. package/src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts +0 -6
  74. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockMessages.ts +0 -3
  75. package/src/store/chat/agents/createAgentExecutors.ts +4 -4
  76. package/src/store/chat/slices/aiAgent/actions/__tests__/agentGroup.test.ts +0 -2
  77. package/src/store/chat/slices/aiAgent/actions/__tests__/runAgent.test.ts +0 -3
  78. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +0 -1
  79. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +0 -4
  80. package/src/store/chat/slices/message/reducer.test.ts +0 -5
  81. package/src/store/chat/slices/message/reducer.ts +1 -1
  82. package/src/store/chat/slices/message/selectors/displayMessage.test.ts +0 -13
  83. package/src/store/chat/slices/message/selectors/displayMessage.ts +3 -34
  84. package/src/store/chat/slices/portal/selectors.test.ts +0 -7
  85. package/src/store/chat/slices/thread/action.test.ts +0 -1
  86. package/src/store/chat/slices/translate/action.test.ts +0 -1
  87. package/src/store/tool/slices/oldStore/action.test.ts +0 -1
@@ -0,0 +1,679 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { merge, mergeArrayById } from './merge';
4
+
5
+ describe('merge', () => {
6
+ describe('basic object merging', () => {
7
+ it('should merge two simple objects', () => {
8
+ const target = { a: 1, b: 2 };
9
+ const source = { b: 3, c: 4 };
10
+
11
+ const result = merge(target, source);
12
+
13
+ expect(result).toEqual({ a: 1, b: 3, c: 4 });
14
+ });
15
+
16
+ it('should merge nested objects', () => {
17
+ const target = { a: { x: 1, y: 2 }, b: 3 };
18
+ const source = { a: { y: 4, z: 5 }, c: 6 };
19
+
20
+ const result = merge(target, source);
21
+
22
+ expect(result).toEqual({
23
+ a: { x: 1, y: 4, z: 5 },
24
+ b: 3,
25
+ c: 6,
26
+ });
27
+ });
28
+
29
+ it('should not mutate the original objects', () => {
30
+ const target = { a: 1, b: 2 };
31
+ const source = { b: 3, c: 4 };
32
+
33
+ const targetClone = JSON.parse(JSON.stringify(target));
34
+ const sourceClone = JSON.parse(JSON.stringify(source));
35
+
36
+ merge(target, source);
37
+
38
+ expect(target).toEqual(targetClone);
39
+ expect(source).toEqual(sourceClone);
40
+ });
41
+ });
42
+
43
+ describe('array handling', () => {
44
+ it('should replace arrays instead of merging them', () => {
45
+ const target = { items: [1, 2, 3] };
46
+ const source = { items: [4, 5] };
47
+
48
+ const result = merge(target, source);
49
+
50
+ expect(result).toEqual({ items: [4, 5] });
51
+ });
52
+
53
+ it('should replace arrays in nested objects', () => {
54
+ const target = {
55
+ config: {
56
+ values: [1, 2, 3],
57
+ name: 'original',
58
+ },
59
+ };
60
+ const source = {
61
+ config: {
62
+ values: [7, 8, 9],
63
+ },
64
+ };
65
+
66
+ const result = merge(target, source);
67
+
68
+ expect(result).toEqual({
69
+ config: {
70
+ values: [7, 8, 9],
71
+ name: 'original',
72
+ },
73
+ });
74
+ });
75
+
76
+ it('should handle empty arrays', () => {
77
+ const target = { items: [1, 2, 3] };
78
+ const source = { items: [] };
79
+
80
+ const result = merge(target, source);
81
+
82
+ expect(result).toEqual({ items: [] });
83
+ });
84
+
85
+ it('should handle arrays with objects', () => {
86
+ const target = { items: [{ id: 1, name: 'a' }] };
87
+ const source = { items: [{ id: 2, name: 'b' }] };
88
+
89
+ const result = merge(target, source);
90
+
91
+ expect(result).toEqual({ items: [{ id: 2, name: 'b' }] });
92
+ });
93
+ });
94
+
95
+ describe('edge cases', () => {
96
+ it('should handle empty target', () => {
97
+ const target = {};
98
+ const source = { a: 1, b: 2 };
99
+
100
+ const result = merge(target, source);
101
+
102
+ expect(result).toEqual({ a: 1, b: 2 });
103
+ });
104
+
105
+ it('should handle empty source', () => {
106
+ const target = { a: 1, b: 2 };
107
+ const source = {};
108
+
109
+ const result = merge(target, source);
110
+
111
+ expect(result).toEqual({ a: 1, b: 2 });
112
+ });
113
+
114
+ it('should handle null values', () => {
115
+ const target = { a: 1, b: 2 };
116
+ const source = { b: null };
117
+
118
+ const result = merge(target, source);
119
+
120
+ expect(result).toEqual({ a: 1, b: null });
121
+ });
122
+
123
+ it('should handle undefined values by not overwriting', () => {
124
+ const target = { a: 1, b: 2 };
125
+ const source = { b: undefined, c: 3 };
126
+
127
+ const result = merge(target, source);
128
+
129
+ // lodash merge doesn't overwrite with undefined
130
+ expect(result).toEqual({ a: 1, b: 2, c: 3 });
131
+ });
132
+
133
+ it('should handle deeply nested objects', () => {
134
+ const target = {
135
+ level1: {
136
+ level2: {
137
+ level3: {
138
+ value: 'original',
139
+ },
140
+ },
141
+ },
142
+ };
143
+ const source = {
144
+ level1: {
145
+ level2: {
146
+ level3: {
147
+ value: 'updated',
148
+ newProp: 'added',
149
+ },
150
+ },
151
+ },
152
+ };
153
+
154
+ const result = merge(target, source);
155
+
156
+ expect(result).toEqual({
157
+ level1: {
158
+ level2: {
159
+ level3: {
160
+ value: 'updated',
161
+ newProp: 'added',
162
+ },
163
+ },
164
+ },
165
+ });
166
+ });
167
+ });
168
+
169
+ describe('mixed data types', () => {
170
+ it('should handle boolean values', () => {
171
+ const target = { enabled: true, active: false };
172
+ const source = { enabled: false };
173
+
174
+ const result = merge(target, source);
175
+
176
+ expect(result).toEqual({ enabled: false, active: false });
177
+ });
178
+
179
+ it('should handle number values', () => {
180
+ const target = { count: 10, limit: 100 };
181
+ const source = { count: 20 };
182
+
183
+ const result = merge(target, source);
184
+
185
+ expect(result).toEqual({ count: 20, limit: 100 });
186
+ });
187
+
188
+ it('should handle string values', () => {
189
+ const target = { name: 'original', description: 'test' };
190
+ const source = { name: 'updated' };
191
+
192
+ const result = merge(target, source);
193
+
194
+ expect(result).toEqual({ name: 'updated', description: 'test' });
195
+ });
196
+
197
+ it('should handle mixed types in nested objects', () => {
198
+ const target = {
199
+ config: {
200
+ enabled: true,
201
+ count: 10,
202
+ items: [1, 2],
203
+ metadata: { key: 'value' },
204
+ },
205
+ };
206
+ const source = {
207
+ config: {
208
+ enabled: false,
209
+ items: [3, 4, 5],
210
+ metadata: { key: 'updated', newKey: 'newValue' },
211
+ },
212
+ };
213
+
214
+ const result = merge(target, source);
215
+
216
+ expect(result).toEqual({
217
+ config: {
218
+ enabled: false,
219
+ count: 10,
220
+ items: [3, 4, 5],
221
+ metadata: { key: 'updated', newKey: 'newValue' },
222
+ },
223
+ });
224
+ });
225
+ });
226
+ });
227
+
228
+ describe('mergeArrayById', () => {
229
+ describe('basic merging', () => {
230
+ it('should merge items by id and preserve default metadata', () => {
231
+ const defaultItems = [
232
+ {
233
+ contextWindowTokens: 128_000,
234
+ description: 'Test model description',
235
+ displayName: 'Test Model',
236
+ enabled: true,
237
+ id: 'test-model',
238
+ maxOutput: 65_536,
239
+ pricing: {
240
+ input: 3,
241
+ output: 12,
242
+ },
243
+ },
244
+ ];
245
+ const userItems = [{ id: 'test-model', displayName: 'Custom Name', enabled: false }];
246
+
247
+ const result = mergeArrayById(defaultItems, userItems);
248
+
249
+ expect(result).toEqual([
250
+ {
251
+ contextWindowTokens: 128_000,
252
+ description: 'Test model description',
253
+ displayName: 'Custom Name',
254
+ enabled: false,
255
+ id: 'test-model',
256
+ maxOutput: 65_536,
257
+ pricing: {
258
+ input: 3,
259
+ output: 12,
260
+ },
261
+ },
262
+ ]);
263
+ });
264
+
265
+ it('should override user values but preserve default metadata', () => {
266
+ const defaultItems = [
267
+ {
268
+ id: 'model-1',
269
+ name: 'Default Name',
270
+ value: 100,
271
+ metadata: { key: 'preserved' },
272
+ },
273
+ ];
274
+ const userItems = [{ id: 'model-1', name: 'User Name', value: 200 }];
275
+
276
+ const result = mergeArrayById(defaultItems, userItems);
277
+
278
+ expect(result).toEqual([
279
+ {
280
+ id: 'model-1',
281
+ name: 'User Name',
282
+ value: 200,
283
+ metadata: { key: 'preserved' },
284
+ },
285
+ ]);
286
+ });
287
+ });
288
+
289
+ describe('empty array handling', () => {
290
+ it('should return empty array when both inputs are empty', () => {
291
+ const result = mergeArrayById([], []);
292
+ expect(result).toEqual([]);
293
+ });
294
+
295
+ it('should return all default items when user items is empty', () => {
296
+ const defaultItems = [
297
+ { id: '1', name: 'Default 1', value: 100 },
298
+ { id: '2', name: 'Default 2', value: 200 },
299
+ ];
300
+
301
+ const result = mergeArrayById(defaultItems, []);
302
+ expect(result).toEqual(defaultItems);
303
+ });
304
+
305
+ it('should return all user items when default items is empty', () => {
306
+ const userItems = [
307
+ { id: '1', name: 'User 1', value: 300 },
308
+ { id: '2', name: 'User 2', value: 400 },
309
+ ];
310
+
311
+ const result = mergeArrayById([], userItems);
312
+ expect(result).toEqual(userItems);
313
+ });
314
+ });
315
+
316
+ describe('ID matching scenarios', () => {
317
+ it('should handle user items with IDs not in default items', () => {
318
+ const defaultItems = [{ id: '1', name: 'Default 1', value: 100 }];
319
+ const userItems = [
320
+ { id: '1', name: 'User 1', value: 200 },
321
+ { id: '2', name: 'User 2', value: 300 },
322
+ ];
323
+
324
+ const result = mergeArrayById(defaultItems, userItems);
325
+
326
+ expect(result).toHaveLength(2);
327
+ expect(result).toContainEqual({ id: '1', name: 'User 1', value: 200 });
328
+ expect(result).toContainEqual({ id: '2', name: 'User 2', value: 300 });
329
+ });
330
+
331
+ it('should preserve default items not in user items', () => {
332
+ const defaultItems = [
333
+ { id: '1', name: 'Default 1', value: 100 },
334
+ { id: '2', name: 'Default 2', value: 200 },
335
+ { id: '3', name: 'Default 3', value: 300 },
336
+ ];
337
+ const userItems = [{ id: '2', name: 'User 2', value: 250 }];
338
+
339
+ const result = mergeArrayById(defaultItems, userItems);
340
+
341
+ expect(result).toHaveLength(3);
342
+ expect(result).toContainEqual({ id: '1', name: 'Default 1', value: 100 });
343
+ expect(result).toContainEqual({ id: '2', name: 'User 2', value: 250 });
344
+ expect(result).toContainEqual({ id: '3', name: 'Default 3', value: 300 });
345
+ });
346
+
347
+ it('should merge multiple items correctly', () => {
348
+ const defaultItems = [
349
+ { id: '1', name: 'Default 1', value: 100, meta: { key: 'value1' } },
350
+ { id: '2', name: 'Default 2', value: 200, meta: { key: 'value2' } },
351
+ ];
352
+ const userItems = [
353
+ { id: '2', name: 'User 2', value: 300 },
354
+ { id: '1', name: 'User 1', value: 400 },
355
+ ];
356
+
357
+ const result = mergeArrayById(defaultItems, userItems);
358
+
359
+ expect(result).toHaveLength(2);
360
+ expect(result).toContainEqual({
361
+ id: '1',
362
+ name: 'User 1',
363
+ value: 400,
364
+ meta: { key: 'value1' },
365
+ });
366
+ expect(result).toContainEqual({
367
+ id: '2',
368
+ name: 'User 2',
369
+ value: 300,
370
+ meta: { key: 'value2' },
371
+ });
372
+ });
373
+ });
374
+
375
+ describe('special value handling', () => {
376
+ it('should handle null values by keeping default values', () => {
377
+ const defaultItems = [{ id: '1', name: 'Default', value: 100, meta: { key: 'value' } }];
378
+ const userItems = [{ id: '1', name: null, value: 200, meta: null }];
379
+
380
+ const result = mergeArrayById(defaultItems, userItems as any);
381
+
382
+ expect(result).toEqual([{ id: '1', name: 'Default', value: 200, meta: { key: 'value' } }]);
383
+ });
384
+
385
+ it('should handle undefined values by keeping default values', () => {
386
+ const defaultItems = [{ id: '1', name: 'Default', value: 100, meta: { key: 'value' } }];
387
+ const userItems = [{ id: '1', name: undefined, value: 200, meta: undefined }];
388
+
389
+ const result = mergeArrayById(defaultItems, userItems as any);
390
+
391
+ expect(result).toEqual([{ id: '1', name: 'Default', value: 200, meta: { key: 'value' } }]);
392
+ });
393
+
394
+ it('should handle empty objects by keeping default values', () => {
395
+ const defaultItems = [
396
+ {
397
+ id: '1',
398
+ name: 'Default',
399
+ config: { key1: 'value1', key2: 'value2' },
400
+ },
401
+ ];
402
+ const userItems = [{ id: '1', name: 'User', config: {} }];
403
+
404
+ const result = mergeArrayById(defaultItems, userItems);
405
+
406
+ expect(result).toEqual([
407
+ {
408
+ id: '1',
409
+ name: 'User',
410
+ config: { key1: 'value1', key2: 'value2' },
411
+ },
412
+ ]);
413
+ });
414
+
415
+ it('should merge nested objects correctly', () => {
416
+ const defaultItems = [
417
+ {
418
+ id: '1',
419
+ config: {
420
+ deep: {
421
+ value: 100,
422
+ keep: true,
423
+ },
424
+ surface: 'default',
425
+ },
426
+ },
427
+ ];
428
+ const userItems = [
429
+ {
430
+ id: '1',
431
+ config: {
432
+ deep: {
433
+ value: 200,
434
+ },
435
+ surface: 'changed',
436
+ },
437
+ },
438
+ ];
439
+
440
+ const result = mergeArrayById(defaultItems, userItems);
441
+
442
+ expect(result[0].config).toEqual({
443
+ deep: {
444
+ value: 200,
445
+ keep: true,
446
+ },
447
+ surface: 'changed',
448
+ });
449
+ });
450
+
451
+ it('should handle deeply nested object merging', () => {
452
+ const defaultItems = [
453
+ {
454
+ id: '1',
455
+ abilities: {
456
+ reasoning: true,
457
+ functionCalling: true,
458
+ },
459
+ config: {
460
+ deploymentName: 'default',
461
+ },
462
+ },
463
+ ];
464
+ const userItems = [
465
+ {
466
+ id: '1',
467
+ abilities: {
468
+ reasoning: false,
469
+ },
470
+ config: {
471
+ deploymentName: 'custom',
472
+ },
473
+ },
474
+ ];
475
+
476
+ const result = mergeArrayById(defaultItems, userItems);
477
+
478
+ expect(result).toEqual([
479
+ {
480
+ id: '1',
481
+ abilities: {
482
+ functionCalling: true,
483
+ reasoning: false,
484
+ },
485
+ config: {
486
+ deploymentName: 'custom',
487
+ },
488
+ },
489
+ ]);
490
+ });
491
+ });
492
+
493
+ describe('edge cases', () => {
494
+ it('should preserve the source objects (no mutation)', () => {
495
+ const defaultItems = [{ id: '1', name: 'Default', meta: { key: 'value' } }];
496
+ const userItems = [{ id: '1', name: 'User' }];
497
+
498
+ const defaultItemsClone = JSON.parse(JSON.stringify(defaultItems));
499
+ const userItemsClone = JSON.parse(JSON.stringify(userItems));
500
+
501
+ mergeArrayById(defaultItems, userItems);
502
+
503
+ expect(defaultItems).toEqual(defaultItemsClone);
504
+ expect(userItems).toEqual(userItemsClone);
505
+ });
506
+
507
+ it('should handle duplicate IDs in user items by using the last occurrence', () => {
508
+ const defaultItems = [{ id: '1', name: 'Default', value: 100 }];
509
+ const userItems = [
510
+ { id: '1', name: 'User 1', value: 200 },
511
+ { id: '1', name: 'User 2', value: 300 },
512
+ ];
513
+
514
+ const result = mergeArrayById(defaultItems, userItems);
515
+
516
+ expect(result).toHaveLength(1);
517
+ expect(result[0]).toEqual({
518
+ id: '1',
519
+ name: 'User 2',
520
+ value: 300,
521
+ });
522
+ });
523
+
524
+ it('should handle duplicate IDs in default items by using the last occurrence', () => {
525
+ const defaultItems = [
526
+ { id: '1', name: 'Default 1', value: 100, meta: 'first' },
527
+ { id: '1', name: 'Default 2', value: 200, meta: 'second' },
528
+ ];
529
+ const userItems = [{ id: '1', name: 'User' }];
530
+
531
+ const result = mergeArrayById(defaultItems, userItems);
532
+
533
+ // Map uses last occurrence when there are duplicates
534
+ expect(result).toHaveLength(1);
535
+ expect(result[0]).toEqual({
536
+ id: '1',
537
+ name: 'User',
538
+ value: 200,
539
+ meta: 'second',
540
+ });
541
+ });
542
+
543
+ it('should handle complex real-world scenario', () => {
544
+ const defaultItems = [
545
+ {
546
+ contextWindowTokens: 200_000,
547
+ description: 'Advanced reasoning model',
548
+ displayName: 'Model O1',
549
+ enabled: true,
550
+ id: 'o1',
551
+ abilities: {
552
+ reasoning: true,
553
+ functionCalling: true,
554
+ },
555
+ config: {
556
+ deploymentName: 'o1',
557
+ },
558
+ maxOutput: 100_000,
559
+ pricing: {
560
+ input: 15,
561
+ output: 60,
562
+ },
563
+ source: 'builtin',
564
+ },
565
+ ];
566
+ const userItems = [
567
+ {
568
+ id: 'o1',
569
+ abilities: {
570
+ reasoning: false,
571
+ },
572
+ config: {
573
+ deploymentName: 'custom-o1',
574
+ },
575
+ displayName: 'Custom O1',
576
+ enabled: false,
577
+ },
578
+ ];
579
+
580
+ const result = mergeArrayById(defaultItems, userItems);
581
+
582
+ expect(result).toEqual([
583
+ {
584
+ contextWindowTokens: 200_000,
585
+ description: 'Advanced reasoning model',
586
+ displayName: 'Custom O1',
587
+ enabled: false,
588
+ id: 'o1',
589
+ abilities: {
590
+ functionCalling: true,
591
+ reasoning: false,
592
+ },
593
+ config: {
594
+ deploymentName: 'custom-o1',
595
+ },
596
+ maxOutput: 100_000,
597
+ pricing: {
598
+ input: 15,
599
+ output: 60,
600
+ },
601
+ source: 'builtin',
602
+ },
603
+ ]);
604
+ });
605
+
606
+ it('should handle items with only id property', () => {
607
+ const defaultItems = [{ id: '1', name: 'Default', value: 100 }];
608
+ const userItems = [{ id: '1' }];
609
+
610
+ const result = mergeArrayById(defaultItems, userItems);
611
+
612
+ expect(result).toEqual([{ id: '1', name: 'Default', value: 100 }]);
613
+ });
614
+
615
+ it('should handle mixed scenario with new, updated, and unchanged items', () => {
616
+ const defaultItems = [
617
+ { id: '1', name: 'Default 1', value: 100 },
618
+ { id: '2', name: 'Default 2', value: 200 },
619
+ { id: '3', name: 'Default 3', value: 300 },
620
+ ];
621
+ const userItems = [
622
+ { id: '2', name: 'Updated 2', value: 250 },
623
+ { id: '4', name: 'New 4', value: 400 },
624
+ ];
625
+
626
+ const result = mergeArrayById(defaultItems, userItems);
627
+
628
+ expect(result).toHaveLength(4);
629
+ expect(result).toContainEqual({ id: '1', name: 'Default 1', value: 100 });
630
+ expect(result).toContainEqual({ id: '2', name: 'Updated 2', value: 250 });
631
+ expect(result).toContainEqual({ id: '3', name: 'Default 3', value: 300 });
632
+ expect(result).toContainEqual({ id: '4', name: 'New 4', value: 400 });
633
+ });
634
+ });
635
+
636
+ describe('primitive value handling in arrays', () => {
637
+ it('should handle simple property replacement', () => {
638
+ const defaultItems = [
639
+ {
640
+ id: '1',
641
+ name: 'Default',
642
+ count: 10,
643
+ },
644
+ ];
645
+ const userItems = [
646
+ {
647
+ id: '1',
648
+ count: 20,
649
+ },
650
+ ];
651
+
652
+ const result = mergeArrayById(defaultItems, userItems);
653
+
654
+ expect(result[0].count).toBe(20);
655
+ expect((result[0] as any).name).toBe('Default');
656
+ });
657
+
658
+ it('should handle string values correctly', () => {
659
+ const defaultItems = [
660
+ {
661
+ id: '1',
662
+ title: 'Default Title',
663
+ description: 'Default Description',
664
+ },
665
+ ];
666
+ const userItems = [
667
+ {
668
+ id: '1',
669
+ title: 'Custom Title',
670
+ },
671
+ ];
672
+
673
+ const result = mergeArrayById(defaultItems, userItems);
674
+
675
+ expect(result[0].title).toBe('Custom Title');
676
+ expect((result[0] as any).description).toBe('Default Description');
677
+ });
678
+ });
679
+ });