Channels: centralize inbound context contracts
This commit is contained in:
parent
79a8905fa4
commit
70aa9204c0
@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { expectChannelInboundContextContract as expectInboundContextContract } from "../../../../src/channels/plugins/contracts/suites.js";
|
||||
import { inboundCtxCapture as capture } from "../../../../test/helpers/inbound-contract-dispatch-mock.js";
|
||||
import { expectInboundContextContract } from "../../../../test/helpers/inbound-contract.js";
|
||||
import type { DiscordMessagePreflightContext } from "./message-handler.preflight.js";
|
||||
import { processDiscordMessage } from "./message-handler.process.js";
|
||||
import {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { MsgContext } from "../../../../src/auto-reply/templating.js";
|
||||
import { expectInboundContextContract } from "../../../../test/helpers/inbound-contract.js";
|
||||
import { expectChannelInboundContextContract as expectInboundContextContract } from "../../../../src/channels/plugins/contracts/suites.js";
|
||||
import { createSignalEventHandler } from "./event-handler.js";
|
||||
import {
|
||||
createBaseSignalEventHandlerDeps,
|
||||
|
||||
@ -3,10 +3,10 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { App } from "@slack/bolt";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { expectChannelInboundContextContract as expectInboundContextContract } from "../../../../../src/channels/plugins/contracts/suites.js";
|
||||
import type { OpenClawConfig } from "../../../../../src/config/config.js";
|
||||
import { resolveAgentRoute } from "../../../../../src/routing/resolve-route.js";
|
||||
import { resolveThreadSessionKeys } from "../../../../../src/routing/session-key.js";
|
||||
import { expectInboundContextContract } from "../../../../../test/helpers/inbound-contract.js";
|
||||
import type { ResolvedSlackAccount } from "../../accounts.js";
|
||||
import type { SlackMessageEvent } from "../../types.js";
|
||||
import type { SlackMonitorContext } from "../context.js";
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { rm } from "node:fs/promises";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { expectChannelInboundContextContract as expectInboundContextContract } from "../../../src/channels/plugins/contracts/suites.js";
|
||||
import {
|
||||
clearPluginInteractiveHandlers,
|
||||
registerPluginInteractiveHandler,
|
||||
} from "../../../src/plugins/interactive.js";
|
||||
import type { PluginInteractiveTelegramHandlerContext } from "../../../src/plugins/types.js";
|
||||
import { escapeRegExp, formatEnvelopeTimestamp } from "../../../test/helpers/envelope-timestamp.js";
|
||||
import { expectInboundContextContract } from "../../../test/helpers/inbound-contract.js";
|
||||
import {
|
||||
answerCallbackQuerySpy,
|
||||
commandSpy,
|
||||
|
||||
@ -2,7 +2,7 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { expectInboundContextContract } from "../../../../../test/helpers/inbound-contract.js";
|
||||
import { expectChannelInboundContextContract as expectInboundContextContract } from "../../../../../src/channels/plugins/contracts/suites.js";
|
||||
|
||||
let capturedCtx: unknown;
|
||||
let capturedDispatchParams: unknown;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { importFreshModule } from "../../../test/helpers/import-fresh.js";
|
||||
import { expectInboundContextContract } from "../../../test/helpers/inbound-contract.js";
|
||||
import { expectChannelInboundContextContract as expectInboundContextContract } from "../../channels/plugins/contracts/suites.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { inboundCtxCapture } from "../../../../test/helpers/inbound-contract-dispatch-mock.js";
|
||||
import { expectChannelInboundContextContract } from "./suites.js";
|
||||
|
||||
const { processDiscordMessage } =
|
||||
await import("../../../../extensions/discord/src/monitor/message-handler.process.js");
|
||||
const { createBaseDiscordMessageContext, createDiscordDirectMessageContextOverrides } =
|
||||
await import("../../../../extensions/discord/src/monitor/message-handler.test-harness.js");
|
||||
|
||||
describe("discord inbound contract", () => {
|
||||
it("keeps inbound context finalized", async () => {
|
||||
inboundCtxCapture.ctx = undefined;
|
||||
const messageCtx = await createBaseDiscordMessageContext({
|
||||
cfg: { messages: {} },
|
||||
ackReactionScope: "direct",
|
||||
...createDiscordDirectMessageContextOverrides(),
|
||||
});
|
||||
|
||||
await processDiscordMessage(messageCtx);
|
||||
|
||||
expect(inboundCtxCapture.ctx).toBeTruthy();
|
||||
expectChannelInboundContextContract(inboundCtxCapture.ctx!);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,73 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createSignalEventHandler } from "../../../../extensions/signal/src/monitor/event-handler.js";
|
||||
import {
|
||||
createBaseSignalEventHandlerDeps,
|
||||
createSignalReceiveEvent,
|
||||
} from "../../../../extensions/signal/src/monitor/event-handler.test-harness.js";
|
||||
import type { MsgContext } from "../../../auto-reply/templating.js";
|
||||
import { expectChannelInboundContextContract } from "./suites.js";
|
||||
|
||||
const capture = vi.hoisted(() => ({ ctx: undefined as MsgContext | undefined }));
|
||||
const dispatchInboundMessageMock = vi.hoisted(() =>
|
||||
vi.fn(
|
||||
async (params: {
|
||||
ctx: MsgContext;
|
||||
replyOptions?: { onReplyStart?: () => void | Promise<void> };
|
||||
}) => {
|
||||
capture.ctx = params.ctx;
|
||||
await Promise.resolve(params.replyOptions?.onReplyStart?.());
|
||||
return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } };
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
vi.mock("../../../auto-reply/dispatch.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../../auto-reply/dispatch.js")>();
|
||||
return {
|
||||
...actual,
|
||||
dispatchInboundMessage: dispatchInboundMessageMock,
|
||||
dispatchInboundMessageWithDispatcher: dispatchInboundMessageMock,
|
||||
dispatchInboundMessageWithBufferedDispatcher: dispatchInboundMessageMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../../../extensions/signal/src/send.js", () => ({
|
||||
sendMessageSignal: vi.fn(),
|
||||
sendTypingSignal: vi.fn(async () => true),
|
||||
sendReadReceiptSignal: vi.fn(async () => true),
|
||||
}));
|
||||
|
||||
vi.mock("../../../pairing/pairing-store.js", () => ({
|
||||
readChannelAllowFromStore: vi.fn().mockResolvedValue([]),
|
||||
upsertChannelPairingRequest: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("signal inbound contract", () => {
|
||||
beforeEach(() => {
|
||||
capture.ctx = undefined;
|
||||
dispatchInboundMessageMock.mockClear();
|
||||
});
|
||||
|
||||
it("keeps inbound context finalized", async () => {
|
||||
const handler = createSignalEventHandler(
|
||||
createBaseSignalEventHandlerDeps({
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
cfg: { messages: { inbound: { debounceMs: 0 } } } as any,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
await handler(
|
||||
createSignalReceiveEvent({
|
||||
dataMessage: {
|
||||
message: "hi",
|
||||
attachments: [],
|
||||
groupInfo: { groupId: "g1", groupName: "Test Group" },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(capture.ctx).toBeTruthy();
|
||||
expectChannelInboundContextContract(capture.ctx!);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ResolvedSlackAccount } from "../../../../extensions/slack/src/accounts.js";
|
||||
import { prepareSlackMessage } from "../../../../extensions/slack/src/monitor/message-handler/prepare.js";
|
||||
import { createInboundSlackTestContext } from "../../../../extensions/slack/src/monitor/message-handler/prepare.test-helpers.js";
|
||||
import type { SlackMessageEvent } from "../../../../extensions/slack/src/types.js";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import { expectChannelInboundContextContract } from "./suites.js";
|
||||
|
||||
function createSlackAccount(config: ResolvedSlackAccount["config"] = {}): ResolvedSlackAccount {
|
||||
return {
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
botTokenSource: "config",
|
||||
appTokenSource: "config",
|
||||
userTokenSource: "none",
|
||||
config,
|
||||
replyToMode: config.replyToMode,
|
||||
replyToModeByChatType: config.replyToModeByChatType,
|
||||
dm: config.dm,
|
||||
};
|
||||
}
|
||||
|
||||
function createSlackMessage(overrides: Partial<SlackMessageEvent>): SlackMessageEvent {
|
||||
return {
|
||||
channel: "D123",
|
||||
channel_type: "im",
|
||||
user: "U1",
|
||||
text: "hi",
|
||||
ts: "1.000",
|
||||
...overrides,
|
||||
} as SlackMessageEvent;
|
||||
}
|
||||
|
||||
describe("slack inbound contract", () => {
|
||||
it("keeps inbound context finalized", async () => {
|
||||
const ctx = createInboundSlackTestContext({
|
||||
cfg: {
|
||||
channels: { slack: { enabled: true } },
|
||||
} as OpenClawConfig,
|
||||
});
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
ctx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
|
||||
const prepared = await prepareSlackMessage({
|
||||
ctx,
|
||||
account: createSlackAccount(),
|
||||
message: createSlackMessage({}),
|
||||
opts: { source: "message" },
|
||||
});
|
||||
|
||||
expect(prepared).toBeTruthy();
|
||||
expectChannelInboundContextContract(prepared!.ctxPayload);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,60 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
getLoadConfigMock,
|
||||
getOnHandler,
|
||||
onSpy,
|
||||
replySpy,
|
||||
} from "../../../../extensions/telegram/src/bot.create-telegram-bot.test-harness.js";
|
||||
import type { MsgContext } from "../../../auto-reply/templating.js";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import { expectChannelInboundContextContract } from "./suites.js";
|
||||
|
||||
const { createTelegramBot } = await import("../../../../extensions/telegram/src/bot.js");
|
||||
|
||||
describe("telegram inbound contract", () => {
|
||||
const loadConfig = getLoadConfigMock();
|
||||
|
||||
beforeEach(() => {
|
||||
onSpy.mockClear();
|
||||
replySpy.mockClear();
|
||||
loadConfig.mockReturnValue({
|
||||
agents: {
|
||||
defaults: {
|
||||
envelopeTimezone: "utc",
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
groupPolicy: "open",
|
||||
groups: { "*": { requireMention: false } },
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig);
|
||||
});
|
||||
|
||||
it("keeps inbound context finalized", async () => {
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
chat: { id: 42, type: "group", title: "Ops" },
|
||||
text: "hello",
|
||||
date: 1736380800,
|
||||
message_id: 2,
|
||||
from: {
|
||||
id: 99,
|
||||
first_name: "Ada",
|
||||
last_name: "Lovelace",
|
||||
username: "ada",
|
||||
},
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||
});
|
||||
|
||||
const payload = replySpy.mock.calls[0]?.[0] as MsgContext | undefined;
|
||||
expect(payload).toBeTruthy();
|
||||
expectChannelInboundContextContract(payload!);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,97 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { processMessage } from "../../../../extensions/whatsapp/src/auto-reply/monitor/process-message.js";
|
||||
import type { MsgContext } from "../../../auto-reply/templating.js";
|
||||
import { expectChannelInboundContextContract } from "./suites.js";
|
||||
|
||||
const capture = vi.hoisted(() => ({
|
||||
ctx: undefined as MsgContext | undefined,
|
||||
}));
|
||||
|
||||
vi.mock("../../../auto-reply/reply/provider-dispatcher.js", () => ({
|
||||
dispatchReplyWithBufferedBlockDispatcher: vi.fn(async (params: { ctx: MsgContext }) => {
|
||||
capture.ctx = params.ctx;
|
||||
return { queuedFinal: false };
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../../../../extensions/whatsapp/src/auto-reply/monitor/last-route.js", () => ({
|
||||
trackBackgroundTask: (tasks: Set<Promise<unknown>>, task: Promise<unknown>) => {
|
||||
tasks.add(task);
|
||||
void task.finally(() => {
|
||||
tasks.delete(task);
|
||||
});
|
||||
},
|
||||
updateLastRouteInBackground: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../../../extensions/whatsapp/src/auto-reply/deliver-reply.js", () => ({
|
||||
deliverWebReply: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
function makeProcessArgs(sessionStorePath: string) {
|
||||
return {
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
cfg: { messages: {}, session: { store: sessionStorePath } } as any,
|
||||
// oxlint-disable-next-line typescript/no-explicit-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 unknown as Record<string, unknown>,
|
||||
route: {
|
||||
agentId: "main",
|
||||
accountId: "default",
|
||||
sessionKey: "agent:main:whatsapp:group:123",
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
} as any,
|
||||
groupHistoryKey: "123@g.us",
|
||||
groupHistories: new Map(),
|
||||
groupMemberNames: new Map(),
|
||||
connectionId: "conn",
|
||||
verbose: false,
|
||||
maxMediaBytes: 1,
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
replyResolver: (async () => undefined) as any,
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
replyLogger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} } as any,
|
||||
backgroundTasks: new Set<Promise<unknown>>(),
|
||||
rememberSentText: () => {},
|
||||
echoHas: () => false,
|
||||
echoForget: () => {},
|
||||
buildCombinedEchoKey: () => "echo",
|
||||
groupHistory: [],
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe("whatsapp inbound contract", () => {
|
||||
let sessionDir = "";
|
||||
|
||||
afterEach(async () => {
|
||||
capture.ctx = undefined;
|
||||
if (sessionDir) {
|
||||
await fs.rm(sessionDir, { recursive: true, force: true });
|
||||
sessionDir = "";
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps inbound context finalized", async () => {
|
||||
sessionDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-whatsapp-contract-"));
|
||||
const sessionStorePath = path.join(sessionDir, "sessions.json");
|
||||
|
||||
await processMessage(makeProcessArgs(sessionStorePath));
|
||||
|
||||
expect(capture.ctx).toBeTruthy();
|
||||
expectChannelInboundContextContract(capture.ctx!);
|
||||
});
|
||||
});
|
||||
@ -1,19 +0,0 @@
|
||||
import { expect } from "vitest";
|
||||
import type { MsgContext } from "../../src/auto-reply/templating.js";
|
||||
import { normalizeChatType } from "../../src/channels/chat-type.js";
|
||||
import { resolveConversationLabel } from "../../src/channels/conversation-label.js";
|
||||
import { validateSenderIdentity } from "../../src/channels/sender-identity.js";
|
||||
|
||||
export function expectInboundContextContract(ctx: MsgContext) {
|
||||
expect(validateSenderIdentity(ctx)).toEqual([]);
|
||||
|
||||
expect(ctx.Body).toBeTypeOf("string");
|
||||
expect(ctx.BodyForAgent).toBeTypeOf("string");
|
||||
expect(ctx.BodyForCommands).toBeTypeOf("string");
|
||||
|
||||
const chatType = normalizeChatType(ctx.ChatType);
|
||||
if (chatType && chatType !== "direct") {
|
||||
const label = ctx.ConversationLabel?.trim() || resolveConversationLabel(ctx);
|
||||
expect(label).toBeTruthy();
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user