fix(deploy): one shared DB-engine resolver so scripts can't silently target a throwaway SQLite file (FIX A)
apply-onboarding-pipeline.ts and migrate-onboarding-templates.ts resolved the engine as `DATABASE_URL ?? ''`, diverging from server.ts's production fallback. On the prod box (DATABASE_URL unset) each script opened a brand-new empty SQLite file, "succeeded" against it, and Postgres was never touched. resolveDbUrl()/requireDbUrlForEnv()/pgTargetDescription() now live in db-pg.ts as the one place this is decided; server.ts and both scripts call it. The scripts additionally hard-fail (never fall back to SQLite) when NODE_ENV=production resolves nothing, log their target engine before doing any work, and close the DB handle in a finally. Also lands client_module.created_at (nullable, additive) — schema groundwork FIX D uses to measure a no-tick project's age from its real creation date instead of treating a missing done_on as infinitely old. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>main
parent
0bcbf8d410
commit
31335cd4ad
@ -0,0 +1,59 @@
|
||||
// apps/hq/test/db-resolve.test.ts — pre-ship audit FIX A: server.ts and the deploy scripts
|
||||
// (apps/hq/scripts/apply-onboarding-pipeline.ts, apps/hq/scripts/migrate-onboarding-templates.ts)
|
||||
// must all resolve the DB engine + target through the ONE shared function. Before this fix
|
||||
// the scripts computed it as `DATABASE_URL ?? ''` (no production fallback), so on the
|
||||
// production box — where DATABASE_URL is unset — each script silently opened a brand-new
|
||||
// empty SQLite file instead of the real Postgres.
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { pgTargetDescription, requireDbUrlForEnv, resolveDbUrl } from '../src/db-pg'
|
||||
|
||||
const HARDCODED = 'postgres://postgres:inv123@host.docker.internal:5432/hq'
|
||||
|
||||
describe('resolveDbUrl (D19 engine selection, the one shared resolver)', () => {
|
||||
it('DATABASE_URL wins regardless of NODE_ENV', () => {
|
||||
expect(resolveDbUrl({ DATABASE_URL: 'postgres://x/y', NODE_ENV: 'production' })).toBe('postgres://x/y')
|
||||
expect(resolveDbUrl({ DATABASE_URL: 'postgres://x/y' })).toBe('postgres://x/y')
|
||||
})
|
||||
|
||||
it('falls back to the hardcoded production URL only when NODE_ENV=production and DATABASE_URL is unset', () => {
|
||||
expect(resolveDbUrl({ NODE_ENV: 'production' }, HARDCODED)).toBe(HARDCODED)
|
||||
})
|
||||
|
||||
it('resolves to SQLite (empty string) outside production with no DATABASE_URL — dev/tests stay green', () => {
|
||||
expect(resolveDbUrl({})).toBe('')
|
||||
expect(resolveDbUrl({ NODE_ENV: 'development' })).toBe('')
|
||||
expect(resolveDbUrl({ NODE_ENV: 'test' })).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('requireDbUrlForEnv (deploy-script guard — hard-fail instead of a throwaway SQLite write)', () => {
|
||||
it('behaves exactly like resolveDbUrl when a Postgres target is available', () => {
|
||||
expect(requireDbUrlForEnv({ DATABASE_URL: 'postgres://x/y' })).toBe('postgres://x/y')
|
||||
expect(requireDbUrlForEnv({ NODE_ENV: 'production' }, HARDCODED)).toBe(HARDCODED)
|
||||
expect(requireDbUrlForEnv({})).toBe('') // dev with no env: SQLite is fine, no hard-fail
|
||||
})
|
||||
|
||||
it('hard-fails instead of silently returning SQLite when NODE_ENV=production resolves nothing', () => {
|
||||
// Simulates the hardcoded URL having been accidentally emptied out.
|
||||
expect(() => requireDbUrlForEnv({ NODE_ENV: 'production' }, '')).toThrow(/refusing to run/i)
|
||||
})
|
||||
})
|
||||
|
||||
describe('pgTargetDescription (never leaks the password into logs)', () => {
|
||||
it('extracts host:port/dbname', () => {
|
||||
expect(pgTargetDescription('postgres://user:secret@db.example.com:5432/hq'))
|
||||
.toBe('db.example.com:5432/hq')
|
||||
})
|
||||
|
||||
it('omits the port when absent', () => {
|
||||
expect(pgTargetDescription('postgres://user:secret@db.example.com/hq')).toBe('db.example.com/hq')
|
||||
})
|
||||
|
||||
it('never includes the credentials', () => {
|
||||
expect(pgTargetDescription('postgres://user:hunter2@host/db')).not.toContain('hunter2')
|
||||
})
|
||||
|
||||
it('degrades gracefully on an unparsable string', () => {
|
||||
expect(pgTargetDescription('not-a-url')).toMatch(/unparsable/i)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue