From 95be2c1605b0d1ac17eb8f00ca566bc93074fdc2 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Tue, 14 Apr 2026 18:53:25 -0400 Subject: [PATCH] QA: replace qa-lab-runtime with qa-runtime Introduce a tiny generic qa-runtime seam for shared live-lane helpers and repoint qa-matrix to it. This keeps the qa-lab host split while removing the host-owned runtime name from runner code. Drop the old qa-lab-runtime shim/export now that nothing consumes it and keep the plugin-sdk surface aligned with the new seam. --- .../runners/contract/model-selection.test.ts | 18 +++---- .../src/runners/contract/model-selection.ts | 8 ++-- .../qa-matrix/src/runners/contract/runtime.ts | 6 +-- package.json | 6 +-- scripts/lib/plugin-sdk-entrypoints.json | 2 +- src/plugin-sdk/qa-runtime.test.ts | 47 +++++++++++++++++++ src/plugin-sdk/qa-runtime.ts | 39 +++++++++++++++ 7 files changed, 107 insertions(+), 19 deletions(-) create mode 100644 src/plugin-sdk/qa-runtime.test.ts create mode 100644 src/plugin-sdk/qa-runtime.ts 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 ede92b96d9c..f4ccf02160c 100644 --- a/extensions/qa-matrix/src/runners/contract/model-selection.test.ts +++ b/extensions/qa-matrix/src/runners/contract/model-selection.test.ts @@ -1,18 +1,20 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -const loadQaLabRuntimeModule = vi.hoisted(() => vi.fn()); +const loadQaRuntimeModule = vi.hoisted(() => vi.fn()); const defaultQaRuntimeModelForMode = vi.hoisted(() => vi.fn()); -vi.mock("openclaw/plugin-sdk/qa-lab-runtime", () => ({ - loadQaLabRuntimeModule, +vi.mock("openclaw/plugin-sdk/qa-runtime", () => ({ + loadQaRuntimeModule, })); describe("matrix qa model selection", () => { beforeEach(() => { - defaultQaRuntimeModelForMode.mockReset().mockImplementation((mode, options) => - options?.alternate ? `${mode}:alt` : `${mode}:primary`, - ); - loadQaLabRuntimeModule.mockReset().mockReturnValue({ + defaultQaRuntimeModelForMode + .mockReset() + .mockImplementation((mode, options) => + options?.alternate ? `${mode}:alt` : `${mode}:primary`, + ); + loadQaRuntimeModule.mockReset().mockReturnValue({ defaultQaRuntimeModelForMode, }); }); @@ -45,7 +47,7 @@ describe("matrix qa model selection", () => { primaryModel: "custom-primary", alternateModel: "custom-alt", }); - expect(loadQaLabRuntimeModule).not.toHaveBeenCalled(); + expect(loadQaRuntimeModule).not.toHaveBeenCalled(); expect(defaultQaRuntimeModelForMode).not.toHaveBeenCalled(); }); }); diff --git a/extensions/qa-matrix/src/runners/contract/model-selection.ts b/extensions/qa-matrix/src/runners/contract/model-selection.ts index 338378d85f8..0a4dd6b1fe2 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 { loadQaLabRuntimeModule } from "openclaw/plugin-sdk/qa-lab-runtime"; +import { loadQaRuntimeModule } from "openclaw/plugin-sdk/qa-runtime"; import { normalizeQaProviderMode, type QaProviderModeInput } from "../../run-config.js"; export type ResolvedMatrixQaModels = { @@ -23,11 +23,11 @@ export function resolveMatrixQaModels(params: { }; } - const qaLabRuntime = loadQaLabRuntimeModule(); + const qaRuntime = loadQaRuntimeModule(); return { providerMode, - primaryModel: primaryModel || qaLabRuntime.defaultQaRuntimeModelForMode(providerMode), + primaryModel: primaryModel || qaRuntime.defaultQaRuntimeModelForMode(providerMode), alternateModel: - alternateModel || qaLabRuntime.defaultQaRuntimeModelForMode(providerMode, { alternate: true }), + alternateModel || qaRuntime.defaultQaRuntimeModelForMode(providerMode, { alternate: true }), }; } diff --git a/extensions/qa-matrix/src/runners/contract/runtime.ts b/extensions/qa-matrix/src/runners/contract/runtime.ts index 12a58f3ff28..824985a840f 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 { loadQaLabRuntimeModule } from "openclaw/plugin-sdk/qa-lab-runtime"; +import { loadQaRuntimeModule } from "openclaw/plugin-sdk/qa-runtime"; import type { QaReportCheck } from "../../report.js"; import { renderQaMarkdownReport } from "../../report.js"; import { type QaProviderModeInput } from "../../run-config.js"; @@ -18,6 +18,7 @@ import { type MatrixQaProvisionResult, } from "../../substrate/client.js"; import { startMatrixQaHarness } from "../../substrate/harness.runtime.js"; +import { resolveMatrixQaModels } from "./model-selection.js"; import { MATRIX_QA_SCENARIOS, buildMatrixReplyDetails, @@ -27,7 +28,6 @@ import { type MatrixQaCanaryArtifact, type MatrixQaScenarioArtifacts, } from "./scenarios.js"; -import { resolveMatrixQaModels } from "./model-selection.js"; type MatrixQaGatewayChild = { call( @@ -284,7 +284,7 @@ async function startMatrixQaLiveLaneGateway(params: { controlUiEnabled?: boolean; mutateConfig?: (cfg: OpenClawConfig) => OpenClawConfig; }): Promise { - return (await loadQaLabRuntimeModule().startQaLiveLaneGateway( + return (await loadQaRuntimeModule().startQaLiveLaneGateway( params, )) as MatrixQaLiveLaneGatewayHarness; } diff --git a/package.json b/package.json index d868dbadf3a..ce4c871ff95 100644 --- a/package.json +++ b/package.json @@ -765,9 +765,9 @@ "types": "./dist/plugin-sdk/matrix-thread-bindings.d.ts", "default": "./dist/plugin-sdk/matrix-thread-bindings.js" }, - "./plugin-sdk/qa-lab-runtime": { - "types": "./dist/plugin-sdk/qa-lab-runtime.d.ts", - "default": "./dist/plugin-sdk/qa-lab-runtime.js" + "./plugin-sdk/qa-runtime": { + "types": "./dist/plugin-sdk/qa-runtime.d.ts", + "default": "./dist/plugin-sdk/qa-runtime.js" }, "./plugin-sdk/qa-runner-runtime": { "types": "./dist/plugin-sdk/qa-runner-runtime.d.ts", diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index 85274c6ccc3..100880b9510 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -179,7 +179,7 @@ "matrix-runtime-surface", "matrix-surface", "matrix-thread-bindings", - "qa-lab-runtime", + "qa-runtime", "qa-runner-runtime", "mattermost", "mattermost-policy", diff --git a/src/plugin-sdk/qa-runtime.test.ts b/src/plugin-sdk/qa-runtime.test.ts new file mode 100644 index 00000000000..cd871db9ff0 --- /dev/null +++ b/src/plugin-sdk/qa-runtime.test.ts @@ -0,0 +1,47 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const loadBundledPluginPublicSurfaceModuleSync = vi.hoisted(() => vi.fn()); + +vi.mock("./facade-runtime.js", () => ({ + loadBundledPluginPublicSurfaceModuleSync, +})); + +describe("plugin-sdk qa-runtime", () => { + beforeEach(() => { + loadBundledPluginPublicSurfaceModuleSync.mockReset(); + }); + + it("stays cold until the runtime seam is used", async () => { + const module = await import("./qa-runtime.js"); + + expect(loadBundledPluginPublicSurfaceModuleSync).not.toHaveBeenCalled(); + expect(typeof module.loadQaRuntimeModule).toBe("function"); + expect(typeof module.isQaRuntimeAvailable).toBe("function"); + }); + + it("loads the qa-lab runtime public surface through the generic seam", async () => { + const runtimeSurface = { + defaultQaRuntimeModelForMode: vi.fn(), + startQaLiveLaneGateway: vi.fn(), + }; + loadBundledPluginPublicSurfaceModuleSync.mockReturnValue(runtimeSurface); + + const module = await import("./qa-runtime.js"); + + expect(module.loadQaRuntimeModule()).toBe(runtimeSurface); + expect(loadBundledPluginPublicSurfaceModuleSync).toHaveBeenCalledWith({ + dirName: "qa-lab", + artifactBasename: "runtime-api.js", + }); + }); + + it("reports the 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-runtime.js"); + + expect(module.isQaRuntimeAvailable()).toBe(false); + }); +}); diff --git a/src/plugin-sdk/qa-runtime.ts b/src/plugin-sdk/qa-runtime.ts new file mode 100644 index 00000000000..4fabbfdd137 --- /dev/null +++ b/src/plugin-sdk/qa-runtime.ts @@ -0,0 +1,39 @@ +import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js"; + +type QaRuntimeSurface = { + defaultQaRuntimeModelForMode: ( + mode: string, + options?: { + alternate?: boolean; + preferredLiveModel?: string; + }, + ) => string; + startQaLiveLaneGateway: (...args: unknown[]) => Promise; +}; + +function isMissingQaRuntimeError(error: unknown) { + return ( + error instanceof Error && + (error.message === "Unable to resolve bundled plugin public surface qa-lab/runtime-api.js" || + error.message.startsWith("Unable to open bundled plugin public surface ")) + ); +} + +export function loadQaRuntimeModule(): QaRuntimeSurface { + return loadBundledPluginPublicSurfaceModuleSync({ + dirName: "qa-lab", + artifactBasename: "runtime-api.js", + }); +} + +export function isQaRuntimeAvailable(): boolean { + try { + loadQaRuntimeModule(); + return true; + } catch (error) { + if (isMissingQaRuntimeError(error)) { + return false; + } + throw error; + } +}