test(qa): support packaged gateway live lanes

This commit is contained in:
Ayaan Zaidi
2026-04-24 09:07:39 +05:30
parent b98a6a46fb
commit 95f6697bd7
3 changed files with 132 additions and 52 deletions

View File

@@ -54,6 +54,13 @@ export type QaGatewayChildStateMutationContext = {
tempRoot: string;
};
export type QaGatewayChildCommand = {
executablePath: string;
argsPrefix?: string[];
cwd?: string;
usePackagedPlugins?: boolean;
};
async function getFreePort() {
return await new Promise<number>((resolve, reject) => {
const server = net.createServer();
@@ -435,6 +442,7 @@ export function resolveQaControlUiRoot(params: { repoRoot: string; controlUiEnab
export async function startQaGatewayChild(params: {
repoRoot: string;
command?: QaGatewayChildCommand;
providerBaseUrl?: string;
transport: Pick<QaTransportAdapter, "requiredPluginIds" | "createGatewayConfig">;
transportBaseUrl: string;
@@ -455,6 +463,10 @@ export async function startQaGatewayChild(params: {
);
const runtimeCwd = tempRoot;
const distEntryPath = path.join(params.repoRoot, "dist", "index.js");
const gatewayCommand = params.command;
const gatewayExecutablePath = gatewayCommand?.executablePath;
const gatewayArgsPrefix = gatewayCommand?.argsPrefix ?? [];
const gatewayCwd = gatewayCommand?.cwd ?? runtimeCwd;
const workspaceDir = path.join(tempRoot, "workspace");
const stateDir = path.join(tempRoot, "state");
const homeDir = path.join(tempRoot, "home");
@@ -558,7 +570,17 @@ export async function startQaGatewayChild(params: {
let env: NodeJS.ProcessEnv | null = null;
try {
const nodeExecPath = await resolveQaNodeExecPath();
const nodeExecPath = gatewayExecutablePath ?? (await resolveQaNodeExecPath());
const buildGatewayArgs = () => [
...(gatewayExecutablePath ? gatewayArgsPrefix : [distEntryPath, ...gatewayArgsPrefix]),
"gateway",
"run",
"--port",
String(gatewayPort),
"--bind",
"loopback",
"--allow-unconfigured",
];
for (let attempt = 1; attempt <= QA_GATEWAY_CHILD_STARTUP_MAX_ATTEMPTS; attempt += 1) {
gatewayPort = await getFreePort();
baseUrl = `http://127.0.0.1:${gatewayPort}`;
@@ -574,16 +596,22 @@ export async function startQaGatewayChild(params: {
);
},
);
const { bundledPluginsDir, stagedRoot } = await createQaBundledPluginsDir({
repoRoot: params.repoRoot,
tempRoot,
allowedPluginIds,
});
stagedBundledPluginsRoot = stagedRoot;
const runtimeHostVersion = await resolveQaRuntimeHostVersion({
repoRoot: params.repoRoot,
allowedPluginIds,
});
const stagedPluginRuntime = gatewayCommand?.usePackagedPlugins
? { bundledPluginsDir: undefined, runtimeHostVersion: undefined }
: {
...(await createQaBundledPluginsDir({
repoRoot: params.repoRoot,
tempRoot,
allowedPluginIds,
})),
runtimeHostVersion: await resolveQaRuntimeHostVersion({
repoRoot: params.repoRoot,
allowedPluginIds,
}),
};
if ("stagedRoot" in stagedPluginRuntime) {
stagedBundledPluginsRoot = stagedPluginRuntime.stagedRoot;
}
env = buildQaRuntimeEnv({
configPath,
gatewayToken,
@@ -593,8 +621,8 @@ export async function startQaGatewayChild(params: {
xdgConfigHome,
xdgDataHome,
xdgCacheHome,
bundledPluginsDir,
compatibilityHostVersion: runtimeHostVersion,
bundledPluginsDir: stagedPluginRuntime.bundledPluginsDir,
compatibilityHostVersion: stagedPluginRuntime.runtimeHostVersion,
providerMode,
forwardHostHomeForClaudeCli: liveProviderIds.includes("claude-cli"),
claudeCliAuthMode: params.claudeCliAuthMode,
@@ -608,25 +636,12 @@ export async function startQaGatewayChild(params: {
throw new Error("qa gateway runtime env not initialized");
}
const attemptChild = spawn(
nodeExecPath,
[
distEntryPath,
"gateway",
"run",
"--port",
String(gatewayPort),
"--bind",
"loopback",
"--allow-unconfigured",
],
{
cwd: runtimeCwd,
env,
detached: process.platform !== "win32",
stdio: ["ignore", "pipe", "pipe"],
},
);
const attemptChild = spawn(nodeExecPath, buildGatewayArgs(), {
cwd: gatewayCwd,
env,
detached: process.platform !== "win32",
stdio: ["ignore", "pipe", "pipe"],
});
attemptChild.stdout.on("data", (chunk) => {
const buffer = Buffer.from(chunk);
stdout.push(buffer);
@@ -714,25 +729,12 @@ export async function startQaGatewayChild(params: {
const runningEnv = env;
const spawnReplacementGatewayChild = async () => {
const nextChild = spawn(
nodeExecPath,
[
distEntryPath,
"gateway",
"run",
"--port",
String(gatewayPort),
"--bind",
"loopback",
"--allow-unconfigured",
],
{
cwd: runtimeCwd,
env: runningEnv,
detached: process.platform !== "win32",
stdio: ["ignore", "pipe", "pipe"],
},
);
const nextChild = spawn(nodeExecPath, buildGatewayArgs(), {
cwd: gatewayCwd,
env: runningEnv,
detached: process.platform !== "win32",
stdio: ["ignore", "pipe", "pipe"],
});
nextChild.stdout.on("data", (chunk) => {
const buffer = Buffer.from(chunk);
stdout.push(buffer);

View File

@@ -1,5 +1,9 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { startQaGatewayChild, type QaCliBackendAuthMode } from "../../gateway-child.js";
import {
startQaGatewayChild,
type QaCliBackendAuthMode,
type QaGatewayChildCommand,
} from "../../gateway-child.js";
import type { QaProviderMode } from "../../model-selection.js";
import { startQaProviderServer } from "../../providers/server-runtime.js";
import type { QaThinkingLevel } from "../../qa-gateway-config.js";
@@ -32,6 +36,7 @@ async function stopQaLiveLaneResources(
export async function startQaLiveLaneGateway(params: {
repoRoot: string;
command?: QaGatewayChildCommand;
transport: {
requiredPluginIds: readonly string[];
createGatewayConfig: (params: {
@@ -53,6 +58,7 @@ export async function startQaLiveLaneGateway(params: {
try {
const gateway = await startQaGatewayChild({
repoRoot: params.repoRoot,
command: params.command,
providerBaseUrl: mock ? `${mock.baseUrl}/v1` : undefined,
transport: params.transport,
transportBaseUrl: params.transportBaseUrl,

View File

@@ -1,6 +1,8 @@
import { execFile } from "node:child_process";
import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
@@ -306,6 +308,7 @@ const QA_REDACT_PUBLIC_METADATA_ENV = "OPENCLAW_QA_REDACT_PUBLIC_METADATA";
const QA_SUITE_PROGRESS_ENV = "OPENCLAW_QA_SUITE_PROGRESS";
const TELEGRAM_QA_PROGRESS_DETAIL_LIMIT = 240;
const TELEGRAM_QA_PROGRESS_PREFIX = "[qa-telegram-live]";
const execFileAsync = promisify(execFile);
const telegramQaCredentialPayloadSchema = z.object({
groupId: z.string().trim().min(1),
@@ -962,9 +965,63 @@ function canaryFailureMessage(params: {
].join("\n");
}
async function runInstalledOpenClawTelegramOnboardingPreflight(params: {
openClawCommand: string;
sutToken: string;
}) {
const tempRoot = await fs.mkdtemp(path.join(process.cwd(), ".tmp-openclaw-npm-telegram-"));
const homeDir = path.join(tempRoot, "home");
const stateDir = path.join(homeDir, ".openclaw");
await fs.mkdir(stateDir, { recursive: true });
const env = {
...process.env,
HOME: homeDir,
OPENCLAW_HOME: stateDir,
OPENCLAW_CONFIG_PATH: path.join(stateDir, "openclaw.json"),
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_GATEWAY_TOKEN: "npm-telegram-live-onboard",
};
try {
await execFileAsync(
params.openClawCommand,
[
"onboard",
"--non-interactive",
"--accept-risk",
"--mode",
"local",
"--auth-choice",
"openai-api-key",
"--secret-input-mode",
"ref",
"--gateway-port",
"18789",
"--gateway-bind",
"loopback",
"--skip-daemon",
"--skip-ui",
"--skip-skills",
"--skip-health",
"--json",
],
{ env },
);
await execFileAsync(
params.openClawCommand,
["channels", "add", "--channel", "telegram", "--token", params.sutToken],
{ env },
);
await execFileAsync(params.openClawCommand, ["doctor", "--non-interactive"], { env });
} finally {
await fs.rm(tempRoot, { recursive: true, force: true }).catch(() => {});
}
}
export async function runTelegramQaLive(params: {
repoRoot?: string;
outputDir?: string;
sutOpenClawCommand?: string;
preflightInstalledOnboarding?: boolean;
providerMode?: QaProviderModeInput;
primaryModel?: string;
alternateModel?: string;
@@ -1022,6 +1079,15 @@ export async function runTelegramQaLive(params: {
const cleanupIssues: string[] = [];
let canaryFailure: string | null = null;
try {
if (params.sutOpenClawCommand && params.preflightInstalledOnboarding === true) {
writeTelegramQaProgress(progressEnabled, "installed package onboarding preflight start");
await runInstalledOpenClawTelegramOnboardingPreflight({
openClawCommand: params.sutOpenClawCommand,
sutToken: runtimeEnv.sutToken,
});
writeTelegramQaProgress(progressEnabled, "installed package onboarding preflight pass");
}
const driverIdentity = await getBotIdentity(runtimeEnv.driverToken);
const sutIdentity = await getBotIdentity(runtimeEnv.sutToken);
const sutUsername = sutIdentity.username?.trim();
@@ -1040,6 +1106,12 @@ export async function runTelegramQaLive(params: {
const gatewayHarness = await startQaLiveLaneGateway({
repoRoot,
command: params.sutOpenClawCommand
? {
executablePath: params.sutOpenClawCommand,
usePackagedPlugins: true,
}
: undefined,
transport: {
requiredPluginIds: [],
createGatewayConfig: () => ({}),