diff --git a/src/channels/channel-config.test.ts b/src/channels/channel-config.test.ts index b4f5c4957640..91d7a94951f0 100644 --- a/src/channels/channel-config.test.ts +++ b/src/channels/channel-config.test.ts @@ -1,6 +1,5 @@ // Channel config tests cover channel config normalization and account lookup behavior. import { describe, expect, it } from "vitest"; -import type { MsgContext } from "../auto-reply/templating.js"; import { typedCases } from "../test-utils/typed-cases.js"; import { type ChannelMatchSource, @@ -12,7 +11,6 @@ import { applyChannelMatchMeta, resolveChannelMatchConfig, } from "./channel-config.js"; -import { validateSenderIdentity } from "./sender-identity.js"; describe("buildChannelKeyCandidates", () => { it("dedupes and trims keys", () => { @@ -136,33 +134,6 @@ describe("resolveChannelMatchConfig", () => { }); }); -describe("validateSenderIdentity", () => { - it("allows direct messages without sender fields", () => { - const ctx: MsgContext = { ChatType: "direct" }; - expect(validateSenderIdentity(ctx)).toStrictEqual([]); - }); - - it("requires some sender identity for non-direct chats", () => { - const ctx: MsgContext = { ChatType: "group" }; - expect(validateSenderIdentity(ctx)).toContain( - "missing sender identity (SenderId/SenderName/SenderUsername/SenderE164)", - ); - }); - - it("validates SenderE164 and SenderUsername shape", () => { - const ctx: MsgContext = { - ChatType: "group", - SenderE164: "123", - SenderUsername: "@ada lovelace", - }; - expect(validateSenderIdentity(ctx)).toEqual([ - "invalid SenderE164: 123", - 'SenderUsername should not include "@": @ada lovelace', - "SenderUsername should not include whitespace: @ada lovelace", - ]); - }); -}); - describe("resolveNestedAllowlistDecision", () => { const cases = [ { diff --git a/src/channels/plugins/contracts/test-helpers.ts b/src/channels/plugins/contracts/test-helpers.ts index 6bc9563fb051..f0f421423362 100644 --- a/src/channels/plugins/contracts/test-helpers.ts +++ b/src/channels/plugins/contracts/test-helpers.ts @@ -8,7 +8,6 @@ import type { DispatchFromConfigResult } from "../../../auto-reply/reply/dispatc import type { MsgContext } from "../../../auto-reply/templating.js"; import { normalizeChatType } from "../../chat-type.js"; import { resolveConversationLabel } from "../../conversation-label.js"; -import { validateSenderIdentity } from "../../sender-identity.js"; import { hasFinalChannelTurnDispatch, hasVisibleChannelTurnDispatch, @@ -32,18 +31,45 @@ export function primeChannelOutboundSendMock( } } -export function expectChannelInboundContextContract(ctx: MsgContext) { - expect(validateSenderIdentity(ctx)).toEqual([]); +function normalizeContextString(value: unknown): string { + return typeof value === "string" ? value.trim() : ""; +} +export function expectChannelInboundContextContract(ctx: MsgContext) { expect(ctx.Body).toBeTypeOf("string"); expect(ctx.BodyForAgent).toBeTypeOf("string"); expect(ctx.BodyForCommands).toBeTypeOf("string"); const chatType = normalizeChatType(ctx.ChatType); + if (chatType !== "direct") { + const senderValues = [ + normalizeContextString(ctx.SenderId), + normalizeContextString(ctx.SenderName), + normalizeContextString(ctx.SenderUsername), + normalizeContextString(ctx.SenderE164), + ].filter(Boolean); + expect(senderValues.length).toBeGreaterThan(0); + } + if (chatType && chatType !== "direct") { const label = ctx.ConversationLabel?.trim() || resolveConversationLabel(ctx); expect(label).toBeTruthy(); } + + const senderE164 = normalizeContextString(ctx.SenderE164); + if (senderE164) { + expect(senderE164).toMatch(/^\+\d{3,}$/); + } + + const senderUsername = normalizeContextString(ctx.SenderUsername); + if (senderUsername) { + expect(senderUsername).not.toContain("@"); + expect(senderUsername).not.toMatch(/\s/); + } + + if (ctx.SenderId != null) { + expect(normalizeContextString(ctx.SenderId)).toBeTruthy(); + } } export function expectChannelTurnDispatchResultContract( diff --git a/src/channels/sender-identity.ts b/src/channels/sender-identity.ts deleted file mode 100644 index 588c096b288c..000000000000 --- a/src/channels/sender-identity.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Core sender identity validation for channel contexts before plugins/tools consume them. - * Keep this generic; channel-specific identity extraction belongs in each plugin. - */ -import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; -import type { MsgContext } from "../auto-reply/templating.js"; -import { normalizeChatType } from "./chat-type.js"; - -/** Validates trusted sender identity fields before channel contexts reach plugins/tools. */ -export function validateSenderIdentity(ctx: MsgContext): string[] { - const issues: string[] = []; - - const chatType = normalizeChatType(ctx.ChatType); - const isDirect = chatType === "direct"; - - const senderId = normalizeOptionalString(ctx.SenderId) || ""; - const senderName = normalizeOptionalString(ctx.SenderName) || ""; - const senderUsername = normalizeOptionalString(ctx.SenderUsername) || ""; - const senderE164 = normalizeOptionalString(ctx.SenderE164) || ""; - - if (!isDirect) { - // Group/channel messages need an actor identity distinct from the conversation target; - // direct chats can derive the actor from the peer route. - if (!senderId && !senderName && !senderUsername && !senderE164) { - issues.push("missing sender identity (SenderId/SenderName/SenderUsername/SenderE164)"); - } - } - - if (senderE164) { - // Keep E.164 canonical here so access-group matching does not compare mixed phone formats. - if (!/^\+\d{3,}$/.test(senderE164)) { - issues.push(`invalid SenderE164: ${senderE164}`); - } - } - - if (senderUsername) { - // Usernames are handle tokens, not display names or @mentions. - if (senderUsername.includes("@")) { - issues.push(`SenderUsername should not include "@": ${senderUsername}`); - } - if (/\s/.test(senderUsername)) { - issues.push(`SenderUsername should not include whitespace: ${senderUsername}`); - } - } - - if (ctx.SenderId != null && !senderId) { - issues.push("SenderId is set but empty"); - } - - return issues; -}