diff --git a/.changeset/bootstrap-harness-override.md b/.changeset/bootstrap-harness-override.md new file mode 100644 index 0000000..e3a48c3 --- /dev/null +++ b/.changeset/bootstrap-harness-override.md @@ -0,0 +1,12 @@ +--- +'@smooai/config': minor +--- + +`bootstrapFetch` now honors a `SMOOAI_HARNESS_` env override (e.g. +`SMOOAI_HARNESS_DATABASE_URL`, `SMOOAI_HARNESS_RLS_DATABASE_URL`) that +short-circuits the HTTP fetch entirely — the same §15 escape hatch +`packages/db` `drizzleClient.resolveDbUrl` already honors at runtime. This makes +the prod-script override work uniformly across ALL cold-start config consumers +(db-migrate and friends), not just the runtime drizzle client. Previously +`bootstrapFetch` ignored the override and fell through to `env='development'` +when no SST stage was set, silently fetching the wrong environment's value. diff --git a/src/bootstrap/__tests__/bootstrap.test.ts b/src/bootstrap/__tests__/bootstrap.test.ts index 1eee4d0..7369081 100644 --- a/src/bootstrap/__tests__/bootstrap.test.ts +++ b/src/bootstrap/__tests__/bootstrap.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { resetBootstrapCacheForTests, bootstrapFetch } from '../index'; +import { resetBootstrapCacheForTests, bootstrapFetch, harnessEnvKey } from '../index'; // Snapshot + restore process.env for full test isolation. const originalEnv = { ...process.env }; @@ -65,6 +65,39 @@ describe('bootstrapFetch', () => { expect(fetchMock).toHaveBeenCalledTimes(2); }); + describe('SMOOAI_HARNESS_ escape hatch', () => { + it('maps camelCase keys to SCREAMING_SNAKE harness env names', () => { + expect(harnessEnvKey('databaseUrl')).toBe('SMOOAI_HARNESS_DATABASE_URL'); + expect(harnessEnvKey('rlsDatabaseUrl')).toBe('SMOOAI_HARNESS_RLS_DATABASE_URL'); + expect(harnessEnvKey('voyageApiKey')).toBe('SMOOAI_HARNESS_VOYAGE_API_KEY'); + }); + + it('short-circuits the HTTP fetch entirely when the override is set', async () => { + const fetchMock = mockFetchResponses([]); // any fetch call → "ran out of queued responses" + process.env.SMOOAI_HARNESS_DATABASE_URL = 'postgres://forced-prod/db'; + + const value = await bootstrapFetch('databaseUrl'); + expect(value).toBe('postgres://forced-prod/db'); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('uses the per-key env name (rlsDatabaseUrl)', async () => { + const fetchMock = mockFetchResponses([]); + process.env.SMOOAI_HARNESS_RLS_DATABASE_URL = 'postgres://forced-rls/db'; + + expect(await bootstrapFetch('rlsDatabaseUrl')).toBe('postgres://forced-rls/db'); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('ignores an empty override and falls through to the HTTP fetch', async () => { + const fetchMock = mockFetchResponses([{ body: { access_token: 'TOKEN' } }, { body: { values: { databaseUrl: 'postgres://from-http' } } }]); + process.env.SMOOAI_HARNESS_DATABASE_URL = ''; + + expect(await bootstrapFetch('databaseUrl')).toBe('postgres://from-http'); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); + }); + it('returns undefined for a missing key without throwing', async () => { mockFetchResponses([{ body: { access_token: 'TOKEN' } }, { body: { values: { otherKey: 'x' } } }]); diff --git a/src/bootstrap/index.ts b/src/bootstrap/index.ts index 310220b..f007320 100644 --- a/src/bootstrap/index.ts +++ b/src/bootstrap/index.ts @@ -112,6 +112,16 @@ function resolveEnv(environment?: string): string { return stage; } +/** + * Map a camelCase config key to its `SMOOAI_HARNESS_` env-var + * name (the documented §15 prod-script override). `databaseUrl` → + * `SMOOAI_HARNESS_DATABASE_URL`, `rlsDatabaseUrl` → `SMOOAI_HARNESS_RLS_DATABASE_URL`. + * Exported for tests. + */ +export function harnessEnvKey(key: string): string { + return `SMOOAI_HARNESS_${key.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase()}`; +} + let cached: Record | undefined; let cachedEnv: string | undefined; @@ -130,6 +140,18 @@ export function resetBootstrapCacheForTests(): void { * NOT throw on missing keys — only on env/auth/network errors. */ export async function bootstrapFetch(key: string, options?: BootstrapOptions): Promise { + // Local-harness escape hatch (mirrors packages/db `drizzleClient.resolveDbUrl`, + // the documented §15 prod-script override): an explicit `SMOOAI_HARNESS_` + // env var short-circuits the HTTP fetch entirely, so prod-targeting scripts + // (db-migrate and friends) can be forced deterministically from a developer + // machine. Without this, bootstrapFetch falls through to env='development' + // (no SST stage) and silently fetches the WRONG environment's value over HTTP. + // The env name is the SCREAMING_SNAKE_CASE of the camelCase key: + // databaseUrl → SMOOAI_HARNESS_DATABASE_URL + // rlsDatabaseUrl → SMOOAI_HARNESS_RLS_DATABASE_URL + const override = process.env[harnessEnvKey(key)]; + if (override !== undefined && override.length > 0) return override; + const env = resolveEnv(options?.environment); if (cached === undefined || cachedEnv !== env) { const creds = readCreds();