Skip to content

Commit 6531004

Browse files
committed
QA: fix matrix runner staging and host registration
1 parent 731d466 commit 6531004

3 files changed

Lines changed: 91 additions & 11 deletions

File tree

extensions/qa-lab/src/gateway-child.test.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -652,13 +652,14 @@ describe("qa bundled plugin dir", () => {
652652
);
653653
});
654654

655-
it("creates a scoped bundled plugin tree for the allowed plugins only", async () => {
655+
it("creates a scoped bundled plugin tree for allowed plugins plus always-allowed runtime facades", async () => {
656656
const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-scope-"));
657657
cleanups.push(async () => {
658658
await rm(repoRoot, { recursive: true, force: true });
659659
});
660660
await mkdir(path.join(repoRoot, "dist", "extensions", "qa-channel"), { recursive: true });
661661
await mkdir(path.join(repoRoot, "dist", "extensions", "memory-core"), { recursive: true });
662+
await mkdir(path.join(repoRoot, "dist", "extensions", "speech-core"), { recursive: true });
662663
await mkdir(path.join(repoRoot, "dist", "extensions", "unused-plugin"), { recursive: true });
663664
await writeFile(path.join(repoRoot, "dist", "shared-chunk-abc123.js"), "export {};\n", "utf8");
664665
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-target-"));
@@ -672,7 +673,11 @@ describe("qa bundled plugin dir", () => {
672673
allowedPluginIds: ["qa-channel", "memory-core"],
673674
});
674675

675-
expect((await readdir(bundledPluginsDir)).toSorted()).toEqual(["memory-core", "qa-channel"]);
676+
expect((await readdir(bundledPluginsDir)).toSorted()).toEqual([
677+
"memory-core",
678+
"qa-channel",
679+
"speech-core",
680+
]);
676681
expect(bundledPluginsDir).toBe(
677682
path.join(
678683
repoRoot,
@@ -688,6 +693,7 @@ describe("qa bundled plugin dir", () => {
688693
);
689694
expect((await lstat(path.join(bundledPluginsDir, "qa-channel"))).isDirectory()).toBe(true);
690695
expect((await lstat(path.join(bundledPluginsDir, "memory-core"))).isDirectory()).toBe(true);
696+
expect((await lstat(path.join(bundledPluginsDir, "speech-core"))).isDirectory()).toBe(true);
691697
await expect(
692698
lstat(
693699
path.join(
@@ -854,4 +860,37 @@ describe("qa bundled plugin dir", () => {
854860
}),
855861
).resolves.toBe("2026.4.8");
856862
});
863+
864+
it("includes always-allowed runtime facade plugins when raising the QA runtime host version", async () => {
865+
const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-runtime-version-runtime-facade-"));
866+
cleanups.push(async () => {
867+
await rm(repoRoot, { recursive: true, force: true });
868+
});
869+
await writeFile(
870+
path.join(repoRoot, "package.json"),
871+
JSON.stringify({ version: "2026.4.7-1" }),
872+
"utf8",
873+
);
874+
const bundledRoot = path.join(repoRoot, "extensions");
875+
await mkdir(path.join(bundledRoot, "qa-channel"), { recursive: true });
876+
await writeFile(
877+
path.join(bundledRoot, "qa-channel", "package.json"),
878+
JSON.stringify({ openclaw: { install: { minHostVersion: ">=2026.4.8" } } }),
879+
"utf8",
880+
);
881+
await mkdir(path.join(bundledRoot, "speech-core"), { recursive: true });
882+
await writeFile(
883+
path.join(bundledRoot, "speech-core", "package.json"),
884+
JSON.stringify({ openclaw: { install: { minHostVersion: ">=2026.4.9" } } }),
885+
"utf8",
886+
);
887+
888+
await expect(
889+
__testing.resolveQaRuntimeHostVersion({
890+
repoRoot,
891+
bundledPluginsSourceRoot: bundledRoot,
892+
allowedPluginIds: ["qa-channel"],
893+
}),
894+
).resolves.toBe("2026.4.9");
895+
});
857896
});

extensions/qa-lab/src/gateway-child.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,14 @@ const QA_MOCK_BLOCKED_ENV_KEY_PATTERNS = Object.freeze([
7878

7979
const QA_LIVE_PROVIDER_CONFIG_PATH_ENV = "OPENCLAW_QA_LIVE_PROVIDER_CONFIG_PATH";
8080
const QA_LIVE_ANTHROPIC_SETUP_TOKEN_ENV = "OPENCLAW_QA_LIVE_ANTHROPIC_SETUP_TOKEN";
81+
// Keep this in sync with the facade runtime's always-allowed bundled surfaces.
82+
// QA child staging must include these runtime helpers even when they are not in
83+
// cfg.plugins.allow, otherwise lazy facade loads can fail inside the child.
84+
const QA_ALWAYS_STAGE_RUNTIME_PLUGIN_IDS = Object.freeze([
85+
"image-generation-core",
86+
"media-understanding-core",
87+
"speech-core",
88+
]);
8189
const QA_LIVE_SETUP_TOKEN_VALUE_ENV = "OPENCLAW_LIVE_SETUP_TOKEN_VALUE";
8290
const QA_LIVE_ANTHROPIC_SETUP_TOKEN_PROFILE_ENV = "OPENCLAW_QA_LIVE_ANTHROPIC_SETUP_TOKEN_PROFILE";
8391
const QA_LIVE_ANTHROPIC_SETUP_TOKEN_PROFILE_ID = "anthropic:qa-setup-token";
@@ -765,8 +773,12 @@ async function resolveQaRuntimeHostVersion(params: {
765773
const rootPackageRaw = await fs.readFile(path.join(params.repoRoot, "package.json"), "utf8");
766774
const rootPackage = JSON.parse(rootPackageRaw) as { version?: string };
767775
let selected = parseStableSemverFloor(rootPackage.version);
776+
const stagedPluginIds = collectQaBundledPluginIds({
777+
sourceRoot: params.bundledPluginsSourceRoot,
778+
allowedPluginIds: params.allowedPluginIds,
779+
});
768780

769-
for (const pluginId of params.allowedPluginIds) {
781+
for (const pluginId of stagedPluginIds) {
770782
const packagePath = path.join(params.bundledPluginsSourceRoot, pluginId, "package.json");
771783
if (!existsSync(packagePath)) {
772784
continue;
@@ -788,12 +800,29 @@ async function resolveQaRuntimeHostVersion(params: {
788800
return selected?.label;
789801
}
790802

803+
function collectQaBundledPluginIds(params: {
804+
sourceRoot: string;
805+
allowedPluginIds: readonly string[];
806+
}) {
807+
const pluginIds = new Set(params.allowedPluginIds);
808+
for (const pluginId of QA_ALWAYS_STAGE_RUNTIME_PLUGIN_IDS) {
809+
if (existsSync(path.join(params.sourceRoot, pluginId))) {
810+
pluginIds.add(pluginId);
811+
}
812+
}
813+
return [...pluginIds];
814+
}
815+
791816
async function createQaBundledPluginsDir(params: {
792817
repoRoot: string;
793818
tempRoot: string;
794819
allowedPluginIds: readonly string[];
795820
}) {
796821
const sourceRoot = resolveQaBundledPluginsSourceRoot(params.repoRoot);
822+
const stagedPluginIds = collectQaBundledPluginIds({
823+
sourceRoot,
824+
allowedPluginIds: params.allowedPluginIds,
825+
});
797826
const sourceTreeRoot = path.dirname(sourceRoot);
798827
if (
799828
sourceTreeRoot === path.join(params.repoRoot, "dist") ||
@@ -814,7 +843,7 @@ async function createQaBundledPluginsDir(params: {
814843
const targetPath = path.join(stagedTreeRoot, entry.name);
815844
if (entry.name === "extensions") {
816845
await fs.mkdir(targetPath, { recursive: true });
817-
for (const pluginId of params.allowedPluginIds) {
846+
for (const pluginId of stagedPluginIds) {
818847
const sourceDir = path.join(sourceRoot, pluginId);
819848
if (!existsSync(sourceDir)) {
820849
throw new Error(`qa bundled plugin not found: ${pluginId} (${sourceDir})`);
@@ -834,7 +863,7 @@ async function createQaBundledPluginsDir(params: {
834863

835864
const bundledPluginsDir = path.join(params.tempRoot, "bundled-plugins");
836865
await fs.mkdir(bundledPluginsDir, { recursive: true });
837-
for (const pluginId of params.allowedPluginIds) {
866+
for (const pluginId of stagedPluginIds) {
838867
const sourceDir = path.join(sourceRoot, pluginId);
839868
if (!existsSync(sourceDir)) {
840869
throw new Error(`qa bundled plugin not found: ${pluginId} (${sourceDir})`);

src/plugin-sdk/qa-runner-runtime.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import type { Command } from "commander";
22
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
33
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
44
import { listBundledQaRunnerCatalog } from "../plugins/qa-runner-catalog.js";
5-
import { tryLoadActivatedBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
5+
import {
6+
loadBundledPluginPublicSurfaceModuleSync,
7+
tryLoadActivatedBundledPluginPublicSurfaceModuleSync,
8+
} from "./facade-runtime.js";
69

710
export type QaRunnerCliRegistration = {
811
commandName: string;
@@ -98,6 +101,19 @@ function buildKnownQaRunnerCatalog(): readonly QaRunnerCliContribution[] {
98101
});
99102
}
100103

104+
function loadQaRunnerRuntimeSurface(plugin: PluginManifestRecord): QaRunnerRuntimeSurface | null {
105+
if (plugin.origin === "bundled") {
106+
return loadBundledPluginPublicSurfaceModuleSync<QaRunnerRuntimeSurface>({
107+
dirName: plugin.id,
108+
artifactBasename: "runtime-api.js",
109+
});
110+
}
111+
return tryLoadActivatedBundledPluginPublicSurfaceModuleSync<QaRunnerRuntimeSurface>({
112+
dirName: plugin.id,
113+
artifactBasename: "runtime-api.js",
114+
});
115+
}
116+
101117
export function listQaRunnerCliContributions(): readonly QaRunnerCliContribution[] {
102118
const contributions = new Map<string, QaRunnerCliContribution>();
103119

@@ -106,11 +122,7 @@ export function listQaRunnerCliContributions(): readonly QaRunnerCliContribution
106122
}
107123

108124
for (const plugin of listDeclaredQaRunnerPlugins()) {
109-
const runtimeSurface =
110-
tryLoadActivatedBundledPluginPublicSurfaceModuleSync<QaRunnerRuntimeSurface>({
111-
dirName: plugin.id,
112-
artifactBasename: "runtime-api.js",
113-
});
125+
const runtimeSurface = loadQaRunnerRuntimeSurface(plugin);
114126
const runtimeRegistrationByCommandName = runtimeSurface
115127
? indexRuntimeRegistrations(plugin.id, runtimeSurface)
116128
: null;

0 commit comments

Comments
 (0)