mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
feat: validate plugins against stable OpenClaw (#3321)
* test: preserve generic plugin inspector remediation * feat: show exact plugin validation reproduction command * feat: reproduce plugin findings against exact target * test: preserve static and compatibility findings * feat: validate plugins against stable OpenClaw * test: seed reproducible plugin findings * chore: pin merged plugin inspector * fix: preserve mixed validation targets * fix: show every validation target * fix: label findings with validation target
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
"@fontsource/noto-sans-sc": "5.3.0",
|
||||
"@monaco-editor/react": "4.7.0",
|
||||
"@openclaw/carapace": "git+https://github.com/openclaw/carapace.git#v0.2.0",
|
||||
"@openclaw/plugin-inspector": "0.3.17",
|
||||
"@openclaw/plugin-inspector": "github:openclaw/plugin-inspector#021c6586af78884d27f1be8acedca492acd9f9ae",
|
||||
"@radix-ui/react-avatar": "1.2.3",
|
||||
"@radix-ui/react-dialog": "1.1.20",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.21",
|
||||
@@ -108,7 +108,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "1.7.0",
|
||||
"@openclaw/plugin-inspector": "0.3.17",
|
||||
"@openclaw/plugin-inspector": "github:openclaw/plugin-inspector#021c6586af78884d27f1be8acedca492acd9f9ae",
|
||||
"arktype": "2.2.3",
|
||||
"commander": "15.0.0",
|
||||
"croner": "10.0.1",
|
||||
@@ -404,6 +404,8 @@
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="],
|
||||
|
||||
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
@@ -452,7 +454,7 @@
|
||||
|
||||
"@openclaw/clawhub-admin": ["@openclaw/clawhub-admin@workspace:packages/clawhub-admin"],
|
||||
|
||||
"@openclaw/plugin-inspector": ["@openclaw/plugin-inspector@0.3.17", "", { "bin": { "plugin-inspector": "src/cli.js" } }, "sha512-JPPHPhiXMsIvrV8UR8RQjhflMjRZX/uIhy9meE81dup7MMSnRJcsTGOXYACohv6e4z2P95z2QuE7nZkWT6Ysuw=="],
|
||||
"@openclaw/plugin-inspector": ["@openclaw/plugin-inspector@github:openclaw/plugin-inspector#021c658", { "dependencies": { "semver": "^7.8.5", "tar": "^7.5.22" }, "bin": { "plugin-inspector": "src/cli.js" } }, "openclaw-plugin-inspector-021c658", "sha512-Cz3NDscaCxvySTNmBz1mQnImKHWdmk50da1VhZnQYKE/EK3qlV/Z/UpYo/7440EEQJ7TQ90V5Eyr6I2811dp6Q=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -1096,6 +1098,8 @@
|
||||
|
||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||
|
||||
"citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="],
|
||||
|
||||
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||
@@ -1606,6 +1610,8 @@
|
||||
|
||||
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
||||
|
||||
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
|
||||
|
||||
"monaco-editor": ["monaco-editor@0.56.0", "", { "dependencies": { "dompurify": "3.4.8", "marked": "14.0.0" } }, "sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
@@ -1882,6 +1888,8 @@
|
||||
|
||||
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||
|
||||
"tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="],
|
||||
|
||||
"thread-stream": ["thread-stream@4.2.0", "", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="],
|
||||
|
||||
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
||||
@@ -2002,7 +2010,7 @@
|
||||
|
||||
"xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
|
||||
|
||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||
|
||||
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||
|
||||
@@ -2146,6 +2154,8 @@
|
||||
|
||||
"vitest/vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="],
|
||||
|
||||
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="],
|
||||
|
||||
"@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="],
|
||||
|
||||
@@ -1374,12 +1374,20 @@ describe("devSeed local fixtures", () => {
|
||||
packageName: scannedPluginName,
|
||||
findingKind: "warning",
|
||||
code: "legacy-before-agent-start",
|
||||
targetOpenClawVersion: "2026.3.24-beta.2",
|
||||
authorRemediation: {
|
||||
summary: "Replace the legacy before_agent_start hook with the current lifecycle API.",
|
||||
},
|
||||
}),
|
||||
expect.objectContaining({
|
||||
packageName: scannedPluginName,
|
||||
findingKind: "error",
|
||||
code: "missing-expected-seam",
|
||||
scanSource: "nightly",
|
||||
targetOpenClawVersion: "2026.4.0",
|
||||
authorRemediation: {
|
||||
summary: "Replace registerTool with an API available in the selected OpenClaw version.",
|
||||
},
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -3703,6 +3703,9 @@ export async function seedLocalModerationFixturesHandler(
|
||||
deprecated: true,
|
||||
message: "legacy before_agent_start hook is deprecated for the current OpenClaw plugin API",
|
||||
evidence: ["src/index.ts:4", "hook:before_agent_start"],
|
||||
authorRemediation: {
|
||||
summary: "Replace the legacy before_agent_start hook with the current lifecycle API.",
|
||||
},
|
||||
inspectorFindingId: "local-scanned-runtime-plugin:legacy-before-agent-start",
|
||||
createdAt: now,
|
||||
});
|
||||
@@ -3723,6 +3726,9 @@ export async function seedLocalModerationFixturesHandler(
|
||||
issueClass: "compatibility-error",
|
||||
message: "registerTool is no longer available on the target OpenClaw compatibility surface",
|
||||
evidence: ["src/index.ts:12", "target:OpenClaw 2026.4.0"],
|
||||
authorRemediation: {
|
||||
summary: "Replace registerTool with an API available in the selected OpenClaw version.",
|
||||
},
|
||||
inspectorFindingId: "local-scanned-runtime-plugin:missing-expected-seam",
|
||||
createdAt: now + 1,
|
||||
});
|
||||
|
||||
@@ -306,7 +306,9 @@ describe("moderation notification email copy", () => {
|
||||
expect(email.text).toContain("OpenClaw Version: 0.9.0");
|
||||
expect(email.text).toContain("Address the findings below in your plugin package.");
|
||||
expect(email.text).toContain("Run the validation command locally against your changes.");
|
||||
expect(email.text).toContain("clawhub package validate <path-to-plugin>");
|
||||
expect(email.text).toContain(
|
||||
"clawhub package validate <path-to-plugin> --openclaw-version 0.9.0",
|
||||
);
|
||||
expect(email.text).toContain(
|
||||
"- **WARNING** `legacy-before-agent-start` (deprecation-warning, P2)",
|
||||
);
|
||||
@@ -330,7 +332,9 @@ describe("moderation notification email copy", () => {
|
||||
expectFooterLinksUnderlined(email.html);
|
||||
expect(email.html).toContain("OpenClaw Version");
|
||||
expect(email.html).toContain("0.9.0");
|
||||
expect(email.html).toContain("clawhub package validate <path-to-plugin>");
|
||||
expect(email.html).toContain(
|
||||
"clawhub package validate <path-to-plugin> --openclaw-version 0.9.0",
|
||||
);
|
||||
expect(email.html).toContain("legacy-before-agent-start");
|
||||
expect(email.html).toContain("legacy-before-agent-start · deprecation-warning · P2");
|
||||
expect(email.html).toContain("Fix");
|
||||
@@ -350,6 +354,49 @@ describe("moderation notification email copy", () => {
|
||||
expect(email.html).not.toContain("published successfully");
|
||||
});
|
||||
|
||||
it("includes one exact validation command per recorded OpenClaw target", async () => {
|
||||
const email = await buildPackageInspectorFindingsEmail({
|
||||
packageName: "demo-plugin",
|
||||
version: "1.0.0",
|
||||
findings: [
|
||||
{
|
||||
findingKind: "warning",
|
||||
code: "legacy-before-agent-start",
|
||||
message: "legacy hook is deprecated",
|
||||
targetOpenClawVersion: "0.9.0",
|
||||
},
|
||||
{
|
||||
findingKind: "error",
|
||||
code: "missing-expected-seam",
|
||||
message: "registerTool is no longer available",
|
||||
targetOpenClawVersion: "0.10.0",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(email.text).toContain("OpenClaw Versions: 0.9.0, 0.10.0");
|
||||
expect(email.text).toContain(
|
||||
"clawhub package validate <path-to-plugin> --openclaw-version 0.9.0",
|
||||
);
|
||||
expect(email.text).toContain(
|
||||
"clawhub package validate <path-to-plugin> --openclaw-version 0.10.0",
|
||||
);
|
||||
expect(email.text).toContain(
|
||||
"`legacy-before-agent-start`\n legacy hook is deprecated\n OpenClaw target: 0.9.0",
|
||||
);
|
||||
expect(email.text).toContain(
|
||||
"`missing-expected-seam`\n registerTool is no longer available\n OpenClaw target: 0.10.0",
|
||||
);
|
||||
expect(email.html).toContain(
|
||||
"clawhub package validate <path-to-plugin> --openclaw-version 0.9.0",
|
||||
);
|
||||
expect(email.html).toContain(
|
||||
"clawhub package validate <path-to-plugin> --openclaw-version 0.10.0",
|
||||
);
|
||||
expect(email.html).toContain("legacy-before-agent-start · OpenClaw 0.9.0");
|
||||
expect(email.html).toContain("missing-expected-seam · OpenClaw 0.10.0");
|
||||
});
|
||||
|
||||
it("builds plugin inspector error copy without publish-time wording", async () => {
|
||||
const email = await buildPackageInspectorFindingsEmail({
|
||||
packageName: "demo-plugin",
|
||||
@@ -372,7 +419,9 @@ describe("moderation notification email copy", () => {
|
||||
expect(email.text).toContain("We found 1 issue with version 1.0.1 of demo-plugin.");
|
||||
expect(email.text).toContain("Address the findings below in your plugin package.");
|
||||
expect(email.text).toContain("Run the validation command locally against your changes.");
|
||||
expect(email.text).toContain("clawhub package validate <path-to-plugin>");
|
||||
expect(email.text).toContain(
|
||||
"clawhub package validate <path-to-plugin> --openclaw-version 0.10.0",
|
||||
);
|
||||
expect(email.text).toContain("- **ERROR** `missing-expected-seam` (compatibility-error, P0)");
|
||||
expect(email.text).not.toContain("Your plugin was published");
|
||||
expect(email.text).not.toContain("was published, but");
|
||||
|
||||
+27
-10
@@ -324,8 +324,9 @@ async function renderSecretBlockedPublishTemplate(args: {
|
||||
return rendered.html;
|
||||
}
|
||||
|
||||
function buildPluginValidateCommand() {
|
||||
return "clawhub package validate <path-to-plugin>";
|
||||
function buildPluginValidateCommand(openClawVersion?: string) {
|
||||
const command = "clawhub package validate <path-to-plugin>";
|
||||
return openClawVersion ? `${command} --openclaw-version ${openClawVersion}` : command;
|
||||
}
|
||||
|
||||
function normalizeEmailFindingSummary(value: string | undefined) {
|
||||
@@ -534,10 +535,16 @@ export async function buildSecretBlockedPublishEmail(args: SecretBlockedPublishE
|
||||
}
|
||||
|
||||
export async function buildPackageInspectorFindingsEmail(args: PackageInspectorFindingsEmailArgs) {
|
||||
const targetOpenClawVersion = args.findings.find(
|
||||
(finding) => finding.targetOpenClawVersion,
|
||||
)?.targetOpenClawVersion;
|
||||
const validateCommand = buildPluginValidateCommand();
|
||||
const targetOpenClawVersions = Array.from(
|
||||
new Set(
|
||||
args.findings
|
||||
.map((finding) => finding.targetOpenClawVersion?.trim())
|
||||
.filter((target): target is string => Boolean(target)),
|
||||
),
|
||||
);
|
||||
const validateCommands = targetOpenClawVersions.length
|
||||
? targetOpenClawVersions.map(buildPluginValidateCommand)
|
||||
: [buildPluginValidateCommand()];
|
||||
const subject = `Plugin Inspector findings for ${args.packageName}@${args.version}`;
|
||||
const findingCount = args.findings.length;
|
||||
const intro = `We found ${findingCount} ${findingCount === 1 ? "issue" : "issues"} with version ${args.version} of ${args.packageName}.`;
|
||||
@@ -549,7 +556,9 @@ export async function buildPackageInspectorFindingsEmail(args: PackageInspectorF
|
||||
const findingLines = formatPackageInspectorFindingsText(args.findings);
|
||||
const metadataLines = [
|
||||
`Plugin: ${args.packageName}@${args.version}`,
|
||||
targetOpenClawVersion ? `OpenClaw Version: ${targetOpenClawVersion}` : null,
|
||||
targetOpenClawVersions.length
|
||||
? `OpenClaw Version${targetOpenClawVersions.length === 1 ? "" : "s"}: ${targetOpenClawVersions.join(", ")}`
|
||||
: null,
|
||||
].filter((line): line is string => line !== null);
|
||||
const lines = [
|
||||
greeting(args.handle),
|
||||
@@ -565,23 +574,28 @@ export async function buildPackageInspectorFindingsEmail(args: PackageInspectorF
|
||||
...findingLines,
|
||||
"",
|
||||
"Validate a local fix:",
|
||||
validateCommand,
|
||||
...validateCommands,
|
||||
];
|
||||
|
||||
const { renderPluginInspectorFindingsEmail } = await import("./emailRendering");
|
||||
const rendered = await renderPluginInspectorFindingsEmail({
|
||||
packageName: args.packageName,
|
||||
version: args.version,
|
||||
...(targetOpenClawVersion ? { openClawVersion: targetOpenClawVersion } : {}),
|
||||
...(targetOpenClawVersions.length
|
||||
? { openClawVersion: targetOpenClawVersions.join(", ") }
|
||||
: {}),
|
||||
findings: args.findings.map((finding) => ({
|
||||
code: finding.code,
|
||||
kind: finding.findingKind,
|
||||
meta: [finding.code, finding.issueClass, finding.severity].filter(Boolean).join(" · "),
|
||||
message: finding.message,
|
||||
...(finding.targetOpenClawVersion
|
||||
? { targetOpenClawVersion: finding.targetOpenClawVersion }
|
||||
: {}),
|
||||
...(finding.authorRemediation?.summary ? { fix: finding.authorRemediation.summary } : {}),
|
||||
...(finding.authorRemediation?.docsUrl ? { docsUrl: finding.authorRemediation.docsUrl } : {}),
|
||||
})),
|
||||
validateCommand,
|
||||
validateCommands,
|
||||
preheader: intro,
|
||||
});
|
||||
|
||||
@@ -672,6 +686,9 @@ function formatPackageInspectorFindingsText(findings: PackageInspectorEmailFindi
|
||||
`- **${finding.findingKind.toUpperCase()}** \`${finding.code}\`${formatFindingMetaText(finding)}`,
|
||||
` ${finding.message}`,
|
||||
];
|
||||
if (finding.targetOpenClawVersion) {
|
||||
lines.push(` OpenClaw target: ${finding.targetOpenClawVersion}`);
|
||||
}
|
||||
if (finding.authorRemediation?.summary) {
|
||||
lines.push(" Fix:");
|
||||
lines.push(` ${finding.authorRemediation.summary}`);
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ declare module "@openclaw/plugin-inspector" {
|
||||
runCheck(options?: {
|
||||
pluginRoot?: string;
|
||||
openclawPath?: string | false;
|
||||
openclawVersion?: string;
|
||||
outDir?: string;
|
||||
capture?: boolean;
|
||||
mockSdk?: boolean;
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
/* @vitest-environment node */
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { normalizeInspectorReportForPublish } from "./packageInspectorNode";
|
||||
import {
|
||||
buildPublishInspectorRunCheckOptions,
|
||||
normalizeInspectorReportForPublish,
|
||||
} from "./packageInspectorNode";
|
||||
|
||||
describe("package inspector publish normalization", () => {
|
||||
it("targets latest stable OpenClaw for publish-time inspection", () => {
|
||||
expect(buildPublishInspectorRunCheckOptions("/tmp/plugin", "2026-07-30T00:00:00.000Z")).toEqual(
|
||||
expect.objectContaining({
|
||||
pluginRoot: "/tmp/plugin",
|
||||
openclawPath: false,
|
||||
openclawVersion: "latest",
|
||||
authorFacing: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps legacy author-facing hard findings without remediation metadata", () => {
|
||||
const result = normalizeInspectorReportForPublish({
|
||||
status: "fail",
|
||||
@@ -97,4 +111,98 @@ describe("package inspector publish normalization", () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
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",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -127,6 +127,23 @@ const inspectorMetadataValidator = v.object({
|
||||
targetOpenClawVersion: v.optional(v.string()),
|
||||
});
|
||||
|
||||
export function buildPublishInspectorRunCheckOptions(root: string, generatedAt: string) {
|
||||
return {
|
||||
pluginRoot: root,
|
||||
openclawPath: false,
|
||||
openclawVersion: "latest",
|
||||
outDir: "reports",
|
||||
capture: false,
|
||||
mockSdk: true,
|
||||
allowExecution: false,
|
||||
authorFacing: true,
|
||||
generatedAt,
|
||||
} as Parameters<(typeof import("@openclaw/plugin-inspector"))["pluginRoot"]["runCheck"]>[0] & {
|
||||
authorFacing: true;
|
||||
generatedAt: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const runPackageInspectorForPublishInternal = internalAction({
|
||||
args: {
|
||||
packageName: v.string(),
|
||||
@@ -164,19 +181,7 @@ export const runPackageInspectorForPublishInternal = internalAction({
|
||||
await writeSyntheticInspectorConfigIfNeeded(root, args.files, args.packageName);
|
||||
|
||||
const { pluginRoot } = await import("@openclaw/plugin-inspector");
|
||||
const runCheckOptions = {
|
||||
pluginRoot: root,
|
||||
openclawPath: false,
|
||||
outDir: "reports",
|
||||
capture: false,
|
||||
mockSdk: true,
|
||||
allowExecution: false,
|
||||
authorFacing: true,
|
||||
generatedAt: new Date().toISOString(),
|
||||
} as Parameters<typeof pluginRoot.runCheck>[0] & {
|
||||
authorFacing: true;
|
||||
generatedAt: string;
|
||||
};
|
||||
const runCheckOptions = buildPublishInspectorRunCheckOptions(root, new Date().toISOString());
|
||||
const { report } = await pluginRoot.runCheck(runCheckOptions);
|
||||
|
||||
return normalizeInspectorReportForPublish(report);
|
||||
|
||||
@@ -11391,6 +11391,10 @@ describe("packages public queries", () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
inspectorVersion: "0.3.19",
|
||||
targetOpenClawVersion: "2026.7.1-2",
|
||||
},
|
||||
})),
|
||||
scheduler: {
|
||||
runAfter: vi.fn(),
|
||||
@@ -11475,6 +11479,8 @@ describe("packages public queries", () => {
|
||||
releaseId: "packageReleases:demo-1",
|
||||
packageName: "demo-plugin",
|
||||
version: "1.0.0",
|
||||
inspectorVersion: "0.3.19",
|
||||
targetOpenClawVersion: "2026.7.1-2",
|
||||
findings: [
|
||||
expect.objectContaining({
|
||||
code: "legacy-before-agent-start",
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
|
||||
export type PluginInspectorFindingEmailItem = FindingCardProps & {
|
||||
code: string;
|
||||
targetOpenClawVersion?: string;
|
||||
};
|
||||
|
||||
export type PluginInspectorFindingsEmailProps = {
|
||||
@@ -19,7 +20,7 @@ export type PluginInspectorFindingsEmailProps = {
|
||||
version: string;
|
||||
openClawVersion?: string;
|
||||
findings: PluginInspectorFindingEmailItem[];
|
||||
validateCommand: string;
|
||||
validateCommands: string[];
|
||||
preheader: string;
|
||||
};
|
||||
|
||||
@@ -28,7 +29,7 @@ export default function PluginInspectorFindingsEmail({
|
||||
version,
|
||||
openClawVersion,
|
||||
findings,
|
||||
validateCommand,
|
||||
validateCommands,
|
||||
preheader,
|
||||
}: PluginInspectorFindingsEmailProps) {
|
||||
const issueText = `${findings.length} ${findings.length === 1 ? "issue" : "issues"}`;
|
||||
@@ -44,11 +45,21 @@ export default function PluginInspectorFindingsEmail({
|
||||
]}
|
||||
/>
|
||||
<HeadingLabel>Findings</HeadingLabel>
|
||||
{findings.map((finding) => (
|
||||
<FindingCard key={finding.code} {...finding} />
|
||||
{findings.map((finding, index) => (
|
||||
<FindingCard
|
||||
key={`${finding.code}:${finding.targetOpenClawVersion ?? index}`}
|
||||
{...finding}
|
||||
meta={
|
||||
finding.targetOpenClawVersion
|
||||
? `${finding.meta} · OpenClaw ${finding.targetOpenClawVersion}`
|
||||
: finding.meta
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<HeadingLabel>Validate a local fix</HeadingLabel>
|
||||
<CodeBox>{validateCommand}</CodeBox>
|
||||
{validateCommands.map((command) => (
|
||||
<CodeBox key={command}>{command}</CodeBox>
|
||||
))}
|
||||
</ClawHubEmailLayout>
|
||||
);
|
||||
}
|
||||
@@ -72,7 +83,7 @@ PluginInspectorFindingsEmail.PreviewProps = {
|
||||
packageName: "demo-plugin",
|
||||
version: "1.0.0",
|
||||
openClawVersion: "2026.4.0",
|
||||
validateCommand: "clawhub package validate <path-to-plugin>",
|
||||
validateCommands: ["clawhub package validate <path-to-plugin>"],
|
||||
preheader: "Plugin Inspector found 1 issue with demo-plugin@1.0.0.",
|
||||
findings: [
|
||||
{
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@
|
||||
"@fontsource/noto-sans-sc": "5.3.0",
|
||||
"@monaco-editor/react": "4.7.0",
|
||||
"@openclaw/carapace": "git+https://github.com/openclaw/carapace.git#v0.2.0",
|
||||
"@openclaw/plugin-inspector": "0.3.17",
|
||||
"@openclaw/plugin-inspector": "github:openclaw/plugin-inspector#021c6586af78884d27f1be8acedca492acd9f9ae",
|
||||
"@radix-ui/react-avatar": "1.2.3",
|
||||
"@radix-ui/react-dialog": "1.1.20",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.21",
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "1.7.0",
|
||||
"@openclaw/plugin-inspector": "0.3.17",
|
||||
"@openclaw/plugin-inspector": "github:openclaw/plugin-inspector#021c6586af78884d27f1be8acedca492acd9f9ae",
|
||||
"arktype": "2.2.3",
|
||||
"commander": "15.0.0",
|
||||
"croner": "10.0.1",
|
||||
|
||||
@@ -630,6 +630,7 @@ registerCommand(packageCmd, ["package", "validate"])
|
||||
.argument("<source>", "Package folder path")
|
||||
.option("--out <dir>", "Directory for Plugin Inspector reports", "reports")
|
||||
.option("--openclaw <path>", "Optional local OpenClaw checkout to inspect against")
|
||||
.option("--openclaw-version <version>", "OpenClaw target: latest, beta, or an exact version")
|
||||
.option("--runtime", "Enable runtime capture; imports plugin code")
|
||||
.option("--allow-execute", "Allow runtime capture in an isolated workspace")
|
||||
.option("--no-mock-sdk", "Disable mocked OpenClaw SDK during runtime capture")
|
||||
|
||||
@@ -258,6 +258,7 @@ describe("package commands", () => {
|
||||
configPath: expect.stringContaining("plugin-inspector.config.json"),
|
||||
mockSdk: true,
|
||||
openclawPath: false,
|
||||
openclawVersion: "latest",
|
||||
outDir: "reports",
|
||||
pluginRoot: folder,
|
||||
authorFacing: true,
|
||||
@@ -276,6 +277,51 @@ describe("package commands", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["latest", "beta", "2026.7.2-beta.4"])(
|
||||
"validates against the requested OpenClaw target %s",
|
||||
async (openclawVersion) => {
|
||||
const workdir = await makeTmpWorkdir();
|
||||
try {
|
||||
const folder = join(workdir, "targeted-plugin");
|
||||
await mkdir(folder, { recursive: true });
|
||||
await writeFile(
|
||||
join(folder, "package.json"),
|
||||
'{"name":"targeted-plugin","version":"1.0.0"}\n',
|
||||
);
|
||||
inspectorMocks.pluginRoot.runCheck.mockResolvedValueOnce({
|
||||
report: { status: "pass", summary: { breakageCount: 0 } },
|
||||
paths: { jsonPath: join(folder, "reports", "plugin-inspector-report.json") },
|
||||
});
|
||||
|
||||
await cmdValidatePackage(makeOpts(workdir), "targeted-plugin", { openclawVersion });
|
||||
|
||||
expect(inspectorMocks.pluginRoot.runCheck).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ openclawVersion }),
|
||||
);
|
||||
} finally {
|
||||
await rm(workdir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects combining a local OpenClaw checkout with a version target", async () => {
|
||||
const workdir = await makeTmpWorkdir();
|
||||
try {
|
||||
const folder = join(workdir, "targeted-plugin");
|
||||
await mkdir(folder, { recursive: true });
|
||||
|
||||
await expect(
|
||||
cmdValidatePackage(makeOpts(workdir), "targeted-plugin", {
|
||||
openclaw: "../openclaw",
|
||||
openclawVersion: "latest",
|
||||
}),
|
||||
).rejects.toThrow("Choose either --openclaw or --openclaw-version");
|
||||
expect(inspectorMocks.pluginRoot.runCheck).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
await rm(workdir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("fails package validation when Plugin Inspector reports hard breakages", async () => {
|
||||
const workdir = await makeTmpWorkdir();
|
||||
try {
|
||||
@@ -340,6 +386,104 @@ describe("package commands", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps missing-API findings with generic remediation and no docs URL", async () => {
|
||||
const workdir = await makeTmpWorkdir();
|
||||
try {
|
||||
const folder = join(workdir, "removed-api-plugin");
|
||||
await mkdir(folder, { recursive: true });
|
||||
await writeFile(
|
||||
join(folder, "package.json"),
|
||||
'{"name":"removed-api-plugin","version":"1.0.0"}\n',
|
||||
);
|
||||
inspectorMocks.pluginRoot.runCheck.mockResolvedValueOnce({
|
||||
report: {
|
||||
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.",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
paths: { jsonPath: join(folder, "reports", "plugin-inspector-report.json") },
|
||||
});
|
||||
|
||||
await expect(cmdValidatePackage(makeOpts(workdir), "removed-api-plugin", {})).rejects.toThrow(
|
||||
"Plugin Inspector found 1 hard error",
|
||||
);
|
||||
|
||||
const output = mockLog.mock.calls.join("\n");
|
||||
expect(output).toContain("ERROR missing-openclaw-api (compatibility-error)");
|
||||
expect(output).toContain(
|
||||
"Fix: Replace this call with an API available in the selected OpenClaw version.",
|
||||
);
|
||||
expect(output).not.toContain("Docs:");
|
||||
} finally {
|
||||
await rm(workdir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("surfaces Honcho-equivalent removed registrations against the affected exact beta", async () => {
|
||||
const workdir = await makeTmpWorkdir();
|
||||
try {
|
||||
const folder = join(workdir, "honcho-plugin");
|
||||
await mkdir(folder, { recursive: true });
|
||||
await writeFile(join(folder, "package.json"), '{"name":"honcho-plugin","version":"1.0.0"}\n');
|
||||
inspectorMocks.pluginRoot.runCheck.mockResolvedValueOnce({
|
||||
report: {
|
||||
status: "fail",
|
||||
targetOpenClaw: {
|
||||
requestedVersion: "2026.7.2-beta.4",
|
||||
version: "2026.7.2-beta.4",
|
||||
eligibilityVersion: "2026.7.2",
|
||||
},
|
||||
summary: { breakageCount: 1, warningCount: 0, issueCount: 1 },
|
||||
issues: [
|
||||
{
|
||||
code: "unknown-registration-name",
|
||||
level: "breakage",
|
||||
severity: "P0",
|
||||
issueClass: "live-issue",
|
||||
message: "fixture calls registrars missing from target OpenClaw",
|
||||
evidence: [
|
||||
"registerMemoryPromptSection @ index.ts:97",
|
||||
"registerMemoryRuntime @ runtime.ts:276",
|
||||
],
|
||||
authorRemediation: {
|
||||
summary:
|
||||
"Update the plugin to use APIs available in the target OpenClaw version, or narrow its declared compatibility range.",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
paths: { jsonPath: join(folder, "reports", "plugin-inspector-report.json") },
|
||||
});
|
||||
|
||||
await expect(
|
||||
cmdValidatePackage(makeOpts(workdir), "honcho-plugin", {
|
||||
openclawVersion: "2026.7.2-beta.4",
|
||||
}),
|
||||
).rejects.toThrow("Plugin Inspector found 1 hard error");
|
||||
|
||||
expect(inspectorMocks.pluginRoot.runCheck).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ openclawVersion: "2026.7.2-beta.4" }),
|
||||
);
|
||||
const output = mockLog.mock.calls.join("\n");
|
||||
expect(output).toContain("registerMemoryPromptSection @ index.ts:97");
|
||||
expect(output).toContain("registerMemoryRuntime @ runtime.ts:276");
|
||||
expect(output).not.toContain("Docs:");
|
||||
} finally {
|
||||
await rm(workdir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("prints author-facing package validation findings before report paths by default", async () => {
|
||||
const workdir = await makeTmpWorkdir();
|
||||
try {
|
||||
|
||||
@@ -175,6 +175,7 @@ type PackagePackOptions = {
|
||||
type PackageValidateOptions = {
|
||||
out?: string;
|
||||
openclaw?: string;
|
||||
openclawVersion?: string;
|
||||
runtime?: boolean;
|
||||
allowExecute?: boolean;
|
||||
mockSdk?: boolean;
|
||||
@@ -716,9 +717,13 @@ export async function cmdValidatePackage(
|
||||
const sourcePath = resolvedSource.path;
|
||||
const sourceStat = await stat(sourcePath).catch(() => null);
|
||||
if (!sourceStat?.isDirectory()) fail("Path must be a package folder");
|
||||
if (options.openclaw?.trim() && options.openclawVersion?.trim()) {
|
||||
fail("Choose either --openclaw or --openclaw-version");
|
||||
}
|
||||
|
||||
const outDir = options.out?.trim() || "reports";
|
||||
const openclawPath = options.openclaw?.trim() ? resolve(opts.workdir, options.openclaw) : false;
|
||||
const openclawVersion = options.openclawVersion?.trim() || (openclawPath ? undefined : "latest");
|
||||
const generatedConfig = await createPluginInspectorConfigIfNeeded(sourcePath);
|
||||
let report: Awaited<ReturnType<typeof pluginRoot.runCheck>>["report"];
|
||||
let paths: Awaited<ReturnType<typeof pluginRoot.runCheck>>["paths"];
|
||||
@@ -730,6 +735,7 @@ export async function cmdValidatePackage(
|
||||
configPath: generatedConfig?.path,
|
||||
mockSdk: options.mockSdk !== false,
|
||||
openclawPath,
|
||||
...(openclawVersion ? { openclawVersion } : {}),
|
||||
outDir,
|
||||
pluginRoot: sourcePath,
|
||||
} as Parameters<typeof pluginRoot.runCheck>[0] & { authorFacing: true };
|
||||
|
||||
@@ -22,6 +22,7 @@ declare module "@openclaw/plugin-inspector" {
|
||||
configPath?: string;
|
||||
mockSdk?: boolean;
|
||||
openclawPath?: string | false;
|
||||
openclawVersion?: string;
|
||||
outDir?: string;
|
||||
pluginRoot?: string;
|
||||
}): Promise<{
|
||||
|
||||
@@ -2016,6 +2016,11 @@ describe("plugin detail route", () => {
|
||||
expect(within(validationRegion).getByText("v1.0.0")).toBeTruthy();
|
||||
expect(within(validationRegion).getByText("Target")).toBeTruthy();
|
||||
expect(within(validationRegion).getByText("OpenClaw 0.9.0")).toBeTruthy();
|
||||
expect(
|
||||
within(validationRegion).getByText(
|
||||
"clawhub package validate <path-to-plugin> --openclaw-version 0.9.0",
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText(/Legacy before_agent_start hook is deprecated\./)).toBeTruthy();
|
||||
expect(screen.getByText(/We found/)).toBeTruthy();
|
||||
expect(screen.queryByText(/Hey,/)).toBeNull();
|
||||
|
||||
@@ -57,6 +57,12 @@ type PluginInspectorFinding = {
|
||||
|
||||
const PLUGIN_VALIDATE_CLI = "clawhub package validate <path-to-plugin>";
|
||||
|
||||
function pluginValidateCommand(openClawVersion?: string) {
|
||||
return openClawVersion
|
||||
? `${PLUGIN_VALIDATE_CLI} --openclaw-version ${openClawVersion}`
|
||||
: PLUGIN_VALIDATE_CLI;
|
||||
}
|
||||
|
||||
export function DashboardNeedsAttention({ items }: DashboardNeedsAttentionProps) {
|
||||
const [selectedGroup, setSelectedGroup] = useState<DashboardAttentionGroup | null>(null);
|
||||
const reviewScrollRef = useRef<HTMLDivElement>(null);
|
||||
@@ -353,7 +359,23 @@ function PluginValidationSheetReview({
|
||||
const errors = displayedFindings.filter((finding) => finding.findingKind === "error");
|
||||
const warnings = displayedFindings.filter((finding) => finding.findingKind !== "error");
|
||||
const version = displayedFindings.find((finding) => finding.version)?.version ?? null;
|
||||
const instructions = buildValidationInstructions(group.title, version, displayedFindings);
|
||||
const targetOpenClawVersions = Array.from(
|
||||
new Set(
|
||||
displayedFindings
|
||||
.map((finding) => finding.targetOpenClawVersion?.trim())
|
||||
.filter((target): target is string => Boolean(target)),
|
||||
),
|
||||
);
|
||||
const validateCommands = targetOpenClawVersions.length
|
||||
? targetOpenClawVersions.map(pluginValidateCommand)
|
||||
: [pluginValidateCommand()];
|
||||
const validateCommand = validateCommands.join("\n");
|
||||
const instructions = buildValidationInstructions(
|
||||
group.title,
|
||||
version,
|
||||
displayedFindings,
|
||||
validateCommands,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -383,12 +405,20 @@ function PluginValidationSheetReview({
|
||||
<span className="plugin-validation-toolbar-label">
|
||||
Validate locally before publishing
|
||||
</span>
|
||||
<div className="plugin-validation-toolbar">
|
||||
<div
|
||||
className={`plugin-validation-toolbar${
|
||||
validateCommands.length > 1 ? " is-multi-target" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="plugin-validation-toolbar-cli">
|
||||
<code className="plugin-validation-command">{PLUGIN_VALIDATE_CLI}</code>
|
||||
<code className="plugin-validation-command">{validateCommand}</code>
|
||||
<InstallCopyButton
|
||||
text={PLUGIN_VALIDATE_CLI}
|
||||
ariaLabel="Copy validate command"
|
||||
text={validateCommand}
|
||||
ariaLabel={
|
||||
validateCommands.length === 1
|
||||
? "Copy validate command"
|
||||
: "Copy validate commands"
|
||||
}
|
||||
showLabel={false}
|
||||
className="plugin-validation-toolbar-copy"
|
||||
/>
|
||||
@@ -542,14 +572,17 @@ function buildValidationInstructions(
|
||||
packageName: string,
|
||||
version: string | null,
|
||||
findings: PluginInspectorFinding[],
|
||||
validateCommands: string[],
|
||||
) {
|
||||
const lines = [
|
||||
`Fix the following OpenClaw plugin validation findings for package "${packageName}".`,
|
||||
...(version ? [`Validated release: v${version}.`] : []),
|
||||
"",
|
||||
"Make the minimum code and manifest changes needed to resolve every issue below.",
|
||||
"After editing, run locally:",
|
||||
PLUGIN_VALIDATE_CLI,
|
||||
validateCommands.length === 1
|
||||
? "After editing, run locally:"
|
||||
: "After editing, run locally against every recorded OpenClaw target:",
|
||||
...validateCommands,
|
||||
"",
|
||||
];
|
||||
for (const finding of findings) {
|
||||
|
||||
+123
-13
@@ -49,6 +49,8 @@ const mocks = vi.hoisted(() => ({
|
||||
rerenderDashboard: null as null | (() => void),
|
||||
}));
|
||||
|
||||
const writeTextMock = vi.fn();
|
||||
|
||||
vi.mock("convex/react", () => ({
|
||||
useQuery: (...args: unknown[]) => mocks.useQuery(...args),
|
||||
usePaginatedQuery: (...args: unknown[]) => mocks.usePaginatedQuery(...args),
|
||||
@@ -366,6 +368,12 @@ describe("Dashboard rows", () => {
|
||||
mocks.dashboardSearch = {};
|
||||
mocks.rerenderDashboard = null;
|
||||
window.localStorage.clear();
|
||||
writeTextMock.mockReset();
|
||||
writeTextMock.mockResolvedValue(undefined);
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
configurable: true,
|
||||
value: { writeText: writeTextMock },
|
||||
});
|
||||
mocks.usePaginatedQuery.mockReturnValue({
|
||||
results: [],
|
||||
status: "LoadingFirstPage",
|
||||
@@ -687,21 +695,43 @@ describe("Dashboard rows", () => {
|
||||
});
|
||||
|
||||
it("links public plugin finding counts to the plugin validation tab", () => {
|
||||
arrangeDashboard({
|
||||
packages: [
|
||||
createPackage({
|
||||
inspectorWarningCount: 2,
|
||||
scanStatus: "clean",
|
||||
stats: { downloads: 42, installs: 9, stars: 0, versions: 1 },
|
||||
latestRelease: {
|
||||
const packages = [
|
||||
createPackage({
|
||||
inspectorWarningCount: 2,
|
||||
scanStatus: "clean",
|
||||
stats: { downloads: 42, installs: 9, stars: 0, versions: 1 },
|
||||
latestRelease: {
|
||||
version: "1.0.0",
|
||||
createdAt: 1,
|
||||
vtStatus: "clean",
|
||||
llmStatus: "clean",
|
||||
staticScanStatus: "clean",
|
||||
},
|
||||
}),
|
||||
];
|
||||
arrangeDashboard({ packages });
|
||||
mocks.useQuery.mockImplementation((query: unknown, args: unknown) => {
|
||||
if (args === "skip") return undefined;
|
||||
const name = getFunctionName(query as never);
|
||||
if (name === "publishers:listMine") return publishers;
|
||||
if (name === "packages:list") return packages;
|
||||
if (name === "dashboard:getDownloadMetrics") return downloadMetrics;
|
||||
if (name === "packages:listPackageInspectorWarningsForManager") {
|
||||
return [
|
||||
{
|
||||
_id: "packageInspectorWarnings:1",
|
||||
packageName: "local-scanned-runtime-plugin",
|
||||
version: "1.0.0",
|
||||
createdAt: 1,
|
||||
vtStatus: "clean",
|
||||
llmStatus: "clean",
|
||||
staticScanStatus: "clean",
|
||||
findingKind: "warning",
|
||||
code: "legacy-before-agent-start",
|
||||
issueClass: "deprecation-warning",
|
||||
severity: "P2",
|
||||
message: "legacy before_agent_start hook is deprecated",
|
||||
targetOpenClawVersion: "0.9.0",
|
||||
},
|
||||
}),
|
||||
],
|
||||
];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
renderDashboard();
|
||||
@@ -715,6 +745,86 @@ describe("Dashboard rows", () => {
|
||||
screen.getByRole("dialog", { name: "Local Flagged Runtime Plugin review" }),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole("heading", { name: "Validation" })).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText("clawhub package validate <path-to-plugin> --openclaw-version 0.9.0"),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("copies one exact validation command per recorded OpenClaw target", async () => {
|
||||
const packages = [
|
||||
createPackage({
|
||||
inspectorWarningCount: 2,
|
||||
scanStatus: "clean",
|
||||
latestRelease: {
|
||||
version: "1.0.0",
|
||||
createdAt: 1,
|
||||
vtStatus: "clean",
|
||||
llmStatus: "clean",
|
||||
staticScanStatus: "clean",
|
||||
},
|
||||
}),
|
||||
];
|
||||
arrangeDashboard({ packages });
|
||||
mocks.useQuery.mockImplementation((query: unknown, args: unknown) => {
|
||||
if (args === "skip") return undefined;
|
||||
const name = getFunctionName(query as never);
|
||||
if (name === "publishers:listMine") return publishers;
|
||||
if (name === "packages:list") return packages;
|
||||
if (name === "dashboard:getDownloadMetrics") return downloadMetrics;
|
||||
if (name === "packages:listPackageInspectorWarningsForManager") {
|
||||
return [
|
||||
{
|
||||
_id: "packageInspectorWarnings:1",
|
||||
findingKind: "warning",
|
||||
code: "legacy-before-agent-start",
|
||||
message: "legacy hook is deprecated",
|
||||
targetOpenClawVersion: "0.9.0",
|
||||
},
|
||||
{
|
||||
_id: "packageInspectorWarnings:2",
|
||||
findingKind: "error",
|
||||
code: "missing-expected-seam",
|
||||
message: "registerTool is no longer available",
|
||||
targetOpenClawVersion: "0.10.0",
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
renderDashboard();
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: /Local Flagged Runtime Plugin\. 1 issue/i }),
|
||||
);
|
||||
expect(document.querySelector(".plugin-validation-command")?.textContent).toContain(
|
||||
"clawhub package validate <path-to-plugin> --openclaw-version 0.9.0",
|
||||
);
|
||||
expect(document.querySelector(".plugin-validation-command")?.textContent).toContain(
|
||||
"clawhub package validate <path-to-plugin> --openclaw-version 0.10.0",
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Copy validate commands" }));
|
||||
await waitFor(() => {
|
||||
expect(writeTextMock).toHaveBeenCalledWith(
|
||||
[
|
||||
"clawhub package validate <path-to-plugin> --openclaw-version 0.9.0",
|
||||
"clawhub package validate <path-to-plugin> --openclaw-version 0.10.0",
|
||||
].join("\n"),
|
||||
);
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Copy fix instructions" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(writeTextMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"clawhub package validate <path-to-plugin> --openclaw-version 0.9.0",
|
||||
),
|
||||
);
|
||||
expect(writeTextMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"clawhub package validate <path-to-plugin> --openclaw-version 0.10.0",
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows a publisher selector and loads org packages when switching publishers", async () => {
|
||||
|
||||
@@ -690,6 +690,12 @@ function PluginManifestSkillsPanel({
|
||||
}
|
||||
|
||||
const PLUGIN_VALIDATE_CLI = "clawhub package validate <path-to-plugin>";
|
||||
|
||||
function pluginValidateCommand(openClawVersion?: string) {
|
||||
return openClawVersion
|
||||
? `${PLUGIN_VALIDATE_CLI} --openclaw-version ${openClawVersion}`
|
||||
: PLUGIN_VALIDATE_CLI;
|
||||
}
|
||||
const PLUGIN_VALIDATE_TOOLBAR_LABEL = "Validate locally before publishing";
|
||||
|
||||
const INSPECTOR_ISSUE_CLASS_LABELS: Record<string, string> = {
|
||||
@@ -954,10 +960,18 @@ function PluginValidationFindingCard({
|
||||
</div>
|
||||
) : null}
|
||||
{finding.targetOpenClawVersion ? (
|
||||
<div className="plugin-warning-meta-field">
|
||||
<dt className="plugin-warning-meta-key">Target</dt>
|
||||
<dd className="plugin-warning-meta-value">OpenClaw {finding.targetOpenClawVersion}</dd>
|
||||
</div>
|
||||
<>
|
||||
<div className="plugin-warning-meta-field">
|
||||
<dt className="plugin-warning-meta-key">Target</dt>
|
||||
<dd className="plugin-warning-meta-value">OpenClaw {finding.targetOpenClawVersion}</dd>
|
||||
</div>
|
||||
<div className="plugin-warning-meta-field">
|
||||
<dt className="plugin-warning-meta-key">Reproduce</dt>
|
||||
<dd className="plugin-warning-meta-value">
|
||||
<code>{pluginValidateCommand(finding.targetOpenClawVersion)}</code>
|
||||
</dd>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</dl>
|
||||
) : null;
|
||||
|
||||
@@ -5192,6 +5192,20 @@ code {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.plugin-validation-toolbar.is-multi-target {
|
||||
height: auto;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.plugin-validation-toolbar.is-multi-target .plugin-validation-toolbar-cli {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.plugin-validation-toolbar.is-multi-target .plugin-validation-command {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.plugin-validation-toolbar-copy.skill-install-copy-button {
|
||||
flex: 0 0 auto;
|
||||
width: 2rem;
|
||||
|
||||
Reference in New Issue
Block a user