mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(plugin-sdk): guard qa runner manifest rows
This commit is contained in:
@@ -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: [
|
||||
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user