test(qa): cover ClawHub package compatibility (#118985)

This commit is contained in:
Vincent Koc
2026-08-04 08:29:29 +08:00
committed by GitHub
parent 945597bb81
commit 07a9856cce
2 changed files with 80 additions and 4 deletions

View File

@@ -0,0 +1,25 @@
title: ClawHub package compatibility validation
scenario:
id: clawhub-package-compatibility
surface: clawhub
category: clawhub.compatibility-and-trust
coverage:
primary:
- clawhub.clawhub-package-compatibility-validation
objective: Verify ClawHub plugin compatibility metadata is enforced before trust checks or artifact download.
successCriteria:
- Incompatible package-level plugin API metadata fails visibly before trust checks or download.
- Incompatible version-level minimum Gateway metadata fails visibly before trust checks or download.
- Rejected installs leave no install record or telemetry.
- Compatible metadata downloads and installs the plugin, persists its install record, and reports telemetry.
docsRefs:
- docs/cli/plugins.md
- docs/plugins/manifest.md
codeRefs:
- src/cli/plugins-cli.clawhub-install.e2e.test.ts
- src/plugins/clawhub.ts
execution:
kind: vitest
path: src/cli/plugins-cli.clawhub-install.e2e.test.ts
summary: Runs real CLI installs against a local ClawHub HTTP fixture and checks compatibility rejection ordering plus compatible install side effects.

View File

@@ -6,13 +6,16 @@ import type { AddressInfo } from "node:net";
import os from "node:os";
import path from "node:path";
import JSZip from "jszip";
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
import { loadInstalledPluginIndexInstallRecords } from "../plugins/installed-plugin-index-records.js";
const PACKAGE_NAME = "@openclaw/telemetry-demo";
const PACKAGE_VERSION = "1.0.0";
const PLUGIN_ID = "telemetry-demo";
const ENCODED_PACKAGE_NAME = encodeURIComponent(PACKAGE_NAME);
const PACKAGE_API_PATH = `/api/v1/packages/${ENCODED_PACKAGE_NAME}`;
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
async function readRequestBody(req: IncomingMessage): Promise<string> {
const chunks: Buffer[] = [];
@@ -71,6 +74,8 @@ async function buildPluginZip(): Promise<Buffer> {
type TestServerOptions = {
artifactSha256?: string;
artifactCompatibility?: Record<string, string> | null;
packageCompatibility?: Record<string, string>;
telemetryStatus?: number;
};
@@ -84,7 +89,7 @@ async function startClawHubServer(options: TestServerOptions = {}) {
async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
const url = new URL(req.url ?? "/", "http://127.0.0.1");
requestLog.push(`${req.method ?? "GET"} ${url.pathname}`);
const packagePath = `/api/v1/packages/${ENCODED_PACKAGE_NAME}`;
const packagePath = PACKAGE_API_PATH;
if (req.method === "GET" && url.pathname === packagePath) {
res.writeHead(200, { "Content-Type": "application/json" });
@@ -99,7 +104,7 @@ async function startClawHubServer(options: TestServerOptions = {}) {
isOfficial: false,
latestVersion: PACKAGE_VERSION,
tags: { latest: PACKAGE_VERSION },
compatibility: {},
compatibility: options.packageCompatibility ?? {},
},
owner: { handle: "openclaw" },
}),
@@ -124,7 +129,9 @@ async function startClawHubServer(options: TestServerOptions = {}) {
createdAt: 1,
changelog: "Initial release",
sha256hash: artifactSha256,
compatibility: {},
...(options.artifactCompatibility === null
? {}
: { compatibility: options.artifactCompatibility ?? {} }),
},
}),
);
@@ -257,6 +264,10 @@ describe("openclaw plugins install ClawHub E2E", () => {
version: PACKAGE_VERSION,
},
]);
expect(testServer.requestLog).toContain(
`GET ${PACKAGE_API_PATH}/versions/${PACKAGE_VERSION}/security`,
);
expect(testServer.requestLog).toContain(`GET ${PACKAGE_API_PATH}/download`);
const repeat = await spawnOpenClaw(
["plugins", "install", `clawhub:${PACKAGE_NAME}@${PACKAGE_VERSION}`, "--force"],
@@ -271,6 +282,46 @@ describe("openclaw plugins install ClawHub E2E", () => {
}
}, 60_000);
it.each([
{
label: "package plugin API",
options: {
packageCompatibility: { pluginApiRange: ">=9999.0.0" },
artifactCompatibility: null,
},
error: "requires plugin API >=9999.0.0",
},
{
label: "version gateway",
options: { artifactCompatibility: { minGatewayVersion: "9999.0.0" } },
error: "requires OpenClaw >=9999.0.0",
},
])(
"rejects incompatible $label metadata before trust and download",
async ({ options, error }) => {
const testServer = await startClawHubServer(options);
const stateDir = tempDirs.make("openclaw-plugin-compatibility-e2e-");
try {
const result = await spawnOpenClaw(
["plugins", "install", `clawhub:${PACKAGE_NAME}@${PACKAGE_VERSION}`],
{ cwd: process.cwd(), env: buildEnv(stateDir, testServer.registry) },
);
expect(result.status).not.toBe(0);
expect(`${result.stdout}\n${result.stderr}`).toContain(error);
expect(testServer.requestLog).not.toContain(
`GET ${PACKAGE_API_PATH}/versions/${PACKAGE_VERSION}/security`,
);
expect(testServer.requestLog).not.toContain(`GET ${PACKAGE_API_PATH}/download`);
expect(testServer.telemetryBodies).toEqual([]);
await expect(readPersistedInstallRecord(stateDir)).resolves.toBeUndefined();
} finally {
await testServer.close();
}
},
30_000,
);
it("does not report success when plugin installation fails", async () => {
const testServer = await startClawHubServer({ artifactSha256: "0".repeat(64) });
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-plugin-telemetry-fail-"));