Files
iios/packages/iios-service/src/outbox/replay.spec.ts
T
maaz519 d3d557766e feat(iios): wire projectors + relay into DLQ (P9 chaos-safe)
Projectors register a replay handler and route bus errors to
dlq.onConsumerFailure (dead-letter + clear the claim-then-fail claim)
instead of swallowing them. Relay dead-letters an outbox event after
IIOS_OUTBOX_MAX_ATTEMPTS instead of retrying forever. Chaos test proves
poison → DLQ → replay resolves while sibling events keep flowing (KG-13/KG-06).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 01:43:57 +05:30

54 lines
2.2 KiB
TypeScript

import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import { PrismaClient } from '@prisma/client';
import { DlqService } from '../outbox/dlq.service';
import { resetDb } from '../test-utils/reset-db';
import { makeFakePorts, portalMessageBasic, replayTwice, isIdempotent } from '@insignia/iios-testkit';
import { IngestService } from '../interactions/ingest.service';
import { OutboxRelay } from './outbox.relay';
import { OutboxBus } from './outbox.bus';
import type { PrismaService } from '../prisma/prisma.service';
const url = process.env.DATABASE_URL ?? 'postgresql://iios:iios@localhost:5434/iios?schema=public';
const prisma = new PrismaClient({ datasources: { db: { url } } });
const asService = prisma as unknown as PrismaService;
async function clean(): Promise<void> {
await resetDb(prisma);
}
beforeAll(async () => { await prisma.$connect(); });
afterAll(async () => { await prisma.$disconnect(); });
beforeEach(async () => { await clean(); });
describe('outbox relay + replay', () => {
it('replaying a fixture twice produces no duplicate state', async () => {
const ingest = new IngestService(asService, makeFakePorts());
const result = await replayTwice(
portalMessageBasic,
(f) => ingest.ingest(f, f.providerEventId!),
() => prisma.iiosInteraction.count(),
);
expect(isIdempotent(result)).toBe(true);
expect(result.countAfterSecond).toBe(1);
});
it('relay publishes exactly one event per interaction, then nothing new', async () => {
const ingest = new IngestService(asService, makeFakePorts());
const bus = new OutboxBus();
const received: string[] = [];
bus.onAny((eventType) => received.push(eventType));
const relay = new OutboxRelay(asService, bus, new DlqService(asService));
await ingest.ingest(portalMessageBasic, portalMessageBasic.providerEventId!);
const published = await relay.relayOnce();
expect(published).toBe(1);
expect(received).toEqual(['com.insignia.iios.interaction.normalized.v1']);
expect(await prisma.iiosOutboxEvent.count({ where: { status: 'PUBLISHED' } })).toBe(1);
expect(await prisma.iiosProcessedEvent.count()).toBe(1);
// A second relay pass finds nothing PENDING.
expect(await relay.relayOnce()).toBe(0);
});
});