






















@@ -1,20 +1,13 @@
1-import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
21import { randomUUID } from "node:crypto";
3-import fs from "node:fs/promises";
42import { request as httpRequest } from "node:http";
5-import net from "node:net";
6-import os from "node:os";
73import path from "node:path";
8-import {
9-BUILD_STAMP_FILE,
10-RUNTIME_POSTBUILD_STAMP_FILE,
11-} from "../../scripts/lib/local-build-metadata-paths.mjs";
124import { GatewayClient } from "../../src/gateway/client.js";
135import { connectGatewayClient } from "../../src/gateway/test-helpers.e2e.js";
146import { loadOrCreateDeviceIdentity } from "../../src/infra/device-identity.js";
157import { extractFirstTextBlock } from "../../src/shared/chat-message-content.js";
168import { sleep } from "../../src/utils.js";
179import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../src/utils/message-channel.js";
10+import { createOpenClawTestInstance, type OpenClawTestInstance } from "./openclaw-test-instance.js";
18111912export { extractFirstTextBlock };
2013@@ -25,287 +18,25 @@ export type ChatEventPayload = {
2518message?: unknown;
2619};
272028-export type GatewayInstance = {
29-name: string;
30-port: number;
31-hookToken: string;
32-gatewayToken: string;
33-homeDir: string;
34-stateDir: string;
35-configPath: string;
36-child: ChildProcessWithoutNullStreams;
37-stdout: string[];
38-stderr: string[];
39-};
21+export type GatewayInstance = OpenClawTestInstance;
402241-const GATEWAY_START_TIMEOUT_MS = 60_000;
42-const GATEWAY_STOP_TIMEOUT_MS = 1_500;
4323const GATEWAY_CONNECT_STATUS_TIMEOUT_MS = 2_000;
4424const GATEWAY_NODE_STATUS_TIMEOUT_MS = 4_000;
4525const GATEWAY_NODE_STATUS_POLL_MS = 20;
46-const GATEWAY_HOME_REMOVE_RETRIES = 5;
47-const GATEWAY_HOME_REMOVE_RETRY_DELAY_MS = 100;
48-const GATEWAY_ENTRYPOINT_PREPARE_TIMEOUT_MS = 120_000;
49-50-let gatewayEntrypointPromise: Promise<string[]> | null = null;
51-52-async function resolveBuiltGatewayEntrypoint(cwd: string): Promise<string[] | null> {
53-const buildStampPath = path.join(cwd, "dist", BUILD_STAMP_FILE);
54-const runtimePostBuildStampPath = path.join(cwd, "dist", RUNTIME_POSTBUILD_STAMP_FILE);
55-for (const entrypoint of ["dist/index.js", "dist/index.mjs"]) {
56-try {
57-await Promise.all([
58-fs.access(path.join(cwd, entrypoint)),
59-fs.access(buildStampPath),
60-fs.access(runtimePostBuildStampPath),
61-]);
62-return [entrypoint];
63-} catch {
64-// try the next built entrypoint
65-}
66-}
67-return null;
68-}
69-70-async function prepareGatewayEntrypoint(cwd: string): Promise<string[]> {
71-const builtEntrypoint = await resolveBuiltGatewayEntrypoint(cwd);
72-if (builtEntrypoint) {
73-return builtEntrypoint;
74-}
75-76-const stdout: string[] = [];
77-const stderr: string[] = [];
78-const child = spawn("node", ["scripts/run-node.mjs", "--help"], {
79- cwd,
80-env: { ...process.env, VITEST: "1" },
81-stdio: ["ignore", "pipe", "pipe"],
82-});
83-child.stdout?.setEncoding("utf8");
84-child.stderr?.setEncoding("utf8");
85-child.stdout?.on("data", (d) => stdout.push(String(d)));
86-child.stderr?.on("data", (d) => stderr.push(String(d)));
87-88-const completed = await Promise.race([
89-new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {
90-child.once("error", reject);
91-child.once("exit", (code, signal) => resolve({ code, signal }));
92-}),
93-sleep(GATEWAY_ENTRYPOINT_PREPARE_TIMEOUT_MS).then(() => null),
94-]);
95-96-if (completed === null) {
97-child.kill("SIGKILL");
98-throw new Error(
99-`timeout preparing gateway entrypoint\n--- stdout ---\n${stdout.join("")}\n--- stderr ---\n${stderr.join("")}`,
100-);
101-}
102-if (completed.code !== 0) {
103-throw new Error(
104-`failed preparing gateway entrypoint (code=${String(completed.code)} signal=${String(
105- completed.signal,
106- )})\n--- stdout ---\n${stdout.join("")}\n--- stderr ---\n${stderr.join("")}`,
107-);
108-}
109-110-return (await resolveBuiltGatewayEntrypoint(cwd)) ?? ["scripts/run-node.mjs"];
111-}
112-113-async function resolveGatewayEntrypoint(cwd: string): Promise<string[]> {
114-gatewayEntrypointPromise ??= prepareGatewayEntrypoint(cwd);
115-return await gatewayEntrypointPromise;
116-}
117-118-const getFreePort = async () => {
119-const srv = net.createServer();
120-await new Promise<void>((resolve) => srv.listen(0, "127.0.0.1", resolve));
121-const addr = srv.address();
122-if (!addr || typeof addr === "string") {
123-srv.close();
124-throw new Error("failed to bind ephemeral port");
125-}
126-await new Promise<void>((resolve) => srv.close(() => resolve()));
127-return addr.port;
128-};
129-130-async function waitForPortOpen(
131-proc: ChildProcessWithoutNullStreams,
132-chunksOut: string[],
133-chunksErr: string[],
134-port: number,
135-timeoutMs: number,
136-) {
137-const startedAt = Date.now();
138-while (Date.now() - startedAt < timeoutMs) {
139-if (proc.exitCode !== null) {
140-const stdout = chunksOut.join("");
141-const stderr = chunksErr.join("");
142-throw new Error(
143-`gateway exited before listening (code=${String(proc.exitCode)} signal=${String(proc.signalCode)})\n` +
144-`--- stdout ---\n${stdout}\n--- stderr ---\n${stderr}`,
145-);
146-}
147-148-try {
149-await new Promise<void>((resolve, reject) => {
150-const socket = net.connect({ host: "127.0.0.1", port });
151-socket.once("connect", () => {
152-socket.destroy();
153-resolve();
154-});
155-socket.once("error", (err) => {
156-socket.destroy();
157-reject(err);
158-});
159-});
160-return;
161-} catch {
162-// keep polling
163-}
164-165-await sleep(10);
166-}
167-const stdout = chunksOut.join("");
168-const stderr = chunksErr.join("");
169-throw new Error(
170-`timeout waiting for gateway to listen on port ${port}\n` +
171-`--- stdout ---\n${stdout}\n--- stderr ---\n${stderr}`,
172-);
173-}
174-175-async function waitForGatewayExit(
176-child: ChildProcessWithoutNullStreams,
177-timeoutMs: number,
178-): Promise<boolean> {
179-return await Promise.race([
180-new Promise<boolean>((resolve) => {
181-if (child.exitCode !== null || child.signalCode !== null) {
182-return resolve(true);
183-}
184-child.once("exit", () => resolve(true));
185-}),
186-sleep(timeoutMs).then(() => false),
187-]);
188-}
189-190-async function removeGatewayHome(homeDir: string) {
191-await fs.rm(homeDir, {
192-recursive: true,
193-force: true,
194-maxRetries: GATEWAY_HOME_REMOVE_RETRIES,
195-retryDelay: GATEWAY_HOME_REMOVE_RETRY_DELAY_MS,
196-});
197-}
1982619927export async function spawnGatewayInstance(name: string): Promise<GatewayInstance> {
200-const port = await getFreePort();
201-const hookToken = `token-${name}-${randomUUID()}`;
202-const gatewayToken = `gateway-${name}-${randomUUID()}`;
203-const homeDir = await fs.mkdtemp(path.join(os.tmpdir(), `openclaw-e2e-${name}-`));
204-const configDir = path.join(homeDir, ".openclaw");
205-await fs.mkdir(configDir, { recursive: true });
206-const configPath = path.join(configDir, "openclaw.json");
207-const stateDir = path.join(configDir, "state");
208-const config = {
209-gateway: {
210- port,
211-auth: { mode: "token", token: gatewayToken },
212-controlUi: { enabled: false },
213-},
214-hooks: { enabled: true, token: hookToken, path: "/hooks" },
215-};
216-await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf8");
217-218-const stdout: string[] = [];
219-const stderr: string[] = [];
220-let child: ChildProcessWithoutNullStreams | null = null;
221-28+const inst = await createOpenClawTestInstance({ name });
22229try {
223-const cwd = process.cwd();
224-const entrypoint = await resolveGatewayEntrypoint(cwd);
225-child = spawn(
226-"node",
227-[
228- ...entrypoint,
229-"gateway",
230-"--port",
231-String(port),
232-"--bind",
233-"loopback",
234-"--allow-unconfigured",
235-],
236-{
237- cwd,
238-env: {
239- ...process.env,
240-HOME: homeDir,
241-OPENCLAW_CONFIG_PATH: configPath,
242-OPENCLAW_STATE_DIR: stateDir,
243-OPENCLAW_GATEWAY_TOKEN: "",
244-OPENCLAW_GATEWAY_PASSWORD: "",
245-OPENCLAW_SKIP_CHANNELS: "1",
246-OPENCLAW_SKIP_PROVIDERS: "1",
247-OPENCLAW_SKIP_GMAIL_WATCHER: "1",
248-OPENCLAW_SKIP_CRON: "1",
249-OPENCLAW_SKIP_BROWSER_CONTROL_SERVER: "1",
250-OPENCLAW_SKIP_CANVAS_HOST: "1",
251-OPENCLAW_TEST_MINIMAL_GATEWAY: "1",
252-VITEST: "1",
253-},
254-stdio: ["ignore", "pipe", "pipe"],
255-},
256-);
257-258-child.stdout?.setEncoding("utf8");
259-child.stderr?.setEncoding("utf8");
260-child.stdout?.on("data", (d) => stdout.push(String(d)));
261-child.stderr?.on("data", (d) => stderr.push(String(d)));
262-263-await waitForPortOpen(child, stdout, stderr, port, GATEWAY_START_TIMEOUT_MS);
264-265-return {
266- name,
267- port,
268- hookToken,
269- gatewayToken,
270- homeDir,
271- stateDir,
272- configPath,
273- child,
274- stdout,
275- stderr,
276-};
30+await inst.startGateway();
31+return inst;
27732} catch (err) {
278-if (child && child.exitCode === null && !child.killed) {
279-try {
280-child.kill("SIGKILL");
281-} catch {
282-// ignore
283-}
284-await waitForGatewayExit(child, GATEWAY_STOP_TIMEOUT_MS);
285-}
286-await removeGatewayHome(homeDir);
33+await inst.cleanup();
28734throw err;
28835}
28936}
2903729138export async function stopGatewayInstance(inst: GatewayInstance) {
292-if (inst.child.exitCode === null && !inst.child.killed) {
293-try {
294-inst.child.kill("SIGTERM");
295-} catch {
296-// ignore
297-}
298-}
299-let exited = await waitForGatewayExit(inst.child, GATEWAY_STOP_TIMEOUT_MS);
300-if (!exited && inst.child.exitCode === null && !inst.child.killed) {
301-try {
302-inst.child.kill("SIGKILL");
303-} catch {
304-// ignore
305-}
306-await waitForGatewayExit(inst.child, GATEWAY_STOP_TIMEOUT_MS);
307-}
308-await removeGatewayHome(inst.homeDir);
39+await inst.cleanup();
30940}
3104131142export async function postJson(
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。