diff --git a/packages/mosaic/src/commands/gateway/install.ts b/packages/mosaic/src/commands/gateway/install.ts index 186f7b2e..ff0b899b 100644 --- a/packages/mosaic/src/commands/gateway/install.ts +++ b/packages/mosaic/src/commands/gateway/install.ts @@ -95,11 +95,17 @@ export async function runInstall(opts: InstallOpts): Promise { // fatal (#1392): an install that reports success over an empty/partial // database is the exact T63 failure this command must never reproduce. let verifyResult: VerifyResult | undefined; + let verificationThrew = false; try { const { runPostInstallVerification } = await import('./verify.js'); verifyResult = await runPostInstallVerification(configResult.host, configResult.port); - } catch { - // Non-fatal — verification is a courtesy + } catch (err) { + // Health/token/bootstrap courtesy failures are non-fatal, but a THROWN + // schema verification must not let install report success either (N2, + // rev-code-02 review 285): mark it and treat as fatal below. + verificationThrew = true; + const msg = err instanceof Error ? err.message : String(err); + prompter.warn(`Post-install verification errored: ${msg}`); } if (verifyResult && verifyResult.schemaMigrated === false) { prompter.warn( @@ -107,6 +113,12 @@ export async function runInstall(opts: InstallOpts): Promise { ); process.exit(1); } + if (verificationThrew) { + prompter.warn( + 'Gateway install ABORTED: post-install verification errored (see above); refusing to report success on an unverified database.', + ); + process.exit(1); + } } catch (err) { // Stages normally return structured results for expected failures. // Anything that reaches here is an unexpected runtime error — render a diff --git a/packages/mosaic/src/commands/gateway/schema-check.spec.ts b/packages/mosaic/src/commands/gateway/schema-check.spec.ts index 199317b9..7ea951e4 100644 --- a/packages/mosaic/src/commands/gateway/schema-check.spec.ts +++ b/packages/mosaic/src/commands/gateway/schema-check.spec.ts @@ -153,4 +153,29 @@ describe('resolveSchemaCheckConfigPath', () => { if (prevHome !== undefined) vi.stubEnv('HOME', prevHome); } }); + + it('gives MOSAIC_CONFIG NO authority (N1, review 285): env never overrides file resolution', async () => { + const { resolveSchemaCheckConfigPath } = await import('./schema-check.js'); + const daemonDir = mkdtempSync(join(tmpdir(), 'schema-check-env-')); + tmpDirs.push(daemonDir); + const daemonHome = join(daemonDir, '.config', 'mosaic', 'gateway'); + mkdirSync(daemonHome, { recursive: true }); + writeFileSync(join(daemonHome, 'mosaic.config.json'), JSON.stringify(LOCAL_CFG)); + // A stale env var pointing at a DIFFERENT file must be ignored entirely: + const decoyDir = mkdtempSync(join(tmpdir(), 'schema-check-decoy-')); + tmpDirs.push(decoyDir); + const decoyPath = join(decoyDir, 'mosaic.config.json'); + writeFileSync(decoyPath, JSON.stringify(STANDALONE_CFG)); + + const prevHome = process.env['HOME']; + vi.stubEnv('HOME', daemonDir); + vi.stubEnv('MOSAIC_CONFIG', decoyPath); + try { + const resolved = resolveSchemaCheckConfigPath(); + expect(resolved).toBe(join(daemonHome, 'mosaic.config.json')); + expect(resolved).not.toBe(decoyPath); + } finally { + if (prevHome !== undefined) vi.stubEnv('HOME', prevHome); + } + }); }); diff --git a/packages/mosaic/src/commands/gateway/schema-check.ts b/packages/mosaic/src/commands/gateway/schema-check.ts index b4ab810a..5d5d2bba 100644 --- a/packages/mosaic/src/commands/gateway/schema-check.ts +++ b/packages/mosaic/src/commands/gateway/schema-check.ts @@ -53,8 +53,11 @@ export const SCHEMA_FAIL_REMEDIATION = [ */ export function resolveSchemaCheckConfigPath(explicit?: string): string | undefined { if (explicit) return resolve(explicit); + // NOTE: no env-var candidate, deliberately. apps/gateway/src/env.ts gives env + // NO config authority (a stale MOSAIC_CONFIG could verify a database the + // daemon never reads — rev-code-02 review 285, note N1). Resolution order + // mirrors the daemon's file priorities only. const candidates = [ - process.env['MOSAIC_CONFIG'], join(homedir(), '.config', 'mosaic', 'gateway', 'mosaic.config.json'), // daemon-written resolve(process.cwd(), 'mosaic.config.json'), join(homedir(), '.mosaic', 'mosaic.config.json'), diff --git a/packages/mosaic/src/commands/gateway/verify.ts b/packages/mosaic/src/commands/gateway/verify.ts index 5ac83dc9..6f7f862e 100644 --- a/packages/mosaic/src/commands/gateway/verify.ts +++ b/packages/mosaic/src/commands/gateway/verify.ts @@ -101,7 +101,7 @@ export async function runPostInstallVerification( const { runMigrations, getMigrationStatus } = await import('@mosaicstack/db'); const result = await checkDatabaseSchema( { runMigrations, getMigrationStatus }, - process.env['MOSAIC_CONFIG'], + undefined, // resolver mirrors daemon file priorities; env has no config authority (N1) ); if (result.status === 'ok') { ok(result.detail);