chore: Test improvements (#10945)

* Lazy queues, correctly closing Redis and server

* feedback

* fix: Tests not correctly split across matrix
This commit is contained in:
Tom Moor
2025-12-17 23:15:55 -05:00
committed by GitHub
parent b722a361ff
commit a54e66e19a
14 changed files with 87 additions and 52 deletions
+1 -1
View File
@@ -133,7 +133,7 @@ jobs:
- run: yarn sequelize db:migrate
- name: Run server tests
run: |
TESTFILES=$(find . -name "*.test.ts" -path "*/server/*" | sort | split -n -d -l $(($(find . -name "*.test.ts" -path "*/server/*" | wc -l)/${{ matrix.shard }})) - | sed -n "${{ matrix.shard }}p")
TESTFILES=$(find . -name "*.test.ts" -path "*/server/*" | sort | awk "NR % 4 == (${{ matrix.shard }} - 1)")
yarn test --maxWorkers=2 $TESTFILES
bundle-size:
+4 -1
View File
@@ -10,7 +10,10 @@
"^@server/(.*)$": "<rootDir>/server/$1",
"^@shared/(.*)$": "<rootDir>/shared/$1"
},
"setupFiles": ["<rootDir>/__mocks__/console.js"],
"setupFiles": [
"<rootDir>/__mocks__/console.js",
"<rootDir>/server/test/setupMocks.js"
],
"setupFilesAfterEnv": ["<rootDir>/server/test/setup.ts"],
"globalTeardown": "<rootDir>/server/test/globalTeardown.js",
"testEnvironment": "node"
+1 -1
View File
@@ -28,7 +28,7 @@
"db:rollback": "sequelize db:migrate:undo",
"db:reset": "sequelize db:drop && sequelize db:create && sequelize db:migrate",
"upgrade": "git fetch && git pull && yarn install && yarn heroku-postbuild",
"test": "TZ=UTC jest --config=.jestconfig.json --forceExit",
"test": "TZ=UTC jest --config=.jestconfig.json",
"test:app": "TZ=UTC jest --config=.jestconfig.json --selectProjects app",
"test:shared": "TZ=UTC jest --config=.jestconfig.json --selectProjects shared-node shared-jsdom",
"test:server": "TZ=UTC jest --config=.jestconfig.json --selectProjects server",
+1 -1
View File
@@ -68,7 +68,7 @@ export default abstract class BaseEmail<
// Ideally we'd use EmailTask.schedule here but importing creates a circular
// dependency so we're pushing onto the task queue in the expected format
return taskQueue.add(
return taskQueue().add(
{
name: "EmailTask",
props: {
+3 -3
View File
@@ -88,11 +88,11 @@ class Event extends IdModel<
// We want to use the parent transaction, otherwise the 'afterCommit' hook will never fire in this case.
// See: https://github.com/sequelize/sequelize/issues/17452
(options.transaction.parent || options.transaction).afterCommit(
() => void globalEventQueue.add(model)
() => void globalEventQueue().add(model)
);
return;
}
void globalEventQueue.add(model);
void globalEventQueue().add(model);
}
// associations
@@ -138,7 +138,7 @@ class Event extends IdModel<
*/
static schedule(event: Partial<Event>) {
const now = new Date();
return globalEventQueue.add(
return globalEventQueue().add(
this.build({
createdAt: now,
...event,
+48 -24
View File
@@ -1,30 +1,54 @@
import { createQueue } from "@server/queues/queue";
import { Second } from "@shared/utils/time";
export const globalEventQueue = createQueue("globalEvents", {
attempts: 5,
backoff: {
type: "exponential",
delay: Second.ms,
},
});
let cachedGlobalEventQueue: ReturnType<typeof createQueue> | undefined;
export const globalEventQueue = () => {
if (!cachedGlobalEventQueue) {
cachedGlobalEventQueue = createQueue("globalEvents", {
attempts: 5,
backoff: {
type: "exponential",
delay: Second.ms,
},
});
}
return cachedGlobalEventQueue;
};
export const processorEventQueue = createQueue("processorEvents", {
attempts: 5,
backoff: {
type: "exponential",
delay: 10 * Second.ms,
},
});
let cachedProcessorEventQueue: ReturnType<typeof createQueue> | undefined;
export const processorEventQueue = () => {
if (!cachedProcessorEventQueue) {
cachedProcessorEventQueue = createQueue("processorEvents", {
attempts: 5,
backoff: {
type: "exponential",
delay: 10 * Second.ms,
},
});
}
return cachedProcessorEventQueue;
};
export const websocketQueue = createQueue("websockets", {
timeout: 10 * Second.ms,
});
let cachedWebsocketQueue: ReturnType<typeof createQueue> | undefined;
export const websocketQueue = () => {
if (!cachedWebsocketQueue) {
cachedWebsocketQueue = createQueue("websockets", {
timeout: 10 * Second.ms,
});
}
return cachedWebsocketQueue;
};
export const taskQueue = createQueue("tasks", {
attempts: 5,
backoff: {
type: "exponential",
delay: 10 * Second.ms,
},
});
let cachedTaskQueue: ReturnType<typeof createQueue> | undefined;
export const taskQueue = () => {
if (!cachedTaskQueue) {
cachedTaskQueue = createQueue("tasks", {
attempts: 5,
backoff: {
type: "exponential",
delay: 10 * Second.ms,
},
});
}
return cachedTaskQueue;
};
@@ -13,7 +13,7 @@ export default class DebounceProcessor extends BaseProcessor {
async perform(event: Event) {
switch (event.name) {
case "documents.update": {
await globalEventQueue.add(
await globalEventQueue().add(
{ ...event, name: "documents.update.delayed" },
{
// speed up revision creation in development, we don't have all the
@@ -41,7 +41,7 @@ export default class DebounceProcessor extends BaseProcessor {
return;
}
await globalEventQueue.add({
await globalEventQueue().add({
...event,
name: "documents.update.debounced",
});
+1 -1
View File
@@ -17,7 +17,7 @@ export abstract class BaseTask<T extends Record<string, any>> {
* @returns A promise that resolves once the job is placed on the task queue
*/
public schedule(props: T, options?: JobOptions): Promise<Job> {
return taskQueue.add(
return taskQueue().add(
{
name: this.constructor.name,
props,
+4 -4
View File
@@ -13,10 +13,10 @@ export default function init(app: Koa) {
const serverAdapter = new KoaAdapter();
createBullBoard({
queues: [
new BullAdapter(globalEventQueue),
new BullAdapter(processorEventQueue),
new BullAdapter(websocketQueue),
new BullAdapter(taskQueue),
new BullAdapter(globalEventQueue()),
new BullAdapter(processorEventQueue()),
new BullAdapter(websocketQueue()),
new BullAdapter(taskQueue()),
],
serverAdapter,
});
+1 -1
View File
@@ -142,7 +142,7 @@ export default function init(
// Handle events from event queue that should be sent to the clients down ws
const websockets = new WebsocketsProcessor();
websocketQueue
websocketQueue()
.process(
traceFunction({
serviceName: "websockets",
+8 -8
View File
@@ -18,7 +18,7 @@ export default async function init() {
await initI18n();
// This queue processes the global event bus
globalEventQueue
globalEventQueue()
.process(
env.WORKER_CONCURRENCY_EVENTS,
traceFunction({
@@ -52,12 +52,12 @@ export default async function init() {
if (name === "WebsocketsProcessor") {
// websockets are a special case on their own queue because they must
// only be consumed by the websockets service rather than workers.
await websocketQueue.add(job.data);
await websocketQueue().add(job.data);
} else if (
ProcessorClass.applicableEvents.includes(event.name) ||
ProcessorClass.applicableEvents.includes("*")
) {
await processorEventQueue.add({ event, name });
await processorEventQueue().add({ event, name });
}
} catch (error) {
Logger.error(
@@ -80,7 +80,7 @@ export default async function init() {
// Jobs for individual processors are processed here. Only applicable events
// as unapplicable events were filtered in the global event queue above.
processorEventQueue
processorEventQueue()
.process(
env.WORKER_CONCURRENCY_EVENTS,
traceFunction({
@@ -131,7 +131,7 @@ export default async function init() {
});
// Jobs for async tasks are processed here.
taskQueue
taskQueue()
.process(
env.WORKER_CONCURRENCY_TASKS,
traceFunction({
@@ -173,7 +173,7 @@ export default async function init() {
Logger.fatal("Error starting taskQueue", err);
});
HealthMonitor.start(globalEventQueue);
HealthMonitor.start(processorEventQueue);
HealthMonitor.start(taskQueue);
HealthMonitor.start(globalEventQueue());
HealthMonitor.start(processorEventQueue());
HealthMonitor.start(taskQueue());
}
+1
View File
@@ -48,6 +48,7 @@ class TestServer {
close() {
this.listener = null;
this.server.closeAllConnections();
this.server.close();
}
delete(path: string, options?: any) {
-5
View File
@@ -7,11 +7,6 @@ import { EventEmitter } from "events";
// This needs to be done before any modules that use EventEmitter are loaded
EventEmitter.defaultMaxListeners = 100;
// Enable mocks for Redis-related modules
jest.mock("ioredis", () => require("ioredis-mock"));
jest.mock("@server/utils/MutexLock");
jest.mock("@server/utils/CacheHelper");
// Enable fetch mocks for testing
require("jest-fetch-mock").enableMocks();
fetchMock.dontMock();
+12
View File
@@ -0,0 +1,12 @@
// This file runs before the test environment is set up to ensure mocks are registered early
// It prevents real Redis clients from being initialized during module imports
// Mock ioredis with ioredis-mock before any imports
jest.mock("ioredis", () => require("ioredis-mock"));
// Mock other Redis-dependent modules
jest.mock("@server/utils/MutexLock");
jest.mock("@server/utils/CacheHelper");
// Mock AWS SDK signature module to prevent aws_logger open handle
jest.mock("@aws-sdk/signature-v4-crt", () => ({}));