feat(iios): email adapter dedup fix + smoke + verification
CI / build (push) Failing after 2m8s

Adds adapter-owned externalEventId extraction to the ChannelAdapter contract
(WebhookAdapter→eventId, EmailAdapter→Message-ID) and uses it in InboundService,
so email replays dedup on Message-ID (previously only payload.eventId worked).
Adds an email-dedup spec + smoke-email.mjs proving inbound → normalized, a reply
joins the same email thread, bad signature rejected, and outbound EMAIL SENT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-03 20:34:26 +05:30
parent f0afca89e3
commit f0ea1ca553
6 changed files with 113 additions and 1 deletions
@@ -113,6 +113,17 @@ describe('Email adapter — inbound + threading', () => {
expect(await prisma.iiosThread.count()).toBe(1); // one email conversation
});
it('redelivered email (same Message-ID) → one raw event, one interaction (dedup)', async () => {
const svc = inbound();
const { body, headers } = signedFixture(emailInboundFixture, SECRET);
await svc.receive('EMAIL', body, headers);
const second = await svc.receive('EMAIL', body, headers);
expect(second.deduped).toBe(true);
await normalizeAll();
expect(await prisma.iiosInboundRawEvent.count()).toBe(1);
expect(await prisma.iiosInteraction.count()).toBe(1);
});
it('bad signature on EMAIL → REJECTED, nothing ingested (fail-closed)', async () => {
const { body } = signedFixture(emailInboundFixture, SECRET);
await expect(inbound().receive('EMAIL', body, { 'x-iios-signature': 'sha256=bad' })).rejects.toThrow();