Files
SnapOtter/tests/unit/api/queues-mutation.test.ts
T
SnapOtterandGitHub 301e6eb01a test: coverage campaign and mutation testing across five packages (#628)
Coverage 83.6 to 87.36% lines, 81.63 to 84.14% branches. Mutation testing across five packages: image-engine 85, media-engine 92, doc-engine 87, shared+enterprise 86, apps/api security and jobs slice. Runs all five lanes weekly. Fixes the silently-broken mutation CI (babel pin), a redact-pdf envelope-shape test bug, an untested enterprise license valid-signature path, and an audit test that only exercised a hand-copied reproduction. Test and config only, no product code changes beyond the babel pin and one test-only oidc export. Full suite: 16,712 pass, 0 fail.
2026-07-24 17:36:57 +08:00

405 lines
15 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// Mutation-focused coverage for apps/api/src/jobs/queues.ts. The sibling
// jobs/queues.behavior.test.ts already exercises the happy paths; this file
// pins the exact literal values a mutant would flip: the per-pool retry
// options object, the getJobCounts/getJobs argument lists, the ?? 0
// count-aggregation fallbacks at their boundary, and the oldest-waiting-age
// subtraction. Every assertion targets a specific value so the corresponding
// mutant dies rather than merely being covered.
interface FakeQueue {
name: string;
options: Record<string, unknown>;
close: ReturnType<typeof vi.fn>;
getJobCounts: ReturnType<typeof vi.fn>;
getJobs: ReturnType<typeof vi.fn>;
}
const queueInstances: FakeQueue[] = [];
const queueCtor = vi.hoisted(() => vi.fn());
async function loadQueuesModule() {
vi.resetModules();
queueInstances.length = 0;
queueCtor.mockReset();
queueCtor.mockImplementation((name: string, options: Record<string, unknown>) => {
const queue: FakeQueue = {
name,
options,
close: vi.fn().mockResolvedValue(undefined),
getJobCounts: vi.fn().mockResolvedValue({ active: 0, waiting: 0, delayed: 0, failed: 0 }),
getJobs: vi.fn().mockResolvedValue([]),
};
queueInstances.push(queue);
return queue;
});
vi.doMock("bullmq", () => ({ Queue: queueCtor }));
vi.doMock("../../../apps/api/src/jobs/connection.js", () => ({
createBullMQConnection: vi.fn(() => ({ marker: "conn" })),
}));
return import("../../../apps/api/src/jobs/queues.js");
}
describe("queues.ts mutation coverage", () => {
beforeEach(() => {
vi.useRealTimers();
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
describe("getQueue construction", () => {
it("gives the ai pool exactly 1 attempt and every other pool exactly 2", async () => {
const { getQueue } = await loadQueuesModule();
const attemptsFor = (pool: "image" | "media" | "ai" | "docs" | "system") => {
getQueue(pool);
const created = queueInstances[queueInstances.length - 1];
const opts = created.options.defaultJobOptions as { attempts: number };
return opts.attempts;
};
expect(attemptsFor("ai")).toBe(1);
expect(attemptsFor("image")).toBe(2);
expect(attemptsFor("media")).toBe(2);
expect(attemptsFor("docs")).toBe(2);
expect(attemptsFor("system")).toBe(2);
});
it("constructs each queue with the exact defaultJobOptions object", async () => {
const { getQueue } = await loadQueuesModule();
getQueue("image");
expect(queueCtor).toHaveBeenCalledTimes(1);
const [, options] = queueCtor.mock.calls[0];
expect(options).toEqual({
connection: { marker: "conn" },
defaultJobOptions: {
attempts: 2,
backoff: { type: "exponential", delay: 1000 },
removeOnComplete: { age: 3600, count: 5000 },
removeOnFail: { age: 24 * 3600, count: 5000 },
},
});
});
it("uses the queueName helper output as the BullMQ queue name", async () => {
const { getQueue } = await loadQueuesModule();
const { queueName } = await import("../../../apps/api/src/jobs/types.js");
getQueue("docs");
expect(queueInstances[0].name).toBe(queueName("docs"));
});
it("passes a freshly created connection to the Queue constructor", async () => {
const { getQueue } = await loadQueuesModule();
getQueue("media");
const [, options] = queueCtor.mock.calls[0];
expect((options as { connection: unknown }).connection).toEqual({ marker: "conn" });
});
it("removeOnFail age is 24 hours in seconds, distinct from the 1 hour complete age", async () => {
const { getQueue } = await loadQueuesModule();
getQueue("system");
const opts = queueInstances[0].options.defaultJobOptions as {
removeOnComplete: { age: number; count: number };
removeOnFail: { age: number; count: number };
};
// 24 * 3600 === 86400; a mutant dropping the *24 (or flipping the *) would
// leave these equal to the complete age (3600).
expect(opts.removeOnFail.age).toBe(86_400);
expect(opts.removeOnComplete.age).toBe(3_600);
expect(opts.removeOnFail.age).not.toBe(opts.removeOnComplete.age);
expect(opts.removeOnComplete.count).toBe(5_000);
expect(opts.removeOnFail.count).toBe(5_000);
});
it("uses an exponential backoff with a 1000ms delay", async () => {
const { getQueue } = await loadQueuesModule();
getQueue("image");
const opts = queueInstances[0].options.defaultJobOptions as {
backoff: { type: string; delay: number };
};
expect(opts.backoff.type).toBe("exponential");
expect(opts.backoff.delay).toBe(1000);
});
it("caches the queue so a second getQueue call does not reconstruct it", async () => {
const { getQueue } = await loadQueuesModule();
const first = getQueue("image");
const second = getQueue("image");
expect(second).toBe(first);
expect(queueCtor).toHaveBeenCalledTimes(1);
});
});
describe("queueCounts aggregation", () => {
it("sums active, waiting, and delayed across every created queue", async () => {
const { getQueue, queueCounts } = await loadQueuesModule();
getQueue("image");
getQueue("media");
getQueue("ai");
queueInstances[0].getJobCounts.mockResolvedValueOnce({
active: 1,
waiting: 10,
delayed: 100,
});
queueInstances[1].getJobCounts.mockResolvedValueOnce({
active: 2,
waiting: 20,
delayed: 200,
});
queueInstances[2].getJobCounts.mockResolvedValueOnce({
active: 4,
waiting: 40,
delayed: 400,
});
// Distinct powers keep each field's sum unique so a swapped accumulator
// (active += counts.waiting, etc.) produces a different total.
await expect(queueCounts()).resolves.toEqual({ active: 7, waiting: 70, delayed: 700 });
});
it("requests the active, waiting, and delayed states by name", async () => {
const { getQueue, queueCounts } = await loadQueuesModule();
getQueue("image");
await queueCounts();
expect(queueInstances[0].getJobCounts).toHaveBeenCalledWith("active", "waiting", "delayed");
});
it("falls back to 0 for each missing count key at the aggregation boundary", async () => {
const { getQueue, queueCounts } = await loadQueuesModule();
getQueue("image");
getQueue("media");
// First queue supplies only active; second only waiting. delayed is
// absent from both, so all three ?? 0 branches must fire.
queueInstances[0].getJobCounts.mockResolvedValueOnce({ active: 3 });
queueInstances[1].getJobCounts.mockResolvedValueOnce({ waiting: 9 });
await expect(queueCounts()).resolves.toEqual({ active: 3, waiting: 9, delayed: 0 });
});
it("skips pools whose queue was never created", async () => {
const { getQueue, queueCounts } = await loadQueuesModule();
// Only one of five pools is instantiated.
getQueue("system");
queueInstances[0].getJobCounts.mockResolvedValueOnce({ active: 5, waiting: 6, delayed: 7 });
await expect(queueCounts()).resolves.toEqual({ active: 5, waiting: 6, delayed: 7 });
// The four skipped pools never call getJobCounts.
expect(queueInstances).toHaveLength(1);
});
it("returns all zeros when no queues have been created", async () => {
const { queueCounts } = await loadQueuesModule();
await expect(queueCounts()).resolves.toEqual({ active: 0, waiting: 0, delayed: 0 });
});
});
describe("perPoolCounts", () => {
it("reports every pool, defaulting uncreated pools to zero", async () => {
const { getQueue, perPoolCounts } = await loadQueuesModule();
getQueue("image");
queueInstances[0].getJobCounts.mockResolvedValueOnce({ active: 11, waiting: 22 });
await expect(perPoolCounts()).resolves.toEqual({
image: { active: 11, waiting: 22 },
media: { active: 0, waiting: 0 },
ai: { active: 0, waiting: 0 },
docs: { active: 0, waiting: 0 },
system: { active: 0, waiting: 0 },
});
});
it("requests only the active and waiting states", async () => {
const { getQueue, perPoolCounts } = await loadQueuesModule();
getQueue("image");
await perPoolCounts();
expect(queueInstances[0].getJobCounts).toHaveBeenCalledWith("active", "waiting");
});
it("defaults each missing count to 0 independently", async () => {
const { getQueue, perPoolCounts } = await loadQueuesModule();
getQueue("docs");
// waiting present, active omitted: both sides of the ?? 0 pair are exercised.
queueInstances[0].getJobCounts.mockResolvedValueOnce({ waiting: 8 });
const result = await perPoolCounts();
expect(result.docs).toEqual({ active: 0, waiting: 8 });
});
});
describe("perPoolHealth", () => {
it("computes oldestWaitingMs as now minus the oldest job timestamp", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-06-29T12:00:00.000Z"));
const { getQueue, perPoolHealth } = await loadQueuesModule();
getQueue("media");
queueInstances[0].getJobCounts.mockResolvedValueOnce({ active: 3, waiting: 2, failed: 1 });
queueInstances[0].getJobs.mockResolvedValueOnce([
{ timestamp: new Date("2026-06-29T11:59:30.000Z").getTime() },
]);
const result = await perPoolHealth();
// 30s between the job timestamp and the frozen clock.
expect(result.media).toEqual({
active: 3,
waiting: 2,
failed: 1,
oldestWaitingMs: 30_000,
});
});
it("requests active, waiting, and failed states from getJobCounts", async () => {
const { getQueue, perPoolHealth } = await loadQueuesModule();
getQueue("image");
await perPoolHealth();
expect(queueInstances[0].getJobCounts).toHaveBeenCalledWith("active", "waiting", "failed");
});
it("fetches exactly the single oldest waiting job with the correct paging args", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-06-29T12:00:00.000Z"));
const { getQueue, perPoolHealth } = await loadQueuesModule();
getQueue("ai");
queueInstances[0].getJobCounts.mockResolvedValueOnce({ active: 0, waiting: 1, failed: 0 });
queueInstances[0].getJobs.mockResolvedValueOnce([
{ timestamp: new Date("2026-06-29T11:59:59.000Z").getTime() },
]);
await perPoolHealth();
// start=0, end=0 (only the first row), asc=true (oldest first).
expect(queueInstances[0].getJobs).toHaveBeenCalledWith(["waiting"], 0, 0, true);
});
it("does not fetch jobs when the waiting count is zero", async () => {
const { getQueue, perPoolHealth } = await loadQueuesModule();
getQueue("docs");
// waiting is exactly 0: the (counts.waiting ?? 0) > 0 guard must be false.
queueInstances[0].getJobCounts.mockResolvedValueOnce({ active: 4, waiting: 0, failed: 2 });
const result = await perPoolHealth();
expect(queueInstances[0].getJobs).not.toHaveBeenCalled();
expect(result.docs).toEqual({ active: 4, waiting: 0, failed: 2, oldestWaitingMs: null });
});
it("fetches jobs when the waiting count is exactly 1 (strictly greater than 0)", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-06-29T12:00:00.000Z"));
const { getQueue, perPoolHealth } = await loadQueuesModule();
getQueue("media");
// Straddles the > 0 boundary: 1 must trigger the getJobs branch.
queueInstances[0].getJobCounts.mockResolvedValueOnce({ active: 0, waiting: 1, failed: 0 });
queueInstances[0].getJobs.mockResolvedValueOnce([
{ timestamp: new Date("2026-06-29T11:59:58.000Z").getTime() },
]);
const result = await perPoolHealth();
expect(queueInstances[0].getJobs).toHaveBeenCalledTimes(1);
expect(result.media.oldestWaitingMs).toBe(2_000);
});
it("leaves oldestWaitingMs null when getJobs returns an empty page", async () => {
const { getQueue, perPoolHealth } = await loadQueuesModule();
getQueue("image");
queueInstances[0].getJobCounts.mockResolvedValueOnce({ active: 0, waiting: 1, failed: 0 });
queueInstances[0].getJobs.mockResolvedValueOnce([]);
const result = await perPoolHealth();
expect(result.image.oldestWaitingMs).toBeNull();
});
it("leaves oldestWaitingMs null when the first returned job slot is undefined", async () => {
const { getQueue, perPoolHealth } = await loadQueuesModule();
getQueue("image");
queueInstances[0].getJobCounts.mockResolvedValueOnce({ active: 0, waiting: 1, failed: 0 });
queueInstances[0].getJobs.mockResolvedValueOnce([undefined]);
const result = await perPoolHealth();
expect(result.image.oldestWaitingMs).toBeNull();
});
it("defaults active, waiting, and failed to 0 for uncreated pools", async () => {
const { getQueue, perPoolHealth } = await loadQueuesModule();
getQueue("image");
queueInstances[0].getJobCounts.mockResolvedValueOnce({ active: 0, waiting: 0, failed: 0 });
const result = await perPoolHealth();
expect(result.system).toEqual({
active: 0,
waiting: 0,
failed: 0,
oldestWaitingMs: null,
});
});
it("defaults each missing count field to 0 while still resolving a waiting age", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-06-29T12:00:00.000Z"));
const { getQueue, perPoolHealth } = await loadQueuesModule();
getQueue("ai");
// active and failed omitted; waiting present so the age still resolves.
queueInstances[0].getJobCounts.mockResolvedValueOnce({ waiting: 1 });
queueInstances[0].getJobs.mockResolvedValueOnce([
{ timestamp: new Date("2026-06-29T11:59:52.000Z").getTime() },
]);
const result = await perPoolHealth();
expect(result.ai).toEqual({
active: 0,
waiting: 1,
failed: 0,
oldestWaitingMs: 8_000,
});
});
});
describe("closeQueues", () => {
it("closes every cached queue and clears the cache", async () => {
const { getQueue, closeQueues, queueCounts } = await loadQueuesModule();
getQueue("image");
getQueue("ai");
await closeQueues();
expect(queueInstances[0].close).toHaveBeenCalledTimes(1);
expect(queueInstances[1].close).toHaveBeenCalledTimes(1);
// After clearing, aggregation touches no queue and returns all zeros.
await expect(queueCounts()).resolves.toEqual({ active: 0, waiting: 0, delayed: 0 });
});
it("resolves without error when there are no queues to close", async () => {
const { closeQueues } = await loadQueuesModule();
await expect(closeQueues()).resolves.toBeUndefined();
});
});
});