mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-21 15:24:48 +00:00
Merged via squash.
Prepared head SHA: 68e5f2ce19
Co-authored-by: 100yenadmin <239388517+100yenadmin@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
186 lines
5.5 KiB
TypeScript
186 lines
5.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { OpenClawConfig } from "../../config/config.js";
|
|
import { handlePluginCommand } from "./commands-plugin.js";
|
|
import type { HandleCommandsParams } from "./commands-types.js";
|
|
|
|
const matchPluginCommandMock = vi.hoisted(() => vi.fn());
|
|
const executePluginCommandMock = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock("../../plugins/commands.js", () => ({
|
|
matchPluginCommand: matchPluginCommandMock,
|
|
executePluginCommand: executePluginCommandMock,
|
|
}));
|
|
|
|
function buildPluginParams(
|
|
commandBodyNormalized: string,
|
|
cfg: OpenClawConfig,
|
|
): HandleCommandsParams {
|
|
return {
|
|
cfg,
|
|
ctx: {
|
|
Provider: "whatsapp",
|
|
Surface: "whatsapp",
|
|
CommandSource: "text",
|
|
GatewayClientScopes: ["operator.write", "operator.pairing"],
|
|
AccountId: undefined,
|
|
},
|
|
command: {
|
|
commandBodyNormalized,
|
|
isAuthorizedSender: true,
|
|
senderId: "owner",
|
|
channel: "whatsapp",
|
|
channelId: "whatsapp",
|
|
from: "test-user",
|
|
to: "test-bot",
|
|
},
|
|
sessionKey: "agent:main:whatsapp:direct:test-user",
|
|
sessionEntry: {
|
|
sessionId: "session-plugin-command",
|
|
updatedAt: Date.now(),
|
|
},
|
|
} as unknown as HandleCommandsParams;
|
|
}
|
|
|
|
describe("handlePluginCommand", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("dispatches registered plugin commands with gateway scopes and session metadata", async () => {
|
|
matchPluginCommandMock.mockReturnValue({
|
|
command: { name: "card" },
|
|
args: "",
|
|
});
|
|
executePluginCommandMock.mockResolvedValue({ text: "from plugin" });
|
|
|
|
const result = await handlePluginCommand(
|
|
buildPluginParams("/card", {
|
|
commands: { text: true },
|
|
channels: { whatsapp: { allowFrom: ["*"] } },
|
|
} as OpenClawConfig),
|
|
true,
|
|
);
|
|
|
|
expect(result?.shouldContinue).toBe(false);
|
|
expect(result?.reply?.text).toBe("from plugin");
|
|
expect(executePluginCommandMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
gatewayClientScopes: ["operator.write", "operator.pairing"],
|
|
sessionKey: "agent:main:whatsapp:direct:test-user",
|
|
sessionId: "session-plugin-command",
|
|
commandBody: "/card",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("prefers the target session entry from sessionStore for plugin command metadata", async () => {
|
|
matchPluginCommandMock.mockReturnValue({
|
|
command: { name: "card" },
|
|
args: "",
|
|
});
|
|
executePluginCommandMock.mockResolvedValue({ text: "from plugin" });
|
|
|
|
const params = buildPluginParams("/card", {
|
|
commands: { text: true },
|
|
channels: { whatsapp: { allowFrom: ["*"] } },
|
|
} as OpenClawConfig);
|
|
params.sessionEntry = {
|
|
sessionId: "wrapper-session",
|
|
sessionFile: "/tmp/wrapper-session.jsonl",
|
|
updatedAt: Date.now(),
|
|
} as HandleCommandsParams["sessionEntry"];
|
|
params.sessionStore = {
|
|
[params.sessionKey]: {
|
|
sessionId: "target-session",
|
|
sessionFile: "/tmp/target-session.jsonl",
|
|
updatedAt: Date.now(),
|
|
},
|
|
};
|
|
|
|
await handlePluginCommand(params, true);
|
|
|
|
expect(executePluginCommandMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
sessionId: "target-session",
|
|
sessionFile: "/tmp/target-session.jsonl",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("continues the agent without leaking continueAgent into the reply payload", async () => {
|
|
matchPluginCommandMock.mockReturnValue({
|
|
command: { name: "card" },
|
|
args: "",
|
|
});
|
|
executePluginCommandMock.mockResolvedValue({
|
|
text: "from plugin",
|
|
continueAgent: true,
|
|
});
|
|
|
|
const result = await handlePluginCommand(
|
|
buildPluginParams("/card", {
|
|
commands: { text: true },
|
|
channels: { whatsapp: { allowFrom: ["*"] } },
|
|
} as OpenClawConfig),
|
|
true,
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
shouldContinue: true,
|
|
reply: { text: "from plugin" },
|
|
});
|
|
});
|
|
|
|
it("enforces requiredScopes through the command handler path", async () => {
|
|
const actualCommands = await vi.importActual<typeof import("../../plugins/commands.js")>(
|
|
"../../plugins/commands.js",
|
|
);
|
|
const handler = vi.fn().mockResolvedValue({
|
|
text: "approved",
|
|
continueAgent: true,
|
|
});
|
|
const command = {
|
|
pluginId: "approval-plugin",
|
|
pluginName: "Approval Plugin",
|
|
pluginRoot: "/tmp/approval-plugin",
|
|
name: "approve-deploy",
|
|
description: "Approve deployment",
|
|
requiredScopes: ["operator.approvals"],
|
|
handler,
|
|
};
|
|
matchPluginCommandMock.mockReturnValue({
|
|
command,
|
|
args: "",
|
|
});
|
|
executePluginCommandMock.mockImplementation(actualCommands.executePluginCommand);
|
|
|
|
const denied = await handlePluginCommand(
|
|
buildPluginParams("/approve-deploy", {
|
|
commands: { text: true },
|
|
channels: { whatsapp: { allowFrom: ["*"] } },
|
|
} as OpenClawConfig),
|
|
true,
|
|
);
|
|
|
|
expect(denied).toEqual({
|
|
shouldContinue: false,
|
|
reply: { text: "⚠️ This command requires gateway scope: operator.approvals." },
|
|
});
|
|
expect(handler).not.toHaveBeenCalled();
|
|
|
|
const allowedParams = buildPluginParams("/approve-deploy", {
|
|
commands: { text: true },
|
|
channels: { whatsapp: { allowFrom: ["*"] } },
|
|
} as OpenClawConfig);
|
|
allowedParams.ctx.GatewayClientScopes = ["operator.approvals"];
|
|
|
|
const allowed = await handlePluginCommand(allowedParams, true);
|
|
|
|
expect(allowed).toEqual({
|
|
shouldContinue: true,
|
|
reply: { text: "approved" },
|
|
});
|
|
expect(handler).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|