Files
synAdminandClaude Opus 5 2bc9440b38 test(ci): record which Postgres actually answered (#154)
This adds the measurement #154 has needed twice and never had. It does not fix the failure and does not guess at it: the issue has been wrong twice from reasoning ahead of evidence, and the point here is to make the next occurrence answer the question rather than reopen it.

The observation that rules out every explanation so far is that the schema comes back. A suite fails because orders does not exist, and a later suite truncating that same table passes. A dropped database does not un-drop itself, so this was never one database losing its schema. More than one server answering to one name produces exactly this, and Docker embedded DNS round-robins every container sharing an alias, so a leftover service container from an earlier run fits every observation including the empty dmesg that killed the OOM theory.

globalSetup now logs every address the database host resolves to. More than one is the answer outright. One address means this reading is wrong too, and the next suspect is a single container restarted with a fresh data directory.

Alongside it, both globalSetup and a failing assertSchemaPresent record which server actually answered. pg_postmaster_start_time is what settles that and needs no special rights: two Postgres instances cannot share one, so differing values within a single run are proof, where a differing inet_server_addr alone could be argued to be one container that moved. The failure message now says to compare the two rather than leaving the reader to know that is the interesting comparison.

Logged on a passing run as well as a failing one, deliberately. A failing run's addresses mean nothing without a passing run's to compare them against, and this issue has twice suffered from having only the failure to look at.

Neither can throw. A diagnostic that fails the run it was added to explain is worse than no diagnostic, so both are wrapped and both degrade to a printed reason.

The failure path costs one extra round trip, taken only when the schema is already known to be missing. assertSchemaPresent is not on the hot path — resetDb calls it only when its TRUNCATE has already failed.

Verified: tsc clean, typecheck:tests clean, lint 0 errors with no new warnings, and the four message patterns schemaLoss.integration.test.ts asserts on are all still present. The probe reads only pg_catalog functions, so it still answers against the dropped schema that suite creates.

Refs #154

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 10:49:03 -05:00

82 lines
3.3 KiB
TypeScript
Executable File

import { Client } from 'pg';
async function waitForDb(retries = 20): Promise<void> {
const config = {
host: process.env.TEST_PGHOST || 'localhost',
port: parseInt(process.env.TEST_PGPORT || '55432', 10),
user: process.env.TEST_PGUSER || 'redefined_test',
password: process.env.TEST_PGPASSWORD || 'redefined_test',
database: process.env.TEST_PGDATABASE || 'redefined_test'
};
for (let i = 0; i < retries; i++) {
const client = new Client(config);
try {
await client.connect();
await client.end();
return;
} catch {
await new Promise(r => setTimeout(r, 1000));
}
}
throw new Error(
'Could not reach the test database on port 55432. Run `npm run db:test:up` before `npm run test:integration`.'
);
}
/**
* Every address the database host resolves to, and which server answered (#154).
*
* This is the measurement that issue has needed twice and never had. The
* schema in a failing run comes back — a suite fails on a missing `orders`,
* and a later suite truncating that same table passes — which a database
* losing its schema cannot do. More than one server answering to one name can,
* and Docker's embedded DNS round-robins every container sharing an alias, so
* a leftover service container from an earlier run fits every observation
* including the empty `dmesg`.
*
* **More than one address printed here is the answer outright.** One address,
* and this reading is wrong too — the next suspect is a single container being
* restarted with a fresh data directory, which the postmaster start time
* recorded alongside will show.
*
* Logged unconditionally rather than only on failure: a passing run's addresses
* are the control, and without them a failing run's have nothing to be compared
* against. Never throws — a diagnostic that can fail the run it was added to
* explain is worse than none.
*/
async function reportDatabaseIdentity(): Promise<void> {
const host = process.env.TEST_PGHOST || 'localhost';
try {
const { lookup } = await import('dns/promises');
const addresses = await lookup(host, { all: true });
const rendered = addresses.map((a) => a.address).join(', ');
console.info(
`[#154] "${host}" resolves to ${addresses.length} address(es): ${rendered}` +
(addresses.length > 1 ? ' <-- more than one server can answer; this is the bug' : '')
);
} catch (err) {
console.info(`[#154] could not resolve "${host}": ${err instanceof Error ? err.message : String(err)}`);
}
}
export default async function globalSetup(): Promise<void> {
await waitForDb();
const { migrate, closeDb, assertSchemaPresent, describeBackend } = await import('./testDb');
await reportDatabaseIdentity();
// Recorded before migrating so the run's baseline is in the log even if the
// migration is the thing that fails. A suite that later reports a different
// postmaster start time is talking to a different instance.
console.info(`[#154] ${await describeBackend('globalSetup reached')}`);
await migrate();
// Cheap, once, and it establishes the fact the rest of the run depends on:
// the schema was here when we started. Without it, a run that never had a
// schema and a run that lost one midway are indistinguishable from the
// failures they produce. See #154.
await assertSchemaPresent('immediately after migrating');
await closeDb();
}