feat(service): outbox relay + thread read endpoint + replay (P1.5, P1 complete)
OutboxRelay: FOR UPDATE SKIP LOCKED claim, in-process bus publish, PENDING-> PUBLISHED with 30s backoff + idempotent-consumer ledger. GET /v1/threads/:id/ messages (policy-scoped). Replay + read specs green; vitest fileParallelism off (shared DB). End-to-end HTTP smoke verified: idempotent ingest, read-back, 400 on missing key. 19 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
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 prisma.iiosOutboxEvent.deleteMany();
|
||||
await prisma.iiosProcessedEvent.deleteMany();
|
||||
await prisma.iiosMessagePart.deleteMany();
|
||||
await prisma.iiosInteraction.deleteMany();
|
||||
await prisma.iiosThreadParticipant.deleteMany();
|
||||
await prisma.iiosThread.deleteMany();
|
||||
await prisma.iiosActorRef.deleteMany();
|
||||
await prisma.iiosSourceHandle.deleteMany();
|
||||
await prisma.iiosChannel.deleteMany();
|
||||
await prisma.iiosScope.deleteMany();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user