Skip to content
2 changes: 2 additions & 0 deletions jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module.exports = {
'src/tests/dummy-connector/metadata-extraction.test.ts',
'src/http/axios-client-internal.test.ts',
'src/tests/event-data-size-limit/.*.test.ts',
'src/tests/upload-failure/upload-failure.slow.test.ts',
],
},
{
Expand All @@ -33,6 +34,7 @@ module.exports = {
'<rootDir>/src/tests/dummy-connector/metadata-extraction.test.ts',
'<rootDir>/src/http/axios-client-internal.test.ts',
'<rootDir>/src/tests/event-data-size-limit/size-limit-1.test.ts',
'<rootDir>/src/tests/upload-failure/upload-failure.slow.test.ts',
],
},
],
Expand Down
9 changes: 9 additions & 0 deletions src/mock-server/mock-server.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ export interface RouteConfig {
headers?: Record<string, string>;
/** Optional retry configuration for simulating failures before success */
retry?: RetryConfig;
/**
* Respond successfully for the first N requests, then return an error.
* Useful for partial-upload scenarios (first batch succeeds, second fails).
*/
succeedThenFail?: {
successCount: number;
errorStatus?: number;
errorBody?: unknown;
};
/** Optional delay in milliseconds before sending the response */
delay?: number;
}
Expand Down
41 changes: 37 additions & 4 deletions src/mock-server/mock-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,18 +216,41 @@ export class MockServer {
* Configures a route to return a specific status code and optional response body.
*/
public setRoute(config: RouteConfig): void {
const { path, method, status, body, bodyBuffer, retry, headers, delay } =
config;
const {
path,
method,
status,
body,
bodyBuffer,
retry,
succeedThenFail,
headers,
delay,
} = config;
const key = this.getRouteKey(method, path);

if (retry) {
if (retry || succeedThenFail) {
this.requestCounts.set(key, 0);
}

this.routeHandlers.set(key, (req: ParsedRequest, res: MockResponse) => {
const sendResponse = (responseDelay?: number) => {
const send = () => {
if (retry) {
if (succeedThenFail) {
const currentCount = this.requestCounts.get(key) || 0;
this.requestCounts.set(key, currentCount + 1);

if (currentCount < succeedThenFail.successCount) {
this.defaultRouteHandler(req, res);
} else {
const errorStatus = succeedThenFail.errorStatus ?? 400;
if (succeedThenFail.errorBody !== undefined) {
res.status(errorStatus).json(succeedThenFail.errorBody);
} else {
res.status(errorStatus).send();
}
}
} else if (retry) {
const currentCount = this.requestCounts.get(key) || 0;
const failureCount = retry.failureCount ?? 4;
const errorStatus = retry.errorStatus ?? 500;
Expand Down Expand Up @@ -303,6 +326,16 @@ export class MockServer {
this.requests = [];
}

/**
* Returns all POST requests to the platform callback URL.
*/
public getCallbackRequests(): RequestInfo[] {
return this.requests.filter(
(req) =>
req.method.toUpperCase() === 'POST' && req.url.includes('callback_url')
);
}

/**
* Returns the most recent request or undefined if no requests exist.
*/
Expand Down
48 changes: 43 additions & 5 deletions src/multithreading/worker-adapter/worker-adapter.emit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,11 @@ describe(`${WorkerAdapter.name}.emit`, () => {
expect(mockPostMessage).toHaveBeenCalledTimes(1);
});

it('should correctly emit one event even if uploadAllRepos errors', async () => {
it('should log state size and current state before posting state', async () => {
// Arrange
const logSpy = jest.spyOn(console, 'log').mockImplementation();
adapter['adapterState'].postState = jest.fn().mockResolvedValue(undefined);
adapter.uploadAllRepos = jest
.fn()
.mockRejectedValue(new Error('uploadAllRepos error'));
adapter.uploadAllRepos = jest.fn().mockResolvedValue(undefined);

// Act
await adapter.emit(ExtractorEventType.MetadataExtractionError, {
Expand All @@ -157,7 +156,46 @@ describe(`${WorkerAdapter.name}.emit`, () => {
});

// Assert
expect(mockPostMessage).toHaveBeenCalledTimes(1);
const stateLogCall = logSpy.mock.calls.find(
([message]) =>
typeof message === 'string' &&
message.includes('Saving ') &&
message.includes('Current state')
);
expect(stateLogCall).toBeDefined();
expect(stateLogCall?.[0]).toContain(
'KB state before emitting event with event type: METADATA_EXTRACTION_ERROR. Current state'
);
expect(stateLogCall?.[1]).toEqual(
expect.objectContaining({ attachments: { completed: false } })
);
});

it('should emit phase extraction error when uploadAllRepos fails', async () => {
const { emit: mockEmit } = require('../../common/control-protocol');
adapter['adapterState'].postState = jest.fn().mockResolvedValue(undefined);
adapter.uploadAllRepos = jest
.fn()
.mockRejectedValue(new Error('uploadAllRepos error'));

await adapter.emit(ExtractorEventType.DataExtractionDone);

expect(mockEmit).toHaveBeenCalledWith(
expect.objectContaining({
eventType: ExtractorEventType.DataExtractionError,
data: expect.objectContaining({
error: expect.objectContaining({
message: expect.stringContaining('uploadAllRepos error'),
}),
}),
})
);
expect(mockPostMessage).toHaveBeenCalledWith(
expect.objectContaining({
subject: 'emit',
payload: { eventType: ExtractorEventType.DataExtractionError },
})
);
});

it('should include artifacts in data for extraction events', async () => {
Expand Down
Loading
Loading