From 85eac42d342131e8a31f303ecc37278050d84519 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Tue, 14 Apr 2026 17:36:43 -0400 Subject: [PATCH] QA: remove runner install fallback catalog Drop the generated qa-runner catalog and the missing/install placeholder path for repo-private QA runners. The host should discover bundled QA commands from manifest plus runtime surface only. Also trim stale qa-matrix install docs and package metadata so the source-only QA policy stays consistent. --- docs/cli/plugins.md | 6 -- docs/help/testing.md | 4 +- extensions/qa-lab/src/cli.test.ts | 21 ------ extensions/qa-lab/src/live-transports/cli.ts | 34 +-------- extensions/qa-matrix/package.json | 9 +-- package.json | 2 - scripts/generate-qa-runner-catalog.ts | 35 --------- scripts/lib/qa-runner-catalog.json | 8 -- .../qa-runner-runtime.integration.test.ts | 33 +++------ src/plugin-sdk/qa-runner-runtime.test.ts | 55 ++++---------- src/plugin-sdk/qa-runner-runtime.ts | 33 --------- src/plugins/qa-runner-catalog.ts | 74 ------------------- 12 files changed, 31 insertions(+), 283 deletions(-) delete mode 100644 scripts/generate-qa-runner-catalog.ts delete mode 100644 scripts/lib/qa-runner-catalog.json delete mode 100644 src/plugins/qa-runner-catalog.ts diff --git a/docs/cli/plugins.md b/docs/cli/plugins.md index 9f86c75422c..0ed8425cd3a 100644 --- a/docs/cli/plugins.md +++ b/docs/cli/plugins.md @@ -197,12 +197,6 @@ Use `--link` to avoid copying a local directory (adds to `plugins.load.paths`): openclaw plugins install -l ./my-plugin ``` -Repo QA example (source-linked dev surface; not shipped in packaged installs): - -```bash -openclaw plugins install -l ./extensions/qa-matrix -``` - `--force` is not supported with `--link` because linked installs reuse the source path instead of copying over a managed install target. diff --git a/docs/help/testing.md b/docs/help/testing.md index 68e597dc060..fda7a6a0a99 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -69,8 +69,8 @@ These commands sit beside the main test suites when you need QA-lab realism: - Runs the Matrix live QA lane against a disposable Docker-backed Tuwunel homeserver. - This QA host is repo/dev-only today. Packaged OpenClaw installs do not ship `qa-lab`, so they do not expose `openclaw qa`. - - Repo checkouts can link the in-tree plugin directly: - `openclaw plugins install -l ./extensions/qa-matrix`. + - Repo checkouts load the bundled runner directly; no separate plugin install + step is needed. - Provisions three temporary Matrix users (`driver`, `sut`, `observer`) plus one private room, then starts a QA gateway child with the real Matrix plugin as the SUT transport. - Uses the pinned stable Tuwunel image `ghcr.io/matrix-construct/tuwunel:v1.5.1` by default. Override with `OPENCLAW_QA_MATRIX_TUWUNEL_IMAGE` when you need to test a different image. - Matrix does not expose shared credential-source flags because the lane provisions disposable users locally. diff --git a/extensions/qa-lab/src/cli.test.ts b/extensions/qa-lab/src/cli.test.ts index 8d60b8079fd..0c287f1e51d 100644 --- a/extensions/qa-lab/src/cli.test.ts +++ b/extensions/qa-lab/src/cli.test.ts @@ -6,7 +6,6 @@ const TEST_QA_RUNNER = { pluginId: "qa-runner-test", commandName: "runner-test", description: "Run the test live QA lane", - npmSpec: "@openclaw/qa-runner-test", } as const; function createAvailableQaRunnerContribution() { @@ -23,16 +22,6 @@ function createAvailableQaRunnerContribution() { } satisfies QaRunnerCliContribution; } -function createMissingQaRunnerContribution(): QaRunnerCliContribution { - return { - pluginId: TEST_QA_RUNNER.pluginId, - commandName: TEST_QA_RUNNER.commandName, - description: TEST_QA_RUNNER.description, - status: "missing", - npmSpec: TEST_QA_RUNNER.npmSpec, - }; -} - function createBlockedQaRunnerContribution(): QaRunnerCliContribution { return { pluginId: TEST_QA_RUNNER.pluginId, @@ -127,16 +116,6 @@ describe("qa cli registration", () => { ); }); - it("shows an install hint when a discovered runner plugin is unavailable", async () => { - listQaRunnerCliContributions.mockReset().mockReturnValue([createMissingQaRunnerContribution()]); - const missingProgram = new Command(); - registerQaLabCli(missingProgram); - - await expect( - missingProgram.parseAsync(["node", "openclaw", "qa", TEST_QA_RUNNER.commandName]), - ).rejects.toThrow(`openclaw plugins install ${TEST_QA_RUNNER.npmSpec}`); - }); - it("shows an enable hint when a discovered runner plugin is installed but blocked", async () => { listQaRunnerCliContributions.mockReset().mockReturnValue([createBlockedQaRunnerContribution()]); const blockedProgram = new Command(); diff --git a/extensions/qa-lab/src/live-transports/cli.ts b/extensions/qa-lab/src/live-transports/cli.ts index 9d2275ae6b4..993ecd79ac4 100644 --- a/extensions/qa-lab/src/live-transports/cli.ts +++ b/extensions/qa-lab/src/live-transports/cli.ts @@ -2,25 +2,6 @@ import { listQaRunnerCliContributions } from "openclaw/plugin-sdk/qa-runner-runt import type { LiveTransportQaCliRegistration } from "./shared/live-transport-cli.js"; import { telegramQaCliRegistration } from "./telegram/cli.js"; -function createMissingQaRunnerCliRegistration(params: { - commandName: string; - description: string; - npmSpec: string; -}): LiveTransportQaCliRegistration { - return { - commandName: params.commandName, - register(qa) { - qa.command(params.commandName) - .description(params.description) - .action(() => { - throw new Error( - `QA runner "${params.commandName}" not installed. Install it with "openclaw plugins install ${params.npmSpec}".`, - ); - }); - }, - }; -} - function createBlockedQaRunnerCliRegistration(params: { commandName: string; description?: string; @@ -46,19 +27,10 @@ function createQaRunnerCliRegistration( if (runner.status === "available") { return runner.registration; } - if (runner.status === "blocked") { - return createBlockedQaRunnerCliRegistration({ - commandName: runner.commandName, - description: runner.description, - pluginId: runner.pluginId, - }); - } - return createMissingQaRunnerCliRegistration({ + return createBlockedQaRunnerCliRegistration({ commandName: runner.commandName, - description: - runner.description ?? - `Run the ${runner.commandName} live QA lane (install ${runner.npmSpec} first)`, - npmSpec: runner.npmSpec, + description: runner.description, + pluginId: runner.pluginId, }); } diff --git a/extensions/qa-matrix/package.json b/extensions/qa-matrix/package.json index 624ce3cdd59..d95ffaf0973 100644 --- a/extensions/qa-matrix/package.json +++ b/extensions/qa-matrix/package.json @@ -1,6 +1,7 @@ { "name": "@openclaw/qa-matrix", "version": "2026.4.12", + "private": true, "description": "OpenClaw Matrix QA runner plugin", "type": "module", "devDependencies": { @@ -19,16 +20,8 @@ "extensions": [ "./index.ts" ], - "install": { - "npmSpec": "@openclaw/qa-matrix", - "defaultChoice": "npm", - "minHostVersion": ">=2026.4.12" - }, "compat": { "pluginApi": ">=2026.4.12" - }, - "build": { - "openclawVersion": "2026.4.12" } } } diff --git a/package.json b/package.json index c2c4464e846..d868dbadf3a 100644 --- a/package.json +++ b/package.json @@ -1243,8 +1243,6 @@ "proxy:install-ca": "node --import tsx scripts/proxy-install-ca.mjs", "proxy:run": "node scripts/run-node.mjs proxy run", "proxy:start": "node scripts/run-node.mjs proxy start", - "qa-runners:check": "node --import tsx scripts/generate-qa-runner-catalog.ts --check", - "qa-runners:gen": "node --import tsx scripts/generate-qa-runner-catalog.ts --write", "qa:e2e": "node --import tsx scripts/qa-e2e.ts", "qa:lab:build": "vite build --config extensions/qa-lab/web/vite.config.ts", "qa:lab:ui": "pnpm openclaw qa ui", diff --git a/scripts/generate-qa-runner-catalog.ts b/scripts/generate-qa-runner-catalog.ts deleted file mode 100644 index a49201628bc..00000000000 --- a/scripts/generate-qa-runner-catalog.ts +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env node -import path from "node:path"; -import { writeBundledQaRunnerCatalog } from "../src/plugins/qa-runner-catalog.js"; - -const args = new Set(process.argv.slice(2)); -const checkOnly = args.has("--check"); -const writeMode = args.has("--write"); - -if (checkOnly === writeMode) { - console.error("Use exactly one of --check or --write."); - process.exit(1); -} - -const repoRoot = process.cwd(); -const result = await writeBundledQaRunnerCatalog({ - repoRoot, - check: checkOnly, -}); - -if (checkOnly) { - if (result.changed) { - console.error( - [ - "QA runner catalog drift detected.", - `Expected current: ${path.relative(repoRoot, result.jsonPath)}`, - "If this QA runner metadata change is intentional, run `pnpm qa-runners:gen` and commit the updated baseline file.", - "If not intentional, fix the bundled plugin metadata drift first.", - ].join("\n"), - ); - process.exit(1); - } - console.log(`OK ${path.relative(repoRoot, result.jsonPath)}`); -} else { - console.log(`Wrote ${path.relative(repoRoot, result.jsonPath)}`); -} diff --git a/scripts/lib/qa-runner-catalog.json b/scripts/lib/qa-runner-catalog.json deleted file mode 100644 index 06864acd78b..00000000000 --- a/scripts/lib/qa-runner-catalog.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - { - "pluginId": "qa-matrix", - "commandName": "matrix", - "description": "Run the Docker-backed Matrix live QA lane against a disposable homeserver", - "npmSpec": "@openclaw/qa-matrix" - } -] diff --git a/src/plugin-sdk/qa-runner-runtime.integration.test.ts b/src/plugin-sdk/qa-runner-runtime.integration.test.ts index 1ad93b1e7b5..411769f30c8 100644 --- a/src/plugin-sdk/qa-runner-runtime.integration.test.ts +++ b/src/plugin-sdk/qa-runner-runtime.integration.test.ts @@ -55,7 +55,7 @@ describe("plugin-sdk qa-runner-runtime linked plugin smoke", () => { } }); - it("loads an activated qa runner from a linked plugin path", async () => { + it("loads an activated qa runner from a linked plugin path without a bundled install fallback", async () => { const stateDir = makeTempDir("openclaw-qa-runner-state-"); const pluginDir = path.join(stateDir, "extensions", "qa-linked"); const configPath = path.join(stateDir, "openclaw.json"); @@ -102,7 +102,7 @@ describe("plugin-sdk qa-runner-runtime linked plugin smoke", () => { }), "utf8", ); - fs.writeFileSync(path.join(pluginDir, "index.js"), 'export default {};\n', "utf8"); + fs.writeFileSync(path.join(pluginDir, "index.js"), "export default {};\n", "utf8"); fs.writeFileSync( path.join(pluginDir, "runtime-api.js"), [ @@ -118,26 +118,17 @@ describe("plugin-sdk qa-runner-runtime linked plugin smoke", () => { const module = await import("./qa-runner-runtime.js"); - expect(module.listQaRunnerCliContributions()).toEqual( - expect.arrayContaining([ - { - pluginId: "qa-linked", + expect(module.listQaRunnerCliContributions()).toEqual([ + { + pluginId: "qa-linked", + commandName: "linked", + description: "Run the linked QA lane", + status: "available", + registration: { commandName: "linked", - description: "Run the linked QA lane", - status: "available", - registration: { - commandName: "linked", - register: expect.any(Function), - }, + register: expect.any(Function), }, - { - pluginId: "qa-matrix", - commandName: "matrix", - description: "Run the Docker-backed Matrix live QA lane against a disposable homeserver", - status: "missing", - npmSpec: "@openclaw/qa-matrix", - }, - ]), - ); + }, + ]); }); }); diff --git a/src/plugin-sdk/qa-runner-runtime.test.ts b/src/plugin-sdk/qa-runner-runtime.test.ts index 73c3e448c5a..f4d772fbc0f 100644 --- a/src/plugin-sdk/qa-runner-runtime.test.ts +++ b/src/plugin-sdk/qa-runner-runtime.test.ts @@ -2,27 +2,15 @@ import type { Command } from "commander"; import { beforeEach, describe, expect, it, vi } from "vitest"; const loadPluginManifestRegistry = vi.hoisted(() => vi.fn()); +const loadBundledPluginPublicSurfaceModuleSync = vi.hoisted(() => vi.fn()); const tryLoadActivatedBundledPluginPublicSurfaceModuleSync = vi.hoisted(() => vi.fn()); -const listBundledQaRunnerCatalog = vi.hoisted(() => - vi.fn< - () => Array<{ - pluginId: string; - commandName: string; - description?: string; - npmSpec: string; - }> - >(() => []), -); vi.mock("../plugins/manifest-registry.js", () => ({ loadPluginManifestRegistry, })); -vi.mock("../plugins/qa-runner-catalog.js", () => ({ - listBundledQaRunnerCatalog, -})); - vi.mock("./facade-runtime.js", () => ({ + loadBundledPluginPublicSurfaceModuleSync, tryLoadActivatedBundledPluginPublicSurfaceModuleSync, })); @@ -32,7 +20,7 @@ describe("plugin-sdk qa-runner-runtime", () => { plugins: [], diagnostics: [], }); - listBundledQaRunnerCatalog.mockReset().mockReturnValue([]); + loadBundledPluginPublicSurfaceModuleSync.mockReset(); tryLoadActivatedBundledPluginPublicSurfaceModuleSync.mockReset(); }); @@ -40,6 +28,7 @@ describe("plugin-sdk qa-runner-runtime", () => { await import("./qa-runner-runtime.js"); expect(loadPluginManifestRegistry).not.toHaveBeenCalled(); + expect(loadBundledPluginPublicSurfaceModuleSync).not.toHaveBeenCalled(); expect(tryLoadActivatedBundledPluginPublicSurfaceModuleSync).not.toHaveBeenCalled(); }); @@ -49,6 +38,7 @@ describe("plugin-sdk qa-runner-runtime", () => { plugins: [ { id: "qa-matrix", + origin: "bundled", qaRunners: [ { commandName: "matrix", @@ -60,7 +50,7 @@ describe("plugin-sdk qa-runner-runtime", () => { ], diagnostics: [], }); - tryLoadActivatedBundledPluginPublicSurfaceModuleSync.mockReturnValue({ + loadBundledPluginPublicSurfaceModuleSync.mockReturnValue({ qaRunnerCliRegistrations: [{ commandName: "matrix", register }], }); @@ -78,7 +68,7 @@ describe("plugin-sdk qa-runner-runtime", () => { }, }, ]); - expect(tryLoadActivatedBundledPluginPublicSurfaceModuleSync).toHaveBeenCalledWith({ + expect(loadBundledPluginPublicSurfaceModuleSync).toHaveBeenCalledWith({ dirName: "qa-matrix", artifactBasename: "runtime-api.js", }); @@ -89,6 +79,7 @@ describe("plugin-sdk qa-runner-runtime", () => { plugins: [ { id: "qa-matrix", + origin: "workspace", qaRunners: [{ commandName: "matrix" }], rootDir: "/tmp/qa-matrix", }, @@ -108,46 +99,25 @@ describe("plugin-sdk qa-runner-runtime", () => { ]); }); - it("reports missing optional runners from the generated catalog", async () => { - listBundledQaRunnerCatalog.mockReturnValue([ - { - pluginId: "qa-matrix", - commandName: "matrix", - description: "Run the Matrix live QA lane", - npmSpec: "@openclaw/qa-matrix", - }, - ]); - - const module = await import("./qa-runner-runtime.js"); - - expect(module.listQaRunnerCliContributions()).toEqual([ - { - pluginId: "qa-matrix", - commandName: "matrix", - description: "Run the Matrix live QA lane", - status: "missing", - npmSpec: "@openclaw/qa-matrix", - }, - ]); - }); - it("fails fast when two plugins declare the same qa runner command", async () => { loadPluginManifestRegistry.mockReturnValue({ plugins: [ { id: "alpha", + origin: "workspace", qaRunners: [{ commandName: "matrix" }], rootDir: "/tmp/alpha", }, { id: "beta", + origin: "workspace", qaRunners: [{ commandName: "matrix" }], rootDir: "/tmp/beta", }, ], diagnostics: [], }); - tryLoadActivatedBundledPluginPublicSurfaceModuleSync.mockReturnValue(null); + loadBundledPluginPublicSurfaceModuleSync.mockReturnValue(null); const module = await import("./qa-runner-runtime.js"); @@ -161,13 +131,14 @@ describe("plugin-sdk qa-runner-runtime", () => { plugins: [ { id: "qa-matrix", + origin: "bundled", qaRunners: [{ commandName: "matrix" }], rootDir: "/tmp/qa-matrix", }, ], diagnostics: [], }); - tryLoadActivatedBundledPluginPublicSurfaceModuleSync.mockReturnValue({ + loadBundledPluginPublicSurfaceModuleSync.mockReturnValue({ qaRunnerCliRegistrations: [ { commandName: "matrix", register: vi.fn() }, { commandName: "extra", register: vi.fn() }, diff --git a/src/plugin-sdk/qa-runner-runtime.ts b/src/plugin-sdk/qa-runner-runtime.ts index 49d0634c17d..6b484566df1 100644 --- a/src/plugin-sdk/qa-runner-runtime.ts +++ b/src/plugin-sdk/qa-runner-runtime.ts @@ -1,7 +1,6 @@ import type { Command } from "commander"; import type { PluginManifestRecord } from "../plugins/manifest-registry.js"; import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js"; -import { listBundledQaRunnerCatalog } from "../plugins/qa-runner-catalog.js"; import { loadBundledPluginPublicSurfaceModuleSync, tryLoadActivatedBundledPluginPublicSurfaceModuleSync, @@ -29,13 +28,6 @@ export type QaRunnerCliContribution = commandName: string; description?: string; status: "blocked"; - } - | { - pluginId: string; - commandName: string; - description?: string; - status: "missing"; - npmSpec: string; }; function listDeclaredQaRunnerPlugins(): Array< @@ -80,27 +72,6 @@ function indexRuntimeRegistrations( return registrationByCommandName; } -function buildKnownQaRunnerCatalog(): readonly QaRunnerCliContribution[] { - const knownRunners = listBundledQaRunnerCatalog(); - const seenCommandNames = new Map(); - return knownRunners.map((runner) => { - const previousOwner = seenCommandNames.get(runner.commandName); - if (previousOwner) { - throw new Error( - `QA runner command "${runner.commandName}" declared by both "${previousOwner}" and "${runner.pluginId}"`, - ); - } - seenCommandNames.set(runner.commandName, runner.pluginId); - return { - pluginId: runner.pluginId, - commandName: runner.commandName, - ...(runner.description ? { description: runner.description } : {}), - status: "missing" as const, - npmSpec: runner.npmSpec, - }; - }); -} - function loadQaRunnerRuntimeSurface(plugin: PluginManifestRecord): QaRunnerRuntimeSurface | null { if (plugin.origin === "bundled") { return loadBundledPluginPublicSurfaceModuleSync({ @@ -117,10 +88,6 @@ function loadQaRunnerRuntimeSurface(plugin: PluginManifestRecord): QaRunnerRunti export function listQaRunnerCliContributions(): readonly QaRunnerCliContribution[] { const contributions = new Map(); - for (const runner of buildKnownQaRunnerCatalog()) { - contributions.set(runner.commandName, runner); - } - for (const plugin of listDeclaredQaRunnerPlugins()) { const runtimeSurface = loadQaRunnerRuntimeSurface(plugin); const runtimeRegistrationByCommandName = runtimeSurface diff --git a/src/plugins/qa-runner-catalog.ts b/src/plugins/qa-runner-catalog.ts deleted file mode 100644 index dd0bba3a460..00000000000 --- a/src/plugins/qa-runner-catalog.ts +++ /dev/null @@ -1,74 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { listBundledPluginMetadata } from "./bundled-plugin-metadata.js"; - -export type QaRunnerCatalogEntry = { - pluginId: string; - commandName: string; - description?: string; - npmSpec: string; -}; - -const QA_RUNNER_CATALOG_JSON_PATH = fileURLToPath( - new URL("../../scripts/lib/qa-runner-catalog.json", import.meta.url), -); - -export function listBundledQaRunnerCatalog(): readonly QaRunnerCatalogEntry[] { - if (!fs.existsSync(QA_RUNNER_CATALOG_JSON_PATH)) { - return []; - } - return JSON.parse(fs.readFileSync(QA_RUNNER_CATALOG_JSON_PATH, "utf8")) as QaRunnerCatalogEntry[]; -} - -export function collectBundledQaRunnerCatalog(params?: { - rootDir?: string; -}): readonly QaRunnerCatalogEntry[] { - const catalog: QaRunnerCatalogEntry[] = []; - const seenCommandNames = new Map(); - - for (const entry of listBundledPluginMetadata({ - rootDir: params?.rootDir, - includeChannelConfigs: false, - })) { - const qaRunners = entry.manifest.qaRunners ?? []; - const npmSpec = entry.packageManifest?.install?.npmSpec?.trim() || entry.packageName?.trim(); - if (!npmSpec) { - continue; - } - for (const runner of qaRunners) { - const previousOwner = seenCommandNames.get(runner.commandName); - if (previousOwner) { - throw new Error( - `QA runner command "${runner.commandName}" declared by both "${previousOwner}" and "${entry.manifest.id}"`, - ); - } - seenCommandNames.set(runner.commandName, entry.manifest.id); - catalog.push({ - pluginId: entry.manifest.id, - commandName: runner.commandName, - ...(runner.description ? { description: runner.description } : {}), - npmSpec, - }); - } - } - - return catalog.toSorted((left, right) => left.commandName.localeCompare(right.commandName)); -} - -export async function writeBundledQaRunnerCatalog(params: { - repoRoot: string; - check: boolean; -}): Promise<{ changed: boolean; jsonPath: string }> { - const jsonPath = path.join(params.repoRoot, "scripts", "lib", "qa-runner-catalog.json"); - const expectedJson = `${JSON.stringify(collectBundledQaRunnerCatalog({ rootDir: params.repoRoot }), null, 2)}\n`; - const currentJson = fs.existsSync(jsonPath) ? fs.readFileSync(jsonPath, "utf8") : ""; - const changed = currentJson !== expectedJson; - - if (!params.check && changed) { - fs.mkdirSync(path.dirname(jsonPath), { recursive: true }); - fs.writeFileSync(jsonPath, expectedJson, "utf8"); - } - - return { changed, jsonPath }; -}