mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
refactor: dedupe command dispatch and process poll tests
This commit is contained in:
@@ -37,24 +37,46 @@ function createBackgroundSession(id: string): ProcessSession {
|
||||
};
|
||||
}
|
||||
|
||||
function createProcessSessionHarness(sessionId: string) {
|
||||
const processTool = createProcessTool();
|
||||
const session = createBackgroundSession(sessionId);
|
||||
addSession(session);
|
||||
return { processTool, session };
|
||||
}
|
||||
|
||||
async function pollSession(
|
||||
processTool: ReturnType<typeof createProcessTool>,
|
||||
callId: string,
|
||||
sessionId: string,
|
||||
timeout?: number | string,
|
||||
) {
|
||||
return processTool.execute(callId, {
|
||||
action: "poll",
|
||||
sessionId,
|
||||
...(timeout === undefined ? {} : { timeout }),
|
||||
});
|
||||
}
|
||||
|
||||
function retryMs(result: Awaited<ReturnType<ReturnType<typeof createProcessTool>["execute"]>>) {
|
||||
return (result.details as { retryInMs?: number }).retryInMs;
|
||||
}
|
||||
|
||||
function pollStatus(result: Awaited<ReturnType<ReturnType<typeof createProcessTool>["execute"]>>) {
|
||||
return (result.details as { status?: string }).status;
|
||||
}
|
||||
|
||||
test("process poll waits for completion when timeout is provided", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const processTool = createProcessTool();
|
||||
const sessionId = "sess";
|
||||
const session = createBackgroundSession(sessionId);
|
||||
addSession(session);
|
||||
const { processTool, session } = createProcessSessionHarness(sessionId);
|
||||
|
||||
setTimeout(() => {
|
||||
appendOutput(session, "stdout", "done\n");
|
||||
markExited(session, 0, null, "completed");
|
||||
}, 10);
|
||||
|
||||
const pollPromise = processTool.execute("toolcall", {
|
||||
action: "poll",
|
||||
sessionId,
|
||||
timeout: 2000,
|
||||
});
|
||||
const pollPromise = pollSession(processTool, "toolcall", sessionId, 2000);
|
||||
|
||||
let resolved = false;
|
||||
void pollPromise.finally(() => {
|
||||
@@ -77,20 +99,14 @@ test("process poll waits for completion when timeout is provided", async () => {
|
||||
test("process poll accepts string timeout values", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const processTool = createProcessTool();
|
||||
const sessionId = "sess-2";
|
||||
const session = createBackgroundSession(sessionId);
|
||||
addSession(session);
|
||||
const { processTool, session } = createProcessSessionHarness(sessionId);
|
||||
setTimeout(() => {
|
||||
appendOutput(session, "stdout", "done\n");
|
||||
markExited(session, 0, null, "completed");
|
||||
}, 10);
|
||||
|
||||
const pollPromise = processTool.execute("toolcall", {
|
||||
action: "poll",
|
||||
sessionId,
|
||||
timeout: "2000",
|
||||
});
|
||||
const pollPromise = pollSession(processTool, "toolcall", sessionId, "2000");
|
||||
await vi.advanceTimersByTimeAsync(350);
|
||||
const poll = await pollPromise;
|
||||
const details = poll.details as { status?: string; aggregated?: string };
|
||||
@@ -102,77 +118,39 @@ test("process poll accepts string timeout values", async () => {
|
||||
});
|
||||
|
||||
test("process poll exposes adaptive retryInMs for repeated no-output polls", async () => {
|
||||
const processTool = createProcessTool();
|
||||
const sessionId = "sess-retry";
|
||||
const session = createBackgroundSession(sessionId);
|
||||
addSession(session);
|
||||
const { processTool } = createProcessSessionHarness(sessionId);
|
||||
|
||||
const poll1 = await processTool.execute("toolcall-1", {
|
||||
action: "poll",
|
||||
sessionId,
|
||||
});
|
||||
const poll2 = await processTool.execute("toolcall-2", {
|
||||
action: "poll",
|
||||
sessionId,
|
||||
});
|
||||
const poll3 = await processTool.execute("toolcall-3", {
|
||||
action: "poll",
|
||||
sessionId,
|
||||
});
|
||||
const poll4 = await processTool.execute("toolcall-4", {
|
||||
action: "poll",
|
||||
sessionId,
|
||||
});
|
||||
const poll5 = await processTool.execute("toolcall-5", {
|
||||
action: "poll",
|
||||
sessionId,
|
||||
});
|
||||
const polls = await Promise.all([
|
||||
pollSession(processTool, "toolcall-1", sessionId),
|
||||
pollSession(processTool, "toolcall-2", sessionId),
|
||||
pollSession(processTool, "toolcall-3", sessionId),
|
||||
pollSession(processTool, "toolcall-4", sessionId),
|
||||
pollSession(processTool, "toolcall-5", sessionId),
|
||||
]);
|
||||
|
||||
expect((poll1.details as { retryInMs?: number }).retryInMs).toBe(5000);
|
||||
expect((poll2.details as { retryInMs?: number }).retryInMs).toBe(10000);
|
||||
expect((poll3.details as { retryInMs?: number }).retryInMs).toBe(30000);
|
||||
expect((poll4.details as { retryInMs?: number }).retryInMs).toBe(60000);
|
||||
expect((poll5.details as { retryInMs?: number }).retryInMs).toBe(60000);
|
||||
expect(polls.map((poll) => retryMs(poll))).toEqual([5000, 10000, 30000, 60000, 60000]);
|
||||
});
|
||||
|
||||
test("process poll resets retryInMs when output appears and clears on completion", async () => {
|
||||
const processTool = createProcessTool();
|
||||
const sessionId = "sess-reset";
|
||||
const session = createBackgroundSession(sessionId);
|
||||
addSession(session);
|
||||
const { processTool, session } = createProcessSessionHarness(sessionId);
|
||||
|
||||
const poll1 = await processTool.execute("toolcall-1", {
|
||||
action: "poll",
|
||||
sessionId,
|
||||
});
|
||||
const poll2 = await processTool.execute("toolcall-2", {
|
||||
action: "poll",
|
||||
sessionId,
|
||||
});
|
||||
expect((poll1.details as { retryInMs?: number }).retryInMs).toBe(5000);
|
||||
expect((poll2.details as { retryInMs?: number }).retryInMs).toBe(10000);
|
||||
const poll1 = await pollSession(processTool, "toolcall-1", sessionId);
|
||||
const poll2 = await pollSession(processTool, "toolcall-2", sessionId);
|
||||
expect(retryMs(poll1)).toBe(5000);
|
||||
expect(retryMs(poll2)).toBe(10000);
|
||||
|
||||
appendOutput(session, "stdout", "step complete\n");
|
||||
const pollWithOutput = await processTool.execute("toolcall-output", {
|
||||
action: "poll",
|
||||
sessionId,
|
||||
});
|
||||
expect((pollWithOutput.details as { retryInMs?: number }).retryInMs).toBe(5000);
|
||||
const pollWithOutput = await pollSession(processTool, "toolcall-output", sessionId);
|
||||
expect(retryMs(pollWithOutput)).toBe(5000);
|
||||
|
||||
markExited(session, 0, null, "completed");
|
||||
const pollCompleted = await processTool.execute("toolcall-completed", {
|
||||
action: "poll",
|
||||
sessionId,
|
||||
});
|
||||
const completedDetails = pollCompleted.details as { status?: string; retryInMs?: number };
|
||||
expect(completedDetails.status).toBe("completed");
|
||||
expect(completedDetails.retryInMs).toBeUndefined();
|
||||
const pollCompleted = await pollSession(processTool, "toolcall-completed", sessionId);
|
||||
expect(pollStatus(pollCompleted)).toBe("completed");
|
||||
expect(retryMs(pollCompleted)).toBeUndefined();
|
||||
|
||||
const pollFinished = await processTool.execute("toolcall-finished", {
|
||||
action: "poll",
|
||||
sessionId,
|
||||
});
|
||||
const finishedDetails = pollFinished.details as { status?: string; retryInMs?: number };
|
||||
expect(finishedDetails.status).toBe("completed");
|
||||
expect(finishedDetails.retryInMs).toBeUndefined();
|
||||
const pollFinished = await pollSession(processTool, "toolcall-finished", sessionId);
|
||||
expect(pollStatus(pollFinished)).toBe("completed");
|
||||
expect(retryMs(pollFinished)).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -30,6 +30,39 @@ const { telegramMessageActions } = await import("./telegram.js");
|
||||
const { signalMessageActions } = await import("./signal.js");
|
||||
const { createSlackActions } = await import("../slack.actions.js");
|
||||
|
||||
function telegramCfg(): OpenClawConfig {
|
||||
return { channels: { telegram: { botToken: "tok" } } } as OpenClawConfig;
|
||||
}
|
||||
|
||||
function slackHarness() {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
const actions = createSlackActions("slack");
|
||||
return { cfg, actions };
|
||||
}
|
||||
|
||||
type SlackActionInput = Parameters<
|
||||
NonNullable<ReturnType<typeof createSlackActions>["handleAction"]>
|
||||
>[0];
|
||||
|
||||
async function runSlackAction(
|
||||
action: SlackActionInput["action"],
|
||||
params: SlackActionInput["params"],
|
||||
) {
|
||||
const { cfg, actions } = slackHarness();
|
||||
await actions.handleAction?.({
|
||||
channel: "slack",
|
||||
action,
|
||||
cfg,
|
||||
params,
|
||||
});
|
||||
return { cfg, actions };
|
||||
}
|
||||
|
||||
function expectFirstSlackAction(expected: Record<string, unknown>) {
|
||||
const [params] = handleSlackAction.mock.calls[0] ?? [];
|
||||
expect(params).toMatchObject(expected);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
@@ -309,14 +342,14 @@ describe("handleDiscordMessageAction", () => {
|
||||
|
||||
describe("telegramMessageActions", () => {
|
||||
it("excludes sticker actions when not enabled", () => {
|
||||
const cfg = { channels: { telegram: { botToken: "tok" } } } as OpenClawConfig;
|
||||
const cfg = telegramCfg();
|
||||
const actions = telegramMessageActions.listActions?.({ cfg }) ?? [];
|
||||
expect(actions).not.toContain("sticker");
|
||||
expect(actions).not.toContain("sticker-search");
|
||||
});
|
||||
|
||||
it("allows media-only sends and passes asVoice", async () => {
|
||||
const cfg = { channels: { telegram: { botToken: "tok" } } } as OpenClawConfig;
|
||||
const cfg = telegramCfg();
|
||||
|
||||
await telegramMessageActions.handleAction?.({
|
||||
channel: "telegram",
|
||||
@@ -343,7 +376,7 @@ describe("telegramMessageActions", () => {
|
||||
});
|
||||
|
||||
it("passes silent flag for silent sends", async () => {
|
||||
const cfg = { channels: { telegram: { botToken: "tok" } } } as OpenClawConfig;
|
||||
const cfg = telegramCfg();
|
||||
|
||||
await telegramMessageActions.handleAction?.({
|
||||
channel: "telegram",
|
||||
@@ -369,7 +402,7 @@ describe("telegramMessageActions", () => {
|
||||
});
|
||||
|
||||
it("maps edit action params into editMessage", async () => {
|
||||
const cfg = { channels: { telegram: { botToken: "tok" } } } as OpenClawConfig;
|
||||
const cfg = telegramCfg();
|
||||
|
||||
await telegramMessageActions.handleAction?.({
|
||||
channel: "telegram",
|
||||
@@ -398,7 +431,7 @@ describe("telegramMessageActions", () => {
|
||||
});
|
||||
|
||||
it("rejects non-integer messageId for edit before reaching telegram-actions", async () => {
|
||||
const cfg = { channels: { telegram: { botToken: "tok" } } } as OpenClawConfig;
|
||||
const cfg = telegramCfg();
|
||||
const handleAction = telegramMessageActions.handleAction;
|
||||
if (!handleAction) {
|
||||
throw new Error("telegram handleAction unavailable");
|
||||
@@ -473,7 +506,7 @@ describe("telegramMessageActions", () => {
|
||||
});
|
||||
|
||||
it("accepts numeric messageId and channelId for reactions", async () => {
|
||||
const cfg = { channels: { telegram: { botToken: "tok" } } } as OpenClawConfig;
|
||||
const cfg = telegramCfg();
|
||||
|
||||
await telegramMessageActions.handleAction?.({
|
||||
channel: "telegram",
|
||||
@@ -500,7 +533,7 @@ describe("telegramMessageActions", () => {
|
||||
});
|
||||
|
||||
it("maps topic-create params into createForumTopic", async () => {
|
||||
const cfg = { channels: { telegram: { botToken: "tok" } } } as OpenClawConfig;
|
||||
const cfg = telegramCfg();
|
||||
|
||||
await telegramMessageActions.handleAction?.({
|
||||
channel: "telegram",
|
||||
@@ -678,21 +711,12 @@ describe("signalMessageActions", () => {
|
||||
|
||||
describe("slack actions adapter", () => {
|
||||
it("forwards threadId for read", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
const actions = createSlackActions("slack");
|
||||
|
||||
await actions.handleAction?.({
|
||||
channel: "slack",
|
||||
action: "read",
|
||||
cfg,
|
||||
params: {
|
||||
channelId: "C1",
|
||||
threadId: "171234.567",
|
||||
},
|
||||
await runSlackAction("read", {
|
||||
channelId: "C1",
|
||||
threadId: "171234.567",
|
||||
});
|
||||
|
||||
const [params] = handleSlackAction.mock.calls[0] ?? [];
|
||||
expect(params).toMatchObject({
|
||||
expectFirstSlackAction({
|
||||
action: "readMessages",
|
||||
channelId: "C1",
|
||||
threadId: "171234.567",
|
||||
@@ -700,42 +724,24 @@ describe("slack actions adapter", () => {
|
||||
});
|
||||
|
||||
it("forwards normalized limit for emoji-list", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
const actions = createSlackActions("slack");
|
||||
|
||||
await actions.handleAction?.({
|
||||
channel: "slack",
|
||||
action: "emoji-list",
|
||||
cfg,
|
||||
params: {
|
||||
limit: "2.9",
|
||||
},
|
||||
await runSlackAction("emoji-list", {
|
||||
limit: "2.9",
|
||||
});
|
||||
|
||||
const [params] = handleSlackAction.mock.calls[0] ?? [];
|
||||
expect(params).toMatchObject({
|
||||
expectFirstSlackAction({
|
||||
action: "emojiList",
|
||||
limit: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards blocks JSON for send", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
const actions = createSlackActions("slack");
|
||||
|
||||
await actions.handleAction?.({
|
||||
channel: "slack",
|
||||
action: "send",
|
||||
cfg,
|
||||
params: {
|
||||
to: "channel:C1",
|
||||
message: "",
|
||||
blocks: JSON.stringify([{ type: "divider" }]),
|
||||
},
|
||||
await runSlackAction("send", {
|
||||
to: "channel:C1",
|
||||
message: "",
|
||||
blocks: JSON.stringify([{ type: "divider" }]),
|
||||
});
|
||||
|
||||
const [params] = handleSlackAction.mock.calls[0] ?? [];
|
||||
expect(params).toMatchObject({
|
||||
expectFirstSlackAction({
|
||||
action: "sendMessage",
|
||||
to: "channel:C1",
|
||||
content: "",
|
||||
@@ -744,22 +750,13 @@ describe("slack actions adapter", () => {
|
||||
});
|
||||
|
||||
it("forwards blocks arrays for send", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
const actions = createSlackActions("slack");
|
||||
|
||||
await actions.handleAction?.({
|
||||
channel: "slack",
|
||||
action: "send",
|
||||
cfg,
|
||||
params: {
|
||||
to: "channel:C1",
|
||||
message: "",
|
||||
blocks: [{ type: "section", text: { type: "mrkdwn", text: "hi" } }],
|
||||
},
|
||||
await runSlackAction("send", {
|
||||
to: "channel:C1",
|
||||
message: "",
|
||||
blocks: [{ type: "section", text: { type: "mrkdwn", text: "hi" } }],
|
||||
});
|
||||
|
||||
const [params] = handleSlackAction.mock.calls[0] ?? [];
|
||||
expect(params).toMatchObject({
|
||||
expectFirstSlackAction({
|
||||
action: "sendMessage",
|
||||
to: "channel:C1",
|
||||
content: "",
|
||||
@@ -768,8 +765,7 @@ describe("slack actions adapter", () => {
|
||||
});
|
||||
|
||||
it("rejects invalid blocks JSON for send", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
const actions = createSlackActions("slack");
|
||||
const { cfg, actions } = slackHarness();
|
||||
|
||||
await expect(
|
||||
actions.handleAction?.({
|
||||
@@ -787,8 +783,7 @@ describe("slack actions adapter", () => {
|
||||
});
|
||||
|
||||
it("rejects empty blocks arrays for send", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
const actions = createSlackActions("slack");
|
||||
const { cfg, actions } = slackHarness();
|
||||
|
||||
await expect(
|
||||
actions.handleAction?.({
|
||||
@@ -806,8 +801,7 @@ describe("slack actions adapter", () => {
|
||||
});
|
||||
|
||||
it("rejects send when both blocks and media are provided", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
const actions = createSlackActions("slack");
|
||||
const { cfg, actions } = slackHarness();
|
||||
|
||||
await expect(
|
||||
actions.handleAction?.({
|
||||
@@ -826,23 +820,14 @@ describe("slack actions adapter", () => {
|
||||
});
|
||||
|
||||
it("forwards blocks JSON for edit", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
const actions = createSlackActions("slack");
|
||||
|
||||
await actions.handleAction?.({
|
||||
channel: "slack",
|
||||
action: "edit",
|
||||
cfg,
|
||||
params: {
|
||||
channelId: "C1",
|
||||
messageId: "171234.567",
|
||||
message: "",
|
||||
blocks: JSON.stringify([{ type: "divider" }]),
|
||||
},
|
||||
await runSlackAction("edit", {
|
||||
channelId: "C1",
|
||||
messageId: "171234.567",
|
||||
message: "",
|
||||
blocks: JSON.stringify([{ type: "divider" }]),
|
||||
});
|
||||
|
||||
const [params] = handleSlackAction.mock.calls[0] ?? [];
|
||||
expect(params).toMatchObject({
|
||||
expectFirstSlackAction({
|
||||
action: "editMessage",
|
||||
channelId: "C1",
|
||||
messageId: "171234.567",
|
||||
@@ -852,23 +837,14 @@ describe("slack actions adapter", () => {
|
||||
});
|
||||
|
||||
it("forwards blocks arrays for edit", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
const actions = createSlackActions("slack");
|
||||
|
||||
await actions.handleAction?.({
|
||||
channel: "slack",
|
||||
action: "edit",
|
||||
cfg,
|
||||
params: {
|
||||
channelId: "C1",
|
||||
messageId: "171234.567",
|
||||
message: "",
|
||||
blocks: [{ type: "section", text: { type: "mrkdwn", text: "updated" } }],
|
||||
},
|
||||
await runSlackAction("edit", {
|
||||
channelId: "C1",
|
||||
messageId: "171234.567",
|
||||
message: "",
|
||||
blocks: [{ type: "section", text: { type: "mrkdwn", text: "updated" } }],
|
||||
});
|
||||
|
||||
const [params] = handleSlackAction.mock.calls[0] ?? [];
|
||||
expect(params).toMatchObject({
|
||||
expectFirstSlackAction({
|
||||
action: "editMessage",
|
||||
channelId: "C1",
|
||||
messageId: "171234.567",
|
||||
@@ -878,8 +854,7 @@ describe("slack actions adapter", () => {
|
||||
});
|
||||
|
||||
it("rejects edit when both message and blocks are missing", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as OpenClawConfig;
|
||||
const actions = createSlackActions("slack");
|
||||
const { cfg, actions } = slackHarness();
|
||||
|
||||
await expect(
|
||||
actions.handleAction?.({
|
||||
|
||||
@@ -266,6 +266,71 @@ function createArgMenusHarness() {
|
||||
return { commands, actions, options, postEphemeral, ctx, account };
|
||||
}
|
||||
|
||||
function requireHandler(
|
||||
handlers: Map<string, (args: unknown) => Promise<void>>,
|
||||
key: string,
|
||||
label: string,
|
||||
): (args: unknown) => Promise<void> {
|
||||
const handler = handlers.get(key);
|
||||
if (!handler) {
|
||||
throw new Error(`Missing ${label} handler`);
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
function createSlashCommand(overrides: Partial<Record<string, string>> = {}) {
|
||||
return {
|
||||
user_id: "U1",
|
||||
user_name: "Ada",
|
||||
channel_id: "C1",
|
||||
channel_name: "directmessage",
|
||||
text: "",
|
||||
trigger_id: "t1",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function runCommandHandler(handler: (args: unknown) => Promise<void>) {
|
||||
const respond = vi.fn().mockResolvedValue(undefined);
|
||||
const ack = vi.fn().mockResolvedValue(undefined);
|
||||
await handler({
|
||||
command: createSlashCommand(),
|
||||
ack,
|
||||
respond,
|
||||
});
|
||||
return { respond, ack };
|
||||
}
|
||||
|
||||
async function runArgMenuAction(
|
||||
handler: (args: unknown) => Promise<void>,
|
||||
params: {
|
||||
action: Record<string, unknown>;
|
||||
userId?: string;
|
||||
userName?: string;
|
||||
channelId?: string;
|
||||
channelName?: string;
|
||||
respond?: ReturnType<typeof vi.fn>;
|
||||
includeRespond?: boolean;
|
||||
},
|
||||
) {
|
||||
const includeRespond = params.includeRespond ?? true;
|
||||
const respond = params.respond ?? vi.fn().mockResolvedValue(undefined);
|
||||
const payload: Record<string, unknown> = {
|
||||
ack: vi.fn().mockResolvedValue(undefined),
|
||||
action: params.action,
|
||||
body: {
|
||||
user: { id: params.userId ?? "U1", name: params.userName ?? "Ada" },
|
||||
channel: { id: params.channelId ?? "C1", name: params.channelName ?? "directmessage" },
|
||||
trigger_id: "t1",
|
||||
},
|
||||
};
|
||||
if (includeRespond) {
|
||||
payload.respond = respond;
|
||||
}
|
||||
await handler(payload);
|
||||
return respond;
|
||||
}
|
||||
|
||||
describe("Slack native command argument menus", () => {
|
||||
let harness: ReturnType<typeof createArgMenusHarness>;
|
||||
let usageHandler: (args: unknown) => Promise<void>;
|
||||
@@ -280,48 +345,14 @@ describe("Slack native command argument menus", () => {
|
||||
beforeAll(async () => {
|
||||
harness = createArgMenusHarness();
|
||||
await registerCommands(harness.ctx, harness.account);
|
||||
|
||||
const usage = harness.commands.get("/usage");
|
||||
if (!usage) {
|
||||
throw new Error("Missing /usage handler");
|
||||
}
|
||||
usageHandler = usage;
|
||||
const report = harness.commands.get("/report");
|
||||
if (!report) {
|
||||
throw new Error("Missing /report handler");
|
||||
}
|
||||
reportHandler = report;
|
||||
const reportCompact = harness.commands.get("/reportcompact");
|
||||
if (!reportCompact) {
|
||||
throw new Error("Missing /reportcompact handler");
|
||||
}
|
||||
reportCompactHandler = reportCompact;
|
||||
const reportExternal = harness.commands.get("/reportexternal");
|
||||
if (!reportExternal) {
|
||||
throw new Error("Missing /reportexternal handler");
|
||||
}
|
||||
reportExternalHandler = reportExternal;
|
||||
const reportLong = harness.commands.get("/reportlong");
|
||||
if (!reportLong) {
|
||||
throw new Error("Missing /reportlong handler");
|
||||
}
|
||||
reportLongHandler = reportLong;
|
||||
const unsafeConfirm = harness.commands.get("/unsafeconfirm");
|
||||
if (!unsafeConfirm) {
|
||||
throw new Error("Missing /unsafeconfirm handler");
|
||||
}
|
||||
unsafeConfirmHandler = unsafeConfirm;
|
||||
|
||||
const argMenu = harness.actions.get("openclaw_cmdarg");
|
||||
if (!argMenu) {
|
||||
throw new Error("Missing arg-menu action handler");
|
||||
}
|
||||
argMenuHandler = argMenu;
|
||||
const argMenuOptions = harness.options.get("openclaw_cmdarg");
|
||||
if (!argMenuOptions) {
|
||||
throw new Error("Missing arg-menu options handler");
|
||||
}
|
||||
argMenuOptionsHandler = argMenuOptions;
|
||||
usageHandler = requireHandler(harness.commands, "/usage", "/usage");
|
||||
reportHandler = requireHandler(harness.commands, "/report", "/report");
|
||||
reportCompactHandler = requireHandler(harness.commands, "/reportcompact", "/reportcompact");
|
||||
reportExternalHandler = requireHandler(harness.commands, "/reportexternal", "/reportexternal");
|
||||
reportLongHandler = requireHandler(harness.commands, "/reportlong", "/reportlong");
|
||||
unsafeConfirmHandler = requireHandler(harness.commands, "/unsafeconfirm", "/unsafeconfirm");
|
||||
argMenuHandler = requireHandler(harness.actions, "openclaw_cmdarg", "arg-menu action");
|
||||
argMenuOptionsHandler = requireHandler(harness.options, "openclaw_cmdarg", "arg-menu options");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -329,21 +360,7 @@ describe("Slack native command argument menus", () => {
|
||||
});
|
||||
|
||||
it("shows a button menu when required args are omitted", async () => {
|
||||
const respond = vi.fn().mockResolvedValue(undefined);
|
||||
const ack = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
await usageHandler({
|
||||
command: {
|
||||
user_id: "U1",
|
||||
user_name: "Ada",
|
||||
channel_id: "C1",
|
||||
channel_name: "directmessage",
|
||||
text: "",
|
||||
trigger_id: "t1",
|
||||
},
|
||||
ack,
|
||||
respond,
|
||||
});
|
||||
const { respond } = await runCommandHandler(usageHandler);
|
||||
|
||||
expect(respond).toHaveBeenCalledTimes(1);
|
||||
const payload = respond.mock.calls[0]?.[0] as { blocks?: Array<{ type: string }> };
|
||||
@@ -357,21 +374,7 @@ describe("Slack native command argument menus", () => {
|
||||
});
|
||||
|
||||
it("shows a static_select menu when choices exceed button row size", async () => {
|
||||
const respond = vi.fn().mockResolvedValue(undefined);
|
||||
const ack = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
await reportHandler({
|
||||
command: {
|
||||
user_id: "U1",
|
||||
user_name: "Ada",
|
||||
channel_id: "C1",
|
||||
channel_name: "directmessage",
|
||||
text: "",
|
||||
trigger_id: "t1",
|
||||
},
|
||||
ack,
|
||||
respond,
|
||||
});
|
||||
const { respond } = await runCommandHandler(reportHandler);
|
||||
|
||||
expect(respond).toHaveBeenCalledTimes(1);
|
||||
const payload = respond.mock.calls[0]?.[0] as { blocks?: Array<{ type: string }> };
|
||||
@@ -386,21 +389,7 @@ describe("Slack native command argument menus", () => {
|
||||
});
|
||||
|
||||
it("falls back to buttons when static_select value limit would be exceeded", async () => {
|
||||
const respond = vi.fn().mockResolvedValue(undefined);
|
||||
const ack = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
await reportLongHandler({
|
||||
command: {
|
||||
user_id: "U1",
|
||||
user_name: "Ada",
|
||||
channel_id: "C1",
|
||||
channel_name: "directmessage",
|
||||
text: "",
|
||||
trigger_id: "t1",
|
||||
},
|
||||
ack,
|
||||
respond,
|
||||
});
|
||||
const { respond } = await runCommandHandler(reportLongHandler);
|
||||
|
||||
expect(respond).toHaveBeenCalledTimes(1);
|
||||
const payload = respond.mock.calls[0]?.[0] as { blocks?: Array<{ type: string }> };
|
||||
@@ -411,21 +400,7 @@ describe("Slack native command argument menus", () => {
|
||||
});
|
||||
|
||||
it("shows an overflow menu when choices fit compact range", async () => {
|
||||
const respond = vi.fn().mockResolvedValue(undefined);
|
||||
const ack = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
await reportCompactHandler({
|
||||
command: {
|
||||
user_id: "U1",
|
||||
user_name: "Ada",
|
||||
channel_id: "C1",
|
||||
channel_name: "directmessage",
|
||||
text: "",
|
||||
trigger_id: "t1",
|
||||
},
|
||||
ack,
|
||||
respond,
|
||||
});
|
||||
const { respond } = await runCommandHandler(reportCompactHandler);
|
||||
|
||||
expect(respond).toHaveBeenCalledTimes(1);
|
||||
const payload = respond.mock.calls[0]?.[0] as { blocks?: Array<{ type: string }> };
|
||||
@@ -437,21 +412,7 @@ describe("Slack native command argument menus", () => {
|
||||
});
|
||||
|
||||
it("escapes mrkdwn characters in confirm dialog text", async () => {
|
||||
const respond = vi.fn().mockResolvedValue(undefined);
|
||||
const ack = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
await unsafeConfirmHandler({
|
||||
command: {
|
||||
user_id: "U1",
|
||||
user_name: "Ada",
|
||||
channel_id: "C1",
|
||||
channel_name: "directmessage",
|
||||
text: "",
|
||||
trigger_id: "t1",
|
||||
},
|
||||
ack,
|
||||
respond,
|
||||
});
|
||||
const { respond } = await runCommandHandler(unsafeConfirmHandler);
|
||||
|
||||
expect(respond).toHaveBeenCalledTimes(1);
|
||||
const payload = respond.mock.calls[0]?.[0] as { blocks?: Array<{ type: string }> };
|
||||
@@ -465,18 +426,10 @@ describe("Slack native command argument menus", () => {
|
||||
});
|
||||
|
||||
it("dispatches the command when a menu button is clicked", async () => {
|
||||
const respond = vi.fn().mockResolvedValue(undefined);
|
||||
await argMenuHandler({
|
||||
ack: vi.fn().mockResolvedValue(undefined),
|
||||
await runArgMenuAction(argMenuHandler, {
|
||||
action: {
|
||||
value: encodeValue({ command: "usage", arg: "mode", value: "tokens", userId: "U1" }),
|
||||
},
|
||||
body: {
|
||||
user: { id: "U1", name: "Ada" },
|
||||
channel: { id: "C1", name: "directmessage" },
|
||||
trigger_id: "t1",
|
||||
},
|
||||
respond,
|
||||
});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(1);
|
||||
@@ -485,20 +438,12 @@ describe("Slack native command argument menus", () => {
|
||||
});
|
||||
|
||||
it("dispatches the command when a static_select option is chosen", async () => {
|
||||
const respond = vi.fn().mockResolvedValue(undefined);
|
||||
await argMenuHandler({
|
||||
ack: vi.fn().mockResolvedValue(undefined),
|
||||
await runArgMenuAction(argMenuHandler, {
|
||||
action: {
|
||||
selected_option: {
|
||||
value: encodeValue({ command: "report", arg: "period", value: "month", userId: "U1" }),
|
||||
},
|
||||
},
|
||||
body: {
|
||||
user: { id: "U1", name: "Ada" },
|
||||
channel: { id: "C1", name: "directmessage" },
|
||||
trigger_id: "t1",
|
||||
},
|
||||
respond,
|
||||
});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(1);
|
||||
@@ -507,9 +452,7 @@ describe("Slack native command argument menus", () => {
|
||||
});
|
||||
|
||||
it("dispatches the command when an overflow option is chosen", async () => {
|
||||
const respond = vi.fn().mockResolvedValue(undefined);
|
||||
await argMenuHandler({
|
||||
ack: vi.fn().mockResolvedValue(undefined),
|
||||
await runArgMenuAction(argMenuHandler, {
|
||||
action: {
|
||||
selected_option: {
|
||||
value: encodeValue({
|
||||
@@ -520,12 +463,6 @@ describe("Slack native command argument menus", () => {
|
||||
}),
|
||||
},
|
||||
},
|
||||
body: {
|
||||
user: { id: "U1", name: "Ada" },
|
||||
channel: { id: "C1", name: "directmessage" },
|
||||
trigger_id: "t1",
|
||||
},
|
||||
respond,
|
||||
});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(1);
|
||||
@@ -534,21 +471,7 @@ describe("Slack native command argument menus", () => {
|
||||
});
|
||||
|
||||
it("shows an external_select menu when choices exceed static_select options max", async () => {
|
||||
const respond = vi.fn().mockResolvedValue(undefined);
|
||||
const ack = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
await reportExternalHandler({
|
||||
command: {
|
||||
user_id: "U1",
|
||||
user_name: "Ada",
|
||||
channel_id: "C1",
|
||||
channel_name: "directmessage",
|
||||
text: "",
|
||||
trigger_id: "t1",
|
||||
},
|
||||
ack,
|
||||
respond,
|
||||
});
|
||||
const { respond } = await runCommandHandler(reportExternalHandler);
|
||||
|
||||
expect(respond).toHaveBeenCalledTimes(1);
|
||||
const payload = respond.mock.calls[0]?.[0] as {
|
||||
@@ -564,21 +487,7 @@ describe("Slack native command argument menus", () => {
|
||||
});
|
||||
|
||||
it("serves filtered options for external_select menus", async () => {
|
||||
const respond = vi.fn().mockResolvedValue(undefined);
|
||||
const ack = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
await reportExternalHandler({
|
||||
command: {
|
||||
user_id: "U1",
|
||||
user_name: "Ada",
|
||||
channel_id: "C1",
|
||||
channel_name: "directmessage",
|
||||
text: "",
|
||||
trigger_id: "t1",
|
||||
},
|
||||
ack,
|
||||
respond,
|
||||
});
|
||||
const { respond } = await runCommandHandler(reportExternalHandler);
|
||||
|
||||
const payload = respond.mock.calls[0]?.[0] as {
|
||||
blocks?: Array<{ type: string; block_id?: string }>;
|
||||
@@ -605,18 +514,12 @@ describe("Slack native command argument menus", () => {
|
||||
});
|
||||
|
||||
it("rejects menu clicks from other users", async () => {
|
||||
const respond = vi.fn().mockResolvedValue(undefined);
|
||||
await argMenuHandler({
|
||||
ack: vi.fn().mockResolvedValue(undefined),
|
||||
const respond = await runArgMenuAction(argMenuHandler, {
|
||||
action: {
|
||||
value: encodeValue({ command: "usage", arg: "mode", value: "tokens", userId: "U1" }),
|
||||
},
|
||||
body: {
|
||||
user: { id: "U2", name: "Eve" },
|
||||
channel: { id: "C1", name: "directmessage" },
|
||||
trigger_id: "t1",
|
||||
},
|
||||
respond,
|
||||
userId: "U2",
|
||||
userName: "Eve",
|
||||
});
|
||||
|
||||
expect(dispatchMock).not.toHaveBeenCalled();
|
||||
@@ -627,10 +530,9 @@ describe("Slack native command argument menus", () => {
|
||||
});
|
||||
|
||||
it("falls back to postEphemeral with token when respond is unavailable", async () => {
|
||||
await argMenuHandler({
|
||||
ack: vi.fn().mockResolvedValue(undefined),
|
||||
await runArgMenuAction(argMenuHandler, {
|
||||
action: { value: "garbage" },
|
||||
body: { user: { id: "U1" }, channel: { id: "C1" } },
|
||||
includeRespond: false,
|
||||
});
|
||||
|
||||
expect(harness.postEphemeral).toHaveBeenCalledWith(
|
||||
@@ -643,10 +545,9 @@ describe("Slack native command argument menus", () => {
|
||||
});
|
||||
|
||||
it("treats malformed percent-encoding as an invalid button (no throw)", async () => {
|
||||
await argMenuHandler({
|
||||
ack: vi.fn().mockResolvedValue(undefined),
|
||||
await runArgMenuAction(argMenuHandler, {
|
||||
action: { value: "cmdarg|%E0%A4%A|mode|on|U1" },
|
||||
body: { user: { id: "U1" }, channel: { id: "C1" } },
|
||||
includeRespond: false,
|
||||
});
|
||||
|
||||
expect(harness.postEphemeral).toHaveBeenCalledWith(
|
||||
|
||||
Reference in New Issue
Block a user