fix(plugin-sdk): guard qa runner manifest rows

This commit is contained in:
Vincent Koc
2026-06-04 03:23:56 +02:00
parent 036b730321
commit a5bf510676
2 changed files with 136 additions and 13 deletions

View File

@@ -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: [

View File

@@ -16,6 +16,12 @@ type QaRunnerRuntimeSurface = {
qaRunnerCliRegistrations?: readonly QaRunnerCliRegistration[];
};
type QaRunnerDeclaration = NonNullable<PluginManifestRecord["qaRunners"]>[number];
type DeclaredQaRunnerPlugin = Pick<PluginManifestRecord, "id" | "origin" | "rootDir"> & {
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<PluginManifestRecord["qaRunners"]>;
}
> {
): DeclaredQaRunnerPlugin[] {
return loadPluginManifestRegistry(env ? { env } : {})
.plugins.filter(
(
plugin,
): plugin is PluginManifestRecord & {
qaRunners: NonNullable<PluginManifestRecord["qaRunners"]>;
} => 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<PluginManifestRecord, "id" | "origin">,
env?: NodeJS.ProcessEnv,
): QaRunnerRuntimeSurface | null {
if (plugin.origin === "bundled") {