Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled

Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

View File

@@ -0,0 +1,817 @@
import { existsSync } from "node:fs";
import fs from "node:fs/promises";
// QA Lab producer proves Gateway and MCP scenarios across real process and protocol boundaries.
import { createServer, type Server } from "node:http";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { WebSocket, WebSocketServer, type RawData } from "ws";
import {
QA_EVIDENCE_FILENAME,
startQaGatewayChild,
type QaGatewayChildCommand,
type QaEvidenceSummaryJson,
type QaGatewayChildListeningContext,
} from "../../../../extensions/qa-lab/api.js";
import {
PROTOCOL_VERSION,
MIN_CLIENT_PROTOCOL_VERSION,
} from "../../../../packages/gateway-protocol/src/version.js";
import { runGatewaySmoke } from "../../../../scripts/dev/gateway-smoke.js";
import type { OpenClawConfig } from "../../../../src/config/types.openclaw.js";
import { formatErrorMessage } from "../../../../src/infra/errors.js";
import { createMcpClientTempState } from "./mcp-client-temp-state.fixture.ts";
import { createQaScriptEvidenceWriter, type QaScriptEvidenceStatus } from "./script-evidence.ts";
const FIXTURE_PLUGIN_ID = "qa-real-transports-fixture";
const FIXTURE_TOOL_NAME = "memory_search";
const FIXTURE_FACT = "MCP fact: the codename is ORBIT-9.";
const STARTUP_GATE_TIMEOUT_MS = 30_000;
const MCP_CONNECT_TIMEOUT_MS = 30_000;
const SOURCE_PATH = "test/e2e/qa-lab/runtime/gateway-mcp-real-transports.ts";
type ScenarioId = "gateway-smoke" | "mcp-gateway-connect-startup-retry" | "mcp-plugin-tools-call";
type ProducerOptions = {
artifactBase: string;
repoRoot: string;
scenarioId: ScenarioId;
};
type ProofResult = {
details?: string;
durationMs: number;
status: QaScriptEvidenceStatus;
};
type GatewayFrameCapture = {
connectFrames: Array<{ minProtocol: number; maxProtocol: number }>;
helloProtocols: number[];
startupUnavailableResponses: number;
};
type GatewayProxy = {
capture: GatewayFrameCapture;
stop: () => Promise<void>;
url: string;
};
type OpenClawCliInvocation = {
argsPrefix: string[];
command: string;
cwd: string;
gatewayCommand: QaGatewayChildCommand;
};
type ChannelMcpInvocation = {
args: string[];
command: string;
cwd: string;
envPatch: NodeJS.ProcessEnv;
};
type McpClientHandle = {
client: Client;
cleanup: () => void;
stderr: () => string;
transport: StdioClientTransport;
};
const SCENARIOS = {
"gateway-smoke": {
title: "Gateway smoke evidence",
sourcePath: "qa/scenarios/runtime/gateway-smoke.yaml",
primaryCoverageIds: [
"gateway.websocket-transport",
"gateway.health-apis",
"gateway.hello-ok-snapshot",
],
docsRefs: ["docs/gateway/index.md", "docs/concepts/qa-e2e-automation.md"],
codeRefs: [
SOURCE_PATH,
"extensions/qa-lab/src/gateway-child.ts",
"scripts/dev/gateway-smoke.ts",
],
},
"mcp-gateway-connect-startup-retry": {
title: "MCP Gateway connect startup retry",
sourcePath: "qa/scenarios/runtime/mcp-gateway-connect-startup-retry.yaml",
primaryCoverageIds: [
"gateway.connect-request",
"gateway.protocol-version-negotiation",
"gateway.startup-retry",
],
docsRefs: ["docs/gateway/protocol.md", "docs/cli/mcp.md"],
codeRefs: [SOURCE_PATH, "extensions/qa-lab/src/gateway-child.ts", "src/mcp/channel-bridge.ts"],
},
"mcp-plugin-tools-call": {
title: "MCP plugin-tools call",
sourcePath: "qa/scenarios/plugins/mcp-plugin-tools-call.yaml",
primaryCoverageIds: ["plugins.mcp-tools", "tools.invocation"],
docsRefs: ["docs/cli/mcp.md", "docs/gateway/protocol.md"],
codeRefs: [SOURCE_PATH, "src/mcp/plugin-tools-serve.ts", "src/mcp/plugin-tools-handlers.ts"],
},
} as const;
function parseOptions(argv: readonly string[]): ProducerOptions {
const readValue = (name: string) => {
const index = argv.indexOf(name);
return index >= 0 ? argv[index + 1] : undefined;
};
const scenarioId = readValue("--scenario");
if (!scenarioId || !(scenarioId in SCENARIOS)) {
throw new Error(`--scenario must be one of: ${Object.keys(SCENARIOS).join(", ")}`);
}
const artifactBase = readValue("--artifact-base");
if (!artifactBase) {
throw new Error("--artifact-base is required");
}
return {
artifactBase: path.resolve(artifactBase),
repoRoot: path.resolve(readValue("--repo-root") ?? process.cwd()),
scenarioId: scenarioId as ScenarioId,
};
}
async function createFixturePlugin() {
// openclaw-temp-dir: allow standalone producer cleans this root in each scenario finally block
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gateway-mcp-fixture-"));
const pluginDir = path.join(root, FIXTURE_PLUGIN_ID);
const startupGatePath = path.join(root, "startup-connect-observed");
await fs.mkdir(pluginDir, { recursive: true });
await fs.writeFile(
path.join(pluginDir, "openclaw.plugin.json"),
`${JSON.stringify(
{
id: FIXTURE_PLUGIN_ID,
activation: { onStartup: true },
configSchema: { type: "object", additionalProperties: false, properties: {} },
contracts: { tools: [FIXTURE_TOOL_NAME] },
},
null,
2,
)}\n`,
"utf8",
);
await fs.writeFile(
path.join(pluginDir, "index.js"),
`const fs = require("node:fs");
module.exports = {
id: ${JSON.stringify(FIXTURE_PLUGIN_ID)},
register(api) {
api.registerTool({
name: ${JSON.stringify(FIXTURE_TOOL_NAME)},
description: "Search fixture memory",
parameters: {
type: "object",
properties: { query: { type: "string" }, maxResults: { type: "number" } },
required: ["query"],
},
async execute(_toolCallId, params) {
return { content: [{ type: "text", text: ${JSON.stringify(FIXTURE_FACT)} + " query=" + String(params.query) }] };
},
});
api.registerService({
id: "qa-startup-delay",
async start() {
const deadline = Date.now() + ${STARTUP_GATE_TIMEOUT_MS};
while (!fs.existsSync(${JSON.stringify(startupGatePath)})) {
if (Date.now() >= deadline) {
throw new Error("timed out waiting for the QA MCP startup connect frame");
}
await new Promise((resolve) => setTimeout(resolve, 50));
}
await new Promise((resolve) => setTimeout(resolve, 1000));
},
stop() {},
});
},
};\n`,
"utf8",
);
return {
pluginDir,
startupGatePath,
cleanup: () => fs.rm(root, { force: true, recursive: true }),
};
}
function withFixturePlugin(config: OpenClawConfig, pluginDir: string): OpenClawConfig {
const existingPaths = config.plugins?.load?.paths ?? [];
const existingAllow = config.plugins?.allow ?? [];
return {
...config,
plugins: {
...config.plugins,
enabled: true,
allow: [...new Set([...existingAllow, FIXTURE_PLUGIN_ID])],
load: {
...config.plugins?.load,
paths: [...new Set([...existingPaths, pluginDir])],
},
entries: {
...config.plugins?.entries,
[FIXTURE_PLUGIN_ID]: { enabled: true },
},
},
};
}
function emptyTransport() {
return {
requiredPluginIds: [] as string[],
createGatewayConfig: () => ({}),
};
}
function resolveOpenClawCliInvocation(repoRoot: string): OpenClawCliInvocation {
for (const relativePath of ["dist/index.mjs", "dist/index.js"]) {
const entryPath = path.join(repoRoot, relativePath);
if (existsSync(entryPath)) {
const argsPrefix = [entryPath];
return {
argsPrefix,
command: process.execPath,
cwd: repoRoot,
gatewayCommand: {
executablePath: process.execPath,
argsPrefix,
cwd: repoRoot,
usePackagedPlugins: true,
},
};
}
}
const sourceEntryPath = path.join(repoRoot, "src/entry.ts");
if (existsSync(sourceEntryPath)) {
const argsPrefix = ["--import", "tsx", sourceEntryPath];
return {
argsPrefix,
command: process.execPath,
cwd: repoRoot,
gatewayCommand: {
executablePath: process.execPath,
argsPrefix,
cwd: repoRoot,
},
};
}
throw new Error("OpenClaw CLI entry not found: expected dist/index.(m)js or src/entry.ts");
}
function resolveChannelMcpInvocation(params: {
gatewayToken: string;
gatewayUrl: string;
repoRoot: string;
tokenFile: string;
}): ChannelMcpInvocation {
for (const relativePath of ["dist/index.mjs", "dist/index.js"]) {
const entryPath = path.join(params.repoRoot, relativePath);
if (existsSync(entryPath)) {
return {
args: [
entryPath,
"mcp",
"serve",
"--url",
params.gatewayUrl,
"--token-file",
params.tokenFile,
"--claude-channel-mode",
"off",
"--verbose",
],
command: process.execPath,
cwd: params.repoRoot,
envPatch: {},
};
}
}
const channelServerPath = path.join(params.repoRoot, "src/mcp/channel-server.ts");
if (existsSync(channelServerPath)) {
const channelServerUrl = pathToFileURL(channelServerPath).href;
return {
args: [
"--import",
"tsx",
"--eval",
[
`import(${JSON.stringify(channelServerUrl)})`,
`.then((module) => module.serveOpenClawChannelMcp({`,
`gatewayUrl: process.env.OPENCLAW_QA_GATEWAY_URL,`,
`gatewayToken: process.env.OPENCLAW_QA_GATEWAY_TOKEN,`,
`claudeChannelMode: "off",`,
`verbose: true`,
`}))`,
].join(""),
],
command: process.execPath,
cwd: params.repoRoot,
envPatch: {
OPENCLAW_QA_GATEWAY_TOKEN: params.gatewayToken,
OPENCLAW_QA_GATEWAY_URL: params.gatewayUrl,
},
};
}
throw new Error(
"OpenClaw channel MCP entry not found: expected dist/index.(m)js or src/mcp/channel-server.ts",
);
}
function parseJsonFrame(data: RawData): Record<string, unknown> | null {
try {
const text = Array.isArray(data)
? Buffer.concat(data).toString("utf8")
: Buffer.from(data).toString("utf8");
const value = JSON.parse(text);
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
} catch {
return null;
}
}
async function startGatewayProxy(
upstreamUrl: string,
onConnectFrame?: () => void,
): Promise<GatewayProxy> {
const capture: GatewayFrameCapture = {
connectFrames: [],
helloProtocols: [],
startupUnavailableResponses: 0,
};
const connectRequestIds = new Set<string>();
const server: Server = createServer();
const wss = new WebSocketServer({ server });
wss.on("connection", (downstream) => {
const upstream = new WebSocket(upstreamUrl);
const pending: RawData[] = [];
downstream.on("message", (data) => {
const frame = parseJsonFrame(data);
if (frame?.method === "connect" && typeof frame.id === "string") {
const params = frame.params as Record<string, unknown> | undefined;
if (typeof params?.minProtocol === "number" && typeof params.maxProtocol === "number") {
capture.connectFrames.push({
minProtocol: params.minProtocol,
maxProtocol: params.maxProtocol,
});
connectRequestIds.add(frame.id);
onConnectFrame?.();
}
}
if (upstream.readyState === WebSocket.OPEN) {
upstream.send(data);
} else {
pending.push(data);
}
});
upstream.on("open", () => {
for (const data of pending.splice(0)) {
upstream.send(data);
}
});
upstream.on("message", (data) => {
const frame = parseJsonFrame(data);
if (typeof frame?.id === "string" && connectRequestIds.has(frame.id)) {
const error = frame.error as Record<string, unknown> | undefined;
const details = error?.details as Record<string, unknown> | undefined;
if (error?.retryable === true && details?.reason === "startup-sidecars") {
capture.startupUnavailableResponses += 1;
}
const payload = frame.payload as Record<string, unknown> | undefined;
if (payload?.type === "hello-ok" && typeof payload.protocol === "number") {
capture.helloProtocols.push(payload.protocol);
}
}
if (downstream.readyState === WebSocket.OPEN) {
downstream.send(data);
}
});
const closeDownstream = () => {
if (downstream.readyState === WebSocket.OPEN) {
downstream.close(1013, "gateway unavailable");
}
};
upstream.on("error", closeDownstream);
upstream.on("close", (code, reason) => {
if (downstream.readyState === WebSocket.OPEN) {
downstream.close(code, reason.toString());
}
});
downstream.on("close", () => upstream.close());
});
await new Promise<void>((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", () => resolve());
});
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("gateway frame proxy did not bind a TCP port");
}
return {
capture,
url: `ws://127.0.0.1:${address.port}`,
async stop() {
for (const client of wss.clients) {
client.terminate();
}
await new Promise<void>((resolve) => {
wss.close(() => resolve());
});
await new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
},
};
}
async function connectChannelMcpClient(params: {
gatewayUrl: string;
gatewayToken: string;
repoRoot: string;
}): Promise<McpClientHandle> {
const tempState = createMcpClientTempState({ gatewayToken: params.gatewayToken });
const mcpInvocation = resolveChannelMcpInvocation({
gatewayToken: params.gatewayToken,
gatewayUrl: params.gatewayUrl,
repoRoot: params.repoRoot,
tokenFile: tempState.tokenFile,
});
const stderrChunks: Buffer[] = [];
const transport = new StdioClientTransport({
command: mcpInvocation.command,
args: mcpInvocation.args,
cwd: mcpInvocation.cwd,
env: {
...process.env,
...mcpInvocation.envPatch,
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS: "1",
OPENCLAW_LOG_LEVEL: "debug",
OPENCLAW_STATE_DIR: tempState.stateDir,
},
stderr: "pipe",
});
transport.stderr?.on("data", (chunk) => stderrChunks.push(Buffer.from(chunk)));
const client = new Client({ name: "qa-gateway-mcp-client", version: "1.0.0" });
let connectTimeout: NodeJS.Timeout | undefined;
try {
await Promise.race([
client.connect(transport),
new Promise<never>((_, reject) => {
connectTimeout = setTimeout(
() => reject(new Error("MCP channel client connect timed out")),
MCP_CONNECT_TIMEOUT_MS,
);
}),
]);
return {
client,
cleanup: tempState.cleanup,
transport,
stderr: () => Buffer.concat(stderrChunks).toString("utf8"),
};
} catch (error) {
await Promise.allSettled([client.close(), transport.close()]);
tempState.cleanup();
throw error;
} finally {
if (connectTimeout) {
clearTimeout(connectTimeout);
}
}
}
async function closeMcpClient(handle: McpClientHandle | undefined) {
if (!handle) {
return;
}
await Promise.allSettled([handle.client.close(), handle.transport.close()]);
handle.cleanup();
}
async function approvePendingMcpPairing(gateway: Awaited<ReturnType<typeof startQaGatewayChild>>) {
const pairing = (await gateway.call("device.pair.list", {})) as {
pending?: Array<{ requestId?: string; role?: string }>;
};
const pending = pairing.pending?.find((entry) => entry.role === "operator");
if (!pending?.requestId) {
return false;
}
try {
await gateway.call("device.pair.approve", { requestId: pending.requestId });
return true;
} catch (error) {
if (formatErrorMessage(error).includes("unknown requestId")) {
return false;
}
throw error;
}
}
async function runGatewaySmokeProof(options: ProducerOptions): Promise<string> {
const gateway = await startQaGatewayChild({
repoRoot: options.repoRoot,
command: resolveOpenClawCliInvocation(options.repoRoot).gatewayCommand,
transport: emptyTransport(),
transportBaseUrl: "http://127.0.0.1",
controlUiEnabled: false,
});
const tempRoot = gateway.tempRoot;
const keepTemp = process.env.OPENCLAW_QA_KEEP_TEMP === "1";
let details = "";
try {
const stdout: string[] = [];
const stderr: string[] = [];
const exitCode = await runGatewaySmoke(
{ token: gateway.token, urlRaw: gateway.wsUrl },
{
stdout: (message) => stdout.push(message),
stderr: (message) => stderr.push(message),
},
);
if (exitCode !== 0) {
throw new Error(`gateway smoke exited ${exitCode}: ${stderr.join("\n")}`);
}
const health = (await gateway.call("health", {})) as { ok?: boolean };
if (health.ok !== true) {
throw new Error(`gateway health RPC returned ${JSON.stringify(health)}`);
}
details = `real Gateway pid=${gateway.pid ?? "unknown"}; ${stdout.join("; ")}; health.ok=true`;
} finally {
await gateway.stop();
}
if (!keepTemp && existsSync(tempRoot)) {
throw new Error(`Gateway temp root was not cleaned up: ${tempRoot}`);
}
return details;
}
async function runMcpGatewayStartupRetryProof(options: ProducerOptions): Promise<string> {
const fixture = await createFixturePlugin();
let proxy: GatewayProxy | undefined;
let mcp: McpClientHandle | undefined;
let gateway: Awaited<ReturnType<typeof startQaGatewayChild>> | undefined;
let beforeSpawnAt = 0;
const keepTemp = process.env.OPENCLAW_QA_KEEP_TEMP === "1";
let details = "";
let proofError: Error | undefined;
try {
const onListening = async (context: QaGatewayChildListeningContext) => {
await closeMcpClient(mcp);
await proxy?.stop();
proxy = await startGatewayProxy(context.wsUrl, () => {
void fs.writeFile(fixture.startupGatePath, "observed\n", "utf8");
});
beforeSpawnAt = Date.now();
mcp = await connectChannelMcpClient({
gatewayUrl: proxy.url,
gatewayToken: context.token,
repoRoot: options.repoRoot,
});
};
gateway = await startQaGatewayChild({
repoRoot: options.repoRoot,
command: resolveOpenClawCliInvocation(options.repoRoot).gatewayCommand,
transport: emptyTransport(),
transportBaseUrl: "http://127.0.0.1",
controlUiEnabled: false,
onListening,
mutateConfig: (config) => withFixturePlugin(config, fixture.pluginDir),
});
if (!proxy || !mcp) {
throw new Error("MCP client was not started by the Gateway before-spawn hook");
}
const gatewayReadyAt = Date.now();
if (beforeSpawnAt >= gatewayReadyAt) {
throw new Error("MCP client did not start before Gateway readiness");
}
if (await approvePendingMcpPairing(gateway)) {
await closeMcpClient(mcp);
mcp = await connectChannelMcpClient({
gatewayUrl: proxy.url,
gatewayToken: gateway.token,
repoRoot: options.repoRoot,
});
}
const tools = await mcp.client.listTools();
if (!tools.tools.some((tool) => tool.name === "conversations_list")) {
throw new Error("real MCP channel server did not expose conversations_list");
}
const conversations = await mcp.client.callTool({
name: "conversations_list",
arguments: { limit: 1 },
});
if (conversations.isError) {
throw new Error(`conversations_list failed: ${JSON.stringify(conversations.content)}`);
}
const capture = proxy.capture;
if (capture.startupUnavailableResponses < 1) {
throw new Error(
`expected a retryable startup-unavailable response; captured=${JSON.stringify(capture)}`,
);
}
if (
!capture.connectFrames.some(
(frame) =>
frame.minProtocol === MIN_CLIENT_PROTOCOL_VERSION &&
frame.maxProtocol === PROTOCOL_VERSION,
)
) {
throw new Error(
`MCP Gateway connect frame used unexpected protocol range: ${JSON.stringify(capture)}`,
);
}
if (!capture.helloProtocols.includes(PROTOCOL_VERSION)) {
throw new Error(`MCP Gateway negotiation did not select protocol ${PROTOCOL_VERSION}`);
}
details = [
`MCP started ${gatewayReadyAt - beforeSpawnAt}ms before Gateway readiness`,
`startup retries=${capture.startupUnavailableResponses}`,
`connect frames=${capture.connectFrames.length}`,
`negotiated protocol=${PROTOCOL_VERSION}`,
].join("; ");
} catch (error) {
const diagnostics = [
mcp?.stderr(),
proxy ? `captured Gateway frames: ${JSON.stringify(proxy.capture)}` : undefined,
gateway?.logs(),
]
.filter((value): value is string => Boolean(value))
.join("\n");
proofError = new Error(`${formatErrorMessage(error)}${diagnostics ? `\n${diagnostics}` : ""}`, {
cause: error,
});
} finally {
await closeMcpClient(mcp);
await proxy?.stop().catch(() => undefined);
const tempRoot = gateway?.tempRoot;
await gateway?.stop().catch(() => undefined);
await fixture.cleanup();
if (!keepTemp && tempRoot && existsSync(tempRoot) && !proofError) {
proofError = new Error(`Gateway temp root was not cleaned up: ${tempRoot}`);
}
}
if (proofError) {
throw proofError;
}
return details;
}
async function writePluginToolsConfig(root: string, pluginDir: string) {
const configPath = path.join(root, "openclaw.json");
const config = withFixturePlugin({} as OpenClawConfig, pluginDir);
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
return configPath;
}
async function runMcpPluginToolsProof(options: ProducerOptions): Promise<string> {
const fixture = await createFixturePlugin();
// openclaw-temp-dir: allow standalone producer cleans and verifies this root in its finally block
const runtimeRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-plugin-tools-mcp-"));
const stateDir = path.join(runtimeRoot, "state");
const homeDir = path.join(runtimeRoot, "home");
await Promise.all([
fs.mkdir(stateDir, { recursive: true }),
fs.mkdir(homeDir, { recursive: true }),
]);
const configPath = await writePluginToolsConfig(runtimeRoot, fixture.pluginDir);
const stderrChunks: Buffer[] = [];
const transport = new StdioClientTransport({
command: process.execPath,
args: [
"--import",
"tsx",
"--eval",
`import(${JSON.stringify(pathToFileURL(path.join(options.repoRoot, "src/mcp/plugin-tools-serve.ts")).href)}).then((module) => module.servePluginToolsMcp())`,
],
cwd: options.repoRoot,
env: {
...process.env,
HOME: homeDir,
OPENCLAW_CONFIG_PATH: configPath,
OPENCLAW_STATE_DIR: stateDir,
},
stderr: "pipe",
});
transport.stderr?.on("data", (chunk) => stderrChunks.push(Buffer.from(chunk)));
const client = new Client({ name: "qa-plugin-tools-client", version: "1.0.0" });
let details = "";
let proofError: Error | undefined;
try {
await client.connect(transport);
const listed = await client.listTools();
if (!listed.tools.some((tool) => tool.name === FIXTURE_TOOL_NAME)) {
throw new Error(
`fixture plugin tool was not listed: ${listed.tools.map((tool) => tool.name).join(", ")}`,
);
}
const result = await client.callTool({
name: FIXTURE_TOOL_NAME,
arguments: { query: "ORBIT-9 codename", maxResults: 3 },
});
if (result.isError || !JSON.stringify(result.content).includes(FIXTURE_FACT)) {
throw new Error(`fixture plugin tool returned unexpected payload: ${JSON.stringify(result)}`);
}
details = `real plugin-tools pid=${transport.pid ?? "unknown"}; listed and called ${FIXTURE_TOOL_NAME}; received ORBIT-9`;
} catch (error) {
const stderr = Buffer.concat(stderrChunks).toString("utf8");
proofError = new Error(
`${formatErrorMessage(error)}${stderr ? `\nplugin-tools stderr:\n${stderr}` : ""}`,
{
cause: error,
},
);
} finally {
await Promise.allSettled([client.close(), transport.close()]);
await Promise.all([fixture.cleanup(), fs.rm(runtimeRoot, { force: true, recursive: true })]);
if (existsSync(runtimeRoot) && !proofError) {
proofError = new Error(`plugin-tools runtime root was not cleaned up: ${runtimeRoot}`);
}
}
if (proofError) {
throw proofError;
}
return details;
}
async function produceProof(options: ProducerOptions): Promise<ProofResult> {
const startedAt = Date.now();
try {
const details =
options.scenarioId === "gateway-smoke"
? await runGatewaySmokeProof(options)
: options.scenarioId === "mcp-gateway-connect-startup-retry"
? await runMcpGatewayStartupRetryProof(options)
: await runMcpPluginToolsProof(options);
return { details, durationMs: Math.max(1, Date.now() - startedAt), status: "pass" };
} catch (error) {
return {
details: formatErrorMessage(error),
durationMs: Math.max(1, Date.now() - startedAt),
status: "fail",
};
}
}
export async function runGatewayMcpRealTransportProducer(
options: ProducerOptions,
): Promise<QaEvidenceSummaryJson> {
const scenario = SCENARIOS[options.scenarioId];
const writer = createQaScriptEvidenceWriter({
artifactBase: options.artifactBase,
logFileName: `${options.scenarioId}.log`,
primaryModel: "mock-openai/gpt-5.5",
providerMode: "mock-openai",
repoRoot: options.repoRoot,
target: {
id: options.scenarioId,
title: scenario.title,
sourcePath: scenario.sourcePath,
primaryCoverageIds: scenario.primaryCoverageIds,
docsRefs: scenario.docsRefs,
codeRefs: scenario.codeRefs,
},
});
const result = await produceProof(options);
writer.appendLog(`${result.status}: ${result.details ?? "no details"}\n`);
return await writer.write(result);
}
async function main(argv: readonly string[]) {
const options = parseOptions(argv);
const evidence = await runGatewayMcpRealTransportProducer(options);
const status = evidence.entries[0]?.result.status;
console.log(`Gateway/MCP real transport evidence: ${QA_EVIDENCE_FILENAME}`);
console.log(`Gateway/MCP real transport status: ${status}`);
return status === "pass" ? 0 : 1;
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
main(process.argv.slice(2))
.then((exitCode) => {
process.exit(exitCode);
})
.catch((error: unknown) => {
console.error(formatErrorMessage(error));
process.exitCode = 1;
});
}
export const testing = {
resolveChannelMcpInvocation,
resolveOpenClawCliInvocation,
};