Files
openclaw/extensions/sms/src/inbound.test.ts
T
Peter Steinberger 0e792b6de3 refactor(channels): centralize inbound orchestration and remove internal compat (#109716)
* refactor(channels): centralize inbound turn orchestration

* refactor(runtime): remove stale compatibility paths

* chore(guards): reject internal deprecated API use

* refactor(channels): simplify core turn planning

* chore(guards): keep deprecated checks boundary-focused

* refactor(memory): keep modern config off compat barrel

* fix(msteams): preserve feedback learning

* test(channels): align modern inbound fixtures

* refactor(channels): finish modern inbound migration

* refactor(channels): tighten core inbound kernel

* fix(channels): preserve turn assembly narrowing

* test(sdk): keep runtime mock binding immutable

* test(matrix): isolate read policy runtime

* test(msteams): mock canonical reply factory

* test(slack): mock core inbound turn dispatch

* test(telegram): inject core session recorder

* test(signal): inject core session recorder

* test(googlechat): assert canonical inbound routing

* test(synology-chat): align core turn fixture

* fix(sdk): preserve direct DM runtime compat

* refactor(channels): own inbound envelope compat in core

* refactor(channels): trim inbound dispatch seams

* refactor(channels): remove redundant async wrappers

* test(synology-chat): type canonical dispatcher mock

* refactor(channels): remove remaining dead compat seams

* chore(sdk): refresh API baseline after rebase

* fix(channels): preserve direct DM identity metadata
2026-07-17 00:56:46 -07:00

180 lines
5.1 KiB
TypeScript

// Sms tests cover inbound plugin behavior.
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it, vi } from "vitest";
import { dispatchSmsInboundEvent, type SmsChannelRuntime } from "./inbound.js";
import type { sendSmsViaTwilio as sendSmsViaTwilioType } from "./twilio.js";
import type { ResolvedSmsAccount } from "./types.js";
const sendSmsViaTwilio = vi.hoisted(() =>
vi.fn<typeof sendSmsViaTwilioType>(async () => ({ sid: "SM-pair", to: "+15551234567" })),
);
vi.mock("./twilio.js", () => ({
sendSmsViaTwilio,
}));
function createAccount(overrides: Partial<ResolvedSmsAccount> = {}): ResolvedSmsAccount {
return {
accountId: "default",
enabled: true,
accountSid: "AC123",
authToken: "secret",
fromNumber: "+15557654321",
messagingServiceSid: "",
defaultTo: "",
webhookPath: "/webhooks/sms",
publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
dangerouslyDisableSignatureValidation: false,
dmPolicy: "pairing",
allowFrom: [],
textChunkLimit: 1500,
...overrides,
};
}
function createRuntime() {
const readAllowFromStore = vi.fn(async () => [] as string[]);
const upsertPairingRequest = vi.fn(async () => ({ code: "PAIR123", created: true }));
const resolveAgentRoute = vi.fn();
const run = vi.fn<
(params: {
adapter: {
ingest: (msg: {
from: string;
to: string;
body: string;
messageSid: string;
accountSid: string;
}) => unknown;
resolveTurn: (
ingested: unknown,
) => Promise<{ route: { agentId: string; sessionKey: string } }>;
};
}) => void
>();
const buildContext = vi.fn();
const resolveStorePath = vi.fn();
const runtime = {
pairing: {
readAllowFromStore,
upsertPairingRequest,
},
routing: {
resolveAgentRoute,
},
inbound: {
run,
buildContext,
},
session: {
resolveStorePath,
recordInboundSession: vi.fn(),
},
reply: {
dispatchReplyWithBufferedBlockDispatcher: vi.fn(),
},
} as unknown as SmsChannelRuntime;
return {
runtime,
readAllowFromStore,
upsertPairingRequest,
resolveAgentRoute,
run,
buildContext,
resolveStorePath,
};
}
describe("dispatchSmsInboundEvent", () => {
it("creates and sends a pairing challenge for first-time SMS senders", async () => {
const { runtime, readAllowFromStore, upsertPairingRequest } = createRuntime();
await dispatchSmsInboundEvent({
cfg: {},
account: createAccount(),
channelRuntime: runtime,
msg: {
from: "+15551234567",
to: "+15557654321",
body: "hello",
messageSid: "SM-inbound",
accountSid: "AC123",
},
});
expect(readAllowFromStore).toHaveBeenCalledWith({
channel: "sms",
accountId: "default",
});
expect(upsertPairingRequest).toHaveBeenCalledWith({
channel: "sms",
accountId: "default",
id: "+15551234567",
meta: undefined,
});
expect(sendSmsViaTwilio).toHaveBeenCalledOnce();
expect(sendSmsViaTwilio).toHaveBeenCalledWith(
expect.objectContaining({
to: "+15551234567",
text: expect.stringContaining("PAIR123"),
}),
);
});
it("uses the canonical routed session key for authorized SMS turns", async () => {
const { runtime, resolveAgentRoute, run, buildContext, resolveStorePath } = createRuntime();
resolveAgentRoute.mockReturnValue({
agentId: "main",
accountId: "default",
sessionKey: "agent:main:sms:direct:+15551234567",
});
buildContext.mockReturnValue({ SessionKey: "agent:main:sms:direct:+15551234567" });
resolveStorePath.mockReturnValue("/tmp/openclaw-sessions");
await dispatchSmsInboundEvent({
cfg: {},
account: createAccount({
dmPolicy: "allowlist",
allowFrom: ["+15551234567"],
}),
channelRuntime: runtime,
msg: {
from: "+15551234567",
to: "+15557654321",
body: "hello",
messageSid: "SM-inbound",
accountSid: "AC123",
},
});
const runParams = expectDefined(run.mock.calls[0]?.[0], "SMS inbound run parameters");
const ingested = runParams.adapter.ingest({
from: "+15551234567",
to: "+15557654321",
body: "hello",
messageSid: "SM-inbound",
accountSid: "AC123",
});
const turn = await runParams.adapter.resolveTurn(ingested);
expect(resolveAgentRoute).toHaveBeenCalledWith(
expect.objectContaining({
peer: { kind: "direct", id: "+15551234567" },
}),
);
expect(buildContext).toHaveBeenCalledWith(
expect.objectContaining({
from: "sms:+15551234567",
sender: expect.objectContaining({ id: "+15551234567" }),
conversation: expect.objectContaining({ id: "+15551234567" }),
reply: { to: "sms:+15551234567" },
route: expect.objectContaining({
routeSessionKey: "agent:main:sms:direct:+15551234567",
dispatchSessionKey: "agent:main:sms:direct:+15551234567",
}),
}),
);
expect(turn.route.sessionKey).toBe("agent:main:sms:direct:+15551234567");
});
});