diff --git a/scripts/e2e/lib/bundled-plugin-install-uninstall/probe.mjs b/scripts/e2e/lib/bundled-plugin-install-uninstall/probe.mjs index c4a83c82f588..34a1b8d783dc 100644 --- a/scripts/e2e/lib/bundled-plugin-install-uninstall/probe.mjs +++ b/scripts/e2e/lib/bundled-plugin-install-uninstall/probe.mjs @@ -102,10 +102,44 @@ function readPluginsList() { `Unable to list packaged bundled plugins: ${result.stderr || result.stdout || `exit ${result.status}`}`, ); } - const payload = JSON.parse(result.stdout); + const payload = parsePluginListOutput(result.stdout); return Array.isArray(payload.plugins) ? payload.plugins : []; } +function parsePluginListOutput(stdout) { + const trimmed = stdout.trim(); + const parsed = parseJsonValue(trimmed); + if (parsed.ok) { + return parsed.value; + } + let lastParsed; + for (const line of trimmed.split(/\r?\n/u).toReversed()) { + if (!line.trimStart().startsWith("{")) { + continue; + } + const candidate = parseJsonValue(line); + if (!candidate.ok) { + continue; + } + lastParsed ??= candidate.value; + if (Array.isArray(candidate.value?.plugins)) { + return candidate.value; + } + } + if (lastParsed !== undefined) { + return lastParsed; + } + throw new Error(`Unable to parse packaged bundled plugin list JSON: ${trimmed}`); +} + +function parseJsonValue(text) { + try { + return { ok: true, value: JSON.parse(text) }; + } catch { + return { ok: false }; + } +} + function pluginRequiresConfig(pluginDir) { const manifestPath = path.join(pluginDir, "openclaw.plugin.json"); if (!fs.existsSync(manifestPath)) { diff --git a/test/scripts/bundled-plugin-install-uninstall-probe.test.ts b/test/scripts/bundled-plugin-install-uninstall-probe.test.ts index 5670ba168d01..37e391594f4c 100644 --- a/test/scripts/bundled-plugin-install-uninstall-probe.test.ts +++ b/test/scripts/bundled-plugin-install-uninstall-probe.test.ts @@ -1316,6 +1316,38 @@ describe("bundled plugin install/uninstall probe", () => { ); }); + it("selects packaged plugins when list output includes structured diagnostics", () => { + const root = makePackageRoot(); + const pluginRoot = path.join(root, "dist-runtime", "extensions", "admin-http-rpc"); + writePluginManifest(root, "dist-runtime/extensions/admin-http-rpc", { + id: "admin-http-rpc", + configSchema: { required: ["port"] }, + }); + fs.writeFileSync( + path.join(root, "dist", "index.js"), + [ + "if (process.argv.slice(2).join(' ') !== 'plugins list --json') {", + " process.exit(1);", + "}", + `console.log(${JSON.stringify( + JSON.stringify({ + plugins: [{ id: "admin-http-rpc", origin: "bundled", rootDir: pluginRoot }], + }), + )});`, + "console.log(JSON.stringify({ level: 'warn', message: 'post-list diagnostic' }));", + "", + ].join("\n"), + "utf8", + ); + + const result = runProbe(root, { + OPENCLAW_BUNDLED_PLUGIN_SWEEP_IDS: undefined, + }); + + expect(result.status).toBe(0); + expect(result.stdout.trim()).toBe(`admin-http-rpc\tadmin-http-rpc\t1\t${pluginRoot}`); + }); + it("does not select source-only bundled plugins for package-backed sweeps", () => { const root = makePackageRoot(); writePluginManifest(root, "extensions/qa-channel", {