fix: finalize inbound contexts
This commit is contained in:
@@ -53,6 +53,7 @@
|
||||
- Discord: truncate skill command descriptions to 100 chars for slash command limits. (#1018) — thanks @evalexpr.
|
||||
- Security: bump `tar` to 7.5.3.
|
||||
- Models: align ZAI thinking toggles.
|
||||
- iMessage/Signal: include sender metadata for non-queued group messages. (#1059)
|
||||
|
||||
## 2026.1.15
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { hasControlCommand } from "../../../../../src/auto-reply/command-detecti
|
||||
import { shouldHandleTextCommands } from "../../../../../src/auto-reply/commands-registry.js";
|
||||
import { formatAgentEnvelope } from "../../../../../src/auto-reply/envelope.js";
|
||||
import { dispatchReplyFromConfig } from "../../../../../src/auto-reply/reply/dispatch-from-config.js";
|
||||
import { finalizeInboundContext } from "../../../../../src/auto-reply/reply/inbound-context.js";
|
||||
import {
|
||||
buildMentionRegexes,
|
||||
matchesMentionPatterns,
|
||||
@@ -354,16 +355,14 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
});
|
||||
|
||||
const groupSystemPrompt = roomConfigInfo.config?.systemPrompt?.trim() || undefined;
|
||||
const ctxPayload = {
|
||||
Body: body,
|
||||
BodyForAgent: body,
|
||||
RawBody: bodyText,
|
||||
CommandBody: bodyText,
|
||||
BodyForCommands: bodyText,
|
||||
From: isDirectMessage ? `matrix:${senderId}` : `matrix:channel:${roomId}`,
|
||||
To: `room:${roomId}`,
|
||||
SessionKey: route.sessionKey,
|
||||
AccountId: route.accountId,
|
||||
const ctxPayload = finalizeInboundContext({
|
||||
Body: body,
|
||||
RawBody: bodyText,
|
||||
CommandBody: bodyText,
|
||||
From: isDirectMessage ? `matrix:${senderId}` : `matrix:channel:${roomId}`,
|
||||
To: `room:${roomId}`,
|
||||
SessionKey: route.sessionKey,
|
||||
AccountId: route.accountId,
|
||||
ChatType: isDirectMessage ? "direct" : "channel",
|
||||
ConversationLabel: envelopeFrom,
|
||||
SenderName: senderName,
|
||||
@@ -382,11 +381,11 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
MediaPath: media?.path,
|
||||
MediaType: media?.contentType,
|
||||
MediaUrl: media?.path,
|
||||
CommandAuthorized: commandAuthorized,
|
||||
CommandSource: "text" as const,
|
||||
OriginatingChannel: "matrix" as const,
|
||||
OriginatingTo: `room:${roomId}`,
|
||||
};
|
||||
CommandAuthorized: commandAuthorized,
|
||||
CommandSource: "text" as const,
|
||||
OriginatingChannel: "matrix" as const,
|
||||
OriginatingTo: `room:${roomId}`,
|
||||
});
|
||||
|
||||
if (isDirectMessage) {
|
||||
const storePath = resolveStorePath(cfg.session?.store, {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
resolveInboundDebounceMs,
|
||||
} from "../../../../src/auto-reply/inbound-debounce.js";
|
||||
import { dispatchReplyFromConfig } from "../../../../src/auto-reply/reply/dispatch-from-config.js";
|
||||
import { finalizeInboundContext } from "../../../../src/auto-reply/reply/inbound-context.js";
|
||||
import {
|
||||
buildPendingHistoryContextFromMap,
|
||||
clearHistoryEntries,
|
||||
@@ -381,16 +382,14 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
});
|
||||
}
|
||||
|
||||
const ctxPayload = {
|
||||
Body: combinedBody,
|
||||
BodyForAgent: combinedBody,
|
||||
RawBody: rawBody,
|
||||
CommandBody: rawBody,
|
||||
BodyForCommands: rawBody,
|
||||
From: teamsFrom,
|
||||
To: teamsTo,
|
||||
SessionKey: route.sessionKey,
|
||||
AccountId: route.accountId,
|
||||
const ctxPayload = finalizeInboundContext({
|
||||
Body: combinedBody,
|
||||
RawBody: rawBody,
|
||||
CommandBody: rawBody,
|
||||
From: teamsFrom,
|
||||
To: teamsTo,
|
||||
SessionKey: route.sessionKey,
|
||||
AccountId: route.accountId,
|
||||
ChatType: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
|
||||
ConversationLabel: envelopeFrom,
|
||||
GroupSubject: !isDirectMessage ? conversationType : undefined,
|
||||
@@ -400,12 +399,12 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
Surface: "msteams" as const,
|
||||
MessageSid: activity.id,
|
||||
Timestamp: timestamp?.getTime() ?? Date.now(),
|
||||
WasMentioned: isDirectMessage || params.wasMentioned || params.implicitMention,
|
||||
CommandAuthorized: true,
|
||||
OriginatingChannel: "msteams" as const,
|
||||
OriginatingTo: teamsTo,
|
||||
...mediaPayload,
|
||||
};
|
||||
WasMentioned: isDirectMessage || params.wasMentioned || params.implicitMention,
|
||||
CommandAuthorized: true,
|
||||
OriginatingChannel: "msteams" as const,
|
||||
OriginatingTo: teamsTo,
|
||||
...mediaPayload,
|
||||
});
|
||||
|
||||
if (shouldLogVerbose()) {
|
||||
logVerbose(`msteams inbound: from=${ctxPayload.From} preview="${preview}"`);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
|
||||
import type { ResolvedZaloAccount } from "./accounts.js";
|
||||
import { finalizeInboundContext } from "../../../src/auto-reply/reply/inbound-context.js";
|
||||
import {
|
||||
ZaloApiError,
|
||||
deleteWebhook,
|
||||
@@ -506,12 +507,10 @@ async function processMessageWithPipeline(params: {
|
||||
body: rawBody,
|
||||
});
|
||||
|
||||
const ctxPayload = {
|
||||
const ctxPayload = finalizeInboundContext({
|
||||
Body: body,
|
||||
BodyForAgent: body,
|
||||
RawBody: rawBody,
|
||||
CommandBody: rawBody,
|
||||
BodyForCommands: rawBody,
|
||||
From: isGroup ? `group:${chatId}` : `zalo:${senderId}`,
|
||||
To: `zalo:${chatId}`,
|
||||
SessionKey: route.sessionKey,
|
||||
@@ -528,7 +527,7 @@ async function processMessageWithPipeline(params: {
|
||||
MediaUrl: mediaPath,
|
||||
OriginatingChannel: "zalo",
|
||||
OriginatingTo: `zalo:${chatId}`,
|
||||
};
|
||||
});
|
||||
|
||||
await deps.dispatchReplyWithBufferedBlockDispatcher({
|
||||
ctx: ctxPayload,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ChildProcess } from "node:child_process";
|
||||
|
||||
import type { RuntimeEnv } from "../../../src/runtime.js";
|
||||
import { finalizeInboundContext } from "../../../src/auto-reply/reply/inbound-context.js";
|
||||
import { loadCoreChannelDeps, type CoreChannelDeps } from "./core-bridge.js";
|
||||
import { sendMessageZalouser } from "./send.js";
|
||||
import type { CoreConfig, ResolvedZalouserAccount, ZcaMessage } from "./types.js";
|
||||
@@ -181,12 +182,10 @@ async function processMessage(
|
||||
body: rawBody,
|
||||
});
|
||||
|
||||
const ctxPayload = {
|
||||
const ctxPayload = finalizeInboundContext({
|
||||
Body: body,
|
||||
BodyForAgent: body,
|
||||
RawBody: rawBody,
|
||||
CommandBody: rawBody,
|
||||
BodyForCommands: rawBody,
|
||||
From: isGroup ? `group:${chatId}` : `zalouser:${senderId}`,
|
||||
To: `zalouser:${chatId}`,
|
||||
SessionKey: route.sessionKey,
|
||||
@@ -200,7 +199,7 @@ async function processMessage(
|
||||
MessageSid: message.msgId ?? `${timestamp}`,
|
||||
OriginatingChannel: "zalouser",
|
||||
OriginatingTo: `zalouser:${chatId}`,
|
||||
};
|
||||
});
|
||||
|
||||
await deps.dispatchReplyWithBufferedBlockDispatcher({
|
||||
ctx: ctxPayload,
|
||||
|
||||
@@ -273,8 +273,10 @@ export async function runPreparedReply(
|
||||
typing.cleanup();
|
||||
return undefined;
|
||||
}
|
||||
const isBareNewOrReset = rawBodyTrimmed === "/new" || rawBodyTrimmed === "/reset";
|
||||
const isBareSessionReset =
|
||||
isNewSession && baseBodyTrimmedRaw.length === 0 && rawBodyTrimmed.length > 0;
|
||||
isNewSession &&
|
||||
((baseBodyTrimmedRaw.length === 0 && rawBodyTrimmed.length > 0) || isBareNewOrReset);
|
||||
const baseBodyFinal = isBareSessionReset ? BARE_SESSION_RESET_PROMPT : baseBody;
|
||||
const baseBodyTrimmed = baseBodyFinal.trim();
|
||||
if (!baseBodyTrimmed) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { resolveDefaultModel } from "./directive-handling.js";
|
||||
import { resolveReplyDirectives } from "./get-reply-directives.js";
|
||||
import { handleInlineActions } from "./get-reply-inline-actions.js";
|
||||
import { runPreparedReply } from "./get-reply-run.js";
|
||||
import { finalizeInboundContext } from "./inbound-context.js";
|
||||
import { initSessionState } from "./session.js";
|
||||
import { stageSandboxMedia } from "./stage-sandbox-media.js";
|
||||
import { createTypingController } from "./typing.js";
|
||||
@@ -74,6 +75,8 @@ export async function getReplyFromConfig(
|
||||
});
|
||||
opts?.onTypingController?.(typing);
|
||||
|
||||
finalizeInboundContext(ctx);
|
||||
|
||||
await applyMediaUnderstanding({
|
||||
ctx,
|
||||
cfg,
|
||||
|
||||
38
src/auto-reply/reply/inbound-context.test.ts
Normal file
38
src/auto-reply/reply/inbound-context.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { MsgContext } from "../templating.js";
|
||||
import { finalizeInboundContext } from "./inbound-context.js";
|
||||
|
||||
describe("finalizeInboundContext", () => {
|
||||
it("fills BodyForAgent/BodyForCommands and normalizes newlines", () => {
|
||||
const ctx: MsgContext = {
|
||||
Body: "a\\nb\r\nc",
|
||||
RawBody: "raw\\nline",
|
||||
ChatType: "room",
|
||||
From: "group:123@g.us",
|
||||
GroupSubject: "Test",
|
||||
};
|
||||
|
||||
const out = finalizeInboundContext(ctx);
|
||||
expect(out.Body).toBe("a\nb\nc");
|
||||
expect(out.RawBody).toBe("raw\nline");
|
||||
expect(out.BodyForAgent).toBe("a\nb\nc");
|
||||
expect(out.BodyForCommands).toBe("raw\nline");
|
||||
expect(out.ChatType).toBe("channel");
|
||||
expect(out.ConversationLabel).toContain("Test");
|
||||
});
|
||||
|
||||
it("can force BodyForCommands to follow updated CommandBody", () => {
|
||||
const ctx: MsgContext = {
|
||||
Body: "base",
|
||||
BodyForCommands: "<media:audio>",
|
||||
CommandBody: "say hi",
|
||||
From: "signal:+15550001111",
|
||||
ChatType: "direct",
|
||||
};
|
||||
|
||||
finalizeInboundContext(ctx, { forceBodyForCommands: true });
|
||||
expect(ctx.BodyForCommands).toBe("say hi");
|
||||
});
|
||||
});
|
||||
|
||||
59
src/auto-reply/reply/inbound-context.ts
Normal file
59
src/auto-reply/reply/inbound-context.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { normalizeChatType } from "../../channels/chat-type.js";
|
||||
import { resolveConversationLabel } from "../../channels/conversation-label.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
import { normalizeInboundTextNewlines } from "./inbound-text.js";
|
||||
|
||||
export type FinalizeInboundContextOptions = {
|
||||
forceBodyForAgent?: boolean;
|
||||
forceBodyForCommands?: boolean;
|
||||
forceChatType?: boolean;
|
||||
forceConversationLabel?: boolean;
|
||||
};
|
||||
|
||||
function normalizeTextField(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") return undefined;
|
||||
return normalizeInboundTextNewlines(value);
|
||||
}
|
||||
|
||||
export function finalizeInboundContext<T extends Record<string, unknown>>(
|
||||
ctx: T,
|
||||
opts: FinalizeInboundContextOptions = {},
|
||||
): T & MsgContext {
|
||||
const normalized = ctx as T & MsgContext;
|
||||
|
||||
normalized.Body = normalizeInboundTextNewlines(
|
||||
typeof normalized.Body === "string" ? normalized.Body : "",
|
||||
);
|
||||
normalized.RawBody = normalizeTextField(normalized.RawBody);
|
||||
normalized.CommandBody = normalizeTextField(normalized.CommandBody);
|
||||
normalized.Transcript = normalizeTextField(normalized.Transcript);
|
||||
normalized.ThreadStarterBody = normalizeTextField(normalized.ThreadStarterBody);
|
||||
|
||||
const chatType = normalizeChatType(normalized.ChatType);
|
||||
if (chatType && (opts.forceChatType || normalized.ChatType !== chatType)) {
|
||||
normalized.ChatType = chatType;
|
||||
}
|
||||
|
||||
const bodyForAgentSource = opts.forceBodyForAgent
|
||||
? normalized.Body
|
||||
: (normalized.BodyForAgent ?? normalized.Body);
|
||||
normalized.BodyForAgent = normalizeInboundTextNewlines(bodyForAgentSource);
|
||||
|
||||
const bodyForCommandsSource = opts.forceBodyForCommands
|
||||
? (normalized.CommandBody ?? normalized.RawBody ?? normalized.Body)
|
||||
: (normalized.BodyForCommands ??
|
||||
normalized.CommandBody ??
|
||||
normalized.RawBody ??
|
||||
normalized.Body);
|
||||
normalized.BodyForCommands = normalizeInboundTextNewlines(bodyForCommandsSource);
|
||||
|
||||
const explicitLabel = normalized.ConversationLabel?.trim();
|
||||
if (opts.forceConversationLabel || !explicitLabel) {
|
||||
const resolved = resolveConversationLabel(normalized)?.trim();
|
||||
if (resolved) normalized.ConversationLabel = resolved;
|
||||
} else {
|
||||
normalized.ConversationLabel = explicitLabel;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
export function normalizeInboundTextNewlines(input: string): string {
|
||||
const text = input.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
|
||||
if (text.includes("\n")) return text;
|
||||
if (!text.includes("\\n")) return text;
|
||||
return text.replaceAll("\\n", "\n");
|
||||
return input.replaceAll("\r\n", "\n").replaceAll("\r", "\n").replaceAll("\\n", "\n");
|
||||
}
|
||||
|
||||
|
||||
90
src/discord/monitor/message-handler.inbound-contract.test.ts
Normal file
90
src/discord/monitor/message-handler.inbound-contract.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { MsgContext } from "../../auto-reply/templating.js";
|
||||
import { expectInboundContextContract } from "../../../test/helpers/inbound-contract.js";
|
||||
|
||||
let capturedCtx: MsgContext | undefined;
|
||||
|
||||
vi.mock("../../auto-reply/reply/dispatch-from-config.js", () => ({
|
||||
dispatchReplyFromConfig: vi.fn(async (params: { ctx: MsgContext }) => {
|
||||
capturedCtx = params.ctx;
|
||||
return { queuedFinal: false, counts: { tool: 0, block: 0 } };
|
||||
}),
|
||||
}));
|
||||
|
||||
import { processDiscordMessage } from "./message-handler.process.js";
|
||||
|
||||
describe("discord processDiscordMessage inbound contract", () => {
|
||||
it("passes a finalized MsgContext to dispatchReplyFromConfig", async () => {
|
||||
capturedCtx = undefined;
|
||||
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-discord-"));
|
||||
const storePath = path.join(dir, "sessions.json");
|
||||
|
||||
await processDiscordMessage({
|
||||
cfg: { messages: {}, session: { store: storePath } } as any,
|
||||
discordConfig: {} as any,
|
||||
accountId: "default",
|
||||
token: "token",
|
||||
runtime: { log: () => {}, error: () => {} } as any,
|
||||
guildHistories: new Map(),
|
||||
historyLimit: 0,
|
||||
mediaMaxBytes: 1024,
|
||||
textLimit: 4000,
|
||||
replyToMode: "off",
|
||||
ackReactionScope: "direct",
|
||||
groupPolicy: "open",
|
||||
data: { guild: null } as any,
|
||||
client: { rest: {} } as any,
|
||||
message: {
|
||||
id: "m1",
|
||||
channelId: "c1",
|
||||
timestamp: new Date().toISOString(),
|
||||
attachments: [],
|
||||
} as any,
|
||||
author: {
|
||||
id: "U1",
|
||||
username: "alice",
|
||||
discriminator: "0",
|
||||
globalName: "Alice",
|
||||
} as any,
|
||||
channelInfo: null,
|
||||
channelName: undefined,
|
||||
isGuildMessage: false,
|
||||
isDirectMessage: true,
|
||||
isGroupDm: false,
|
||||
commandAuthorized: true,
|
||||
baseText: "hi",
|
||||
messageText: "hi",
|
||||
wasMentioned: false,
|
||||
shouldRequireMention: false,
|
||||
canDetectMention: false,
|
||||
effectiveWasMentioned: false,
|
||||
threadChannel: null,
|
||||
threadParentId: undefined,
|
||||
threadParentName: undefined,
|
||||
threadParentType: undefined,
|
||||
threadName: undefined,
|
||||
displayChannelSlug: "",
|
||||
guildInfo: null,
|
||||
guildSlug: "",
|
||||
channelConfig: null,
|
||||
baseSessionKey: "agent:main:discord:dm:U1",
|
||||
route: {
|
||||
agentId: "main",
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
sessionKey: "agent:main:discord:dm:U1",
|
||||
mainSessionKey: "agent:main:main",
|
||||
} as any,
|
||||
} as any);
|
||||
|
||||
expect(capturedCtx).toBeTruthy();
|
||||
expectInboundContextContract(capturedCtx!);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
buildPendingHistoryContextFromMap,
|
||||
clearHistoryEntries,
|
||||
} from "../../auto-reply/reply/history.js";
|
||||
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
|
||||
import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js";
|
||||
import type { ReplyPayload } from "../../auto-reply/types.js";
|
||||
import { resolveStorePath, updateLastRoute } from "../../config/sessions.js";
|
||||
@@ -219,12 +220,10 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
return;
|
||||
}
|
||||
|
||||
const ctxPayload = {
|
||||
const ctxPayload = finalizeInboundContext({
|
||||
Body: combinedBody,
|
||||
BodyForAgent: combinedBody,
|
||||
RawBody: baseText,
|
||||
CommandBody: baseText,
|
||||
BodyForCommands: baseText,
|
||||
From: effectiveFrom,
|
||||
To: effectiveTo,
|
||||
SessionKey: autoThreadContext?.SessionKey ?? threadKeys.sessionKey,
|
||||
@@ -253,7 +252,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
// Originating channel for reply routing.
|
||||
OriginatingChannel: "discord" as const,
|
||||
OriginatingTo: autoThreadContext?.OriginatingTo ?? replyTarget,
|
||||
};
|
||||
});
|
||||
|
||||
if (isDirectMessage) {
|
||||
const sessionCfg = cfg.session;
|
||||
|
||||
@@ -30,6 +30,7 @@ import type {
|
||||
NativeCommandSpec,
|
||||
} from "../../auto-reply/commands-registry.js";
|
||||
import { dispatchReplyWithDispatcher } from "../../auto-reply/reply/provider-dispatcher.js";
|
||||
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
|
||||
import type { ReplyPayload } from "../../auto-reply/types.js";
|
||||
import type { ClawdbotConfig, loadConfig } from "../../config/config.js";
|
||||
import { buildPairingReply } from "../../pairing/pairing-messages.js";
|
||||
@@ -570,11 +571,10 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
},
|
||||
});
|
||||
const conversationLabel = isDirectMessage ? (user.globalName ?? user.username) : channelId;
|
||||
const ctxPayload = {
|
||||
const ctxPayload = finalizeInboundContext({
|
||||
Body: prompt,
|
||||
BodyForAgent: prompt,
|
||||
RawBody: prompt,
|
||||
CommandBody: prompt,
|
||||
BodyForCommands: prompt,
|
||||
CommandArgs: commandArgs,
|
||||
From: isDirectMessage ? `discord:${user.id}` : `group:${channelId}`,
|
||||
To: `slash:${user.id}`,
|
||||
@@ -607,7 +607,7 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
Timestamp: Date.now(),
|
||||
CommandAuthorized: commandAuthorized,
|
||||
CommandSource: "native" as const,
|
||||
};
|
||||
});
|
||||
|
||||
let didReply = false;
|
||||
await dispatchReplyWithDispatcher({
|
||||
|
||||
@@ -378,6 +378,12 @@ describe("monitorIMessageProvider", () => {
|
||||
closeResolve?.();
|
||||
await run;
|
||||
|
||||
expect(replyMock).toHaveBeenCalledOnce();
|
||||
const ctx = replyMock.mock.calls[0]?.[0] as { Body?: string; ChatType?: string };
|
||||
expect(ctx.ChatType).toBe("group");
|
||||
expect(String(ctx.Body ?? "")).toContain("[from:");
|
||||
expect(String(ctx.Body ?? "")).toContain("+15550002222");
|
||||
|
||||
expect(sendMock).toHaveBeenCalledWith(
|
||||
"chat_id:42",
|
||||
"yo",
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
resolveInboundDebounceMs,
|
||||
} from "../../auto-reply/inbound-debounce.js";
|
||||
import { dispatchReplyFromConfig } from "../../auto-reply/reply/dispatch-from-config.js";
|
||||
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
|
||||
import {
|
||||
buildPendingHistoryContextFromMap,
|
||||
clearHistoryEntries,
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
} from "../../auto-reply/reply/history.js";
|
||||
import { buildMentionRegexes, matchesMentionPatterns } from "../../auto-reply/reply/mentions.js";
|
||||
import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.js";
|
||||
import { formatInboundBodyWithSenderMeta } from "../../auto-reply/reply/inbound-sender-meta.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import {
|
||||
resolveChannelGroupPolicy,
|
||||
@@ -387,12 +389,10 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
||||
}
|
||||
|
||||
const imessageTo = (isGroup ? chatTarget : undefined) || `imessage:${sender}`;
|
||||
const ctxPayload = {
|
||||
const ctxPayload = finalizeInboundContext({
|
||||
Body: combinedBody,
|
||||
BodyForAgent: combinedBody,
|
||||
RawBody: bodyText,
|
||||
CommandBody: bodyText,
|
||||
BodyForCommands: bodyText,
|
||||
From: isGroup ? `group:${chatId}` : `imessage:${sender}`,
|
||||
To: imessageTo,
|
||||
SessionKey: route.sessionKey,
|
||||
@@ -416,7 +416,8 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
||||
// Originating channel for reply routing.
|
||||
OriginatingChannel: "imessage" as const,
|
||||
OriginatingTo: imessageTo,
|
||||
};
|
||||
});
|
||||
ctxPayload.Body = formatInboundBodyWithSenderMeta({ ctx: ctxPayload, body: ctxPayload.Body });
|
||||
|
||||
if (!isGroup) {
|
||||
const sessionCfg = cfg.session;
|
||||
|
||||
@@ -81,6 +81,8 @@ describe("applyMediaUnderstanding", () => {
|
||||
expect(ctx.Body).toBe("[Audio]\nTranscript:\ntranscribed text");
|
||||
expect(ctx.CommandBody).toBe("transcribed text");
|
||||
expect(ctx.RawBody).toBe("transcribed text");
|
||||
expect(ctx.BodyForAgent).toBe(ctx.Body);
|
||||
expect(ctx.BodyForCommands).toBe("transcribed text");
|
||||
});
|
||||
|
||||
it("handles URL-only attachments for audio transcription", async () => {
|
||||
@@ -254,6 +256,8 @@ describe("applyMediaUnderstanding", () => {
|
||||
expect(ctx.Body).toBe("[Image]\nUser text:\nshow Dom\nDescription:\nimage description");
|
||||
expect(ctx.CommandBody).toBe("show Dom");
|
||||
expect(ctx.RawBody).toBe("show Dom");
|
||||
expect(ctx.BodyForAgent).toBe(ctx.Body);
|
||||
expect(ctx.BodyForCommands).toBe("show Dom");
|
||||
});
|
||||
|
||||
it("uses shared media models list when capability config is missing", async () => {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import type { MsgContext } from "../auto-reply/templating.js";
|
||||
import { applyTemplate } from "../auto-reply/templating.js";
|
||||
import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
|
||||
import { resolveApiKeyForProvider } from "../agents/model-auth.js";
|
||||
import { ensureClawdbotModelsJson } from "../agents/models-config.js";
|
||||
import { minimaxUnderstandImage } from "../agents/minimax-vlm.js";
|
||||
import { logVerbose, shouldLogVerbose } from "../globals.js";
|
||||
import { runExec } from "../process/exec.js";
|
||||
import type {
|
||||
@@ -449,6 +452,7 @@ export async function applyMediaUnderstanding(params: {
|
||||
ctx.RawBody = originalUserText;
|
||||
}
|
||||
ctx.MediaUnderstanding = [...(ctx.MediaUnderstanding ?? []), ...outputs];
|
||||
finalizeInboundContext(ctx, { forceBodyForAgent: true, forceBodyForCommands: true });
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
66
src/signal/monitor/event-handler.inbound-contract.test.ts
Normal file
66
src/signal/monitor/event-handler.inbound-contract.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { MsgContext } from "../../auto-reply/templating.js";
|
||||
import { expectInboundContextContract } from "../../../test/helpers/inbound-contract.js";
|
||||
|
||||
let capturedCtx: MsgContext | undefined;
|
||||
|
||||
vi.mock("../../auto-reply/reply/dispatch-from-config.js", () => ({
|
||||
dispatchReplyFromConfig: vi.fn(async (params: { ctx: MsgContext }) => {
|
||||
capturedCtx = params.ctx;
|
||||
return { queuedFinal: false, counts: { tool: 0, block: 0 } };
|
||||
}),
|
||||
}));
|
||||
|
||||
import { createSignalEventHandler } from "./event-handler.js";
|
||||
|
||||
describe("signal createSignalEventHandler inbound contract", () => {
|
||||
it("passes a finalized MsgContext to dispatchReplyFromConfig", async () => {
|
||||
capturedCtx = undefined;
|
||||
|
||||
const handler = createSignalEventHandler({
|
||||
runtime: { log: () => {}, error: () => {} } as any,
|
||||
cfg: { messages: { inbound: { debounceMs: 0 } } } as any,
|
||||
baseUrl: "http://localhost",
|
||||
accountId: "default",
|
||||
historyLimit: 0,
|
||||
groupHistories: new Map(),
|
||||
textLimit: 4000,
|
||||
dmPolicy: "open",
|
||||
allowFrom: ["*"],
|
||||
groupAllowFrom: ["*"],
|
||||
groupPolicy: "open",
|
||||
reactionMode: "off",
|
||||
reactionAllowlist: [],
|
||||
mediaMaxBytes: 1024,
|
||||
ignoreAttachments: true,
|
||||
fetchAttachment: async () => null,
|
||||
deliverReplies: async () => {},
|
||||
resolveSignalReactionTargets: () => [],
|
||||
isSignalReactionMessage: () => false as any,
|
||||
shouldEmitSignalReactionNotification: () => false,
|
||||
buildSignalReactionSystemEventText: () => "reaction",
|
||||
});
|
||||
|
||||
await handler({
|
||||
event: "receive",
|
||||
data: JSON.stringify({
|
||||
envelope: {
|
||||
sourceNumber: "+15550001111",
|
||||
sourceName: "Alice",
|
||||
timestamp: 1700000000000,
|
||||
dataMessage: {
|
||||
message: "hi",
|
||||
attachments: [],
|
||||
groupInfo: { groupId: "g1", groupName: "Test Group" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(capturedCtx).toBeTruthy();
|
||||
expectInboundContextContract(capturedCtx!);
|
||||
expect(String(capturedCtx?.Body ?? "")).toContain("[from:");
|
||||
expect(String(capturedCtx?.Body ?? "")).toContain("Alice");
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
buildPendingHistoryContextFromMap,
|
||||
clearHistoryEntries,
|
||||
} from "../../auto-reply/reply/history.js";
|
||||
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
|
||||
import { formatInboundBodyWithSenderMeta } from "../../auto-reply/reply/inbound-sender-meta.js";
|
||||
import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.js";
|
||||
import { resolveStorePath, updateLastRoute } from "../../config/sessions.js";
|
||||
import { danger, logVerbose, shouldLogVerbose } from "../../globals.js";
|
||||
@@ -102,12 +104,10 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
|
||||
},
|
||||
});
|
||||
const signalTo = entry.isGroup ? `group:${entry.groupId}` : `signal:${entry.senderRecipient}`;
|
||||
const ctxPayload = {
|
||||
const ctxPayload = finalizeInboundContext({
|
||||
Body: combinedBody,
|
||||
BodyForAgent: combinedBody,
|
||||
RawBody: entry.bodyText,
|
||||
CommandBody: entry.bodyText,
|
||||
BodyForCommands: entry.bodyText,
|
||||
From: entry.isGroup
|
||||
? `group:${entry.groupId ?? "unknown"}`
|
||||
: `signal:${entry.senderRecipient}`,
|
||||
@@ -129,7 +129,8 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
|
||||
CommandAuthorized: entry.commandAuthorized,
|
||||
OriginatingChannel: "signal" as const,
|
||||
OriginatingTo: signalTo,
|
||||
};
|
||||
});
|
||||
ctxPayload.Body = formatInboundBodyWithSenderMeta({ ctx: ctxPayload, body: ctxPayload.Body });
|
||||
|
||||
if (!entry.isGroup) {
|
||||
const sessionCfg = deps.cfg.session;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { App } from "@slack/bolt";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { ClawdbotConfig } from "../../../config/config.js";
|
||||
import type { RuntimeEnv } from "../../../runtime.js";
|
||||
import { expectInboundContextContract } from "../../../../test/helpers/inbound-contract.js";
|
||||
import type { ResolvedSlackAccount } from "../../accounts.js";
|
||||
import type { SlackMessageEvent } from "../../types.js";
|
||||
import { createSlackMonitorContext } from "../context.js";
|
||||
import { prepareSlackMessage } from "./prepare.js";
|
||||
|
||||
describe("slack prepareSlackMessage inbound contract", () => {
|
||||
it("produces a finalized MsgContext", async () => {
|
||||
const slackCtx = createSlackMonitorContext({
|
||||
cfg: {
|
||||
channels: { slack: { enabled: true } },
|
||||
} as ClawdbotConfig,
|
||||
accountId: "default",
|
||||
botToken: "token",
|
||||
app: { client: {} } as App,
|
||||
runtime: {} as RuntimeEnv,
|
||||
botUserId: "B1",
|
||||
teamId: "T1",
|
||||
apiAppId: "A1",
|
||||
historyLimit: 0,
|
||||
sessionScope: "per-sender",
|
||||
mainKey: "main",
|
||||
dmEnabled: true,
|
||||
dmPolicy: "open",
|
||||
allowFrom: [],
|
||||
groupDmEnabled: true,
|
||||
groupDmChannels: [],
|
||||
defaultRequireMention: true,
|
||||
groupPolicy: "open",
|
||||
useAccessGroups: false,
|
||||
reactionMode: "off",
|
||||
reactionAllowlist: [],
|
||||
replyToMode: "off",
|
||||
threadHistoryScope: "thread",
|
||||
threadInheritParent: false,
|
||||
slashCommand: {
|
||||
enabled: false,
|
||||
name: "clawd",
|
||||
sessionPrefix: "slack:slash",
|
||||
ephemeral: true,
|
||||
},
|
||||
textLimit: 4000,
|
||||
ackReactionScope: "group-mentions",
|
||||
mediaMaxBytes: 1024,
|
||||
removeAckAfterReply: false,
|
||||
});
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" } as any);
|
||||
|
||||
const account: ResolvedSlackAccount = {
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
botTokenSource: "config",
|
||||
appTokenSource: "config",
|
||||
config: {},
|
||||
};
|
||||
|
||||
const message: SlackMessageEvent = {
|
||||
channel: "D123",
|
||||
channel_type: "im",
|
||||
user: "U1",
|
||||
text: "hi",
|
||||
ts: "1.000",
|
||||
} as SlackMessageEvent;
|
||||
|
||||
const prepared = await prepareSlackMessage({
|
||||
ctx: slackCtx,
|
||||
account,
|
||||
message,
|
||||
opts: { source: "message" },
|
||||
});
|
||||
|
||||
expect(prepared).toBeTruthy();
|
||||
expectInboundContextContract(prepared!.ctxPayload as any);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
buildPendingHistoryContextFromMap,
|
||||
recordPendingHistoryEntry,
|
||||
} from "../../../auto-reply/reply/history.js";
|
||||
import { finalizeInboundContext } from "../../../auto-reply/reply/inbound-context.js";
|
||||
import { buildMentionRegexes, matchesMentionPatterns } from "../../../auto-reply/reply/mentions.js";
|
||||
import { logVerbose, shouldLogVerbose } from "../../../globals.js";
|
||||
import { enqueueSystemEvent } from "../../../infra/system-events.js";
|
||||
@@ -404,12 +405,10 @@ export async function prepareSlackMessage(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const ctxPayload = {
|
||||
const ctxPayload = finalizeInboundContext({
|
||||
Body: combinedBody,
|
||||
BodyForAgent: combinedBody,
|
||||
RawBody: rawBody,
|
||||
CommandBody: rawBody,
|
||||
BodyForCommands: rawBody,
|
||||
From: slackFrom,
|
||||
To: slackTo,
|
||||
SessionKey: sessionKey,
|
||||
@@ -435,7 +434,7 @@ export async function prepareSlackMessage(params: {
|
||||
CommandAuthorized: commandAuthorized,
|
||||
OriginatingChannel: "slack" as const,
|
||||
OriginatingTo: slackTo,
|
||||
} satisfies Record<string, unknown>;
|
||||
}) satisfies Record<string, unknown>;
|
||||
|
||||
const replyTarget = ctxPayload.To ?? undefined;
|
||||
if (!replyTarget) return null;
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "../../auto-reply/commands-registry.js";
|
||||
import { listSkillCommandsForAgents } from "../../auto-reply/skill-commands.js";
|
||||
import { dispatchReplyWithDispatcher } from "../../auto-reply/reply/provider-dispatcher.js";
|
||||
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
|
||||
import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "../../config/commands.js";
|
||||
import { danger, logVerbose } from "../../globals.js";
|
||||
import { buildPairingReply } from "../../pairing/pairing-messages.js";
|
||||
@@ -336,11 +337,11 @@ export function registerSlackMonitorSlashCommands(params: {
|
||||
const groupSystemPrompt =
|
||||
systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
|
||||
|
||||
const ctxPayload = {
|
||||
const ctxPayload = finalizeInboundContext({
|
||||
Body: prompt,
|
||||
BodyForAgent: prompt,
|
||||
RawBody: prompt,
|
||||
CommandBody: prompt,
|
||||
CommandArgs: commandArgs,
|
||||
BodyForCommands: prompt,
|
||||
From: isDirectMessage
|
||||
? `slack:${command.user_id}`
|
||||
: isRoom
|
||||
@@ -375,7 +376,7 @@ export function registerSlackMonitorSlashCommands(params: {
|
||||
CommandAuthorized: commandAuthorized,
|
||||
OriginatingChannel: "slack" as const,
|
||||
OriginatingTo: `user:${command.user_id}`,
|
||||
};
|
||||
});
|
||||
|
||||
const { counts } = await dispatchReplyWithDispatcher({
|
||||
ctx: ctxPayload,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
buildPendingHistoryContextFromMap,
|
||||
recordPendingHistoryEntry,
|
||||
} from "../auto-reply/reply/history.js";
|
||||
import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
|
||||
import { buildMentionRegexes, matchesMentionPatterns } from "../auto-reply/reply/mentions.js";
|
||||
import { formatLocationText, toLocationContext } from "../channels/location.js";
|
||||
import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
|
||||
@@ -358,12 +359,10 @@ export const buildTelegramMessageContext = async ({
|
||||
const groupSystemPrompt =
|
||||
systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
|
||||
const commandBody = normalizeCommandBody(rawBody, { botUsername });
|
||||
const ctxPayload = {
|
||||
const ctxPayload = finalizeInboundContext({
|
||||
Body: combinedBody,
|
||||
BodyForAgent: combinedBody,
|
||||
RawBody: rawBody,
|
||||
CommandBody: commandBody,
|
||||
BodyForCommands: commandBody,
|
||||
From: isGroup ? buildTelegramGroupFrom(chatId, resolvedThreadId) : `telegram:${chatId}`,
|
||||
To: `telegram:${chatId}`,
|
||||
SessionKey: route.sessionKey,
|
||||
@@ -399,7 +398,7 @@ export const buildTelegramMessageContext = async ({
|
||||
// Originating channel for reply routing.
|
||||
OriginatingChannel: "telegram" as const,
|
||||
OriginatingTo: `telegram:${chatId}`,
|
||||
};
|
||||
});
|
||||
|
||||
if (replyTarget && shouldLogVerbose()) {
|
||||
const preview = replyTarget.body.replace(/\s+/g, " ").slice(0, 120);
|
||||
|
||||
@@ -13,6 +13,7 @@ import { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js";
|
||||
import type { CommandArgs } from "../auto-reply/commands-registry.js";
|
||||
import { resolveTelegramCustomCommands } from "../config/telegram-custom-commands.js";
|
||||
import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
|
||||
import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
|
||||
import { danger, logVerbose } from "../globals.js";
|
||||
import { resolveAgentRoute } from "../routing/resolve-route.js";
|
||||
import { deliverReplies } from "./bot/delivery.js";
|
||||
@@ -251,11 +252,11 @@ export const registerTelegramNativeCommands = ({
|
||||
const conversationLabel = isGroup
|
||||
? (msg.chat.title ? `${msg.chat.title} id:${chatId}` : `group:${chatId}`)
|
||||
: (buildSenderName(msg) ?? String(senderId || chatId));
|
||||
const ctxPayload = {
|
||||
const ctxPayload = finalizeInboundContext({
|
||||
Body: prompt,
|
||||
BodyForAgent: prompt,
|
||||
RawBody: prompt,
|
||||
CommandBody: prompt,
|
||||
CommandArgs: commandArgs,
|
||||
BodyForCommands: prompt,
|
||||
From: isGroup ? buildTelegramGroupFrom(chatId, resolvedThreadId) : `telegram:${chatId}`,
|
||||
To: `slash:${senderId || chatId}`,
|
||||
ChatType: isGroup ? "group" : "direct",
|
||||
@@ -275,7 +276,7 @@ export const registerTelegramNativeCommands = ({
|
||||
CommandTargetSessionKey: route.sessionKey,
|
||||
MessageThreadId: resolvedThreadId,
|
||||
IsForum: isForum,
|
||||
};
|
||||
});
|
||||
|
||||
const disableBlockStreaming =
|
||||
typeof telegramCfg.blockStreaming === "boolean"
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { expectInboundContextContract } from "../../../../test/helpers/inbound-contract.js";
|
||||
|
||||
let capturedCtx: unknown;
|
||||
|
||||
vi.mock("../../../auto-reply/reply/provider-dispatcher.js", () => ({
|
||||
dispatchReplyWithBufferedBlockDispatcher: vi.fn(async (params: { ctx: unknown }) => {
|
||||
capturedCtx = params.ctx;
|
||||
return { queuedFinal: false };
|
||||
}),
|
||||
}));
|
||||
|
||||
import { processMessage } from "./process-message.js";
|
||||
|
||||
describe("web processMessage inbound contract", () => {
|
||||
it("passes a finalized MsgContext to the dispatcher", async () => {
|
||||
capturedCtx = undefined;
|
||||
|
||||
await processMessage({
|
||||
cfg: { messages: {} } as any,
|
||||
msg: {
|
||||
id: "msg1",
|
||||
from: "123@g.us",
|
||||
to: "+15550001111",
|
||||
chatType: "group",
|
||||
body: "hi",
|
||||
senderName: "Alice",
|
||||
senderJid: "alice@s.whatsapp.net",
|
||||
senderE164: "+15550002222",
|
||||
groupSubject: "Test Group",
|
||||
groupParticipants: [],
|
||||
} as any,
|
||||
route: { agentId: "main", accountId: "default", sessionKey: "agent:main:whatsapp:group:123" } as any,
|
||||
groupHistoryKey: "123@g.us",
|
||||
groupHistories: new Map(),
|
||||
groupMemberNames: new Map(),
|
||||
connectionId: "conn",
|
||||
verbose: false,
|
||||
maxMediaBytes: 1,
|
||||
replyResolver: (async () => undefined) as any,
|
||||
replyLogger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} } as any,
|
||||
backgroundTasks: new Set(),
|
||||
rememberSentText: (_text: string | undefined, _opts: unknown) => {},
|
||||
echoHas: () => false,
|
||||
echoForget: () => {},
|
||||
buildCombinedEchoKey: () => "echo",
|
||||
groupHistory: [],
|
||||
} as any);
|
||||
|
||||
expect(capturedCtx).toBeTruthy();
|
||||
expectInboundContextContract(capturedCtx as any);
|
||||
});
|
||||
});
|
||||
@@ -16,13 +16,13 @@ import {
|
||||
import { dispatchReplyWithBufferedBlockDispatcher } from "../../../auto-reply/reply/provider-dispatcher.js";
|
||||
import type { getReplyFromConfig } from "../../../auto-reply/reply.js";
|
||||
import type { ReplyPayload } from "../../../auto-reply/types.js";
|
||||
import { finalizeInboundContext } from "../../../auto-reply/reply/inbound-context.js";
|
||||
import { toLocationContext } from "../../../channels/location.js";
|
||||
import type { loadConfig } from "../../../config/config.js";
|
||||
import { logVerbose, shouldLogVerbose } from "../../../globals.js";
|
||||
import type { getChildLogger } from "../../../logging.js";
|
||||
import type { resolveAgentRoute } from "../../../routing/resolve-route.js";
|
||||
import { jidToE164, normalizeE164 } from "../../../utils.js";
|
||||
import { normalizeChatType } from "../../../channels/chat-type.js";
|
||||
import { newConnectionId } from "../../reconnect.js";
|
||||
import { formatError } from "../../session.js";
|
||||
import { deliverWebReply } from "../deliver-reply.js";
|
||||
@@ -196,12 +196,10 @@ export async function processMessage(params: {
|
||||
};
|
||||
|
||||
const { queuedFinal } = await dispatchReplyWithBufferedBlockDispatcher({
|
||||
ctx: {
|
||||
ctx: finalizeInboundContext({
|
||||
Body: combinedBody,
|
||||
BodyForAgent: combinedBody,
|
||||
RawBody: params.msg.body,
|
||||
CommandBody: params.msg.body,
|
||||
BodyForCommands: params.msg.body,
|
||||
From: params.msg.from,
|
||||
To: params.msg.to,
|
||||
SessionKey: params.route.sessionKey,
|
||||
@@ -213,7 +211,7 @@ export async function processMessage(params: {
|
||||
MediaPath: params.msg.mediaPath,
|
||||
MediaUrl: params.msg.mediaUrl,
|
||||
MediaType: params.msg.mediaType,
|
||||
ChatType: normalizeChatType(params.msg.chatType) ?? params.msg.chatType,
|
||||
ChatType: params.msg.chatType,
|
||||
ConversationLabel: params.msg.chatType === "group" ? conversationId : params.msg.from,
|
||||
GroupSubject: params.msg.groupSubject,
|
||||
GroupMembers: formatGroupMembers({
|
||||
@@ -230,7 +228,7 @@ export async function processMessage(params: {
|
||||
Surface: "whatsapp",
|
||||
OriginatingChannel: "whatsapp",
|
||||
OriginatingTo: params.msg.from,
|
||||
},
|
||||
}),
|
||||
cfg: params.cfg,
|
||||
replyResolver: params.replyResolver,
|
||||
dispatcherOptions: {
|
||||
|
||||
163
test/inbound-contract.providers.test.ts
Normal file
163
test/inbound-contract.providers.test.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { describe, it } from "vitest";
|
||||
|
||||
import type { MsgContext } from "../src/auto-reply/templating.js";
|
||||
import { finalizeInboundContext } from "../src/auto-reply/reply/inbound-context.js";
|
||||
import { expectInboundContextContract } from "./helpers/inbound-contract.js";
|
||||
|
||||
describe("inbound context contract (providers + extensions)", () => {
|
||||
const cases: Array<{ name: string; ctx: MsgContext }> = [
|
||||
{
|
||||
name: "whatsapp group",
|
||||
ctx: {
|
||||
Provider: "whatsapp",
|
||||
Surface: "whatsapp",
|
||||
ChatType: "group",
|
||||
From: "123@g.us",
|
||||
To: "+15550001111",
|
||||
Body: "[WhatsApp 123@g.us] hi",
|
||||
RawBody: "hi",
|
||||
CommandBody: "hi",
|
||||
SenderName: "Alice",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "telegram group",
|
||||
ctx: {
|
||||
Provider: "telegram",
|
||||
Surface: "telegram",
|
||||
ChatType: "group",
|
||||
From: "group:123",
|
||||
To: "telegram:123",
|
||||
Body: "[Telegram group:123] hi",
|
||||
RawBody: "hi",
|
||||
CommandBody: "hi",
|
||||
GroupSubject: "Telegram Group",
|
||||
SenderName: "Alice",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "slack channel",
|
||||
ctx: {
|
||||
Provider: "slack",
|
||||
Surface: "slack",
|
||||
ChatType: "channel",
|
||||
From: "slack:channel:C123",
|
||||
To: "channel:C123",
|
||||
Body: "[Slack #general] hi",
|
||||
RawBody: "hi",
|
||||
CommandBody: "hi",
|
||||
GroupSubject: "#general",
|
||||
SenderName: "Alice",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "discord channel",
|
||||
ctx: {
|
||||
Provider: "discord",
|
||||
Surface: "discord",
|
||||
ChatType: "channel",
|
||||
From: "group:123",
|
||||
To: "channel:123",
|
||||
Body: "[Discord #general] hi",
|
||||
RawBody: "hi",
|
||||
CommandBody: "hi",
|
||||
GroupSubject: "#general",
|
||||
SenderName: "Alice",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "signal dm",
|
||||
ctx: {
|
||||
Provider: "signal",
|
||||
Surface: "signal",
|
||||
ChatType: "direct",
|
||||
From: "signal:+15550001111",
|
||||
To: "signal:+15550002222",
|
||||
Body: "[Signal] hi",
|
||||
RawBody: "hi",
|
||||
CommandBody: "hi",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "imessage group",
|
||||
ctx: {
|
||||
Provider: "imessage",
|
||||
Surface: "imessage",
|
||||
ChatType: "group",
|
||||
From: "group:chat_id:123",
|
||||
To: "chat_id:123",
|
||||
Body: "[iMessage Group] hi",
|
||||
RawBody: "hi",
|
||||
CommandBody: "hi",
|
||||
GroupSubject: "iMessage Group",
|
||||
SenderName: "Alice",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "matrix channel",
|
||||
ctx: {
|
||||
Provider: "matrix",
|
||||
Surface: "matrix",
|
||||
ChatType: "channel",
|
||||
From: "matrix:channel:!room:example.org",
|
||||
To: "room:!room:example.org",
|
||||
Body: "[Matrix] hi",
|
||||
RawBody: "hi",
|
||||
CommandBody: "hi",
|
||||
GroupSubject: "#general",
|
||||
SenderName: "Alice",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "msteams channel",
|
||||
ctx: {
|
||||
Provider: "msteams",
|
||||
Surface: "msteams",
|
||||
ChatType: "channel",
|
||||
From: "msteams:channel:19:abc@thread.tacv2",
|
||||
To: "msteams:channel:19:abc@thread.tacv2",
|
||||
Body: "[Teams] hi",
|
||||
RawBody: "hi",
|
||||
CommandBody: "hi",
|
||||
GroupSubject: "Teams Channel",
|
||||
SenderName: "Alice",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "zalo dm",
|
||||
ctx: {
|
||||
Provider: "zalo",
|
||||
Surface: "zalo",
|
||||
ChatType: "direct",
|
||||
From: "zalo:123",
|
||||
To: "zalo:123",
|
||||
Body: "[Zalo] hi",
|
||||
RawBody: "hi",
|
||||
CommandBody: "hi",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "zalouser group",
|
||||
ctx: {
|
||||
Provider: "zalouser",
|
||||
Surface: "zalouser",
|
||||
ChatType: "group",
|
||||
From: "group:123",
|
||||
To: "zalouser:123",
|
||||
Body: "[Zalo Personal] hi",
|
||||
RawBody: "hi",
|
||||
CommandBody: "hi",
|
||||
GroupSubject: "Zalouser Group",
|
||||
SenderName: "Alice",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const entry of cases) {
|
||||
it(entry.name, () => {
|
||||
const ctx = finalizeInboundContext({ ...entry.ctx });
|
||||
expectInboundContextContract(ctx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user