@devrev/ts-adaas 1.19.4 → 1.19.6-beta.0

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 (50) hide show
  1. package/dist/attachments-streaming/attachments-streaming-pool.test.js +3 -6
  2. package/dist/common/event-type-translation.test.d.ts +2 -0
  3. package/dist/common/event-type-translation.test.d.ts.map +1 -0
  4. package/dist/common/event-type-translation.test.js +175 -0
  5. package/dist/common/time-value-resolver.test.js +0 -1
  6. package/dist/deprecated/uploader/index.d.ts +3 -1
  7. package/dist/deprecated/uploader/index.d.ts.map +1 -1
  8. package/dist/deprecated/uploader/index.js +29 -22
  9. package/dist/multithreading/create-worker.test.js +34 -16
  10. package/dist/multithreading/process-task.test.d.ts +2 -0
  11. package/dist/multithreading/process-task.test.d.ts.map +1 -0
  12. package/dist/multithreading/process-task.test.js +166 -0
  13. package/dist/multithreading/spawn/spawn.test.d.ts +2 -0
  14. package/dist/multithreading/spawn/spawn.test.d.ts.map +1 -0
  15. package/dist/multithreading/spawn/spawn.test.js +223 -0
  16. package/dist/multithreading/worker-adapter/worker-adapter.emit.test.d.ts +2 -0
  17. package/dist/multithreading/worker-adapter/worker-adapter.emit.test.d.ts.map +1 -0
  18. package/dist/multithreading/worker-adapter/worker-adapter.emit.test.js +415 -0
  19. package/dist/multithreading/worker-adapter/worker-adapter.extraction.test.d.ts +2 -0
  20. package/dist/multithreading/worker-adapter/worker-adapter.extraction.test.d.ts.map +1 -0
  21. package/dist/multithreading/worker-adapter/worker-adapter.extraction.test.js +801 -0
  22. package/dist/multithreading/worker-adapter/worker-adapter.loading.test.d.ts +2 -0
  23. package/dist/multithreading/worker-adapter/worker-adapter.loading.test.d.ts.map +1 -0
  24. package/dist/multithreading/worker-adapter/worker-adapter.loading.test.js +598 -0
  25. package/dist/multithreading/worker-adapter/worker-adapter.serialization.test.d.ts +2 -0
  26. package/dist/multithreading/worker-adapter/worker-adapter.serialization.test.d.ts.map +1 -0
  27. package/dist/multithreading/worker-adapter/worker-adapter.serialization.test.js +71 -0
  28. package/dist/repo/repo.test.js +41 -0
  29. package/dist/state/state.extract-window.test.d.ts +2 -0
  30. package/dist/state/state.extract-window.test.d.ts.map +1 -0
  31. package/dist/state/state.extract-window.test.js +163 -0
  32. package/dist/state/state.pending-boundaries.test.d.ts +2 -0
  33. package/dist/state/state.pending-boundaries.test.d.ts.map +1 -0
  34. package/dist/state/state.pending-boundaries.test.js +189 -0
  35. package/dist/state/state.post-state.test.d.ts +2 -0
  36. package/dist/state/state.post-state.test.d.ts.map +1 -0
  37. package/dist/state/state.post-state.test.js +77 -0
  38. package/dist/state/state.test.js +23 -506
  39. package/dist/state/state.time-value-resolution.test.d.ts +2 -0
  40. package/dist/state/state.time-value-resolution.test.d.ts.map +1 -0
  41. package/dist/state/state.time-value-resolution.test.js +175 -0
  42. package/dist/types/extraction.d.ts +20 -1
  43. package/dist/types/extraction.d.ts.map +1 -1
  44. package/dist/types/extraction.test.js +57 -21
  45. package/dist/uploader/uploader.helpers.test.js +0 -11
  46. package/dist/uploader/uploader.test.js +0 -9
  47. package/package.json +7 -7
  48. package/dist/multithreading/worker-adapter/worker-adapter.test.d.ts +0 -2
  49. package/dist/multithreading/worker-adapter/worker-adapter.test.d.ts.map +0 -1
  50. package/dist/multithreading/worker-adapter/worker-adapter.test.js +0 -1243
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=worker-adapter.loading.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-adapter.loading.test.d.ts","sourceRoot":"","sources":["../../../src/multithreading/worker-adapter/worker-adapter.loading.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,598 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const state_1 = require("../../state/state");
4
+ const jest_setup_1 = require("../../tests/jest.setup");
5
+ const test_utils_1 = require("../../common/test-utils");
6
+ const types_1 = require("../../types");
7
+ const loading_1 = require("../../types/loading");
8
+ const worker_adapter_1 = require("./worker-adapter");
9
+ jest.mock('../../common/control-protocol', () => ({
10
+ emit: jest.fn().mockResolvedValue({}),
11
+ }));
12
+ jest.mock('../../mappers/mappers');
13
+ jest.mock('../../uploader/uploader');
14
+ jest.mock('../../repo/repo');
15
+ jest.mock('node:worker_threads', () => ({
16
+ parentPort: { postMessage: jest.fn() },
17
+ }));
18
+ jest.mock('../../attachments-streaming/attachments-streaming-pool', () => ({
19
+ AttachmentsStreamingPool: jest.fn().mockImplementation(() => ({
20
+ streamAll: jest.fn().mockResolvedValue(undefined),
21
+ })),
22
+ }));
23
+ function makeAdapter(eventType) {
24
+ const event = (0, test_utils_1.createMockEvent)(jest_setup_1.mockServer.baseUrl, {
25
+ payload: { event_type: eventType },
26
+ });
27
+ const initialState = {
28
+ attachments: { completed: false },
29
+ lastSyncStarted: '',
30
+ lastSuccessfulSyncStarted: '',
31
+ snapInVersionId: '',
32
+ toDevRev: {
33
+ attachmentsMetadata: {
34
+ artifactIds: [],
35
+ lastProcessed: 0,
36
+ lastProcessedAttachmentsIdsList: [],
37
+ },
38
+ },
39
+ };
40
+ const adapterState = new state_1.State({ event, initialState });
41
+ const adapter = new worker_adapter_1.WorkerAdapter({ event, adapterState });
42
+ return { adapter, event, adapterState };
43
+ }
44
+ function makeLoaderItem(devrevId = 'dev-1') {
45
+ return {
46
+ id: { devrev: devrevId, external: 'ext-1' },
47
+ created_date: '',
48
+ modified_date: '',
49
+ data: {},
50
+ };
51
+ }
52
+ function setupLoaderFile(adapter, items, itemType = 'tasks') {
53
+ adapter['adapterState'].state.fromDevRev = {
54
+ filesToLoad: [
55
+ {
56
+ id: 'artifact-1',
57
+ file_name: 'file.json',
58
+ itemType,
59
+ count: items.length,
60
+ lineToProcess: 0,
61
+ completed: false,
62
+ },
63
+ ],
64
+ };
65
+ adapter['uploader'].getJsonObjectByArtifactId = jest
66
+ .fn()
67
+ .mockResolvedValue({ response: items });
68
+ }
69
+ describe(`${worker_adapter_1.WorkerAdapter.name}.loadItemTypes — timeout and unexpected errors`, () => {
70
+ let adapter;
71
+ let exitSpy;
72
+ let emitSpy;
73
+ beforeEach(() => {
74
+ jest.clearAllMocks();
75
+ ({ adapter } = makeAdapter(types_1.EventType.ContinueLoadingData));
76
+ exitSpy = jest
77
+ .spyOn(process, 'exit')
78
+ .mockImplementation(() => undefined);
79
+ emitSpy = jest.spyOn(adapter, 'emit').mockResolvedValue();
80
+ });
81
+ afterEach(() => {
82
+ exitSpy.mockRestore();
83
+ jest.restoreAllMocks();
84
+ });
85
+ it('should emit DataLoadingProgress and exit on timeout', async () => {
86
+ // Arrange
87
+ const items = [makeLoaderItem('dev-1'), makeLoaderItem('dev-2')];
88
+ setupLoaderFile(adapter, items);
89
+ adapter.isTimeout = true;
90
+ const itemTypesToLoad = [
91
+ { itemType: 'tasks', create: jest.fn(), update: jest.fn() },
92
+ ];
93
+ // Act
94
+ await adapter.loadItemTypes({ itemTypesToLoad });
95
+ // Assert
96
+ expect(emitSpy).toHaveBeenCalledWith(types_1.LoaderEventType.DataLoadingProgress);
97
+ expect(exitSpy).toHaveBeenCalledWith(0);
98
+ });
99
+ it('should emit DataLoadingProgress mid-loop when timeout arrives between items', async () => {
100
+ // Arrange
101
+ const items = [
102
+ makeLoaderItem('dev-1'),
103
+ makeLoaderItem('dev-2'),
104
+ makeLoaderItem('dev-3'),
105
+ ];
106
+ setupLoaderFile(adapter, items);
107
+ exitSpy.mockRestore();
108
+ exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => {
109
+ throw new Error('process.exit');
110
+ }));
111
+ let loadItemCallCount = 0;
112
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/require-await
113
+ jest.spyOn(adapter, 'loadItem').mockImplementation(async () => {
114
+ loadItemCallCount++;
115
+ if (loadItemCallCount === 1) {
116
+ adapter.isTimeout = true;
117
+ }
118
+ return { report: { item_type: 'tasks', updated: 1 } };
119
+ });
120
+ const itemTypesToLoad = [
121
+ { itemType: 'tasks', create: jest.fn(), update: jest.fn() },
122
+ ];
123
+ // Act & Assert
124
+ await expect(adapter.loadItemTypes({ itemTypesToLoad })).rejects.toThrow('process.exit');
125
+ expect(loadItemCallCount).toBe(1);
126
+ expect(emitSpy).toHaveBeenCalledWith(types_1.LoaderEventType.DataLoadingProgress);
127
+ });
128
+ it('should emit DataLoadingError and exit(1) on unexpected error', async () => {
129
+ // Arrange
130
+ adapter['adapterState'].state.fromDevRev = {
131
+ filesToLoad: [
132
+ {
133
+ id: 'artifact-1',
134
+ file_name: 'file1.json',
135
+ itemType: 'tasks',
136
+ count: 1,
137
+ lineToProcess: 0,
138
+ completed: false,
139
+ },
140
+ ],
141
+ };
142
+ adapter['uploader'].getJsonObjectByArtifactId = jest
143
+ .fn()
144
+ .mockRejectedValue(new Error('Unexpected network failure'));
145
+ const itemTypesToLoad = [
146
+ { itemType: 'tasks', create: jest.fn(), update: jest.fn() },
147
+ ];
148
+ // Act
149
+ await adapter.loadItemTypes({ itemTypesToLoad });
150
+ // Assert
151
+ expect(emitSpy).toHaveBeenCalledWith(types_1.LoaderEventType.DataLoadingError, expect.objectContaining({
152
+ error: expect.objectContaining({
153
+ message: expect.stringContaining('Error during data loading'),
154
+ }),
155
+ }));
156
+ expect(exitSpy).toHaveBeenCalledWith(1);
157
+ });
158
+ });
159
+ describe(`${worker_adapter_1.WorkerAdapter.name}.loadItemTypes — loadItem branch coverage via public API`, () => {
160
+ let adapter;
161
+ let emitSpy;
162
+ let exitSpy;
163
+ const itemTypesToLoad = [
164
+ { itemType: 'tasks', create: jest.fn(), update: jest.fn() },
165
+ ];
166
+ beforeEach(() => {
167
+ jest.clearAllMocks();
168
+ ({ adapter } = makeAdapter(types_1.EventType.ContinueLoadingData));
169
+ emitSpy = jest.spyOn(adapter, 'emit').mockResolvedValue();
170
+ exitSpy = jest
171
+ .spyOn(process, 'exit')
172
+ .mockImplementation(() => undefined);
173
+ itemTypesToLoad[0].create = jest.fn();
174
+ itemTypesToLoad[0].update = jest.fn();
175
+ });
176
+ afterEach(() => {
177
+ exitSpy.mockRestore();
178
+ jest.restoreAllMocks();
179
+ });
180
+ it('should accumulate an UPDATED report when the connector updates the item and the mapper sync succeeds', async () => {
181
+ // Arrange
182
+ setupLoaderFile(adapter, [makeLoaderItem('dev-1')]);
183
+ adapter['_mappers'].getByTargetId = jest.fn().mockResolvedValue({
184
+ data: { sync_mapper_record: { id: 'smr-1' } },
185
+ });
186
+ adapter['_mappers'].update = jest.fn().mockResolvedValue({ data: {} });
187
+ itemTypesToLoad[0].update = jest
188
+ .fn()
189
+ .mockResolvedValue({ id: 'ext-updated-1' });
190
+ // Act
191
+ const { reports } = await adapter.loadItemTypes({ itemTypesToLoad });
192
+ // Assert
193
+ expect(reports).toEqual(expect.arrayContaining([
194
+ expect.objectContaining({
195
+ item_type: 'tasks',
196
+ [loading_1.ActionType.UPDATED]: 1,
197
+ }),
198
+ ]));
199
+ expect(emitSpy).not.toHaveBeenCalled();
200
+ });
201
+ it('should fall back to create and accumulate a CREATED report when the mapper record does not exist (404)', async () => {
202
+ // Arrange
203
+ setupLoaderFile(adapter, [makeLoaderItem('dev-2')]);
204
+ const axiosError = { isAxiosError: true, response: { status: 404 } };
205
+ adapter['_mappers'].getByTargetId = jest.fn().mockRejectedValue(axiosError);
206
+ adapter['_mappers'].create = jest.fn().mockResolvedValue({ data: {} });
207
+ itemTypesToLoad[0].create = jest
208
+ .fn()
209
+ .mockResolvedValue({ id: 'new-ext-id' });
210
+ // Act
211
+ const { reports } = await adapter.loadItemTypes({ itemTypesToLoad });
212
+ // Assert
213
+ expect(reports).toEqual(expect.arrayContaining([
214
+ expect.objectContaining({
215
+ item_type: 'tasks',
216
+ [loading_1.ActionType.CREATED]: 1,
217
+ }),
218
+ ]));
219
+ expect(emitSpy).not.toHaveBeenCalled();
220
+ });
221
+ it('should emit DataLoadingDelayed and stop processing when the connector signals a rate-limit delay', async () => {
222
+ // Arrange
223
+ setupLoaderFile(adapter, [makeLoaderItem('dev-3')]);
224
+ adapter['_mappers'].getByTargetId = jest.fn().mockResolvedValue({
225
+ data: { sync_mapper_record: { id: 'smr-1' } },
226
+ });
227
+ itemTypesToLoad[0].update = jest.fn().mockResolvedValue({ delay: 15 });
228
+ // Act
229
+ await adapter.loadItemTypes({ itemTypesToLoad });
230
+ // Assert
231
+ expect(emitSpy).toHaveBeenCalledWith(types_1.LoaderEventType.DataLoadingDelayed, expect.objectContaining({ delay: 15 }));
232
+ });
233
+ it('should count the item as FAILED when the update succeeds but the mapper sync throws', async () => {
234
+ // Arrange
235
+ setupLoaderFile(adapter, [makeLoaderItem('dev-4')]);
236
+ adapter['_mappers'].getByTargetId = jest.fn().mockResolvedValue({
237
+ data: { sync_mapper_record: { id: 'smr-1' } },
238
+ });
239
+ adapter['_mappers'].update = jest
240
+ .fn()
241
+ .mockRejectedValue(new Error('mapper down'));
242
+ itemTypesToLoad[0].update = jest.fn().mockResolvedValue({ id: 'ext-id' });
243
+ // Act
244
+ const { reports } = await adapter.loadItemTypes({ itemTypesToLoad });
245
+ // Assert
246
+ expect(emitSpy).not.toHaveBeenCalled();
247
+ expect(reports).toBeDefined();
248
+ });
249
+ it('should not emit for a non-404 Axios error from the mapper (recorded as item-level error)', async () => {
250
+ // Arrange
251
+ setupLoaderFile(adapter, [makeLoaderItem('dev-5')]);
252
+ const axiosError = {
253
+ isAxiosError: true,
254
+ message: 'internal server error',
255
+ response: { status: 500 },
256
+ };
257
+ adapter['_mappers'].getByTargetId = jest.fn().mockRejectedValue(axiosError);
258
+ // Act
259
+ await adapter.loadItemTypes({ itemTypesToLoad });
260
+ // Assert
261
+ expect(emitSpy).not.toHaveBeenCalled();
262
+ });
263
+ it('should handle a null sync_mapper_record gracefully and continue loading', async () => {
264
+ // Arrange
265
+ setupLoaderFile(adapter, [makeLoaderItem('dev-6')]);
266
+ adapter['_mappers'].getByTargetId = jest
267
+ .fn()
268
+ .mockResolvedValue({ data: null });
269
+ // Act
270
+ const { reports } = await adapter.loadItemTypes({ itemTypesToLoad });
271
+ // Assert
272
+ expect(emitSpy).not.toHaveBeenCalled();
273
+ expect(reports).toBeDefined();
274
+ });
275
+ });
276
+ describe(`${worker_adapter_1.WorkerAdapter.name}.loadItemTypes — additional branches`, () => {
277
+ let adapter;
278
+ let emitSpy;
279
+ beforeEach(() => {
280
+ jest.clearAllMocks();
281
+ ({ adapter } = makeAdapter(types_1.EventType.ContinueLoadingData));
282
+ emitSpy = jest.spyOn(adapter, 'emit').mockResolvedValue();
283
+ });
284
+ afterEach(() => {
285
+ jest.restoreAllMocks();
286
+ });
287
+ it('should return immediately with empty reports when filesToLoad is empty', async () => {
288
+ // Arrange
289
+ adapter['adapterState'].state.fromDevRev = { filesToLoad: [] };
290
+ // Act
291
+ const result = await adapter.loadItemTypes({
292
+ itemTypesToLoad: [
293
+ { itemType: 'tasks', create: jest.fn(), update: jest.fn() },
294
+ ],
295
+ });
296
+ // Assert
297
+ expect(result.reports).toEqual([]);
298
+ expect(emitSpy).not.toHaveBeenCalled();
299
+ });
300
+ it('should emit DataLoadingError when a file references an item type not in itemTypesToLoad', async () => {
301
+ // Arrange
302
+ adapter['adapterState'].state.fromDevRev = {
303
+ filesToLoad: [
304
+ {
305
+ id: 'art-1',
306
+ file_name: 'file.json',
307
+ itemType: 'unknown-type',
308
+ count: 1,
309
+ lineToProcess: 0,
310
+ completed: false,
311
+ },
312
+ ],
313
+ };
314
+ adapter['uploader'].getJsonObjectByArtifactId = jest
315
+ .fn()
316
+ .mockResolvedValue({ response: [makeLoaderItem()] });
317
+ // Act
318
+ await adapter.loadItemTypes({
319
+ itemTypesToLoad: [
320
+ { itemType: 'tasks', create: jest.fn(), update: jest.fn() },
321
+ ],
322
+ });
323
+ // Assert
324
+ expect(emitSpy).toHaveBeenCalledWith(types_1.LoaderEventType.DataLoadingError, expect.objectContaining({
325
+ error: expect.objectContaining({
326
+ message: expect.stringContaining('unknown-type'),
327
+ }),
328
+ }));
329
+ });
330
+ });
331
+ describe(`${worker_adapter_1.WorkerAdapter.name}.loadAttachments — timeout, transformer errors, unexpected errors`, () => {
332
+ let adapter;
333
+ let exitSpy;
334
+ let emitSpy;
335
+ function setupFilesToLoad(a, items) {
336
+ a['adapterState'].state.fromDevRev = {
337
+ filesToLoad: [
338
+ {
339
+ id: 'artifact-1',
340
+ file_name: 'attachments.json',
341
+ itemType: 'attachment',
342
+ count: items.length,
343
+ lineToProcess: 0,
344
+ completed: false,
345
+ },
346
+ ],
347
+ };
348
+ a['uploader'].getJsonObjectByArtifactId = jest
349
+ .fn()
350
+ .mockResolvedValue({ response: items });
351
+ }
352
+ beforeEach(() => {
353
+ jest.clearAllMocks();
354
+ ({ adapter } = makeAdapter(types_1.EventType.ContinueLoadingAttachments));
355
+ exitSpy = jest
356
+ .spyOn(process, 'exit')
357
+ .mockImplementation(() => undefined);
358
+ emitSpy = jest.spyOn(adapter, 'emit').mockResolvedValue();
359
+ });
360
+ afterEach(() => {
361
+ exitSpy.mockRestore();
362
+ jest.restoreAllMocks();
363
+ });
364
+ it('should emit AttachmentLoadingProgress and exit on timeout', async () => {
365
+ // Arrange
366
+ const items = [
367
+ {
368
+ reference_id: 'ref-1',
369
+ parent_type: 'task',
370
+ parent_reference_id: 'parent-1',
371
+ file_name: 'file.pdf',
372
+ file_type: 'application/pdf',
373
+ file_size: 100,
374
+ url: 'https://example.com/file.pdf',
375
+ valid_until: '',
376
+ created_by_id: 'user-1',
377
+ created_date: '',
378
+ modified_by_id: 'user-1',
379
+ modified_date: '',
380
+ },
381
+ ];
382
+ setupFilesToLoad(adapter, items);
383
+ adapter.isTimeout = true;
384
+ // Act
385
+ await adapter.loadAttachments({
386
+ create: jest.fn(),
387
+ });
388
+ // Assert
389
+ expect(emitSpy).toHaveBeenCalledWith(types_1.LoaderEventType.AttachmentLoadingProgress);
390
+ expect(exitSpy).toHaveBeenCalledWith(0);
391
+ });
392
+ it('should emit AttachmentLoadingError on transformer file error', async () => {
393
+ // Arrange
394
+ adapter['adapterState'].state.fromDevRev = {
395
+ filesToLoad: [
396
+ {
397
+ id: 'bad-artifact',
398
+ file_name: 'attachments.json',
399
+ itemType: 'attachment',
400
+ count: 1,
401
+ lineToProcess: 0,
402
+ completed: false,
403
+ },
404
+ ],
405
+ };
406
+ adapter['uploader'].getJsonObjectByArtifactId = jest
407
+ .fn()
408
+ .mockResolvedValue({
409
+ response: null,
410
+ error: new Error('Artifact not found'),
411
+ });
412
+ // Act
413
+ await adapter.loadAttachments({
414
+ create: jest.fn(),
415
+ });
416
+ // Assert
417
+ expect(emitSpy).toHaveBeenCalledWith(types_1.LoaderEventType.AttachmentLoadingError, expect.objectContaining({
418
+ error: expect.objectContaining({
419
+ message: expect.stringContaining('Transformer file not found'),
420
+ }),
421
+ }));
422
+ });
423
+ it('should emit AttachmentLoadingError and exit(1) on unexpected error', async () => {
424
+ // Arrange
425
+ const items = [
426
+ {
427
+ reference_id: 'ref-1',
428
+ parent_type: 'task',
429
+ parent_reference_id: 'parent-1',
430
+ file_name: 'file.pdf',
431
+ file_type: 'application/pdf',
432
+ file_size: 100,
433
+ url: 'https://example.com/file.pdf',
434
+ valid_until: '',
435
+ created_by_id: 'user-1',
436
+ created_date: '',
437
+ modified_by_id: 'user-1',
438
+ modified_date: '',
439
+ },
440
+ ];
441
+ setupFilesToLoad(adapter, items);
442
+ const mockCreate = jest
443
+ .fn()
444
+ .mockRejectedValue(new Error('Unexpected API failure'));
445
+ // Act
446
+ await adapter.loadAttachments({ create: mockCreate });
447
+ // Assert
448
+ expect(emitSpy).toHaveBeenCalledWith(types_1.LoaderEventType.AttachmentLoadingError, expect.objectContaining({
449
+ error: expect.objectContaining({
450
+ message: expect.stringContaining('Error during attachment loading'),
451
+ }),
452
+ }));
453
+ expect(exitSpy).toHaveBeenCalledWith(1);
454
+ });
455
+ });
456
+ describe(`${worker_adapter_1.WorkerAdapter.name}.loadAttachments — additional branches`, () => {
457
+ let adapter;
458
+ let emitSpy;
459
+ beforeEach(() => {
460
+ jest.clearAllMocks();
461
+ ({ adapter } = makeAdapter(types_1.EventType.ContinueLoadingAttachments));
462
+ emitSpy = jest.spyOn(adapter, 'emit').mockResolvedValue();
463
+ });
464
+ afterEach(() => {
465
+ jest.restoreAllMocks();
466
+ });
467
+ it('should return immediately with empty reports when fromDevRev is not set', async () => {
468
+ // Arrange
469
+ adapter['adapterState'].state.fromDevRev = undefined;
470
+ // Act
471
+ const result = await adapter.loadAttachments({ create: jest.fn() });
472
+ // Assert
473
+ expect(result.reports).toEqual([]);
474
+ expect(emitSpy).not.toHaveBeenCalled();
475
+ });
476
+ it('should emit AttachmentLoadingDelayed and stop the loop when the connector signals a rate-limit delay', async () => {
477
+ // Arrange
478
+ adapter['adapterState'].state.fromDevRev = {
479
+ filesToLoad: [
480
+ {
481
+ id: 'art-1',
482
+ file_name: 'attachments.json',
483
+ itemType: 'attachment',
484
+ count: 1,
485
+ lineToProcess: 0,
486
+ completed: false,
487
+ },
488
+ ],
489
+ };
490
+ adapter['uploader'].getJsonObjectByArtifactId = jest
491
+ .fn()
492
+ .mockResolvedValue({
493
+ response: [
494
+ {
495
+ reference_id: 'ref-1',
496
+ parent_type: 'task',
497
+ parent_reference_id: 'parent-1',
498
+ file_name: 'file.pdf',
499
+ file_type: 'application/pdf',
500
+ file_size: 100,
501
+ url: 'https://example.com/file.pdf',
502
+ valid_until: '',
503
+ created_by_id: 'user-1',
504
+ created_date: '',
505
+ modified_by_id: 'user-1',
506
+ modified_date: '',
507
+ },
508
+ ],
509
+ });
510
+ jest
511
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
512
+ .spyOn(adapter, 'loadAttachment')
513
+ .mockResolvedValue({ rateLimit: { delay: 20 } });
514
+ // Act
515
+ await adapter.loadAttachments({ create: jest.fn() });
516
+ // Assert
517
+ expect(emitSpy).toHaveBeenCalledWith(types_1.LoaderEventType.AttachmentLoadingDelayed, expect.objectContaining({ delay: 20 }));
518
+ });
519
+ });
520
+ describe(`${worker_adapter_1.WorkerAdapter.name}.loadAttachment`, () => {
521
+ let adapter;
522
+ beforeEach(() => {
523
+ jest.clearAllMocks();
524
+ ({ adapter } = makeAdapter(types_1.EventType.ContinueLoadingAttachments));
525
+ });
526
+ afterEach(() => {
527
+ jest.restoreAllMocks();
528
+ });
529
+ function makeAttachment() {
530
+ return {
531
+ reference_id: 'ref-1',
532
+ parent_type: 'task',
533
+ parent_reference_id: 'parent-1',
534
+ file_name: 'file.pdf',
535
+ file_type: 'application/pdf',
536
+ file_size: 100,
537
+ url: 'https://example.com/file.pdf',
538
+ valid_until: '',
539
+ created_by_id: 'user-1',
540
+ created_date: '',
541
+ modified_by_id: 'user-1',
542
+ modified_date: '',
543
+ };
544
+ }
545
+ it('should return a CREATED report when create succeeds', async () => {
546
+ var _a, _b;
547
+ // Arrange
548
+ adapter['_mappers'].create = jest.fn().mockResolvedValue({ data: {} });
549
+ const create = jest.fn().mockResolvedValue({ id: 'att-ext-1' });
550
+ // Act
551
+ const result = await adapter['loadAttachment']({
552
+ item: makeAttachment(),
553
+ create,
554
+ });
555
+ // Assert
556
+ expect((_a = result.report) === null || _a === void 0 ? void 0 : _a.item_type).toBe('attachment');
557
+ expect((_b = result.report) === null || _b === void 0 ? void 0 : _b[loading_1.ActionType.CREATED]).toBe(1);
558
+ });
559
+ it('should still return CREATED even when mapper create fails — attachment loading is resilient', async () => {
560
+ var _a;
561
+ // Arrange
562
+ adapter['_mappers'].create = jest
563
+ .fn()
564
+ .mockRejectedValue(new Error('mapper failed'));
565
+ const create = jest.fn().mockResolvedValue({ id: 'att-ext-1' });
566
+ // Act
567
+ const result = await adapter['loadAttachment']({
568
+ item: makeAttachment(),
569
+ create,
570
+ });
571
+ // Assert
572
+ expect((_a = result.report) === null || _a === void 0 ? void 0 : _a[loading_1.ActionType.CREATED]).toBe(1);
573
+ });
574
+ it('should propagate rate-limit delay when the connector signals one', async () => {
575
+ var _a;
576
+ // Arrange
577
+ const create = jest.fn().mockResolvedValue({ delay: 30 });
578
+ // Act
579
+ const result = await adapter['loadAttachment']({
580
+ item: makeAttachment(),
581
+ create,
582
+ });
583
+ // Assert
584
+ expect((_a = result.rateLimit) === null || _a === void 0 ? void 0 : _a.delay).toBe(30);
585
+ });
586
+ it('should return a FAILED report when create returns neither id nor delay', async () => {
587
+ var _a;
588
+ // Arrange
589
+ const create = jest.fn().mockResolvedValue({ id: null, delay: null });
590
+ // Act
591
+ const result = await adapter['loadAttachment']({
592
+ item: makeAttachment(),
593
+ create,
594
+ });
595
+ // Assert
596
+ expect((_a = result.report) === null || _a === void 0 ? void 0 : _a[loading_1.ActionType.FAILED]).toBe(1);
597
+ });
598
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=worker-adapter.serialization.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-adapter.serialization.test.d.ts","sourceRoot":"","sources":["../../../src/multithreading/worker-adapter/worker-adapter.serialization.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const js_jsonl_1 = require("js-jsonl");
4
+ // Pin the serialization contract for items that reach the uploader.
5
+ //
6
+ // The SDK uploads items via Uploader.upload(), which calls jsonl.stringify() on
7
+ // the input. That means user `create`/`update` callbacks and normalizers can
8
+ // silently produce inputs that fail or lose information at the wire boundary:
9
+ //
10
+ // - Circular references throw "Converting circular structure to JSON"
11
+ // - BigInt values throw "Do not know how to serialize a BigInt"
12
+ // - Date objects are converted to ISO strings (information loss: no Date on
13
+ // the other side, just a string)
14
+ // - undefined fields are dropped (null fields are preserved)
15
+ //
16
+ // These tests exist to catch regressions in the serialization layer (e.g.,
17
+ // silently switching to a different serializer that masks BigInt or mangles
18
+ // Dates) before they reach production.
19
+ describe('serialization boundary for items uploaded via jsonl', () => {
20
+ it('throws when an item contains a circular reference', () => {
21
+ // Arrange
22
+ const item = { id: 'a' };
23
+ item.self = item;
24
+ // Act & Assert
25
+ expect(() => js_jsonl_1.jsonl.stringify([item])).toThrow(/circular/i);
26
+ });
27
+ it('throws when an item contains a BigInt field', () => {
28
+ // Arrange
29
+ const item = { id: 'a', counter: BigInt(1) };
30
+ // Act & Assert
31
+ expect(() => js_jsonl_1.jsonl.stringify([item])).toThrow(/BigInt/i);
32
+ });
33
+ it('serializes Date instances to ISO strings (information loss — consumer receives a string)', () => {
34
+ // Arrange
35
+ const item = {
36
+ id: 'a',
37
+ created: new Date('2025-01-01T00:00:00.000Z'),
38
+ };
39
+ // Act
40
+ const output = js_jsonl_1.jsonl.stringify([item]);
41
+ const parsed = JSON.parse(output);
42
+ // Assert
43
+ expect(parsed.created).toBe('2025-01-01T00:00:00.000Z');
44
+ expect(typeof parsed.created).toBe('string');
45
+ });
46
+ it('drops undefined fields but preserves null fields', () => {
47
+ // Arrange
48
+ const item = {
49
+ id: 'a',
50
+ present: null,
51
+ missing: undefined,
52
+ };
53
+ // Act
54
+ const output = js_jsonl_1.jsonl.stringify([item]);
55
+ const parsed = JSON.parse(output);
56
+ // Assert
57
+ expect(parsed).toEqual({ id: 'a', present: null });
58
+ expect(parsed).not.toHaveProperty('missing');
59
+ });
60
+ it('emits one newline-terminated line per item (jsonl format)', () => {
61
+ // Arrange
62
+ const items = [{ id: 'a' }, { id: 'b' }, { id: 'c' }];
63
+ // Act
64
+ const output = js_jsonl_1.jsonl.stringify(items);
65
+ const lines = output.split('\n').filter((l) => l.length > 0);
66
+ // Assert
67
+ expect(lines).toHaveLength(3);
68
+ expect(JSON.parse(lines[0])).toEqual({ id: 'a' });
69
+ expect(JSON.parse(lines[2])).toEqual({ id: 'c' });
70
+ });
71
+ });