From b5a9179bc6c42fcddfe24f208a7246da841b1d4d Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 15 Apr 2026 20:55:47 -0400 Subject: [PATCH] fix: align private QA runner roots --- .../facade-activation-check.runtime.ts | 24 ++++++++++--- src/plugin-sdk/facade-loader.ts | 15 ++++++-- src/plugin-sdk/facade-runtime.ts | 19 ++++++++-- src/plugin-sdk/qa-runner-runtime.test.ts | 36 +++++++++++++++++++ src/plugin-sdk/qa-runner-runtime.ts | 21 +++++++---- 5 files changed, 99 insertions(+), 16 deletions(-) diff --git a/src/plugin-sdk/facade-activation-check.runtime.ts b/src/plugin-sdk/facade-activation-check.runtime.ts index 87752a9043d..c022d6aa132 100644 --- a/src/plugin-sdk/facade-activation-check.runtime.ts +++ b/src/plugin-sdk/facade-activation-check.runtime.ts @@ -144,7 +144,10 @@ function getFacadeBoundaryResolvedConfig() { return resolved; } -function getFacadeManifestRegistry(params: { cacheKey: string }): readonly PluginManifestRecord[] { +function getFacadeManifestRegistry(params: { + cacheKey: string; + env?: NodeJS.ProcessEnv; +}): readonly PluginManifestRecord[] { const cached = cachedManifestRegistryByKey.get(params.cacheKey); if (cached) { return cached; @@ -152,6 +155,7 @@ function getFacadeManifestRegistry(params: { cacheKey: string }): readonly Plugi const loaded = loadPluginManifestRegistry({ config: getFacadeBoundaryResolvedConfig().config, cache: true, + ...(params.env ? { env: params.env } : {}), }).plugins; cachedManifestRegistryByKey.set(params.cacheKey, loaded); return loaded; @@ -161,8 +165,12 @@ export function resolveRegistryPluginModuleLocation(params: { dirName: string; artifactBasename: string; resolutionKey: string; + env?: NodeJS.ProcessEnv; }): FacadeModuleLocation | null { - const registry = getFacadeManifestRegistry({ cacheKey: params.resolutionKey }); + const registry = getFacadeManifestRegistry({ + cacheKey: params.resolutionKey, + ...(params.env ? { env: params.env } : {}), + }); type RegistryRecord = (typeof registry)[number]; const tiers: Array<(plugin: RegistryRecord) => boolean> = [ (plugin) => plugin.id === params.dirName, @@ -229,6 +237,7 @@ function resolveBundledMetadataManifestRecord(params: { artifactBasename: string; location: FacadeModuleLocation | null; sourceExtensionsRoot: string; + env?: NodeJS.ProcessEnv; }): FacadePluginManifestLike | null { if (!params.location) { return null; @@ -247,7 +256,7 @@ function resolveBundledMetadataManifestRecord(params: { resolvedDirName, }); } - const bundledPluginsDir = resolveBundledPluginsDir(); + const bundledPluginsDir = resolveBundledPluginsDir(params.env ?? process.env); if (!bundledPluginsDir) { return null; } @@ -275,6 +284,7 @@ function resolveBundledPluginManifestRecord(params: { location: FacadeModuleLocation | null; sourceExtensionsRoot: string; resolutionKey: string; + env?: NodeJS.ProcessEnv; }): FacadePluginManifestLike | null { if (cachedFacadeManifestRecordsByKey.has(params.resolutionKey)) { return cachedFacadeManifestRecordsByKey.get(params.resolutionKey) ?? null; @@ -286,7 +296,10 @@ function resolveBundledPluginManifestRecord(params: { return metadataRecord; } - const registry = getFacadeManifestRegistry({ cacheKey: params.resolutionKey }); + const registry = getFacadeManifestRegistry({ + cacheKey: params.resolutionKey, + ...(params.env ? { env: params.env } : {}), + }); const resolved = (params.location ? registry.find((plugin) => { @@ -312,6 +325,7 @@ export function resolveTrackedFacadePluginId(params: { location: FacadeModuleLocation | null; sourceExtensionsRoot: string; resolutionKey: string; + env?: NodeJS.ProcessEnv; }): string { return resolveBundledPluginManifestRecord(params)?.id ?? params.dirName; } @@ -322,6 +336,7 @@ export function resolveBundledPluginPublicSurfaceAccess(params: { location: FacadeModuleLocation | null; sourceExtensionsRoot: string; resolutionKey: string; + env?: NodeJS.ProcessEnv; }): { allowed: boolean; pluginId?: string; reason?: string } { const cached = cachedFacadePublicSurfaceAccessByKey.get(params.resolutionKey); if (cached) { @@ -410,6 +425,7 @@ export function resolveActivatedBundledPluginPublicSurfaceAccessOrThrow(params: location: FacadeModuleLocation | null; sourceExtensionsRoot: string; resolutionKey: string; + env?: NodeJS.ProcessEnv; }) { const access = resolveBundledPluginPublicSurfaceAccess(params); if (!access.allowed) { diff --git a/src/plugin-sdk/facade-loader.ts b/src/plugin-sdk/facade-loader.ts index e6ba89332e7..1e51b1c272a 100644 --- a/src/plugin-sdk/facade-loader.ts +++ b/src/plugin-sdk/facade-loader.ts @@ -52,16 +52,21 @@ function getOpenClawPackageRoot() { return cachedOpenClawPackageRoot; } -function createFacadeResolutionKey(params: { dirName: string; artifactBasename: string }): string { - const bundledPluginsDir = resolveBundledPluginsDir(); +function createFacadeResolutionKey(params: { + dirName: string; + artifactBasename: string; + env?: NodeJS.ProcessEnv; +}): string { + const bundledPluginsDir = resolveBundledPluginsDir(params.env ?? process.env); return `${params.dirName}::${params.artifactBasename}::${bundledPluginsDir ? path.resolve(bundledPluginsDir) : ""}`; } function resolveFacadeModuleLocationUncached(params: { dirName: string; artifactBasename: string; + env?: NodeJS.ProcessEnv; }): { modulePath: string; boundaryRoot: string } | null { - const bundledPluginsDir = resolveBundledPluginsDir(); + const bundledPluginsDir = resolveBundledPluginsDir(params.env ?? process.env); const preferSource = !CURRENT_MODULE_PATH.includes(`${path.sep}dist${path.sep}`); if (preferSource) { const modulePath = @@ -71,6 +76,7 @@ function resolveFacadeModuleLocationUncached(params: { }) ?? resolveBundledPluginPublicSurfacePath({ rootDir: getOpenClawPackageRoot(), + env: params.env, ...(bundledPluginsDir ? { bundledPluginsDir } : {}), dirName: params.dirName, artifactBasename: params.artifactBasename, @@ -88,6 +94,7 @@ function resolveFacadeModuleLocationUncached(params: { } const modulePath = resolveBundledPluginPublicSurfacePath({ rootDir: getOpenClawPackageRoot(), + env: params.env, ...(bundledPluginsDir ? { bundledPluginsDir } : {}), dirName: params.dirName, artifactBasename: params.artifactBasename, @@ -107,6 +114,7 @@ function resolveFacadeModuleLocationUncached(params: { function resolveFacadeModuleLocation(params: { dirName: string; artifactBasename: string; + env?: NodeJS.ProcessEnv; }): { modulePath: string; boundaryRoot: string } | null { const key = createFacadeResolutionKey(params); if (cachedFacadeModuleLocationsByKey.has(key)) { @@ -253,6 +261,7 @@ export function loadBundledPluginPublicSurfaceModuleSync(param dirName: string; artifactBasename: string; trackedPluginId?: string | (() => string); + env?: NodeJS.ProcessEnv; }): T { const location = resolveFacadeModuleLocation(params); if (!location) { diff --git a/src/plugin-sdk/facade-runtime.ts b/src/plugin-sdk/facade-runtime.ts index c257b066724..ae87087c820 100644 --- a/src/plugin-sdk/facade-runtime.ts +++ b/src/plugin-sdk/facade-runtime.ts @@ -42,8 +42,12 @@ const cachedFacadeModuleLocationsByKey = new Map< } | null >(); -function createFacadeResolutionKey(params: { dirName: string; artifactBasename: string }): string { - const bundledPluginsDir = resolveBundledPluginsDir(); +function createFacadeResolutionKey(params: { + dirName: string; + artifactBasename: string; + env?: NodeJS.ProcessEnv; +}): string { + const bundledPluginsDir = resolveBundledPluginsDir(params.env ?? process.env); return `${params.dirName}::${params.artifactBasename}::${bundledPluginsDir ? path.resolve(bundledPluginsDir) : ""}`; } @@ -81,6 +85,7 @@ function resolveRegistryPluginModuleLocationFromRegistry(params: { function resolveRegistryPluginModuleLocation(params: { dirName: string; artifactBasename: string; + env?: NodeJS.ProcessEnv; }): { modulePath: string; boundaryRoot: string } | null { return loadFacadeActivationCheckRuntime().resolveRegistryPluginModuleLocation({ ...params, @@ -91,8 +96,9 @@ function resolveRegistryPluginModuleLocation(params: { function resolveFacadeModuleLocationUncached(params: { dirName: string; artifactBasename: string; + env?: NodeJS.ProcessEnv; }): { modulePath: string; boundaryRoot: string } | null { - const bundledPluginsDir = resolveBundledPluginsDir(); + const bundledPluginsDir = resolveBundledPluginsDir(params.env ?? process.env); const preferSource = !CURRENT_MODULE_PATH.includes(`${path.sep}dist${path.sep}`); if (preferSource) { const modulePath = @@ -102,6 +108,7 @@ function resolveFacadeModuleLocationUncached(params: { }) ?? resolveBundledPluginPublicSurfacePath({ rootDir: OPENCLAW_PACKAGE_ROOT, + env: params.env, ...(bundledPluginsDir ? { bundledPluginsDir } : {}), dirName: params.dirName, artifactBasename: params.artifactBasename, @@ -119,6 +126,7 @@ function resolveFacadeModuleLocationUncached(params: { } const modulePath = resolveBundledPluginPublicSurfacePath({ rootDir: OPENCLAW_PACKAGE_ROOT, + env: params.env, ...(bundledPluginsDir ? { bundledPluginsDir } : {}), dirName: params.dirName, artifactBasename: params.artifactBasename, @@ -138,6 +146,7 @@ function resolveFacadeModuleLocationUncached(params: { function resolveFacadeModuleLocation(params: { dirName: string; artifactBasename: string; + env?: NodeJS.ProcessEnv; }): { modulePath: string; boundaryRoot: string } | null { const key = createFacadeResolutionKey(params); if (cachedFacadeModuleLocationsByKey.has(key)) { @@ -151,6 +160,7 @@ function resolveFacadeModuleLocation(params: { type BundledPluginPublicSurfaceParams = { dirName: string; artifactBasename: string; + env?: NodeJS.ProcessEnv; }; type FacadeActivationCheckRuntimeModule = typeof import("./facade-activation-check.runtime.js"); @@ -252,6 +262,7 @@ export function loadBundledPluginPublicSurfaceModuleSync( export function canLoadActivatedBundledPluginPublicSurface(params: { dirName: string; artifactBasename: string; + env?: NodeJS.ProcessEnv; }): boolean { return loadFacadeActivationCheckRuntime().resolveBundledPluginPublicSurfaceAccess( buildFacadeActivationCheckParams(params), @@ -261,6 +272,7 @@ export function canLoadActivatedBundledPluginPublicSurface(params: { export function loadActivatedBundledPluginPublicSurfaceModuleSync(params: { dirName: string; artifactBasename: string; + env?: NodeJS.ProcessEnv; }): T { loadFacadeActivationCheckRuntime().resolveActivatedBundledPluginPublicSurfaceAccessOrThrow( buildFacadeActivationCheckParams(params), @@ -271,6 +283,7 @@ export function loadActivatedBundledPluginPublicSurfaceModuleSync(params: { dirName: string; artifactBasename: string; + env?: NodeJS.ProcessEnv; }): T | null { const access = loadFacadeActivationCheckRuntime().resolveBundledPluginPublicSurfaceAccess( buildFacadeActivationCheckParams(params), diff --git a/src/plugin-sdk/qa-runner-runtime.test.ts b/src/plugin-sdk/qa-runner-runtime.test.ts index 270415d3141..be937ced849 100644 --- a/src/plugin-sdk/qa-runner-runtime.test.ts +++ b/src/plugin-sdk/qa-runner-runtime.test.ts @@ -72,6 +72,34 @@ describe("plugin-sdk qa-runner-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-runner-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 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"); @@ -194,6 +222,14 @@ describe("plugin-sdk qa-runner-runtime", () => { OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(sourceRoot, "extensions"), }), }); + expect(loadBundledPluginPublicSurfaceModuleSync).toHaveBeenCalledWith({ + dirName: "qa-matrix", + artifactBasename: "runtime-api.js", + env: expect.objectContaining({ + OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1", + OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(sourceRoot, "extensions"), + }), + }); }); it("fails fast when two plugins declare the same qa runner command", async () => { diff --git a/src/plugin-sdk/qa-runner-runtime.ts b/src/plugin-sdk/qa-runner-runtime.ts index 06d942009b9..4f782777112 100644 --- a/src/plugin-sdk/qa-runner-runtime.ts +++ b/src/plugin-sdk/qa-runner-runtime.ts @@ -56,9 +56,11 @@ function isMissingQaRuntimeError(error: unknown) { } export function loadQaRuntimeModule(): QaRuntimeSurface { + const env = resolvePrivateQaRunnerEnv(); return loadBundledPluginPublicSurfaceModuleSync({ dirName: ["qa", "lab"].join("-"), artifactBasename: ["runtime-api", "js"].join("."), + ...(env ? { env } : {}), }); } @@ -74,7 +76,7 @@ export function isQaRuntimeAvailable(): boolean { } } -function resolvePrivateQaRunnerManifestEnv( +function resolvePrivateQaRunnerEnv( env: NodeJS.ProcessEnv = process.env, ): NodeJS.ProcessEnv | undefined { if (env.OPENCLAW_ENABLE_PRIVATE_QA_CLI !== "1") { @@ -102,12 +104,13 @@ function resolvePrivateQaRunnerManifestEnv( }; } -function listDeclaredQaRunnerPlugins(): Array< +function listDeclaredQaRunnerPlugins( + env: NodeJS.ProcessEnv | undefined = resolvePrivateQaRunnerEnv(), +): Array< PluginManifestRecord & { qaRunners: NonNullable; } > { - const env = resolvePrivateQaRunnerManifestEnv(); return loadPluginManifestRegistry({ cache: true, ...(env ? { env } : {}) }) .plugins.filter( ( @@ -145,24 +148,30 @@ function indexRuntimeRegistrations( return registrationByCommandName; } -function loadQaRunnerRuntimeSurface(plugin: PluginManifestRecord): QaRunnerRuntimeSurface | null { +function loadQaRunnerRuntimeSurface( + plugin: PluginManifestRecord, + env?: NodeJS.ProcessEnv, +): QaRunnerRuntimeSurface | null { if (plugin.origin === "bundled") { return loadBundledPluginPublicSurfaceModuleSync({ dirName: plugin.id, artifactBasename: "runtime-api.js", + ...(env ? { env } : {}), }); } return tryLoadActivatedBundledPluginPublicSurfaceModuleSync({ dirName: plugin.id, artifactBasename: "runtime-api.js", + ...(env ? { env } : {}), }); } export function listQaRunnerCliContributions(): readonly QaRunnerCliContribution[] { + const env = resolvePrivateQaRunnerEnv(); const contributions = new Map(); - for (const plugin of listDeclaredQaRunnerPlugins()) { - const runtimeSurface = loadQaRunnerRuntimeSurface(plugin); + for (const plugin of listDeclaredQaRunnerPlugins(env)) { + const runtimeSurface = loadQaRunnerRuntimeSurface(plugin, env); const runtimeRegistrationByCommandName = runtimeSurface ? indexRuntimeRegistrations(plugin.id, runtimeSurface) : null;