import 'reflect-metadata'; import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; import * as nodeOs from 'node:os'; import { dirname, isAbsolute, join, relative, resolve } from 'node:path'; import * as nodeUrl from 'node:url'; import { MODULE_METADATA } from '@nestjs/common/constants.js'; import { describe, expect, it, vi } from 'vitest'; import type { MosaicConfig } from '@mosaicstack/config'; interface ComposedModuleGraph { imports: readonly unknown[]; federationModule: unknown; bootLogLines: readonly string[]; mosaicConfig: MosaicConfig; resolvedConfigPath: string; } type StorageTier = 'local' | 'standalone' | 'federated'; interface ModuleGraphFixture { tempRoot: string; anchor: string; homePath: string; cwdPath: string; monorepoRootEnvPath: string; gatewayLocalEnvPath: string; daemonEnvPath: string; monorepoRootConfigPath: string; gatewayLocalConfigPath: string; } interface ModuleGraphFixtureOptions { rootEnvMode?: 'present' | 'absent'; rootTier?: StorageTier; rootEnvContents?: string; redactionMarker?: string; gatewayLocalTier?: StorageTier; gatewayLocalEnvContents?: string; daemonEnvContents?: string; inheritedTier?: StorageTier; expectedProcessTier?: string; setup?: (fixture: ModuleGraphFixture) => Promise; } // Each case uses vi.resetModules() and re-imports the full gateway graph for distinct ambient FS/env; CI needs headroom, while this still guards genuine hangs. const MODULE_IMPORT_TIMEOUT_MS = 120_000; const MONOREPO_ROOT_DOTENV_LABEL = 'monorepo-root .env'; const DAEMON_DOTENV_LABEL = 'daemon .env'; function configJson(tier: StorageTier): string { if (tier === 'local') { return JSON.stringify({ tier, storage: { type: 'pglite', dataDir: '.mosaic/storage-pglite' }, queue: { type: 'local', dataDir: '.mosaic/queue' }, memory: { type: 'keyword' }, }); } return JSON.stringify({ tier, storage: { type: 'postgres', url: 'postgresql://fixture.invalid/mosaic' }, queue: { type: 'bullmq' }, memory: { type: tier === 'federated' ? 'pgvector' : 'keyword' }, }); } function snapshotProcessEnv(): Record { return { ...process.env }; } function restoreProcessEnv(snapshot: Record): void { for (const key of Object.keys(process.env)) { if (!(key in snapshot)) { delete process.env[key]; } } for (const [key, value] of Object.entries(snapshot)) { if (value === undefined) { delete process.env[key]; continue; } process.env[key] = value; } } function expectPathUnderTempRoot(path: string, tempRoot: string): void { const relativePath = relative(tempRoot, path); expect(relativePath === '' || (!relativePath.startsWith('..') && !isAbsolute(relativePath))).toBe( true, ); } async function writeFixture(path: string, contents: string, tempRoot: string): Promise { expectPathUnderTempRoot(path, tempRoot); await mkdir(dirname(path), { recursive: true }); await writeFile(path, contents, 'utf8'); } interface ConfigModuleProvider { provide: string; useFactory: () => MosaicConfig; } function isConfigModuleProvider(value: unknown): value is ConfigModuleProvider { if (typeof value !== 'object' || value === null) { return false; } if (!('provide' in value) || typeof value.provide !== 'string') { return false; } return 'useFactory' in value && typeof value.useFactory === 'function'; } function singleBootLogLine(bootLogLines: readonly string[]): string { expect(bootLogLines).toHaveLength(1); const [bootLogLine] = bootLogLines; if (bootLogLine === undefined) { throw new Error('Expected a single boot log line'); } return bootLogLine; } function expectBootLogLine( bootLogLines: readonly string[], tier: StorageTier, source: string, ): void { const bootLogLine = singleBootLogLine(bootLogLines); expect(bootLogLine).toContain(`storage tier=${tier}`); expect(bootLogLine).toContain(`source=${source}`); } async function loadModuleGraphFromDotenv( options: ModuleGraphFixtureOptions, ): Promise { const originalEnv = snapshotProcessEnv(); const tempRoot = await mkdtemp(join(nodeOs.tmpdir(), 'mosaic-gateway-module-')); let consoleInfoSpy: ReturnType | undefined; let cwdSpy: ReturnType | undefined; try { const anchor = join(tempRoot, 'anchored', 'apps', 'gateway', 'src'); const homePath = join(tempRoot, 'home'); const cwdPath = join(tempRoot, 'ambient', 'parent', 'cwd'); const fixture: ModuleGraphFixture = { tempRoot, anchor, homePath, cwdPath, monorepoRootEnvPath: resolve(anchor, '../../..', '.env'), gatewayLocalEnvPath: resolve(anchor, '..', '.env'), daemonEnvPath: join(homePath, '.config', 'mosaic', 'gateway', '.env'), monorepoRootConfigPath: resolve(anchor, '../../..', 'mosaic.config.json'), gatewayLocalConfigPath: resolve(anchor, '..', 'mosaic.config.json'), }; consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation((): void => undefined); for (const path of Object.values(fixture)) { expectPathUnderTempRoot(path, tempRoot); } await mkdir(anchor, { recursive: true }); await mkdir(cwdPath, { recursive: true }); if ((options.rootEnvMode ?? 'present') === 'absent') { if ( options.rootEnvContents !== undefined || options.rootTier !== undefined || options.redactionMarker !== undefined ) { throw new Error('Expected no root env fixture values when rootEnvMode is absent'); } } else { if (options.rootEnvContents === undefined && options.rootTier === undefined) { throw new Error('Expected rootTier or rootEnvContents'); } const rootFixture = options.rootEnvContents ?? `MOSAIC_STORAGE_TIER=${options.rootTier}\n`; const rootFixtureWithMarker = options.redactionMarker ? `${rootFixture}BETTER_AUTH_SECRET=${options.redactionMarker}\n` : rootFixture; await writeFixture(fixture.monorepoRootEnvPath, rootFixtureWithMarker, tempRoot); } if (options.daemonEnvContents !== undefined) { await writeFixture(fixture.daemonEnvPath, options.daemonEnvContents, tempRoot); } if (options.gatewayLocalEnvContents !== undefined) { await writeFixture(fixture.gatewayLocalEnvPath, options.gatewayLocalEnvContents, tempRoot); } else if (options.gatewayLocalTier !== undefined) { await writeFixture( fixture.gatewayLocalEnvPath, `MOSAIC_STORAGE_TIER=${options.gatewayLocalTier}\n`, tempRoot, ); } process.env['HOME'] = homePath; delete process.env['MOSAIC_STORAGE_TIER']; delete process.env['DATABASE_URL']; delete process.env['VALKEY_URL']; delete process.env['MOSAIC_GATEWAY_HOME']; await options.setup?.(fixture); if (options.inheritedTier !== undefined) { process.env['MOSAIC_STORAGE_TIER'] = options.inheritedTier; } vi.resetModules(); vi.doMock('node:os', () => ({ ...nodeOs, homedir: (): string => homePath })); vi.doMock('node:url', () => ({ ...nodeUrl, fileURLToPath: (url: string | URL): string => { const actualPath = nodeUrl.fileURLToPath(url); if ( actualPath.endsWith('/apps/gateway/src/env.ts') || actualPath.endsWith('/apps/gateway/src/env.js') ) { return join(anchor, 'env.ts'); } return actualPath; }, })); cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(cwdPath); if (options.inheritedTier === undefined) { expect(process.env['MOSAIC_STORAGE_TIER']).toBeUndefined(); } else { expect(process.env['MOSAIC_STORAGE_TIER']).toBe(options.inheritedTier); } const envModule = await import('./env.js'); expect(process.env['MOSAIC_STORAGE_TIER']).toBe( options.expectedProcessTier ?? options.rootTier, ); const { AppModule } = await import('./app.module.js'); const { FederationModule } = await import('./federation/federation.module.js'); const imports: unknown = Reflect.getMetadata(MODULE_METADATA.IMPORTS, AppModule); if (!Array.isArray(imports)) { throw new Error('AppModule imports metadata is not an array'); } const { ConfigModule, MOSAIC_CONFIG } = await import('./config/config.module.js'); const providers: unknown = Reflect.getMetadata(MODULE_METADATA.PROVIDERS, ConfigModule); if (!Array.isArray(providers)) { throw new Error('ConfigModule providers metadata is not an array'); } const configProvider = providers .filter(isConfigModuleProvider) .find((provider: ConfigModuleProvider): boolean => provider.provide === MOSAIC_CONFIG); if (!configProvider) { throw new Error('MOSAIC_CONFIG provider factory not found'); } return { imports, federationModule: FederationModule, bootLogLines: consoleInfoSpy.mock.calls.map((args: readonly unknown[]): string => args.map((value: unknown): string => String(value)).join(' '), ), mosaicConfig: configProvider.useFactory(), resolvedConfigPath: envModule.resolveGatewayConfigPath(), }; } finally { cwdSpy?.mockRestore(); vi.doUnmock('node:url'); vi.doUnmock('node:os'); vi.resetModules(); consoleInfoSpy?.mockRestore(); restoreProcessEnv(originalEnv); await rm(tempRoot, { recursive: true, force: true }); } } describe('AppModule federation gating', (): void => { it('loads dotenv before tracing and AppModule evaluation', async (): Promise => { const mainSource = await readFile(new URL('./main.ts', import.meta.url), 'utf8'); const envImportIndex = mainSource.indexOf("import './env.js';"); const tracingImportIndex = mainSource.indexOf("import './tracing.js';"); const appModuleImportIndex = mainSource.indexOf("import { AppModule } from './app.module.js';"); expect(envImportIndex).toBeGreaterThan(-1); expect(envImportIndex).toBeLessThan(tracingImportIndex); expect(envImportIndex).toBeLessThan(appModuleImportIndex); }); it( 'ignores ambient cwd/.env and cwd/../.env files', async (): Promise => { const graph = await loadModuleGraphFromDotenv({ rootTier: 'local', setup: async (fixture: ModuleGraphFixture): Promise => { await writeFixture( join(fixture.cwdPath, '.env'), 'MOSAIC_STORAGE_TIER=federated\n', fixture.tempRoot, ); await writeFixture( resolve(fixture.cwdPath, '..', '.env'), 'MOSAIC_STORAGE_TIER=federated\n', fixture.tempRoot, ); }, }); expect(graph.imports).not.toContain(graph.federationModule); expectBootLogLine(graph.bootLogLines, 'local', MONOREPO_ROOT_DOTENV_LABEL); }, MODULE_IMPORT_TIMEOUT_MS, ); it( 'ignores an ambient cwd/mosaic.config.json federated config', async (): Promise => { const graph = await loadModuleGraphFromDotenv({ rootTier: 'local', setup: async (fixture: ModuleGraphFixture): Promise => { await writeFixture( join(fixture.cwdPath, 'mosaic.config.json'), configJson('federated'), fixture.tempRoot, ); }, }); expect(graph.imports).not.toContain(graph.federationModule); expectBootLogLine(graph.bootLogLines, 'local', MONOREPO_ROOT_DOTENV_LABEL); }, MODULE_IMPORT_TIMEOUT_MS, ); it( 'ignores an ambient cwd/../../mosaic.config.json federated config', async (): Promise => { const graph = await loadModuleGraphFromDotenv({ rootTier: 'local', setup: async (fixture: ModuleGraphFixture): Promise => { await writeFixture( resolve(fixture.cwdPath, '../..', 'mosaic.config.json'), configJson('federated'), fixture.tempRoot, ); }, }); expect(graph.imports).not.toContain(graph.federationModule); expectBootLogLine(graph.bootLogLines, 'local', MONOREPO_ROOT_DOTENV_LABEL); }, MODULE_IMPORT_TIMEOUT_MS, ); it( 'anchored gateway-local config wins monorepo-root config and registers FederationModule', async (): Promise => { let gatewayLocalConfigPath = ''; const graph = await loadModuleGraphFromDotenv({ rootTier: 'local', setup: async (fixture: ModuleGraphFixture): Promise => { gatewayLocalConfigPath = fixture.gatewayLocalConfigPath; await writeFixture( fixture.gatewayLocalConfigPath, configJson('federated'), fixture.tempRoot, ); await writeFixture(fixture.monorepoRootConfigPath, configJson('local'), fixture.tempRoot); }, }); expect(graph.resolvedConfigPath).toBe(gatewayLocalConfigPath); expect(graph.mosaicConfig.tier).toBe('federated'); expect(graph.imports).toContain(graph.federationModule); expectBootLogLine(graph.bootLogLines, 'federated', 'mosaic.config.json'); }, MODULE_IMPORT_TIMEOUT_MS, ); it( 'resolves the daemon-installed GATEWAY_HOME/mosaic.config.json ahead of gateway-local and monorepo-root configs', async (): Promise => { let daemonConfigPath = ''; const graph = await loadModuleGraphFromDotenv({ rootEnvMode: 'absent', setup: async (fixture: ModuleGraphFixture): Promise => { const externalGatewayHome = join(fixture.tempRoot, 'external-gateway-home'); daemonConfigPath = join(externalGatewayHome, 'mosaic.config.json'); await writeFixture(daemonConfigPath, configJson('federated'), fixture.tempRoot); await writeFixture( fixture.gatewayLocalConfigPath, configJson('standalone'), fixture.tempRoot, ); await writeFixture(fixture.monorepoRootConfigPath, configJson('local'), fixture.tempRoot); process.env['MOSAIC_GATEWAY_HOME'] = externalGatewayHome; process.env['DATABASE_URL'] = 'postgresql://fixture.invalid/mosaic'; }, }); expect(graph.resolvedConfigPath).toBe(daemonConfigPath); expect(graph.mosaicConfig.tier).toBe('federated'); expect(graph.imports).toContain(graph.federationModule); expectBootLogLine(graph.bootLogLines, 'federated', 'mosaic.config.json'); }, MODULE_IMPORT_TIMEOUT_MS, ); it( 'logs mosaic.config.json when anchored config and env tiers are both federated', async (): Promise => { const graph = await loadModuleGraphFromDotenv({ rootTier: 'federated', setup: async (fixture: ModuleGraphFixture): Promise => { await writeFixture( fixture.monorepoRootConfigPath, configJson('federated'), fixture.tempRoot, ); }, }); expect(graph.imports).toContain(graph.federationModule); expectBootLogLine(graph.bootLogLines, 'federated', 'mosaic.config.json'); }, MODULE_IMPORT_TIMEOUT_MS, ); it( 'logs standalone from a monorepo-root .env DATABASE_URL fallback', async (): Promise => { const graph = await loadModuleGraphFromDotenv({ rootEnvContents: 'DATABASE_URL=fixture-database-url\n', }); expect(graph.imports).not.toContain(graph.federationModule); expectBootLogLine(graph.bootLogLines, 'standalone', MONOREPO_ROOT_DOTENV_LABEL); }, MODULE_IMPORT_TIMEOUT_MS, ); it( 'attributes an invalid monorepo-root dotenv tier to the default', async (): Promise => { const graph = await loadModuleGraphFromDotenv({ rootEnvContents: 'MOSAIC_STORAGE_TIER=invalid\n', expectedProcessTier: 'invalid', }); expect(graph.imports).not.toContain(graph.federationModule); expectBootLogLine(graph.bootLogLines, 'local', 'default'); }, MODULE_IMPORT_TIMEOUT_MS, ); it( 'attributes DATABASE_URL fallback to daemon .env ahead of inherited local tier', async (): Promise => { const graph = await loadModuleGraphFromDotenv({ rootEnvMode: 'absent', daemonEnvContents: 'DATABASE_URL=fixture-database-url\n', inheritedTier: 'local', expectedProcessTier: 'local', }); expect(graph.imports).not.toContain(graph.federationModule); expectBootLogLine(graph.bootLogLines, 'standalone', DAEMON_DOTENV_LABEL); }, MODULE_IMPORT_TIMEOUT_MS, ); it( 'daemon .env wins over monorepo-root and gateway-local tier values', async (): Promise => { const graph = await loadModuleGraphFromDotenv({ rootTier: 'local', gatewayLocalTier: 'federated', daemonEnvContents: 'MOSAIC_STORAGE_TIER=standalone\n', expectedProcessTier: 'standalone', }); expect(graph.imports).not.toContain(graph.federationModule); expectBootLogLine(graph.bootLogLines, 'standalone', DAEMON_DOTENV_LABEL); }, MODULE_IMPORT_TIMEOUT_MS, ); it( 'inherits process.env.MOSAIC_STORAGE_TIER over daemon, monorepo-root, and gateway-local dotenv values', async (): Promise => { const graph = await loadModuleGraphFromDotenv({ rootTier: 'local', gatewayLocalTier: 'federated', daemonEnvContents: 'MOSAIC_STORAGE_TIER=federated\n', inheritedTier: 'standalone', expectedProcessTier: 'standalone', }); expect(graph.imports).not.toContain(graph.federationModule); expectBootLogLine(graph.bootLogLines, 'standalone', 'process environment'); }, MODULE_IMPORT_TIMEOUT_MS, ); it( 'gateway-local .env configures the tier and source when the monorepo-root .env is absent', async (): Promise => { const graph = await loadModuleGraphFromDotenv({ rootEnvMode: 'absent', gatewayLocalTier: 'federated', expectedProcessTier: 'federated', }); expect(graph.imports).toContain(graph.federationModule); expectBootLogLine(graph.bootLogLines, 'federated', 'gateway-local .env'); }, MODULE_IMPORT_TIMEOUT_MS, ); it( 'monorepo-root .env wins over gateway-local tier values', async (): Promise => { const graph = await loadModuleGraphFromDotenv({ rootTier: 'standalone', gatewayLocalTier: 'federated', }); expect(graph.imports).not.toContain(graph.federationModule); expectBootLogLine(graph.bootLogLines, 'standalone', MONOREPO_ROOT_DOTENV_LABEL); }, MODULE_IMPORT_TIMEOUT_MS, ); it.each(['local', 'standalone'] as const)( 'does not register FederationModule for the %s tier', async (tier): Promise => { const graph = await loadModuleGraphFromDotenv({ rootTier: tier }); expect(graph.imports).not.toContain(graph.federationModule); expectBootLogLine(graph.bootLogLines, tier, MONOREPO_ROOT_DOTENV_LABEL); }, MODULE_IMPORT_TIMEOUT_MS, ); it( 'registers FederationModule when federated tier is supplied by the anchored monorepo root .env', async (): Promise => { const redactionMarker = 'redaction-fixture-marker'; const graph = await loadModuleGraphFromDotenv({ rootTier: 'federated', redactionMarker, }); expect(graph.imports).toContain(graph.federationModule); expectBootLogLine(graph.bootLogLines, 'federated', MONOREPO_ROOT_DOTENV_LABEL); expect(singleBootLogLine(graph.bootLogLines)).not.toContain(redactionMarker); }, MODULE_IMPORT_TIMEOUT_MS, ); it( 'MOSAIC_CONFIG provider ignores an ambient cwd/mosaic.config.json config', async (): Promise => { const graph = await loadModuleGraphFromDotenv({ rootTier: 'local', setup: async (fixture: ModuleGraphFixture): Promise => { await writeFixture( join(fixture.cwdPath, 'mosaic.config.json'), JSON.stringify({ tier: 'federated', storage: { type: 'postgres', url: 'postgresql://ambient-attacker.invalid/mosaic', enableVector: true, }, queue: { type: 'bullmq' }, memory: { type: 'pgvector' }, }), fixture.tempRoot, ); }, }); expect(graph.mosaicConfig.tier).toBe('local'); expect(graph.mosaicConfig.storage).not.toEqual( expect.objectContaining({ url: 'postgresql://ambient-attacker.invalid/mosaic' }), ); }, MODULE_IMPORT_TIMEOUT_MS, ); it( 'MOSAIC_CONFIG provider resolves from the anchored monorepo-root mosaic.config.json', async (): Promise => { const graph = await loadModuleGraphFromDotenv({ rootTier: 'local', setup: async (fixture: ModuleGraphFixture): Promise => { await writeFixture( fixture.monorepoRootConfigPath, configJson('federated'), fixture.tempRoot, ); }, }); expect(graph.mosaicConfig.tier).toBe('federated'); expect(graph.mosaicConfig.storage).toEqual( expect.objectContaining({ url: 'postgresql://fixture.invalid/mosaic' }), ); }, MODULE_IMPORT_TIMEOUT_MS, ); });