test: avoid eager message action plugin discovery

Skip bundled channel discovery for plain message-action params and only resolve
plugin-owned media params when an extension field is actually present. This
keeps normal sends on the lightweight path while preserving plugin media-field
coverage.
This commit is contained in:
Gustavo Madeira Santana
2026-04-17 18:35:22 -04:00
parent 6f4d13f3bd
commit 3ca8ad3845
5 changed files with 124 additions and 3 deletions

View File

@@ -0,0 +1,57 @@
import { normalizeOptionalString } from "../../shared/string-coerce.js";
const STANDARD_MESSAGE_ACTION_PARAM_KEYS = new Set([
"accountId",
"asDocument",
"base64",
"bestEffort",
"blocks",
"buttons",
"caption",
"card",
"channel",
"channelId",
"components",
"contentType",
"dryRun",
"filePath",
"fileUrl",
"filename",
"forceDocument",
"gifPlayback",
"image",
"interactive",
"media",
"mediaUrl",
"message",
"mimeType",
"path",
"pollAnonymous",
"pollDurationHours",
"pollMulti",
"pollOption",
"pollPublic",
"pollQuestion",
"replyTo",
"silent",
"target",
"targets",
"text",
"threadId",
"to",
]);
export function hasPotentialPluginActionParam(params: Record<string, unknown>): boolean {
return Object.entries(params).some(([key, value]) => {
if (STANDARD_MESSAGE_ACTION_PARAM_KEYS.has(key)) {
return false;
}
if (typeof value === "string") {
return Boolean(normalizeOptionalString(value));
}
if (typeof value === "number") {
return Number.isFinite(value);
}
return value !== undefined;
});
}

View File

@@ -1,13 +1,23 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
const { resolveChannelMessageToolMediaSourceParamKeysMock } = vi.hoisted(() => ({
resolveChannelMessageToolMediaSourceParamKeysMock: vi.fn(() => ["avatarPath", "avatarUrl"]),
}));
vi.mock("../../channels/plugins/message-action-discovery.js", () => ({
resolveChannelMessageToolMediaSourceParamKeys: resolveChannelMessageToolMediaSourceParamKeysMock,
}));
import {
collectActionMediaSourceHints,
hydrateAttachmentParamsForAction,
normalizeSandboxMediaList,
normalizeSandboxMediaParams,
resolveExtraActionMediaSourceParamKeys,
resolveAttachmentMediaPolicy,
} from "./message-action-params.js";
@@ -16,6 +26,52 @@ const maybeIt = process.platform === "win32" ? it.skip : it;
const matrixMediaSourceParamKeys = ["avatarPath", "avatarUrl"] as const;
describe("message action media helpers", () => {
beforeEach(() => {
resolveChannelMessageToolMediaSourceParamKeysMock.mockClear();
});
it("skips plugin media discovery when args only use standard action params", () => {
expect(
resolveExtraActionMediaSourceParamKeys({
cfg,
action: "send",
channel: "slack",
args: {
channel: "slack",
target: "#C12345678",
message: "hi",
media: "https://example.com/photo.png",
},
}),
).toEqual([]);
expect(resolveChannelMessageToolMediaSourceParamKeysMock).not.toHaveBeenCalled();
});
it("discovers plugin media params when args include an extension-owned field", () => {
expect(
resolveExtraActionMediaSourceParamKeys({
cfg,
action: "set-profile",
channel: "matrix",
args: {
channel: "matrix",
avatarPath: "/workspace/avatars/profile.png",
},
}),
).toEqual(["avatarPath", "avatarUrl"]);
expect(resolveChannelMessageToolMediaSourceParamKeysMock).toHaveBeenCalledWith({
cfg,
action: "set-profile",
channel: "matrix",
accountId: undefined,
sessionKey: undefined,
sessionId: undefined,
agentId: undefined,
requesterSenderId: undefined,
senderIsOwner: undefined,
});
});
it("prefers sandbox media policy when sandbox roots are non-blank", () => {
expect(
resolveAttachmentMediaPolicy({

View File

@@ -17,6 +17,7 @@ import { loadWebMedia } from "../../media/web-media.js";
import { resolveSnakeCaseParamKey } from "../../param-key.js";
import { readBooleanParam as readBooleanParamShared } from "../../plugin-sdk/boolean-param.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { hasPotentialPluginActionParam } from "./message-action-param-keys.js";
export const readBooleanParam = readBooleanParamShared;
@@ -60,6 +61,7 @@ function buildActionMediaSourceParamKeys(extraParamKeys?: readonly string[]): st
export function resolveExtraActionMediaSourceParamKeys(params: {
cfg: OpenClawConfig;
action?: ChannelMessageActionName;
args: Record<string, unknown>;
channel?: string;
accountId?: string | null;
sessionKey?: string | null;
@@ -68,6 +70,9 @@ export function resolveExtraActionMediaSourceParamKeys(params: {
requesterSenderId?: string | null;
senderIsOwner?: boolean;
}): string[] {
if (!hasPotentialPluginActionParam(params.args)) {
return [];
}
return resolveChannelMessageToolMediaSourceParamKeys({
cfg: params.cfg,
action: params.action,

View File

@@ -860,6 +860,7 @@ export async function runMessageAction(
const extraActionMediaSourceParamKeys = resolveExtraActionMediaSourceParamKeys({
cfg,
action,
args: params,
channel,
accountId,
sessionKey: input.sessionKey,

View File

@@ -4,6 +4,7 @@ import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../../shared/string-coerce.js";
import { hasPotentialPluginActionParam } from "./message-action-param-keys.js";
export type MessageActionTargetMode = "to" | "channelId" | "none";
@@ -84,6 +85,7 @@ const ACTION_TARGET_ALIASES: Partial<Record<ChannelMessageActionName, ActionTarg
function listActionTargetAliasSpecs(
action: ChannelMessageActionName,
params: Record<string, unknown>,
channel?: string,
): ActionTargetAliasSpec[] {
const specs: ActionTargetAliasSpec[] = [];
@@ -92,7 +94,7 @@ function listActionTargetAliasSpecs(
specs.push(coreSpec);
}
const normalizedChannel = normalizeOptionalLowercaseString(channel);
if (!normalizedChannel) {
if (!normalizedChannel || !hasPotentialPluginActionParam(params)) {
return specs;
}
const plugin = getBootstrapChannelPlugin(normalizedChannel);
@@ -120,7 +122,7 @@ export function actionHasTarget(
if (channelId) {
return true;
}
const specs = listActionTargetAliasSpecs(action, options?.channel);
const specs = listActionTargetAliasSpecs(action, params, options?.channel);
if (specs.length === 0) {
return false;
}