From 943cb472749f6f0827b95f1d2fb60810b191f2de Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 15 Apr 2026 20:26:06 +0100 Subject: [PATCH] fix(qa): use exported runner sdk seam --- .../runners/contract/model-selection.test.ts | 2 +- .../src/runners/contract/model-selection.ts | 2 +- .../qa-matrix/src/runners/contract/runtime.ts | 2 +- src/plugin-sdk/qa-runner-runtime.test.ts | 26 ++++++++++++ src/plugin-sdk/qa-runner-runtime.ts | 41 +++++++++++++++++++ 5 files changed, 70 insertions(+), 3 deletions(-) diff --git a/extensions/qa-matrix/src/runners/contract/model-selection.test.ts b/extensions/qa-matrix/src/runners/contract/model-selection.test.ts index f4ccf02160c..41cd1a19082 100644 --- a/extensions/qa-matrix/src/runners/contract/model-selection.test.ts +++ b/extensions/qa-matrix/src/runners/contract/model-selection.test.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const loadQaRuntimeModule = vi.hoisted(() => vi.fn()); const defaultQaRuntimeModelForMode = vi.hoisted(() => vi.fn()); -vi.mock("openclaw/plugin-sdk/qa-runtime", () => ({ +vi.mock("openclaw/plugin-sdk/qa-runner-runtime", () => ({ loadQaRuntimeModule, })); diff --git a/extensions/qa-matrix/src/runners/contract/model-selection.ts b/extensions/qa-matrix/src/runners/contract/model-selection.ts index 0a4dd6b1fe2..4570a28486a 100644 --- a/extensions/qa-matrix/src/runners/contract/model-selection.ts +++ b/extensions/qa-matrix/src/runners/contract/model-selection.ts @@ -1,4 +1,4 @@ -import { loadQaRuntimeModule } from "openclaw/plugin-sdk/qa-runtime"; +import { loadQaRuntimeModule } from "openclaw/plugin-sdk/qa-runner-runtime"; import { normalizeQaProviderMode, type QaProviderModeInput } from "../../run-config.js"; export type ResolvedMatrixQaModels = { diff --git a/extensions/qa-matrix/src/runners/contract/runtime.ts b/extensions/qa-matrix/src/runners/contract/runtime.ts index 554a9b193c4..fc950956777 100644 --- a/extensions/qa-matrix/src/runners/contract/runtime.ts +++ b/extensions/qa-matrix/src/runners/contract/runtime.ts @@ -4,7 +4,7 @@ import path from "node:path"; import { setTimeout as sleep } from "node:timers/promises"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; -import { loadQaRuntimeModule } from "openclaw/plugin-sdk/qa-runtime"; +import { loadQaRuntimeModule } from "openclaw/plugin-sdk/qa-runner-runtime"; import type { QaReportCheck } from "../../report.js"; import { renderQaMarkdownReport } from "../../report.js"; import { type QaProviderModeInput } from "../../run-config.js"; diff --git a/src/plugin-sdk/qa-runner-runtime.test.ts b/src/plugin-sdk/qa-runner-runtime.test.ts index f4d772fbc0f..fe7dabbd2b8 100644 --- a/src/plugin-sdk/qa-runner-runtime.test.ts +++ b/src/plugin-sdk/qa-runner-runtime.test.ts @@ -32,6 +32,32 @@ describe("plugin-sdk qa-runner-runtime", () => { expect(tryLoadActivatedBundledPluginPublicSurfaceModuleSync).not.toHaveBeenCalled(); }); + it("loads the qa-lab runtime public surface through the public runner seam", async () => { + const runtimeSurface = { + defaultQaRuntimeModelForMode: vi.fn(), + startQaLiveLaneGateway: vi.fn(), + }; + loadBundledPluginPublicSurfaceModuleSync.mockReturnValue(runtimeSurface); + + const module = await import("./qa-runner-runtime.js"); + + expect(module.loadQaRuntimeModule()).toBe(runtimeSurface); + expect(loadBundledPluginPublicSurfaceModuleSync).toHaveBeenCalledWith({ + dirName: "qa-lab", + artifactBasename: "runtime-api.js", + }); + }); + + it("reports the qa runtime as unavailable when the qa-lab surface is missing", async () => { + loadBundledPluginPublicSurfaceModuleSync.mockImplementation(() => { + throw new Error("Unable to resolve bundled plugin public surface qa-lab/runtime-api.js"); + }); + + const module = await import("./qa-runner-runtime.js"); + + expect(module.isQaRuntimeAvailable()).toBe(false); + }); + it("returns activated runner registrations declared in plugin manifests", async () => { const register = vi.fn((qa: Command) => qa); loadPluginManifestRegistry.mockReturnValue({ diff --git a/src/plugin-sdk/qa-runner-runtime.ts b/src/plugin-sdk/qa-runner-runtime.ts index 6b484566df1..44c5a1a6c6e 100644 --- a/src/plugin-sdk/qa-runner-runtime.ts +++ b/src/plugin-sdk/qa-runner-runtime.ts @@ -15,6 +15,17 @@ type QaRunnerRuntimeSurface = { qaRunnerCliRegistrations?: readonly QaRunnerCliRegistration[]; }; +type QaRuntimeSurface = { + defaultQaRuntimeModelForMode: ( + mode: string, + options?: { + alternate?: boolean; + preferredLiveModel?: string; + }, + ) => string; + startQaLiveLaneGateway: (...args: unknown[]) => Promise; +}; + export type QaRunnerCliContribution = | { pluginId: string; @@ -30,6 +41,36 @@ export type QaRunnerCliContribution = status: "blocked"; }; +function isMissingQaRuntimeError(error: unknown) { + if (!(error instanceof Error)) { + return false; + } + return ( + error.message.includes("qa-lab") && + (error.message.includes("runtime-api.js") || + error.message.startsWith("Unable to open bundled plugin public surface ")) + ); +} + +export function loadQaRuntimeModule(): QaRuntimeSurface { + return loadBundledPluginPublicSurfaceModuleSync({ + dirName: ["qa", "lab"].join("-"), + artifactBasename: ["runtime-api", "js"].join("."), + }); +} + +export function isQaRuntimeAvailable(): boolean { + try { + loadQaRuntimeModule(); + return true; + } catch (error) { + if (isMissingQaRuntimeError(error)) { + return false; + } + throw error; + } +} + function listDeclaredQaRunnerPlugins(): Array< PluginManifestRecord & { qaRunners: NonNullable;