mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-15 19:32:20 +03:00
* test(infra): retry recursive temp-dir removal on main (main twin of #11968)
`main` has been red since b342c1a361 on the vitest and integration gates:
✖ tests/unit/autoCombo/provider-family-combos.test.ts > auto/<family>
✖ chat pipeline applies Codex OAuth fingerprint and priority tier inside combos
Both call resetStorage() from beforeEach, which does an fs.rmSync(TEST_DATA_DIR,
{recursive: true, force: true}) with no retry, and intermittently loses the race
with a not-yet-released SQLite handle (ENOTEMPTY).
release/v3.8.51 fixed this in #11968 with a mechanical codemod adding
maxRetries/retryDelay to every recursive rm/rmSync/rmdirSync under tests/, but
that PR landed only on the release branch. Because main only receives work at
the release squash, it stayed broken for the whole cycle — and repo-wide gates
then turn every open PR into main red on checks unrelated to their diff.
This is the --base main twin: re-runs the same codemod that already shipped on
the release branch (scripts/ad-hoc/codemod-rm-maxretries.mjs), so the two
branches converge on identical test-teardown semantics. Test-only; no product
logic is touched.
The remaining three failures reported on #12133 (unit full suite exceeding its
4800s ceiling, package-artifact exceeding 1200s, and the boot-smoke that is
skipped as a consequence) are runner-contention timeouts, not code defects —
validate-release-green.mjs runs those heavy gates concurrently on one shared
hosted runner. There is no fix to port for those.
* chore(scripts): carry the rm-maxretries codemod onto main alongside its output
The codemod that generated the previous commit lives in the repo on
release/v3.8.51 (added by #11968) but was never on main. Bringing it over keeps
the tool next to the change it produced, so the transformation stays
reproducible and auditable from either branch.
211 lines
9.8 KiB
TypeScript
211 lines
9.8 KiB
TypeScript
// ENVIRONMENT NOTE (sandbox better-sqlite3 / glibc limitation, not a code defect):
|
|
// This test constructs or exercises a real better-sqlite3-backed SQLite database.
|
|
// better-sqlite3 is a native addon; production and CI load it normally, but some
|
|
// sandboxes/dev boxes ship a system glibc older than the prebuilt binary requires
|
|
// ("GLIBC_2.29 not found"), so the native module fails to dlopen and any test that
|
|
// reaches better-sqlite3 directly (or asserts stdout that the load-failure warning
|
|
// would pollute) fails HERE while passing in CI. This is a known environment
|
|
// limitation, not a defect in the code under test: the OmniRoute runtime itself
|
|
// cascades to node:sqlite/sql.js when better-sqlite3 is unavailable. See
|
|
// tests/unit/_helpers/betterSqlite3Availability.ts for a guard helper.
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
// Regression guard for #7288 / #7494 — a startup step reaching
|
|
// `getDbInstance()` before `preInitSqlJs()` had run threw the misleading
|
|
// "sql.js WASM ainda não foi pré-inicializado" error for an EXISTING DB file
|
|
// when both synchronous drivers (better-sqlite3, node:sqlite) failed.
|
|
//
|
|
// NOTE on approach: an earlier version of this fix added a top-level
|
|
// `await preInitSqlJs(...)` barrier that used to sit at the bottom of
|
|
// `src/lib/db/core.ts` so merely *importing* core.ts guaranteed the
|
|
// pre-init. That made core.ts an async ES module — esbuild's CJS bundling
|
|
// path (used by `tsx`'s CJS require hook, and hit by several other test
|
|
// files that `require("../../src/lib/db/core.ts")` for cleanup, e.g.
|
|
// tests/unit/stmt-cache-lru.test.ts) rejects any `require()` of a module
|
|
// whose dependency graph contains a top-level await ("This require call is
|
|
// not allowed because the transitive dependency ... contains a top-level
|
|
// await"), and even where esbuild didn't hard-fail, sharing that pending
|
|
// top-level-await Promise across node:test's process broke unrelated tests'
|
|
// event-loop bookkeeping ("Promise resolution is still pending but the
|
|
// event loop has already resolved" — reproduced with
|
|
// tests/unit/api/compression/compression-api.test.ts run in the same
|
|
// process as any of the `require(".../core.ts")` cleanup helpers above).
|
|
//
|
|
// The fix instead closes the ordering gap at the real startup entrypoint:
|
|
// `registerNodejs()` (src/instrumentation-node.ts) now calls
|
|
// `ensureDbReadyForBoot()` — which pre-initializes sql.js when needed —
|
|
// BEFORE any other startup step (ensureSecrets(), clearStaleCrashCooldowns(),
|
|
// getSettings(), initAuditLog()) can reach `getDbInstance()`. No top-level
|
|
// await anywhere in core.ts.
|
|
|
|
async function importFreshCore() {
|
|
const url = new URL("../../src/lib/db/core.ts", import.meta.url).href;
|
|
return import(`${url}?test=${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
}
|
|
|
|
let dataDir: string;
|
|
let prevDataDir: string | undefined;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
let coreModule: any;
|
|
|
|
test.after(() => {
|
|
try {
|
|
coreModule?.resetDbInstance?.();
|
|
} catch {
|
|
/* best-effort cleanup */
|
|
}
|
|
if (prevDataDir === undefined) delete process.env.DATA_DIR;
|
|
else process.env.DATA_DIR = prevDataDir;
|
|
if (dataDir) fs.rmSync(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
});
|
|
|
|
test("src/lib/db/core.ts has no top-level await (breaks esbuild's CJS require() bundling — #7288 hotfix)", () => {
|
|
const corePath = fileURLToPath(new URL("../../src/lib/db/core.ts", import.meta.url));
|
|
const source = fs.readFileSync(corePath, "utf8");
|
|
|
|
// A bare `await <expr>;` at column 0 (module top-level scope, not inside
|
|
// any function) is the exact pattern that broke esbuild's CJS bundling for
|
|
// every transitive `require()` of this module (tsx's CJS require hook,
|
|
// used by tests/unit/stmt-cache-lru.test.ts and friends).
|
|
assert.doesNotMatch(
|
|
source,
|
|
/^await\s/m,
|
|
"core.ts must not contain a top-level `await` — it makes the module " +
|
|
"un-require()-able via esbuild's CJS bundling path and breaks other " +
|
|
"tests' event-loop bookkeeping when required in the same process"
|
|
);
|
|
});
|
|
|
|
test(
|
|
"registerNodejs() calls ensureDbReadyForBoot() before any startup step that " +
|
|
"reaches getDbInstance() (ensureSecrets/clearStaleCrashCooldowns/getSettings/" +
|
|
"initAuditLog) — closes the #7288/#7494 ordering gap at the real entrypoint",
|
|
() => {
|
|
const instrumentationPath = fileURLToPath(
|
|
new URL("../../src/instrumentation-node.ts", import.meta.url)
|
|
);
|
|
const source = fs.readFileSync(instrumentationPath, "utf8");
|
|
|
|
const registerStart = source.indexOf("export async function registerNodejs(");
|
|
assert.ok(registerStart >= 0, "registerNodejs() must exist in instrumentation-node.ts");
|
|
|
|
const dbReadyIndex = source.indexOf("await ensureDbReadyForBoot();", registerStart);
|
|
assert.ok(
|
|
dbReadyIndex >= 0,
|
|
"registerNodejs() must call `await ensureDbReadyForBoot();` — it is the only " +
|
|
"caller of preInitSqlJs()"
|
|
);
|
|
|
|
for (const laterDbTouch of [
|
|
"await ensureSecrets();",
|
|
"clearStaleCrashCooldowns()",
|
|
"await getSettings();",
|
|
"initAuditLog();",
|
|
]) {
|
|
const touchIndex = source.indexOf(laterDbTouch, registerStart);
|
|
assert.ok(touchIndex >= 0, `expected to find \`${laterDbTouch}\` in registerNodejs()`);
|
|
assert.ok(
|
|
dbReadyIndex < touchIndex,
|
|
`\`await ensureDbReadyForBoot();\` (index ${dbReadyIndex}) must run before ` +
|
|
`\`${laterDbTouch}\` (index ${touchIndex}) — otherwise that step can reach ` +
|
|
"getDbInstance() before sql.js has had a chance to pre-initialize (#7288 / #7494)"
|
|
);
|
|
}
|
|
}
|
|
);
|
|
|
|
test(
|
|
"getDbInstance() called after the REAL ensureDbReadyForBoot() warm-up (the one " +
|
|
"registerNodejs() now runs ahead of every other startup step) no longer throws " +
|
|
"the ordering-gap 'sql.js WASM ainda não foi pré-inicializado' error when both " +
|
|
"sync drivers fail on an EXISTING db file (#7288 / #7494)",
|
|
async () => {
|
|
dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-7288-"));
|
|
const sqliteFile = path.join(dataDir, "storage.sqlite");
|
|
// A directory in place of the sqlite file makes BOTH better-sqlite3 and
|
|
// node:sqlite fail to open it for real (no mocking needed), while
|
|
// fs.existsSync(sqliteFile) stays true — the same shape of failure a
|
|
// real ABI mismatch would produce for the two sync drivers.
|
|
fs.mkdirSync(sqliteFile);
|
|
|
|
prevDataDir = process.env.DATA_DIR;
|
|
process.env.DATA_DIR = dataDir;
|
|
|
|
coreModule = await importFreshCore();
|
|
|
|
// Exercise the REAL production warm-up, not a stand-in: registerNodejs()
|
|
// awaits ensureDbReadyForBoot() -> ensureDbInitialized() (which itself
|
|
// calls preInitSqlJs() when the sync drivers can't open the file) BEFORE
|
|
// any other startup step (ensureSecrets() / clearStaleCrashCooldowns() /
|
|
// getSettings() / initAuditLog()) reaches getDbInstance().
|
|
const { ensureDbReadyForBoot } = await import("../../src/instrumentation-node");
|
|
try {
|
|
await ensureDbReadyForBoot(coreModule.ensureDbInitialized);
|
|
} catch {
|
|
// A literal directory can never become a valid DB for ANY driver, so the
|
|
// warm-up itself is expected to fail here. What matters is only WHICH
|
|
// error getDbInstance() reports afterwards — see the assertion below.
|
|
}
|
|
|
|
let thrownMessage: string | null = null;
|
|
try {
|
|
coreModule.getDbInstance();
|
|
} catch (err) {
|
|
thrownMessage = err instanceof Error ? err.message : String(err);
|
|
}
|
|
|
|
// Acceptance criterion (#7288): "an existing storage.sqlite still boots
|
|
// via the sql.js fallback (no 'ainda não foi pré-inicializado')". A
|
|
// literal directory can't be opened by ANY driver — including sql.js's
|
|
// own fs.readFileSync — so a residual, *different* I/O error here (e.g.
|
|
// EISDIR) is expected and is not the ordering-gap bug under test: what
|
|
// this test proves is that preInitSqlJs() is actually attempted ahead of
|
|
// getDbInstance() (the fix), not that a synthetic directory becomes a
|
|
// valid database (impossible for any driver).
|
|
assert.ok(
|
|
thrownMessage === null || !/ainda não foi pré-inicializado/.test(thrownMessage),
|
|
"expected the fix to make preInitSqlJs() run ahead of getDbInstance() instead of " +
|
|
"throwing the 'not pre-initialized yet' error when both sync drivers fail on an " +
|
|
`existing DB file — got: ${thrownMessage}`
|
|
);
|
|
}
|
|
);
|
|
|
|
test(
|
|
"the warm-up costs nothing on the happy path: sql.js stays un-initialized when a " +
|
|
"sync driver can already open the file",
|
|
async () => {
|
|
const dir2 = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-7288-happy-"));
|
|
const file2 = path.join(dir2, "storage.sqlite");
|
|
try {
|
|
const { tryOpenSync, getSqlJsAdapter } = await import(
|
|
"../../src/lib/db/adapters/driverFactory"
|
|
);
|
|
const { default: Database } = await import("better-sqlite3");
|
|
const seed = new Database(file2);
|
|
seed.exec("CREATE TABLE t (id INTEGER)");
|
|
seed.close();
|
|
|
|
// The sync-driver probe is what gates the sql.js/WASM fallback: when it
|
|
// succeeds, nothing downstream should ever reach preInitSqlJs().
|
|
const probe = tryOpenSync(file2, { readonly: true });
|
|
assert.ok(probe, "sanity: a sync driver must be able to open a healthy sqlite file here");
|
|
probe!.close();
|
|
|
|
assert.equal(
|
|
getSqlJsAdapter(file2),
|
|
null,
|
|
"sql.js must NOT be pre-initialized when a sync driver can already open the file — " +
|
|
"otherwise every boot would pay the WASM-load cost even on the happy path"
|
|
);
|
|
} finally {
|
|
fs.rmSync(dir2, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
}
|
|
}
|
|
);
|