Files
openclaw/test/scripts/plugins-assertions.test.ts
Peter Steinberger 2bdf8709fc test(core): remove low-value tests (batch d009) (#158725)
* test(codex): deslop s003 tests

* test(gateway): deslop s010 tests

* test(agents): deslop s013 tests

* test(commands): deslop s011 tests

* test(gateway): deslop s014 tests

* test(gateway): deslop s019 tests

* test(infra): deslop s016 tests

* test(agents): deslop s020 tests

* test(scripts): deslop s017 tests

* test(auto-reply): deslop s021 tests

* test: align d009 replay with current fixture contracts

* test(agents): remove orphaned BTW fixture export

* test: preserve independent regression coverage in d009

Restore eight independent authentication, retry, transcript, goal, reply ownership, and Xcode coverage contracts identified during review. Final correction review passed; final Testbox validation remains pending after lease and transport failures.
2026-09-26 11:08:36 +00:00

2165 lines
79 KiB
TypeScript

// Plugins Assertions tests cover plugins assertions script behavior.
import { spawn, spawnSync } from "node:child_process";
import {
chmodSync,
copyFileSync,
existsSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
realpathSync,
rmSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { createServer, request as httpRequest } from "node:http";
import { tmpdir } from "node:os";
import path from "node:path";
import { createInterface } from "node:readline";
import { pathToFileURL } from "node:url";
import { gzipSync } from "node:zlib";
import { afterEach, describe, expect, it } from "vitest";
import { resolveTestNodeExecPath } from "../../src/test-utils/node-process.js";
import { createBoundedChildOutput } from "../helpers/bounded-child-output.js";
import { createFixtureLifetime } from "../helpers/fixture-lifetime.js";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
const ASSERTIONS_SCRIPT = "scripts/e2e/lib/plugins/assertions.mjs";
const autoCleanupTempDirs = useAutoCleanupTempDirTracker(afterEach);
const publicationFixtures = createFixtureLifetime();
afterEach(() => publicationFixtures.cleanup());
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");
}
function runAssertionAsync(args: string[], env: NodeJS.ProcessEnv) {
return new Promise<{ status: number | null; stdout: string; stderr: string }>(
(resolve, reject) => {
const child = spawn(process.execPath, [ASSERTIONS_SCRIPT, ...args], {
env: { ...process.env, ...env },
stdio: ["ignore", "pipe", "pipe"],
});
const stdout = createBoundedChildOutput();
const stderr = createBoundedChildOutput();
const timeout = setTimeout(() => {
child.kill("SIGKILL");
reject(new Error(`assertion helper did not exit: ${args.join(" ")}`));
}, 2_000);
timeout.unref();
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk) => {
stdout.append(chunk);
});
child.stderr.on("data", (chunk) => {
stderr.append(chunk);
});
child.on("error", (error) => {
clearTimeout(timeout);
reject(error);
});
child.on("close", (status) => {
clearTimeout(timeout);
resolve({ status, stdout: stdout.text(), stderr: stderr.text() });
});
},
);
}
function writeFixtureServerShims(
binDir: string,
pidPath: string,
termAction: "exit 0" | ":" = "exit 0",
): void {
mkdirSync(binDir, { recursive: true });
writeFileSync(
path.join(binDir, "node"),
[
"#!/bin/bash",
'printf "%s\\n" "$$" >"$OPENCLAW_TEST_FIXTURE_SERVER_PID"',
`trap '${termAction}' 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 writeCrashingFixtureServerShim(binDir: string): void {
mkdirSync(binDir, { recursive: true });
writeFileSync(
path.join(binDir, "node"),
[
"#!/bin/bash",
'printf "DO_NOT_DUMP_PLUGIN_FIXTURE_PREFIX\\n"',
'printf "%2048s" "" | tr " " x',
'printf "\\nPLUGIN_FIXTURE_TAIL_MARKER\\n"',
"exit 1",
"",
].join("\n"),
);
writeFileSync(path.join(binDir, "sleep"), "#!/bin/bash\nexit 0\n");
chmodSync(path.join(binDir, "node"), 0o755);
chmodSync(path.join(binDir, "sleep"), 0o755);
}
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, 5);
}
}
function runPluginsSweepShell(script: string, env: NodeJS.ProcessEnv = {}) {
return spawnSync("/bin/bash", ["-c", script], {
cwd: process.cwd(),
encoding: "utf8",
env: { ...process.env, ...env },
});
}
async function waitForPortFile(portFile: string): Promise<number> {
for (let attempt = 0; attempt < 200; attempt += 1) {
if (existsSync(portFile)) {
const port = Number(readFileSync(portFile, "utf8"));
if (Number.isInteger(port) && port > 0) {
return port;
}
}
await new Promise<void>((resolve) => {
setTimeout(resolve, 5);
});
}
throw new Error(`timed out waiting for ${portFile}`);
}
function requestFixtureRegistry(
port: number,
requestPath: string,
headers: Record<string, string> = {},
): Promise<{ body: string; contentLength: string | undefined; statusCode: number | undefined }> {
return new Promise((resolve, reject) => {
const request = httpRequest(
{ headers, host: "127.0.0.1", method: "GET", path: requestPath, port },
(response) => {
let body = "";
response.setEncoding("utf8");
response.on("data", (chunk: string) => {
body += chunk;
});
response.on("end", () => {
resolve({
body,
contentLength: response.headers["content-length"],
statusCode: response.statusCode,
});
});
},
);
request.setTimeout(2_000, () => {
request.destroy(new Error(`timed out requesting ${requestPath}`));
});
request.on("error", reject);
request.end();
});
}
function startFixtureRegistry(
portFile: string,
tarballPath: string,
env: NodeJS.ProcessEnv = {},
preload?: string,
) {
return spawn(
process.execPath,
[
...(preload ? ["--import", pathToFileURL(preload).href] : []),
"scripts/e2e/lib/plugins/npm-registry-server.mjs",
portFile,
"@openclaw/demo-plugin-npm",
"1.0.0",
tarballPath,
],
{ cwd: process.cwd(), env: { ...process.env, ...env }, stdio: ["ignore", "pipe", "pipe"] },
);
}
async function stopFixtureRegistry(child: ReturnType<typeof startFixtureRegistry>) {
if (child.exitCode === null) {
child.kill();
await new Promise((resolve) => {
child.once("close", resolve);
});
}
}
describe("plugins Docker assertions", () => {
it("rejects loose ClawHub preflight limits instead of parsing prefixes", () => {
const timeoutResult = spawnSync(process.execPath, [ASSERTIONS_SCRIPT, "clawhub-preflight"], {
encoding: "utf8",
env: {
...process.env,
CLAWHUB_PLUGIN_SPEC: "clawhub:@openclaw/kitchen-sink",
OPENCLAW_PLUGINS_E2E_CLAWHUB_PREFLIGHT_TIMEOUT_MS: "1e3",
},
});
expect(timeoutResult.status).not.toBe(0);
expect(timeoutResult.stderr).toContain(
"invalid OPENCLAW_PLUGINS_E2E_CLAWHUB_PREFLIGHT_TIMEOUT_MS: 1e3",
);
const bodyLimitResult = spawnSync(process.execPath, [ASSERTIONS_SCRIPT, "clawhub-preflight"], {
encoding: "utf8",
env: {
...process.env,
CLAWHUB_PLUGIN_SPEC: "clawhub:@openclaw/kitchen-sink",
OPENCLAW_PLUGINS_E2E_CLAWHUB_PREFLIGHT_BODY_MAX_BYTES: "1000bytes",
},
});
expect(bodyLimitResult.status).not.toBe(0);
expect(bodyLimitResult.stderr).toContain(
"invalid OPENCLAW_PLUGINS_E2E_CLAWHUB_PREFLIGHT_BODY_MAX_BYTES: 1000bytes",
);
});
it("keeps sweep artifact paths aligned with the assertion scratch root", () => {
const scripts = [
"scripts/e2e/lib/plugins/sweep.sh",
"scripts/e2e/lib/plugins/marketplace.sh",
"scripts/e2e/lib/plugins/clawhub.sh",
];
for (const scriptPath of scripts) {
const script = readFileSync(scriptPath, "utf8");
const scriptWithoutDefaultScratch = script.replace(
'mktemp -d "/tmp/openclaw-plugins.XXXXXX"',
"",
);
expect(script).toContain("OPENCLAW_PLUGINS_TMP_DIR");
expect(scriptWithoutDefaultScratch).not.toMatch(
/\/tmp\/(?:plugins|marketplace|demo-plugin|is-number|openclaw-plugin|openclaw-clawhub)/,
);
}
});
it("cleans the default plugin sweep scratch root", () => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-plugin-sweep-cleanup-"));
const marker = path.join(root, "scratch-path.txt");
try {
const result = runPluginsSweepShell(
`
set -euo pipefail
export OPENCLAW_PLUGINS_SWEEP_SOURCE_ONLY=1
source scripts/e2e/lib/plugins/sweep.sh
printf '%s\\n' "$OPENCLAW_PLUGINS_TMP_DIR" > "$MARKER"
test -d "$OPENCLAW_PLUGINS_TMP_DIR"
cleanup_openclaw_plugins_sweep
test ! -e "$OPENCLAW_PLUGINS_TMP_DIR"
`,
{ MARKER: marker },
);
expect(result.stdout).toBe("");
expect(result.stderr).toBe("");
expect(result.status).toBe(0);
const scratchRoot = readFileSync(marker, "utf8").trim();
expect(scratchRoot).toContain("/tmp/openclaw-plugins.");
expect(existsSync(scratchRoot)).toBe(false);
} finally {
rmSync(root, { force: true, recursive: true });
}
});
it("preserves caller-provided plugin sweep scratch roots", () => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-plugin-sweep-caller-"));
const scratchRoot = path.join(root, "scratch");
try {
const result = runPluginsSweepShell(
`
set -euo pipefail
export OPENCLAW_PLUGINS_SWEEP_SOURCE_ONLY=1
export OPENCLAW_PLUGINS_TMP_DIR="$SCRATCH_ROOT"
source scripts/e2e/lib/plugins/sweep.sh
test -d "$OPENCLAW_PLUGINS_TMP_DIR"
cleanup_openclaw_plugins_sweep
test -d "$OPENCLAW_PLUGINS_TMP_DIR"
`,
{ SCRATCH_ROOT: scratchRoot },
);
expect(result.stdout).toBe("");
expect(result.stderr).toBe("");
expect(result.status).toBe(0);
expect(existsSync(scratchRoot)).toBe(true);
} finally {
rmSync(root, { force: true, recursive: true });
}
});
it.each([
...(["capture", "logged"] as const).flatMap((mode) =>
[0, 23, 124].flatMap((status) =>
[false, true].map((traceEnabled) => ({
mode,
status,
traceEnabled,
traceValue: traceEnabled ? "1" : "0",
changeAfterSource: false,
})),
),
),
...(
[
["1", true],
["true", true],
["TRUE", true],
["yes", true],
["YES", true],
[undefined, false],
["", false],
["0", false],
["True", false],
["Yes", false],
["on", false],
[" true ", false],
["1 ", false],
] as const
).map(([traceValue, traceEnabled]) => ({
mode: "logged" as const,
status: 0,
traceEnabled,
traceValue,
changeAfterSource: true,
})),
])(
"bounds $mode diagnostics with exit $status and lifecycle tracing $traceEnabled ($traceValue, changed after source: $changeAfterSource)",
(testCase) => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-plugin-sweep-diagnostics-"));
const outputFile = path.join(root, "plugins-git-inspect.json");
const capturedOutput = `DO_NOT_DUMP_CAPTURED_PLUGIN_OUTPUT\n${"x".repeat(32 * 1024)}\nCAPTURED_PLUGIN_OUTPUT_TAIL`;
const fixtureApiKey = ["sk", "proj", "plugin", "fixture", "secret"].join("-");
const punctuationApiKey = ["sess", "plugin", "punctuation", "secret"].join("-");
const startOfLineApiKey = ["rk", "plugin", "start", "secret"].join("-");
const boundarySecretSuffix = "PLUGIN_BOUNDARY_SECRET_SUFFIX_MUST_NOT_LEAK";
const boundaryApiKey = ["sk", "proj", "z".repeat(256), boundarySecretSuffix].join("-");
const capturedError = `DO_NOT_DUMP_PLUGIN_STDERR_PREFIX\n${"y".repeat(32 * 1024)}\n${startOfLineApiKey}\nPLUGIN_STDERR_TAIL_MARKER task-runner risk-score disk-space Authorization: Bearer plugin-fixture-bearer OPENAI_API_KEY=${fixtureApiKey} [${punctuationApiKey}]\n${boundaryApiKey}`;
const command =
testCase.mode === "capture"
? 'run_plugins_openclaw_capture "$OUTPUT_FILE" plugins inspect demo-plugin --runtime --json'
: 'run_plugins_openclaw_logged install-git plugins install "git:file:///tmp/fixture@revision" --force';
try {
const unredactedTail = Buffer.from(`${capturedError}\n`, "utf8")
.subarray(-192)
.toString("utf8");
expect(unredactedTail).toContain(boundarySecretSuffix);
expect(unredactedTail).not.toContain(["sk", "proj"].join("-"));
const result = runPluginsSweepShell(
`
set -euo pipefail
export OPENCLAW_PLUGINS_SWEEP_SOURCE_ONLY=1
export OPENCLAW_PLUGINS_TMP_DIR="$SCRATCH_ROOT"
export OPENCLAW_PLUGINS_CLI_TIMEOUT=1s
export OPENCLAW_ENTRY=fixture-entry
source scripts/e2e/lib/plugins/sweep.sh
${
testCase.changeAfterSource
? testCase.traceValue === undefined
? "unset OPENCLAW_PLUGIN_LIFECYCLE_TRACE"
: `export OPENCLAW_PLUGIN_LIFECYCLE_TRACE=${shellQuote(testCase.traceValue)}`
: ""
}
umask 000
openclaw_e2e_maybe_timeout() {
local raw_stderr_file
local raw_stderr_mode
for raw_stderr_file in "$SCRATCH_ROOT"/openclaw-plugin-stderr.*; do
[[ -f "$raw_stderr_file" ]] || return 86
if raw_stderr_mode="$(stat -f '%Lp' "$raw_stderr_file" 2>/dev/null)"; then
:
elif raw_stderr_mode="$(stat -c '%a' "$raw_stderr_file" 2>/dev/null)"; then
:
else
return 87
fi
[[ "$raw_stderr_mode" == "600" ]] || return 88
done
printf '%s\\n' "$CAPTURED_OUTPUT"
printf '%s\\n' "$CAPTURED_STDERR" >&2
if [[ "\${OPENCLAW_PLUGIN_LIFECYCLE_TRACE:-}" == "1" ]]; then
printf '%s\\n' '[plugins:lifecycle] shim' >&2
fi
return "$CAPTURE_STATUS"
}
${command}
`,
{
CAPTURED_OUTPUT: capturedOutput,
CAPTURED_STDERR: capturedError,
CAPTURE_STATUS: String(testCase.status),
OPENCLAW_DOCKER_E2E_LOG_PRINT_BYTES: "192",
OPENCLAW_PLUGIN_LIFECYCLE_TRACE: testCase.changeAfterSource
? testCase.traceEnabled
? "0"
: "1"
: testCase.traceValue,
OUTPUT_FILE: outputFile,
SCRATCH_ROOT: root,
},
);
expect(result.status).toBe(testCase.status);
expect(result.stdout).toBe("");
if (testCase.mode === "capture") {
expect(readFileSync(outputFile, "utf8")).toBe(`${capturedOutput}\n`);
}
expect(result.stderr.length).toBeLessThan(1_024);
expect(result.stderr).not.toContain("DO_NOT_DUMP_CAPTURED_PLUGIN_OUTPUT");
expect(result.stderr).not.toContain("CAPTURED_PLUGIN_OUTPUT_TAIL");
expect(result.stderr).not.toContain("DO_NOT_DUMP_PLUGIN_STDERR_PREFIX");
expect(result.stderr).not.toContain("plugin-fixture-bearer");
expect(result.stderr).not.toContain(fixtureApiKey);
expect(result.stderr).not.toContain(punctuationApiKey);
expect(result.stderr).not.toContain(startOfLineApiKey);
expect(result.stderr).not.toContain(boundarySecretSuffix);
expect(result.stderr).not.toContain(boundaryApiKey);
expect(
readdirSync(root).filter(
(name) => name.startsWith("openclaw-plugin-") || name.endsWith(".stderr.log"),
),
).toEqual([]);
const printsDiagnostics =
testCase.mode === "capture" || testCase.status !== 0 || testCase.traceEnabled;
if (printsDiagnostics) {
expect(result.stderr).toContain("truncated: showing last 192");
expect(result.stderr).toContain("PLUGIN_STDERR_TAIL_MARKER");
expect(result.stderr).toContain("task-runner");
expect(result.stderr).toContain("risk-score");
expect(result.stderr).toContain("disk-space");
expect(result.stderr).toContain("Authorization: Bearer [REDACTED]");
expect(result.stderr).toContain("OPENAI_API_KEY=[REDACTED]");
expect(result.stderr).toContain("[[REDACTED]]");
expect(result.stderr).toContain("[REDACTED]");
} else {
expect(result.stderr).toBe("");
}
if (testCase.traceValue === "1") {
expect(result.stderr).toContain("[plugins:lifecycle]");
} else {
expect(result.stderr).not.toContain("[plugins:lifecycle]");
}
if (testCase.status !== 0) {
const label = testCase.mode === "capture" ? "plugins-git-inspect.json" : "install-git";
const noun = testCase.mode === "capture" ? "capture" : "command";
const detail =
testCase.status === 124
? `timed out after 1s: ${label}`
: `failed with status ${testCase.status}: ${label}`;
expect(result.stderr).toContain(`Plugin sweep ${noun} ${detail}`);
}
} finally {
rmSync(root, { force: true, recursive: true });
}
},
);
it("scans plugin assertion logs without echoing whole files on failure", async () => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-plugin-update-log-"));
try {
const passRoot = path.join(root, "pass");
mkdirSync(passRoot, { recursive: true });
const markerPrefix = 'Skipping "demo';
writeFileSync(
path.join(passRoot, "plugins-dir-update.log"),
`${"x".repeat(64 * 1024 - markerPrefix.length)}${markerPrefix}-plugin-dir" (source: path).\n${"x".repeat(256 * 1024)}`,
"utf8",
);
const pass = await runAssertionAsync(["plugin-dir-update-skipped"], {
OPENCLAW_PLUGINS_TMP_DIR: passRoot,
});
expect(pass.status).toBe(0);
const failRoot = path.join(root, "fail");
mkdirSync(failRoot, { recursive: true });
writeFileSync(
path.join(failRoot, "plugins-dir-update.log"),
`${"x".repeat(256 * 1024)}\nmissing marker tail`,
"utf8",
);
const fail = await runAssertionAsync(["plugin-dir-update-skipped"], {
OPENCLAW_PLUGINS_TMP_DIR: failRoot,
});
expect(fail.status).toBe(1);
expect(fail.stderr).toContain("Output tail:");
expect(fail.stderr).toContain("missing marker tail");
expect(fail.stderr.length).toBeLessThan(20 * 1024);
const invalidRoot = path.join(root, "invalid");
const invalidHome = path.join(root, "home");
mkdirSync(invalidRoot, { recursive: true });
mkdirSync(invalidHome, { recursive: true });
writeFileSync(
path.join(invalidRoot, "plugins-invalid-openclaw-extensions.log"),
`openclaw.extensions[1]\n${"x".repeat(256 * 1024)}\nmissing validation tail`,
"utf8",
);
writeJson(path.join(invalidRoot, "plugins-invalid-openclaw-extensions-list.json"), {
plugins: [],
});
const invalid = await runAssertionAsync(["invalid-openclaw-extensions"], {
HOME: invalidHome,
OPENCLAW_PLUGINS_TMP_DIR: invalidRoot,
});
expect(invalid.status).toBe(1);
expect(invalid.stderr).toContain("malformed metadata install output");
expect(invalid.stderr).toContain("missing validation tail");
expect(invalid.stderr.length).toBeLessThan(20 * 1024);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it("routes npm through both registry environment spellings after replacing a parent registry", () => {
const root = autoCleanupTempDirs.make("openclaw-plugin-npm-fixture-routing-");
writeFileSync(path.join(root, "fixture.tgz"), "fixture package archive");
const result = runPluginsSweepShell(
`
set -euo pipefail
source scripts/e2e/lib/plugins/fixtures.sh
unset NPM_CONFIG_REGISTRY npm_config_registry
export NPM_CONFIG_REGISTRY=http://127.0.0.1:1 npm_config_registry=http://127.0.0.1:1
start_npm_fixture_registry fixture-pkg 1.0.0 "$REGISTRY_ROOT/fixture.tgz" "$REGISTRY_ROOT"
# Duplicate-case precedence depends on environment order; exercise each accepted spelling.
registry_index=0
for registry_key in NPM_CONFIG_REGISTRY npm_config_registry; do
env -u "$registry_key" npm view fixture-pkg@1.0.0 version --json --fetch-retries=0 --fetch-timeout=1000 --cache "$REGISTRY_ROOT/cache" > "$REGISTRY_ROOT/version-$registry_index.json"
registry_index=$((registry_index + 1))
done
`,
{
HOME: root,
REGISTRY_ROOT: root,
},
);
expect(result.status, result.stderr).toBe(0);
for (const index of [0, 1]) {
const version = JSON.parse(readFileSync(path.join(root, `version-${index}.json`), "utf8"));
// npm versions differ in scalar/array output; each command emits one complete JSON value.
expect(Array.isArray(version) ? version : [version]).toEqual(["1.0.0"]);
}
});
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 = runPluginsSweepShell(
[
"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"),
{
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("force-kills stubborn npm fixture registry children during cleanup", () => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-plugin-npm-fixture-kill-"));
try {
const binDir = path.join(root, "bin");
const fixtureDir = path.join(root, "fixture");
const pidPath = path.join(root, "server.pid");
mkdirSync(fixtureDir);
writeFixtureServerShims(binDir, pidPath, ":");
const result = runPluginsSweepShell(
[
"set -euo pipefail",
"source scripts/e2e/lib/plugins/fixtures.sh",
"set +e",
`( start_npm_fixture_registry fixture-pkg 1.0.0 ${shellQuote(path.join(root, "fixture.tgz"))} ${shellQuote(fixtureDir)} )`,
'status="$?"',
"set -e",
'[ "$status" != "0" ]',
].join("\n"),
{
OPENCLAW_PLUGINS_FIXTURE_STOP_ATTEMPTS: "2",
OPENCLAW_PLUGINS_FIXTURE_STOP_INTERVAL_SECONDS: "0.05",
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);
} finally {
rmSync(root, { force: true, recursive: true });
}
});
it("rejects invalid fixture stop attempts before cleanup polling", () => {
const result = runPluginsSweepShell(
[
"set -euo pipefail",
"source scripts/e2e/lib/plugins/fixtures.sh",
"openclaw_plugins_signal_fixture_process() { echo signal; }",
"openclaw_plugins_fixture_process_alive() { echo probe; return 1; }",
"set +e",
"openclaw_plugins_stop_fixture_process 12345",
'status="$?"',
"set -e",
'exit "$status"',
].join("\n"),
{ OPENCLAW_PLUGINS_FIXTURE_STOP_ATTEMPTS: "2x" },
);
expect(result.status).toBe(2);
expect(result.stderr).toContain("invalid OPENCLAW_PLUGINS_FIXTURE_STOP_ATTEMPTS: 2x");
expect(result.stdout).not.toContain("signal");
expect(result.stdout).not.toContain("probe");
});
it("rejects invalid fixture stop intervals before cleanup polling", () => {
const result = runPluginsSweepShell(
[
"set -euo pipefail",
"source scripts/e2e/lib/plugins/fixtures.sh",
"openclaw_plugins_signal_fixture_process() { echo signal; }",
"openclaw_plugins_fixture_process_alive() { echo probe; return 1; }",
"set +e",
"openclaw_plugins_stop_fixture_process 12345",
'status="$?"',
"set -e",
'exit "$status"',
].join("\n"),
{
OPENCLAW_PLUGINS_FIXTURE_STOP_ATTEMPTS: "2",
OPENCLAW_PLUGINS_FIXTURE_STOP_INTERVAL_SECONDS: "soon",
},
);
expect(result.status).toBe(2);
expect(result.stderr).toContain("invalid OPENCLAW_PLUGINS_FIXTURE_STOP_INTERVAL_SECONDS: soon");
expect(result.stdout).not.toContain("signal");
expect(result.stdout).not.toContain("probe");
});
it("bounds npm fixture registry logs when readiness fails", () => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-plugin-npm-fixture-log-"));
try {
const binDir = path.join(root, "bin");
const fixtureDir = path.join(root, "fixture");
mkdirSync(fixtureDir);
writeCrashingFixtureServerShim(binDir);
const result = runPluginsSweepShell(
[
"set -euo pipefail",
"source scripts/e2e/lib/plugins/fixtures.sh",
"set +e",
`start_npm_fixture_registry fixture-pkg 1.0.0 ${shellQuote(path.join(root, "fixture.tgz"))} ${shellQuote(fixtureDir)}`,
'status="$?"',
"set -e",
'printf "status=%s\\n" "$status"',
'[ "$status" != "0" ]',
].join("\n"),
{
OPENCLAW_DOCKER_E2E_LOG_PRINT_BYTES: "80",
PATH: `${binDir}${path.delimiter}/usr/bin${path.delimiter}/bin`,
},
);
expect(result.status, result.stderr || result.stdout).toBe(0);
expect(result.stdout).toContain("truncated: showing last 80");
expect(result.stdout).toContain("PLUGIN_FIXTURE_TAIL_MARKER");
expect(result.stdout).not.toContain("DO_NOT_DUMP_PLUGIN_FIXTURE_PREFIX");
} finally {
rmSync(root, { force: true, recursive: true });
}
});
it.for([
{ initial: null, fault: "", label: "new destination" },
{ initial: "", fault: "", label: "existing empty destination" },
{ initial: "12345", fault: "", label: "existing port" },
{ initial: null, fault: "write", label: "failed write" },
{ initial: "12345", fault: "rename", label: "failed replacement" },
])("publishes complete npm fixture port bytes: $label", ({ initial, fault }, { signal }) =>
publicationFixtures.run(async () => {
const root = publicationFixtures.createTempDir("openclaw-plugin-npm-publication-");
const registryScript = "scripts/e2e/lib/plugins/npm-registry-server.mjs";
// Docker and private observers copy this plain-Node closure without repository packages.
for (const file of [registryScript, "scripts/lib/bounded-response.mjs"]) {
mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
copyFileSync(file, path.join(root, file));
}
const portDir = path.join(root, "readiness");
mkdirSync(portDir);
const portFile = path.join(portDir, "port");
if (initial !== null) {
writeFileSync(portFile, initial);
}
const tarballPath = path.join(root, "fixture.tgz");
const archive = "fixture package archive";
writeFileSync(tarballPath, archive);
const preload = path.join(root, "publication-preload.mjs");
writeFileSync(
preload,
`import fs from "node:fs";
import path from "node:path";
const portFile = process.argv[2];
const fault = ${JSON.stringify(fault)};
const acknowledgments = ${JSON.stringify(root)};
const wait = new Int32Array(new SharedArrayBuffer(4));
function observe(phase, payload) {
fs.writeSync(1, JSON.stringify({ phase, payload }) + "\\n");
while (!fs.existsSync(path.join(acknowledgments, phase + ".ack"))) {
Atomics.wait(wait, 0, 0, 5);
}
}
const writeFile = fs.writeFileSync;
fs.writeFileSync = (file, data, options) => {
if (path.dirname(file) !== path.dirname(portFile)) return writeFile(file, data, options);
const bytes = Buffer.from(data);
const fd = fs.openSync(file, options?.flag ?? "w", options?.mode);
try {
observe("opened", data);
fs.writeSync(fd, bytes, 0, 1);
observe("first-byte", data);
if (fault === "write") throw new Error("injected port write failure");
fs.writeSync(fd, bytes, 1, bytes.length - 1);
} finally {
fs.closeSync(fd);
}
observe("complete", data);
setImmediate(() => observe("ready", data));
};
const rename = fs.renameSync;
fs.renameSync = (source, destination) => {
if (destination === portFile && fault === "rename") throw new Error("injected port rename failure");
return rename(source, destination);
};
`,
);
const nodeExecPath = resolveTestNodeExecPath();
const child = spawn(
nodeExecPath,
[
"--import",
pathToFileURL(preload).href,
registryScript,
portFile,
"fixture-pkg",
"1.0.0",
tarballPath,
],
{
cwd: root,
env: {
...process.env,
OPENCLAW_NPM_REGISTRY_PORT: "0",
OPENCLAW_NPM_REGISTRY_BIND_HOST: "127.0.0.1",
OPENCLAW_NPM_REGISTRY_UPSTREAM: "",
OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_URL: "",
OPENCLAW_NPM_REGISTRY_MERGE_UPSTREAM: "",
OPENCLAW_NPM_REGISTRY_DIST_TAGS: "",
},
signal,
killSignal: "SIGKILL",
stdio: ["ignore", "pipe", "pipe"],
},
);
const stderr = createBoundedChildOutput();
child.stderr.on("data", stderr.append);
child.on("error", (error) => stderr.append(error));
const closed = new Promise<void>((resolve) => {
child.once("close", () => resolve());
});
const lines = createInterface({ input: child.stdout });
const observations: Array<{ phase: string; payload: string; observed: string | null }> = [];
try {
for await (const line of lines) {
const observation = JSON.parse(line) as (typeof observations)[number];
// The writer stays at this boundary until this independent process has read it.
observations.push({
...observation,
observed: existsSync(portFile) ? readFileSync(portFile, "utf8") : null,
});
writeFileSync(path.join(root, `${observation.phase}.ack`), "");
if (observation.phase === "ready") {
break;
}
}
expect(
observations.map(({ phase }) => phase),
stderr.text(),
).toEqual(
fault === "write"
? ["opened", "first-byte"]
: fault === "rename"
? ["opened", "first-byte", "complete"]
: ["opened", "first-byte", "complete", "ready"],
);
for (const { phase, payload, observed } of observations) {
expect(payload).toMatch(/^[1-9][0-9]*$/u);
expect([initial, payload], `port file exposed incomplete bytes at ${phase}`).toContain(
observed,
);
}
if (fault) {
await closed;
expect(child.exitCode, stderr.text()).toBe(1);
expect(stderr.text()).toContain(`injected port ${fault} failure`);
expect(existsSync(portFile) ? readFileSync(portFile, "utf8") : null).toBe(initial);
} else {
const published = readFileSync(portFile, "utf8");
expect(observations.at(-1)).toEqual({
phase: "ready",
payload: published,
observed: published,
});
const metadata = await requestFixtureRegistry(Number(published), "/fixture-pkg");
expect(metadata.statusCode, stderr.text()).toBe(200);
const manifest = JSON.parse(metadata.body).versions["1.0.0"];
expect(manifest).toMatchObject({ name: "fixture-pkg", version: "1.0.0" });
const tarball = new URL(manifest.dist.tarball);
expect(tarball.origin).toBe(`http://127.0.0.1:${published}`);
const response = await requestFixtureRegistry(Number(published), tarball.pathname);
expect(response.statusCode).toBe(200);
expect(response.body).toBe(archive);
expect(response.contentLength).toBe(String(Buffer.byteLength(archive)));
}
expect(readdirSync(portDir)).toEqual(initial !== null || !fault ? ["port"] : []);
} finally {
lines.close();
if (child.exitCode === null && child.signalCode === null) {
child.kill("SIGKILL");
}
await closed;
rmSync(root, { recursive: true, force: true });
expect(existsSync(root)).toBe(false);
}
}),
);
it("keeps npm fixture registry alive after malformed package paths", async () => {
const root = autoCleanupTempDirs.make("openclaw-plugin-npm-fixture-request-");
const portFile = path.join(root, "port");
const tarballPath = path.join(root, "demo-plugin.tgz");
writeFileSync(tarballPath, "fixture package archive", "utf8");
const child = startFixtureRegistry(portFile, tarballPath);
const stderr = createBoundedChildOutput();
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk) => {
stderr.append(chunk);
});
try {
const port = await waitForPortFile(portFile);
const malformed = await requestFixtureRegistry(port, "/%");
expect(malformed.statusCode).toBe(404);
expect(malformed.body).toContain("not found");
expect(child.exitCode, stderr.text()).toBeNull();
const valid = await requestFixtureRegistry(port, "/@openclaw%2Fdemo-plugin-npm");
expect(valid.statusCode, stderr.text()).toBe(200);
expect(JSON.parse(valid.body)).toMatchObject({
name: "@openclaw/demo-plugin-npm",
"dist-tags": { latest: "1.0.0" },
});
} finally {
await stopFixtureRegistry(child);
}
});
it("serves scoped candidate tarballs through canonical npm shrinkwrap paths", async () => {
const root = autoCleanupTempDirs.make("openclaw-plugin-npm-scoped-tarball-");
const portFile = path.join(root, "port");
const tarballPath = path.join(root, "openclaw-ai-2026.7.34.tgz");
const archive = "scoped candidate package archive";
writeFileSync(tarballPath, archive, "utf8");
const child = spawn(
process.execPath,
[
"scripts/e2e/lib/plugins/npm-registry-server.mjs",
portFile,
"@openclaw/ai",
"2026.7.34",
tarballPath,
],
{ cwd: process.cwd(), stdio: ["ignore", "pipe", "pipe"] },
);
const stderr = createBoundedChildOutput();
child.stderr.setEncoding("utf8");
child.stderr.on("data", stderr.append);
const closed = new Promise<void>((resolve) => {
child.once("close", () => resolve());
});
try {
for (let attempt = 0; attempt < 100 && !existsSync(portFile); attempt += 1) {
await new Promise<void>((resolve) => {
setTimeout(resolve, 10);
});
}
const port = Number(readFileSync(portFile, "utf8"));
for (const pathname of [
"/@openclaw/ai/-/ai-2026.7.34.tgz",
"/@openclaw%2Fai/-/ai-2026.7.34.tgz",
"/@openclaw%2Fai/-/openclaw-ai-2026.7.34.tgz",
]) {
const response = await requestFixtureRegistry(port, pathname);
expect(response.statusCode, `${pathname}: ${stderr.text()}`).toBe(200);
expect(response.body).toBe(archive);
}
} finally {
child.kill("SIGKILL");
await closed;
}
});
it("serves drive-qualified tarball dependencies using the request-visible registry origin", async () => {
const root = autoCleanupTempDirs.make("openclaw-plugin-npm-fixture-package-");
const packageDir = path.join(root, "package");
const portFile = path.join(root, "port");
// On POSIX, a relative D:/ path reproduces GNU tar's Windows remote-archive parsing.
const archiveDir = process.platform === "win32" ? root : "D:/packages";
const tarballPath = path.join(archiveDir, "openclaw.tgz");
mkdirSync(path.resolve(root, archiveDir), { recursive: true });
mkdirSync(packageDir);
writeJson(path.join(packageDir, "package.json"), {
name: "openclaw",
version: "2026.7.1-beta.3",
dependencies: {
"@openclaw/ai": "2026.7.1-beta.3",
zod: "4.3.6",
},
optionalDependencies: {
"sqlite-vec": "0.1.7-alpha.2",
},
});
const packed = spawnSync("tar", ["-czf", "openclaw.tgz", "-C", root, "package"], {
cwd: path.resolve(root, archiveDir),
encoding: "utf8",
});
expect(packed.status, packed.stderr).toBe(0);
const child = spawn(
process.execPath,
[
path.resolve("scripts/e2e/lib/plugins/npm-registry-server.mjs"),
portFile,
"openclaw",
"2026.7.1-beta.3",
tarballPath,
],
{
cwd: root,
env: {
...process.env,
OPENCLAW_NPM_REGISTRY_DIST_TAGS: "latest=0.0.0,beta=2026.7.1-beta.3",
// Fail locally if GNU tar mistakes the synthetic drive letter for a remote host.
TAR_OPTIONS: "--rsh-command=false",
},
stdio: ["ignore", "pipe", "pipe"],
},
);
try {
const port = await waitForPortFile(portFile);
const response = await requestFixtureRegistry(port, "/openclaw", {
host: `192.0.2.2:${port}`,
});
const metadata = JSON.parse(response.body);
expect(response.statusCode).toBe(200);
expect(metadata["dist-tags"]).toEqual({
latest: "0.0.0",
beta: "2026.7.1-beta.3",
});
expect(metadata.versions["2026.7.1-beta.3"].dependencies).toEqual({
"@openclaw/ai": "2026.7.1-beta.3",
zod: "4.3.6",
});
expect(metadata.versions["2026.7.1-beta.3"].optionalDependencies).toEqual({
"sqlite-vec": "0.1.7-alpha.2",
});
expect(metadata.versions["2026.7.1-beta.3"].dist.tarball).toBe(
`http://192.0.2.2:${port}/openclaw/-/openclaw.tgz`,
);
} finally {
await stopFixtureRegistry(child);
}
});
it.each([false, true])(
"projects upstream tarballs per request origin without changing external URLs (merged=%s)",
async (merged) => {
const root = autoCleanupTempDirs.make("openclaw-plugin-npm-fixture-proxy-");
const portFile = path.join(root, "port");
const tarballPath = path.join(root, "demo-plugin.tgz");
const packageName = merged ? "@openclaw/demo-plugin-npm" : "upstream-package";
const externalTarball = "https://external.invalid/upstream-package.tgz";
let upstreamRequests = 0;
writeFileSync(tarballPath, "fixture package archive", "utf8");
const upstream = createServer((request, response) => {
upstreamRequests += 1;
const compressedBody = gzipSync(
JSON.stringify({
name: packageName,
payload: "x".repeat(1_000),
versions: {
"0.9.0": {
name: packageName,
version: "0.9.0",
dist: {
tarball: `http://${request.headers.host}/upstream-package/-/package.tgz?download=1`,
},
},
"0.8.0": {
name: packageName,
version: "0.8.0",
dist: { tarball: externalTarball },
},
},
}),
);
response.writeHead(200, {
"content-encoding": "gzip",
"content-length": String(compressedBody.length),
"content-type": "application/json",
});
response.end(compressedBody);
});
await new Promise<void>((resolve) => {
upstream.listen(0, "127.0.0.1", resolve);
});
const upstreamAddress = upstream.address();
if (!upstreamAddress || typeof upstreamAddress === "string") {
throw new Error("expected upstream registry address");
}
const child = startFixtureRegistry(portFile, tarballPath, {
OPENCLAW_NPM_REGISTRY_UPSTREAM: `http://127.0.0.1:${upstreamAddress.port}`,
OPENCLAW_NPM_REGISTRY_MERGE_UPSTREAM: merged ? "1" : "",
});
try {
const port = await waitForPortFile(portFile);
for (const host of [`192.0.2.2:${port}`, `192.0.2.3:${port}`]) {
const response = await requestFixtureRegistry(
port,
`/${encodeURIComponent(packageName)}`,
{ host },
);
const metadata = JSON.parse(response.body);
expect(response.statusCode).toBe(200);
expect(metadata.versions["0.9.0"].dist.tarball).toBe(
`http://${host}/upstream-package/-/package.tgz?download=1`,
);
expect(metadata.versions["0.8.0"].dist.tarball).toBe(externalTarball);
if (merged) {
expect(new URL(metadata.versions["1.0.0"].dist.tarball).origin).toBe(`http://${host}`);
}
if (!merged) {
expect(response.contentLength).toBe(String(Buffer.byteLength(response.body)));
}
}
expect(upstreamRequests).toBe(merged ? 1 : 2);
} finally {
await stopFixtureRegistry(child);
await new Promise<void>((resolve) => {
upstream.close(() => resolve());
});
}
},
);
it("streams proxied npm tarballs without buffering a content length", async () => {
const root = autoCleanupTempDirs.make("openclaw-plugin-npm-fixture-tarball-proxy-");
const portFile = path.join(root, "port");
const tarballPath = path.join(root, "demo-plugin.tgz");
const upstreamBody = "x".repeat(1024 * 1024);
writeFileSync(tarballPath, "fixture package archive", "utf8");
const upstream = createServer((_request, response) => {
response.writeHead(200, { "content-type": "application/octet-stream" });
response.write(upstreamBody.slice(0, upstreamBody.length / 2));
response.end(upstreamBody.slice(upstreamBody.length / 2));
});
await new Promise<void>((resolve) => {
upstream.listen(0, "127.0.0.1", resolve);
});
const upstreamAddress = upstream.address();
if (!upstreamAddress || typeof upstreamAddress === "string") {
throw new Error("expected upstream registry address");
}
const child = startFixtureRegistry(portFile, tarballPath, {
OPENCLAW_NPM_REGISTRY_UPSTREAM: `http://127.0.0.1:${upstreamAddress.port}`,
});
try {
const port = await waitForPortFile(portFile);
const response = await requestFixtureRegistry(
port,
"/@openai/codex/-/codex-0.145.0-linux-arm64.tgz",
);
expect(response.statusCode).toBe(200);
expect(response.contentLength).toBeUndefined();
expect(response.body).toBe(upstreamBody);
} finally {
await stopFixtureRegistry(child);
await new Promise<void>((resolve) => {
upstream.close(() => resolve());
});
}
});
it("rejects oversized upstream bodies without stopping the fixture registry", async () => {
const root = autoCleanupTempDirs.make("openclaw-plugin-npm-fixture-proxy-limit-");
const portFile = path.join(root, "port");
const tarballPath = path.join(root, "demo-plugin.tgz");
writeFileSync(tarballPath, "fixture package archive", "utf8");
const upstream = createServer((_request, response) => {
response.writeHead(200, {
"content-length": String(64 * 1024 * 1024 + 1),
"content-type": "application/octet-stream",
});
response.end("oversized");
});
await new Promise<void>((resolve) => {
upstream.listen(0, "127.0.0.1", resolve);
});
const upstreamAddress = upstream.address();
if (!upstreamAddress || typeof upstreamAddress === "string") {
throw new Error("expected upstream registry address");
}
const child = startFixtureRegistry(portFile, tarballPath, {
OPENCLAW_NPM_REGISTRY_UPSTREAM: `http://127.0.0.1:${upstreamAddress.port}`,
});
const stderr = createBoundedChildOutput();
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk) => {
stderr.append(chunk);
});
try {
const port = await waitForPortFile(portFile);
const oversized = await requestFixtureRegistry(port, "/oversized-package");
expect(oversized.statusCode, stderr.text()).toBe(502);
expect(oversized.body).toContain(
"npm registry upstream response body exceeded 67108864 bytes",
);
const local = await requestFixtureRegistry(port, "/@openclaw%2Fdemo-plugin-npm");
expect(local.statusCode, stderr.text()).toBe(200);
expect(child.exitCode, stderr.text()).toBeNull();
} finally {
await stopFixtureRegistry(child);
upstream.closeAllConnections();
await new Promise<void>((resolve) => {
upstream.close(() => resolve());
});
}
});
it("times out stalled upstream response bodies without stopping the fixture registry", async () => {
const root = autoCleanupTempDirs.make("openclaw-plugin-npm-fixture-proxy-timeout-");
const portFile = path.join(root, "port");
const preloadPath = path.join(root, "shorten-abort-timeout.mjs");
const tarballPath = path.join(root, "demo-plugin.tgz");
let upstreamHits = 0;
writeFileSync(
preloadPath,
[
"const nativeFetch = globalThis.fetch;",
"let timeoutController;",
"let timeoutWatchdog;",
"AbortSignal.timeout = () => {",
" timeoutController = new AbortController();",
" timeoutWatchdog = setTimeout(() => process.exit(86), 5_000);",
" timeoutWatchdog.unref();",
" return timeoutController.signal;",
"};",
"globalThis.fetch = async (...args) => {",
" const response = await nativeFetch(...args);",
" let firstChunk = true;",
" const body = response.body.pipeThrough(",
" new TransformStream({",
" transform(chunk, controller) {",
" controller.enqueue(chunk);",
" if (firstChunk) {",
" firstChunk = false;",
" clearTimeout(timeoutWatchdog);",
' queueMicrotask(() => timeoutController.abort(new DOMException("timeout", "TimeoutError")));',
" }",
" },",
" }),",
" );",
" return new Response(body, response);",
"};",
"",
].join("\n"),
"utf8",
);
writeFileSync(tarballPath, "fixture package archive", "utf8");
const upstream = createServer((_request, response) => {
upstreamHits += 1;
response.writeHead(200, {
"content-length": "1024",
"content-type": "application/json",
});
response.write('{"partial":');
});
await new Promise<void>((resolve) => {
upstream.listen(0, "127.0.0.1", resolve);
});
const upstreamAddress = upstream.address();
if (!upstreamAddress || typeof upstreamAddress === "string") {
throw new Error("expected upstream registry address");
}
const child = startFixtureRegistry(
portFile,
tarballPath,
{
OPENCLAW_NPM_REGISTRY_UPSTREAM: `http://127.0.0.1:${upstreamAddress.port}`,
},
preloadPath,
);
const stderr = createBoundedChildOutput();
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk) => {
stderr.append(chunk);
});
try {
const port = await waitForPortFile(portFile);
const stalled = await requestFixtureRegistry(port, "/stalled-package");
expect(stalled.statusCode, stderr.text()).toBe(502);
expect(stalled.body).toContain("upstream registry request failed");
expect(upstreamHits).toBe(1);
const local = await requestFixtureRegistry(port, "/@openclaw%2Fdemo-plugin-npm");
expect(local.statusCode, stderr.text()).toBe(200);
expect(child.exitCode, stderr.text()).toBeNull();
} finally {
await stopFixtureRegistry(child);
upstream.closeAllConnections();
await new Promise<void>((resolve) => {
upstream.close(() => resolve());
});
}
});
it("does not let absolute-form request targets escape the configured upstream", async () => {
const root = autoCleanupTempDirs.make("openclaw-plugin-npm-fixture-proxy-origin-");
const portFile = path.join(root, "port");
const tarballPath = path.join(root, "demo-plugin.tgz");
let configuredUpstreamHits = 0;
let escapeServerHits = 0;
let configuredUpstreamTarget: string | undefined;
writeFileSync(tarballPath, "fixture package archive", "utf8");
const configuredUpstream = createServer((request, response) => {
configuredUpstreamHits += 1;
configuredUpstreamTarget = request.url;
response.writeHead(200, { "content-type": "text/plain" });
response.end("configured upstream");
});
const escapeServer = createServer((_request, response) => {
escapeServerHits += 1;
response.writeHead(200, { "content-type": "text/plain" });
response.end("escaped upstream");
});
await Promise.all([
new Promise<void>((resolve) => {
configuredUpstream.listen(0, "127.0.0.1", resolve);
}),
new Promise<void>((resolve) => {
escapeServer.listen(0, "127.0.0.1", resolve);
}),
]);
const configuredAddress = configuredUpstream.address();
const escapeAddress = escapeServer.address();
if (
!configuredAddress ||
typeof configuredAddress === "string" ||
!escapeAddress ||
typeof escapeAddress === "string"
) {
throw new Error("expected upstream registry addresses");
}
const child = startFixtureRegistry(portFile, tarballPath, {
OPENCLAW_NPM_REGISTRY_UPSTREAM: `http://127.0.0.1:${configuredAddress.port}`,
});
try {
const port = await waitForPortFile(portFile);
const escaped = await requestFixtureRegistry(
port,
`http://registry.invalid//127.0.0.1:${escapeAddress.port}/probe`,
);
expect(escaped.statusCode).toBe(502);
expect(escaped.body).toContain("refusing non-origin registry request URL");
expect(configuredUpstreamHits).toBe(0);
expect(escapeServerHits).toBe(0);
const valid = await requestFixtureRegistry(port, "/pkg?x=1");
expect(valid.statusCode).toBe(200);
expect(valid.body).toBe("configured upstream");
expect(configuredUpstreamHits).toBe(1);
expect(configuredUpstreamTarget).toBe("/pkg?x=1");
expect(escapeServerHits).toBe(0);
} finally {
await stopFixtureRegistry(child);
await Promise.all([
new Promise<void>((resolve) => {
configuredUpstream.close(() => resolve());
}),
new Promise<void>((resolve) => {
escapeServer.close(() => resolve());
}),
]);
}
});
it("rejects invalid plugin fixture log byte limits before npm fixture setup", () => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-plugin-npm-fixture-log-invalid-"));
try {
const binDir = path.join(root, "bin");
const fixtureDir = path.join(root, "fixture");
mkdirSync(binDir, { recursive: true });
mkdirSync(fixtureDir);
writeFileSync(
path.join(binDir, "node"),
"#!/bin/bash\necho node should not run >&2\nexit 1\n",
);
chmodSync(path.join(binDir, "node"), 0o755);
const result = runPluginsSweepShell(
[
"set -euo pipefail",
"source scripts/e2e/lib/plugins/fixtures.sh",
"set +e",
`start_npm_fixture_registry fixture-pkg 1.0.0 ${shellQuote(path.join(root, "fixture.tgz"))} ${shellQuote(fixtureDir)}`,
'status="$?"',
"set -e",
'exit "$status"',
].join("\n"),
{
OPENCLAW_DOCKER_E2E_LOG_PRINT_BYTES: "64kb",
PATH: `${binDir}${path.delimiter}/usr/bin${path.delimiter}/bin`,
},
);
expect(result.status).toBe(2);
expect(result.stderr).toContain("invalid OPENCLAW_DOCKER_E2E_LOG_PRINT_BYTES: 64kb");
expect(result.stderr).not.toContain("node should not run");
} 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 = runPluginsSweepShell(
[
"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"),
{
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("rejects invalid plugin fixture log byte limits before ClawHub fixture setup", () => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-plugin-clawhub-fixture-log-invalid-"));
try {
const binDir = path.join(root, "bin");
const tmpDir = path.join(root, "scratch");
mkdirSync(binDir, { recursive: true });
mkdirSync(tmpDir);
writeFileSync(
path.join(binDir, "node"),
"#!/bin/bash\necho node should not run >&2\nexit 1\n",
);
chmodSync(path.join(binDir, "node"), 0o755);
const result = runPluginsSweepShell(
[
"set -euo pipefail",
"source scripts/e2e/lib/plugins/fixtures.sh",
"source scripts/e2e/lib/plugins/clawhub.sh",
"set +e",
"run_plugins_clawhub_scenario",
'status="$?"',
"set -e",
'exit "$status"',
].join("\n"),
{
OPENCLAW_DOCKER_E2E_LOG_PRINT_BYTES: "64kb",
OPENCLAW_PLUGINS_E2E_LIVE_CLAWHUB: "0",
OPENCLAW_PLUGINS_TMP_DIR: tmpDir,
PATH: `${binDir}${path.delimiter}/usr/bin${path.delimiter}/bin`,
},
);
expect(result.status).toBe(2);
expect(result.stderr).toContain("invalid OPENCLAW_DOCKER_E2E_LOG_PRINT_BYTES: 64kb");
expect(result.stderr).not.toContain("node should not run");
} finally {
rmSync(root, { force: true, recursive: true });
}
});
it("bounds ClawHub fixture server logs when readiness fails", () => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-plugin-clawhub-fixture-log-"));
try {
const binDir = path.join(root, "bin");
const tmpDir = path.join(root, "scratch");
mkdirSync(tmpDir);
writeCrashingFixtureServerShim(binDir);
const result = runPluginsSweepShell(
[
"set -euo pipefail",
"source scripts/e2e/lib/plugins/fixtures.sh",
"source scripts/e2e/lib/plugins/clawhub.sh",
"set +e",
"run_plugins_clawhub_scenario",
'status="$?"',
"set -e",
'printf "status=%s\\n" "$status"',
'[ "$status" != "0" ]',
].join("\n"),
{
OPENCLAW_DOCKER_E2E_LOG_PRINT_BYTES: "80",
OPENCLAW_PLUGINS_E2E_LIVE_CLAWHUB: "0",
OPENCLAW_PLUGINS_TMP_DIR: tmpDir,
PATH: `${binDir}${path.delimiter}/usr/bin${path.delimiter}/bin`,
},
);
expect(result.status, result.stderr || result.stdout).toBe(0);
expect(result.stdout).toContain("truncated: showing last 80");
expect(result.stdout).toContain("PLUGIN_FIXTURE_TAIL_MARKER");
expect(result.stdout).not.toContain("DO_NOT_DUMP_PLUGIN_FIXTURE_PREFIX");
} 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");
const scratchRoot = path.join(root, "scratch");
const installPath = path.join(home, "managed-plugin");
mkdirSync(installPath, { recursive: true });
try {
writeJson(path.join(scratchRoot, "plugins2.json"), {
plugins: [{ id: "demo-plugin-tgz", status: "loaded" }],
});
writeJson(path.join(scratchRoot, "plugins2-inspect.json"), {
gatewayMethods: ["demo.tgz"],
});
writeJson(path.join(home, ".openclaw", "plugins", "installs.json"), {
installRecords: {
"demo-plugin-tgz": {
source: "archive",
installPath: String.raw`~\managed-plugin`,
},
},
});
const result = spawnSync(process.execPath, [ASSERTIONS_SCRIPT, "plugin-tgz"], {
encoding: "utf8",
env: {
...process.env,
HOME: home,
OPENCLAW_PLUGINS_E2E_CLAWHUB_PREFLIGHT_TIMEOUT_MS: "1e3",
OPENCLAW_PLUGINS_TMP_DIR: scratchRoot,
},
});
expect(result.status).toBe(0);
} finally {
rmSync(root, { force: true, recursive: true });
}
});
it("compares local plugin source paths by canonical path", () => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-plugins-assertions-"));
const home = path.join(root, "home");
const scratchRoot = path.join(root, "scratch");
const sourceParent = path.join(root, "source");
const sourcePath = `${sourceParent}//plugin`;
const normalizedSourcePath = path.join(sourceParent, "plugin");
const installPath = path.join(home, ".openclaw", "extensions", "demo-plugin-dir");
mkdirSync(sourcePath, { recursive: true });
mkdirSync(installPath, { recursive: true });
try {
writeJson(path.join(scratchRoot, "plugins3.json"), {
plugins: [{ id: "demo-plugin-dir", status: "loaded" }],
});
writeJson(path.join(scratchRoot, "plugins3-inspect.json"), {
gatewayMethods: ["demo.dir"],
});
writeJson(path.join(home, ".openclaw", "plugins", "installs.json"), {
installRecords: {
"demo-plugin-dir": {
source: "path",
sourcePath: normalizedSourcePath,
installPath,
},
},
});
const result = spawnSync(process.execPath, [ASSERTIONS_SCRIPT, "plugin-dir", sourcePath], {
encoding: "utf8",
env: {
...process.env,
HOME: home,
OPENCLAW_CONFIG_PATH: path.join(home, ".openclaw", "openclaw.json"),
OPENCLAW_PLUGINS_TMP_DIR: scratchRoot,
OPENCLAW_STATE_DIR: path.join(home, ".openclaw"),
},
});
expect(result.status).toBe(0);
} finally {
rmSync(root, { force: true, recursive: true });
}
});
it("still requires archive managed install directories to be removed", () => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-plugins-assertions-"));
const home = path.join(root, "home");
const scratchRoot = path.join(root, "scratch");
const installPath = path.join(home, ".openclaw", "extensions", "demo-plugin-tgz");
mkdirSync(installPath, { recursive: true });
try {
writeJson(path.join(scratchRoot, "plugins2-uninstalled.json"), { plugins: [] });
writeFileSync(path.join(scratchRoot, "plugins2-install-path.txt"), installPath, "utf8");
writeJson(path.join(home, ".openclaw", "plugins", "installs.json"), {
installRecords: {},
});
writeJson(path.join(home, ".openclaw", "openclaw.json"), {
plugins: { entries: { "demo-plugin-tgz": { enabled: false } } },
});
const result = spawnSync(process.execPath, [ASSERTIONS_SCRIPT, "plugin-tgz-removed"], {
encoding: "utf8",
env: {
...process.env,
HOME: home,
OPENCLAW_PLUGINS_TMP_DIR: scratchRoot,
},
});
expect(result.status).not.toBe(0);
expect(result.stderr).toContain("managed install path still exists after uninstall");
} finally {
rmSync(root, { force: true, recursive: true });
}
});
it("requires the resolved legacy profile before allowing the pre-marker uninstall contract", () => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-plugins-assertions-"));
const home = path.join(root, "home");
const scratchRoot = path.join(root, "scratch");
const removedInstallPath = path.join(home, ".openclaw", "extensions", "demo-plugin-tgz");
try {
writeJson(path.join(scratchRoot, "plugins2-uninstalled.json"), { plugins: [] });
writeFileSync(
path.join(scratchRoot, "plugins2-install-path.txt"),
removedInstallPath,
"utf8",
);
writeJson(path.join(home, ".openclaw", "plugins", "installs.json"), {
installRecords: {},
});
const baseEnv = {
...process.env,
HOME: home,
OPENCLAW_CONFIG_PATH: path.join(home, ".openclaw", "openclaw.json"),
OPENCLAW_PLUGINS_TMP_DIR: scratchRoot,
OPENCLAW_STATE_DIR: path.join(home, ".openclaw"),
};
const current = spawnSync(process.execPath, [ASSERTIONS_SCRIPT, "plugin-tgz-removed"], {
encoding: "utf8",
env: baseEnv,
});
const rawFrozenAuthorization = spawnSync(
process.execPath,
[ASSERTIONS_SCRIPT, "plugin-tgz-removed"],
{
encoding: "utf8",
env: { ...baseEnv, OPENCLAW_ALLOW_FROZEN_TARGET_SCENARIO_OMISSIONS: "1" },
},
);
const legacy = spawnSync(process.execPath, [ASSERTIONS_SCRIPT, "plugin-tgz-removed"], {
encoding: "utf8",
env: { ...baseEnv, OPENCLAW_FROZEN_TARGET_PLUGIN_UNINSTALL_MODE: "legacy" },
});
expect(current.status).not.toBe(0);
expect(current.stderr).toContain("exact disabled uninstall marker missing");
expect(rawFrozenAuthorization.status).not.toBe(0);
expect(rawFrozenAuthorization.stderr).toContain("exact disabled uninstall marker missing");
expect(legacy.status).toBe(0);
} finally {
rmSync(root, { force: true, recursive: true });
}
});
it("keeps the legacy npm project cleanup assertion scoped to the resolved profile", () => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-plugins-assertions-"));
const home = path.join(root, "home");
const scratchRoot = path.join(root, "scratch");
const npmProjectRoot = path.join(home, ".openclaw", "npm", "projects", "demo-plugin-npm");
const installPath = path.join(npmProjectRoot, "node_modules", "@openclaw", "demo-plugin-npm");
const dependencyPackagePath = path.join(
npmProjectRoot,
"node_modules",
"is-number",
"package.json",
);
try {
mkdirSync(npmProjectRoot, { recursive: true });
writeJson(path.join(scratchRoot, "plugins-npm-uninstalled.json"), { plugins: [] });
writeFileSync(path.join(scratchRoot, "plugins-npm-install-path.txt"), installPath, "utf8");
writeFileSync(
path.join(scratchRoot, "plugins-npm-dependency-path.txt"),
dependencyPackagePath,
"utf8",
);
writeJson(path.join(home, ".openclaw", "plugins", "installs.json"), { installRecords: {} });
writeJson(path.join(home, ".openclaw", "openclaw.json"), {
plugins: { entries: { "demo-plugin-npm": { enabled: false } } },
});
const baseEnv = {
...process.env,
HOME: home,
OPENCLAW_CONFIG_PATH: path.join(home, ".openclaw", "openclaw.json"),
OPENCLAW_PLUGINS_E2E_CLAWHUB_PREFLIGHT_TIMEOUT_MS: "1000",
OPENCLAW_PLUGINS_TMP_DIR: scratchRoot,
OPENCLAW_STATE_DIR: path.join(home, ".openclaw"),
};
const current = spawnSync(process.execPath, [ASSERTIONS_SCRIPT, "plugin-npm-removed"], {
encoding: "utf8",
env: baseEnv,
});
writeJson(path.join(home, ".openclaw", "openclaw.json"), { plugins: { entries: {} } });
const legacy = spawnSync(process.execPath, [ASSERTIONS_SCRIPT, "plugin-npm-removed"], {
encoding: "utf8",
env: { ...baseEnv, OPENCLAW_FROZEN_TARGET_PLUGIN_UNINSTALL_MODE: "legacy" },
});
expect(current.status).not.toBe(0);
expect(current.stderr).toContain("npm managed project still exists after uninstall");
expect(legacy.status, legacy.stderr).toBe(0);
} finally {
rmSync(root, { force: true, recursive: true });
}
});
it("accepts a retained legacy npm listing only for the keep-files assertion", () => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-plugins-assertions-"));
const home = path.join(root, "home");
const scratchRoot = path.join(root, "scratch");
const installPath = path.join(home, ".openclaw", "npm", "demo-plugin-npm");
const dependencyPackagePath = path.join(installPath, "node_modules", "is-number");
try {
mkdirSync(dependencyPackagePath, { recursive: true });
writeJson(path.join(scratchRoot, "plugins-npm-retained.json"), {
plugins: [{ id: "demo-plugin-npm", status: "disabled", enabled: false }],
});
writeFileSync(path.join(scratchRoot, "plugins-npm-install-path.txt"), installPath, "utf8");
writeFileSync(
path.join(scratchRoot, "plugins-npm-dependency-path.txt"),
dependencyPackagePath,
"utf8",
);
writeJson(path.join(home, ".openclaw", "plugins", "installs.json"), {
installRecords: {},
});
writeJson(path.join(home, ".openclaw", "openclaw.json"), { plugins: { entries: {} } });
const env = {
...process.env,
HOME: home,
OPENCLAW_CONFIG_PATH: path.join(home, ".openclaw", "openclaw.json"),
OPENCLAW_PLUGINS_TMP_DIR: scratchRoot,
OPENCLAW_STATE_DIR: path.join(home, ".openclaw"),
};
const current = spawnSync(process.execPath, [ASSERTIONS_SCRIPT, "plugin-npm-retained"], {
encoding: "utf8",
env,
});
const legacy = spawnSync(process.execPath, [ASSERTIONS_SCRIPT, "plugin-npm-retained"], {
encoding: "utf8",
env: { ...env, OPENCLAW_FROZEN_TARGET_PLUGIN_UNINSTALL_MODE: "legacy" },
});
expect(current.status).not.toBe(0);
expect(current.stderr).toContain("still listed after uninstall");
expect(legacy.status, legacy.stderr).toBe(0);
} finally {
rmSync(root, { force: true, recursive: true });
}
});
it("rejects unreadable config during plugin uninstall proof", () => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-plugins-assertions-"));
const home = path.join(root, "home");
const scratchRoot = path.join(root, "scratch");
const removedInstallPath = path.join(home, ".openclaw", "extensions", "demo-plugin-tgz");
try {
writeJson(path.join(scratchRoot, "plugins2-uninstalled.json"), { plugins: [] });
writeFileSync(
path.join(scratchRoot, "plugins2-install-path.txt"),
removedInstallPath,
"utf8",
);
writeJson(path.join(home, ".openclaw", "plugins", "installs.json"), {
installRecords: {},
});
writeFileSync(path.join(home, ".openclaw", "openclaw.json"), "{ malformed\n", "utf8");
const result = spawnSync(process.execPath, [ASSERTIONS_SCRIPT, "plugin-tgz-removed"], {
encoding: "utf8",
env: {
...process.env,
HOME: home,
OPENCLAW_PLUGINS_TMP_DIR: scratchRoot,
},
});
expect(result.status).not.toBe(0);
expect(result.stderr).toContain("failed to read OpenClaw config");
} finally {
rmSync(root, { force: true, recursive: true });
}
});
it.each([
{
name: "rejects ClawHub install paths that resolve outside the managed extensions root",
escaped: true,
recordOverrides: {},
errorPrefix: null,
pathError: false,
},
{
name: "accepts legacy ZIP without later ClawPack or npm fields",
recordOverrides: {},
errorPrefix: null,
pathError: false,
},
{
name: "rejects a legacy artifact with the wrong format before later metadata",
recordOverrides: { artifactFormat: "tgz" },
errorPrefix: "missing ClawHub legacy ZIP artifact metadata",
pathError: false,
},
{
name: "rejects a non-legacy artifact kind before ClawPack metadata",
recordOverrides: { artifactKind: "other" },
errorPrefix: "missing ClawHub artifact metadata",
pathError: false,
},
{
name: "rejects missing ClawPack metadata before npm metadata",
recordOverrides: { artifactKind: "npm-pack", artifactFormat: "tgz" },
errorPrefix: "missing ClawHub ClawPack metadata",
pathError: false,
},
{
name: "rejects a string ClawPack size",
recordOverrides: {
artifactKind: "npm-pack",
artifactFormat: "tgz",
clawpackSha256: "digest",
clawpackSize: "0",
},
errorPrefix: "missing ClawHub ClawPack metadata",
pathError: false,
},
{
name: "accepts zero size before rejecting missing npm metadata",
recordOverrides: {
artifactKind: "npm-pack",
artifactFormat: "tgz",
clawpackSha256: "digest",
clawpackSize: 0,
},
errorPrefix: "missing ClawHub npm artifact metadata",
pathError: false,
},
{
name: "accepts zero size and truthy non-string metadata with a real npm peer",
recordOverrides: {
artifactKind: "npm-pack",
artifactFormat: "tgz",
clawpackSha256: { digest: 1 },
clawpackSize: 0,
npmIntegrity: 1,
npmShasum: true,
npmTarballName: ["package.tgz"],
},
errorPrefix: null,
pathError: false,
},
{
name: "rejects an npm peer linked to a different host",
recordOverrides: {
artifactKind: "npm-pack",
artifactFormat: "tgz",
clawpackSha256: "digest",
clawpackSize: 0,
npmIntegrity: "integrity",
npmShasum: "shasum",
npmTarballName: "package.tgz",
},
wrongPeerTarget: true,
},
{
name: "accepts an optional dependency inside the ClawHub installation",
recordOverrides: {
artifactKind: "npm-pack",
artifactFormat: "tgz",
clawpackSha256: "digest",
clawpackSize: 0,
npmIntegrity: "integrity",
npmShasum: "shasum",
npmTarballName: "package.tgz",
},
optionalDependency: "inside",
},
{
name: "rejects an optional dependency linked outside the ClawHub installation",
recordOverrides: {
artifactKind: "npm-pack",
artifactFormat: "tgz",
clawpackSha256: "digest",
clawpackSize: 0,
npmIntegrity: "integrity",
npmShasum: "shasum",
npmTarballName: "package.tgz",
},
optionalDependency: "escaped",
},
{
name: "rejects an empty install path before invalid metadata",
recordOverrides: { artifactFormat: "tgz", installPath: "" },
errorPrefix: null,
pathError: true,
},
{
name: "rejects a non-string install path before invalid metadata",
recordOverrides: { artifactFormat: "tgz", installPath: 42 },
errorPrefix: null,
pathError: true,
},
])(
"$name",
({ escaped, recordOverrides, errorPrefix, pathError, wrongPeerTarget, optionalDependency }) => {
const root = autoCleanupTempDirs.make("openclaw-plugins-clawhub-path-");
const home = path.join(root, "home");
const scratchRoot = path.join(root, "scratch");
const extensionsRoot = path.join(home, ".openclaw", "extensions");
const installPath = escaped
? `${extensionsRoot}${path.sep}..${path.sep}escaped-clawhub`
: path.join(extensionsRoot, "openclaw-kitchen-sink-fixture");
mkdirSync(extensionsRoot, { recursive: true });
mkdirSync(installPath, { recursive: true });
const record = {
artifactFormat: "zip",
artifactKind: "legacy-zip",
clawhubFamily: "code-plugin",
clawhubPackage: "@openclaw/kitchen-sink",
installPath,
source: "clawhub",
spec: "clawhub:@openclaw/kitchen-sink",
...recordOverrides,
};
if (record.artifactKind === "npm-pack") {
mkdirSync(path.join(installPath, "node_modules"), { recursive: true });
const peerTarget = wrongPeerTarget ? path.join(root, "other-host") : process.cwd();
if (wrongPeerTarget) {
mkdirSync(peerTarget);
}
symlinkSync(
peerTarget,
path.join(installPath, "node_modules", "openclaw"),
process.platform === "win32" ? "junction" : "dir",
);
if (optionalDependency) {
const dependencyPath = path.join(installPath, "node_modules", "is-number");
const dependencyTarget =
optionalDependency === "escaped" ? path.join(root, "other-dependency") : dependencyPath;
writeJson(path.join(dependencyTarget, "package.json"), { name: "is-number" });
if (optionalDependency === "escaped") {
symlinkSync(
dependencyTarget,
dependencyPath,
process.platform === "win32" ? "junction" : "dir",
);
}
}
}
writeJson(path.join(scratchRoot, "plugins-clawhub-installed.json"), {
plugins: [{ id: "openclaw-kitchen-sink-fixture", status: "loaded" }],
});
writeJson(path.join(scratchRoot, "plugins-clawhub-inspect.json"), {
plugin: { id: "openclaw-kitchen-sink-fixture" },
});
writeJson(path.join(home, ".openclaw", "plugins", "installs.json"), {
installRecords: {
"openclaw-kitchen-sink-fixture": record,
},
});
const result = spawnSync(process.execPath, [ASSERTIONS_SCRIPT, "clawhub-installed"], {
encoding: "utf8",
env: {
...process.env,
CLAWHUB_PLUGIN_ID: "openclaw-kitchen-sink-fixture",
CLAWHUB_PLUGIN_SPEC: "clawhub:@openclaw/kitchen-sink",
HOME: home,
OPENCLAW_STATE_DIR: path.join(home, ".openclaw"),
OPENCLAW_CONFIG_PATH: path.join(home, ".openclaw", "openclaw.json"),
OPENCLAW_PLUGINS_TMP_DIR: scratchRoot,
},
});
if (escaped) {
expect(result.status).toBe(1);
expect(result.stderr).toContain("ClawHub install path resolved outside");
} else if (pathError) {
expect(result.status).toBe(1);
expect(result.stderr.match(/^(?:Error|error): (.*)$/m)?.[1]).toBe(
"missing ClawHub install path for openclaw-kitchen-sink-fixture",
);
} else if (wrongPeerTarget || optionalDependency === "escaped") {
expect(result.status).toBe(1);
const expectedError = wrongPeerTarget
? `expected ClawHub openclaw peer ${realpathSync(path.join(root, "other-host"))} to target ${realpathSync(process.cwd())}`
: `ClawHub isolated dependency resolved outside ${installPath}: ${realpathSync(path.join(root, "other-dependency", "package.json"))}`;
expect(result.stderr.match(/^(?:Error|error): (.*)$/m)?.[1]).toBe(expectedError);
} else if (errorPrefix) {
expect(result.status).toBe(1);
expect(result.stderr.match(/^(?:Error|error): (.*)$/m)?.[1]).toBe(
`${errorPrefix} for openclaw-kitchen-sink-fixture: ${JSON.stringify(record)}`,
);
} else {
expect(result.status, result.stderr).toBe(0);
}
},
);
it("times out stalled ClawHub package metadata requests", async () => {
const server = createServer((_request, _response) => {});
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", resolve);
});
try {
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("expected TCP server address");
}
const result = await runAssertionAsync(["clawhub-preflight"], {
CLAWHUB_PLUGIN_ID: "openclaw-kitchen-sink-fixture",
CLAWHUB_PLUGIN_SPEC: "clawhub:@openclaw/kitchen-sink",
OPENCLAW_CLAWHUB_URL: `http://127.0.0.1:${address.port}`,
OPENCLAW_PLUGINS_E2E_CLAWHUB_PREFLIGHT_TIMEOUT_MS: "25",
});
expect(result.status).not.toBe(0);
expect(result.stderr).toContain(
"ClawHub package preflight for @openclaw/kitchen-sink timed out after 25ms",
);
} finally {
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
}
});
it("times out stalled ClawHub package metadata bodies", async () => {
const server = createServer((_request, response) => {
response.writeHead(200, { "content-type": "application/json" });
response.flushHeaders();
response.write("{");
});
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", resolve);
});
try {
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("expected TCP server address");
}
const result = await runAssertionAsync(["clawhub-preflight"], {
CLAWHUB_PLUGIN_ID: "openclaw-kitchen-sink-fixture",
CLAWHUB_PLUGIN_SPEC: "clawhub:@openclaw/kitchen-sink",
NODE_OPTIONS: `--import=data:text/javascript,${encodeURIComponent(
"const response = await fetch(process.env.OPENCLAW_CLAWHUB_URL); globalThis.fetch = async () => response;",
)}`,
OPENCLAW_CLAWHUB_URL: `http://127.0.0.1:${address.port}`,
OPENCLAW_PLUGINS_E2E_CLAWHUB_PREFLIGHT_TIMEOUT_MS: "75",
});
expect(result.status).not.toBe(0);
expect(result.stderr).toContain(
"ClawHub package preflight response for @openclaw/kitchen-sink timed out after 75ms",
);
} finally {
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
}
});
it("bounds ClawHub package metadata response bodies", async () => {
const server = createServer((_request, response) => {
response.writeHead(500, { "content-type": "text/plain" });
response.end("x".repeat(128));
});
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", resolve);
});
try {
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("expected TCP server address");
}
const result = await runAssertionAsync(["clawhub-preflight"], {
CLAWHUB_PLUGIN_ID: "openclaw-kitchen-sink-fixture",
CLAWHUB_PLUGIN_SPEC: "clawhub:@openclaw/kitchen-sink",
OPENCLAW_CLAWHUB_URL: `http://127.0.0.1:${address.port}`,
OPENCLAW_PLUGINS_E2E_CLAWHUB_PREFLIGHT_BODY_MAX_BYTES: "16",
OPENCLAW_PLUGINS_E2E_CLAWHUB_PREFLIGHT_TIMEOUT_MS: "1000",
});
expect(result.status).not.toBe(0);
expect(result.stderr).toContain(
"ClawHub package preflight response for @openclaw/kitchen-sink response body exceeded 16 bytes",
);
expect(result.stderr).not.toContain("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
} finally {
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
}
});
});