@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,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 @@
|
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"worker-adapter.extraction.test.d.ts","sourceRoot":"","sources":["../../../src/multithreading/worker-adapter/worker-adapter.extraction.test.ts"],"names":[],"mappings":""}
|