fix(testing): probe plugin CLI help while installed

This commit is contained in:
Vincent Koc
2026-06-02 13:48:27 +02:00
parent 10d10faa25
commit d830e4affc
2 changed files with 217 additions and 11 deletions

View File

@@ -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 =

View File

@@ -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(