Skip to content
178 changes: 178 additions & 0 deletions packages/drivers/driver-sql/src/live-dialect-matrix.budget.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* [#16434] The live-cell test budget, pinned to the two bounds it was DERIVED
* from rather than to the literal it happens to be.
*
* `LIVE_CELL_TIMEOUT_MS`'s docblock states the derivation; this file makes it
* executable, in the shape #13691 used for `MAX_SPAN_MS` — "the derivation is
* pinned by arithmetic ... so the two halves cannot drift apart in silence".
* Three things could move it and none of them would touch this constant:
*
* - the driver's own connection bounds (`withConnectBound`), which the budget
* must stay ABOVE so a connect fault reports the driver's envelope and not
* vitest's stopwatch;
* - the live job's stall guard, which the budget must stay BELOW so a hung
* live test is NAMED instead of being swallowed as an unattributed stall;
* - vitest's own cascade rules, which are what makes one seam-level suite
* option reach 40 files' live cells while leaving their explicit per-`it`
* budgets alone.
*
* ⚠️ Every bound here is read from the thing it describes — a constructed knex
* config, the workflow file — never re-typed. A pin that copies both sides of
* an equality cannot fail. Each read carries a non-vacuity assertion for the
* same reason: a regex that silently matches nothing is a phantom check.
*
* Runs on every runner: it constructs a driver but never connects, so no live
* server is required and no cell of the matrix is involved.
*/

import { describe, expect, it } from 'vitest';
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { SqlDriver } from './index.js';
import { LIVE_CELL_TIMEOUT_MS } from './live-dialect-matrix.testkit.js';

/** The workspace root, found the way the testkit finds it — by its marker file. */
function repoRoot(): string {
let dir = dirname(fileURLToPath(import.meta.url));
for (;;) {
if (existsSync(join(dir, 'pnpm-workspace.yaml'))) return dir;
const parent = dirname(dir);
if (parent === dir) throw new Error('no pnpm-workspace.yaml above this file');
dir = parent;
}
}

/**
* The connection bounds the driver ACTUALLY installs, read off a constructed
* pg config rather than copied from `SqlDriver`'s private constants.
*
* Constructing a driver opens no socket — knex builds its pool lazily — so this
* is a pure read of the config the driver would connect with.
*/
function installedConnectBounds(): { poolCreateMs: number; dialectConnectMs: number } {
const driver = new SqlDriver({
client: 'pg',
connection: 'postgres://u:p@127.0.0.1:5432/never_connected',
} as any);
const config = (driver as any).knex.client.config;
return {
poolCreateMs: Number(config?.pool?.createTimeoutMillis),
dialectConnectMs: Number(config?.connection?.connectionTimeoutMillis),
};
}

describe('[#16434] the live-cell budget stays inside the corridor it was derived from', () => {
it('sits ABOVE the longest wait the driver is entitled to for one connection', () => {
const { poolCreateMs, dialectConnectMs } = installedConnectBounds();

// Non-vacuity: if the driver stopped installing these, both reads would be
// NaN and every comparison below would be vacuously false-y rather than red.
expect(
Number.isFinite(poolCreateMs),
'the driver installed no `pool.createTimeoutMillis` — this pin read nothing, so it is ' +
'measuring nothing (see `withConnectBound`)',
).toBe(true);
expect(
Number.isFinite(dialectConnectMs),
'the driver installed no per-dialect connect timeout — this pin read nothing (see ' +
'`DIALECT_CONNECT_TIMEOUT`)',
).toBe(true);

// The floor. At or below the pool's create backstop, vitest kills the test
// while the driver is still inside a wait it declares legal, and the
// accurate connect message never prints.
expect(
LIVE_CELL_TIMEOUT_MS,
`a live cell budget of ${LIVE_CELL_TIMEOUT_MS} ms does not clear the ${poolCreateMs} ms ` +
`pool create backstop the driver installs, so a connect fault would be reported as ` +
`"Test timed out" instead of by the driver's own envelope`,
).toBeGreaterThan(poolCreateMs);
expect(LIVE_CELL_TIMEOUT_MS).toBeGreaterThan(dialectConnectMs);

// ⭐ The status quo this card is about, asserted rather than recounted:
// vitest's own default is below even the dialect connect bound.
const VITEST_DEFAULT_TEST_TIMEOUT_MS = 5_000;
expect(
VITEST_DEFAULT_TEST_TIMEOUT_MS,
'vitest’s default no longer sits below the driver’s connect bound — re-derive the ' +
'floor above, because the reason an unbudgeted live cell could never report a connect ' +
'fault has changed',
).toBeLessThan(dialectConnectMs);
});

it('sits BELOW the stall guard the live job wraps this suite in', () => {
const ci = readFileSync(join(repoRoot(), '.github/workflows/ci.yml'), 'utf8');
const guarded = /run-with-stall-guard\.mjs[^\n]*--stall-minutes\s+(\d+)[\s\S]{0,400}?driver-sql/;
const match = guarded.exec(ci);

// Non-vacuity: no match means the workflow moved and this pin is measuring
// nothing — a louder failure than a green over a regex that matches nothing.
expect(
match,
'no `run-with-stall-guard --stall-minutes N` step wrapping the driver-sql suite was found ' +
'in .github/workflows/ci.yml — the ceiling half of this budget’s derivation now reads ' +
'nothing, so re-derive it against wherever that guard moved to',
).not.toBeNull();

const stallWindowMs = Number(match![1]) * 60_000;
expect(stallWindowMs).toBeGreaterThan(0);
expect(
LIVE_CELL_TIMEOUT_MS,
`a live cell budget of ${LIVE_CELL_TIMEOUT_MS} ms is not comfortably under the ` +
`${stallWindowMs} ms stall window: at that size a hung live test is killed as an ` +
`unattributed stall instead of being named by vitest`,
).toBeLessThan(stallWindowMs / 2);
});
});

describe('[#16434] the seam-level suite option behaves the way the seam assumes', () => {
const SUITE_BUDGET = 4_242;
const OWN_BUDGET = 1_337;

describe('a suite option', { timeout: SUITE_BUDGET }, () => {
it('reaches a test declared directly in that suite', (ctx) => {
expect(ctx.task.timeout).toBe(SUITE_BUDGET);
});

describe('and a describe nested inside it — the shape every matrix consumer writes', () => {
it('reaches a test one level deeper too', (ctx) => {
expect(ctx.task.timeout).toBe(SUITE_BUDGET);
});

it(
'but does NOT override a budget the test declared for itself',
(ctx) => {
// The 62 explicit budgets already in this package (60 x 60_000, one
// 40_000, one 120_000) keep the value their own site chose.
expect(ctx.task.timeout).toBe(OWN_BUDGET);
},
OWN_BUDGET,
);
});
});

it('leaves a test OUTSIDE that suite on the runner default — the SQLite cells', (ctx) => {
expect(
ctx.task.timeout,
'a suite option leaked out of its own suite — the whole "live cells only" claim rests on ' +
'it not doing that',
).not.toBe(SUITE_BUDGET);

// ⛔ The fence, executable: this package must keep inheriting the runner
// default outside a live cell. It reds two ways, and both are the point —
// a package-wide `testTimeout` added to `vitest.config.ts` (which is the
// fix #16434 declined), or a vitest upgrade that moves the default out from
// under the FLOOR argument in `LIVE_CELL_TIMEOUT_MS`'s docblock. Either one
// needs a human to re-derive, not a number bumped here.
expect(
ctx.task.timeout,
'a test outside every live cell no longer runs at vitest’s 5000 ms default — either this ' +
'package grew a package-wide `testTimeout` (the fix #16434 declined, because it has no ' +
'cell-level discrimination) or the runner default moved; re-derive LIVE_CELL_TIMEOUT_MS ' +
'rather than editing this number',
).toBe(5_000);
});
});
114 changes: 113 additions & 1 deletion packages/drivers/driver-sql/src/live-dialect-matrix.testkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,95 @@ export function declareUnprovisionedCell(cell: DialectCell, matrix: string): voi
});
}

