diff --git a/src/plugin-sdk/private-qa-bundled-env.ts b/src/plugin-sdk/private-qa-bundled-env.ts new file mode 100644 index 00000000000..7c77932dde4 --- /dev/null +++ b/src/plugin-sdk/private-qa-bundled-env.ts @@ -0,0 +1,31 @@ +import fs from "node:fs"; +import path from "node:path"; +import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js"; + +export function resolvePrivateQaBundledPluginsEnv( + env: NodeJS.ProcessEnv = process.env, +): NodeJS.ProcessEnv | undefined { + if (env.OPENCLAW_ENABLE_PRIVATE_QA_CLI !== "1") { + return undefined; + } + const packageRoot = resolveOpenClawPackageRootSync({ + argv1: process.argv[1], + cwd: process.cwd(), + moduleUrl: import.meta.url, + }); + if (!packageRoot) { + return undefined; + } + const sourceExtensionsDir = path.join(packageRoot, "extensions"); + if ( + !fs.existsSync(path.join(packageRoot, ".git")) || + !fs.existsSync(path.join(packageRoot, "src")) || + !fs.existsSync(sourceExtensionsDir) + ) { + return undefined; + } + return { + ...env, + OPENCLAW_BUNDLED_PLUGINS_DIR: sourceExtensionsDir, + }; +} diff --git a/src/plugin-sdk/qa-runner-runtime.ts b/src/plugin-sdk/qa-runner-runtime.ts index 4f782777112..7a149bdc39a 100644 --- a/src/plugin-sdk/qa-runner-runtime.ts +++ b/src/plugin-sdk/qa-runner-runtime.ts @@ -1,13 +1,11 @@ -import fs from "node:fs"; -import path from "node:path"; import type { Command } from "commander"; -import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js"; import type { PluginManifestRecord } from "../plugins/manifest-registry.js"; import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js"; import { loadBundledPluginPublicSurfaceModuleSync, tryLoadActivatedBundledPluginPublicSurfaceModuleSync, } from "./facade-runtime.js"; +import { resolvePrivateQaBundledPluginsEnv } from "./private-qa-bundled-env.js"; export type QaRunnerCliRegistration = { commandName: string; @@ -56,7 +54,7 @@ function isMissingQaRuntimeError(error: unknown) { } export function loadQaRuntimeModule(): QaRuntimeSurface { - const env = resolvePrivateQaRunnerEnv(); + const env = resolvePrivateQaBundledPluginsEnv(); return loadBundledPluginPublicSurfaceModuleSync({ dirName: ["qa", "lab"].join("-"), artifactBasename: ["runtime-api", "js"].join("."), @@ -76,36 +74,8 @@ export function isQaRuntimeAvailable(): boolean { } } -function resolvePrivateQaRunnerEnv( - env: NodeJS.ProcessEnv = process.env, -): NodeJS.ProcessEnv | undefined { - if (env.OPENCLAW_ENABLE_PRIVATE_QA_CLI !== "1") { - return undefined; - } - const packageRoot = resolveOpenClawPackageRootSync({ - argv1: process.argv[1], - cwd: process.cwd(), - moduleUrl: import.meta.url, - }); - if (!packageRoot) { - return undefined; - } - const sourceExtensionsDir = path.join(packageRoot, "extensions"); - if ( - !fs.existsSync(path.join(packageRoot, ".git")) || - !fs.existsSync(path.join(packageRoot, "src")) || - !fs.existsSync(sourceExtensionsDir) - ) { - return undefined; - } - return { - ...env, - OPENCLAW_BUNDLED_PLUGINS_DIR: sourceExtensionsDir, - }; -} - function listDeclaredQaRunnerPlugins( - env: NodeJS.ProcessEnv | undefined = resolvePrivateQaRunnerEnv(), + env: NodeJS.ProcessEnv | undefined = resolvePrivateQaBundledPluginsEnv(), ): Array< PluginManifestRecord & { qaRunners: NonNullable; @@ -167,7 +137,7 @@ function loadQaRunnerRuntimeSurface( } export function listQaRunnerCliContributions(): readonly QaRunnerCliContribution[] { - const env = resolvePrivateQaRunnerEnv(); + const env = resolvePrivateQaBundledPluginsEnv(); const contributions = new Map(); for (const plugin of listDeclaredQaRunnerPlugins(env)) { diff --git a/src/plugin-sdk/qa-runtime.test.ts b/src/plugin-sdk/qa-runtime.test.ts index cd871db9ff0..026cb142d78 100644 --- a/src/plugin-sdk/qa-runtime.test.ts +++ b/src/plugin-sdk/qa-runtime.test.ts @@ -1,14 +1,38 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const loadBundledPluginPublicSurfaceModuleSync = vi.hoisted(() => vi.fn()); +const resolveOpenClawPackageRootSync = vi.hoisted(() => vi.fn()); vi.mock("./facade-runtime.js", () => ({ loadBundledPluginPublicSurfaceModuleSync, })); +vi.mock("../infra/openclaw-root.js", () => ({ + resolveOpenClawPackageRootSync, +})); + describe("plugin-sdk qa-runtime", () => { + const tempDirs: string[] = []; + const originalPrivateQaCli = process.env.OPENCLAW_ENABLE_PRIVATE_QA_CLI; + beforeEach(() => { loadBundledPluginPublicSurfaceModuleSync.mockReset(); + resolveOpenClawPackageRootSync.mockReset().mockReturnValue(null); + delete process.env.OPENCLAW_ENABLE_PRIVATE_QA_CLI; + }); + + afterEach(() => { + for (const dir of tempDirs.splice(0)) { + fs.rmSync(dir, { recursive: true, force: true }); + } + if (originalPrivateQaCli === undefined) { + delete process.env.OPENCLAW_ENABLE_PRIVATE_QA_CLI; + } else { + process.env.OPENCLAW_ENABLE_PRIVATE_QA_CLI = originalPrivateQaCli; + } }); it("stays cold until the runtime seam is used", async () => { @@ -35,6 +59,34 @@ describe("plugin-sdk qa-runtime", () => { }); }); + it("uses the source bundled tree for qa-lab runtime loading in private qa mode", async () => { + const sourceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-qa-runtime-root-")); + tempDirs.push(sourceRoot); + fs.mkdirSync(path.join(sourceRoot, "src"), { recursive: true }); + fs.mkdirSync(path.join(sourceRoot, "extensions"), { recursive: true }); + fs.writeFileSync(path.join(sourceRoot, ".git"), "gitdir: /tmp/mock\n", "utf8"); + process.env.OPENCLAW_ENABLE_PRIVATE_QA_CLI = "1"; + resolveOpenClawPackageRootSync.mockReturnValue(sourceRoot); + + 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", + env: expect.objectContaining({ + OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1", + OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(sourceRoot, "extensions"), + }), + }); + }); + 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"); diff --git a/src/plugin-sdk/qa-runtime.ts b/src/plugin-sdk/qa-runtime.ts index 4fabbfdd137..390a7755bfb 100644 --- a/src/plugin-sdk/qa-runtime.ts +++ b/src/plugin-sdk/qa-runtime.ts @@ -1,4 +1,5 @@ import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js"; +import { resolvePrivateQaBundledPluginsEnv } from "./private-qa-bundled-env.js"; type QaRuntimeSurface = { defaultQaRuntimeModelForMode: ( @@ -20,9 +21,11 @@ function isMissingQaRuntimeError(error: unknown) { } export function loadQaRuntimeModule(): QaRuntimeSurface { + const env = resolvePrivateQaBundledPluginsEnv(); return loadBundledPluginPublicSurfaceModuleSync({ dirName: "qa-lab", artifactBasename: "runtime-api.js", + ...(env ? { env } : {}), }); }