mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 03:15:46 -06:00
test(outbound): make poll coverage order-independent (#121923)
This commit is contained in:
committed by
GitHub
parent
210aca6de3
commit
465aad50a7
@@ -1,28 +1,105 @@
|
||||
// Covers message-action poll handling through plugin dispatch and core gateway
|
||||
// poll fallback.
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
clearMessageActionPollMocks,
|
||||
messageActionRunnerMocks as mocks,
|
||||
pollerConfig,
|
||||
pollerTestPlugin,
|
||||
resetMessageActionPollMocks,
|
||||
runPollAction,
|
||||
} from "./message-action-runner.test-helpers.js";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
// Covers message-action poll normalization and direct provider delivery.
|
||||
import type {
|
||||
ChannelPlugin,
|
||||
ChannelThreadingToolContext,
|
||||
} from "../../channels/plugins/types.public.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { executeMessagePoll } from "./message-action-execution.js";
|
||||
|
||||
describe("runMessageAction poll handling", () => {
|
||||
beforeEach(() => {
|
||||
resetMessageActionPollMocks();
|
||||
});
|
||||
const pollerConfig = {
|
||||
channels: {
|
||||
poller: {
|
||||
botToken: "poller-test",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
afterEach(() => {
|
||||
clearMessageActionPollMocks();
|
||||
});
|
||||
it("passes shared poll fields and auto threadId to executePollAction", async () => {
|
||||
const call = await runPollAction({
|
||||
type PollerSendPoll = NonNullable<NonNullable<ChannelPlugin["outbound"]>["sendPoll"]>;
|
||||
|
||||
const pollerSendPoll = vi.fn<PollerSendPoll>(async () => ({
|
||||
messageId: "poll-test",
|
||||
}));
|
||||
|
||||
const pollerTestPlugin: ChannelPlugin = {
|
||||
id: "poller",
|
||||
meta: {
|
||||
id: "poller",
|
||||
label: "Poller",
|
||||
selectionLabel: "Poller",
|
||||
docsPath: "/channels/poller",
|
||||
blurb: "Poller test plugin.",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct", "group"] },
|
||||
config: {
|
||||
listAccountIds: () => ["default"],
|
||||
resolveAccount: () => ({ botToken: "poller-test" }),
|
||||
isConfigured: () => true,
|
||||
},
|
||||
outbound: {
|
||||
deliveryMode: "direct",
|
||||
sendPoll: pollerSendPoll,
|
||||
},
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
looksLikeId: () => true,
|
||||
resolveTarget: async ({ normalized }) => ({
|
||||
to: normalized,
|
||||
kind: "user",
|
||||
source: "normalized",
|
||||
}),
|
||||
},
|
||||
},
|
||||
threading: {
|
||||
resolveAutoThreadId: ({ toolContext, to, replyToId }) => {
|
||||
if (replyToId || toolContext?.currentChannelId !== to) {
|
||||
return undefined;
|
||||
}
|
||||
return toolContext.currentThreadTs;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async function runPollAction(params: {
|
||||
actionParams: Record<string, unknown>;
|
||||
toolContext?: ChannelThreadingToolContext;
|
||||
}) {
|
||||
const target = params.actionParams.target;
|
||||
if (typeof target !== "string") {
|
||||
throw new Error("poll test target is required");
|
||||
}
|
||||
const actionParams = { ...params.actionParams, to: target };
|
||||
await executeMessagePoll({
|
||||
cfg: pollerConfig,
|
||||
params: actionParams,
|
||||
channel: "poller",
|
||||
channelPlugin: pollerTestPlugin,
|
||||
mediaAccess: {},
|
||||
accountId: "default",
|
||||
dryRun: false,
|
||||
input: {
|
||||
cfg: pollerConfig,
|
||||
action: "poll",
|
||||
params: actionParams,
|
||||
toolContext: params.toolContext,
|
||||
},
|
||||
});
|
||||
const call = pollerSendPoll.mock.calls[0]?.[0];
|
||||
if (!call) {
|
||||
throw new Error("expected poller sendPoll call");
|
||||
}
|
||||
return call;
|
||||
}
|
||||
|
||||
describe("executeMessagePoll", () => {
|
||||
beforeEach(() => {
|
||||
pollerSendPoll.mockReset();
|
||||
pollerSendPoll.mockResolvedValue({ messageId: "poll-test" });
|
||||
});
|
||||
|
||||
it("passes normalized poll fields and auto threadId to the provider", async () => {
|
||||
const call = await runPollAction({
|
||||
actionParams: {
|
||||
channel: "poller",
|
||||
target: "poller:123",
|
||||
pollQuestion: "Lunch?",
|
||||
pollOption: ["Pizza", "Sushi"],
|
||||
@@ -34,11 +111,13 @@ describe("runMessageAction poll handling", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(call?.durationHours).toBe(2);
|
||||
expect(call?.threadId).toBe("42");
|
||||
expect(call?.ctx?.params?.threadId).toBe("42");
|
||||
expect(call?.ctx?.plugin).toBe(pollerTestPlugin);
|
||||
expect(mocks.resolveOutboundChannelPlugin).toHaveBeenCalledTimes(1);
|
||||
expect(call.poll).toMatchObject({
|
||||
question: "Lunch?",
|
||||
options: ["Pizza", "Sushi"],
|
||||
durationHours: 2,
|
||||
maxSelections: 1,
|
||||
});
|
||||
expect(call.threadId).toBe("42");
|
||||
});
|
||||
|
||||
it.each([0, -1, 1.5, "1.5", "soon"])(
|
||||
@@ -46,9 +125,7 @@ describe("runMessageAction poll handling", () => {
|
||||
async (pollDurationHours) => {
|
||||
await expect(
|
||||
runPollAction({
|
||||
cfg: pollerConfig,
|
||||
actionParams: {
|
||||
channel: "poller",
|
||||
target: "poller:123",
|
||||
pollQuestion: "Lunch?",
|
||||
pollOption: ["Pizza", "Sushi"],
|
||||
@@ -59,41 +136,9 @@ describe("runMessageAction poll handling", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("passes inbound event kind to poll execution", async () => {
|
||||
const call = await runPollAction({
|
||||
cfg: pollerConfig,
|
||||
actionParams: {
|
||||
channel: "poller",
|
||||
target: "poller:123",
|
||||
pollQuestion: "Lunch?",
|
||||
pollOption: ["Pizza", "Sushi"],
|
||||
},
|
||||
inboundEventKind: "room_event",
|
||||
});
|
||||
|
||||
expect(call?.ctx?.inboundEventKind).toBe("room_event");
|
||||
});
|
||||
|
||||
it("copies the normalized idempotency key into poll execution context", async () => {
|
||||
const call = await runPollAction({
|
||||
cfg: pollerConfig,
|
||||
actionParams: {
|
||||
channel: "poller",
|
||||
target: "poller:123",
|
||||
pollQuestion: "Lunch?",
|
||||
pollOption: ["Pizza", "Sushi"],
|
||||
idempotencyKey: " run-1:message-tool:poll-1:fingerprint ",
|
||||
},
|
||||
});
|
||||
|
||||
expect(call?.ctx?.idempotencyKey).toBe("run-1:message-tool:poll-1:fingerprint");
|
||||
});
|
||||
|
||||
it("expands maxSelections when pollMulti is enabled", async () => {
|
||||
const call = await runPollAction({
|
||||
cfg: pollerConfig,
|
||||
actionParams: {
|
||||
channel: "poller",
|
||||
target: "poller:123",
|
||||
pollQuestion: "Lunch?",
|
||||
pollOption: ["Pizza", "Sushi", "Soup"],
|
||||
@@ -101,20 +146,30 @@ describe("runMessageAction poll handling", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(call?.maxSelections).toBe(3);
|
||||
expect(call.poll.maxSelections).toBe(3);
|
||||
});
|
||||
|
||||
it("defaults maxSelections to one choice when pollMulti is omitted", async () => {
|
||||
const call = await runPollAction({
|
||||
cfg: pollerConfig,
|
||||
actionParams: {
|
||||
channel: "poller",
|
||||
target: "poller:123",
|
||||
pollQuestion: "Lunch?",
|
||||
pollOption: ["Pizza", "Sushi", "Soup"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(call?.maxSelections).toBe(1);
|
||||
expect(call.poll.maxSelections).toBe(1);
|
||||
});
|
||||
|
||||
it("requires at least two poll options", async () => {
|
||||
await expect(
|
||||
runPollAction({
|
||||
actionParams: {
|
||||
target: "poller:123",
|
||||
pollQuestion: "Lunch?",
|
||||
pollOption: ["Pizza"],
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/pollOption requires at least two values/i);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// Covers message-action poll handling through plugin dispatch and core gateway
|
||||
// poll fallback.
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
clearMessageActionPollMocks,
|
||||
messageActionRunnerMocks as mocks,
|
||||
pollerConfig,
|
||||
resetMessageActionPollMocks,
|
||||
runPollAction,
|
||||
} from "./message-action-runner.test-helpers.js";
|
||||
|
||||
describe("runMessageAction poll handling", () => {
|
||||
beforeEach(() => {
|
||||
resetMessageActionPollMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearMessageActionPollMocks();
|
||||
});
|
||||
it("requires at least two poll options", async () => {
|
||||
await expect(
|
||||
runPollAction({
|
||||
cfg: pollerConfig,
|
||||
actionParams: {
|
||||
channel: "poller",
|
||||
target: "poller:123",
|
||||
pollQuestion: "Lunch?",
|
||||
pollOption: ["Pizza"],
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/pollOption requires at least two values/i);
|
||||
expect(mocks.executePollAction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,6 @@ import type {
|
||||
ChannelMessageActionName,
|
||||
ChannelPlugin,
|
||||
} from "../../channels/plugins/types.public.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import {
|
||||
normalizeMessagePresentation,
|
||||
renderMessagePresentationFallbackText,
|
||||
@@ -349,119 +348,3 @@ export async function resetMessageActionMediaMocks() {
|
||||
mocks.loadWebMedia.mockReset();
|
||||
mocks.loadWebMedia.mockImplementation(actualLoadWebMedia);
|
||||
}
|
||||
|
||||
export const pollerConfig = {
|
||||
channels: {
|
||||
poller: {
|
||||
botToken: "poller-test",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
export const pollerTestPlugin: ChannelPlugin = {
|
||||
id: "poller",
|
||||
meta: {
|
||||
id: "poller",
|
||||
label: "Poller",
|
||||
selectionLabel: "Poller",
|
||||
docsPath: "/channels/poller",
|
||||
blurb: "Poller test plugin.",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct", "group"] },
|
||||
config: {
|
||||
listAccountIds: () => ["default"],
|
||||
resolveAccount: () => ({ botToken: "poller-test" }),
|
||||
isConfigured: () => true,
|
||||
},
|
||||
outbound: {
|
||||
deliveryMode: "gateway",
|
||||
sendPoll: async () => ({
|
||||
messageId: "poll-test",
|
||||
}),
|
||||
},
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
looksLikeId: () => true,
|
||||
resolveTarget: async ({ normalized }) => ({
|
||||
to: normalized,
|
||||
kind: "user",
|
||||
source: "normalized",
|
||||
}),
|
||||
},
|
||||
},
|
||||
threading: {
|
||||
resolveAutoThreadId: ({ toolContext, to, replyToId }) => {
|
||||
if (replyToId) {
|
||||
return undefined;
|
||||
}
|
||||
if (toolContext?.currentChannelId !== to) {
|
||||
return undefined;
|
||||
}
|
||||
return toolContext.currentThreadTs;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export async function runPollAction(params: {
|
||||
cfg: OpenClawConfig;
|
||||
actionParams: Record<string, unknown>;
|
||||
toolContext?: Record<string, unknown>;
|
||||
inboundEventKind?: "user_request" | "room_event";
|
||||
}) {
|
||||
await runMessageAction({
|
||||
cfg: params.cfg,
|
||||
action: "poll",
|
||||
params: params.actionParams as never,
|
||||
toolContext: params.toolContext as never,
|
||||
inboundEventKind: params.inboundEventKind,
|
||||
});
|
||||
const call = messageActionRunnerMocks.executePollAction.mock.calls[0]?.[0] as
|
||||
| {
|
||||
resolveCorePoll?: () => {
|
||||
durationHours?: number;
|
||||
maxSelections?: number;
|
||||
threadId?: string;
|
||||
};
|
||||
ctx?: {
|
||||
plugin?: ChannelPlugin;
|
||||
inboundEventKind?: string;
|
||||
idempotencyKey?: string;
|
||||
params?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
if (!call) {
|
||||
throw new Error("expected executePollAction call");
|
||||
}
|
||||
return {
|
||||
...call.resolveCorePoll?.(),
|
||||
ctx: call.ctx,
|
||||
};
|
||||
}
|
||||
|
||||
export function resetMessageActionPollMocks() {
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([{ pluginId: "poller", source: "test", plugin: pollerTestPlugin }]),
|
||||
);
|
||||
const mocks = messageActionRunnerMocks;
|
||||
mocks.resolveOutboundChannelPlugin.mockReset();
|
||||
mocks.resolveOutboundChannelPlugin.mockImplementation(
|
||||
({ channel }: { channel: string }) =>
|
||||
getActivePluginRegistry()?.channels.find((entry) => entry?.plugin?.id === channel)?.plugin,
|
||||
);
|
||||
mocks.executeSendAction.mockReset();
|
||||
mocks.executeSendAction.mockImplementation(async () => {
|
||||
throw new Error("executeSendAction should not run in poll tests");
|
||||
});
|
||||
mocks.executePollAction.mockReset();
|
||||
mocks.executePollAction.mockImplementation(async (input) => ({
|
||||
handledBy: "core",
|
||||
payload: { ok: true, corePoll: input.resolveCorePoll() },
|
||||
pollResult: { ok: true },
|
||||
}));
|
||||
}
|
||||
|
||||
export function clearMessageActionPollMocks() {
|
||||
setActivePluginRegistry(createTestRegistry([]));
|
||||
messageActionRunnerMocks.executePollAction.mockReset();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user