@ -35,10 +35,10 @@ describe('requireDbUrlForEnv (deploy-script guard — OUTCOME-based, not NODE_EN
expect ( requireDbUrlForEnv ( { NODE_ENV : 'production' } , HARDCODED ) ) . toBe ( HARDCODED )
} )
it ( 'resolves to SQLite without hard-failing when the target file already exists (mocked fs check)', ( ) = > {
expect ( requireDbUrlForEnv ( { } , undefined , ( ) = > true ) ) . toBe ( '' )
it ( 'resolves to SQLite without hard-failing when it is opted into and the target file exists (mocked fs check)', ( ) = > {
expect ( requireDbUrlForEnv ( { HQ_DB_TARGET : 'sqlite' } , undefined , ( ) = > true ) ) . toBe ( '' )
// Even NODE_ENV=production is fine as long as the SQLite file it would land on already exists.
expect ( requireDbUrlForEnv ( { NODE_ENV : 'production' } , '' , ( ) = > true ) ) . toBe ( '' )
expect ( requireDbUrlForEnv ( { NODE_ENV : 'production' , HQ_DB_TARGET : 'sqlite' } , '' , ( ) = > true ) ) . toBe ( '' )
} )
it ( ':memory: never hard-fails — the guard never even calls the file-existence check' , ( ) = > {
@ -52,10 +52,52 @@ describe('requireDbUrlForEnv (deploy-script guard — OUTCOME-based, not NODE_EN
it ( 'refuses when resolution selects SQLite and the target does not exist — regardless of NODE_ENV' , ( ) = > {
// This is the original prod bug: an operator shell has no NODE_ENV set at all, so the old
// NODE_ENV==='production' gate never fired. The guard must fire on the OUTCOME instead.
expect ( ( ) = > requireDbUrlForEnv ( { } , undefined , ( ) = > false ) ) . toThrow ( /refusing to run/i )
expect ( ( ) = > requireDbUrlForEnv ( { } , undefined , ( ) = > false ) ) . toThrow ( /CREATE a new SQLite database/ )
const optIn = { HQ_DB_TARGET : 'sqlite' }
expect ( ( ) = > requireDbUrlForEnv ( optIn , undefined , ( ) = > false ) ) . toThrow ( /refusing to run/i )
expect ( ( ) = > requireDbUrlForEnv ( optIn , undefined , ( ) = > false ) ) . toThrow ( /CREATE a new SQLite database/ )
// Also still refuses in the NODE_ENV=production case (hardcoded emptied out + no existing file).
expect ( ( ) = > requireDbUrlForEnv ( { NODE_ENV : 'production' } , '' , ( ) = > false ) ) . toThrow ( /refusing to run/i )
expect ( ( ) = > requireDbUrlForEnv ( { NODE_ENV : 'production' , HQ_DB_TARGET : 'sqlite' } , '' , ( ) = > false ) )
. toThrow ( /refusing to run/i )
} )
} )
describe ( 'requireDbUrlForEnv — SQLite must be an explicit opt-in, never an accident' , ( ) = > {
// The bug this closes: apply-onboarding-pipeline.ts documented its own invocation WITHOUT a
// `DATABASE_URL=` prefix. An operator who copy-pastes that on the prod box resolves to SQLite;
// if a stale ./data/hq.db happens to exist under their cwd, the file-existence guard passes and
// the migration "succeeds" against the wrong database. Landing on SQLite must be a decision.
const existingFile = ( ) : boolean = > true
it ( 'refuses when nothing selects a target, even though the SQLite file exists' , ( ) = > {
expect ( ( ) = > requireDbUrlForEnv ( { } , undefined , existingFile ) ) . toThrow ( /refusing to run/i )
} )
it ( 'names BOTH escape hatches in the error — DATABASE_URL and the SQLite opt-in' , ( ) = > {
expect ( ( ) = > requireDbUrlForEnv ( { } , undefined , existingFile ) ) . toThrow ( /DATABASE_URL=postgres/ )
expect ( ( ) = > requireDbUrlForEnv ( { } , undefined , existingFile ) ) . toThrow ( /HQ_DB_TARGET=sqlite/ )
expect ( ( ) = > requireDbUrlForEnv ( { } , undefined , existingFile ) ) . toThrow ( /--sqlite/ )
} )
it ( 'accepts HQ_DB_TARGET=sqlite (case/space tolerant)' , ( ) = > {
expect ( requireDbUrlForEnv ( { HQ_DB_TARGET : 'sqlite' } , undefined , existingFile ) ) . toBe ( '' )
expect ( requireDbUrlForEnv ( { HQ_DB_TARGET : ' SQLite ' } , undefined , existingFile ) ) . toBe ( '' )
} )
it ( 'accepts a --sqlite argv flag (alongside the script\'s own flags)' , ( ) = > {
expect ( requireDbUrlForEnv ( { } , undefined , existingFile , [ 'node' , 'script.ts' , '--force' , '--sqlite' ] ) ) . toBe ( '' )
} )
it ( 'rejects an unrelated HQ_DB_TARGET value rather than guessing' , ( ) = > {
expect ( ( ) = > requireDbUrlForEnv ( { HQ_DB_TARGET : 'postgres' } , undefined , existingFile ) ) . toThrow ( /refusing to run/i )
} )
it ( 'the opt-in does NOT weaken the "never CREATE a database" guard' , ( ) = > {
expect ( ( ) = > requireDbUrlForEnv ( { HQ_DB_TARGET : 'sqlite' } , undefined , ( ) = > false ) )
. toThrow ( /CREATE a new SQLite database/ )
} )
it ( 'DATABASE_URL needs no opt-in — Postgres is the intended target and never touches the flag' , ( ) = > {
expect ( requireDbUrlForEnv ( { DATABASE_URL : 'postgres://x/y' } , undefined , ( ) = > false , [ ] ) ) . toBe ( 'postgres://x/y' )
} )
} )
@ -63,17 +105,20 @@ describe('requireDbUrlForEnv — real filesystem (the actual script entry path,
it ( 'refuses a non-existent SQLite path' , ( ) = > {
const dir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'hq-db-resolve-' ) )
try {
expect ( ( ) = > requireDbUrlForEnv ( { HQ_DATA_DIR : dir } ) ) . toThrow ( /refusing to run/i )
expect ( ( ) = > requireDbUrlForEnv ( { HQ_DATA_DIR : dir , HQ_DB_TARGET : 'sqlite' } ) )
. toThrow ( /CREATE a new SQLite database/ )
} finally {
fs . rmSync ( dir , { recursive : true , force : true } )
}
} )
it ( 'accepts an existing SQLite path ', ( ) = > {
it ( 'accepts an existing SQLite path once SQLite is opted into ', ( ) = > {
const dir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'hq-db-resolve-' ) )
try {
fs . writeFileSync ( path . join ( dir , 'hq.db' ) , '' )
expect ( requireDbUrlForEnv ( { HQ_DATA_DIR : dir } ) ) . toBe ( '' )
expect ( requireDbUrlForEnv ( { HQ_DATA_DIR : dir , HQ_DB_TARGET : 'sqlite' } ) ) . toBe ( '' )
// …and still refuses the same existing path with no opt-in at all.
expect ( ( ) = > requireDbUrlForEnv ( { HQ_DATA_DIR : dir } ) ) . toThrow ( /HQ_DB_TARGET=sqlite/ )
} finally {
fs . rmSync ( dir , { recursive : true , force : true } )
}