feat(test): run tests against a dedicated truncated database
Bootstrap a per-run test database (ensure, push schema, truncate, seed baselines, repair sequences), wire vitest/playwright globals, and gate schema push behind DB_PUSH. Serial test files share the one database. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
18bb6712cd
commit
8135c9850b
12 changed files with 353 additions and 8 deletions
|
|
@ -66,4 +66,9 @@ APP_PORT=3000
|
||||||
# Shared secret required by the /api/arma/v1/* routes. The Arma server sends
|
# Shared secret required by the /api/arma/v1/* routes. The Arma server sends
|
||||||
# it as the `x-arma-bridge-key` header alongside `x-arma-server-id`. Generate
|
# it as the `x-arma-bridge-key` header alongside `x-arma-server-id`. Generate
|
||||||
# with: openssl rand -hex 32. Leave unset to disable the bridge (routes 503).
|
# with: openssl rand -hex 32. Leave unset to disable the bridge (routes 503).
|
||||||
# ARMA_BRIDGE_API_KEY=
|
# ARMA_BRIDGE_API_KEY=
|
||||||
|
# Optional: exact URI for the test database (defaults to <DATABASE_URI database>_test)
|
||||||
|
# TEST_DATABASE_URI=postgres://ptfapp:polaristaskforcedb@127.0.0.1:5432/ptf-app-dev-test
|
||||||
|
|
||||||
|
# Optional: set to 1 to drop and recreate the test database from scratch instead of truncating
|
||||||
|
# TEST_DB_HARD_RESET=1
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,15 @@ import { defineConfig, devices } from "@playwright/test";
|
||||||
*/
|
*/
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
|
|
||||||
|
import { resolveTestDatabaseUri } from "./tests/test-db";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* E2E runs against the dedicated test database, never the development one.
|
||||||
|
* The webServer (dev server) inherits DATABASE_URI pointed at the test DB;
|
||||||
|
* global setup guarantees it exists, is migrated and is empty.
|
||||||
|
*/
|
||||||
|
const testDatabaseUri = resolveTestDatabaseUri();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See https://playwright.dev/docs/test-configuration.
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
*/
|
*/
|
||||||
|
|
@ -53,5 +62,10 @@ export default defineConfig({
|
||||||
command: "pnpm dev",
|
command: "pnpm dev",
|
||||||
reuseExistingServer: true,
|
reuseExistingServer: true,
|
||||||
url: "http://localhost:3000",
|
url: "http://localhost:3000",
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
DATABASE_URI: testDatabaseUri,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
globalSetup: "./tests/e2e/global-setup.ts",
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -414,10 +414,11 @@ export default buildConfig({
|
||||||
connectionString: process.env.DATABASE_URI || "",
|
connectionString: process.env.DATABASE_URI || "",
|
||||||
},
|
},
|
||||||
// push: process.env.NODE_ENV === "development",
|
// push: process.env.NODE_ENV === "development",
|
||||||
push: false,
|
// Env-gated drizzle push: only the test-database bootstrap (DB_PUSH=1) uses
|
||||||
|
// it, because the migration chain assumes a schema that push created.
|
||||||
|
push: process.env.DB_PUSH === "1",
|
||||||
prodMigrations: migrations,
|
prodMigrations: migrations,
|
||||||
}),
|
}), sharp,
|
||||||
sharp,
|
|
||||||
bin: [
|
bin: [
|
||||||
{
|
{
|
||||||
scriptPath: path.resolve(dirname, "scripts/gameTick.ts"),
|
scriptPath: path.resolve(dirname, "scripts/gameTick.ts"),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { getPayload } from "payload";
|
import { getPayload } from "payload";
|
||||||
import config from "@payload-config";
|
import config from "@payload-config";
|
||||||
import type { Permission } from "@/permissions";
|
import { ALL_PERMISSION_VALUES, type Permission } from "@/permissions";
|
||||||
|
|
||||||
const resolvedConfig = await config;
|
const resolvedConfig = await config;
|
||||||
const COLLECTION_SLUGS = resolvedConfig.collections.map((c) => c.slug);
|
const COLLECTION_SLUGS = resolvedConfig.collections.map((c) => c.slug);
|
||||||
|
|
@ -13,6 +13,16 @@ const ALL_ADMIN_PAGE_MANAGE: Permission[] = COLLECTION_SLUGS.map(
|
||||||
(slug) => `admin:${slug}:manage` as Permission,
|
(slug) => `admin:${slug}:manage` as Permission,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry grants only: the Roles field's select options come from the
|
||||||
|
* permission registry, so collection reads generated for plugin-injected
|
||||||
|
* collections (story-beats, voice-submissions, ...) are not grantable and
|
||||||
|
* would fail validation.
|
||||||
|
*/
|
||||||
|
const GRANTABLE = new Set<string>(ALL_PERMISSION_VALUES);
|
||||||
|
const grantable = (permissions: Permission[]): Permission[] =>
|
||||||
|
permissions.filter((p) => GRANTABLE.has(p));
|
||||||
|
|
||||||
const USER_PERMISSIONS: Permission[] = [
|
const USER_PERMISSIONS: Permission[] = [
|
||||||
"users:read",
|
"users:read",
|
||||||
"ranks:read",
|
"ranks:read",
|
||||||
|
|
@ -168,8 +178,8 @@ export const seedRoles = async () => {
|
||||||
roleIdMap.set(role.slug, doc.id);
|
roleIdMap.set(role.slug, doc.id);
|
||||||
|
|
||||||
if (role.isSystem) {
|
if (role.isSystem) {
|
||||||
const existingPerms = doc.permissions ?? [];
|
const existingPerms = (doc.permissions ?? []).filter((p) => GRANTABLE.has(p));
|
||||||
const missing = role.permissions.filter((p) => !existingPerms.includes(p));
|
const missing = grantable(role.permissions).filter((p) => !existingPerms.includes(p));
|
||||||
if (missing.length > 0) {
|
if (missing.length > 0) {
|
||||||
await payload.update({
|
await payload.update({
|
||||||
collection: "roles",
|
collection: "roles",
|
||||||
|
|
@ -199,7 +209,7 @@ export const seedRoles = async () => {
|
||||||
name: role.name,
|
name: role.name,
|
||||||
slug: role.slug,
|
slug: role.slug,
|
||||||
description: role.description,
|
description: role.description,
|
||||||
permissions: role.permissions,
|
permissions: grantable(role.permissions),
|
||||||
isSystem: role.isSystem,
|
isSystem: role.isSystem,
|
||||||
isSuperuser: role.isSuperuser,
|
isSuperuser: role.isSuperuser,
|
||||||
},
|
},
|
||||||
|
|
@ -266,3 +276,7 @@ export const seedRoles = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
await seedRoles();
|
await seedRoles();
|
||||||
|
|
||||||
|
// CLI script: the Payload pool keeps the event loop alive after completion;
|
||||||
|
// exit explicitly (same convention as the game-tick bins).
|
||||||
|
process.exit(0);
|
||||||
|
|
|
||||||
10
tests/e2e/global-setup.ts
Normal file
10
tests/e2e/global-setup.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { bootstrapTestDatabase } from "../test-db";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playwright global setup: ensure the dedicated test database exists, is
|
||||||
|
* migrated, seeded and emptied before the dev server (started by webServer
|
||||||
|
* with DATABASE_URI pointed at it) serves the e2e suite.
|
||||||
|
*/
|
||||||
|
export default async function globalSetup(): Promise<void> {
|
||||||
|
await bootstrapTestDatabase();
|
||||||
|
}
|
||||||
12
tests/push-schema.ts
Normal file
12
tests/push-schema.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { getPayload } from "payload";
|
||||||
|
import config from "@payload-config";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One-shot schema push for the test database. Run with DB_PUSH=1 and
|
||||||
|
* DATABASE_URI pointing at the test database (tests/test-db.ts does this);
|
||||||
|
* initializing the Payload adapter with push: true makes drizzle create or
|
||||||
|
* diff the full schema. Exits immediately after initialization.
|
||||||
|
*/
|
||||||
|
const payload = await getPayload({ config });
|
||||||
|
void payload;
|
||||||
|
process.exit(0);
|
||||||
56
tests/seed-baseline.ts
Normal file
56
tests/seed-baseline.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { getPayload } from "payload";
|
||||||
|
import config from "@payload-config";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baseline data every test run can assume: a Game Rules global with a main
|
||||||
|
* currency resource. Runs after truncation, so it recreates the baseline on
|
||||||
|
* the empty database each run (idempotent by construction). Banking and
|
||||||
|
* economy code paths throw without a main currency.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const CURRENCY_CODE_NAME = "res_test_credits";
|
||||||
|
|
||||||
|
const payload = await getPayload({ config });
|
||||||
|
|
||||||
|
const existingCurrency = await payload.find({
|
||||||
|
collection: "resources",
|
||||||
|
where: { codeName: { equals: CURRENCY_CODE_NAME } },
|
||||||
|
limit: 1,
|
||||||
|
overrideAccess: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let currencyId = existingCurrency.docs[0]?.id as number | undefined;
|
||||||
|
if (!currencyId) {
|
||||||
|
const created = await payload.create({
|
||||||
|
collection: "resources",
|
||||||
|
data: {
|
||||||
|
name: "Test Credits",
|
||||||
|
codeName: CURRENCY_CODE_NAME,
|
||||||
|
unitOfMeasure: "dollar",
|
||||||
|
approvalStatus: "in_progress",
|
||||||
|
type: "abstract",
|
||||||
|
baseValue: 1,
|
||||||
|
rarity: "common",
|
||||||
|
massPerUnit: 0,
|
||||||
|
gridWidth: 1,
|
||||||
|
gridHeight: 1,
|
||||||
|
tradeable: false,
|
||||||
|
},
|
||||||
|
draft: false,
|
||||||
|
overrideAccess: true,
|
||||||
|
});
|
||||||
|
currencyId = created.id as number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rules = await payload.findGlobal({ slug: "game-rules" });
|
||||||
|
await payload.updateGlobal({
|
||||||
|
slug: "game-rules",
|
||||||
|
data: {
|
||||||
|
...(rules as unknown as Record<string, unknown>),
|
||||||
|
mainCurrency: currencyId,
|
||||||
|
},
|
||||||
|
overrideAccess: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
payload.logger.info(`[test-db] baseline ready: main currency resource ${currencyId}`);
|
||||||
|
process.exit(0);
|
||||||
197
tests/test-db.ts
Normal file
197
tests/test-db.ts
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import { Client } from "pg";
|
||||||
|
|
||||||
|
// Global setup/teardown processes never load .env (only setupFiles do); load
|
||||||
|
// it here so every entry point sees the same variables. dotenv does not
|
||||||
|
// override variables that are already set.
|
||||||
|
import "dotenv/config";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dedicated test database machinery for the vitest and Playwright suites.
|
||||||
|
*
|
||||||
|
* Tests never touch the development database: they run against a sibling
|
||||||
|
* `<name>_test` database that is ensured, migrated, seeded and truncated per
|
||||||
|
* run. All operations are idempotent, so a crashed run's leftovers are wiped
|
||||||
|
* by the next run no matter what leaked.
|
||||||
|
*
|
||||||
|
* Override points:
|
||||||
|
* - `TEST_DATABASE_URI` — use this exact connection string instead of the
|
||||||
|
* derived one.
|
||||||
|
* - `TEST_DB_HARD_RESET=1` — drop and recreate the test database (fresh
|
||||||
|
* schema from migrations) instead of the default truncate flow.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Tables that must survive truncation: PostGIS internals and migration journals. */
|
||||||
|
const SKIP_TRUNCATE = new Set(["spatial_ref_sys", "__drizzle_migrations", "payload_migrations"]);
|
||||||
|
|
||||||
|
/** True when truncation should be skipped in favor of a full drop/recreate. */
|
||||||
|
export function hardResetRequested(): boolean {
|
||||||
|
return process.env.TEST_DB_HARD_RESET === "1";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive the test connection string: `TEST_DATABASE_URI` when set, otherwise
|
||||||
|
* the dev URI with its database name suffixed `_test`
|
||||||
|
* (`postgres://host/ptf_app_dev` -> `postgres://host/ptf_app_test`).
|
||||||
|
*/
|
||||||
|
export function resolveTestDatabaseUri(): string {
|
||||||
|
const override = process.env.TEST_DATABASE_URI;
|
||||||
|
if (override) return override;
|
||||||
|
|
||||||
|
const base = process.env.DATABASE_URI;
|
||||||
|
if (!base) {
|
||||||
|
throw new Error("DATABASE_URI (or TEST_DATABASE_URI) must be set to bootstrap the test database");
|
||||||
|
}
|
||||||
|
const url = new URL(base);
|
||||||
|
const segments = url.pathname.split("/").filter(Boolean);
|
||||||
|
const dbName = segments.pop();
|
||||||
|
if (!dbName) {
|
||||||
|
throw new Error("DATABASE_URI has no database name; cannot derive a test database");
|
||||||
|
}
|
||||||
|
url.pathname = [...segments, `${dbName}_test`].join("/");
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function testDatabaseName(testUri: string): string {
|
||||||
|
const name = new URL(testUri).pathname.split("/").filter(Boolean).pop();
|
||||||
|
if (!name) throw new Error("Test database URI has no database name");
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function quotedIdentifier(identifier: string): string {
|
||||||
|
return `"${identifier.replace(/"/g, '""')}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Open a client to the server's maintenance database (`postgres`). */
|
||||||
|
async function withAdminClient(testUri: string, fn: (client: Client) => Promise<void>): Promise<void> {
|
||||||
|
const url = new URL(testUri);
|
||||||
|
url.pathname = "/postgres";
|
||||||
|
const client = new Client({ connectionString: url.toString() });
|
||||||
|
await client.connect();
|
||||||
|
try {
|
||||||
|
await fn(client);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the test database when missing. With `hardReset`, terminate
|
||||||
|
* connections, drop it and recreate from scratch instead.
|
||||||
|
*/
|
||||||
|
export async function ensureTestDatabase(testUri: string, hardReset: boolean): Promise<void> {
|
||||||
|
const dbName = testDatabaseName(testUri);
|
||||||
|
await withAdminClient(testUri, async (admin) => {
|
||||||
|
const existing = await admin.query("SELECT 1 FROM pg_database WHERE datname = $1", [dbName]);
|
||||||
|
if (hardReset && existing.rows.length > 0) {
|
||||||
|
await admin.query(
|
||||||
|
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()",
|
||||||
|
[dbName],
|
||||||
|
);
|
||||||
|
await admin.query(`DROP DATABASE ${quotedIdentifier(dbName)}`);
|
||||||
|
existing.rows.length = 0;
|
||||||
|
}
|
||||||
|
if (existing.rows.length === 0) {
|
||||||
|
await admin.query(`CREATE DATABASE ${quotedIdentifier(dbName)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or diff the full schema via drizzle push. The migration chain
|
||||||
|
* assumes a schema that push originally created (its first migration alters
|
||||||
|
* existing tables), so a fresh database is built by pushing, not migrating.
|
||||||
|
*/
|
||||||
|
export function pushSchema(testUri: string): void {
|
||||||
|
execSync("bun run tests/push-schema.ts", {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: { ...process.env, DATABASE_URI: testUri, DB_PUSH: "1" },
|
||||||
|
stdio: "inherit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncate every table in the public schema except migration journals and
|
||||||
|
* PostGIS internals. Idempotent by construction: the result is the same empty
|
||||||
|
* database no matter how many rows leaked from earlier runs.
|
||||||
|
*/
|
||||||
|
export async function truncateAllData(testUri: string): Promise<void> {
|
||||||
|
const client = new Client({ connectionString: testUri });
|
||||||
|
await client.connect();
|
||||||
|
try {
|
||||||
|
const tables = await client.query(
|
||||||
|
"SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename",
|
||||||
|
);
|
||||||
|
const names = tables.rows
|
||||||
|
.map((row: { tablename: string }) => row.tablename)
|
||||||
|
.filter((name) => !SKIP_TRUNCATE.has(name));
|
||||||
|
if (names.length === 0) return;
|
||||||
|
await client.query(`TRUNCATE TABLE ${names.map(quotedIdentifier).join(", ")} CASCADE`);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-align every `<table>_id_seq` sequence with `MAX(id)` of its table.
|
||||||
|
* Migrations backfill baseline rows with explicit ids, which leaves fresh
|
||||||
|
* sequences behind the existing rows; without this repair the next create
|
||||||
|
* collides and fails with `ValidationError: field is invalid: id`.
|
||||||
|
*/
|
||||||
|
export async function repairSequences(testUri: string): Promise<void> {
|
||||||
|
const client = new Client({ connectionString: testUri });
|
||||||
|
await client.connect();
|
||||||
|
try {
|
||||||
|
const sequences = await client.query(
|
||||||
|
"SELECT sequencename FROM pg_sequences WHERE schemaname = 'public' AND sequencename LIKE '%\\_id\\_seq'",
|
||||||
|
);
|
||||||
|
for (const row of sequences.rows) {
|
||||||
|
const tableName = String(row.sequencename).replace(/_id_seq$/, "");
|
||||||
|
const tableExists = await client.query(
|
||||||
|
"SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1",
|
||||||
|
[tableName],
|
||||||
|
);
|
||||||
|
if (tableExists.rows.length === 0) continue;
|
||||||
|
await client.query(
|
||||||
|
`SELECT setval($1::regclass, COALESCE((SELECT MAX(id) FROM ${quotedIdentifier(tableName)}), 1), true)`,
|
||||||
|
[row.sequencename],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Seed the shared baseline data tests rely on (RBAC roles, Game Rules). */
|
||||||
|
export function runSeedScripts(testUri: string): void {
|
||||||
|
// Self-executing scripts; run them as child processes so the bootstrap
|
||||||
|
// module graph stays free of @payload-config (which vitest globalSetup and
|
||||||
|
// Playwright's transpiler resolve differently).
|
||||||
|
for (const script of ["src/tools/seed/seedRoles.ts", "tests/seed-baseline.ts"]) {
|
||||||
|
execSync(`bun run ${script}`, {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: { ...process.env, DATABASE_URI: testUri },
|
||||||
|
stdio: "inherit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full bootstrap, safe to call repeatedly: ensure the database (drop/recreate
|
||||||
|
* on hard reset), apply migrations, truncate leftover data, reseed baselines
|
||||||
|
* and repair sequences.
|
||||||
|
*/
|
||||||
|
export async function bootstrapTestDatabase(): Promise<string> {
|
||||||
|
const testUri = resolveTestDatabaseUri();
|
||||||
|
const hardReset = hardResetRequested();
|
||||||
|
|
||||||
|
await ensureTestDatabase(testUri, hardReset);
|
||||||
|
pushSchema(testUri);
|
||||||
|
await truncateAllData(testUri);
|
||||||
|
runSeedScripts(testUri);
|
||||||
|
await repairSequences(testUri);
|
||||||
|
|
||||||
|
const dbName = testDatabaseName(testUri);
|
||||||
|
console.log(`[test-db] ready: ${dbName}${hardReset ? " (hard reset)" : ""}`);
|
||||||
|
return testUri;
|
||||||
|
}
|
||||||
10
tests/vitest-global.ts
Normal file
10
tests/vitest-global.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { bootstrapTestDatabase } from "./test-db";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vitest global setup: point the suite at the dedicated test database and
|
||||||
|
* leave it in a known state (ensured, migrated, truncated, seeded, sequences
|
||||||
|
* repaired) before any test file loads.
|
||||||
|
*/
|
||||||
|
export default async function globalSetup(): Promise<void> {
|
||||||
|
await bootstrapTestDatabase();
|
||||||
|
}
|
||||||
14
tests/vitest-teardown.ts
Normal file
14
tests/vitest-teardown.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { resolveTestDatabaseUri, truncateAllData } from "./test-db";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vitest global teardown: leave the test database empty after the run. The
|
||||||
|
* next run truncates again anyway, so this is hygiene, not correctness:
|
||||||
|
* failures here must never fail the suite.
|
||||||
|
*/
|
||||||
|
export default async function globalTeardown(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await truncateAllData(resolveTestDatabaseUri());
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[test-db] teardown truncate failed:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,5 +8,10 @@ export default defineConfig({
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
setupFiles: ["./vitest.setup.ts"],
|
setupFiles: ["./vitest.setup.ts"],
|
||||||
include: ["tests/int/**/*.int.spec.ts"],
|
include: ["tests/int/**/*.int.spec.ts"],
|
||||||
|
// Serial test files: every spec shares one test database, and parallel
|
||||||
|
// workers against it produced flaky cross-file interference.
|
||||||
|
fileParallelism: false,
|
||||||
|
globalSetup: ["./tests/vitest-global.ts"],
|
||||||
|
globalTeardown: ["./tests/vitest-teardown.ts"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,10 @@
|
||||||
|
|
||||||
// Load .env files
|
// Load .env files
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
|
|
||||||
|
import { resolveTestDatabaseUri } from "./tests/test-db";
|
||||||
|
|
||||||
|
// Tests never touch the development database: every payload connection in the
|
||||||
|
// test process targets the dedicated test database (see tests/test-db.ts).
|
||||||
|
// Must run before any test file imports the Payload config.
|
||||||
|
process.env.DATABASE_URI = resolveTestDatabaseUri();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue