test(federation): integration tests for federated tier gateway boot (FED-M1-07)
Adds three integration tests gated by FEDERATED_INTEGRATION=1 that exercise the federated tier against a live docker-compose federated profile: - federated-boot.success — detectAndAssertTier resolves; pg_extension lists vector - federated-boot.pg-unreachable — fail-fast TierDetectionError(service=postgres) using OS-reserved closed port to avoid host-port collisions - federated-pgvector — TEMP table with vector(3) column round-trips data Real services, no mocks. Tests skip cleanly when env var is unset. Refs #460
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Test B — Gateway boot refuses (fail-fast) when PG is unreachable.
|
||||
*
|
||||
* Prereq: docker compose -f docker-compose.federated.yml --profile federated up -d
|
||||
* (Valkey must be running; only PG is intentionally misconfigured.)
|
||||
* Run: FEDERATED_INTEGRATION=1 pnpm --filter @mosaicstack/gateway test src/__tests__/integration/federated-boot.pg-unreachable.integration.test.ts
|
||||
*
|
||||
* Skipped when FEDERATED_INTEGRATION !== '1'.
|
||||
*/
|
||||
|
||||
import net from 'node:net';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import { TierDetectionError, detectAndAssertTier } from '@mosaicstack/storage';
|
||||
|
||||
const run = process.env['FEDERATED_INTEGRATION'] === '1';
|
||||
|
||||
const VALKEY_URL = 'redis://localhost:6380';
|
||||
|
||||
/**
|
||||
* Reserves a guaranteed-closed port at runtime by binding to an ephemeral OS
|
||||
* port (port 0) and immediately releasing it. The OS will not reassign the
|
||||
* port during the TIME_WAIT window, so it remains closed for the duration of
|
||||
* this test.
|
||||
*/
|
||||
async function reserveClosedPort(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
const addr = server.address();
|
||||
if (typeof addr !== 'object' || !addr) return reject(new Error('no addr'));
|
||||
const port = addr.port;
|
||||
server.close(() => resolve(port));
|
||||
});
|
||||
server.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
describe.skipIf(!run)('federated boot — PG unreachable', () => {
|
||||
let badPgUrl: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const closedPort = await reserveClosedPort();
|
||||
badPgUrl = `postgresql://mosaic:mosaic@localhost:${closedPort}/mosaic`;
|
||||
});
|
||||
|
||||
it('detectAndAssertTier throws TierDetectionError with service: postgres when PG is down', async () => {
|
||||
const brokenConfig = {
|
||||
tier: 'federated' as const,
|
||||
storage: {
|
||||
type: 'postgres' as const,
|
||||
url: badPgUrl,
|
||||
enableVector: true,
|
||||
},
|
||||
queue: {
|
||||
type: 'bullmq',
|
||||
url: VALKEY_URL,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(detectAndAssertTier(brokenConfig)).rejects.toSatisfy(
|
||||
(err: unknown) => err instanceof TierDetectionError && err.service === 'postgres',
|
||||
);
|
||||
}, 10_000);
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Test A — Gateway boot succeeds when federated services are up.
|
||||
*
|
||||
* Prereq: docker compose -f docker-compose.federated.yml --profile federated up -d
|
||||
* Run: FEDERATED_INTEGRATION=1 pnpm --filter @mosaicstack/gateway test src/__tests__/integration/federated-boot.success.integration.test.ts
|
||||
*
|
||||
* Skipped when FEDERATED_INTEGRATION !== '1'.
|
||||
*/
|
||||
|
||||
import postgres from 'postgres';
|
||||
import { afterAll, describe, expect, it } from 'vitest';
|
||||
import { detectAndAssertTier } from '@mosaicstack/storage';
|
||||
|
||||
const run = process.env['FEDERATED_INTEGRATION'] === '1';
|
||||
|
||||
const PG_URL = 'postgresql://mosaic:mosaic@localhost:5433/mosaic';
|
||||
const VALKEY_URL = 'redis://localhost:6380';
|
||||
|
||||
const federatedConfig = {
|
||||
tier: 'federated' as const,
|
||||
storage: {
|
||||
type: 'postgres' as const,
|
||||
url: PG_URL,
|
||||
enableVector: true,
|
||||
},
|
||||
queue: {
|
||||
type: 'bullmq',
|
||||
url: VALKEY_URL,
|
||||
},
|
||||
};
|
||||
|
||||
describe.skipIf(!run)('federated boot — success path', () => {
|
||||
let sql: ReturnType<typeof postgres> | undefined;
|
||||
|
||||
afterAll(async () => {
|
||||
if (sql) {
|
||||
await sql.end({ timeout: 2 }).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
it('detectAndAssertTier resolves without throwing when federated services are up', async () => {
|
||||
await expect(detectAndAssertTier(federatedConfig)).resolves.toBeUndefined();
|
||||
}, 10_000);
|
||||
|
||||
it('pgvector extension is registered (pg_extension row exists)', async () => {
|
||||
sql = postgres(PG_URL, { max: 1, connect_timeout: 5, idle_timeout: 5 });
|
||||
const rows = await sql`SELECT * FROM pg_extension WHERE extname = 'vector'`;
|
||||
expect(rows).toHaveLength(1);
|
||||
}, 10_000);
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Test C — pgvector extension is functional end-to-end.
|
||||
*
|
||||
* Creates a temp table with a vector(3) column, inserts a row, and queries it
|
||||
* back — confirming the extension is not just registered but operational.
|
||||
*
|
||||
* Prereq: docker compose -f docker-compose.federated.yml --profile federated up -d
|
||||
* Run: FEDERATED_INTEGRATION=1 pnpm --filter @mosaicstack/gateway test src/__tests__/integration/federated-pgvector.integration.test.ts
|
||||
*
|
||||
* Skipped when FEDERATED_INTEGRATION !== '1'.
|
||||
*/
|
||||
|
||||
import postgres from 'postgres';
|
||||
import { afterAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const run = process.env['FEDERATED_INTEGRATION'] === '1';
|
||||
|
||||
const PG_URL = 'postgresql://mosaic:mosaic@localhost:5433/mosaic';
|
||||
|
||||
let sql: ReturnType<typeof postgres> | undefined;
|
||||
|
||||
afterAll(async () => {
|
||||
if (sql) {
|
||||
await sql.end({ timeout: 2 }).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
describe.skipIf(!run)('federated pgvector — functional end-to-end', () => {
|
||||
it('vector ops round-trip: INSERT [1,2,3] and SELECT returns [1,2,3]', async () => {
|
||||
sql = postgres(PG_URL, { max: 1, connect_timeout: 5, idle_timeout: 5 });
|
||||
|
||||
await sql`CREATE TEMP TABLE t (id int, embedding vector(3))`;
|
||||
await sql`INSERT INTO t VALUES (1, '[1,2,3]')`;
|
||||
const rows = await sql`SELECT embedding FROM t`;
|
||||
|
||||
expect(rows).toHaveLength(1);
|
||||
// The postgres driver returns vector columns as strings like '[1,2,3]'.
|
||||
// Normalise by parsing the string representation.
|
||||
const raw = rows[0]?.['embedding'] as string;
|
||||
const parsed = JSON.parse(raw) as number[];
|
||||
expect(parsed).toEqual([1, 2, 3]);
|
||||
}, 10_000);
|
||||
});
|
||||
Reference in New Issue
Block a user