Skip to content

Commit aebdcaf

Browse files
committed
test(driver-sql): the black-hole connect-bound test advances a faked clock over its real socket
The one slow test in sql-driver-connect-bound.test.ts waited out the driver's real 10s pg connectionTimeoutMillis against a loopback listener that accepts and never answers (10.01s of the file's 10.15s of test time, 3 runs alone). The listener stays real; only setTimeout/clearTimeout are faked, and the clock is moved in 500ms steps until the query settles, so the pg 10s bound, the tarn 15s backstop and an unbounded connect each still show in how far the clock had to move. Same message and elapsed-band assertions as before. Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9d4fb3c commit aebdcaf

1 file changed

Lines changed: 50 additions & 10 deletions

File tree

‎packages/drivers/driver-sql/src/sql-driver-connect-bound.test.ts‎

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,26 @@
66
// wait is 30s per query on the request path. SqlDriver now bounds it by default,
77
// while leaving a host's own choice alone.
88

9-
import { describe, it, expect, afterEach } from 'vitest';
9+
import { describe, it, expect, afterEach, vi } from 'vitest';
1010
import net from 'node:net';
1111
import { SqlDriver } from './sql-driver.js';
1212

13-
/** A listener that accepts sockets and then says nothing, ever. */
13+
/**
14+
* A listener that accepts sockets and then says nothing, ever. `accepted`
15+
* settles once the first socket is held — the moment the connection attempt
16+
* is a TCP connection that will never see a handshake.
17+
*/
1418
function blackHole() {
1519
const held: net.Socket[] = [];
16-
const server = net.createServer((sock) => { sock.on('error', () => {}); held.push(sock); });
20+
let firstAccepted!: () => void;
21+
const accepted = new Promise<void>((resolve) => { firstAccepted = resolve; });
22+
const server = net.createServer((sock) => {
23+
sock.on('error', () => {});
24+
held.push(sock);
25+
firstAccepted();
26+
});
1727
return {
28+
accepted,
1829
listen: () => new Promise<number>((resolve) => {
1930
server.listen(0, '127.0.0.1', () => resolve((server.address() as net.AddressInfo).port));
2031
}),
@@ -199,32 +210,61 @@ describe('SqlDriver — connection-attempt bound (framework#3769)', () => {
199210
it('actually bounds a query against a black-holing endpoint', async () => {
200211
const bh = blackHole();
201212
const port = await bh.listen();
213+
// The endpoint is real: a loopback listener that holds the accepted socket
214+
// and never answers, so only the driver's own bound can end the attempt.
215+
// The CLOCK is faked (setTimeout/clearTimeout only — `net` I/O and the
216+
// microtask queue stay real), because the default bound is a 10s wall-clock
217+
// timer and waiting it out was the whole 10s this file used to cost. Fake
218+
// time still proves the bound: pg arms `connectionTimeoutMillis` and tarn
219+
// arms `createTimeoutMillis` on the faked `setTimeout`, so whichever
220+
// fires — or none, for an unbounded connect — shows in how far the fake
221+
// clock has to move before the query settles.
222+
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
202223
try {
203224
// The default is 10s; unbounded would be knex/tarn's 30s.
204225
const d = make({
205226
client: 'pg',
206227
connection: `postgres://u:p@127.0.0.1:${port}/d`,
207228
pool: { min: 0, max: 2 },
208229
});
209-
const started = Date.now();
210-
const err = await (d as any).knex.raw('SELECT 1').then(
230+
let err: Error | undefined;
231+
const outcome = (d as any).knex.raw('SELECT 1').then(
211232
() => { throw new Error('query resolved but should have failed'); },
212-
(e: Error) => e,
233+
(e: Error) => { err = e; },
213234
);
214-
const elapsed = Date.now() - started;
235+
236+
// Real I/O: do not move the clock until the server holds the socket, so
237+
// the attempt being timed is a connection that was accepted and then
238+
// went silent — not one that never started.
239+
await bh.accepted;
240+
241+
// Move the fake clock in steps until the query settles, never past 20s.
242+
// Each step also lets a real macrotask run, so the socket teardown and
243+
// the rejection it causes land before the next step is judged.
244+
let elapsed = 0;
245+
while (err === undefined && elapsed < 20_000) {
246+
await vi.advanceTimersByTimeAsync(500);
247+
await new Promise<void>((resolve) => setImmediate(resolve));
248+
elapsed += 500;
249+
}
250+
expect(err, 'still pending after 20s of fake time — the connect is unbounded').toBeDefined();
251+
await outcome; // surfaces the "resolved but should have failed" throw, if any
215252

216253
// The pg-level timeout fires first (10s), so the message names the
217254
// connection attempt rather than blaming pool sizing. Guarding against
218255
// the regression is the point: knex's own wording would send an operator
219256
// to tune `pool.max` while the network is what is broken.
220-
expect(err.message).toContain('timeout expired');
221-
expect(err.message).not.toContain('pool is probably full');
257+
expect(err!.message).toContain('timeout expired');
258+
expect(err!.message).not.toContain('pool is probably full');
222259
expect(elapsed).toBeGreaterThan(8_000);
223260
expect(elapsed).toBeLessThan(20_000);
224261
} finally {
262+
// Real timers back BEFORE afterEach disconnects the driver and the
263+
// listener closes: teardown must not depend on a clock nobody advances.
264+
vi.useRealTimers();
225265
bh.close();
226266
}
227-
}, 40_000);
267+
}, 15_000);
228268

229269
it('does not disturb a working sqlite connection', async () => {
230270
const d = make({ client: 'better-sqlite3', connection: { filename: ':memory:' }, useNullAsDefault: true });

0 commit comments

Comments
 (0)