When the integration run lost its schema partway through, it presented as 36 assertion errors about categories, price filters and favourite notifications. The real message — `relation "items" does not exist` — was further down the same log, and hours went into chasing the assertions instead. The cause is still open and needs runner-side evidence this cannot reach: whether the Postgres service container is being recreated mid-run, which would come back with an empty data directory. This is the half that can be fixed from here — whatever the cause, the next occurrence reads as "the database lost its schema" on the first line, names which tables are gone, and says that nothing in the suite drops tables so the database was replaced underneath the run. globalSetup asserts once after migrating, which establishes the fact the rest of the run depends on. Without it, a run that never had a schema and one that lost it midway are indistinguishable from the failures they produce. resetDb checks only when its TRUNCATE fails, rather than on every reset. It runs in a beforeEach several hundred times a suite, and an extra round trip each time to guard against a rare event would be paying continuously for it. When the schema is fine, an unrelated failure is passed through untouched rather than dressed up as a schema problem. The diagnostic is tested against a database that has actually lost its schema, not reasoned about. An earlier attempt dropped the schema before the run and proved nothing — globalSetup re-migrates, so it repaired itself and every test passed. The suite drops and rebuilds around each case and restores in afterAll; the full integration suite was then run twice to confirm the restore holds for everything ordered after it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
77 lines
2.9 KiB
TypeScript
77 lines
2.9 KiB
TypeScript
import { pool } from '../../src/db';
|
|
import { assertSchemaPresent, migrate, resetDb, testPool, closeDb } from './setup/testDb';
|
|
|
|
/**
|
|
* The diagnostic for #154, exercised against a database that has actually lost
|
|
* its schema.
|
|
*
|
|
* The failure this guards is not hypothetical and not cheap: the integration run
|
|
* lost its schema partway through, and it presented as 36 assertion errors about
|
|
* categories, price filters and favourite notifications. The real message —
|
|
* `relation "items" does not exist` — was further down the same log.
|
|
*
|
|
* This suite destroys and rebuilds the schema, so it restores in afterAll and
|
|
* `migrate()` is what puts it back: dropping the schema takes `pgmigrations`
|
|
* with it, so node-pg-migrate re-runs everything rather than believing it has
|
|
* already done so.
|
|
*/
|
|
async function dropEverything(): Promise<void> {
|
|
await testPool.query('DROP SCHEMA public CASCADE; CREATE SCHEMA public;');
|
|
}
|
|
|
|
afterAll(async () => {
|
|
// Put it back before anything else in the run touches it. Ordering matters:
|
|
// jest runs these in band, so a suite after this one would otherwise find
|
|
// nothing at all.
|
|
await migrate();
|
|
await pool.end();
|
|
await closeDb();
|
|
});
|
|
|
|
describe('when the database loses its schema', () => {
|
|
it('assertSchemaPresent says so, rather than letting the suite guess', async () => {
|
|
await dropEverything();
|
|
|
|
await expect(assertSchemaPresent('in a test')).rejects.toThrow(/has no schema \(in a test\)/);
|
|
|
|
await migrate();
|
|
});
|
|
|
|
it('names the tables that are missing', async () => {
|
|
await dropEverything();
|
|
|
|
// The count and a few names, not the whole list: the point is that a reader
|
|
// can tell at a glance this is a missing schema rather than a logic bug.
|
|
await expect(assertSchemaPresent('in a test')).rejects.toThrow(/Missing 18 of 18 tables/);
|
|
await expect(assertSchemaPresent('in a test')).rejects.toThrow(/items/);
|
|
|
|
await migrate();
|
|
});
|
|
|
|
it('points at the cause rather than leaving it to be rediscovered', async () => {
|
|
await dropEverything();
|
|
|
|
await expect(assertSchemaPresent('in a test')).rejects.toThrow(/TRUNCATE does not/);
|
|
await expect(assertSchemaPresent('in a test')).rejects.toThrow(/#154/);
|
|
|
|
await migrate();
|
|
});
|
|
|
|
// The path a real run takes: resetDb's TRUNCATE is what fails first, and it
|
|
// has to turn that into the schema message rather than passing on a bare
|
|
// "relation does not exist".
|
|
it('resetDb turns a failed truncate into the schema message', async () => {
|
|
await dropEverything();
|
|
|
|
await expect(resetDb()).rejects.toThrow(/has no schema \(while resetting between tests\)/);
|
|
|
|
await migrate();
|
|
});
|
|
|
|
// And it must not swallow unrelated failures behind a schema message.
|
|
it('says nothing about the schema when the schema is fine', async () => {
|
|
await expect(assertSchemaPresent('in a test')).resolves.toBeUndefined();
|
|
await expect(resetDb()).resolves.toBeUndefined();
|
|
});
|
|
});
|