From 82535771cd818a0acec2bdee8d069bce8980643d Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 7 Apr 2026 11:11:24 +0100 Subject: [PATCH] fix(qa): pin gateway child control ui root --- extensions/qa-lab/src/gateway-child.test.ts | 39 ++++++++++++++++++- extensions/qa-lab/src/gateway-child.ts | 14 +++++++ .../qa-lab/src/qa-gateway-config.test.ts | 14 +++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/extensions/qa-lab/src/gateway-child.test.ts b/extensions/qa-lab/src/gateway-child.test.ts index f099a431c32..f57bfdeb7a5 100644 --- a/extensions/qa-lab/src/gateway-child.test.ts +++ b/extensions/qa-lab/src/gateway-child.test.ts @@ -1,5 +1,16 @@ -import { describe, expect, it } from "vitest"; -import { buildQaRuntimeEnv } from "./gateway-child.js"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { buildQaRuntimeEnv, resolveQaControlUiRoot } from "./gateway-child.js"; + +const cleanups: Array<() => Promise> = []; + +afterEach(async () => { + while (cleanups.length > 0) { + await cleanups.pop()?.(); + } +}); function createParams(baseEnv?: NodeJS.ProcessEnv) { return { @@ -83,3 +94,27 @@ describe("buildQaRuntimeEnv", () => { expect(env.OPENCLAW_LIVE_GEMINI_KEY).toBeUndefined(); }); }); + +describe("resolveQaControlUiRoot", () => { + it("returns the built control ui root when repo assets exist", async () => { + const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-control-ui-root-")); + cleanups.push(async () => { + await rm(repoRoot, { recursive: true, force: true }); + }); + const controlUiRoot = path.join(repoRoot, "dist", "control-ui"); + await mkdir(controlUiRoot, { recursive: true }); + await writeFile(path.join(controlUiRoot, "index.html"), "", "utf8"); + + expect(resolveQaControlUiRoot({ repoRoot })).toBe(controlUiRoot); + }); + + it("returns undefined when control ui is disabled or not built", async () => { + const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-control-ui-root-missing-")); + cleanups.push(async () => { + await rm(repoRoot, { recursive: true, force: true }); + }); + + expect(resolveQaControlUiRoot({ repoRoot })).toBeUndefined(); + expect(resolveQaControlUiRoot({ repoRoot, controlUiEnabled: false })).toBeUndefined(); + }); +}); diff --git a/extensions/qa-lab/src/gateway-child.ts b/extensions/qa-lab/src/gateway-child.ts index 72019b40c61..ec856554965 100644 --- a/extensions/qa-lab/src/gateway-child.ts +++ b/extensions/qa-lab/src/gateway-child.ts @@ -1,5 +1,6 @@ import { spawn } from "node:child_process"; import { randomUUID } from "node:crypto"; +import { existsSync } from "node:fs"; import fs from "node:fs/promises"; import net from "node:net"; import os from "node:os"; @@ -184,6 +185,15 @@ async function runCliJson(params: { cwd: string; env: NodeJS.ProcessEnv; args: s return text ? (JSON.parse(text) as unknown) : {}; } +export function resolveQaControlUiRoot(params: { repoRoot: string; controlUiEnabled?: boolean }) { + if (params.controlUiEnabled === false) { + return undefined; + } + const controlUiRoot = path.join(params.repoRoot, "dist", "control-ui"); + const indexPath = path.join(controlUiRoot, "index.html"); + return existsSync(indexPath) ? controlUiRoot : undefined; +} + export async function startQaGatewayChild(params: { repoRoot: string; providerBaseUrl?: string; @@ -224,6 +234,10 @@ export async function startQaGatewayChild(params: { providerBaseUrl: params.providerBaseUrl, qaBusBaseUrl: params.qaBusBaseUrl, workspaceDir, + controlUiRoot: resolveQaControlUiRoot({ + repoRoot: params.repoRoot, + controlUiEnabled: params.controlUiEnabled, + }), providerMode: params.providerMode, primaryModel: params.primaryModel, alternateModel: params.alternateModel, diff --git a/extensions/qa-lab/src/qa-gateway-config.test.ts b/extensions/qa-lab/src/qa-gateway-config.test.ts index cb0d889da25..386a9b78f66 100644 --- a/extensions/qa-lab/src/qa-gateway-config.test.ts +++ b/extensions/qa-lab/src/qa-gateway-config.test.ts @@ -87,4 +87,18 @@ describe("buildQaGatewayConfig", () => { expect(cfg.gateway?.controlUi).not.toHaveProperty("allowInsecureAuth"); expect(cfg.gateway?.controlUi).not.toHaveProperty("allowedOrigins"); }); + + it("pins control ui to a provided built root when available", () => { + const cfg = buildQaGatewayConfig({ + bind: "loopback", + gatewayPort: 18789, + gatewayToken: "token", + qaBusBaseUrl: "http://127.0.0.1:43124", + workspaceDir: "/tmp/qa-workspace", + controlUiRoot: "/tmp/openclaw/dist/control-ui", + }); + + expect(cfg.gateway?.controlUi?.enabled).toBe(true); + expect(cfg.gateway?.controlUi?.root).toBe("/tmp/openclaw/dist/control-ui"); + }); });