/**
* [#16434] The per-test budget a LIVE cell runs under — the one the matrix was
* missing, and the reason a merge-queue build dequeued an unrelated PR.
*
* ## Why a live cell needs its own budget at all
*
* This package sets no `testTimeout`, so every cell inherited vitest's default
* 5000 ms — the SQLite cell, which does no I/O, and the live cells, which talk
* to a separate server over a socket. Measured against a live Postgres 16.13
* and a live MySQL 8.0.46 in ONE run, on the cell that actually timed out:
* `sql-driver-11224-update-stamp-precision.test.ts` §2 on live mysql (six
* rounds of create → read → update → a server-side cursor comparison, so 24
* live round-trips in one test body):
*
* ```
* §2 idle loop loop held by a re-scheduling 12 ms hog (8 on 4 CPUs)
* sqlite 21 ms 24 ms
* live postgres 50 ms 50 ms
* live mysql 64 ms 121 ms
* ```
*
* ⚠️ Read what that does NOT license, in three directions.
*
* - It does not derive this number, and cannot. The queue build that dequeued
* PR #16430 spent MORE than 5000 ms in that same live-mysql body — 40x to
* 75x the figures above. A budget written as "measured cost times a margin"
* would have landed in the low hundreds of milliseconds and been wrong by
* two orders of magnitude. What the measurement establishes is the opposite:
* the cost of the WORK is not what sets this bound, so the bound is derived
* from what it has to sit BETWEEN instead.
* - It is not the cost of live cells in general. This file is one of the
* heavier ones; `sql-driver-12998-shadow-null-safe-key.test.ts`'s live cells
* were measured on this same container at 248 ms for the slowest of them.
* ⛔ Nothing here claims any live cell is normally near this ceiling.
* - The numbers above are ONE world. Earlier readings taken on this container
* with only `OS_TEST_POSTGRES_URL` set are not comparable with them: the box
* and the cell population both differ. Whole-row comparisons only.
*
* ## The two bounds it sits between, both read off the code it guards
*
* FLOOR — the driver's own longest LEGAL wait for one connection. `SqlDriver`
* bounds every live connection itself: a per-dialect connect timeout of
* 10_000 ms and a deliberately looser `pool.createTimeoutMillis` backstop of
* 15_000 ms ("The two bounds must not be equal. They race, and knex wins a
* tie", `withConnectBound`). Any live round-trip may have to acquire a pooled
* connection, so 15_000 ms is a wait the driver is ENTITLED to inside a test
* body. A budget at or below it pre-empts the driver's own envelope: vitest
* kills the test with `Test timed out in Nms` while the driver was still inside
* a legal wait, and the accurate message the black-hole test pins (`timeout
* expired` from pg, `connect ETIMEDOUT` from mysql2) never prints.
* ⇒ the budget must be strictly ABOVE 15_000 ms.
* ⭐ Note where that leaves the status quo: 5000 ms is below even the 10_000 ms
* dialect connect bound, so an unbudgeted live cell could never report a
* connect fault at all — vitest always won that race.
*
* CEILING — the stall guard the live job wraps this suite in
* (`run-with-stall-guard.mjs --stall-minutes 10`, ci.yml). A per-test budget at
* or above ten minutes of silence never fires first: the guard kills the
* process group and reports an unattributed stall, losing WHICH test hung.
* ⇒ the budget must be well BELOW 600_000 ms.
*
* ## The point inside that corridor, stated as a choice rather than a measurement
*
* Nothing in the corridor (15_000, 600_000) is distinguishable by measurement,
* so the value is fixed by this package's OWN existing answer for live-touching
* sites: 60 explicit `60_000` budgets across 22 files — #13688 and its sweep
* #13902 put them on live test BODIES, #14213 and #14628 on the hooks that pay
* a live connect. Adopting it leaves the live matrix with ONE live budget
* instead of two, so a red at 60_000 ms is unambiguous about which bound it hit.
*
* ⭐ That convention states its own reasoning, and states its own limit —
* `sql-driver-12998-shadow-null-safe-key.test.ts`, on the four budgets #13902
* gave it: "Sized like this package's siblings — 60_000 is 7 of its 9 explicit
* budgets — and NOT an assertion that these tests are normally anywhere near
* that slow." So the precedent picked the value by convention and said so; what
* it never had is a CORRIDOR the value must lie in. That is what this constant
* adds, and it is the half that is derived. ⛔ It is NOT `driver-mongodb`'s 30_000 carried over
* by analogy — that is that package's number, and this one is this package's.
*
* It clears the derived floor by 4x — arithmetically, room for four
* full-length pool creations inside one test body before the budget could
* pre-empt the driver — and sits an order of magnitude under the derived
* ceiling. `live-dialect-matrix.budget.test.ts` pins both inequalities
* against the bound the driver ACTUALLY installs, read off a constructed
* connection rather than copied here, so the two halves cannot drift apart in
* silence.
*/
export const LIVE_CELL_TIMEOUT_MS = 60_000;

