mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-23 22:55:24 +00:00
test: speed up cli backend live lane
This commit is contained in:
@@ -1,243 +0,0 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { clearRuntimeConfigSnapshot, type OpenClawConfig } from "../src/config/config.js";
|
||||
import { GatewayClient } from "../src/gateway/client.js";
|
||||
import { startGatewayServer } from "../src/gateway/server.js";
|
||||
import { extractPayloadText } from "../src/gateway/test-helpers.agent-results.js";
|
||||
import { getFreePortBlockWithPermissionFallback } from "../src/test-utils/ports.js";
|
||||
import { GATEWAY_CLIENT_NAMES } from "../src/utils/message-channel.js";
|
||||
|
||||
const DEFAULT_CLAUDE_ARGS = [
|
||||
"-p",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--include-partial-messages",
|
||||
"--verbose",
|
||||
"--permission-mode",
|
||||
"bypassPermissions",
|
||||
];
|
||||
const DEFAULT_CLEAR_ENV = ["ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY_OLD"];
|
||||
const CLI_BOOTSTRAP_TIMEOUT_MS = 300_000;
|
||||
const GATEWAY_CONNECT_TIMEOUT_MS = 30_000;
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function withMcpConfigOverrides(args: string[], mcpConfigPath: string): string[] {
|
||||
const next = [...args];
|
||||
if (!next.includes("--strict-mcp-config")) {
|
||||
next.push("--strict-mcp-config");
|
||||
}
|
||||
if (!next.includes("--mcp-config")) {
|
||||
next.push("--mcp-config", mcpConfigPath);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
async function connectClient(params: { url: string; token: string }) {
|
||||
const startedAt = Date.now();
|
||||
let attempt = 0;
|
||||
let lastError: Error | null = null;
|
||||
|
||||
while (Date.now() - startedAt < GATEWAY_CONNECT_TIMEOUT_MS) {
|
||||
attempt += 1;
|
||||
const remainingMs = GATEWAY_CONNECT_TIMEOUT_MS - (Date.now() - startedAt);
|
||||
if (remainingMs <= 0) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
return await connectClientOnce({
|
||||
...params,
|
||||
timeoutMs: Math.min(remainingMs, 10_000),
|
||||
});
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
if (!isRetryableGatewayConnectError(lastError) || remainingMs <= 2_000) {
|
||||
throw lastError;
|
||||
}
|
||||
await sleep(Math.min(500 * attempt, 2_000));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError ?? new Error("gateway connect timeout");
|
||||
}
|
||||
|
||||
async function connectClientOnce(params: { url: string; token: string; timeoutMs: number }) {
|
||||
return await new Promise<GatewayClient>((resolve, reject) => {
|
||||
let done = false;
|
||||
let client: GatewayClient | undefined;
|
||||
const finish = (result: { client?: GatewayClient; error?: Error }) => {
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
done = true;
|
||||
clearTimeout(connectTimeout);
|
||||
if (result.error) {
|
||||
if (client) {
|
||||
void client.stopAndWait({ timeoutMs: 1_000 }).catch(() => {});
|
||||
}
|
||||
reject(result.error);
|
||||
return;
|
||||
}
|
||||
resolve(result.client as GatewayClient);
|
||||
};
|
||||
client = new GatewayClient({
|
||||
url: params.url,
|
||||
token: params.token,
|
||||
clientName: GATEWAY_CLIENT_NAMES.TEST,
|
||||
clientVersion: "dev",
|
||||
mode: "test",
|
||||
requestTimeoutMs: params.timeoutMs,
|
||||
connectChallengeTimeoutMs: params.timeoutMs,
|
||||
onHelloOk: () => finish({ client }),
|
||||
onConnectError: (error) => finish({ error }),
|
||||
onClose: (code, reason) =>
|
||||
finish({ error: new Error(`gateway closed during connect (${code}): ${reason}`) }),
|
||||
});
|
||||
const connectTimeout = setTimeout(
|
||||
() => finish({ error: new Error("gateway connect timeout") }),
|
||||
params.timeoutMs,
|
||||
);
|
||||
connectTimeout.unref();
|
||||
client.start();
|
||||
});
|
||||
}
|
||||
|
||||
function isRetryableGatewayConnectError(error: Error): boolean {
|
||||
const message = error.message.toLowerCase();
|
||||
return (
|
||||
message.includes("gateway closed during connect (1000)") ||
|
||||
message.includes("gateway connect timeout") ||
|
||||
message.includes("gateway connect challenge timeout")
|
||||
);
|
||||
}
|
||||
|
||||
async function getFreeGatewayPort(): Promise<number> {
|
||||
return await getFreePortBlockWithPermissionFallback({
|
||||
offsets: [0, 1, 2, 4],
|
||||
fallbackBase: 40_000,
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const preservedEnv = new Set(
|
||||
JSON.parse(process.env.OPENCLAW_LIVE_CLI_BACKEND_PRESERVE_ENV ?? "[]") as string[],
|
||||
);
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-inline-bootstrap-"));
|
||||
const workspaceRootDir = path.join(tempDir, "workspace");
|
||||
const workspaceDir = path.join(workspaceRootDir, "dev");
|
||||
const soulSecret = `SOUL-${randomUUID()}`;
|
||||
const identitySecret = `IDENTITY-${randomUUID()}`;
|
||||
const userSecret = `USER-${randomUUID()}`;
|
||||
await fs.mkdir(workspaceDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(workspaceDir, "AGENTS.md"),
|
||||
[
|
||||
"# AGENTS.md",
|
||||
"",
|
||||
"When the user sends a BOOTSTRAP_CHECK token, reply with exactly:",
|
||||
`BOOTSTRAP_OK ${soulSecret} ${identitySecret} ${userSecret}`,
|
||||
"Do not add any other words or punctuation.",
|
||||
].join("\n"),
|
||||
);
|
||||
await fs.writeFile(path.join(workspaceDir, "SOUL.md"), `${soulSecret}\n`);
|
||||
await fs.writeFile(path.join(workspaceDir, "IDENTITY.md"), `${identitySecret}\n`);
|
||||
await fs.writeFile(path.join(workspaceDir, "USER.md"), `${userSecret}\n`);
|
||||
|
||||
const cfg: OpenClawConfig = {};
|
||||
const existingBackends = cfg.agents?.defaults?.cliBackends ?? {};
|
||||
const claudeBackend = existingBackends["claude-cli"] ?? {};
|
||||
const cliCommand =
|
||||
process.env.OPENCLAW_LIVE_CLI_BACKEND_COMMAND ?? claudeBackend.command ?? "claude";
|
||||
let cliArgs = claudeBackend.args ?? DEFAULT_CLAUDE_ARGS;
|
||||
const mcpConfigPath = path.join(tempDir, "claude-mcp.json");
|
||||
await fs.writeFile(mcpConfigPath, `${JSON.stringify({ mcpServers: {} }, null, 2)}\n`);
|
||||
cliArgs = withMcpConfigOverrides(cliArgs, mcpConfigPath);
|
||||
const cliClearEnv = (claudeBackend.clearEnv ?? DEFAULT_CLEAR_ENV).filter(
|
||||
(name) => !preservedEnv.has(name),
|
||||
);
|
||||
const preservedCliEnv = Object.fromEntries(
|
||||
[...preservedEnv]
|
||||
.map((name) => [name, process.env[name]])
|
||||
.filter((entry): entry is [string, string] => typeof entry[1] === "string"),
|
||||
);
|
||||
const nextCfg = {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
workspace: workspaceRootDir,
|
||||
model: { primary: "claude-cli/claude-sonnet-4-6" },
|
||||
models: { "claude-cli/claude-sonnet-4-6": {} },
|
||||
cliBackends: {
|
||||
...existingBackends,
|
||||
"claude-cli": {
|
||||
...claudeBackend,
|
||||
command: cliCommand,
|
||||
args: cliArgs,
|
||||
clearEnv: cliClearEnv.length > 0 ? cliClearEnv : undefined,
|
||||
env: Object.keys(preservedCliEnv).length > 0 ? preservedCliEnv : undefined,
|
||||
systemPromptWhen: "first",
|
||||
},
|
||||
},
|
||||
sandbox: { mode: "off" },
|
||||
},
|
||||
},
|
||||
};
|
||||
const tempConfigPath = path.join(tempDir, "openclaw.json");
|
||||
await fs.writeFile(tempConfigPath, `${JSON.stringify(nextCfg, null, 2)}\n`);
|
||||
process.env.OPENCLAW_CONFIG_PATH = tempConfigPath;
|
||||
process.env.OPENCLAW_SKIP_CHANNELS = "1";
|
||||
process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1";
|
||||
process.env.OPENCLAW_SKIP_CRON = "1";
|
||||
process.env.OPENCLAW_SKIP_CANVAS_HOST = "1";
|
||||
|
||||
const port = await getFreeGatewayPort();
|
||||
const token = `test-${randomUUID()}`;
|
||||
process.env.OPENCLAW_GATEWAY_TOKEN = token;
|
||||
|
||||
const server = await startGatewayServer(port, {
|
||||
bind: "loopback",
|
||||
auth: { mode: "token", token },
|
||||
controlUiEnabled: false,
|
||||
});
|
||||
const client = await connectClient({ url: `ws://127.0.0.1:${port}`, token });
|
||||
try {
|
||||
const payload = await client.request(
|
||||
"agent",
|
||||
{
|
||||
sessionKey: `agent:dev:inline-cli-bootstrap-${randomUUID()}`,
|
||||
idempotencyKey: `idem-${randomUUID()}`,
|
||||
message: `BOOTSTRAP_CHECK ${randomUUID()}`,
|
||||
deliver: false,
|
||||
},
|
||||
{ expectFinal: true, timeoutMs: CLI_BOOTSTRAP_TIMEOUT_MS },
|
||||
);
|
||||
const text = extractPayloadText(payload?.result);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
ok: true,
|
||||
text,
|
||||
expectedText: `BOOTSTRAP_OK ${soulSecret} ${identitySecret} ${userSecret}`,
|
||||
systemPromptReport: payload?.result?.meta?.systemPromptReport ?? null,
|
||||
})}\n`,
|
||||
);
|
||||
} finally {
|
||||
await client.stopAndWait();
|
||||
await server.close({ reason: "bootstrap live probe done" });
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
clearRuntimeConfigSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await main();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
process.stderr.write(`${String(error)}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user