test(integration): run files in parallel, one cloned database per worker

Global setup migrates one template database and reads what a fresh install
holds: its schemas, its tables and the rows the migrations seed. Each Vitest
worker clones the template on its first file with CREATE DATABASE ... TEMPLATE
and keeps the clone for the rest of the run, so files run in parallel without
seeing each other's rows, advisory locks or queue schema. Schemas created at
runtime (pg-boss, migration probes) are dropped when the next file starts.

The truncate list is the catalogue now, every table in public except the
migration bookkeeping, instead of a hand-kept list of 42 of 166 tables. The
seeded catalogues are put back after each truncate in foreign-key order, so a
test starts from what a fresh install has. The settings singleton is the one
seeded table a test starts without, declared with its reason.

next/headers and the boot compatibility probe are mocked once in the setup
file. Two lock-wait probes now look at their own database only, since other
workers' backends share pg_stat_activity.
This commit is contained in:
Marc-André Bombeck committed 2026-10-22 05:30:00 +02:00
1 parent fdd0f50130
commit a6787cfc03
9 files changed
+451 -161

No files matched your search

@@ -12,6 +12,18 @@
import { describe, expect, it, vi } from "vitest";
import { truncateAllTables } from "../../tests/integration/setup";
import type { IntegrationDatabasePlan } from "../../tests/integration/worker-database";
/** What global setup reads off the migrated template, cut down to the shape. */
const PLAN: IntegrationDatabasePlan = {
templateSchemas: ["public"],
tables: ["app_settings", "mood_tag_categories", "mood_tags", "users"],
reference: [
{ table: "app_settings", rowsJson: '[{"id":"singleton"}]' },
{ table: "mood_tag_categories", rowsJson: '[{"id":"c1"}]' },
{ table: "mood_tags", rowsJson: '[{"id":"t1","category_id":"c1"}]' },
],
};
function deadlock(): Error & { code: string } {
const error = new Error("deadlock detected") as Error & { code: string };
@@ -19,45 +31,61 @@ function deadlock(): Error & { code: string } {
return error;
}
function fakeClient(executeRaw: ReturnType<typeof vi.fn>) {
const empty = vi.fn().mockResolvedValue([]);
return {
$executeRawUnsafe: executeRaw,
moodTag: { findMany: empty, createMany: vi.fn() },
moodTagCategory: { findMany: empty, createMany: vi.fn() },
} as unknown as Parameters<typeof truncateAllTables>[0];
type Execute = (query: string, ...values: unknown[]) => Promise<number>;
function fakeClient(executeRaw: Execute) {
return { $executeRawUnsafe: executeRaw };
}
describe("truncateAllTables", () => {
it("retries a deadlocked truncate and succeeds once the lock clears", async () => {
const executeRaw = vi
.fn()
.fn<Execute>()
.mockRejectedValueOnce(deadlock())
.mockRejectedValueOnce(deadlock())
.mockResolvedValue(0);
await truncateAllTables(fakeClient(executeRaw));
await truncateAllTables(fakeClient(executeRaw), PLAN);
expect(executeRaw).toHaveBeenCalledTimes(3);
// Three truncate attempts, then the two seeded tables put back.
expect(executeRaw).toHaveBeenCalledTimes(5);
});
it("empties every catalogued table and puts the seeded rows back in key order", async () => {
const executeRaw = vi.fn<Execute>().mockResolvedValue(0);
await truncateAllTables(fakeClient(executeRaw), PLAN);
const statements = executeRaw.mock.calls.map((call) => String(call[0]));
expect(statements[0]).toBe(
'TRUNCATE TABLE "app_settings", "mood_tag_categories", "mood_tags", "users" RESTART IDENTITY CASCADE;',
);
// The settings singleton starts empty; the catalogues come back, the
// category before the tag that references it.
expect(statements.slice(1)).toEqual([
expect.stringContaining('INSERT INTO "mood_tag_categories"'),
expect.stringContaining('INSERT INTO "mood_tags"'),
]);
expect(executeRaw.mock.calls[2][1]).toBe(PLAN.reference[2].rowsJson);
});
it("gives up on an error that is not a deadlock, on the first attempt", async () => {
const other = new Error("permission denied") as Error & { code: string };
other.code = "42501";
const executeRaw = vi.fn().mockRejectedValue(other);
const executeRaw = vi.fn<Execute>().mockRejectedValue(other);
await expect(truncateAllTables(fakeClient(executeRaw))).rejects.toThrow(
"permission denied",
);
await expect(
truncateAllTables(fakeClient(executeRaw), PLAN),
).rejects.toThrow("permission denied");
expect(executeRaw).toHaveBeenCalledTimes(1);
});
it("stops retrying a deadlock that never clears", async () => {
const executeRaw = vi.fn().mockRejectedValue(deadlock());
const executeRaw = vi.fn<Execute>().mockRejectedValue(deadlock());
await expect(truncateAllTables(fakeClient(executeRaw))).rejects.toThrow(
"deadlock detected",
);
await expect(
truncateAllTables(fakeClient(executeRaw), PLAN),
).rejects.toThrow("deadlock detected");
expect(executeRaw).toHaveBeenCalledTimes(5);
});
});