Files
openclaw/test/scripts/openclaw-e2e-instance.test.ts
2026-05-27 11:03:50 +01:00

627 lines
21 KiB
TypeScript

import { execFileSync, spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
const helperPath = path.resolve("scripts/lib/openclaw-e2e-instance.sh");
const hostPath = [
path.dirname(process.execPath),
"/usr/local/bin",
"/opt/homebrew/bin",
"/usr/bin",
"/bin",
].join(path.delimiter);
function shellQuote(value: string): string {
return `'${value.replace(/'/gu, `'\\''`)}'`;
}
function runHelper(payload: string) {
return spawnSync(
"bash",
[
"-lc",
[
"set -euo pipefail",
`source ${shellQuote(helperPath)}`,
`openclaw_e2e_eval_test_state_from_b64 ${shellQuote(payload)}`,
'printf "value=%s" "${OPENCLAW_E2E_INSTANCE_TEST:-unset}"',
].join("; "),
],
{ encoding: "utf8" },
);
}
function base64(script: string): string {
return execFileSync("base64", { input: script, encoding: "utf8" }).replace(/\s+/gu, "");
}
function shellTestEnv(overrides: Record<string, string | undefined>): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {
HOME: process.env.HOME ?? os.tmpdir(),
PATH: hostPath,
SHELL: "/bin/bash",
TMPDIR: process.env.TMPDIR ?? os.tmpdir(),
};
for (const [key, value] of Object.entries(overrides)) {
if (value !== undefined) {
env[key] = value;
}
}
return env;
}
function expectShellSuccess(result: ReturnType<typeof spawnSync>) {
expect(result.status, result.stderr || result.stdout || result.error?.message).toBe(0);
}
function writePackageFixture(packagePath: string): void {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-e2e-package-"));
try {
const packageDir = path.join(root, "package");
fs.mkdirSync(packageDir);
fs.writeFileSync(
path.join(packageDir, "package.json"),
JSON.stringify({ name: "openclaw-e2e-fixture", version: "0.0.0" }),
);
execFileSync("tar", ["-czf", packagePath, "-C", root, "package"]);
} finally {
fs.rmSync(root, { force: true, recursive: true });
}
}
function writeNodeShim(binDir: string): void {
const nodePath = path.join(binDir, "node");
try {
fs.symlinkSync(process.execPath, nodePath);
} catch {
fs.writeFileSync(nodePath, `#!/bin/sh\nexec ${shellQuote(process.execPath)} "$@"\n`);
fs.chmodSync(nodePath, 0o755);
}
}
function writeBashExecutable(filePath: string, lines: string[]): void {
fs.writeFileSync(filePath, ["#!/bin/bash", "set -euo pipefail", ...lines, ""].join("\n"));
fs.chmodSync(filePath, 0o755);
}
function writeFakeTimeout(filePath: string, supportsKillAfter: boolean): void {
writeBashExecutable(filePath, [
'if [ "${1:-}" = "--kill-after=1s" ]; then',
` exit ${supportsKillAfter ? 0 : 1}`,
"fi",
'printf "%s\\n" "$*" >"$OPENCLAW_TEST_TIMEOUT_ARGS"',
'while [ "$#" -gt 0 ]; do',
' case "$1" in',
" --)",
" shift",
" break",
" ;;",
" -k|--kill-after)",
" shift 2",
" ;;",
" --kill-after=*|-*)",
" shift",
" ;;",
" *)",
" shift",
" break",
" ;;",
" esac",
"done",
'exec "$@"',
]);
}
function writeFakeNpm(filePath: string): void {
writeBashExecutable(filePath, ['printf "%s\\n" "$*" >"$OPENCLAW_TEST_NPM_ARGS"']);
}
function expectNpmInstallObserved(argsPath: string, expectedArgs: string, prefix: string): void {
if (fs.existsSync(argsPath)) {
expect(fs.readFileSync(argsPath, "utf8").trim()).toBe(expectedArgs);
return;
}
expect(
fs.existsSync(path.join(prefix, "lib/node_modules/openclaw-e2e-fixture/package.json")),
).toBe(true);
}
describe("scripts/lib/openclaw-e2e-instance.sh", () => {
it("sources decoded test-state scripts", () => {
const result = runHelper(base64('export OPENCLAW_E2E_INSTANCE_TEST="ok"\n'));
expect(result.status).toBe(0);
expect(result.stdout).toBe("value=ok");
});
it("fails when the test-state payload is not valid base64", () => {
const result = runHelper("@@@");
expect(result.status).not.toBe(0);
expect(result.stdout).not.toContain("value=");
expect(result.stderr).toContain("Invalid OpenClaw test-state base64 payload");
});
it("fails when the test-state payload decodes to an empty script", () => {
const result = runHelper(base64("\n"));
expect(result.status).not.toBe(0);
expect(result.stdout).not.toContain("value=");
expect(result.stderr).toContain("decoded to an empty script");
});
it("wraps package installs with the configured timeout", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-e2e-instance-"));
try {
const timeoutArgsPath = path.join(tempDir, "timeout-args.txt");
const npmArgsPath = path.join(tempDir, "npm-args.txt");
const logPath = path.join(tempDir, "install.log");
const packagePath = path.join(tempDir, "openclaw.tgz");
const prefixPath = path.join(tempDir, "prefix");
writePackageFixture(packagePath);
writeFakeTimeout(path.join(tempDir, "timeout"), true);
writeFakeNpm(path.join(tempDir, "npm"));
const result = spawnSync(
"/bin/bash",
[
"-c",
[
"set -euo pipefail",
`source ${shellQuote(helperPath)}`,
`openclaw_e2e_install_package ${shellQuote(logPath)} ${shellQuote("fixture package")} ${shellQuote(prefixPath)}`,
].join("; "),
],
{
encoding: "utf8",
env: shellTestEnv({
PATH: `${tempDir}${path.delimiter}${hostPath}`,
OPENCLAW_CURRENT_PACKAGE_TGZ: packagePath,
OPENCLAW_E2E_NPM_INSTALL_TIMEOUT: "42s",
OPENCLAW_TEST_TIMEOUT_ARGS: timeoutArgsPath,
OPENCLAW_TEST_NPM_ARGS: npmArgsPath,
OPENCLAW_TEST_NPM_BIN: path.join(tempDir, "npm"),
}),
},
);
expectShellSuccess(result);
expect(result.stdout).toContain("Installing fixture package...");
expect(fs.readFileSync(timeoutArgsPath, "utf8").trim()).toBe(
`--kill-after=30s 42s npm install -g --prefix ${prefixPath} ${packagePath} --no-fund --no-audit`,
);
expectNpmInstallObserved(
npmArgsPath,
`install -g --prefix ${prefixPath} ${packagePath} --no-fund --no-audit`,
prefixPath,
);
} finally {
fs.rmSync(tempDir, { force: true, recursive: true });
}
});
it("falls back to plain timeout when kill-after is unavailable", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-e2e-instance-plain-timeout-"));
try {
const timeoutArgsPath = path.join(tempDir, "timeout-args.txt");
const npmArgsPath = path.join(tempDir, "npm-args.txt");
const logPath = path.join(tempDir, "install.log");
const packagePath = path.join(tempDir, "openclaw.tgz");
const prefixPath = path.join(tempDir, "prefix");
writePackageFixture(packagePath);
writeFakeTimeout(path.join(tempDir, "timeout"), false);
writeFakeNpm(path.join(tempDir, "npm"));
const result = spawnSync(
"/bin/bash",
[
"-c",
[
"set -euo pipefail",
`source ${shellQuote(helperPath)}`,
`openclaw_e2e_install_package ${shellQuote(logPath)} ${shellQuote("fixture package")} ${shellQuote(prefixPath)}`,
].join("; "),
],
{
encoding: "utf8",
env: shellTestEnv({
PATH: `${tempDir}${path.delimiter}${hostPath}`,
OPENCLAW_CURRENT_PACKAGE_TGZ: packagePath,
OPENCLAW_E2E_NPM_INSTALL_TIMEOUT: "42s",
OPENCLAW_TEST_TIMEOUT_ARGS: timeoutArgsPath,
OPENCLAW_TEST_NPM_ARGS: npmArgsPath,
OPENCLAW_TEST_NPM_BIN: path.join(tempDir, "npm"),
}),
},
);
expectShellSuccess(result);
expect(fs.readFileSync(timeoutArgsPath, "utf8").trim()).toBe(
`42s npm install -g --prefix ${prefixPath} ${packagePath} --no-fund --no-audit`,
);
expectNpmInstallObserved(
npmArgsPath,
`install -g --prefix ${prefixPath} ${packagePath} --no-fund --no-audit`,
prefixPath,
);
} finally {
fs.rmSync(tempDir, { force: true, recursive: true });
}
});
it("uses gtimeout when GNU timeout is not on PATH", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-e2e-instance-gtimeout-"));
try {
const timeoutArgsPath = path.join(tempDir, "timeout-args.txt");
const npmArgsPath = path.join(tempDir, "npm-args.txt");
const logPath = path.join(tempDir, "install.log");
const packagePath = path.join(tempDir, "openclaw.tgz");
const prefixPath = path.join(tempDir, "prefix");
writePackageFixture(packagePath);
writeFakeTimeout(path.join(tempDir, "gtimeout"), true);
writeFakeNpm(path.join(tempDir, "npm"));
const result = spawnSync(
"/bin/bash",
[
"-c",
[
"set -euo pipefail",
`source ${shellQuote(helperPath)}`,
`openclaw_e2e_install_package ${shellQuote(logPath)} ${shellQuote("fixture package")} ${shellQuote(prefixPath)}`,
].join("; "),
],
{
encoding: "utf8",
env: shellTestEnv({
PATH: tempDir,
OPENCLAW_CURRENT_PACKAGE_TGZ: packagePath,
OPENCLAW_E2E_NPM_INSTALL_TIMEOUT: "42s",
OPENCLAW_TEST_TIMEOUT_ARGS: timeoutArgsPath,
OPENCLAW_TEST_NPM_ARGS: npmArgsPath,
OPENCLAW_TEST_NPM_BIN: path.join(tempDir, "npm"),
}),
},
);
expectShellSuccess(result);
expect(fs.readFileSync(timeoutArgsPath, "utf8").trim()).toBe(
`--kill-after=30s 42s npm install -g --prefix ${prefixPath} ${packagePath} --no-fund --no-audit`,
);
expectNpmInstallObserved(
npmArgsPath,
`install -g --prefix ${prefixPath} ${packagePath} --no-fund --no-audit`,
prefixPath,
);
} finally {
fs.rmSync(tempDir, { force: true, recursive: true });
}
});
it("uses the Node watchdog when timeout is unavailable", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-e2e-instance-no-timeout-"));
try {
const npmArgsPath = path.join(tempDir, "npm-args.txt");
const logPath = path.join(tempDir, "install.log");
const packagePath = path.join(tempDir, "openclaw.tgz");
const prefixPath = path.join(tempDir, "prefix");
writePackageFixture(packagePath);
writeNodeShim(tempDir);
writeFakeNpm(path.join(tempDir, "npm"));
const result = spawnSync(
"/bin/bash",
[
"-c",
[
"set -euo pipefail",
`source ${shellQuote(helperPath)}`,
`openclaw_e2e_install_package ${shellQuote(logPath)} ${shellQuote("fixture package")} ${shellQuote(prefixPath)}`,
].join("; "),
],
{
encoding: "utf8",
env: shellTestEnv({
PATH: tempDir,
OPENCLAW_CURRENT_PACKAGE_TGZ: packagePath,
OPENCLAW_E2E_NPM_INSTALL_TIMEOUT: "42s",
OPENCLAW_TEST_NPM_ARGS: npmArgsPath,
}),
},
);
expectShellSuccess(result);
expect(fs.readFileSync(logPath, "utf8")).toContain("using Node watchdog");
expectNpmInstallObserved(
npmArgsPath,
`install -g --prefix ${prefixPath} ${packagePath} --no-fund --no-audit`,
prefixPath,
);
} finally {
fs.rmSync(tempDir, { force: true, recursive: true });
}
});
it("bounds commands with the Node watchdog when timeout is unavailable", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-e2e-instance-node-watchdog-"));
try {
writeNodeShim(tempDir);
const startedAt = Date.now();
const result = spawnSync(
"/bin/bash",
[
"-c",
[
"set -euo pipefail",
`source ${shellQuote(helperPath)}`,
`openclaw_e2e_maybe_timeout 200ms ${shellQuote(process.execPath)} -e ${shellQuote("setInterval(() => {}, 1000)")}`,
].join("; "),
],
{
encoding: "utf8",
env: shellTestEnv({
PATH: tempDir,
}),
timeout: 5_000,
},
);
const elapsedMs = Date.now() - startedAt;
expect(result.status).toBe(124);
expect(elapsedMs).toBeLessThan(4_000);
expect(result.stderr).toContain("using Node watchdog");
expect(result.stderr).toContain("OpenClaw E2E command timed out after 200ms");
} finally {
fs.rmSync(tempDir, { force: true, recursive: true });
}
});
it("bounds HTTP readiness probes when a server accepts connections but never responds", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-e2e-http-probe-"));
try {
const portPath = path.join(tempDir, "port.txt");
const serverPath = path.join(tempDir, "stalling-server.cjs");
fs.writeFileSync(
serverPath,
[
"const fs = require('node:fs');",
"const net = require('node:net');",
"const server = net.createServer((socket) => socket.on('data', () => {}));",
"server.listen(0, '127.0.0.1', () => {",
" fs.writeFileSync(process.argv[2], String(server.address().port));",
"});",
"process.on('SIGTERM', () => server.close(() => process.exit(0)));",
"",
].join("\n"),
);
const startedAt = Date.now();
const result = spawnSync(
"/bin/bash",
[
"-c",
[
"set -euo pipefail",
`${shellQuote(process.execPath)} ${shellQuote(serverPath)} ${shellQuote(portPath)} & server_pid=$!`,
'trap \'kill "$server_pid" 2>/dev/null || true; wait "$server_pid" 2>/dev/null || true\' EXIT',
`for _ in $(seq 1 50); do [ -s ${shellQuote(portPath)} ] && break; sleep 0.02; done`,
`port="$(cat ${shellQuote(portPath)})"`,
`source ${shellQuote(helperPath)}`,
'openclaw_e2e_probe_http_status "http://127.0.0.1:${port}/health" 200 100',
].join("; "),
],
{ encoding: "utf8", timeout: 3_000 },
);
const elapsedMs = Date.now() - startedAt;
expect(result.error).toBeUndefined();
expect(result.status).not.toBe(0);
expect(elapsedMs).toBeLessThan(2_500);
} finally {
fs.rmSync(tempDir, { force: true, recursive: true });
}
});
it("wraps logged OpenClaw E2E commands with the configured timeout", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-e2e-instance-run-logged-"));
const logLabel = path.basename(tempDir);
const logPath = `/tmp/openclaw-onboard-${logLabel}.log`;
try {
const timeoutArgsPath = path.join(tempDir, "timeout-args.txt");
const commandArgsPath = path.join(tempDir, "command-args.txt");
fs.writeFileSync(
path.join(tempDir, "timeout"),
[
"#!/usr/bin/env bash",
"set -euo pipefail",
'if [ "${1:-}" = "--kill-after=1s" ]; then exit 0; fi',
'printf "%s\\n" "$*" >"$OPENCLAW_TEST_TIMEOUT_ARGS"',
'while [ "$#" -gt 0 ] && [ "$1" != "fixture-command" ]; do shift; done',
'[ "$#" -gt 0 ] || exit 127',
"shift",
'exec "$OPENCLAW_TEST_COMMAND_BIN" "$@"',
"",
].join("\n"),
);
fs.writeFileSync(
path.join(tempDir, "fixture-command"),
[
"#!/usr/bin/env bash",
"set -euo pipefail",
'printf "%s\\n" "$*" >"$OPENCLAW_TEST_COMMAND_ARGS"',
'printf "fixture output\\n"',
"",
].join("\n"),
);
fs.chmodSync(path.join(tempDir, "timeout"), 0o755);
fs.chmodSync(path.join(tempDir, "fixture-command"), 0o755);
const result = spawnSync(
"/bin/bash",
[
"-c",
[
"set -euo pipefail",
`source ${shellQuote(helperPath)}`,
`openclaw_e2e_run_logged ${shellQuote(logLabel)} fixture-command one two`,
].join("; "),
],
{
encoding: "utf8",
env: shellTestEnv({
PATH: `${tempDir}:${hostPath}`,
OPENCLAW_E2E_COMMAND_TIMEOUT: "17s",
OPENCLAW_TEST_TIMEOUT_ARGS: timeoutArgsPath,
OPENCLAW_TEST_COMMAND_ARGS: commandArgsPath,
OPENCLAW_TEST_COMMAND_BIN: path.join(tempDir, "fixture-command"),
}),
},
);
expect(result.status).toBe(0);
expect(fs.readFileSync(timeoutArgsPath, "utf8").trim()).toBe(
"--kill-after=30s 17s fixture-command one two",
);
expect(fs.readFileSync(commandArgsPath, "utf8").trim()).toBe("one two");
expect(fs.readFileSync(logPath, "utf8")).toContain("fixture output");
} finally {
fs.rmSync(tempDir, { force: true, recursive: true });
fs.rmSync(logPath, { force: true });
}
});
it("wraps package-installed OpenClaw CLI calls with the configured timeout", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-e2e-instance-openclaw-cli-"));
try {
const timeoutArgsPath = path.join(tempDir, "timeout-args.txt");
const commandArgsPath = path.join(tempDir, "openclaw-args.txt");
fs.writeFileSync(
path.join(tempDir, "timeout"),
[
"#!/usr/bin/env bash",
"set -euo pipefail",
'if [ "${1:-}" = "--kill-after=1s" ]; then exit 0; fi',
'printf "%s\\n" "$*" >"$OPENCLAW_TEST_TIMEOUT_ARGS"',
`while [ "$#" -gt 0 ] && [ "$1" != ${shellQuote(path.join(tempDir, "openclaw"))} ]; do shift; done`,
'[ "$#" -gt 0 ] || exit 127',
"shift",
'exec "$OPENCLAW_TEST_OPENCLAW_BIN" "$@"',
"",
].join("\n"),
);
fs.writeFileSync(
path.join(tempDir, "openclaw"),
[
"#!/usr/bin/env bash",
"set -euo pipefail",
'printf "%s\\n" "$*" >"$OPENCLAW_TEST_COMMAND_ARGS"',
"",
].join("\n"),
);
fs.chmodSync(path.join(tempDir, "timeout"), 0o755);
fs.chmodSync(path.join(tempDir, "openclaw"), 0o755);
const result = spawnSync(
"/bin/bash",
[
"-c",
[
"set -euo pipefail",
`source ${shellQuote(helperPath)}`,
"openclaw_e2e_enable_openclaw_cli_timeout",
"openclaw_e2e_enable_openclaw_cli_timeout",
"openclaw plugins list --json",
].join("; "),
],
{
encoding: "utf8",
env: shellTestEnv({
PATH: `${tempDir}:${hostPath}`,
OPENCLAW_E2E_COMMAND_TIMEOUT: "23s",
OPENCLAW_TEST_TIMEOUT_ARGS: timeoutArgsPath,
OPENCLAW_TEST_COMMAND_ARGS: commandArgsPath,
OPENCLAW_TEST_OPENCLAW_BIN: path.join(tempDir, "openclaw"),
}),
},
);
expect(result.status).toBe(0);
expect(fs.readFileSync(timeoutArgsPath, "utf8").trim()).toBe(
`--kill-after=30s 23s ${path.join(tempDir, "openclaw")} plugins list --json`,
);
expect(fs.readFileSync(commandArgsPath, "utf8").trim()).toBe("plugins list --json");
} finally {
fs.rmSync(tempDir, { force: true, recursive: true });
}
});
it("wraps interactive PTY scripts with the configured timeout", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-e2e-instance-pty-timeout-"));
try {
const timeoutArgsPath = path.join(tempDir, "timeout-args.txt");
const scriptArgsPath = path.join(tempDir, "script-args.txt");
const logPath = path.join(tempDir, "pty.log");
fs.writeFileSync(
path.join(tempDir, "timeout"),
[
"#!/usr/bin/env bash",
"set -euo pipefail",
'if [ "${1:-}" = "--kill-after=1s" ]; then exit 0; fi',
'printf "%s\\n" "$*" >"$OPENCLAW_TEST_TIMEOUT_ARGS"',
'while [ "$#" -gt 0 ] && [ "$1" != "script" ]; do shift; done',
'[ "$#" -gt 0 ] || exit 127',
"shift",
'exec "$OPENCLAW_TEST_SCRIPT_BIN" "$@"',
"",
].join("\n"),
);
fs.writeFileSync(
path.join(tempDir, "script"),
[
"#!/usr/bin/env bash",
"set -euo pipefail",
'if [ "${1:-}" = "--version" ]; then exit 0; fi',
'printf "%s\\n" "$*" >"$OPENCLAW_TEST_SCRIPT_ARGS"',
"",
].join("\n"),
);
fs.chmodSync(path.join(tempDir, "timeout"), 0o755);
fs.chmodSync(path.join(tempDir, "script"), 0o755);
const result = spawnSync(
"/bin/bash",
[
"-c",
[
"set -euo pipefail",
`source ${shellQuote(helperPath)}`,
`openclaw_e2e_run_script_with_pty ${shellQuote("node /tmp/entry onboard")} ${shellQuote(logPath)}`,
].join("; "),
],
{
encoding: "utf8",
env: shellTestEnv({
PATH: `${tempDir}:${hostPath}`,
OPENCLAW_E2E_COMMAND_TIMEOUT: "31s",
OPENCLAW_TEST_TIMEOUT_ARGS: timeoutArgsPath,
OPENCLAW_TEST_SCRIPT_ARGS: scriptArgsPath,
OPENCLAW_TEST_SCRIPT_BIN: path.join(tempDir, "script"),
}),
},
);
expect(result.status).toBe(0);
expect(fs.readFileSync(timeoutArgsPath, "utf8").trim()).toBe(
`--kill-after=30s 31s script -q -f -c node /tmp/entry onboard ${logPath}`,
);
expect(fs.readFileSync(scriptArgsPath, "utf8").trim()).toBe(
`-q -f -c node /tmp/entry onboard ${logPath}`,
);
} finally {
fs.rmSync(tempDir, { force: true, recursive: true });
}
});
});