@devrev/ts-adaas 1.19.4 → 1.19.5
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/dist/attachments-streaming/attachments-streaming-pool.test.js +3 -6
- package/dist/common/event-type-translation.test.d.ts +2 -0
- package/dist/common/event-type-translation.test.d.ts.map +1 -0
- package/dist/common/event-type-translation.test.js +175 -0
- package/dist/common/time-value-resolver.test.js +0 -1
- package/dist/multithreading/create-worker.test.js +34 -16
- package/dist/multithreading/process-task.test.d.ts +2 -0
- package/dist/multithreading/process-task.test.d.ts.map +1 -0
- package/dist/multithreading/process-task.test.js +166 -0
- package/dist/multithreading/spawn/spawn.test.d.ts +2 -0
- package/dist/multithreading/spawn/spawn.test.d.ts.map +1 -0
- package/dist/multithreading/spawn/spawn.test.js +223 -0
- package/dist/multithreading/worker-adapter/worker-adapter.emit.test.d.ts +2 -0
- package/dist/multithreading/worker-adapter/worker-adapter.emit.test.d.ts.map +1 -0
- package/dist/multithreading/worker-adapter/worker-adapter.emit.test.js +415 -0
- package/dist/multithreading/worker-adapter/worker-adapter.extraction.test.d.ts +2 -0
- package/dist/multithreading/worker-adapter/worker-adapter.extraction.test.d.ts.map +1 -0
- package/dist/multithreading/worker-adapter/worker-adapter.extraction.test.js +801 -0
- package/dist/multithreading/worker-adapter/worker-adapter.loading.test.d.ts +2 -0
- package/dist/multithreading/worker-adapter/worker-adapter.loading.test.d.ts.map +1 -0
- package/dist/multithreading/worker-adapter/worker-adapter.loading.test.js +598 -0
- package/dist/multithreading/worker-adapter/worker-adapter.serialization.test.d.ts +2 -0
- package/dist/multithreading/worker-adapter/worker-adapter.serialization.test.d.ts.map +1 -0
- package/dist/multithreading/worker-adapter/worker-adapter.serialization.test.js +71 -0
- package/dist/repo/repo.test.js +41 -0
- package/dist/state/state.extract-window.test.d.ts +2 -0
- package/dist/state/state.extract-window.test.d.ts.map +1 -0
- package/dist/state/state.extract-window.test.js +163 -0
- package/dist/state/state.pending-boundaries.test.d.ts +2 -0
- package/dist/state/state.pending-boundaries.test.d.ts.map +1 -0
- package/dist/state/state.pending-boundaries.test.js +189 -0
- package/dist/state/state.post-state.test.d.ts +2 -0
- package/dist/state/state.post-state.test.d.ts.map +1 -0
- package/dist/state/state.post-state.test.js +77 -0
- package/dist/state/state.test.js +23 -506
- package/dist/state/state.time-value-resolution.test.d.ts +2 -0
- package/dist/state/state.time-value-resolution.test.d.ts.map +1 -0
- package/dist/state/state.time-value-resolution.test.js +175 -0
- package/dist/types/extraction.test.js +57 -21
- package/dist/uploader/uploader.helpers.test.js +0 -11
- package/dist/uploader/uploader.test.js +0 -9
- package/package.json +7 -6
- package/dist/multithreading/worker-adapter/worker-adapter.test.d.ts +0 -2
- package/dist/multithreading/worker-adapter/worker-adapter.test.d.ts.map +0 -1
- package/dist/multithreading/worker-adapter/worker-adapter.test.js +0 -1243
|
@@ -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 @@
|
|
|
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
|
+
});
|