mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 09:02:16 +00:00
248 lines
7.8 KiB
TypeScript
248 lines
7.8 KiB
TypeScript
import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { setTimeout as delay } from "node:timers/promises";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
const ASSERTIONS_SCRIPT = "scripts/e2e/lib/release-user-journey/assertions.mjs";
|
|
|
|
function writeJson(filePath: string, value: unknown) {
|
|
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
}
|
|
|
|
function runAssertion(
|
|
home: string,
|
|
args: string[],
|
|
options: { env?: Record<string, string>; timeoutMs?: number } = {},
|
|
) {
|
|
return spawnSync(process.execPath, [ASSERTIONS_SCRIPT, ...args], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
HOME: home,
|
|
...options.env,
|
|
},
|
|
killSignal: "SIGKILL",
|
|
timeout: options.timeoutMs,
|
|
});
|
|
}
|
|
|
|
async function waitForFile(filePath: string, timeoutMs = 3000): Promise<string> {
|
|
const startedAt = Date.now();
|
|
while (Date.now() - startedAt < timeoutMs) {
|
|
if (existsSync(filePath)) {
|
|
return readFileSync(filePath, "utf8");
|
|
}
|
|
await delay(25);
|
|
}
|
|
throw new Error(`timed out waiting for ${filePath}`);
|
|
}
|
|
|
|
async function stopChild(child: ChildProcessWithoutNullStreams): Promise<void> {
|
|
if (child.exitCode !== null) {
|
|
return;
|
|
}
|
|
child.kill("SIGTERM");
|
|
const startedAt = Date.now();
|
|
while (child.exitCode === null && Date.now() - startedAt < 1000) {
|
|
await delay(25);
|
|
}
|
|
if (child.exitCode === null) {
|
|
child.kill("SIGKILL");
|
|
}
|
|
}
|
|
|
|
function startTcpFixture(portPath: string, connectionHandlerSource: string) {
|
|
return spawn(
|
|
process.execPath,
|
|
[
|
|
"--input-type=module",
|
|
"--eval",
|
|
[
|
|
'import net from "node:net";',
|
|
'import fs from "node:fs";',
|
|
`const server = net.createServer(${connectionHandlerSource});`,
|
|
'server.listen(0, "127.0.0.1", () => {',
|
|
" const address = server.address();",
|
|
" fs.writeFileSync(process.env.PORT_FILE, String(address.port));",
|
|
"});",
|
|
"setInterval(() => {}, 1000);",
|
|
].join("\n"),
|
|
],
|
|
{
|
|
env: { ...process.env, PORT_FILE: portPath },
|
|
stdio: "pipe",
|
|
},
|
|
);
|
|
}
|
|
|
|
describe("release user journey assertions", () => {
|
|
it("fails when uninstall leaves the managed plugin directory behind", () => {
|
|
const root = mkdtempSync(path.join(tmpdir(), "openclaw-release-user-assertions-"));
|
|
const home = path.join(root, "home");
|
|
const pluginId = "journey-plugin-a";
|
|
const installPath = path.join(home, ".openclaw", "extensions", pluginId);
|
|
const installPathFile = path.join(root, "install-path.txt");
|
|
|
|
try {
|
|
writeJson(path.join(home, ".openclaw", "openclaw.json"), {
|
|
plugins: {
|
|
entries: {},
|
|
allow: [],
|
|
deny: [],
|
|
},
|
|
});
|
|
writeJson(path.join(home, ".openclaw", "plugins", "installs.json"), {
|
|
installRecords: {},
|
|
});
|
|
mkdirSync(installPath, { recursive: true });
|
|
writeFileSync(installPathFile, installPath, "utf8");
|
|
|
|
const result = runAssertion(home, ["assert-plugin-uninstalled", pluginId, installPathFile]);
|
|
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain("managed plugin directory still present");
|
|
} finally {
|
|
rmSync(root, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("passes after uninstall clears config, records, and managed files", () => {
|
|
const root = mkdtempSync(path.join(tmpdir(), "openclaw-release-user-assertions-"));
|
|
const home = path.join(root, "home");
|
|
const installPathFile = path.join(root, "install-path.txt");
|
|
|
|
try {
|
|
writeJson(path.join(home, ".openclaw", "openclaw.json"), {
|
|
plugins: {
|
|
entries: {},
|
|
allow: [],
|
|
deny: [],
|
|
},
|
|
});
|
|
writeJson(path.join(home, ".openclaw", "plugins", "installs.json"), {
|
|
installRecords: {},
|
|
});
|
|
writeFileSync(
|
|
installPathFile,
|
|
path.join(home, ".openclaw", "extensions", "journey-plugin-a"),
|
|
"utf8",
|
|
);
|
|
|
|
const result = runAssertion(home, [
|
|
"assert-plugin-uninstalled",
|
|
"journey-plugin-a",
|
|
installPathFile,
|
|
]);
|
|
|
|
expect(result.status).toBe(0);
|
|
} finally {
|
|
rmSync(root, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("remembers the installed plugin path from the install record", () => {
|
|
const root = mkdtempSync(path.join(tmpdir(), "openclaw-release-user-assertions-"));
|
|
const home = path.join(root, "home");
|
|
const pluginId = "journey-plugin-a";
|
|
const sourcePath = path.join(root, "source", pluginId);
|
|
const installPath = path.join(home, ".openclaw", "extensions", pluginId);
|
|
const installPathFile = path.join(root, "install-path.txt");
|
|
const sourcePathFile = path.join(root, "source-path.txt");
|
|
|
|
try {
|
|
mkdirSync(sourcePath, { recursive: true });
|
|
mkdirSync(installPath, { recursive: true });
|
|
writeJson(path.join(home, ".openclaw", "plugins", "installs.json"), {
|
|
installRecords: {
|
|
[pluginId]: {
|
|
source: "path",
|
|
sourcePath,
|
|
installPath,
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = runAssertion(home, [
|
|
"remember-plugin-install-path",
|
|
pluginId,
|
|
installPathFile,
|
|
sourcePathFile,
|
|
sourcePath,
|
|
]);
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.stderr).toBe("");
|
|
} finally {
|
|
rmSync(root, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("accepts ready ClickClack fixture state", async () => {
|
|
const root = mkdtempSync(path.join(tmpdir(), "openclaw-release-user-assertions-"));
|
|
const home = path.join(root, "home");
|
|
const portPath = path.join(root, "port.txt");
|
|
const server = startTcpFixture(
|
|
portPath,
|
|
[
|
|
"(socket) => {",
|
|
" const body = JSON.stringify({ socketCount: 1 });",
|
|
" socket.end(`HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nContent-Length: ${Buffer.byteLength(body)}\\r\\n\\r\\n${body}`);",
|
|
"}",
|
|
].join("\n"),
|
|
);
|
|
|
|
try {
|
|
const port = Number.parseInt((await waitForFile(portPath)).trim(), 10);
|
|
const result = runAssertion(
|
|
home,
|
|
["wait-clickclack-socket", `http://127.0.0.1:${port}`, "1"],
|
|
{
|
|
env: { OPENCLAW_RELEASE_USER_JOURNEY_HTTP_TIMEOUT_MS: "1000" },
|
|
timeoutMs: 2500,
|
|
},
|
|
);
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.stderr).toBe("");
|
|
} finally {
|
|
await stopChild(server);
|
|
rmSync(root, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("bounds stalled ClickClack fixture HTTP probes", async () => {
|
|
const root = mkdtempSync(path.join(tmpdir(), "openclaw-release-user-assertions-"));
|
|
const home = path.join(root, "home");
|
|
const portPath = path.join(root, "port.txt");
|
|
const server = startTcpFixture(
|
|
portPath,
|
|
'(socket) => socket.write("HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\n\\r\\n")',
|
|
);
|
|
|
|
try {
|
|
const port = Number.parseInt((await waitForFile(portPath)).trim(), 10);
|
|
const startedAt = Date.now();
|
|
const result = runAssertion(
|
|
home,
|
|
["wait-clickclack-socket", `http://127.0.0.1:${port}`, "1"],
|
|
{
|
|
env: { OPENCLAW_RELEASE_USER_JOURNEY_HTTP_TIMEOUT_MS: "100" },
|
|
timeoutMs: 2500,
|
|
},
|
|
);
|
|
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.signal).not.toBe("SIGKILL");
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain("Timed out waiting for ClickClack websocket connection");
|
|
expect(Date.now() - startedAt).toBeLessThan(2500);
|
|
} finally {
|
|
await stopChild(server);
|
|
rmSync(root, { force: true, recursive: true });
|
|
}
|
|
});
|
|
});
|