Files
openclaw/src/plugins/clawhub.test.ts
2026-03-28 04:28:54 +00:00

253 lines
7.7 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const parseClawHubPluginSpecMock = vi.fn();
const fetchClawHubPackageDetailMock = vi.fn();
const fetchClawHubPackageVersionMock = vi.fn();
const downloadClawHubPackageArchiveMock = vi.fn();
const resolveLatestVersionFromPackageMock = vi.fn();
const resolveCompatibilityHostVersionMock = vi.fn();
const installPluginFromArchiveMock = vi.fn();
vi.mock("../infra/clawhub.js", async () => {
const actual = await vi.importActual<typeof import("../infra/clawhub.js")>("../infra/clawhub.js");
return {
...actual,
parseClawHubPluginSpec: (...args: unknown[]) => parseClawHubPluginSpecMock(...args),
fetchClawHubPackageDetail: (...args: unknown[]) => fetchClawHubPackageDetailMock(...args),
fetchClawHubPackageVersion: (...args: unknown[]) => fetchClawHubPackageVersionMock(...args),
downloadClawHubPackageArchive: (...args: unknown[]) =>
downloadClawHubPackageArchiveMock(...args),
resolveLatestVersionFromPackage: (...args: unknown[]) =>
resolveLatestVersionFromPackageMock(...args),
};
});
vi.mock("../version.js", () => ({
resolveCompatibilityHostVersion: (...args: unknown[]) =>
resolveCompatibilityHostVersionMock(...args),
}));
vi.mock("./install.js", () => ({
installPluginFromArchive: (...args: unknown[]) => installPluginFromArchiveMock(...args),
}));
const { ClawHubRequestError } = await import("../infra/clawhub.js");
const { CLAWHUB_INSTALL_ERROR_CODE, formatClawHubSpecifier, installPluginFromClawHub } =
await import("./clawhub.js");
async function expectClawHubInstallError(params: {
setup?: () => void;
spec: string;
expected: {
ok: false;
code: (typeof CLAWHUB_INSTALL_ERROR_CODE)[keyof typeof CLAWHUB_INSTALL_ERROR_CODE];
error: string;
};
}) {
params.setup?.();
await expect(installPluginFromClawHub({ spec: params.spec })).resolves.toMatchObject(
params.expected,
);
}
function createLoggerSpies() {
return {
info: vi.fn(),
warn: vi.fn(),
};
}
function expectClawHubInstallFlow(params: {
baseUrl: string;
version: string;
archivePath: string;
}) {
expect(fetchClawHubPackageDetailMock).toHaveBeenCalledWith(
expect.objectContaining({
name: "demo",
baseUrl: params.baseUrl,
}),
);
expect(fetchClawHubPackageVersionMock).toHaveBeenCalledWith(
expect.objectContaining({
name: "demo",
version: params.version,
}),
);
expect(installPluginFromArchiveMock).toHaveBeenCalledWith(
expect.objectContaining({
archivePath: params.archivePath,
}),
);
}
function expectSuccessfulClawHubInstall(result: unknown) {
expect(result).toMatchObject({
ok: true,
pluginId: "demo",
version: "2026.3.22",
clawhub: {
source: "clawhub",
clawhubPackage: "demo",
clawhubFamily: "code-plugin",
clawhubChannel: "official",
integrity: "sha256-demo",
},
});
}
describe("installPluginFromClawHub", () => {
beforeEach(() => {
parseClawHubPluginSpecMock.mockReset();
fetchClawHubPackageDetailMock.mockReset();
fetchClawHubPackageVersionMock.mockReset();
downloadClawHubPackageArchiveMock.mockReset();
resolveLatestVersionFromPackageMock.mockReset();
resolveCompatibilityHostVersionMock.mockReset();
installPluginFromArchiveMock.mockReset();
parseClawHubPluginSpecMock.mockReturnValue({ name: "demo" });
fetchClawHubPackageDetailMock.mockResolvedValue({
package: {
name: "demo",
displayName: "Demo",
family: "code-plugin",
channel: "official",
isOfficial: true,
createdAt: 0,
updatedAt: 0,
compatibility: {
pluginApiRange: ">=2026.3.22",
minGatewayVersion: "2026.3.0",
},
},
});
resolveLatestVersionFromPackageMock.mockReturnValue("2026.3.22");
fetchClawHubPackageVersionMock.mockResolvedValue({
version: {
version: "2026.3.22",
createdAt: 0,
changelog: "",
compatibility: {
pluginApiRange: ">=2026.3.22",
minGatewayVersion: "2026.3.0",
},
},
});
downloadClawHubPackageArchiveMock.mockResolvedValue({
archivePath: "/tmp/clawhub-demo/archive.zip",
integrity: "sha256-demo",
});
resolveCompatibilityHostVersionMock.mockReturnValue("2026.3.22");
installPluginFromArchiveMock.mockResolvedValue({
ok: true,
pluginId: "demo",
targetDir: "/tmp/openclaw/plugins/demo",
version: "2026.3.22",
});
});
it("formats clawhub specifiers", () => {
expect(formatClawHubSpecifier({ name: "demo" })).toBe("clawhub:demo");
expect(formatClawHubSpecifier({ name: "demo", version: "1.2.3" })).toBe("clawhub:demo@1.2.3");
});
it("installs a ClawHub code plugin through the archive installer", async () => {
const logger = createLoggerSpies();
const result = await installPluginFromClawHub({
spec: "clawhub:demo",
baseUrl: "https://clawhub.ai",
logger,
});
expectClawHubInstallFlow({
baseUrl: "https://clawhub.ai",
version: "2026.3.22",
archivePath: "/tmp/clawhub-demo/archive.zip",
});
expectSuccessfulClawHubInstall(result);
expect(logger.info).toHaveBeenCalledWith("ClawHub code-plugin demo@2026.3.22 channel=official");
expect(logger.info).toHaveBeenCalledWith(
"Compatibility: pluginApi=>=2026.3.22 minGateway=2026.3.0",
);
expect(logger.warn).not.toHaveBeenCalled();
});
it.each([
{
name: "rejects packages whose plugin API range exceeds the runtime version",
setup: () => {
resolveCompatibilityHostVersionMock.mockReturnValueOnce("2026.3.21");
},
spec: "clawhub:demo",
expected: {
ok: false,
code: CLAWHUB_INSTALL_ERROR_CODE.INCOMPATIBLE_PLUGIN_API,
error:
'Plugin "demo" requires plugin API >=2026.3.22, but this OpenClaw runtime exposes 2026.3.21.',
},
},
{
name: "rejects skill families and redirects to skills install",
setup: () => {
fetchClawHubPackageDetailMock.mockResolvedValueOnce({
package: {
name: "calendar",
displayName: "Calendar",
family: "skill",
channel: "official",
isOfficial: true,
createdAt: 0,
updatedAt: 0,
},
});
},
spec: "clawhub:calendar",
expected: {
ok: false,
code: CLAWHUB_INSTALL_ERROR_CODE.SKILL_PACKAGE,
error: '"calendar" is a skill. Use "openclaw skills install calendar" instead.',
},
},
{
name: "returns typed package-not-found failures",
setup: () => {
fetchClawHubPackageDetailMock.mockRejectedValueOnce(
new ClawHubRequestError({
path: "/api/v1/packages/demo",
status: 404,
body: "Package not found",
}),
);
},
spec: "clawhub:demo",
expected: {
ok: false,
code: CLAWHUB_INSTALL_ERROR_CODE.PACKAGE_NOT_FOUND,
error: "Package not found on ClawHub.",
},
},
{
name: "returns typed version-not-found failures",
setup: () => {
parseClawHubPluginSpecMock.mockReturnValueOnce({ name: "demo", version: "9.9.9" });
fetchClawHubPackageVersionMock.mockRejectedValueOnce(
new ClawHubRequestError({
path: "/api/v1/packages/demo/versions/9.9.9",
status: 404,
body: "Version not found",
}),
);
},
spec: "clawhub:demo@9.9.9",
expected: {
ok: false,
code: CLAWHUB_INSTALL_ERROR_CODE.VERSION_NOT_FOUND,
error: "Version not found on ClawHub: demo@9.9.9.",
},
},
] as const)("$name", async ({ setup, spec, expected }) => {
await expectClawHubInstallError({ setup, spec, expected });
});
});