mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
288 lines
9.2 KiB
TypeScript
288 lines
9.2 KiB
TypeScript
/* @vitest-environment node */
|
|
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
buildPublishInspectorRunCheckOptions,
|
|
createPackageInspectorWorkspace,
|
|
normalizeInspectorReportForPublish,
|
|
preparePublishInspectorOpenClawTarget,
|
|
} from "./packageInspectorNode";
|
|
|
|
const originalPlatform = process.platform;
|
|
|
|
afterEach(() => {
|
|
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
});
|
|
|
|
describe("package inspector publish normalization", () => {
|
|
it("falls back to /tmp when the configured temp directory is unavailable", async () => {
|
|
Object.defineProperty(process, "platform", { value: "linux" });
|
|
const attemptedPrefixes: string[] = [];
|
|
const createTempDir = async (prefix: string) => {
|
|
attemptedPrefixes.push(prefix);
|
|
if (attemptedPrefixes.length === 1) {
|
|
throw Object.assign(new Error("configured temp directory is unavailable"), {
|
|
code: "ENOENT",
|
|
});
|
|
}
|
|
return `${prefix}workspace`;
|
|
};
|
|
|
|
await expect(
|
|
createPackageInspectorWorkspace("/home/sbx_user1051", createTempDir),
|
|
).resolves.toBe(path.join("/tmp", "clawhub-plugin-inspector-workspace"));
|
|
expect(attemptedPrefixes).toEqual([
|
|
path.join("/home/sbx_user1051", "clawhub-plugin-inspector-"),
|
|
path.join("/tmp", "clawhub-plugin-inspector-"),
|
|
]);
|
|
});
|
|
|
|
it("does not use the POSIX fallback on Windows", async () => {
|
|
Object.defineProperty(process, "platform", { value: "win32" });
|
|
const error = Object.assign(new Error("configured temp directory is unavailable"), {
|
|
code: "ENOENT",
|
|
});
|
|
const createTempDir = async () => {
|
|
throw error;
|
|
};
|
|
|
|
await expect(createPackageInspectorWorkspace("C:\\Temp", createTempDir)).rejects.toBe(error);
|
|
});
|
|
|
|
it("does not hide unrelated workspace creation errors", async () => {
|
|
const error = Object.assign(new Error("temporary storage failed"), { code: "EIO" });
|
|
const createTempDir = async () => {
|
|
throw error;
|
|
};
|
|
|
|
await expect(createPackageInspectorWorkspace("/home/sbx_user1051", createTempDir)).rejects.toBe(
|
|
error,
|
|
);
|
|
});
|
|
|
|
it("prepares latest stable OpenClaw with a cache outside the inspected package", async () => {
|
|
const resolvedTarget = { version: "2026.7.0" };
|
|
const preparedTarget = { status: "ok", version: "2026.7.0" };
|
|
const resolveVersion = vi.fn(async () => resolvedTarget);
|
|
const prepare = vi.fn(async () => preparedTarget);
|
|
|
|
await expect(
|
|
preparePublishInspectorOpenClawTarget("/tmp/plugin", { resolveVersion, prepare }),
|
|
).resolves.toBe(preparedTarget);
|
|
expect(resolveVersion).toHaveBeenCalledWith("latest");
|
|
expect(prepare).toHaveBeenCalledWith(resolvedTarget, {
|
|
cacheDir: path.join("/tmp/plugin", ".plugin-inspector-cache"),
|
|
});
|
|
expect(path.join("/tmp/plugin", ".plugin-inspector-cache")).not.toContain(
|
|
path.join("/tmp/plugin", "package") + path.sep,
|
|
);
|
|
});
|
|
|
|
it("uses the prepared OpenClaw target for publish-time inspection", () => {
|
|
const targetOpenClaw = { status: "ok", version: "2026.7.0" };
|
|
expect(
|
|
buildPublishInspectorRunCheckOptions(
|
|
"/tmp/plugin",
|
|
"2026-07-30T00:00:00.000Z",
|
|
targetOpenClaw,
|
|
),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
pluginRoot: "/tmp/plugin",
|
|
openclawPath: false,
|
|
targetOpenClaw,
|
|
authorFacing: true,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("keeps legacy author-facing hard findings without remediation metadata", () => {
|
|
const result = normalizeInspectorReportForPublish({
|
|
status: "fail",
|
|
summary: { breakageCount: 1, warningCount: 1, issueCount: 2 },
|
|
issues: [
|
|
{
|
|
code: "package-entrypoint-missing",
|
|
level: "breakage",
|
|
message: "declared OpenClaw entrypoint does not exist",
|
|
},
|
|
{
|
|
code: "runtime-tool-capture",
|
|
level: "warning",
|
|
message: "runtime tools need capture before contract judgment",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
status: "fail",
|
|
summary: {
|
|
breakageCount: 1,
|
|
warningCount: 0,
|
|
issueCount: 1,
|
|
},
|
|
breakages: [
|
|
{
|
|
code: "package-entrypoint-missing",
|
|
authorRemediation: {
|
|
summary:
|
|
"Publish the entrypoint declared in OpenClaw package metadata or update the metadata to point at an existing file.",
|
|
docsUrl:
|
|
"https://docs.openclaw.ai/clawhub/plugin-validation-fixes#package-entrypoint-missing",
|
|
},
|
|
},
|
|
],
|
|
warnings: [],
|
|
});
|
|
});
|
|
|
|
it("keeps legacy author-facing warnings and drops internal coverage findings", () => {
|
|
const result = normalizeInspectorReportForPublish({
|
|
status: "pass",
|
|
summary: { breakageCount: 0, warningCount: 3, issueCount: 3 },
|
|
warnings: [
|
|
{
|
|
code: "package-plugin-api-compat-missing",
|
|
level: "warning",
|
|
issueClass: "upstream-metadata",
|
|
message: "package.json is missing openclaw.compat.pluginApi",
|
|
},
|
|
{
|
|
code: "runtime-tool-capture",
|
|
level: "warning",
|
|
message: "runtime tools need capture before contract judgment",
|
|
},
|
|
{
|
|
code: "sdk-session-file-helper",
|
|
level: "warning",
|
|
issueClass: "deprecation-warning",
|
|
message: "deprecated session file-path helper is still used",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
status: "pass",
|
|
summary: {
|
|
breakageCount: 0,
|
|
warningCount: 2,
|
|
issueCount: 2,
|
|
},
|
|
warnings: [
|
|
{
|
|
code: "package-plugin-api-compat-missing",
|
|
authorRemediation: {
|
|
summary: "Declare the OpenClaw plugin API range this package supports.",
|
|
docsUrl:
|
|
"https://docs.openclaw.ai/clawhub/plugin-validation-fixes#package-plugin-api-compat-missing",
|
|
},
|
|
},
|
|
{
|
|
code: "sdk-session-file-helper",
|
|
authorRemediation: {
|
|
summary:
|
|
"Replace deprecated session file-path helpers with session entry and transcript identity APIs.",
|
|
docsUrl:
|
|
"https://docs.openclaw.ai/clawhub/plugin-validation-fixes#sdk-session-file-helper",
|
|
},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it("keeps missing-API findings with generic remediation and no docs URL", () => {
|
|
const result = normalizeInspectorReportForPublish({
|
|
status: "fail",
|
|
summary: { breakageCount: 1, warningCount: 0, issueCount: 1 },
|
|
issues: [
|
|
{
|
|
code: "missing-openclaw-api",
|
|
level: "breakage",
|
|
issueClass: "compatibility-error",
|
|
message: "registerMemoryRuntime is unavailable in the selected OpenClaw target",
|
|
authorRemediation: {
|
|
summary: "Replace this call with an API available in the selected OpenClaw version.",
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(result.breakages).toEqual([
|
|
expect.objectContaining({
|
|
code: "missing-openclaw-api",
|
|
authorRemediation: {
|
|
summary: "Replace this call with an API available in the selected OpenClaw version.",
|
|
docsUrl: undefined,
|
|
},
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("keeps static and compatibility findings with the exact resolved target", () => {
|
|
const result = normalizeInspectorReportForPublish({
|
|
status: "fail",
|
|
targetOpenClaw: {
|
|
requested: "beta",
|
|
version: "2026.8.0-beta.4",
|
|
},
|
|
issues: [
|
|
{
|
|
code: "package-entrypoint-missing",
|
|
level: "breakage",
|
|
issueClass: "package-integrity",
|
|
message: "declared OpenClaw entrypoint does not exist",
|
|
},
|
|
{
|
|
code: "missing-openclaw-api",
|
|
level: "breakage",
|
|
issueClass: "compatibility-error",
|
|
message: "registerMemoryRuntime is unavailable in the selected OpenClaw target",
|
|
authorRemediation: {
|
|
summary: "Replace this call with an API available in the selected OpenClaw version.",
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(result.metadata.targetOpenClawVersion).toBe("2026.8.0-beta.4");
|
|
expect(result.breakages.map((finding) => finding.code)).toEqual([
|
|
"package-entrypoint-missing",
|
|
"missing-openclaw-api",
|
|
]);
|
|
});
|
|
|
|
it("keeps out-of-range compatibility information non-blocking", () => {
|
|
const result = normalizeInspectorReportForPublish({
|
|
status: "pass",
|
|
targetOpenClaw: { version: "2026.7.2-beta.4" },
|
|
issues: [
|
|
{
|
|
code: "unknown-registration-name",
|
|
level: "suggestion",
|
|
severity: "P2",
|
|
issueClass: "compatibility-information",
|
|
message:
|
|
"plugin calls registrars missing from target OpenClaw outside its declared range",
|
|
authorRemediation: {
|
|
summary: "Widen the declared range only after updating these registrations.",
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
status: "pass",
|
|
summary: { breakageCount: 0, warningCount: 1 },
|
|
breakages: [],
|
|
warnings: [
|
|
{
|
|
code: "unknown-registration-name",
|
|
level: "suggestion",
|
|
issueClass: "compatibility-information",
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|