evOo-local/packages/adapters/src/sendblue-emulator.test.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

175 lines
6.1 KiB
TypeScript

import type { AdapterContext, MessagingInboundEvent } from "@rakazo/adapter-kit";
import { describe, expect, it } from "vitest";
import { ChatSdkMessagingSurface } from "./chat-sdk-surface.js";
import { createEmulatedSendbluePlatform, SendBlueEmulator } from "./sendblue-emulator.js";
const context: AdapterContext = {
operationId: "op-1",
traceId: "trace-1",
spaceId: "ws-1",
userId: "user-1",
signal: new AbortController().signal,
};
/** The production sendblue pipeline (Chat SDK + official SDK) over the emulator. */
function createHarness() {
const emulator = new SendBlueEmulator();
const surface = new ChatSdkMessagingSurface([createEmulatedSendbluePlatform(emulator)]);
const events: MessagingInboundEvent[] = [];
surface.onInbound(async (event) => {
events.push(event);
});
return { emulator, surface, events };
}
describe("emulated sendblue platform inbound", () => {
it("normalizes a DM webhook into a direct inbound message", async () => {
const { emulator, surface, events } = createHarness();
const response = await surface.handleWebhook(
"sendblue",
emulator.buildInboundRequest({
fromNumber: "+15551234567",
content: "hi there",
handle: "handle-dm-1",
}),
);
expect(response?.status).toBe(200);
// handleWebhook drains waitUntil, so the sink has finished by ACK.
expect(events).toHaveLength(1);
expect(events[0]).toEqual({
type: "message",
provider: "sendblue",
handle: "handle-dm-1",
threadId: expect.stringMatching(/^sendblue:/),
isDirect: true,
from: "+15551234567",
fromLabel: null,
channelName: null,
participants: [],
content: "hi there",
mediaUrl: null,
});
// The DM thread id matches what outbound resolution would open.
const dmThreadId = await surface.openDirectThread("sendblue", "+15551234567", context);
expect((events[0] as { threadId: string }).threadId).toBe(dmThreadId);
});
it("normalizes a group webhook with roster and display name", async () => {
const { emulator, surface, events } = createHarness();
const response = await surface.handleWebhook(
"sendblue",
emulator.buildInboundRequest({
fromNumber: "+15551111111",
content: "hello group",
groupId: "grp-1",
groupName: "Family",
participants: ["+15551111111", "+15552222222", emulator.phoneNumber],
}),
);
expect(response?.status).toBe(200);
expect(events).toHaveLength(1);
expect(events[0]).toEqual(
expect.objectContaining({
type: "message",
provider: "sendblue",
threadId: expect.stringMatching(/^sendblue:/),
isDirect: false,
from: "+15551111111",
channelName: "Family",
// The deployment's own line number never appears in the roster.
participants: ["+15551111111", "+15552222222"],
content: "hello group",
}),
);
});
});
describe("emulated sendblue platform outbound", () => {
it("sends a DM through openDirectThread + sendToThread", async () => {
const { emulator, surface } = createHarness();
const threadId = await surface.openDirectThread("sendblue", "+15557654321", context);
const sent = await surface.sendToThread({ threadId, body: "hello you" }, context);
expect(sent.handle).toMatch(/^emulated-handle-/);
expect(emulator.sent).toEqual([
{ kind: "dm", to: "+15557654321", body: "hello you", handle: sent.handle },
]);
});
it("posts to a group via the thread id captured from an inbound group event", async () => {
const { emulator, surface, events } = createHarness();
await surface.handleWebhook(
"sendblue",
emulator.buildInboundRequest({
fromNumber: "+15551111111",
content: "hello group",
groupId: "grp-1",
participants: ["+15551111111", emulator.phoneNumber],
}),
);
expect(events).toHaveLength(1);
const threadId = (events[0] as { threadId: string }).threadId;
const sent = await surface.sendToThread({ threadId, body: "hi all" }, context);
expect(emulator.sent).toEqual([
{ kind: "group", groupId: "grp-1", body: "hi all", handle: sent.handle },
]);
});
it("rejects the send when the vendor API fails", async () => {
const { emulator, surface } = createHarness();
const threadId = await surface.openDirectThread("sendblue", "+15557654321", context);
// The official SDK retries 5xx twice before surfacing the failure.
emulator.failNextSends(3);
await expect(surface.sendToThread({ threadId, body: "will fail" }, context)).rejects.toThrow();
expect(emulator.sent).toHaveLength(0);
});
});
describe("emulated sendblue platform delivery status", () => {
it("surfaces a signed status webhook to the sink", async () => {
const { emulator, surface, events } = createHarness();
const response = await surface.handleWebhook(
"sendblue",
emulator.buildStatusRequest({ handle: "handle-9", status: "DELIVERED" }),
);
expect(response?.status).toBe(200);
expect(events).toEqual([
{ type: "status", provider: "sendblue", handle: "handle-9", status: "DELIVERED" },
]);
});
it("rejects a wrong signing secret with 401 and no sink event", async () => {
const { emulator, surface, events } = createHarness();
const good = emulator.buildStatusRequest({ handle: "handle-9", status: "DELIVERED" });
const forged = new Request(good.url, {
method: "POST",
headers: {
"content-type": "application/json",
"sb-signing-secret": "not-the-secret",
},
body: await good.text(),
});
const response = await surface.handleWebhook("sendblue", forged);
expect(response?.status).toBe(401);
expect(events).toHaveLength(0);
});
});
describe("emulated sendblue platform typing", () => {
it("records DM typing indicators", async () => {
const { emulator, surface } = createHarness();
const threadId = await surface.openDirectThread("sendblue", "+15557654321", context);
await surface.sendTyping(threadId, context);
expect(emulator.typingIndicators).toEqual(["+15557654321"]);
});
});