Co-Authored-By: Claude Haiku 4.5 <[email protected]>
164 lines
5.5 KiB
TypeScript
164 lines
5.5 KiB
TypeScript
import 'reflect-metadata';
|
|
import { mkdir, mkdtemp, 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 type { MosaicConfig } from '@mosaicstack/config';
|
|
import type * as MosaicStorage from '@mosaicstack/storage';
|
|
import { describe, expect, it, vi, type MockInstance } from 'vitest';
|
|
|
|
const MODULE_IMPORT_TIMEOUT_MS = 30_000;
|
|
|
|
function snapshotProcessEnv(): Record<string, string | undefined> {
|
|
return { ...process.env };
|
|
}
|
|
|
|
function restoreProcessEnv(snapshot: Record<string, string | undefined>): 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<void> {
|
|
expectPathUnderTempRoot(path, tempRoot);
|
|
await mkdir(dirname(path), { recursive: true });
|
|
await writeFile(path, contents, 'utf8');
|
|
}
|
|
|
|
interface BootstrapPreflightResult {
|
|
capturedConfig: MosaicConfig | undefined;
|
|
}
|
|
|
|
async function runBootstrapPreflight(
|
|
anchoredConfigContents: string,
|
|
ambientConfigContents: string,
|
|
): Promise<BootstrapPreflightResult> {
|
|
const originalEnv = snapshotProcessEnv();
|
|
const tempRoot = await mkdtemp(join(nodeOs.tmpdir(), 'mosaic-gateway-main-preflight-'));
|
|
let cwdSpy: ReturnType<typeof vi.spyOn> | undefined;
|
|
let exitSpy: MockInstance<typeof process.exit> | undefined;
|
|
let consoleInfoSpy: ReturnType<typeof vi.spyOn> | undefined;
|
|
let capturedConfig: MosaicConfig | undefined;
|
|
|
|
try {
|
|
const anchor = join(tempRoot, 'anchored', 'apps', 'gateway', 'src');
|
|
const homePath = join(tempRoot, 'home');
|
|
const cwdPath = join(tempRoot, 'ambient', 'cwd');
|
|
const monorepoRootConfigPath = resolve(anchor, '../../..', 'mosaic.config.json');
|
|
|
|
await mkdir(anchor, { recursive: true });
|
|
await mkdir(cwdPath, { recursive: true });
|
|
|
|
await writeFixture(monorepoRootConfigPath, anchoredConfigContents, tempRoot);
|
|
await writeFixture(join(cwdPath, 'mosaic.config.json'), ambientConfigContents, tempRoot);
|
|
|
|
process.env['HOME'] = homePath;
|
|
process.env['BETTER_AUTH_SECRET'] = 'fixture-secret';
|
|
delete process.env['MOSAIC_STORAGE_TIER'];
|
|
delete process.env['DATABASE_URL'];
|
|
delete process.env['VALKEY_URL'];
|
|
|
|
consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation((): void => undefined);
|
|
const exitMock = vi.fn<typeof process.exit>();
|
|
exitSpy = vi.spyOn(process, 'exit').mockImplementation(exitMock);
|
|
|
|
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);
|
|
vi.doMock('./tracing.js', () => ({}));
|
|
|
|
const preflightSentinel = new Error('preflight-capture-sentinel');
|
|
vi.doMock('@mosaicstack/storage', async () => {
|
|
const actual = await vi.importActual<typeof MosaicStorage>('@mosaicstack/storage');
|
|
return {
|
|
...actual,
|
|
detectAndAssertTier: vi.fn((config: MosaicConfig): Promise<void> => {
|
|
capturedConfig = config;
|
|
throw preflightSentinel;
|
|
}),
|
|
};
|
|
});
|
|
|
|
await import('./main.js');
|
|
await vi.waitFor((): void => {
|
|
expect(exitSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
return { capturedConfig };
|
|
} finally {
|
|
cwdSpy?.mockRestore();
|
|
exitSpy?.mockRestore();
|
|
consoleInfoSpy?.mockRestore();
|
|
vi.doUnmock('@mosaicstack/storage');
|
|
vi.doUnmock('./tracing.js');
|
|
vi.doUnmock('node:url');
|
|
vi.doUnmock('node:os');
|
|
vi.resetModules();
|
|
restoreProcessEnv(originalEnv);
|
|
await rm(tempRoot, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
describe('main bootstrap preflight config anchoring', (): void => {
|
|
it(
|
|
'passes the anchored monorepo-root config to detectAndAssertTier, not an ambient cwd config',
|
|
async (): Promise<void> => {
|
|
const anchoredConfig = JSON.stringify({
|
|
tier: 'local',
|
|
storage: { type: 'pglite', dataDir: '.mosaic/storage-pglite' },
|
|
queue: { type: 'local', dataDir: '.mosaic/queue' },
|
|
memory: { type: 'keyword' },
|
|
});
|
|
const ambientConfig = JSON.stringify({
|
|
tier: 'federated',
|
|
storage: {
|
|
type: 'postgres',
|
|
url: 'postgresql://ambient-attacker.invalid/mosaic',
|
|
enableVector: true,
|
|
},
|
|
queue: { type: 'bullmq' },
|
|
memory: { type: 'pgvector' },
|
|
});
|
|
|
|
const { capturedConfig } = await runBootstrapPreflight(anchoredConfig, ambientConfig);
|
|
|
|
expect(capturedConfig?.tier).toBe('local');
|
|
expect(capturedConfig?.storage).not.toEqual(
|
|
expect.objectContaining({ url: 'postgresql://ambient-attacker.invalid/mosaic' }),
|
|
);
|
|
},
|
|
MODULE_IMPORT_TIMEOUT_MS,
|
|
);
|
|
});
|