























@@ -1,7 +1,8 @@
1-import { spawnSync } from "node:child_process";
2-import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
1+import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process";
2+import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
33import { tmpdir } from "node:os";
44import path from "node:path";
5+import { setTimeout as delay } from "node:timers/promises";
56import { describe, expect, it } from "vitest";
6778const ASSERTIONS_SCRIPT = "scripts/e2e/lib/release-user-journey/assertions.mjs";
@@ -11,16 +12,72 @@ function writeJson(filePath: string, value: unknown) {
1112writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
1213}
131414-function runAssertion(home: string, args: string[]) {
15+function runAssertion(
16+home: string,
17+args: string[],
18+options: { env?: Record<string, string>; timeoutMs?: number } = {},
19+) {
1520return spawnSync(process.execPath, [ASSERTIONS_SCRIPT, ...args], {
1621encoding: "utf8",
1722env: {
1823 ...process.env,
1924HOME: home,
25+ ...options.env,
2026},
27+killSignal: "SIGKILL",
28+timeout: options.timeoutMs,
2129});
2230}
233132+async function waitForFile(filePath: string, timeoutMs = 3000): Promise<string> {
33+const startedAt = Date.now();
34+while (Date.now() - startedAt < timeoutMs) {
35+if (existsSync(filePath)) {
36+return readFileSync(filePath, "utf8");
37+}
38+await delay(25);
39+}
40+throw new Error(`timed out waiting for ${filePath}`);
41+}
42+43+async function stopChild(child: ChildProcessWithoutNullStreams): Promise<void> {
44+if (child.exitCode !== null) {
45+return;
46+}
47+child.kill("SIGTERM");
48+const startedAt = Date.now();
49+while (child.exitCode === null && Date.now() - startedAt < 1000) {
50+await delay(25);
51+}
52+if (child.exitCode === null) {
53+child.kill("SIGKILL");
54+}
55+}
56+57+function startTcpFixture(portPath: string, connectionHandlerSource: string) {
58+return spawn(
59+process.execPath,
60+[
61+"--input-type=module",
62+"--eval",
63+[
64+'import net from "node:net";',
65+'import fs from "node:fs";',
66+`const server = net.createServer(${connectionHandlerSource});`,
67+'server.listen(0, "127.0.0.1", () => {',
68+" const address = server.address();",
69+" fs.writeFileSync(process.env.PORT_FILE, String(address.port));",
70+"});",
71+"setInterval(() => {}, 1000);",
72+].join("\n"),
73+],
74+{
75+env: { ...process.env, PORT_FILE: portPath },
76+stdio: "pipe",
77+},
78+);
79+}
80+2481describe("release user journey assertions", () => {
2582it("fails when uninstall leaves the managed plugin directory behind", () => {
2683const root = mkdtempSync(path.join(tmpdir(), "openclaw-release-user-assertions-"));
@@ -43,11 +100,7 @@ describe("release user journey assertions", () => {
43100mkdirSync(installPath, { recursive: true });
44101writeFileSync(installPathFile, installPath, "utf8");
4510246-const result = runAssertion(home, [
47-"assert-plugin-uninstalled",
48-pluginId,
49-installPathFile,
50-]);
103+const result = runAssertion(home, ["assert-plugin-uninstalled", pluginId, installPathFile]);
5110452105expect(result.status).not.toBe(0);
53106expect(result.stderr).toContain("managed plugin directory still present");
@@ -126,4 +179,69 @@ describe("release user journey assertions", () => {
126179rmSync(root, { force: true, recursive: true });
127180}
128181});
182+183+it("accepts ready ClickClack fixture state", async () => {
184+const root = mkdtempSync(path.join(tmpdir(), "openclaw-release-user-assertions-"));
185+const home = path.join(root, "home");
186+const portPath = path.join(root, "port.txt");
187+const server = startTcpFixture(
188+portPath,
189+[
190+"(socket) => {",
191+" const body = JSON.stringify({ socketCount: 1 });",
192+" socket.end(`HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nContent-Length: ${Buffer.byteLength(body)}\\r\\n\\r\\n${body}`);",
193+"}",
194+].join("\n"),
195+);
196+197+try {
198+const port = Number.parseInt((await waitForFile(portPath)).trim(), 10);
199+const result = runAssertion(
200+home,
201+["wait-clickclack-socket", `http://127.0.0.1:${port}`, "1"],
202+{
203+env: { OPENCLAW_RELEASE_USER_JOURNEY_HTTP_TIMEOUT_MS: "1000" },
204+timeoutMs: 2500,
205+},
206+);
207+208+expect(result.status).toBe(0);
209+expect(result.stderr).toBe("");
210+} finally {
211+await stopChild(server);
212+rmSync(root, { force: true, recursive: true });
213+}
214+});
215+216+it("bounds stalled ClickClack fixture HTTP probes", async () => {
217+const root = mkdtempSync(path.join(tmpdir(), "openclaw-release-user-assertions-"));
218+const home = path.join(root, "home");
219+const portPath = path.join(root, "port.txt");
220+const server = startTcpFixture(
221+portPath,
222+'(socket) => socket.write("HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\n\\r\\n")',
223+);
224+225+try {
226+const port = Number.parseInt((await waitForFile(portPath)).trim(), 10);
227+const startedAt = Date.now();
228+const result = runAssertion(
229+home,
230+["wait-clickclack-socket", `http://127.0.0.1:${port}`, "1"],
231+{
232+env: { OPENCLAW_RELEASE_USER_JOURNEY_HTTP_TIMEOUT_MS: "100" },
233+timeoutMs: 2500,
234+},
235+);
236+237+expect(result.error).toBeUndefined();
238+expect(result.signal).not.toBe("SIGKILL");
239+expect(result.status).not.toBe(0);
240+expect(result.stderr).toContain("Timed out waiting for ClickClack websocket connection");
241+expect(Date.now() - startedAt).toBeLessThan(2500);
242+} finally {
243+await stopChild(server);
244+rmSync(root, { force: true, recursive: true });
245+}
246+});
129247});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。