From d830e4affc5202b1a3919f274db3759e749058fb Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 2 Jun 2026 13:48:27 +0200 Subject: [PATCH] fix(testing): probe plugin CLI help while installed --- scripts/check-plugin-gateway-gauntlet.mjs | 65 ++++++-- test/scripts/plugin-gateway-gauntlet.test.ts | 163 +++++++++++++++++++ 2 files changed, 217 insertions(+), 11 deletions(-) diff --git a/scripts/check-plugin-gateway-gauntlet.mjs b/scripts/check-plugin-gateway-gauntlet.mjs index 06fde980fb1..fcf9efb90dd 100644 --- a/scripts/check-plugin-gateway-gauntlet.mjs +++ b/scripts/check-plugin-gateway-gauntlet.mjs @@ -595,6 +595,24 @@ export function hasGauntletWorkRows(rows) { return rows.some((row) => row.phase !== "prebuild"); } +function isPluginOwnedCliAlias(alias) { + return alias.kind === "runtime-slash" && alias.cliCommand === alias.name; +} + +function buildSlashHelpProbe(params) { + const command = params.alias.cliCommand ?? params.alias.name; + return { + cwd: params.repoRoot, + env: params.env, + logDir: path.join(params.outputDir, "logs", "slash-help"), + ...openclawCommand(params.repoRoot, [command, "--help"]), + label: `${params.plugin.id}-slash-${params.alias.name}`, + phase: "slash:help", + pluginId: params.plugin.id, + timeoutMs: params.commandTimeoutMs, + }; +} + async function runPluginLifecycle(params) { for (const plugin of params.plugins) { const commands = [ @@ -603,13 +621,34 @@ async function runPluginLifecycle(params) { args: ["install", plugin.id], }, { phase: "inspect", args: ["inspect", plugin.id, "--json"] }, + ...(params.skipSlashHelp + ? [] + : plugin.cliCommandAliases + .filter(isPluginOwnedCliAlias) + .map((alias) => ({ phase: `slash-help:${alias.name}`, alias }))), { phase: "disable", args: ["disable", plugin.id] }, ...(plugin.hasRequiredConfigFields ? [] : [{ phase: "enable", args: ["enable", plugin.id] }]), { phase: "doctor", args: ["doctor"] }, { phase: "uninstall", args: ["uninstall", plugin.id, "--force"] }, ]; - for (const { phase, args } of commands) { + for (const { phase, args, alias } of commands) { process.stderr.write(`[plugin-gauntlet] ${plugin.id} ${phase}\n`); + if (alias) { + params.rows.push( + await runMeasuredCommand({ + ...buildSlashHelpProbe({ + repoRoot: params.repoRoot, + outputDir: params.outputDir, + env: params.env, + plugin, + alias, + commandTimeoutMs: params.commandTimeoutMs, + }), + label: `${plugin.id}-${phase}`, + }), + ); + continue; + } params.rows.push( await runMeasuredCommand({ cwd: params.repoRoot, @@ -628,19 +667,21 @@ async function runPluginLifecycle(params) { async function runSlashHelpProbes(params) { for (const plugin of params.plugins) { - for (const alias of plugin.cliCommandAliases) { - const command = alias.cliCommand ?? alias.name; + const aliases = params.includePluginOwnedCliAliases + ? plugin.cliCommandAliases + : plugin.cliCommandAliases.filter((entry) => !isPluginOwnedCliAlias(entry)); + for (const alias of aliases) { process.stderr.write(`[plugin-gauntlet] ${plugin.id} slash-help /${alias.name}\n`); params.rows.push( await runMeasuredCommand({ - cwd: params.repoRoot, - env: params.env, - logDir: path.join(params.outputDir, "logs", "slash-help"), - ...openclawCommand(params.repoRoot, [command, "--help"]), - label: `${plugin.id}-slash-${alias.name}`, - phase: "slash:help", - pluginId: plugin.id, - timeoutMs: params.commandTimeoutMs, + ...buildSlashHelpProbe({ + repoRoot: params.repoRoot, + outputDir: params.outputDir, + env: params.env, + plugin, + alias, + commandTimeoutMs: params.commandTimeoutMs, + }), }), ); } @@ -816,6 +857,7 @@ async function main() { plugins: selectedPlugins, rows, commandTimeoutMs: options.commandTimeoutMs, + skipSlashHelp: options.skipSlashHelp, }); } if (!prebuildFailed && !options.skipSlashHelp) { @@ -826,6 +868,7 @@ async function main() { plugins: selectedPlugins, rows, commandTimeoutMs: options.commandTimeoutMs, + includePluginOwnedCliAliases: options.skipLifecycle, }); } const qaSummaries = diff --git a/test/scripts/plugin-gateway-gauntlet.test.ts b/test/scripts/plugin-gateway-gauntlet.test.ts index 3508f8c8d60..f796578988d 100644 --- a/test/scripts/plugin-gateway-gauntlet.test.ts +++ b/test/scripts/plugin-gateway-gauntlet.test.ts @@ -745,6 +745,169 @@ setInterval(() => {}, 1000); await expect(fs.stat(summary.isolatedRunRoot)).rejects.toHaveProperty("code", "ENOENT"); }); + it("probes plugin-owned slash help while the plugin is installed", async () => { + const outputDir = path.join(repoRoot, "artifacts"); + await writeManifest( + "workboard", + "openclaw.plugin.json", + JSON.stringify({ + id: "workboard", + commandAliases: [ + { + name: "workboard", + kind: "runtime-slash", + cliCommand: "workboard", + }, + ], + }), + ); + await fs.writeFile(path.join(repoRoot, "extensions", "workboard", "index.ts"), "export {};\n"); + await fs.mkdir(path.join(repoRoot, "dist"), { recursive: true }); + await fs.writeFile( + path.join(repoRoot, "dist", "entry.js"), + [ + 'const fs = require("node:fs");', + 'const path = require("node:path");', + 'const stateDir = process.env.OPENCLAW_STATE_DIR ?? process.cwd();', + 'const marker = path.join(stateDir, "workboard-enabled");', + "const args = process.argv.slice(2);", + 'if (args[0] === "plugins") {', + ' if (args[1] === "install" || args[1] === "enable") fs.writeFileSync(marker, "1");', + ' if (args[1] === "disable" || args[1] === "uninstall") fs.rmSync(marker, { force: true });', + ' if (args[1] === "inspect") console.log("{}");', + " process.exit(0);", + "}", + 'if (args[0] === "workboard" && args[1] === "--help") {', + " if (fs.existsSync(marker)) {", + ' console.log("Usage: openclaw workboard");', + " process.exit(0);", + " }", + ' console.error("workboard help was probed after uninstall");', + " process.exit(1);", + "}", + "process.exit(0);", + ].join("\n"), + "utf8", + ); + + const result = spawnSync( + process.execPath, + [ + path.resolve("scripts/check-plugin-gateway-gauntlet.mjs"), + "--repo-root", + repoRoot, + "--output-dir", + outputDir, + "--skip-prebuild", + "--skip-qa", + "--plugin", + "workboard", + ], + { + cwd: path.resolve("."), + encoding: "utf8", + }, + ); + + expect(result.status, result.stderr).toBe(0); + const summary = JSON.parse( + await fs.readFile(path.join(outputDir, "plugin-gateway-gauntlet-summary.json"), "utf8"), + ); + expect(summary.failures).toEqual([]); + const slashHelpRow = summary.rows.find( + (row: { label?: string; logPath?: string }) => + row.label === "workboard-slash-help:workboard", + ); + expect(summary.rows).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + label: "workboard-slash-help:workboard", + phase: "slash:help", + pluginId: "workboard", + status: 0, + }), + ]), + ); + const slashHelpLogPath = slashHelpRow?.logPath; + expect(slashHelpLogPath).toEqual(expect.any(String)); + await expect(fs.readFile(slashHelpLogPath as string, "utf8")).resolves.toContain( + "Usage: openclaw workboard", + ); + + const skipOutputDir = path.join(repoRoot, "artifacts-skip"); + const skipResult = spawnSync( + process.execPath, + [ + path.resolve("scripts/check-plugin-gateway-gauntlet.mjs"), + "--repo-root", + repoRoot, + "--output-dir", + skipOutputDir, + "--skip-prebuild", + "--skip-qa", + "--skip-slash-help", + "--plugin", + "workboard", + ], + { + cwd: path.resolve("."), + encoding: "utf8", + }, + ); + + expect(skipResult.status, skipResult.stderr).toBe(0); + const skipSummary = JSON.parse( + await fs.readFile(path.join(skipOutputDir, "plugin-gateway-gauntlet-summary.json"), "utf8"), + ); + expect(skipSummary.failures).toEqual([]); + expect(skipSummary.rows).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + phase: "slash:help", + pluginId: "workboard", + }), + ]), + ); + + const slashOnlyOutputDir = path.join(repoRoot, "artifacts-slash-only"); + const slashOnlyResult = spawnSync( + process.execPath, + [ + path.resolve("scripts/check-plugin-gateway-gauntlet.mjs"), + "--repo-root", + repoRoot, + "--output-dir", + slashOnlyOutputDir, + "--skip-prebuild", + "--skip-lifecycle", + "--skip-qa", + "--plugin", + "workboard", + ], + { + cwd: path.resolve("."), + encoding: "utf8", + }, + ); + + expect(slashOnlyResult.status, slashOnlyResult.stderr).toBe(1); + const slashOnlySummary = JSON.parse( + await fs.readFile( + path.join(slashOnlyOutputDir, "plugin-gateway-gauntlet-summary.json"), + "utf8", + ), + ); + expect(slashOnlySummary.guardFailures).toEqual([]); + expect(slashOnlySummary.failures).toEqual([ + expect.objectContaining({ + label: "workboard-slash-workboard", + phase: "slash:help", + pluginId: "workboard", + status: 1, + }), + ]); + }); + it("carries bounded build ids into QA run-node chunks", async () => { const outputDir = path.join(repoRoot, "artifacts"); const qaSummaryJson = JSON.stringify(