mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 00:11:19 +00:00
fix(qa): run gateway MCP producer from source checkout
This commit is contained in:
parent
e7808dbe60
commit
5f2df24619
2 changed files with 229 additions and 14 deletions
99
test/e2e/qa-lab/runtime/gateway-mcp-real-transports.test.ts
Normal file
99
test/e2e/qa-lab/runtime/gateway-mcp-real-transports.test.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { useAutoCleanupTempDirTracker } from "../../../helpers/temp-dir.js";
|
||||
import { testing } from "./gateway-mcp-real-transports.js";
|
||||
|
||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
|
||||
function createRepoRoot() {
|
||||
return tempDirs.make("openclaw-qalab-cli-entry-");
|
||||
}
|
||||
|
||||
async function writeEntry(root: string, relativePath: string) {
|
||||
const entryPath = path.join(root, relativePath);
|
||||
await mkdir(path.dirname(entryPath), { recursive: true });
|
||||
await writeFile(entryPath, "", "utf8");
|
||||
return entryPath;
|
||||
}
|
||||
|
||||
describe("gateway MCP real transport producer", () => {
|
||||
it("uses the source CLI entry when build output is absent", async () => {
|
||||
const root = createRepoRoot();
|
||||
const entryPath = await writeEntry(root, "src/entry.ts");
|
||||
|
||||
const cli = testing.resolveOpenClawCliInvocation(root);
|
||||
|
||||
expect(cli.command).toBe(process.execPath);
|
||||
expect(cli.argsPrefix).toStrictEqual(["--import", "tsx", entryPath]);
|
||||
expect(cli.cwd).toBe(root);
|
||||
expect(cli.gatewayCommand).toMatchObject({
|
||||
executablePath: process.execPath,
|
||||
argsPrefix: ["--import", "tsx", entryPath],
|
||||
cwd: root,
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers built package output when it exists", async () => {
|
||||
const root = createRepoRoot();
|
||||
const distEntry = await writeEntry(root, "dist/index.mjs");
|
||||
await writeEntry(root, "src/entry.ts");
|
||||
|
||||
const cli = testing.resolveOpenClawCliInvocation(root);
|
||||
|
||||
expect(cli.argsPrefix).toStrictEqual([distEntry]);
|
||||
expect(cli.gatewayCommand).toMatchObject({
|
||||
argsPrefix: [distEntry],
|
||||
usePackagedPlugins: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the source channel MCP module when build output is absent", async () => {
|
||||
const root = createRepoRoot();
|
||||
const channelServerPath = await writeEntry(root, "src/mcp/channel-server.ts");
|
||||
|
||||
const mcp = testing.resolveChannelMcpInvocation({
|
||||
gatewayToken: "secret-token",
|
||||
gatewayUrl: "ws://127.0.0.1:12345",
|
||||
repoRoot: root,
|
||||
tokenFile: "/tmp/token-file",
|
||||
});
|
||||
|
||||
expect(mcp.command).toBe(process.execPath);
|
||||
expect(mcp.args.slice(0, 3)).toStrictEqual(["--import", "tsx", "--eval"]);
|
||||
expect(mcp.args[3]).toContain(channelServerPath);
|
||||
expect(mcp.args[3]).toContain("serveOpenClawChannelMcp");
|
||||
expect(mcp.cwd).toBe(root);
|
||||
expect(mcp.envPatch).toStrictEqual({
|
||||
OPENCLAW_QA_GATEWAY_TOKEN: "secret-token",
|
||||
OPENCLAW_QA_GATEWAY_URL: "ws://127.0.0.1:12345",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the packaged CLI for channel MCP when build output exists", async () => {
|
||||
const root = createRepoRoot();
|
||||
const distEntry = await writeEntry(root, "dist/index.js");
|
||||
await writeEntry(root, "src/mcp/channel-server.ts");
|
||||
|
||||
const mcp = testing.resolveChannelMcpInvocation({
|
||||
gatewayToken: "secret-token",
|
||||
gatewayUrl: "ws://127.0.0.1:12345",
|
||||
repoRoot: root,
|
||||
tokenFile: "/tmp/token-file",
|
||||
});
|
||||
|
||||
expect(mcp.args).toStrictEqual([
|
||||
distEntry,
|
||||
"mcp",
|
||||
"serve",
|
||||
"--url",
|
||||
"ws://127.0.0.1:12345",
|
||||
"--token-file",
|
||||
"/tmp/token-file",
|
||||
"--claude-channel-mode",
|
||||
"off",
|
||||
"--verbose",
|
||||
]);
|
||||
expect(mcp.envPatch).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
|
|
@ -11,6 +11,7 @@ import { WebSocket, WebSocketServer, type RawData } from "ws";
|
|||
import {
|
||||
QA_EVIDENCE_FILENAME,
|
||||
startQaGatewayChild,
|
||||
type QaGatewayChildCommand,
|
||||
type QaEvidenceSummaryJson,
|
||||
type QaGatewayChildListeningContext,
|
||||
} from "../../../../extensions/qa-lab/api.js";
|
||||
|
|
@ -57,6 +58,20 @@ type GatewayProxy = {
|
|||
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;
|
||||
|
|
@ -212,6 +227,104 @@ function emptyTransport() {
|
|||
};
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
@ -332,24 +445,20 @@ async function connectChannelMcpClient(params: {
|
|||
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: process.execPath,
|
||||
args: [
|
||||
path.join(params.repoRoot, "dist/index.js"),
|
||||
"mcp",
|
||||
"serve",
|
||||
"--url",
|
||||
params.gatewayUrl,
|
||||
"--token-file",
|
||||
tempState.tokenFile,
|
||||
"--claude-channel-mode",
|
||||
"off",
|
||||
"--verbose",
|
||||
],
|
||||
cwd: params.repoRoot,
|
||||
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,
|
||||
|
|
@ -416,6 +525,7 @@ async function approvePendingMcpPairing(gateway: Awaited<ReturnType<typeof start
|
|||
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,
|
||||
|
|
@ -475,6 +585,7 @@ async function runMcpGatewayStartupRetryProof(options: ProducerOptions): Promise
|
|||
};
|
||||
gateway = await startQaGatewayChild({
|
||||
repoRoot: options.repoRoot,
|
||||
command: resolveOpenClawCliInvocation(options.repoRoot).gatewayCommand,
|
||||
transport: emptyTransport(),
|
||||
transportBaseUrl: "http://127.0.0.1",
|
||||
controlUiEnabled: false,
|
||||
|
|
@ -699,3 +810,8 @@ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
|
||||
export const testing = {
|
||||
resolveChannelMcpInvocation,
|
||||
resolveOpenClawCliInvocation,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue