@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,223 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const events_1 = require("events");
4
+ const constants_1 = require("../../common/constants");
5
+ const extraction_1 = require("../../types/extraction");
6
+ const workers_1 = require("../../types/workers");
7
+ const test_utils_1 = require("../../common/test-utils");
8
+ // ---------------------------------------------------------------------------
9
+ // Mocks
10
+ // ---------------------------------------------------------------------------
11
+ jest.mock('../create-worker', () => ({
12
+ createWorker: jest.fn(),
13
+ }));
14
+ jest.mock('../../common/control-protocol', () => ({
15
+ emit: jest.fn().mockResolvedValue({}),
16
+ }));
17
+ jest.mock('../../logger/logger', () => ({
18
+ Logger: jest.fn().mockImplementation(() => ({
19
+ log: jest.fn(),
20
+ warn: jest.fn(),
21
+ error: jest.fn(),
22
+ info: jest.fn(),
23
+ logFn: jest.fn(),
24
+ })),
25
+ serializeError: jest.fn((e) => String(e)),
26
+ }));
27
+ jest.mock('../../common/helpers', () => ({
28
+ getLibraryVersion: jest.fn().mockReturnValue('1.0.0-test'),
29
+ getMemoryUsage: jest.fn().mockReturnValue({
30
+ formattedMessage: 'Memory: RSS 100/512MB (19.53%) [...]',
31
+ rssUsedMB: '100.00',
32
+ rssUsedPercent: '19.53%',
33
+ heapUsedPercent: '30.00%',
34
+ externalMB: '10.00',
35
+ arrayBuffersMB: '5.00',
36
+ }),
37
+ sleep: jest.fn(),
38
+ truncateFilename: jest.fn((f) => f),
39
+ truncateMessage: jest.fn((m) => m),
40
+ }));
41
+ // ---------------------------------------------------------------------------
42
+ // Imports after mocks
43
+ // ---------------------------------------------------------------------------
44
+ const spawn_1 = require("./spawn");
45
+ const create_worker_1 = require("../create-worker");
46
+ const control_protocol_1 = require("../../common/control-protocol");
47
+ const helpers_1 = require("../../common/helpers");
48
+ // ---------------------------------------------------------------------------
49
+ // Factory for a fake worker (EventEmitter with postMessage + terminate)
50
+ // ---------------------------------------------------------------------------
51
+ function makeWorker() {
52
+ const w = new events_1.EventEmitter();
53
+ w.postMessage = jest.fn();
54
+ w.terminate = jest.fn().mockResolvedValue(0);
55
+ return w;
56
+ }
57
+ // ---------------------------------------------------------------------------
58
+ // Helper: instantiate Spawn directly, injecting a mock logger via console swap
59
+ // ---------------------------------------------------------------------------
60
+ function buildSpawn(overrides) {
61
+ var _a;
62
+ const event = (0, test_utils_1.createMockEvent)('http://localhost:0', {
63
+ payload: { event_type: extraction_1.EventType.StartExtractingData },
64
+ });
65
+ const mockLogger = {
66
+ log: jest.fn(),
67
+ warn: jest.fn(),
68
+ error: jest.fn(),
69
+ info: jest.fn(),
70
+ logFn: jest.fn(),
71
+ };
72
+ const originalConsole = console;
73
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
74
+ global.console = mockLogger;
75
+ const s = new spawn_1.Spawn({
76
+ event,
77
+ worker: overrides.worker,
78
+ options: overrides.options,
79
+ resolve: (_a = overrides.resolve) !== null && _a !== void 0 ? _a : jest.fn(),
80
+ originalConsole: originalConsole,
81
+ });
82
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
+ global.console = originalConsole;
84
+ return s;
85
+ }
86
+ // ---------------------------------------------------------------------------
87
+ // spawn() factory tests
88
+ // ---------------------------------------------------------------------------
89
+ describe('spawn() factory', () => {
90
+ beforeEach(() => {
91
+ jest.clearAllMocks();
92
+ jest.useFakeTimers({ legacyFakeTimers: true });
93
+ });
94
+ afterEach(() => {
95
+ jest.useRealTimers();
96
+ });
97
+ it('should emit a no-script event and NOT spawn a worker for an unknown event type', async () => {
98
+ // Arrange
99
+ const event = (0, test_utils_1.createMockEvent)('http://localhost:0', {
100
+ payload: { event_type: extraction_1.EventType.UnknownEventType },
101
+ });
102
+ // Act
103
+ await (0, spawn_1.spawn)({ event, initialState: {} });
104
+ // Assert: no worker process should be started, but the platform should
105
+ // still receive a terminal event (so the run doesn't hang).
106
+ expect(create_worker_1.createWorker).not.toHaveBeenCalled();
107
+ expect(control_protocol_1.emit).toHaveBeenCalledWith(expect.objectContaining({ event, eventType: expect.any(String) }));
108
+ });
109
+ it('should reject the returned promise when createWorker throws', async () => {
110
+ // Arrange
111
+ create_worker_1.createWorker.mockRejectedValue(new Error('worker boom'));
112
+ const event = (0, test_utils_1.createMockEvent)('http://localhost:0', {
113
+ payload: { event_type: extraction_1.EventType.StartExtractingData },
114
+ });
115
+ // Act & Assert
116
+ await expect((0, spawn_1.spawn)({ event, initialState: {}, workerPath: '/fake/path.js' })).rejects.toThrow('worker boom');
117
+ });
118
+ });
119
+ // ---------------------------------------------------------------------------
120
+ // Spawn class — lifecycle tests
121
+ // ---------------------------------------------------------------------------
122
+ describe('Spawn class', () => {
123
+ let worker;
124
+ let resolveMock;
125
+ beforeEach(() => {
126
+ jest.clearAllMocks();
127
+ jest.useFakeTimers({ legacyFakeTimers: true });
128
+ worker = makeWorker();
129
+ resolveMock = jest.fn();
130
+ });
131
+ afterEach(() => {
132
+ jest.useRealTimers();
133
+ });
134
+ // -------------------------------------------------------------------------
135
+ // WorkerMessageFailed captured and propagated in error emit
136
+ // -------------------------------------------------------------------------
137
+ it('should include the WorkerMessageFailed reason in the error event emitted to the platform', async () => {
138
+ // Arrange
139
+ buildSpawn({ worker, resolve: resolveMock });
140
+ // Act
141
+ worker.emit(workers_1.WorkerEvent.WorkerMessage, {
142
+ subject: workers_1.WorkerMessageSubject.WorkerMessageFailed,
143
+ payload: { message: 'connector exploded' },
144
+ });
145
+ worker.emit(workers_1.WorkerEvent.WorkerExit, 1);
146
+ await Promise.resolve();
147
+ await Promise.resolve();
148
+ // Assert: the platform receives an error event whose message contains
149
+ // the reason sent by the worker — this is what operators see in the run log.
150
+ expect(control_protocol_1.emit).toHaveBeenCalledWith(expect.objectContaining({
151
+ data: expect.objectContaining({
152
+ error: expect.objectContaining({
153
+ message: expect.stringContaining('connector exploded'),
154
+ }),
155
+ }),
156
+ }));
157
+ expect(resolveMock).toHaveBeenCalled();
158
+ });
159
+ it('should emit a data extraction error when the worker exits without ever emitting', async () => {
160
+ // Arrange
161
+ buildSpawn({ worker, resolve: resolveMock });
162
+ // Act
163
+ worker.emit(workers_1.WorkerEvent.WorkerExit, 1);
164
+ await Promise.resolve();
165
+ await Promise.resolve();
166
+ // Assert: a silent worker exit must surface a DataExtractionError to the
167
+ // platform with a non-empty message. The event type is the upstream
168
+ // contract; the exact message wording is implementation detail.
169
+ expect(control_protocol_1.emit).toHaveBeenCalledTimes(1);
170
+ const [emitted] = control_protocol_1.emit.mock.calls[0];
171
+ expect(emitted.eventType).toBe(extraction_1.ExtractorEventType.DataExtractionError);
172
+ expect(emitted.data.error.message).toEqual(expect.any(String));
173
+ expect(emitted.data.error.message.length).toBeGreaterThan(0);
174
+ expect(resolveMock).toHaveBeenCalled();
175
+ });
176
+ // Soft-timeout and hard-timeout timer behavior is covered end-to-end by
177
+ // src/tests/timeout-handling/ (real workers, real timers). Unit tests with
178
+ // fake timers only re-asserted the mocked setTimeout call and gave no signal.
179
+ // -------------------------------------------------------------------------
180
+ // Memory monitoring — error clears the interval
181
+ // -------------------------------------------------------------------------
182
+ it('should clear the memory monitoring interval when getMemoryUsage throws to prevent repeated crashes', async () => {
183
+ // Arrange
184
+ helpers_1.getMemoryUsage.mockImplementation(() => {
185
+ throw new Error('OOM');
186
+ });
187
+ const clearIntervalSpy = jest.spyOn(global, 'clearInterval');
188
+ buildSpawn({ worker, resolve: resolveMock });
189
+ // Act
190
+ jest.advanceTimersByTime(30001); // MEMORY_LOG_INTERVAL = 30 s
191
+ await Promise.resolve();
192
+ // Assert
193
+ expect(clearIntervalSpy).toHaveBeenCalled();
194
+ });
195
+ // -------------------------------------------------------------------------
196
+ // Soft-timeout race: worker emits AFTER softTimeoutSent — no double-emit
197
+ // -------------------------------------------------------------------------
198
+ it('should NOT emit an error when the worker successfully emits just after receiving the soft-timeout signal', async () => {
199
+ // Arrange
200
+ buildSpawn({ worker, resolve: resolveMock });
201
+ // Act: trigger soft timeout — sends WorkerMessageExit to the worker
202
+ jest.advanceTimersByTime(constants_1.DEFAULT_LAMBDA_TIMEOUT + 1);
203
+ await Promise.resolve();
204
+ // Confirm the soft-timeout path actually fired — guards against a silent
205
+ // pass if the timeout constant changes and the timer never runs.
206
+ expect(worker.postMessage).toHaveBeenCalledWith(expect.objectContaining({
207
+ subject: workers_1.WorkerMessageSubject.WorkerMessageExit,
208
+ }));
209
+ // Worker responds: emits its event successfully, then exits normally
210
+ worker.emit(workers_1.WorkerEvent.WorkerMessage, {
211
+ subject: workers_1.WorkerMessageSubject.WorkerMessageEmitted,
212
+ });
213
+ worker.emit(workers_1.WorkerEvent.WorkerExit, 0);
214
+ // The exit handler defers via setImmediate when softTimeoutSent=true
215
+ jest.runAllImmediates();
216
+ await Promise.resolve();
217
+ await Promise.resolve();
218
+ // Assert: no error should reach the platform — the worker completed its job
219
+ const errorEmits = control_protocol_1.emit.mock.calls.filter((call) => { var _a, _b; return (_b = (_a = call[0]) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.error; });
220
+ expect(errorEmits).toHaveLength(0);
221
+ expect(resolveMock).toHaveBeenCalled();
222
+ });
223
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=worker-adapter.emit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-adapter.emit.test.d.ts","sourceRoot":"","sources":["../../../src/multithreading/worker-adapter/worker-adapter.emit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,415 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const constants_1 = require("../../common/constants");
4
+ const state_1 = require("../../state/state");
5
+ const jest_setup_1 = require("../../tests/jest.setup");
6
+ const test_utils_1 = require("../../common/test-utils");
7
+ const types_1 = require("../../types");
8
+ const loading_1 = require("../../types/loading");
9
+ const worker_adapter_1 = require("./worker-adapter");
10
+ /* eslint-disable @typescript-eslint/no-require-imports */
11
+ jest.mock('../../common/control-protocol', () => ({
12
+ emit: jest.fn().mockResolvedValue({}),
13
+ }));
14
+ jest.mock('../../mappers/mappers');
15
+ jest.mock('../../uploader/uploader');
16
+ jest.mock('../../repo/repo');
17
+ jest.mock('node:worker_threads', () => ({
18
+ parentPort: { postMessage: jest.fn() },
19
+ }));
20
+ jest.mock('../../attachments-streaming/attachments-streaming-pool', () => ({
21
+ AttachmentsStreamingPool: jest.fn().mockImplementation(() => ({
22
+ streamAll: jest.fn().mockResolvedValue(undefined),
23
+ })),
24
+ }));
25
+ function makeAdapter(eventType = types_1.EventType.StartExtractingData) {
26
+ const event = (0, test_utils_1.createMockEvent)(jest_setup_1.mockServer.baseUrl, {
27
+ payload: { event_type: eventType },
28
+ });
29
+ const initialState = {
30
+ attachments: { completed: false },
31
+ lastSyncStarted: '',
32
+ lastSuccessfulSyncStarted: '',
33
+ snapInVersionId: '',
34
+ toDevRev: {
35
+ attachmentsMetadata: {
36
+ artifactIds: [],
37
+ lastProcessed: 0,
38
+ lastProcessedAttachmentsIdsList: [],
39
+ },
40
+ },
41
+ };
42
+ const adapterState = new state_1.State({ event, initialState });
43
+ const adapter = new worker_adapter_1.WorkerAdapter({ event, adapterState });
44
+ return { adapter, event, adapterState };
45
+ }
46
+ describe(`${worker_adapter_1.WorkerAdapter.name}.emit`, () => {
47
+ let adapter;
48
+ let mockPostMessage;
49
+ beforeEach(() => {
50
+ jest.clearAllMocks();
51
+ ({ adapter } = makeAdapter());
52
+ const workerThreads = require('node:worker_threads');
53
+ mockPostMessage = jest.fn();
54
+ if (workerThreads.parentPort) {
55
+ jest
56
+ .spyOn(workerThreads.parentPort, 'postMessage')
57
+ .mockImplementation(mockPostMessage);
58
+ }
59
+ else {
60
+ workerThreads.parentPort = { postMessage: mockPostMessage };
61
+ }
62
+ });
63
+ afterEach(() => {
64
+ jest.restoreAllMocks();
65
+ });
66
+ it('should emit only one event when multiple events of same type are sent', async () => {
67
+ // Arrange
68
+ adapter['adapterState'].postState = jest.fn().mockResolvedValue(undefined);
69
+ adapter.uploadAllRepos = jest.fn().mockResolvedValue(undefined);
70
+ // Act
71
+ await adapter.emit(types_1.ExtractorEventType.MetadataExtractionError, {
72
+ reports: [],
73
+ processed_files: [],
74
+ });
75
+ await adapter.emit(types_1.ExtractorEventType.MetadataExtractionError, {
76
+ reports: [],
77
+ processed_files: [],
78
+ });
79
+ // Assert
80
+ expect(mockPostMessage).toHaveBeenCalledTimes(1);
81
+ });
82
+ it('should emit only once even when a different event type follows', async () => {
83
+ // Arrange
84
+ adapter['adapterState'].postState = jest.fn().mockResolvedValue(undefined);
85
+ adapter.uploadAllRepos = jest.fn().mockResolvedValue(undefined);
86
+ // Act
87
+ await adapter.emit(types_1.ExtractorEventType.MetadataExtractionError, {
88
+ reports: [],
89
+ processed_files: [],
90
+ });
91
+ await adapter.emit(types_1.ExtractorEventType.DataExtractionError, {
92
+ reports: [],
93
+ processed_files: [],
94
+ });
95
+ await adapter.emit(types_1.ExtractorEventType.AttachmentExtractionError, {
96
+ reports: [],
97
+ processed_files: [],
98
+ });
99
+ // Assert
100
+ expect(mockPostMessage).toHaveBeenCalledTimes(1);
101
+ });
102
+ it('should correctly emit one event even if postState errors', async () => {
103
+ // Arrange
104
+ adapter['adapterState'].postState = jest
105
+ .fn()
106
+ .mockRejectedValue(new Error('postState error'));
107
+ adapter.uploadAllRepos = jest.fn().mockResolvedValue(undefined);
108
+ // Act
109
+ await adapter.emit(types_1.ExtractorEventType.MetadataExtractionError, {
110
+ reports: [],
111
+ processed_files: [],
112
+ });
113
+ // Assert
114
+ expect(mockPostMessage).toHaveBeenCalledTimes(1);
115
+ });
116
+ it('should correctly emit one event even if uploadAllRepos errors', async () => {
117
+ // Arrange
118
+ adapter['adapterState'].postState = jest.fn().mockResolvedValue(undefined);
119
+ adapter.uploadAllRepos = jest
120
+ .fn()
121
+ .mockRejectedValue(new Error('uploadAllRepos error'));
122
+ // Act
123
+ await adapter.emit(types_1.ExtractorEventType.MetadataExtractionError, {
124
+ reports: [],
125
+ processed_files: [],
126
+ });
127
+ // Assert
128
+ expect(mockPostMessage).toHaveBeenCalledTimes(1);
129
+ });
130
+ it('should include artifacts in data for extraction events', async () => {
131
+ // Arrange
132
+ const { emit: mockEmit } = require('../../common/control-protocol');
133
+ adapter['adapterState'].postState = jest.fn().mockResolvedValue(undefined);
134
+ adapter.uploadAllRepos = jest.fn().mockResolvedValue(undefined);
135
+ adapter['_artifacts'] = [
136
+ { id: 'art-1', item_count: 10, item_type: 'issues' },
137
+ ];
138
+ // Act
139
+ await adapter.emit(types_1.ExtractorEventType.DataExtractionDone);
140
+ // Assert
141
+ expect(mockEmit).toHaveBeenCalledWith(expect.objectContaining({
142
+ data: expect.objectContaining({
143
+ artifacts: expect.arrayContaining([
144
+ expect.objectContaining({ id: 'art-1' }),
145
+ ]),
146
+ }),
147
+ }));
148
+ const callData = mockEmit.mock.calls[0][0].data;
149
+ expect(callData).not.toHaveProperty('reports');
150
+ expect(callData).not.toHaveProperty('processed_files');
151
+ });
152
+ it('should include reports and processed_files in data for loader events', async () => {
153
+ // Arrange
154
+ const { emit: mockEmit } = require('../../common/control-protocol');
155
+ adapter['adapterState'].postState = jest.fn().mockResolvedValue(undefined);
156
+ adapter.uploadAllRepos = jest.fn().mockResolvedValue(undefined);
157
+ adapter['loaderReports'] = [
158
+ { item_type: 'tasks', [loading_1.ActionType.CREATED]: 5 },
159
+ ];
160
+ adapter['_processedFiles'] = ['file-1', 'file-2'];
161
+ // Act
162
+ await adapter.emit(types_1.LoaderEventType.DataLoadingDone);
163
+ // Assert
164
+ expect(mockEmit).toHaveBeenCalledWith(expect.objectContaining({
165
+ data: expect.objectContaining({
166
+ reports: expect.arrayContaining([
167
+ expect.objectContaining({ item_type: 'tasks' }),
168
+ ]),
169
+ processed_files: ['file-1', 'file-2'],
170
+ }),
171
+ }));
172
+ const callData = mockEmit.mock.calls[0][0].data;
173
+ expect(callData).not.toHaveProperty('artifacts');
174
+ });
175
+ it('should not include artifacts, reports, or processed_files for unknown event types', async () => {
176
+ // Arrange
177
+ const { emit: mockEmit } = require('../../common/control-protocol');
178
+ adapter['adapterState'].postState = jest.fn().mockResolvedValue(undefined);
179
+ adapter.uploadAllRepos = jest.fn().mockResolvedValue(undefined);
180
+ adapter['_artifacts'] = [
181
+ { id: 'art-1', item_count: 10, item_type: 'issues' },
182
+ ];
183
+ adapter['loaderReports'] = [
184
+ { item_type: 'tasks', [loading_1.ActionType.CREATED]: 5 },
185
+ ];
186
+ adapter['_processedFiles'] = ['file-1'];
187
+ // Act
188
+ await adapter.emit('SOME_UNKNOWN_EVENT');
189
+ // Assert
190
+ const callData = mockEmit.mock.calls[0][0].data;
191
+ expect(callData).not.toHaveProperty('artifacts');
192
+ expect(callData).not.toHaveProperty('reports');
193
+ expect(callData).not.toHaveProperty('processed_files');
194
+ });
195
+ it('should include artifacts for all ExtractorEventType values', async () => {
196
+ var _a, _b;
197
+ // Arrange
198
+ const { emit: mockEmit } = require('../../common/control-protocol');
199
+ const extractorEvents = [
200
+ types_1.ExtractorEventType.DataExtractionDone,
201
+ types_1.ExtractorEventType.DataExtractionProgress,
202
+ types_1.ExtractorEventType.DataExtractionError,
203
+ types_1.ExtractorEventType.AttachmentExtractionDone,
204
+ types_1.ExtractorEventType.AttachmentExtractionProgress,
205
+ ];
206
+ for (const eventType of extractorEvents) {
207
+ jest.clearAllMocks();
208
+ adapter.hasWorkerEmitted = false;
209
+ adapter['adapterState'].postState = jest
210
+ .fn()
211
+ .mockResolvedValue(undefined);
212
+ adapter.uploadAllRepos = jest.fn().mockResolvedValue(undefined);
213
+ // Act
214
+ await adapter.emit(eventType);
215
+ // Assert
216
+ const callData = (_b = (_a = mockEmit.mock.calls[0]) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.data;
217
+ expect(callData).toHaveProperty('artifacts');
218
+ expect(callData).not.toHaveProperty('reports');
219
+ }
220
+ });
221
+ it('should include reports and processed_files for all LoaderEventType values', async () => {
222
+ var _a, _b;
223
+ // Arrange
224
+ const { emit: mockEmit } = require('../../common/control-protocol');
225
+ const loaderEvents = [
226
+ types_1.LoaderEventType.DataLoadingDone,
227
+ types_1.LoaderEventType.DataLoadingProgress,
228
+ types_1.LoaderEventType.DataLoadingError,
229
+ types_1.LoaderEventType.AttachmentLoadingDone,
230
+ types_1.LoaderEventType.AttachmentLoadingProgress,
231
+ ];
232
+ for (const eventType of loaderEvents) {
233
+ jest.clearAllMocks();
234
+ adapter.hasWorkerEmitted = false;
235
+ adapter['adapterState'].postState = jest
236
+ .fn()
237
+ .mockResolvedValue(undefined);
238
+ adapter.uploadAllRepos = jest.fn().mockResolvedValue(undefined);
239
+ // Act
240
+ await adapter.emit(eventType);
241
+ // Assert
242
+ const callData = (_b = (_a = mockEmit.mock.calls[0]) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.data;
243
+ expect(callData).toHaveProperty('reports');
244
+ expect(callData).toHaveProperty('processed_files');
245
+ expect(callData).not.toHaveProperty('artifacts');
246
+ }
247
+ });
248
+ it('should truncate a long error message, preserving the original prefix', async () => {
249
+ var _a, _b;
250
+ // Arrange
251
+ adapter['adapterState'].postState = jest.fn().mockResolvedValue(undefined);
252
+ adapter.uploadAllRepos = jest.fn().mockResolvedValue(undefined);
253
+ const longMessage = 'E'.repeat(20000);
254
+ // Act
255
+ await adapter.emit(types_1.ExtractorEventType.DataExtractionError, {
256
+ error: { message: longMessage },
257
+ });
258
+ // Assert
259
+ const { emit: mockEmit } = require('../../common/control-protocol');
260
+ const emittedMessage = (_b = (_a = mockEmit.mock.calls[0][0].data) === null || _a === void 0 ? void 0 : _a.error) === null || _b === void 0 ? void 0 : _b.message;
261
+ expect(emittedMessage.length).toBeLessThan(longMessage.length);
262
+ expect(emittedMessage.startsWith('E'.repeat(100))).toBe(true);
263
+ });
264
+ });
265
+ describe(`${worker_adapter_1.WorkerAdapter.name}.emit — ExternalSyncUnitExtractionDone legacy path`, () => {
266
+ it('should upload ESUs via a repo and strip external_sync_units from the emitted payload', async () => {
267
+ // Arrange
268
+ const { adapter } = makeAdapter(types_1.EventType.StartExtractingExternalSyncUnits);
269
+ adapter['adapterState'].postState = jest.fn().mockResolvedValue(undefined);
270
+ adapter.uploadAllRepos = jest.fn().mockResolvedValue(undefined);
271
+ const pushMock = jest.fn().mockResolvedValue(undefined);
272
+ jest.spyOn(adapter, 'initializeRepos');
273
+ jest.spyOn(adapter, 'getRepo').mockReturnValue({ push: pushMock });
274
+ const esus = [{ id: 'esu-1' }, { id: 'esu-2' }];
275
+ // Act
276
+ await adapter.emit(types_1.ExtractorEventType.ExternalSyncUnitExtractionDone, {
277
+ external_sync_units: esus,
278
+ });
279
+ // Assert
280
+ expect(pushMock).toHaveBeenCalledWith(esus);
281
+ // external_sync_units must NOT appear in the payload sent to the platform
282
+ // (it would be too large for SQS — that is the entire reason this path exists).
283
+ const { emit: mockEmit } = require('../../common/control-protocol');
284
+ const emittedData = mockEmit.mock.calls[0][0].data;
285
+ expect(emittedData).not.toHaveProperty('external_sync_units');
286
+ });
287
+ });
288
+ describe('WorkerAdapter — workersOldest / workersNewest boundary updates', () => {
289
+ let adapter;
290
+ let mockPostMessage;
291
+ beforeEach(() => {
292
+ jest.clearAllMocks();
293
+ ({ adapter } = makeAdapter());
294
+ const workerThreads = require('node:worker_threads');
295
+ mockPostMessage = jest.fn();
296
+ if (workerThreads.parentPort) {
297
+ jest
298
+ .spyOn(workerThreads.parentPort, 'postMessage')
299
+ .mockImplementation(mockPostMessage);
300
+ }
301
+ else {
302
+ workerThreads.parentPort = { postMessage: mockPostMessage };
303
+ }
304
+ adapter['adapterState'].postState = jest.fn().mockResolvedValue(undefined);
305
+ adapter.uploadAllRepos = jest.fn().mockResolvedValue(undefined);
306
+ });
307
+ afterEach(() => {
308
+ jest.restoreAllMocks();
309
+ });
310
+ async function emitDone(adapterInstance, extractionStart, extractionEnd) {
311
+ adapterInstance.event.payload.event_context.extract_from = extractionStart;
312
+ adapterInstance.event.payload.event_context.extract_to = extractionEnd;
313
+ // Reset the emit guard so we can emit multiple times within one test.
314
+ adapterInstance['hasWorkerEmitted'] = false;
315
+ await adapterInstance.emit(types_1.ExtractorEventType.AttachmentExtractionDone, {
316
+ reports: [],
317
+ processed_files: [],
318
+ });
319
+ }
320
+ describe('initial import with UNBOUNDED start', () => {
321
+ it('should set workersOldest to UNBOUNDED_DATE_TIME_VALUE and workersNewest to extraction end', async () => {
322
+ await emitDone(adapter, constants_1.UNBOUNDED_DATE_TIME_VALUE, '2025-06-01T00:00:00.000Z');
323
+ expect(adapter.state.workersOldest).toBe(constants_1.UNBOUNDED_DATE_TIME_VALUE);
324
+ expect(adapter.state.workersNewest).toBe('2025-06-01T00:00:00.000Z');
325
+ });
326
+ });
327
+ describe('reconciliation after UNBOUNDED initial import', () => {
328
+ it('should NOT overwrite workersOldest when reconciliation start is later than sentinel', async () => {
329
+ await emitDone(adapter, constants_1.UNBOUNDED_DATE_TIME_VALUE, '2025-06-01T00:00:00.000Z');
330
+ await emitDone(adapter, '2025-01-01T00:00:00.000Z', '2025-03-01T00:00:00.000Z');
331
+ expect(adapter.state.workersOldest).toBe(constants_1.UNBOUNDED_DATE_TIME_VALUE);
332
+ expect(adapter.state.workersNewest).toBe('2025-06-01T00:00:00.000Z');
333
+ });
334
+ it('should NOT overwrite workersOldest even when reconciliation start is very early', async () => {
335
+ await emitDone(adapter, constants_1.UNBOUNDED_DATE_TIME_VALUE, '2025-06-01T00:00:00.000Z');
336
+ await emitDone(adapter, '1980-01-01T00:00:00.000Z', '1990-01-01T00:00:00.000Z');
337
+ expect(adapter.state.workersOldest).toBe(constants_1.UNBOUNDED_DATE_TIME_VALUE);
338
+ expect(adapter.state.workersNewest).toBe('2025-06-01T00:00:00.000Z');
339
+ });
340
+ });
341
+ describe('forward sync after UNBOUNDED initial import', () => {
342
+ it('should expand workersNewest forward while preserving workersOldest', async () => {
343
+ await emitDone(adapter, constants_1.UNBOUNDED_DATE_TIME_VALUE, '2025-06-01T00:00:00.000Z');
344
+ await emitDone(adapter, '2025-06-01T00:00:00.000Z', '2025-07-01T00:00:00.000Z');
345
+ expect(adapter.state.workersOldest).toBe(constants_1.UNBOUNDED_DATE_TIME_VALUE);
346
+ expect(adapter.state.workersNewest).toBe('2025-07-01T00:00:00.000Z');
347
+ });
348
+ });
349
+ describe('reconciliation with end beyond current newest', () => {
350
+ it('should expand workersNewest when reconciliation end is later', async () => {
351
+ await emitDone(adapter, constants_1.UNBOUNDED_DATE_TIME_VALUE, '2025-06-01T00:00:00.000Z');
352
+ await emitDone(adapter, '2024-01-01T00:00:00.000Z', '2025-08-01T00:00:00.000Z');
353
+ expect(adapter.state.workersOldest).toBe(constants_1.UNBOUNDED_DATE_TIME_VALUE);
354
+ expect(adapter.state.workersNewest).toBe('2025-08-01T00:00:00.000Z');
355
+ });
356
+ });
357
+ describe('first sync with absolute dates (no UNBOUNDED)', () => {
358
+ it('should set both boundaries from the extraction range', async () => {
359
+ await emitDone(adapter, '2025-01-01T00:00:00.000Z', '2025-03-01T00:00:00.000Z');
360
+ expect(adapter.state.workersOldest).toBe('2025-01-01T00:00:00.000Z');
361
+ expect(adapter.state.workersNewest).toBe('2025-03-01T00:00:00.000Z');
362
+ });
363
+ });
364
+ describe('reconciliation after absolute initial sync', () => {
365
+ it('should expand workersOldest backward when reconciliation start is earlier', async () => {
366
+ await emitDone(adapter, '2025-01-01T00:00:00.000Z', '2025-03-01T00:00:00.000Z');
367
+ await emitDone(adapter, '2024-06-01T00:00:00.000Z', '2025-02-01T00:00:00.000Z');
368
+ expect(adapter.state.workersOldest).toBe('2024-06-01T00:00:00.000Z');
369
+ expect(adapter.state.workersNewest).toBe('2025-03-01T00:00:00.000Z');
370
+ });
371
+ it('should NOT change boundaries when reconciliation is within existing range', async () => {
372
+ await emitDone(adapter, '2025-01-01T00:00:00.000Z', '2025-03-01T00:00:00.000Z');
373
+ await emitDone(adapter, '2025-01-15T00:00:00.000Z', '2025-02-15T00:00:00.000Z');
374
+ expect(adapter.state.workersOldest).toBe('2025-01-01T00:00:00.000Z');
375
+ expect(adapter.state.workersNewest).toBe('2025-03-01T00:00:00.000Z');
376
+ });
377
+ it('should expand both boundaries when reconciliation exceeds both', async () => {
378
+ await emitDone(adapter, '2025-01-01T00:00:00.000Z', '2025-03-01T00:00:00.000Z');
379
+ await emitDone(adapter, '2024-06-01T00:00:00.000Z', '2025-09-01T00:00:00.000Z');
380
+ expect(adapter.state.workersOldest).toBe('2024-06-01T00:00:00.000Z');
381
+ expect(adapter.state.workersNewest).toBe('2025-09-01T00:00:00.000Z');
382
+ });
383
+ });
384
+ describe('multiple forward syncs', () => {
385
+ it('should progressively expand workersNewest while preserving workersOldest', async () => {
386
+ await emitDone(adapter, constants_1.UNBOUNDED_DATE_TIME_VALUE, '2025-06-01T00:00:00.000Z');
387
+ await emitDone(adapter, '2025-06-01T00:00:00.000Z', '2025-07-01T00:00:00.000Z');
388
+ expect(adapter.state.workersNewest).toBe('2025-07-01T00:00:00.000Z');
389
+ await emitDone(adapter, '2025-07-01T00:00:00.000Z', '2025-08-01T00:00:00.000Z');
390
+ expect(adapter.state.workersNewest).toBe('2025-08-01T00:00:00.000Z');
391
+ expect(adapter.state.workersOldest).toBe(constants_1.UNBOUNDED_DATE_TIME_VALUE);
392
+ });
393
+ });
394
+ describe('non-AttachmentExtractionDone events should NOT update boundaries', () => {
395
+ it.each([
396
+ types_1.ExtractorEventType.DataExtractionDone,
397
+ types_1.ExtractorEventType.DataExtractionProgress,
398
+ types_1.ExtractorEventType.MetadataExtractionError,
399
+ types_1.ExtractorEventType.AttachmentExtractionError,
400
+ ])('should not update boundaries on %s', async (eventType) => {
401
+ adapter.state.workersOldest = '2025-01-01T00:00:00.000Z';
402
+ adapter.state.workersNewest = '2025-03-01T00:00:00.000Z';
403
+ adapter.event.payload.event_context.extract_from =
404
+ '2024-01-01T00:00:00.000Z';
405
+ adapter.event.payload.event_context.extract_to =
406
+ '2025-12-01T00:00:00.000Z';
407
+ await adapter.emit(eventType, {
408
+ reports: [],
409
+ processed_files: [],
410
+ });
411
+ expect(adapter.state.workersOldest).toBe('2025-01-01T00:00:00.000Z');
412
+ expect(adapter.state.workersNewest).toBe('2025-03-01T00:00:00.000Z');
413
+ });
414
+ });
415
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=worker-adapter.extraction.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-adapter.extraction.test.d.ts","sourceRoot":"","sources":["../../../src/multithreading/worker-adapter/worker-adapter.extraction.test.ts"],"names":[],"mappings":""}