fix(ci): repair sms channel checks

This commit is contained in:
Peter Steinberger
2026-05-31 05:02:10 -04:00
parent 84b025eb62
commit 3a4943ef87
8 changed files with 62 additions and 33 deletions

View File

@@ -29,7 +29,7 @@ function parseList(raw: string | string[] | undefined): string[] {
return [];
}
return (Array.isArray(raw) ? raw : normalizeStringEntries(raw.split(",")))
.map((entry) => normalizeSmsAllowFrom(String(entry)))
.map((entry) => normalizeSmsAllowFrom(entry))
.filter(Boolean);
}
@@ -83,17 +83,21 @@ export function resolveSmsAccount(
): ResolvedSmsAccount {
const channelCfg = getChannelConfig(cfg) ?? {};
const id = normalizeOptionalAccountId(accountId) ?? resolveDefaultSmsAccountId(cfg);
const accountConfig = resolveAccountEntry(
channelCfg.accounts as
| Record<string, Partial<Record<string, unknown> & SmsChannelConfig>>
| undefined,
id,
);
const accountConfig = resolveAccountEntry(channelCfg.accounts, id);
const channelConfig: Record<string, unknown> & SmsChannelConfig = { ...channelCfg };
const accountEntries:
| Record<string, Partial<Record<string, unknown> & SmsChannelConfig>>
| undefined = channelCfg.accounts
? Object.fromEntries(
Object.entries(channelCfg.accounts).map(([accountKey, account]) => [
accountKey,
{ ...account },
]),
)
: undefined;
const merged = resolveMergedAccountConfig<Record<string, unknown> & SmsChannelConfig>({
channelConfig: channelCfg as Record<string, unknown> & SmsChannelConfig,
accounts: channelCfg.accounts as
| Record<string, Partial<Record<string, unknown> & SmsChannelConfig>>
| undefined,
channelConfig,
accounts: accountEntries,
accountId: id,
omitKeys: ["defaultAccount"],
});
@@ -115,8 +119,8 @@ export function resolveSmsAccount(
? process.env.SMS_DANGEROUSLY_DISABLE_SIGNATURE_VALIDATION
: undefined;
const webhookPath = String(merged.webhookPath ?? envWebhookPath ?? DEFAULT_WEBHOOK_PATH).trim();
const publicWebhookUrl = String(merged.publicWebhookUrl ?? envPublicWebhookUrl ?? "").trim();
const webhookPath = (merged.webhookPath ?? envWebhookPath ?? DEFAULT_WEBHOOK_PATH).trim();
const publicWebhookUrl = (merged.publicWebhookUrl ?? envPublicWebhookUrl ?? "").trim();
const authToken =
normalizeResolvedSecretInputString({
value: merged.authToken ?? envAuthToken,
@@ -128,11 +132,11 @@ export function resolveSmsAccount(
return {
accountId: id,
enabled: channelCfg.enabled !== false && accountConfig?.enabled !== false,
accountSid: String(merged.accountSid ?? envAccountSid ?? "").trim(),
accountSid: (merged.accountSid ?? envAccountSid ?? "").trim(),
authToken,
fromNumber: normalizeSmsPhoneNumber(String(merged.fromNumber ?? envFromNumber ?? "")),
messagingServiceSid: String(merged.messagingServiceSid ?? envMessagingServiceSid ?? "").trim(),
defaultTo: normalizeSmsPhoneNumber(String(merged.defaultTo ?? "")),
fromNumber: normalizeSmsPhoneNumber(merged.fromNumber ?? envFromNumber ?? ""),
messagingServiceSid: (merged.messagingServiceSid ?? envMessagingServiceSid ?? "").trim(),
defaultTo: normalizeSmsPhoneNumber(merged.defaultTo ?? ""),
webhookPath: webhookPath || DEFAULT_WEBHOOK_PATH,
publicWebhookUrl,
dangerouslyDisableSignatureValidation:

View File

@@ -103,15 +103,15 @@ function applySmsAccountConfig(params: {
input: Record<string, unknown>;
}): OpenClawConfig {
const patch = smsSetupPatch(params.input);
const channels = { ...(params.cfg.channels ?? {}) };
const current = { ...((channels[CHANNEL_ID] as Record<string, unknown> | undefined) ?? {}) };
const channels = { ...params.cfg.channels };
const current = { ...(channels[CHANNEL_ID] as Record<string, unknown> | undefined) };
if (params.accountId === DEFAULT_ACCOUNT_ID) {
channels[CHANNEL_ID] = { ...current, ...patch };
return { ...params.cfg, channels };
}
const accounts = { ...((current.accounts as Record<string, unknown> | undefined) ?? {}) };
const accounts = { ...(current.accounts as Record<string, unknown> | undefined) };
accounts[params.accountId] = {
...((accounts[params.accountId] as Record<string, unknown> | undefined) ?? {}),
...(accounts[params.accountId] as Record<string, unknown> | undefined),
...patch,
};
channels[CHANNEL_ID] = { ...current, accounts };
@@ -254,6 +254,12 @@ export const smsPlugin: ChannelPlugin<ResolvedSmsAccount> = createChatChannelPlu
},
},
status: {
buildAccountSnapshot: ({ account }) => ({
accountId: account.accountId,
name: account.fromNumber || account.messagingServiceSid || "SMS",
configured: isSmsAccountConfigured(account),
enabled: account.enabled,
}),
buildCapabilitiesDiagnostics: async ({ account }) => ({
lines: collectSmsStartupWarnings(account).map((text) => ({ text, tone: "warn" })),
}),

View File

@@ -6,7 +6,7 @@ import type { ResolvedSmsAccount } from "./types.js";
const registerPluginHttpRoute = vi.hoisted(() => vi.fn(() => vi.fn()));
vi.mock("openclaw/plugin-sdk/webhook-ingress", async (importOriginal) => ({
...((await importOriginal()) as Record<string, unknown>),
...(await importOriginal<typeof import("openclaw/plugin-sdk/webhook-ingress")>()),
registerPluginHttpRoute,
}));

View File

@@ -1,9 +1,10 @@
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(async () => ({ sid: "SM-pair", to: "+15551234567" })),
vi.fn<typeof sendSmsViaTwilioType>(async () => ({ sid: "SM-pair", to: "+15551234567" })),
);
vi.mock("./twilio.js", () => ({

View File

@@ -29,6 +29,16 @@ function createAccount(overrides: Partial<ResolvedSmsAccount> = {}): ResolvedSms
};
}
function readUrlEncodedRequestBody(init: RequestInit | undefined): URLSearchParams {
if (typeof init?.body === "string") {
return new URLSearchParams(init.body);
}
if (init?.body instanceof URLSearchParams) {
return init.body;
}
throw new Error("Expected Twilio request body to be URL-encoded.");
}
describe("Twilio SMS helpers", () => {
it("parses Twilio form bodies and inbound messages", () => {
const form = parseTwilioFormBody(
@@ -105,7 +115,7 @@ describe("Twilio SMS helpers", () => {
});
it("sends SMS through Twilio's Messages API", async () => {
const fetchImpl = vi.fn(
const fetchImpl = vi.fn<typeof fetch>(
async () =>
new Response(
JSON.stringify({
@@ -156,14 +166,14 @@ describe("Twilio SMS helpers", () => {
authorization: `Basic ${Buffer.from("AC123:secret").toString("base64")}`,
"content-type": "application/x-www-form-urlencoded",
});
const body = new URLSearchParams(String(init?.body));
const body = readUrlEncodedRequestBody(init);
expect(body.get("From")).toBe("+15557654321");
expect(body.get("To")).toBe("+15551234567");
expect(body.get("Body")).toBe("hello");
});
it("can send through a Twilio Messaging Service SID", async () => {
const fetchImpl = vi.fn(
const fetchImpl = vi.fn<typeof fetch>(
async () =>
new Response(JSON.stringify({ sid: "SM789" }), {
status: 201,
@@ -193,14 +203,14 @@ describe("Twilio SMS helpers", () => {
});
const [, init] = fetchImpl.mock.calls[0] ?? [];
const body = new URLSearchParams(String(init?.body));
const body = readUrlEncodedRequestBody(init);
expect(body.get("MessagingServiceSid")).toBe("MG123");
expect(body.get("To")).toBe("+15551234567");
expect(body.get("Body")).toBe("hello");
});
it("prefers an explicit from number when both sender options are resolved", async () => {
const fetchImpl = vi.fn(
const fetchImpl = vi.fn<typeof fetch>(
async () =>
new Response(JSON.stringify({ sid: "SM999" }), {
status: 201,
@@ -230,7 +240,7 @@ describe("Twilio SMS helpers", () => {
});
const [, init] = fetchImpl.mock.calls[0] ?? [];
const body = new URLSearchParams(String(init?.body));
const body = readUrlEncodedRequestBody(init);
expect(body.get("From")).toBe("+15557654321");
expect(body.get("MessagingServiceSid")).toBeNull();
});

View File

@@ -76,7 +76,7 @@ function parseTwilioSuccessPayload(text: string): TwilioMessagePayload {
if (err instanceof Error && err.message === "Twilio SMS send returned malformed JSON.") {
throw err;
}
throw new Error("Twilio SMS send returned malformed JSON.");
throw new Error("Twilio SMS send returned malformed JSON.", { cause: err });
}
}
@@ -145,7 +145,7 @@ export function computeTwilioSignature(params: {
const data =
params.url +
Object.keys(params.form)
.sort()
.toSorted()
.map((key) => `${key}${params.form[key] ?? ""}`)
.join("");
return createHmac("sha1", params.authToken).update(data).digest("base64");

View File

@@ -4,5 +4,13 @@
"rootDir": "."
},
"include": ["./*.ts", "./src/**/*.ts"],
"exclude": ["./**/*.test.ts", "./dist/**", "./node_modules/**"]
"exclude": [
"./**/*.test.ts",
"./dist/**",
"./node_modules/**",
"./src/test-support/**",
"./src/**/*test-helpers.ts",
"./src/**/*test-harness.ts",
"./src/**/*test-support.ts"
]
}

View File

@@ -357,7 +357,7 @@ describe("gateway server agent", () => {
const res = await rpcReq(ws, "agent", {
message: "hi",
sessionKey: "main",
channel: "sms",
channel: "missing-channel",
idempotencyKey: "idem-agent-bad-channel",
});
expect(res.ok).toBe(false);