fix: align private QA runner roots

This commit is contained in:
Gustavo Madeira Santana
2026-04-15 20:55:47 -04:00
parent 899eb2582f
commit b5a9179bc6
5 changed files with 99 additions and 16 deletions

View File

@@ -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) {

View File

@@ -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) : "<default>"}`;
}
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<T extends object>(param
dirName: string;
artifactBasename: string;
trackedPluginId?: string | (() => string);
env?: NodeJS.ProcessEnv;
}): T {
const location = resolveFacadeModuleLocation(params);
if (!location) {

View File

@@ -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) : "<default>"}`;
}
@@ -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<T extends object>(
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<T extends object>(params: {
dirName: string;
artifactBasename: string;
env?: NodeJS.ProcessEnv;
}): T {
loadFacadeActivationCheckRuntime().resolveActivatedBundledPluginPublicSurfaceAccessOrThrow(
buildFacadeActivationCheckParams(params),
@@ -271,6 +283,7 @@ export function loadActivatedBundledPluginPublicSurfaceModuleSync<T extends obje
export function tryLoadActivatedBundledPluginPublicSurfaceModuleSync<T extends object>(params: {
dirName: string;
artifactBasename: string;
env?: NodeJS.ProcessEnv;
}): T | null {
const access = loadFacadeActivationCheckRuntime().resolveBundledPluginPublicSurfaceAccess(
buildFacadeActivationCheckParams(params),

View File

@@ -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 () => {

View File

@@ -56,9 +56,11 @@ function isMissingQaRuntimeError(error: unknown) {
}
export function loadQaRuntimeModule(): QaRuntimeSurface {
const env = resolvePrivateQaRunnerEnv();
return loadBundledPluginPublicSurfaceModuleSync<QaRuntimeSurface>({
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<PluginManifestRecord["qaRunners"]>;
}
> {
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<QaRunnerRuntimeSurface>({
dirName: plugin.id,
artifactBasename: "runtime-api.js",
...(env ? { env } : {}),
});
}
return tryLoadActivatedBundledPluginPublicSurfaceModuleSync<QaRunnerRuntimeSurface>({
dirName: plugin.id,
artifactBasename: "runtime-api.js",
...(env ? { env } : {}),
});
}
export function listQaRunnerCliContributions(): readonly QaRunnerCliContribution[] {
const env = resolvePrivateQaRunnerEnv();
const contributions = new Map<string, QaRunnerCliContribution>();
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;