Files
openclaw/src/auto-reply/reply/commands-plugins.test.ts
2026-04-06 06:03:25 +01:00

239 lines
7.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import { handlePluginsCommand } from "./commands-plugins.js";
import type { HandleCommandsParams } from "./commands-types.js";
const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn());
const validateConfigObjectWithPluginsMock = vi.hoisted(() => vi.fn());
const writeConfigFileMock = vi.hoisted(() => vi.fn(async () => undefined));
const buildPluginSnapshotReportMock = vi.hoisted(() => vi.fn());
const buildPluginDiagnosticsReportMock = vi.hoisted(() => vi.fn());
const buildPluginInspectReportMock = vi.hoisted(() => vi.fn());
const buildAllPluginInspectReportsMock = vi.hoisted(() => vi.fn());
const formatPluginCompatibilityNoticeMock = vi.hoisted(() => vi.fn(() => "ok"));
vi.mock("../../cli/npm-resolution.js", () => ({
buildNpmInstallRecordFields: vi.fn(),
}));
vi.mock("../../cli/plugins-command-helpers.js", () => ({
buildPreferredClawHubSpec: vi.fn(() => null),
createPluginInstallLogger: vi.fn(() => ({})),
decidePreferredClawHubFallback: vi.fn(() => "fallback_to_npm"),
resolveFileNpmSpecToLocalPath: vi.fn(() => null),
}));
vi.mock("../../cli/plugins-install-persist.js", () => ({
persistPluginInstall: vi.fn(async () => undefined),
}));
vi.mock("../../config/config.js", () => ({
readConfigFileSnapshot: readConfigFileSnapshotMock,
validateConfigObjectWithPlugins: validateConfigObjectWithPluginsMock,
writeConfigFile: writeConfigFileMock,
}));
vi.mock("../../infra/archive.js", () => ({
resolveArchiveKind: vi.fn(() => null),
}));
vi.mock("../../infra/clawhub.js", () => ({
parseClawHubPluginSpec: vi.fn(() => null),
}));
vi.mock("../../plugins/clawhub.js", () => ({
installPluginFromClawHub: vi.fn(),
}));
vi.mock("../../plugins/install.js", () => ({
installPluginFromNpmSpec: vi.fn(),
installPluginFromPath: vi.fn(),
}));
vi.mock("../../plugins/manifest-registry.js", () => ({
clearPluginManifestRegistryCache: vi.fn(),
}));
vi.mock("../../plugins/status.js", () => ({
buildAllPluginInspectReports: buildAllPluginInspectReportsMock,
buildPluginDiagnosticsReport: buildPluginDiagnosticsReportMock,
buildPluginInspectReport: buildPluginInspectReportMock,
buildPluginSnapshotReport: buildPluginSnapshotReportMock,
formatPluginCompatibilityNotice: formatPluginCompatibilityNoticeMock,
}));
vi.mock("../../plugins/toggle-config.js", () => ({
setPluginEnabledInConfig: vi.fn((config: OpenClawConfig, id: string, enabled: boolean) => ({
...config,
plugins: {
...config.plugins,
entries: {
...config.plugins?.entries,
[id]: { enabled },
},
},
})),
}));
vi.mock("../../utils.js", async () => {
const actual = await vi.importActual<typeof import("../../utils.js")>("../../utils.js");
return {
...actual,
resolveUserPath: vi.fn((value: string) => value),
};
});
function buildCfg(): OpenClawConfig {
return {
plugins: { enabled: true },
commands: { text: true, plugins: true },
};
}
function buildPluginsParams(
commandBodyNormalized: string,
cfg: OpenClawConfig,
): HandleCommandsParams {
return {
cfg,
ctx: {
Provider: "whatsapp",
Surface: "whatsapp",
CommandSource: "text",
GatewayClientScopes: ["operator.write", "operator.pairing"],
AccountId: undefined,
},
command: {
commandBodyNormalized,
rawBodyNormalized: commandBodyNormalized,
isAuthorizedSender: true,
senderIsOwner: true,
senderId: "owner",
channel: "whatsapp",
channelId: "whatsapp",
surface: "whatsapp",
ownerList: [],
from: "test-user",
to: "test-bot",
},
sessionKey: "agent:main:whatsapp:direct:test-user",
sessionEntry: {
sessionId: "session-plugin-command",
updatedAt: Date.now(),
},
workspaceDir: "/tmp/plugins-workspace",
} as unknown as HandleCommandsParams;
}
describe("handlePluginsCommand", () => {
beforeEach(() => {
vi.clearAllMocks();
readConfigFileSnapshotMock.mockResolvedValue({
valid: true,
path: "/tmp/openclaw.json",
resolved: buildCfg(),
});
validateConfigObjectWithPluginsMock.mockReturnValue({
ok: true,
config: buildCfg(),
issues: [],
});
buildPluginSnapshotReportMock.mockReturnValue({
workspaceDir: "/tmp/plugins-workspace",
plugins: [
{
id: "superpowers",
name: "superpowers",
status: "disabled",
format: "openclaw",
bundleFormat: "claude",
},
],
});
buildPluginDiagnosticsReportMock.mockReturnValue({
workspaceDir: "/tmp/plugins-workspace",
plugins: [
{
id: "superpowers",
name: "superpowers",
status: "disabled",
format: "openclaw",
bundleFormat: "claude",
},
],
});
buildPluginInspectReportMock.mockReturnValue({
plugin: {
id: "superpowers",
},
compatibility: [],
bundleFormat: "claude",
shape: { commands: ["review"] },
});
buildAllPluginInspectReportsMock.mockReturnValue([
{
plugin: { id: "superpowers" },
compatibility: [],
},
]);
});
it("lists discovered plugins and inspects plugin details", async () => {
const listResult = await handlePluginsCommand(
buildPluginsParams("/plugins list", buildCfg()),
true,
);
expect(listResult?.reply?.text).toContain("Plugins");
expect(listResult?.reply?.text).toContain("superpowers");
expect(listResult?.reply?.text).toContain("[disabled]");
const showResult = await handlePluginsCommand(
buildPluginsParams("/plugins inspect superpowers", buildCfg()),
true,
);
expect(showResult?.reply?.text).toContain('"id": "superpowers"');
expect(showResult?.reply?.text).toContain('"bundleFormat": "claude"');
expect(showResult?.reply?.text).toContain('"shape"');
expect(showResult?.reply?.text).toContain('"compatibilityWarnings": []');
const inspectAllResult = await handlePluginsCommand(
buildPluginsParams("/plugins inspect all", buildCfg()),
true,
);
expect(inspectAllResult?.reply?.text).toContain("```json");
expect(inspectAllResult?.reply?.text).toContain('"plugin"');
expect(inspectAllResult?.reply?.text).toContain('"compatibilityWarnings"');
expect(inspectAllResult?.reply?.text).toContain('"superpowers"');
});
it("rejects internal writes without operator.admin", async () => {
const params = buildPluginsParams("/plugins enable superpowers", buildCfg());
params.command.channel = "webchat";
params.command.channelId = "webchat";
params.command.surface = "webchat";
params.ctx.Provider = "webchat";
params.ctx.Surface = "webchat";
params.ctx.GatewayClientScopes = ["operator.write"];
const result = await handlePluginsCommand(params, true);
expect(result?.reply?.text).toContain("requires operator.admin");
});
it("returns an explicit unauthorized reply for native /plugins list", async () => {
const params = buildPluginsParams("/plugins list", buildCfg());
params.command.senderIsOwner = false;
params.ctx.Provider = "telegram";
params.ctx.Surface = "telegram";
params.ctx.CommandSource = "native";
params.command.channel = "telegram";
params.command.channelId = "telegram";
params.command.surface = "telegram";
const result = await handlePluginsCommand(params, true);
expect(result).toEqual({
shouldContinue: false,
reply: { text: "You are not authorized to use this command." },
});
});
});