From a5bf510676ed4100aca7feddd0cb56ece688a7e8 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 4 Jun 2026 03:23:56 +0200 Subject: [PATCH] fix(plugin-sdk): guard qa runner manifest rows --- src/plugin-sdk/qa-runner-runtime.test.ts | 60 ++++++++++++++++ src/plugin-sdk/qa-runner-runtime.ts | 89 ++++++++++++++++++++---- 2 files changed, 136 insertions(+), 13 deletions(-) diff --git a/src/plugin-sdk/qa-runner-runtime.test.ts b/src/plugin-sdk/qa-runner-runtime.test.ts index 9b2f2e10402d..40ea3b816dcc 100644 --- a/src/plugin-sdk/qa-runner-runtime.test.ts +++ b/src/plugin-sdk/qa-runner-runtime.test.ts @@ -162,6 +162,66 @@ describe("plugin-sdk qa-runner-runtime", () => { }); }); + it("skips unreadable manifest rows when discovering qa runner registrations", async () => { + const register = vi.fn((qa: Command) => qa); + const unreadableQaRunners = Object.defineProperty( + { + id: "poisoned-qa-runners", + origin: "bundled", + rootDir: "/tmp/poisoned-qa-runners", + }, + "qaRunners", + { + get() { + throw new Error("qa runner metadata exploded"); + }, + }, + ); + const unreadableRootDir = Object.defineProperty( + { + id: "qa-matrix", + origin: "bundled", + qaRunners: [{ commandName: "matrix" }], + }, + "rootDir", + { + get() { + throw new Error("qa runner root metadata exploded"); + }, + }, + ); + loadPluginManifestRegistry.mockReturnValue({ + plugins: [ + unreadableQaRunners, + unreadableRootDir, + { + id: "qa-matrix", + origin: "bundled", + qaRunners: [{ commandName: "matrix" }], + rootDir: "/tmp/qa-matrix", + }, + ], + diagnostics: [], + }); + loadBundledPluginPublicSurfaceModuleSync.mockReturnValue({ + qaRunnerCliRegistrations: [{ commandName: "matrix", register }], + }); + + const module = await import("./qa-runner-runtime.js"); + + expect(module.listQaRunnerCliContributions()).toEqual([ + { + pluginId: "qa-matrix", + commandName: "matrix", + status: "available", + registration: { + commandName: "matrix", + register, + }, + }, + ]); + }); + it("reports declared runners as blocked when the plugin is present but not activated", async () => { loadPluginManifestRegistry.mockReturnValue({ plugins: [ diff --git a/src/plugin-sdk/qa-runner-runtime.ts b/src/plugin-sdk/qa-runner-runtime.ts index 432e3c79e039..9c465f26d880 100644 --- a/src/plugin-sdk/qa-runner-runtime.ts +++ b/src/plugin-sdk/qa-runner-runtime.ts @@ -16,6 +16,12 @@ type QaRunnerRuntimeSurface = { qaRunnerCliRegistrations?: readonly QaRunnerCliRegistration[]; }; +type QaRunnerDeclaration = NonNullable[number]; + +type DeclaredQaRunnerPlugin = Pick & { + qaRunners: QaRunnerDeclaration[]; +}; + type QaRuntimeSurface = { defaultQaRuntimeModelForMode: ( mode: string, @@ -86,19 +92,12 @@ export function isQaRuntimeAvailable(): boolean { function listDeclaredQaRunnerPlugins( env: NodeJS.ProcessEnv | undefined = resolvePrivateQaBundledPluginsEnv(), -): Array< - PluginManifestRecord & { - qaRunners: NonNullable; - } -> { +): DeclaredQaRunnerPlugin[] { return loadPluginManifestRegistry(env ? { env } : {}) - .plugins.filter( - ( - plugin, - ): plugin is PluginManifestRecord & { - qaRunners: NonNullable; - } => Array.isArray(plugin.qaRunners) && plugin.qaRunners.length > 0, - ) + .plugins.flatMap((plugin) => { + const record = readDeclaredQaRunnerPlugin(plugin); + return record ? [record] : []; + }) .toSorted((left, right) => { const idCompare = left.id.localeCompare(right.id); if (idCompare !== 0) { @@ -108,6 +107,70 @@ function listDeclaredQaRunnerPlugins( }); } +function readDeclaredQaRunnerPlugin(plugin: unknown): DeclaredQaRunnerPlugin | null { + if (!plugin || typeof plugin !== "object") { + return null; + } + + try { + const candidate = plugin as { + id?: unknown; + origin?: unknown; + qaRunners?: unknown; + rootDir?: unknown; + }; + const { id, origin, qaRunners, rootDir } = candidate; + if (typeof id !== "string" || id.length === 0) { + return null; + } + if (!isPluginOrigin(origin)) { + return null; + } + if (typeof rootDir !== "string" || rootDir.length === 0) { + return null; + } + if (!Array.isArray(qaRunners)) { + return null; + } + const runners = qaRunners.flatMap((runner) => { + const declaration = readQaRunnerDeclaration(runner); + return declaration ? [declaration] : []; + }); + if (runners.length === 0) { + return null; + } + return { id, origin, qaRunners: runners, rootDir }; + } catch { + return null; + } +} + +function isPluginOrigin(value: unknown): value is PluginManifestRecord["origin"] { + return value === "bundled" || value === "global" || value === "workspace" || value === "config"; +} + +function readQaRunnerDeclaration(runner: unknown): QaRunnerDeclaration | null { + if (!runner || typeof runner !== "object") { + return null; + } + + try { + const candidate = runner as { + commandName?: unknown; + description?: unknown; + }; + if (typeof candidate.commandName !== "string" || candidate.commandName.length === 0) { + return null; + } + return { + commandName: candidate.commandName, + ...(typeof candidate.description === "string" ? { description: candidate.description } : {}), + }; + } catch { + return null; + } +} + function indexRuntimeRegistrations( pluginId: string, surface: QaRunnerRuntimeSurface, @@ -129,7 +192,7 @@ function indexRuntimeRegistrations( } function loadQaRunnerRuntimeSurface( - plugin: PluginManifestRecord, + plugin: Pick, env?: NodeJS.ProcessEnv, ): QaRunnerRuntimeSurface | null { if (plugin.origin === "bundled") {