diff --git a/scripts/e2e/lib/plugins/clawhub.sh b/scripts/e2e/lib/plugins/clawhub.sh index 6248ca2739a6..846c424bac21 100644 --- a/scripts/e2e/lib/plugins/clawhub.sh +++ b/scripts/e2e/lib/plugins/clawhub.sh @@ -16,11 +16,11 @@ run_plugins_clawhub_scenario() { node scripts/e2e/lib/clawhub-fixture-server.cjs plugins "$server_port_file" >"$server_log" 2>&1 & local server_pid="$!" echo "$server_pid" >"$server_pid_file" + openclaw_plugins_register_fixture_pid_file "$server_pid_file" for _ in $(seq 1 100); do if [[ -s "$server_port_file" ]]; then export OPENCLAW_CLAWHUB_URL="http://127.0.0.1:$(cat "$server_port_file")" - openclaw_plugins_register_fixture_pid_file "$server_pid_file" return 0 fi if ! kill -0 "$server_pid" 2>/dev/null; then diff --git a/scripts/e2e/lib/plugins/fixtures.sh b/scripts/e2e/lib/plugins/fixtures.sh index 3ac506fd82f9..e9a88ee450c5 100644 --- a/scripts/e2e/lib/plugins/fixtures.sh +++ b/scripts/e2e/lib/plugins/fixtures.sh @@ -1,4 +1,6 @@ OPENCLAW_PLUGINS_FIXTURE_PID_FILES=() +OPENCLAW_PLUGINS_FIXTURE_EXIT_TRAP_INSTALLED=0 +OPENCLAW_PLUGINS_FIXTURE_PREVIOUS_EXIT_ACTION="" openclaw_plugins_cleanup_fixture_servers() { local pid_file @@ -16,7 +18,33 @@ openclaw_plugins_cleanup_fixture_servers() { openclaw_plugins_register_fixture_pid_file() { local pid_file="$1" OPENCLAW_PLUGINS_FIXTURE_PID_FILES+=("$pid_file") - trap openclaw_plugins_cleanup_fixture_servers EXIT + openclaw_plugins_install_fixture_cleanup_trap +} + +openclaw_plugins_install_fixture_cleanup_trap() { + if [[ "${OPENCLAW_PLUGINS_FIXTURE_EXIT_TRAP_INSTALLED:-0}" = "1" ]]; then + return + fi + + local existing_trap + existing_trap="$(trap -p EXIT || true)" + if [[ -n "$existing_trap" && "$existing_trap" != *openclaw_plugins_fixture_exit_trap* ]]; then + local existing_action="${existing_trap#trap -- }" + existing_action="${existing_action% EXIT}" + eval "OPENCLAW_PLUGINS_FIXTURE_PREVIOUS_EXIT_ACTION=$existing_action" + fi + + OPENCLAW_PLUGINS_FIXTURE_EXIT_TRAP_INSTALLED=1 + trap openclaw_plugins_fixture_exit_trap EXIT +} + +openclaw_plugins_fixture_exit_trap() { + local status="$?" + openclaw_plugins_cleanup_fixture_servers + if [[ -n "${OPENCLAW_PLUGINS_FIXTURE_PREVIOUS_EXIT_ACTION:-}" ]]; then + eval "$OPENCLAW_PLUGINS_FIXTURE_PREVIOUS_EXIT_ACTION" + fi + exit "$status" } record_fixture_plugin_trust() { @@ -142,11 +170,11 @@ start_npm_fixture_registry() { node scripts/e2e/lib/plugins/npm-registry-server.mjs "$server_port_file" "$package_name" "$version" "$tarball" "$@" >"$server_log" 2>&1 & local server_pid="$!" echo "$server_pid" >"$server_pid_file" + openclaw_plugins_register_fixture_pid_file "$server_pid_file" for _ in $(seq 1 100); do if [[ -s "$server_port_file" ]]; then export NPM_CONFIG_REGISTRY="http://127.0.0.1:$(cat "$server_port_file")" - openclaw_plugins_register_fixture_pid_file "$server_pid_file" return 0 fi if ! kill -0 "$server_pid" 2>/dev/null; then diff --git a/test/scripts/plugins-assertions.test.ts b/test/scripts/plugins-assertions.test.ts index b3506b655461..f81535444266 100644 --- a/test/scripts/plugins-assertions.test.ts +++ b/test/scripts/plugins-assertions.test.ts @@ -1,5 +1,5 @@ import { spawn, spawnSync } from "node:child_process"; -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { createServer } from "node:http"; import { tmpdir } from "node:os"; import path from "node:path"; @@ -7,6 +7,10 @@ import { describe, expect, it } from "vitest"; const ASSERTIONS_SCRIPT = "scripts/e2e/lib/plugins/assertions.mjs"; +function shellQuote(value: string): string { + return `'${value.replace(/'/gu, `'\\''`)}'`; +} + function writeJson(filePath: string, value: unknown) { mkdirSync(path.dirname(filePath), { recursive: true }); writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); @@ -47,6 +51,43 @@ function runAssertionAsync(args: string[], env: NodeJS.ProcessEnv) { ); } +function writeFixtureServerShims(binDir: string, pidPath: string): void { + mkdirSync(binDir, { recursive: true }); + writeFileSync( + path.join(binDir, "node"), + [ + "#!/bin/bash", + 'printf "%s\\n" "$$" >"$OPENCLAW_TEST_FIXTURE_SERVER_PID"', + "trap 'exit 0' TERM", + "while true; do /bin/sleep 1; done", + "", + ].join("\n"), + ); + writeFileSync(path.join(binDir, "sleep"), "#!/bin/bash\nexit 0\n"); + chmodSync(path.join(binDir, "node"), 0o755); + chmodSync(path.join(binDir, "sleep"), 0o755); + writeFileSync(pidPath, ""); +} + +function isProcessAlive(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +} + +function waitForDead(pid: number, timeoutMs = 2_000): void { + const startedAt = Date.now(); + while (isProcessAlive(pid)) { + if (Date.now() - startedAt > timeoutMs) { + throw new Error(`pid ${pid} is still alive`); + } + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20); + } +} + describe("plugins Docker assertions", () => { it("rejects loose ClawHub preflight limits instead of parsing prefixes", () => { const timeoutResult = spawnSync(process.execPath, [ASSERTIONS_SCRIPT, "clawhub-preflight"], { @@ -92,6 +133,99 @@ describe("plugins Docker assertions", () => { } }); + it("cleans npm fixture registry children when readiness times out", () => { + const root = mkdtempSync(path.join(tmpdir(), "openclaw-plugin-npm-fixture-cleanup-")); + try { + const binDir = path.join(root, "bin"); + const fixtureDir = path.join(root, "fixture"); + const cleanupPath = path.join(root, "caller-cleanup"); + const pidPath = path.join(root, "server.pid"); + mkdirSync(fixtureDir); + writeFixtureServerShims(binDir, pidPath); + + const result = spawnSync( + "/bin/bash", + [ + "-c", + [ + "set -euo pipefail", + "source scripts/e2e/lib/plugins/fixtures.sh", + "set +e", + `( set -e; trap 'printf caller-cleanup > ${shellQuote(cleanupPath)}' EXIT; start_npm_fixture_registry fixture-pkg 1.0.0 ${shellQuote(path.join(root, "fixture.tgz"))} ${shellQuote(fixtureDir)} )`, + 'status="$?"', + "set -e", + '[ "$status" != "0" ]', + ].join("\n"), + ], + { + cwd: process.cwd(), + encoding: "utf8", + env: { + ...process.env, + OPENCLAW_TEST_FIXTURE_SERVER_PID: pidPath, + PATH: `${binDir}${path.delimiter}/usr/bin${path.delimiter}/bin`, + }, + }, + ); + + expect(result.status, result.stderr || result.stdout).toBe(0); + const pid = Number(readFileSync(pidPath, "utf8")); + expect(Number.isInteger(pid)).toBe(true); + waitForDead(pid); + expect(readFileSync(cleanupPath, "utf8")).toBe("caller-cleanup"); + } finally { + rmSync(root, { force: true, recursive: true }); + } + }); + + it("cleans ClawHub fixture children when readiness times out", () => { + const root = mkdtempSync(path.join(tmpdir(), "openclaw-plugin-clawhub-fixture-cleanup-")); + try { + const binDir = path.join(root, "bin"); + const cleanupPath = path.join(root, "caller-cleanup"); + const tmpDir = path.join(root, "scratch"); + const pidPath = path.join(root, "server.pid"); + mkdirSync(tmpDir); + writeFixtureServerShims(binDir, pidPath); + + const result = spawnSync( + "/bin/bash", + [ + "-c", + [ + "set -euo pipefail", + "source scripts/e2e/lib/plugins/fixtures.sh", + "source scripts/e2e/lib/plugins/clawhub.sh", + "set +e", + `( set -e; trap 'printf caller-cleanup > ${shellQuote(cleanupPath)}' EXIT; run_plugins_clawhub_scenario )`, + 'status="$?"', + "set -e", + '[ "$status" != "0" ]', + ].join("\n"), + ], + { + cwd: process.cwd(), + encoding: "utf8", + env: { + ...process.env, + OPENCLAW_PLUGINS_E2E_LIVE_CLAWHUB: "0", + OPENCLAW_PLUGINS_TMP_DIR: tmpDir, + OPENCLAW_TEST_FIXTURE_SERVER_PID: pidPath, + PATH: `${binDir}${path.delimiter}/usr/bin${path.delimiter}/bin`, + }, + }, + ); + + expect(result.status, result.stderr || result.stdout).toBe(0); + const pid = Number(readFileSync(pidPath, "utf8")); + expect(Number.isInteger(pid)).toBe(true); + waitForDead(pid); + expect(readFileSync(cleanupPath, "utf8")).toBe("caller-cleanup"); + } finally { + rmSync(root, { force: true, recursive: true }); + } + }); + it("uses the configured scratch root and resolves Windows home-relative install paths", () => { const root = mkdtempSync(path.join(tmpdir(), "openclaw-plugins-assertions-")); const home = path.join(root, "home");