From 3e31935a28107bd8c7082fe253b3fe24d16317f6 Mon Sep 17 00:00:00 2001 From: ops-deploy-01 Date: Tue, 25 Aug 2026 09:47:08 -0500 Subject: [PATCH] =?UTF-8?q?fix(#1392=20followup):=20review-285=20N1+N2=20?= =?UTF-8?q?=E2=80=94=20env=20has=20no=20config=20authority=20in=20schema-c?= =?UTF-8?q?heck;=20verification=20throws=20are=20fatal=20at=20install?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit N1: resolveSchemaCheckConfigPath drops the MOSAIC_CONFIG env candidate entirely (mirrors apps/gateway/src/env.ts's deliberate env-has-no-config-authority stance; a stale env var could verify a database the daemon never reads). verify.ts now passes undefined rather than the env var. New spec: a decoy MOSAIC_CONFIG must never win over the daemon-written config. N2: install treats a THROWN post-install verification as fatal (exit 1 with remediation pointer) in addition to the existing result-object hard-fail — an install must never report success over an unverified database, whichever way the check failed. --- .../mosaic/src/commands/gateway/install.ts | 16 ++++++++++-- .../src/commands/gateway/schema-check.spec.ts | 25 +++++++++++++++++++ .../src/commands/gateway/schema-check.ts | 5 +++- .../mosaic/src/commands/gateway/verify.ts | 2 +- 4 files changed, 44 insertions(+), 4 deletions(-) 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); -- 2.54.0