/**
* Run a cell EITHER WAY — measured when it is provisioned, declared un-run when
* it is not — with no third outcome available to the caller.
Expand Down Expand Up @@ -423,7 +512,30 @@ export function declareDialectCell(
declareUnprovisionedCell(cell, matrix);
return;
}
measure(cell);
// [#16434] LIVE cells only — the budget is applied HERE, at the one seam
// every matrix consumer already goes through, rather than at each `it` in the
// 40 files that call this. Same argument the rest of this module makes: a
// guard copy-pasted per suite is a guard that can weaken in one copy and
// nowhere else, and a new live file would arrive without it.
//
// ⛔ Deliberately NOT a package-wide `testTimeout` in `vitest.config.ts`.
// That is the one knob with no cell-level discrimination, so it would raise
// the ceiling for the SQLite cell too — measured at 21 ms idle / 24 ms hogged
// for the same test body the live-mysql cell spends 64-121 ms on — and this
// package's fast in-memory cells are where a 5 s guard is doing real work.
//
// A suite-level `timeout` cascades to the tests the consumer's own describes
// declare, and an explicit per-`it` third argument still wins over it — both
// asserted in `live-dialect-matrix.budget.test.ts`, so the 62 explicit
// budgets already in this package (60 x 60_000, one 40_000, one 120_000)
// keep the value their own site chose.
if (!cell.live) {
measure(cell);
return;
}
describe(`live cell budget (${LIVE_CELL_TIMEOUT_MS} ms)`, { timeout: LIVE_CELL_TIMEOUT_MS }, () => {
measure(cell);
});
}

/** What a server reports about its own timezone. */
Expand Down
Loading