Files
TREK/server/tests/unit/db/database-lazy-open.test.ts
T
Maurice 0a4c6fbace refactor(db): open the database through a DatabaseLifecycle provider
db/database.ts opened the connection as a side effect of being imported,
so the moment anything reached it the database was open. index.ts had to
defer loading the app until a first-boot restore was done, and nothing
in the container decided when the connection opened or closed.

The module now opens the connection on first use and opens nothing at
import. A connection closed on purpose (a restore, the shutdown) stays
closed until it is reopened, as before. The new DatabaseLifecycle
provider owns the decisions: buildApp() opens through it, which also
binds the ORM to later swaps (the hook attachOrm used to register) and
runs the schema bootstrap in the same order as before, and the shutdown
closes through it. Its file property is the one resolved database path.

The module keeps every export, so the test suites that replace it with
vi.mock keep working; buildDbMock gains the new openDb.
2026-10-08 18:38:28 +02:00

81 lines
3.0 KiB
TypeScript

/**
* db/database.ts opens the connection on first use, not at import.
*
* It used to call initDb() at module load, so importing anything that reached
* it opened the database: index.ts had to defer loading the app until a
* first-boot restore had put the backup's file in place. The connection is now
* opened by its first reader (the ORM's first connect inside buildApp(), driven
* by DatabaseLifecycle), and a connection closed on purpose for a restore stays
* closed until it is reopened.
*
* The cases share the module's one connection and run in order.
*/
import { describe, expect, it, vi } from 'vitest';
const opened = vi.hoisted(() => ({ count: 0 }));
vi.mock('../../../src/db/connection', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../../src/db/connection')>();
return {
...actual,
openDatabase: (...args: Parameters<typeof actual.openDatabase>) => {
opened.count++;
return actual.openDatabase(...args);
},
};
});
import { closeDb, db, getRawConnection, openDb, registerReinitializeHook, reinitialize } from '../../../src/db/database';
describe('db/database.ts connection lifecycle', () => {
it('DBOPEN-001: importing the module opens nothing', () => {
expect(opened.count).toBe(0);
});
it('DBOPEN-002: the first reader opens it once, with the pragmas applied', () => {
const handle = getRawConnection();
expect(getRawConnection()).toBe(handle);
expect(opened.count).toBe(1);
expect(handle.pragma('foreign_keys', { simple: true })).toBe(1);
expect(handle.pragma('busy_timeout', { simple: true })).toBe(5000);
// The Proxy reads through to the same handle.
expect(db.open).toBe(true);
});
it('DBOPEN-003: a connection closed on purpose stays closed instead of quietly reopening', () => {
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined);
try {
closeDb();
} finally {
log.mockRestore();
}
expect(() => getRawConnection()).toThrow('Database connection is not available (restore in progress?)');
expect(() => db.prepare('SELECT 1')).toThrow('Database connection is not available (restore in progress?)');
expect(opened.count).toBe(1);
});
it('DBOPEN-004: openDb() reopens it, and opening an open connection is a no-op', () => {
openDb();
openDb();
expect(opened.count).toBe(2);
expect(getRawConnection().open).toBe(true);
});
it('DBOPEN-005: reinitialize() swaps in a fresh handle and then runs the registered hook', async () => {
const before = getRawConnection();
const seen: boolean[] = [];
registerReinitializeHook(async () => {
seen.push(getRawConnection() !== before && getRawConnection().open);
});
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined);
try {
await reinitialize();
} finally {
log.mockRestore();
}
expect(before.open).toBe(false);
expect(seen).toEqual([true]);
expect(opened.count).toBe(3);
});
});