ci/woodpecker/push/publish Pipeline was canceled
Co-authored-by: ops-deploy-01 <[email protected]>
182 lines
6.8 KiB
TypeScript
182 lines
6.8 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import {
|
|
checkDatabaseSchema,
|
|
SCHEMA_FAIL_REMEDIATION,
|
|
type SchemaCheckDeps,
|
|
} from './schema-check.js';
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Fixture config */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
const tmpDirs: string[] = [];
|
|
|
|
function writeConfig(cfg: Record<string, unknown>): string {
|
|
const dir = mkdtempSync(join(tmpdir(), 'schema-check-'));
|
|
tmpDirs.push(dir);
|
|
const path = join(dir, 'mosaic.config.json');
|
|
writeFileSync(path, JSON.stringify(cfg));
|
|
return path;
|
|
}
|
|
|
|
const STANDALONE_CFG = {
|
|
tier: 'standalone',
|
|
storage: { type: 'postgres', url: 'postgresql://u:p@localhost:5434/x' },
|
|
queue: { type: 'bullmq', url: 'redis://localhost:6380' },
|
|
memory: { type: 'keyword' },
|
|
};
|
|
|
|
const LOCAL_CFG = {
|
|
tier: 'local',
|
|
storage: { type: 'pglite', dataDir: '.mosaic/storage-pglite' },
|
|
queue: { type: 'local', dataDir: '.mosaic/queue' },
|
|
memory: { type: 'keyword' },
|
|
};
|
|
|
|
function deps(overrides: Partial<SchemaCheckDeps> = {}): SchemaCheckDeps {
|
|
return {
|
|
runMigrations: vi.fn().mockResolvedValue(undefined),
|
|
getMigrationStatus: vi.fn().mockResolvedValue({
|
|
appliedCount: 17,
|
|
expectedCount: 17,
|
|
expectedLastTag: '0016_x',
|
|
complete: true,
|
|
}),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Tests */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
describe('checkDatabaseSchema', () => {
|
|
beforeEach(() => {
|
|
process.env['MOSAIC_CONFIG'] = '';
|
|
vi.stubEnv('DATABASE_URL', 'postgresql://env:env@localhost:9999/env');
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
for (const d of tmpDirs.splice(0)) rmSync(d, { recursive: true, force: true });
|
|
});
|
|
|
|
it('passes when the ledger matches the shipped journal', async () => {
|
|
const cfg = writeConfig(STANDALONE_CFG);
|
|
const d = deps();
|
|
const result = await checkDatabaseSchema(d, cfg);
|
|
expect(result.status).toBe('ok');
|
|
expect(result.detail).toContain('17/17');
|
|
expect(d.runMigrations).toHaveBeenCalledWith(STANDALONE_CFG.storage.url);
|
|
expect(d.getMigrationStatus).toHaveBeenCalledWith(STANDALONE_CFG.storage.url);
|
|
});
|
|
|
|
it('FAILS when the ledger is incomplete — the #1389 empty-database signature', async () => {
|
|
const cfg = writeConfig(STANDALONE_CFG);
|
|
const d = deps({
|
|
getMigrationStatus: vi.fn().mockResolvedValue({
|
|
appliedCount: 0,
|
|
expectedCount: 17,
|
|
expectedLastTag: '0016_x',
|
|
complete: false,
|
|
}),
|
|
});
|
|
const result = await checkDatabaseSchema(d, cfg);
|
|
expect(result.status).toBe('fail');
|
|
if (result.status !== 'fail') throw new Error('expected fail');
|
|
expect(result.detail).toContain('0/17');
|
|
expect(result.remediation).toBe(SCHEMA_FAIL_REMEDIATION);
|
|
expect(result.remediation).toContain('#1389');
|
|
});
|
|
|
|
it('FAILS when the ledger is only PARTIALLY migrated (#1402 upgrade case)', async () => {
|
|
const cfg = writeConfig(STANDALONE_CFG);
|
|
const d = deps({
|
|
getMigrationStatus: vi.fn().mockResolvedValue({
|
|
appliedCount: 9,
|
|
expectedCount: 17,
|
|
expectedLastTag: '0016_x',
|
|
complete: false,
|
|
}),
|
|
});
|
|
const result = await checkDatabaseSchema(d, cfg);
|
|
expect(result.status).toBe('fail');
|
|
expect(result.detail).toContain('9/17');
|
|
});
|
|
|
|
it('FAILS (never crashes) when the migration run itself throws', async () => {
|
|
const cfg = writeConfig(STANDALONE_CFG);
|
|
const d = deps({
|
|
runMigrations: vi.fn().mockRejectedValue(new Error('connection refused')),
|
|
});
|
|
const result = await checkDatabaseSchema(d, cfg);
|
|
expect(result.status).toBe('fail');
|
|
expect(result.detail).toContain('connection refused');
|
|
});
|
|
|
|
it('skips the local tier (gateway migrates its own PGlite at startup)', async () => {
|
|
const cfg = writeConfig(LOCAL_CFG);
|
|
const d = deps();
|
|
const result = await checkDatabaseSchema(d, cfg);
|
|
expect(result.status).toBe('skipped');
|
|
expect(d.runMigrations).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Config resolution priority */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
describe('resolveSchemaCheckConfigPath', () => {
|
|
it('prefers the daemon-written gateway config over cwd copies', async () => {
|
|
const { resolveSchemaCheckConfigPath } = await import('./schema-check.js');
|
|
const cwdCfg = writeConfig(STANDALONE_CFG); // in a temp dir
|
|
const daemonDir = mkdtempSync(join(tmpdir(), 'schema-check-daemon-'));
|
|
tmpDirs.push(daemonDir);
|
|
const daemonHome = join(daemonDir, '.config', 'mosaic', 'gateway');
|
|
mkdirSync(daemonHome, { recursive: true });
|
|
writeFileSync(join(daemonHome, 'mosaic.config.json'), JSON.stringify(LOCAL_CFG));
|
|
|
|
const prevHome = process.env['HOME'];
|
|
vi.stubEnv('HOME', daemonDir);
|
|
vi.stubEnv('MOSAIC_CONFIG', '');
|
|
try {
|
|
const resolved = resolveSchemaCheckConfigPath();
|
|
// Must NOT pick the cwd copy (cwd is the vitest project dir, not our
|
|
// temp dir — so the only resolvable candidates are daemon + $HOME/.mosaic).
|
|
expect(resolved).toBe(join(daemonHome, 'mosaic.config.json'));
|
|
void cwdCfg;
|
|
} finally {
|
|
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);
|
|
}
|
|
});
|
|
});
|