Files
SnapOtter/tests/setup/per-fork-env.ts
SnapOtterandGitHub bc32f86a07 test: correct the SYNC_WAIT_MS comment and guard six 202 branches (#656)
The comment added in #652 was wrong. It said SYNC_WAIT_MS=0 drives every tool through its 202 path; it does the opposite. BullMQ arms its timer under `if (ttl)`, so 0 is falsy, waitUntilFinished waits forever and every route answers 200, matching the repo-wide convention that 0 means unlimited. A small positive value such as 1 is what forces the async path.

That error mattered: the local validation claimed for #652 exercised the synchronous path throughout and never reached settleAsyncFallback. Redone with SYNC_WAIT_MS=1, it surfaced six specs whose status gate accepts 202 but whose else branch then demands an error body a 202 never carries, a latent flake on a slow runner.

Each now settles the job first. Under a forced 1ms window the 'expected undefined to be defined' failures drop from 55 to 0, and all 1183 tests still pass on the normal 30s window. Per-shard totals unchanged at 9903 tests, 9435 passed, 468 skipped.

Specs asserting a bare 200 without listing 202 are deliberately untouched: they own the synchronous contract.
2026-07-27 14:20:29 +08:00

65 lines
2.8 KiB
TypeScript

import crypto from "node:crypto";
import os from "node:os";
import path from "node:path";
import pg from "pg";
// Each test file (forks pool, isolated) gets its own Postgres database cloned
// from the migrated template built in tests/global-setup.ts, plus its own
// workspace dir. setupFiles run before any app module loads, so
// apps/api/src/config.ts captures the per-file DATABASE_URL.
const suffix = `${process.pid}_${crypto.randomUUID().slice(0, 8).replace(/-/g, "")}`;
const forkDir = path.join(os.tmpdir(), `SnapOtter-test-${suffix}`);
process.env.WORKSPACE_PATH = path.join(forkDir, "workspace");
const baseUrl = process.env.TEST_PG_BASE_URL;
if (!baseUrl) {
throw new Error("TEST_PG_BASE_URL missing; tests/global-setup.ts did not run");
}
const redisBaseUrl = process.env.TEST_REDIS_BASE_URL;
if (!redisBaseUrl) {
throw new Error("TEST_REDIS_BASE_URL missing; tests/global-setup.ts did not run");
}
process.env.REDIS_URL = redisBaseUrl;
process.env.BULLMQ_PREFIX = `snapotter_test_${suffix}`;
// Heavy format conversions can exceed the 8s production default under parallel
// test forks; 30s keeps tool routes synchronous (200) in tests while production
// stays at 8s.
//
// An explicit SYNC_WAIT_MS is honored verbatim rather than floored, so the
// constrained docker test image (macOS Docker VM, where Sharp and FFmpeg run
// ~2-3x slower) can widen the window.
//
// Careful with 0: per the repo-wide convention it means unlimited, not
// instant. BullMQ's waitUntilFinished only arms its timer under `if (ttl)`, so
// 0 waits forever and every route answers 200. To force the 202 path (the only
// way to exercise it on a machine fast enough never to hit it naturally), pass
// a small positive value such as SYNC_WAIT_MS=1. Note that most specs assert a
// bare 200 and will fail under it; only the ones that call
// settleAsyncFallback are written to survive.
const requestedSyncWait = process.env.SYNC_WAIT_MS?.trim();
const hasExplicitSyncWait =
Boolean(requestedSyncWait) && Number.isFinite(Number(requestedSyncWait));
process.env.SYNC_WAIT_MS = hasExplicitSyncWait ? (requestedSyncWait as string) : "30000";
const dbName = `snapotter_test_${suffix}`; // pid digits + uuid hex: identifier-safe
const admin = new pg.Client({ connectionString: baseUrl });
await admin.connect();
// Concurrent CREATE DATABASE ... TEMPLATE from parallel forks can transiently
// conflict; retry briefly.
let created = false;
for (let attempt = 0; attempt < 5 && !created; attempt++) {
try {
await admin.query(`CREATE DATABASE ${dbName} TEMPLATE snapotter_template`);
created = true;
} catch (err) {
if (attempt === 4) throw err;
await new Promise((r) => setTimeout(r, 150 + Math.floor(150 * attempt)));
}
}
await admin.end();
const forkUrl = new URL(baseUrl);
forkUrl.pathname = `/${dbName}`;
process.env.DATABASE_URL = forkUrl.toString();