From a2fa799a5c81238d269a4715deb501846b99d004 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Thu, 19 Mar 2026 01:15:03 -0400 Subject: [PATCH] Tests: stabilize poll fallback coverage --- docs/plugins/architecture.md | 14 +++ .../message-action-runner.poll.test.ts | 108 ++++++++++++------ 2 files changed, 90 insertions(+), 32 deletions(-) diff --git a/docs/plugins/architecture.md b/docs/plugins/architecture.md index f857b8f1b1c..19783028721 100644 --- a/docs/plugins/architecture.md +++ b/docs/plugins/architecture.md @@ -979,6 +979,20 @@ Compatibility note: them today. Their presence does not by itself mean every exported helper is a long-term frozen external contract. +## Message tool schemas + +Plugins should own channel-specific `describeMessageTool(...)` schema +contributions. Keep provider-specific fields in the plugin, not in shared core. + +For shared portable schema fragments, reuse the generic helpers exported through +`openclaw/plugin-sdk/channel-runtime`: + +- `createMessageToolButtonsSchema()` for button-grid style payloads +- `createMessageToolCardSchema()` for structured card payloads + +If a schema shape only makes sense for one provider, define it in that plugin's +own source instead of promoting it into the shared SDK. + ## Channel target resolution Channel plugins should own channel-specific target semantics. Keep the shared diff --git a/src/infra/outbound/message-action-runner.poll.test.ts b/src/infra/outbound/message-action-runner.poll.test.ts index dabc9bab35f..7581be956e2 100644 --- a/src/infra/outbound/message-action-runner.poll.test.ts +++ b/src/infra/outbound/message-action-runner.poll.test.ts @@ -1,4 +1,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { ChannelPlugin } from "../../channels/plugins/types.js"; +import type { OpenClawConfig } from "../../config/config.js"; +import { setActivePluginRegistry } from "../../plugins/runtime.js"; +import { createTestRegistry } from "../../test-utils/channel-plugins.js"; +import { runMessageAction } from "./message-action-runner.js"; + const mocks = vi.hoisted(() => ({ executePollAction: vi.fn(), })); @@ -13,17 +19,54 @@ vi.mock("./outbound-send-service.js", async () => { }; }); -type MessageActionRunnerModule = typeof import("./message-action-runner.js"); -type MessageActionRunnerTestHelpersModule = - typeof import("./message-action-runner.test-helpers.js"); +const telegramConfig = { + channels: { + telegram: { + botToken: "telegram-test", + }, + }, +} as OpenClawConfig; -let runMessageAction: MessageActionRunnerModule["runMessageAction"]; -let installMessageActionRunnerTestRegistry: MessageActionRunnerTestHelpersModule["installMessageActionRunnerTestRegistry"]; -let resetMessageActionRunnerTestRegistry: MessageActionRunnerTestHelpersModule["resetMessageActionRunnerTestRegistry"]; -let telegramConfig: MessageActionRunnerTestHelpersModule["telegramConfig"]; +const telegramPollTestPlugin: ChannelPlugin = { + id: "telegram", + meta: { + id: "telegram", + label: "Telegram", + selectionLabel: "Telegram", + docsPath: "/channels/telegram", + blurb: "Telegram poll test plugin.", + }, + capabilities: { chatTypes: ["direct", "group"] }, + config: { + listAccountIds: () => ["default"], + resolveAccount: () => ({ botToken: "telegram-test" }), + isConfigured: () => true, + }, + 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; + }, + }, +}; async function runPollAction(params: { - cfg: MessageActionRunnerTestHelpersModule["slackConfig"]; + cfg: OpenClawConfig; actionParams: Record; toolContext?: Record; }) { @@ -51,16 +94,19 @@ async function runPollAction(params: { ctx: call.ctx, }; } + describe("runMessageAction poll handling", () => { - beforeEach(async () => { - vi.resetModules(); - ({ runMessageAction } = await import("./message-action-runner.js")); - ({ - installMessageActionRunnerTestRegistry, - resetMessageActionRunnerTestRegistry, - telegramConfig, - } = await import("./message-action-runner.test-helpers.js")); - installMessageActionRunnerTestRegistry(); + beforeEach(() => { + setActivePluginRegistry( + createTestRegistry([ + { + pluginId: "telegram", + source: "test", + plugin: telegramPollTestPlugin, + }, + ]), + ); + mocks.executePollAction.mockReset(); mocks.executePollAction.mockImplementation(async (input) => ({ handledBy: "core", payload: { ok: true, corePoll: input.resolveCorePoll() }, @@ -69,24 +115,22 @@ describe("runMessageAction poll handling", () => { }); afterEach(() => { - resetMessageActionRunnerTestRegistry?.(); + setActivePluginRegistry(createTestRegistry([])); mocks.executePollAction.mockReset(); }); - it.each([ - { - name: "requires at least two poll options", - getCfg: () => telegramConfig, - actionParams: { - channel: "telegram", - target: "telegram:123", - pollQuestion: "Lunch?", - pollOption: ["Pizza"], - }, - message: /pollOption requires at least two values/i, - }, - ])("$name", async ({ getCfg, actionParams, message }) => { - await expect(runPollAction({ cfg: getCfg(), actionParams })).rejects.toThrow(message); + it("requires at least two poll options", async () => { + await expect( + runPollAction({ + cfg: telegramConfig, + actionParams: { + channel: "telegram", + target: "telegram:123", + pollQuestion: "Lunch?", + pollOption: ["Pizza"], + }, + }), + ).rejects.toThrow(/pollOption requires at least two values/i); expect(mocks.executePollAction).toHaveBeenCalledTimes(1); });