mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-18 13:30:48 +00:00
refactor: split plugin interactive dispatch adapters
This commit is contained in:
@@ -1,13 +1,62 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from "vitest";
|
||||
import * as conversationBinding from "./conversation-binding.js";
|
||||
import {
|
||||
clearPluginInteractiveHandlers,
|
||||
dispatchPluginInteractiveHandler,
|
||||
registerPluginInteractiveHandler,
|
||||
} from "./interactive.js";
|
||||
|
||||
let requestPluginConversationBindingMock: MockInstance<
|
||||
typeof conversationBinding.requestPluginConversationBinding
|
||||
>;
|
||||
let detachPluginConversationBindingMock: MockInstance<
|
||||
typeof conversationBinding.detachPluginConversationBinding
|
||||
>;
|
||||
let getCurrentPluginConversationBindingMock: MockInstance<
|
||||
typeof conversationBinding.getCurrentPluginConversationBinding
|
||||
>;
|
||||
|
||||
describe("plugin interactive handlers", () => {
|
||||
beforeEach(() => {
|
||||
clearPluginInteractiveHandlers();
|
||||
requestPluginConversationBindingMock = vi
|
||||
.spyOn(conversationBinding, "requestPluginConversationBinding")
|
||||
.mockResolvedValue({
|
||||
status: "bound",
|
||||
binding: {
|
||||
bindingId: "binding-1",
|
||||
pluginId: "codex-plugin",
|
||||
pluginName: "Codex",
|
||||
pluginRoot: "/plugins/codex",
|
||||
channel: "telegram",
|
||||
accountId: "default",
|
||||
conversationId: "-10099:topic:77",
|
||||
parentConversationId: "-10099",
|
||||
threadId: 77,
|
||||
boundAt: 1,
|
||||
},
|
||||
});
|
||||
detachPluginConversationBindingMock = vi
|
||||
.spyOn(conversationBinding, "detachPluginConversationBinding")
|
||||
.mockResolvedValue({ removed: true });
|
||||
getCurrentPluginConversationBindingMock = vi
|
||||
.spyOn(conversationBinding, "getCurrentPluginConversationBinding")
|
||||
.mockResolvedValue({
|
||||
bindingId: "binding-1",
|
||||
pluginId: "codex-plugin",
|
||||
pluginName: "Codex",
|
||||
pluginRoot: "/plugins/codex",
|
||||
channel: "telegram",
|
||||
accountId: "default",
|
||||
conversationId: "-10099:topic:77",
|
||||
parentConversationId: "-10099",
|
||||
threadId: 77,
|
||||
boundAt: 1,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("routes Telegram callbacks by namespace and dedupes callback ids", async () => {
|
||||
@@ -213,6 +262,359 @@ describe("plugin interactive handlers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("wires Telegram conversation binding helpers with topic context", async () => {
|
||||
const requestResult = {
|
||||
status: "bound" as const,
|
||||
binding: {
|
||||
bindingId: "binding-telegram",
|
||||
pluginId: "codex-plugin",
|
||||
pluginName: "Codex",
|
||||
pluginRoot: "/plugins/codex",
|
||||
channel: "telegram",
|
||||
accountId: "default",
|
||||
conversationId: "-10099:topic:77",
|
||||
parentConversationId: "-10099",
|
||||
threadId: 77,
|
||||
boundAt: 1,
|
||||
},
|
||||
};
|
||||
const currentBinding = {
|
||||
...requestResult.binding,
|
||||
boundAt: 2,
|
||||
};
|
||||
requestPluginConversationBindingMock.mockResolvedValueOnce(requestResult);
|
||||
getCurrentPluginConversationBindingMock.mockResolvedValueOnce(currentBinding);
|
||||
|
||||
const handler = vi.fn(async (ctx) => {
|
||||
await expect(
|
||||
ctx.requestConversationBinding({
|
||||
summary: "Bind this topic",
|
||||
detachHint: "Use /new to detach",
|
||||
}),
|
||||
).resolves.toEqual(requestResult);
|
||||
await expect(ctx.detachConversationBinding()).resolves.toEqual({ removed: true });
|
||||
await expect(ctx.getCurrentConversationBinding()).resolves.toEqual(currentBinding);
|
||||
return { handled: true };
|
||||
});
|
||||
expect(
|
||||
registerPluginInteractiveHandler(
|
||||
"codex-plugin",
|
||||
{
|
||||
channel: "telegram",
|
||||
namespace: "codex",
|
||||
handler,
|
||||
},
|
||||
{ pluginName: "Codex", pluginRoot: "/plugins/codex" },
|
||||
),
|
||||
).toEqual({ ok: true });
|
||||
|
||||
await expect(
|
||||
dispatchPluginInteractiveHandler({
|
||||
channel: "telegram",
|
||||
data: "codex:bind",
|
||||
callbackId: "cb-bind",
|
||||
ctx: {
|
||||
accountId: "default",
|
||||
callbackId: "cb-bind",
|
||||
conversationId: "-10099:topic:77",
|
||||
parentConversationId: "-10099",
|
||||
senderId: "user-1",
|
||||
senderUsername: "ada",
|
||||
threadId: 77,
|
||||
isGroup: true,
|
||||
isForum: true,
|
||||
auth: { isAuthorizedSender: true },
|
||||
callbackMessage: {
|
||||
messageId: 55,
|
||||
chatId: "-10099",
|
||||
messageText: "Pick a thread",
|
||||
},
|
||||
},
|
||||
respond: {
|
||||
reply: vi.fn(async () => {}),
|
||||
editMessage: vi.fn(async () => {}),
|
||||
editButtons: vi.fn(async () => {}),
|
||||
clearButtons: vi.fn(async () => {}),
|
||||
deleteMessage: vi.fn(async () => {}),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
matched: true,
|
||||
handled: true,
|
||||
duplicate: false,
|
||||
});
|
||||
|
||||
expect(requestPluginConversationBindingMock).toHaveBeenCalledWith({
|
||||
pluginId: "codex-plugin",
|
||||
pluginName: "Codex",
|
||||
pluginRoot: "/plugins/codex",
|
||||
requestedBySenderId: "user-1",
|
||||
conversation: {
|
||||
channel: "telegram",
|
||||
accountId: "default",
|
||||
conversationId: "-10099:topic:77",
|
||||
parentConversationId: "-10099",
|
||||
threadId: 77,
|
||||
},
|
||||
binding: {
|
||||
summary: "Bind this topic",
|
||||
detachHint: "Use /new to detach",
|
||||
},
|
||||
});
|
||||
expect(detachPluginConversationBindingMock).toHaveBeenCalledWith({
|
||||
pluginRoot: "/plugins/codex",
|
||||
conversation: {
|
||||
channel: "telegram",
|
||||
accountId: "default",
|
||||
conversationId: "-10099:topic:77",
|
||||
parentConversationId: "-10099",
|
||||
threadId: 77,
|
||||
},
|
||||
});
|
||||
expect(getCurrentPluginConversationBindingMock).toHaveBeenCalledWith({
|
||||
pluginRoot: "/plugins/codex",
|
||||
conversation: {
|
||||
channel: "telegram",
|
||||
accountId: "default",
|
||||
conversationId: "-10099:topic:77",
|
||||
parentConversationId: "-10099",
|
||||
threadId: 77,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("wires Discord conversation binding helpers with parent channel context", async () => {
|
||||
const requestResult = {
|
||||
status: "bound" as const,
|
||||
binding: {
|
||||
bindingId: "binding-discord",
|
||||
pluginId: "codex-plugin",
|
||||
pluginName: "Codex",
|
||||
pluginRoot: "/plugins/codex",
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
conversationId: "channel-1",
|
||||
parentConversationId: "parent-1",
|
||||
boundAt: 1,
|
||||
},
|
||||
};
|
||||
const currentBinding = {
|
||||
...requestResult.binding,
|
||||
boundAt: 2,
|
||||
};
|
||||
requestPluginConversationBindingMock.mockResolvedValueOnce(requestResult);
|
||||
getCurrentPluginConversationBindingMock.mockResolvedValueOnce(currentBinding);
|
||||
|
||||
const handler = vi.fn(async (ctx) => {
|
||||
await expect(ctx.requestConversationBinding({ summary: "Bind Discord" })).resolves.toEqual(
|
||||
requestResult,
|
||||
);
|
||||
await expect(ctx.detachConversationBinding()).resolves.toEqual({ removed: true });
|
||||
await expect(ctx.getCurrentConversationBinding()).resolves.toEqual(currentBinding);
|
||||
return { handled: true };
|
||||
});
|
||||
expect(
|
||||
registerPluginInteractiveHandler(
|
||||
"codex-plugin",
|
||||
{
|
||||
channel: "discord",
|
||||
namespace: "codex",
|
||||
handler,
|
||||
},
|
||||
{ pluginName: "Codex", pluginRoot: "/plugins/codex" },
|
||||
),
|
||||
).toEqual({ ok: true });
|
||||
|
||||
await expect(
|
||||
dispatchPluginInteractiveHandler({
|
||||
channel: "discord",
|
||||
data: "codex:bind",
|
||||
interactionId: "ix-bind",
|
||||
ctx: {
|
||||
accountId: "default",
|
||||
interactionId: "ix-bind",
|
||||
conversationId: "channel-1",
|
||||
parentConversationId: "parent-1",
|
||||
guildId: "guild-1",
|
||||
senderId: "user-1",
|
||||
senderUsername: "ada",
|
||||
auth: { isAuthorizedSender: true },
|
||||
interaction: {
|
||||
kind: "button",
|
||||
messageId: "message-1",
|
||||
values: ["allow"],
|
||||
},
|
||||
},
|
||||
respond: {
|
||||
acknowledge: vi.fn(async () => {}),
|
||||
reply: vi.fn(async () => {}),
|
||||
followUp: vi.fn(async () => {}),
|
||||
editMessage: vi.fn(async () => {}),
|
||||
clearComponents: vi.fn(async () => {}),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
matched: true,
|
||||
handled: true,
|
||||
duplicate: false,
|
||||
});
|
||||
|
||||
expect(requestPluginConversationBindingMock).toHaveBeenCalledWith({
|
||||
pluginId: "codex-plugin",
|
||||
pluginName: "Codex",
|
||||
pluginRoot: "/plugins/codex",
|
||||
requestedBySenderId: "user-1",
|
||||
conversation: {
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
conversationId: "channel-1",
|
||||
parentConversationId: "parent-1",
|
||||
},
|
||||
binding: {
|
||||
summary: "Bind Discord",
|
||||
},
|
||||
});
|
||||
expect(detachPluginConversationBindingMock).toHaveBeenCalledWith({
|
||||
pluginRoot: "/plugins/codex",
|
||||
conversation: {
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
conversationId: "channel-1",
|
||||
parentConversationId: "parent-1",
|
||||
},
|
||||
});
|
||||
expect(getCurrentPluginConversationBindingMock).toHaveBeenCalledWith({
|
||||
pluginRoot: "/plugins/codex",
|
||||
conversation: {
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
conversationId: "channel-1",
|
||||
parentConversationId: "parent-1",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("wires Slack conversation binding helpers with thread context", async () => {
|
||||
const requestResult = {
|
||||
status: "bound" as const,
|
||||
binding: {
|
||||
bindingId: "binding-slack",
|
||||
pluginId: "codex-plugin",
|
||||
pluginName: "Codex",
|
||||
pluginRoot: "/plugins/codex",
|
||||
channel: "slack",
|
||||
accountId: "default",
|
||||
conversationId: "C123",
|
||||
parentConversationId: "C123",
|
||||
threadId: "1710000000.000100",
|
||||
boundAt: 1,
|
||||
},
|
||||
};
|
||||
const currentBinding = {
|
||||
...requestResult.binding,
|
||||
boundAt: 2,
|
||||
};
|
||||
requestPluginConversationBindingMock.mockResolvedValueOnce(requestResult);
|
||||
getCurrentPluginConversationBindingMock.mockResolvedValueOnce(currentBinding);
|
||||
|
||||
const handler = vi.fn(async (ctx) => {
|
||||
await expect(ctx.requestConversationBinding({ summary: "Bind Slack" })).resolves.toEqual(
|
||||
requestResult,
|
||||
);
|
||||
await expect(ctx.detachConversationBinding()).resolves.toEqual({ removed: true });
|
||||
await expect(ctx.getCurrentConversationBinding()).resolves.toEqual(currentBinding);
|
||||
return { handled: true };
|
||||
});
|
||||
expect(
|
||||
registerPluginInteractiveHandler(
|
||||
"codex-plugin",
|
||||
{
|
||||
channel: "slack",
|
||||
namespace: "codex",
|
||||
handler,
|
||||
},
|
||||
{ pluginName: "Codex", pluginRoot: "/plugins/codex" },
|
||||
),
|
||||
).toEqual({ ok: true });
|
||||
|
||||
await expect(
|
||||
dispatchPluginInteractiveHandler({
|
||||
channel: "slack",
|
||||
data: "codex:bind",
|
||||
interactionId: "slack-bind",
|
||||
ctx: {
|
||||
accountId: "default",
|
||||
interactionId: "slack-bind",
|
||||
conversationId: "C123",
|
||||
parentConversationId: "C123",
|
||||
threadId: "1710000000.000100",
|
||||
senderId: "user-1",
|
||||
senderUsername: "ada",
|
||||
auth: { isAuthorizedSender: true },
|
||||
interaction: {
|
||||
kind: "button",
|
||||
actionId: "codex",
|
||||
blockId: "codex_actions",
|
||||
messageTs: "1710000000.000200",
|
||||
threadTs: "1710000000.000100",
|
||||
value: "bind",
|
||||
selectedValues: ["bind"],
|
||||
selectedLabels: ["Bind"],
|
||||
triggerId: "trigger-1",
|
||||
responseUrl: "https://hooks.slack.test/response",
|
||||
},
|
||||
},
|
||||
respond: {
|
||||
acknowledge: vi.fn(async () => {}),
|
||||
reply: vi.fn(async () => {}),
|
||||
followUp: vi.fn(async () => {}),
|
||||
editMessage: vi.fn(async () => {}),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
matched: true,
|
||||
handled: true,
|
||||
duplicate: false,
|
||||
});
|
||||
|
||||
expect(requestPluginConversationBindingMock).toHaveBeenCalledWith({
|
||||
pluginId: "codex-plugin",
|
||||
pluginName: "Codex",
|
||||
pluginRoot: "/plugins/codex",
|
||||
requestedBySenderId: "user-1",
|
||||
conversation: {
|
||||
channel: "slack",
|
||||
accountId: "default",
|
||||
conversationId: "C123",
|
||||
parentConversationId: "C123",
|
||||
threadId: "1710000000.000100",
|
||||
},
|
||||
binding: {
|
||||
summary: "Bind Slack",
|
||||
},
|
||||
});
|
||||
expect(detachPluginConversationBindingMock).toHaveBeenCalledWith({
|
||||
pluginRoot: "/plugins/codex",
|
||||
conversation: {
|
||||
channel: "slack",
|
||||
accountId: "default",
|
||||
conversationId: "C123",
|
||||
parentConversationId: "C123",
|
||||
threadId: "1710000000.000100",
|
||||
},
|
||||
});
|
||||
expect(getCurrentPluginConversationBindingMock).toHaveBeenCalledWith({
|
||||
pluginRoot: "/plugins/codex",
|
||||
conversation: {
|
||||
channel: "slack",
|
||||
accountId: "default",
|
||||
conversationId: "C123",
|
||||
parentConversationId: "C123",
|
||||
threadId: "1710000000.000100",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not consume dedupe keys when a handler throws", async () => {
|
||||
const handler = vi
|
||||
.fn(async () => ({ handled: true }))
|
||||
|
||||
Reference in New Issue
Block a user