test: speed up and stabilize full suite

This commit is contained in:
Peter Steinberger
2026-07-05 07:59:48 -04:00
parent 785ab74779
commit 1f484a8dbd
142 changed files with 2872 additions and 1734 deletions

View File

@@ -60,15 +60,7 @@ describe("bench-cli-startup", () => {
it("rejects duplicate benchmark cases before running benchmarks", () => {
const result = spawnSync(
process.execPath,
[
"--import",
"tsx",
"scripts/bench-cli-startup.ts",
"--case",
"version",
"--case",
"version",
],
["--import", "tsx", "scripts/bench-cli-startup.ts", "--case", "version", "--case", "version"],
{
cwd: join(__dirname, "../.."),
encoding: "utf8",
@@ -83,9 +75,9 @@ describe("bench-cli-startup", () => {
});
it("rejects duplicate single-value controls before running benchmarks", () => {
expect(() =>
testing.validateCliArgs(["--output", "one.json", "--output", "two.json"]),
).toThrow("--output was provided more than once");
expect(() => testing.validateCliArgs(["--output", "one.json", "--output", "two.json"])).toThrow(
"--output was provided more than once",
);
const result = spawnSync(
process.execPath,
@@ -158,6 +150,11 @@ describe("bench-cli-startup", () => {
{
cwd: join(__dirname, "../.."),
encoding: "utf8",
env: {
...process.env,
OPENCLAW_TEST_CLI_STARTUP_TIMEOUT_KILL_GRACE_MS: "50",
VITEST: "1",
},
timeout: 8_000,
},
);

View File

@@ -701,7 +701,7 @@ describe("bundled plugin install/uninstall probe", () => {
let descendantPid: number | undefined;
try {
const commandResult = runtimeSmoke
.runCommand(process.execPath, [commandPath], { detached: undefined, timeoutMs: 1000 })
.runCommand(process.execPath, [commandPath], { detached: undefined, timeoutMs: 250 })
.catch((error: unknown) => error);
await waitForFile(descendantPidPath, 1000);
descendantPid = Number(fs.readFileSync(descendantPidPath, "utf8"));
@@ -709,7 +709,7 @@ describe("bundled plugin install/uninstall probe", () => {
if (!(error instanceof Error)) {
throw new Error("expected runtime command to time out");
}
expect(error.message).toMatch(/timed out after 1000ms/u);
expect(error.message).toMatch(/timed out after 250ms/u);
await waitForDead(descendantPid, 2000);
} finally {

View File

@@ -210,7 +210,7 @@ function makeSlowVersionCrabbox(helpText: string): string {
const script = [
"#!/usr/bin/env node",
"const args = process.argv.slice(2);",
'if (args[0] === "--version") { setTimeout(() => process.exit(0), 6_000); }',
'if (args[0] === "--version") { setTimeout(() => process.exit(0), 1_000); }',
`else if (args[0] === "run" && args[1] === "--help") { process.stdout.write(${JSON.stringify(helpText)}); }`,
].join("\n");
writeFileSync(crabboxPath, `${script}\n`, "utf8");
@@ -497,6 +497,7 @@ async function runSignalCleanupProof(sendSignals: (pid: number) => Promise<void>
{
env: {
OPENCLAW_FAKE_CRABBOX_DESCENDANT_PID_PATH: descendantPidPath,
OPENCLAW_TEST_CRABBOX_CHILD_KILL_GRACE_MS: "100",
},
nodePreload: testTimingPreload({ clockScale: 20 }),
},
@@ -575,7 +576,7 @@ afterAll(() => {
}
});
describe.concurrent("scripts/crabbox-wrapper", () => {
describe("scripts/crabbox-wrapper", () => {
const azureProviderHelp =
"provider: hetzner, aws, azure, local-container, blacksmith-testbox, or cloudflare\n";
const advertisedProviderAliasHelp = [
@@ -3116,6 +3117,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
it("times out hung sanity probes before rejecting the selected binary", () => {
const helpText = "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n";
const result = runWrapper(helpText, ["--version"], {
env: { OPENCLAW_TEST_CRABBOX_METADATA_PROBE_TIMEOUT_MS: "100" },
extraPathEntries: [makeSlowVersionCrabbox(helpText)],
nodePreload: testTimingPreload({ spawnTimeoutMs: 25 }),
});

View File

@@ -593,42 +593,17 @@ describe("script-specific dev tooling hardening", () => {
);
});
it("prints OpenAI realtime smoke help without launching live checks", () => {
it("formats OpenAI realtime smoke help without launching live checks", () => {
expect(realtimeSmokeTesting.parseRealtimeSmokeArgs(["--help"])).toEqual({ help: true });
const result = spawnSync(
process.execPath,
["--import", "tsx", "scripts/dev/realtime-talk-live-smoke.ts", "--help"],
{
cwd: process.cwd(),
encoding: "utf8",
},
);
expect(result.status).toBe(0);
expect(result.stdout).toContain(
expect(realtimeSmokeTesting.usage()).toContain(
"Usage: node --import tsx scripts/dev/realtime-talk-live-smoke.ts",
);
expect(result.stderr).toBe("");
});
it("rejects unknown OpenAI realtime smoke args before launching live checks", () => {
it("rejects unknown OpenAI realtime smoke args before runtime setup", () => {
expect(() => realtimeSmokeTesting.parseRealtimeSmokeArgs(["--wat"])).toThrow(
"Unknown argument: --wat",
);
const result = spawnSync(
process.execPath,
["--import", "tsx", "scripts/dev/realtime-talk-live-smoke.ts", "--wat"],
{
cwd: process.cwd(),
encoding: "utf8",
},
);
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
expect(result.stderr.trim()).toBe("Unknown argument: --wat");
});
it("bounds OpenAI realtime smoke response body reads by content-length", async () => {
@@ -774,7 +749,7 @@ describe("script-specific dev tooling hardening", () => {
...process.env,
CLAUDE_BIN: fakeClaudeBin,
OPENCLAW_PROMPT_TEXT: "timeout cleanup proof",
OPENCLAW_PROMPT_TIMEOUT_MS: "1000",
OPENCLAW_PROMPT_TIMEOUT_MS: "500",
OPENCLAW_PROMPT_TRANSPORT: "direct",
},
stdio: "ignore",
@@ -953,7 +928,7 @@ describe("script-specific dev tooling hardening", () => {
},
},
50,
1_000,
100,
);
expect(stopped).toBe(true);
@@ -1006,7 +981,7 @@ describe("script-specific dev tooling hardening", () => {
`const child = childProcess.spawn(process.execPath, ['--input-type=module', '--eval', ${JSON.stringify(leaderScript)}], { detached: true, stdio: 'ignore' });`,
"let stopPromise;",
"const stopGateway = () => {",
" stopPromise ??= testing.stopGatewayPromptChild(child, { close: async () => {} }, 50, 1000);",
" stopPromise ??= testing.stopGatewayPromptChild(child, { close: async () => {} }, 50, 100);",
" return stopPromise;",
"};",
"testing.installGatewayPromptParentSignalHandlers(child, stopGateway);",

View File

@@ -1,6 +1,14 @@
// Docker All Scheduler tests cover docker all scheduler script behavior.
import { spawn, spawnSync } from "node:child_process";
import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import {
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { setTimeout as delay } from "node:timers/promises";
@@ -15,10 +23,12 @@ import {
LOG_TAIL_MAX_BYTES,
parseDockerAllCliArgs,
resolveDockerPreflightPlatform,
runCleanupSmokePhase,
runShellCaptureCommand,
runShellCommand,
SHELL_CAPTURE_MAX_CHARS,
tailFile,
writeRunSummary,
} from "../../scripts/test-docker-all.mjs";
import { createScriptTestHarness } from "./test-helpers.js";
@@ -196,12 +206,12 @@ describe("scripts/test-docker-all scheduler", () => {
}
});
posixIt("writes Docker run artifacts when cleanup smoke fails", () => {
posixIt("writes Docker run artifacts when cleanup smoke fails", async () => {
const root = mkdtempSync(`${tmpdir()}/openclaw-docker-all-cleanup-`);
const logDir = path.join(root, "logs");
const packageTgz = path.join(root, "openclaw-current.tgz");
const fakePnpm = path.join(root, "pnpm");
writeFileSync(packageTgz, "fake package\n", "utf8");
const phases: Array<Record<string, unknown>> = [];
mkdirSync(logDir, { recursive: true });
writeFileSync(
fakePnpm,
`#!/usr/bin/env node
@@ -217,28 +227,30 @@ process.exit(0);
chmodSync(fakePnpm, 0o755);
try {
const result = spawnSync(process.execPath, ["scripts/test-docker-all.mjs"], {
cwd: process.cwd(),
encoding: "utf8",
env: {
...process.env,
OPENCLAW_CURRENT_PACKAGE_TGZ: packageTgz,
OPENCLAW_DOCKER_ALL_BUILD: "0",
OPENCLAW_DOCKER_ALL_LIVE_MODE: "skip",
OPENCLAW_DOCKER_ALL_LOG_DIR: logDir,
OPENCLAW_DOCKER_ALL_PARALLELISM: "16",
OPENCLAW_DOCKER_ALL_PREFLIGHT: "0",
OPENCLAW_DOCKER_ALL_START_STAGGER_MS: "0",
OPENCLAW_DOCKER_ALL_STATUS_INTERVAL_MS: "0",
OPENCLAW_DOCKER_ALL_TAIL_PARALLELISM: "16",
OPENCLAW_DOCKER_ALL_TIMINGS: "0",
PATH: `${root}${path.delimiter}${process.env.PATH ?? ""}`,
const baseEnv = {
...process.env,
OPENCLAW_DOCKER_E2E_IMAGE: "openclaw-test-image",
PATH: `${root}${path.delimiter}${process.env.PATH ?? ""}`,
};
const cleanupFailure = await runCleanupSmokePhase(baseEnv, logDir, phases);
expect(cleanupFailure).toMatchObject({ name: "cleanup-smoke", status: 42 });
if (!cleanupFailure) {
throw new Error("expected cleanup smoke failure");
}
await writeRunSummary(logDir, {
failures: [cleanupFailure],
image: baseEnv.OPENCLAW_DOCKER_E2E_IMAGE,
images: {
bare: "openclaw-test-bare",
functional: "openclaw-test-image",
},
lanes: [],
phases,
profile: "local",
startedAt: new Date().toISOString(),
status: "failed",
});
expect(result.status).toBe(1);
expect(result.stderr).toContain("cleanup smoke failed intentionally");
const summary = JSON.parse(readFileSync(path.join(logDir, "summary.json"), "utf8"));
expect(summary.status).toBe("failed");
expect(summary.failures).toHaveLength(1);
@@ -560,7 +572,7 @@ setInterval(() => {}, 1000);
env: process.env,
label: "timeout-leader-exits",
timeoutKillGraceMs: 25,
timeoutMs: 1_000,
timeoutMs: 250,
});
await waitFor(() => existsSync(grandchildPidPath));

View File

@@ -552,7 +552,7 @@ shift 2
join(binDir, "docker"),
`#!/bin/sh
printf "captured docker build log\\n"
/bin/sleep 2
/bin/sleep 0.05
`,
);
chmodSync(join(binDir, "docker"), 0o755);
@@ -567,7 +567,8 @@ export OPENCLAW_DOCKER_BUILD_HEARTBEAT_SECONDS=1
source "$ROOT_DIR/scripts/lib/docker-build.sh"
output="$(docker_build_run e2e-build -t demo-image .)"
printf "captured docker build log\\n" >"$TMPDIR/build.log"
output="$(docker_build_maybe_print_heartbeat e2e-build 1 1 "$TMPDIR/build.log")"
[[ "$output" = *"Docker build e2e-build still running ("* ]]
[[ "$output" = *"log bytes captured"* ]]
[[ "$output" != *"captured docker build log"* ]]
@@ -627,7 +628,7 @@ docker_build_run e2e-build -t demo-image .
if (existsSync(filePath)) {
return;
}
await delay(100);
await delay(10);
}
throw new Error(`file was not written: ${filePath}`);
};
@@ -642,7 +643,7 @@ docker_build_run e2e-build -t demo-image .
} catch {
return;
}
await delay(100);
await delay(10);
}
throw new Error(`process stayed alive: ${pid}`);
};
@@ -3099,10 +3100,11 @@ export ROOT_DIR TMPDIR
source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh"
output="$(run_logged_print_heartbeat plugins-run 1 bash -c 'printf "captured container log\\\\n"; /bin/sleep 4')"
printf "captured container log\\n" >"$TMPDIR/run.log"
output="$(docker_e2e_maybe_print_log_heartbeat plugins-run 1 1 "$TMPDIR/run.log")"
[[ "$output" = *"still running plugins-run ("* ]]
[[ "$output" = *"log bytes captured"* ]]
[[ "$output" = *"captured container log"* ]]
[[ "$output" != *"captured container log"* ]]
`;
execFileSync("bash", ["-lc", script], { encoding: "utf8" });
@@ -3121,17 +3123,18 @@ set -euo pipefail
ROOT_DIR=${shellQuote(rootDir)}
TMPDIR=${shellQuote(workDir)}
export ROOT_DIR TMPDIR
export OPENCLAW_DOCKER_E2E_HEARTBEAT_TERM_GRACE_SECONDS=1
source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh"
command_pid_file="$TMPDIR/command.pid"
(
run_logged_print_heartbeat plugins-run 30 bash -c 'printf "%s" "$$" > "$1"; while true; do /bin/sleep 1; done' bash "$command_pid_file"
run_logged_print_heartbeat plugins-run 30 bash -c 'trap "exit 0" TERM; printf "%s" "$$" > "$1"; while true; do /bin/sleep 0.05; done' bash "$command_pid_file"
) &
wrapper_pid="$!"
for _ in $(seq 1 100); do
[ -s "$command_pid_file" ] && break
/bin/sleep 0.1
/bin/sleep 0.01
done
if [ ! -s "$command_pid_file" ]; then
kill -TERM "$wrapper_pid" 2>/dev/null || true
@@ -3140,13 +3143,12 @@ if [ ! -s "$command_pid_file" ]; then
fi
command_pid="$(cat "$command_pid_file")"
kill -TERM "$wrapper_pid"
/bin/sleep 2
for _ in $(seq 1 50); do
if ! kill -0 "$command_pid" 2>/dev/null; then
wait "$wrapper_pid" 2>/dev/null || true
exit 0
fi
/bin/sleep 0.1
/bin/sleep 0.01
done
kill -TERM "$command_pid" 2>/dev/null || true
kill -TERM "$wrapper_pid" 2>/dev/null || true
@@ -3217,8 +3219,10 @@ docker() {
test -n "$cidfile"
printf "container-term\\n" >"$cidfile"
printf "started\\n" >"$TMPDIR/docker-started"
printf "docker running\\n"
while true; do /bin/sleep 10; done
trap 'exit 143' TERM
while true; do /bin/sleep 0.05; done
}
export -f docker
@@ -3227,15 +3231,16 @@ export -f docker
) &
wrapper_pid="$!"
for _ in $(seq 1 50); do
[ -s "$TMPDIR/docker-rm-seen" ] && break
/bin/sleep 0.1
[ -s "$TMPDIR/docker-started" ] && break
/bin/sleep 0.01
kill -0 "$wrapper_pid" 2>/dev/null || true
done
test -s "$TMPDIR/docker-started"
kill -TERM "$wrapper_pid" 2>/dev/null || true
wait "$wrapper_pid" 2>/dev/null || true
for _ in $(seq 1 50); do
grep -qx "container-term" "$TMPDIR/docker-rm-seen" 2>/dev/null && break
/bin/sleep 0.1
/bin/sleep 0.01
done
grep -qx "container-term" "$TMPDIR/docker-rm-seen"
test -z "$(find "$TMPDIR" -maxdepth 1 -name 'openclaw-docker-e2e-container.*' -print)"

View File

@@ -1,21 +0,0 @@
// Docs i18n behavior tests keep JSON fixture edits tied to the Go baseline suite.
import { spawnSync } from "node:child_process";
import { describe, expect, it } from "vitest";
const hasGoToolchain = spawnSync("go", ["version"], { encoding: "utf8" }).status === 0;
describe.skipIf(!hasGoToolchain)("docs-i18n behavior baselines", () => {
it("keeps behavior fixtures passing", () => {
const result = spawnSync(
"go",
["test", "./...", "-run", "TestDocsI18nBehaviorBaselines", "-count=1"],
{
cwd: "scripts/docs-i18n",
encoding: "utf8",
},
);
expect(result.error).toBeUndefined();
expect(result.status, result.stderr || result.stdout).toBe(0);
});
});

View File

@@ -1,17 +1,52 @@
// Docs i18n tests cover the Go module backing docs translation.
// Docs i18n tests cover the Go module and behavior fixtures backing docs translation.
import { spawnSync } from "node:child_process";
import { describe, expect, it } from "vitest";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
const hasGoToolchain = spawnSync("go", ["version"], { encoding: "utf8" }).status === 0;
describe.skipIf(!hasGoToolchain)("docs-i18n Go module", () => {
it("passes Go tests", () => {
const result = spawnSync("go", ["test", "./...", "-count=1"], {
let binaryPath = "";
let tempDir = "";
beforeAll(() => {
const tempRoot = tmpdir() === "/tmp" ? "/var/tmp" : tmpdir();
tempDir = mkdtempSync(path.join(tempRoot, "openclaw-docs-i18n-test-"));
binaryPath = path.join(
tempDir,
process.platform === "win32" ? "docs-i18n.test.exe" : "docs-i18n.test",
);
const result = spawnSync("go", ["test", "-c", "-o", binaryPath, "."], {
cwd: "scripts/docs-i18n",
encoding: "utf8",
});
if (result.error || result.status !== 0) {
throw result.error ?? new Error(result.stderr || result.stdout || "failed to build Go tests");
}
});
afterAll(() => {
if (tempDir) {
rmSync(tempDir, { force: true, recursive: true });
}
});
it.each([
["A-F", "^Test[A-F]"],
["G-L", "^Test[G-L]"],
["M-R", "^Test[M-R]"],
["S-Z", "^Test[S-Z]"],
])("passes Go tests in the %s partition", (_partition, pattern) => {
const result = spawnSync(binaryPath, ["-test.count=1", `-test.run=${pattern}`], {
cwd: "scripts/docs-i18n",
encoding: "utf8",
// The fixture verifies Codex auth never lands under the shared system temp directory.
env: { ...process.env, XDG_CACHE_HOME: path.join(tempDir, "cache") },
});
expect(result.error).toBeUndefined();
expect(result.status, result.stderr || result.stdout).toBe(0);
expect(result.status, [result.stderr, result.stdout].filter(Boolean).join("\n")).toBe(0);
});
});

View File

@@ -872,7 +872,7 @@ NODE
[
"#!/usr/bin/env bash",
'if [[ "$1" == "prefix" && "$2" == "-g" ]]; then',
" sleep 3",
" sleep 2",
" exit 0",
"fi",
'if [[ "$1" == "config" && "$2" == "get" && "$3" == "prefix" ]]; then',
@@ -889,7 +889,7 @@ NODE
const result = runInstallShell(
[`source ${JSON.stringify(SCRIPT_PATH)}`, "npm_global_bin_dir"].join("\n"),
{
OPENCLAW_INSTALL_PROBE_TIMEOUT_SECONDS: "1",
OPENCLAW_INSTALL_PROBE_TIMEOUT_SECONDS: "0.1",
PATH: `${tmp}:${process.env.PATH ?? ""}`,
},
);

View File

@@ -1,59 +1,61 @@
// Issue 78851 profiler CLI tests cover argument handling before work starts.
import { spawnSync } from "node:child_process";
import { describe, expect, it } from "vitest";
function runProfiler(...args: string[]) {
return spawnSync(
process.execPath,
["--import", "tsx", "scripts/perf/issue-78851-model-resolution.ts", ...args],
{
cwd: process.cwd(),
encoding: "utf8",
},
);
}
import {
issue78851ModelResolutionHelpRequested,
issue78851ModelResolutionUsage,
parseIssue78851ModelResolutionOptions,
} from "../../scripts/perf/issue-78851-model-resolution-cli.js";
describe("issue 78851 model resolution profiler CLI", () => {
it("prints help without starting the profiler", () => {
const result = runProfiler("--help");
const usage = issue78851ModelResolutionUsage();
expect(result.status).toBe(0);
expect(result.stdout).toContain("OpenClaw issue #78851 model-resolution profiler");
expect(result.stdout).toContain(
expect(issue78851ModelResolutionHelpRequested(["--help"])).toBe(true);
expect(usage).toContain("OpenClaw issue #78851 model-resolution profiler");
expect(usage).toContain(
"node --import tsx scripts/perf/issue-78851-model-resolution.ts [options]",
);
expect(result.stderr).toBe("");
});
it("rejects unknown arguments before starting the profiler", () => {
const result = runProfiler("--wat");
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
expect(result.stderr.trim()).toBe("Unknown argument: --wat");
expect(() => parseIssue78851ModelResolutionOptions(["--wat"])).toThrow(
"Unknown argument: --wat",
);
});
it("rejects partial numeric arguments before starting the profiler", () => {
const result = runProfiler("--providers", "48junk");
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
expect(result.stderr.trim()).toBe("--providers must be a positive integer");
expect(() => parseIssue78851ModelResolutionOptions(["--providers", "48junk"])).toThrow(
"--providers must be a positive integer",
);
});
it("rejects short flag values before starting the profiler", () => {
const result = runProfiler("--providers", "-h");
expect(() => parseIssue78851ModelResolutionOptions(["--providers", "-h"])).toThrow(
"--providers requires a value",
);
});
it("rejects invalid arguments even when help is also requested", () => {
const result = spawnSync(
process.execPath,
[
"--import",
"tsx",
"scripts/perf/issue-78851-model-resolution.ts",
"--wat",
"--help",
],
{ encoding: "utf8" },
);
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
expect(result.stderr.trim()).toBe("--providers requires a value");
expect(result.stderr).toContain("Unknown argument: --wat");
});
it("rejects duplicate value flags before starting the profiler", () => {
const result = runProfiler("--providers", "48", "--providers", "96");
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
expect(result.stderr.trim()).toBe("--providers was provided more than once");
expect(() =>
parseIssue78851ModelResolutionOptions(["--providers", "48", "--providers", "96"]),
).toThrow("--providers was provided more than once");
});
});

View File

@@ -842,6 +842,7 @@ exit "$status"
const scratchRoot = path.join(parent, "scratch");
const fixtureDir = path.join(scratchRoot, "clawhub-fixture");
const nodeShim = path.join(fakeBin, "node");
const sleepShim = path.join(fakeBin, "sleep");
try {
mkdirSync(fakeBin, { recursive: true });
mkdirSync(fixtureDir, { recursive: true });
@@ -852,11 +853,24 @@ exit "$status"
"printf 'DO_NOT_DUMP_CLAWHUB_PREFIX\\n'",
"head -c 2048 /dev/zero | tr '\\0' x",
"printf '\\nFIXTURE_TAIL_MARKER\\n'",
"sleep 30",
"/bin/sleep 30",
"",
].join("\n"),
);
chmodSync(nodeShim, 0o755);
writeFileSync(
sleepShim,
[
"#!/usr/bin/env bash",
"for _ in $(seq 1 50); do",
' grep -q "FIXTURE_TAIL_MARKER" "$FIXTURE_DIR/clawhub-fixture.log" && exit 0',
" /bin/sleep 0.01",
"done",
"exit 1",
"",
].join("\n"),
);
chmodSync(sleepShim, 0o755);
const result = runSweepShell(
`
@@ -864,7 +878,7 @@ set -euo pipefail
export PATH="$FAKE_BIN:$PATH"
export KITCHEN_SINK_SWEEP_SOURCE_ONLY=1
export KITCHEN_SINK_TMP_DIR="$SCRATCH_ROOT"
export OPENCLAW_CLAWHUB_FIXTURE_WAIT_ATTEMPTS=25
export OPENCLAW_CLAWHUB_FIXTURE_WAIT_ATTEMPTS=1
export OPENCLAW_DOCKER_E2E_LOG_PRINT_BYTES=64
source scripts/e2e/lib/kitchen-sink-plugin/sweep.sh
set +e

View File

@@ -70,6 +70,7 @@ import {
resolveWindowsSystem32Path,
resolveWindowsTaskkillPath,
} from "../../scripts/lib/windows-taskkill.mjs";
import { formatGatewayClientRequestErrorJson } from "../../src/gateway/call.js";
import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js";
const posixIt = process.platform === "win32" ? it.skip : it;
@@ -177,9 +178,7 @@ describe("kitchen-sink RPC isolated state", () => {
it("clamps timer env values before they reach Node timers", () => {
const oversizedTimerMs = String(Number.MAX_SAFE_INTEGER);
expect(readPositiveTimerMs(oversizedTimerMs, 60_000)).toBe(
MAX_KITCHEN_SINK_TIMER_TIMEOUT_MS,
);
expect(readPositiveTimerMs(oversizedTimerMs, 60_000)).toBe(MAX_KITCHEN_SINK_TIMER_TIMEOUT_MS);
const config = resolveKitchenSinkRpcConfig({
OPENCLAW_KITCHEN_SINK_RPC_CALL_MS: oversizedTimerMs,
@@ -764,11 +763,15 @@ setInterval(() => {}, 1000);
"utf8",
);
const runPromise = runCommand(process.execPath, [scriptPath, grandchildPidPath, grandchildReadyPath], {
detached: undefined,
timeoutKillGraceMs: 25,
timeoutMs: 500,
});
const runPromise = runCommand(
process.execPath,
[scriptPath, grandchildPidPath, grandchildReadyPath],
{
detached: undefined,
timeoutKillGraceMs: 25,
timeoutMs: 500,
},
);
const runErrorPromise = runPromise.then(
() => {
throw new Error("expected timed command to reject");
@@ -1057,10 +1060,14 @@ setInterval(() => {}, 1000);
"utf8",
);
const runPromise = runCommand(process.execPath, [scriptPath, grandchildPidPath, grandchildReadyPath], {
timeoutKillGraceMs: 2_000,
timeoutMs: 1_500,
});
const runPromise = runCommand(
process.execPath,
[scriptPath, grandchildPidPath, grandchildReadyPath],
{
timeoutKillGraceMs: 100,
timeoutMs: 100,
},
);
const runErrorPromise = runPromise.then(
() => {
throw new Error("expected timed command to reject");
@@ -1077,7 +1084,7 @@ setInterval(() => {}, 1000);
const runError = await runErrorPromise;
expect(runError).toBeInstanceOf(Error);
expect((runError as Error).message).toContain("timed out after 1500ms");
expect((runError as Error).message).toContain("timed out after 100ms");
await waitFor(() => !isProcessAlive(grandchildPid), 5_000);
} finally {
await runPromise.catch(() => {});
@@ -1138,6 +1145,10 @@ await runCommand(process.execPath, [${JSON.stringify(scriptPath)}], {
try {
runner = spawn(process.execPath, [runnerPath], {
cwd: process.cwd(),
env: {
...process.env,
OPENCLAW_TEST_KITCHEN_SINK_PARENT_SIGNAL_KILL_GRACE_MS: "100",
},
stdio: ["ignore", "ignore", "pipe"],
});
await waitFor(() => existsSync(readyPath) && existsSync(grandchildPidPath));
@@ -1374,7 +1385,6 @@ describe("kitchen-sink RPC command catalog assertions", () => {
});
it("reconstructs typed request failures from gateway CLI JSON", async () => {
const { formatGatewayClientRequestErrorJson } = await import("../../src/gateway/call.js");
const payload = formatGatewayClientRequestErrorJson(
Object.assign(new Error("unauthorized role: operator"), {
name: "GatewayClientRequestError",

View File

@@ -363,20 +363,24 @@ describe("Parallels smoke model selection", () => {
it("rejects short flags as Parallels smoke option values", () => {
const cases = [
[TS_PATHS.linux, "--mode", "-h"],
[TS_PATHS.macos, "--vm", "-h"],
[TS_PATHS.windows, "--model", "-h"],
[TS_PATHS.npmUpdate, "--target-tarball", "-h"],
];
[parseLinuxSmokeArgs, "--mode", "-h"],
[parseMacosSmokeArgs, "--vm", "-h"],
[parseWindowsSmokeArgs, "--model", "-h"],
[parseNpmUpdateSmokeArgs, "--target-tarball", "-h"],
] as const;
const stderr = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
const exit = vi.spyOn(process, "exit").mockImplementation((code) => {
throw new Error(`process.exit(${code})`);
});
for (const [scriptPath, flag, value] of cases) {
const result = spawnNodeEvalSync(
`process.argv = ["node", "${scriptPath}", "${flag}", "${value}"]; await import("./${scriptPath}");`,
{ env: process.env, imports: ["tsx"] },
);
expect(result.status).toBe(1);
expect(result.stderr).toContain(`error: ${flag} requires a value`);
try {
for (const [parseArgs, flag, value] of cases) {
expect(() => parseArgs([flag, value])).toThrow("process.exit(1)");
expect(stderr).toHaveBeenLastCalledWith(`error: ${flag} requires a value\n`);
}
} finally {
exit.mockRestore();
stderr.mockRestore();
}
});
@@ -1567,7 +1571,7 @@ if (isPrlctl) {
READY_FILE: readyFile,
},
quiet: true,
timeoutMs: 1_000,
timeoutMs: 250,
});
expect(result.status).toBe(124);

View File

@@ -145,9 +145,7 @@ describe("plugin gateway gauntlet helpers", () => {
).toThrow("Duplicate --qa-scenario value: channel-chat-baseline");
vi.stubEnv("OPENCLAW_PLUGIN_GATEWAY_GAUNTLET_IDS", "telegram,discord");
expect(() => parseArgs(["--plugin", "telegram"])).toThrow(
"Duplicate --plugin value: telegram",
);
expect(() => parseArgs(["--plugin", "telegram"])).toThrow("Duplicate --plugin value: telegram");
});
it("rejects duplicate single-value controls", () => {
@@ -663,7 +661,7 @@ setInterval(() => {}, 1000);
label: "timeout-leader-exits",
phase: "probe",
timeoutKillGraceMs: 25,
timeoutMs: 1_000,
timeoutMs: 250,
timeMode: "none",
});
@@ -952,8 +950,8 @@ const promise = runMeasuredCommandLive({
)}, ${JSON.stringify(leaderExitedPath)}],
label: "timeout-parent-termination",
phase: "probe",
timeoutKillGraceMs: 1_000,
timeoutMs: 1_000,
timeoutKillGraceMs: 250,
timeoutMs: 200,
timeMode: "none",
});
for (let attempt = 0; attempt < 200 && !fs.existsSync(${JSON.stringify(
@@ -964,7 +962,7 @@ for (let attempt = 0; attempt < 200 && !fs.existsSync(${JSON.stringify(
if (!fs.existsSync(${JSON.stringify(leaderExitedPath)})) {
process.exit(2);
}
await delay(100);
await delay(50);
process.kill(process.pid, "SIGTERM");
await promise;
process.exit(7);
@@ -1369,8 +1367,17 @@ process.exit(7);
expect(result.stdout).toContain("failures=0");
});
it("probes plugin-owned slash help while the plugin is installed", async () => {
const outputDir = path.join(repoRoot, "artifacts");
it.each([
["probes plugin-owned slash help while the plugin is installed", "default", [], 0],
["skips plugin-owned slash help when requested", "skip", ["--skip-slash-help"], 0],
[
"rejects slash-only probes without the install lifecycle",
"slash-only",
["--skip-lifecycle"],
1,
],
] as const)("%s", async (_title, mode, extraArgs, expectedStatus) => {
const outputDir = path.join(repoRoot, `artifacts-${mode}`);
await writeManifest(
"workboard",
"openclaw.plugin.json",
@@ -1424,6 +1431,7 @@ process.exit(7);
outputDir,
"--skip-prebuild",
"--skip-qa",
...extraArgs,
"--plugin",
"workboard",
],
@@ -1433,95 +1441,49 @@ process.exit(7);
},
);
expect(result.status, result.stderr).toBe(0);
expect(result.status, result.stderr).toBe(expectedStatus);
const summary = JSON.parse(
await fs.readFile(path.join(outputDir, "plugin-gateway-gauntlet-summary.json"), "utf8"),
);
expect(summary.failures).toEqual([]);
const slashHelpRow = summary.rows.find(
(row: { label?: string; logPath?: string }) => row.label === "workboard-slash-help:workboard",
);
expect(summary.rows).toEqual(
expect.arrayContaining([
expect.objectContaining({
label: "workboard-slash-help:workboard",
phase: "slash:help",
pluginId: "workboard",
status: 0,
}),
]),
);
const slashHelpLogPath = slashHelpRow?.logPath;
expect(slashHelpLogPath).toEqual(expect.any(String));
await expect(fs.readFile(slashHelpLogPath as string, "utf8")).resolves.toContain(
"Usage: openclaw workboard",
);
if (mode === "default") {
expect(summary.failures).toEqual([]);
const slashHelpRow = summary.rows.find(
(row: { label?: string; logPath?: string }) =>
row.label === "workboard-slash-help:workboard",
);
expect(summary.rows).toEqual(
expect.arrayContaining([
expect.objectContaining({
label: "workboard-slash-help:workboard",
phase: "slash:help",
pluginId: "workboard",
status: 0,
}),
]),
);
const slashHelpLogPath = slashHelpRow?.logPath;
expect(slashHelpLogPath).toEqual(expect.any(String));
await expect(fs.readFile(slashHelpLogPath as string, "utf8")).resolves.toContain(
"Usage: openclaw workboard",
);
return;
}
const skipOutputDir = path.join(repoRoot, "artifacts-skip");
const skipResult = spawnSync(
process.execPath,
[
path.resolve("scripts/check-plugin-gateway-gauntlet.mjs"),
"--repo-root",
repoRoot,
"--output-dir",
skipOutputDir,
"--skip-prebuild",
"--skip-qa",
"--skip-slash-help",
"--plugin",
"workboard",
],
{
cwd: path.resolve("."),
encoding: "utf8",
},
);
if (mode === "skip") {
expect(summary.failures).toEqual([]);
expect(summary.rows).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
phase: "slash:help",
pluginId: "workboard",
}),
]),
);
return;
}
expect(skipResult.status, skipResult.stderr).toBe(0);
const skipSummary = JSON.parse(
await fs.readFile(path.join(skipOutputDir, "plugin-gateway-gauntlet-summary.json"), "utf8"),
);
expect(skipSummary.failures).toEqual([]);
expect(skipSummary.rows).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
phase: "slash:help",
pluginId: "workboard",
}),
]),
);
const slashOnlyOutputDir = path.join(repoRoot, "artifacts-slash-only");
const slashOnlyResult = spawnSync(
process.execPath,
[
path.resolve("scripts/check-plugin-gateway-gauntlet.mjs"),
"--repo-root",
repoRoot,
"--output-dir",
slashOnlyOutputDir,
"--skip-prebuild",
"--skip-lifecycle",
"--skip-qa",
"--plugin",
"workboard",
],
{
cwd: path.resolve("."),
encoding: "utf8",
},
);
expect(slashOnlyResult.status, slashOnlyResult.stderr).toBe(1);
const slashOnlySummary = JSON.parse(
await fs.readFile(
path.join(slashOnlyOutputDir, "plugin-gateway-gauntlet-summary.json"),
"utf8",
),
);
expect(slashOnlySummary.guardFailures).toEqual([]);
expect(slashOnlySummary.failures).toEqual([
expect(summary.guardFailures).toEqual([]);
expect(summary.failures).toEqual([
expect.objectContaining({
label: "workboard-slash-workboard",
phase: "slash:help",

View File

@@ -314,8 +314,8 @@ describe("plugin lifecycle resource sampler", () => {
encoding: "utf8",
env: {
...process.env,
OPENCLAW_PLUGIN_LIFECYCLE_PHASE_TIMEOUT_MS: "1000",
OPENCLAW_PLUGIN_LIFECYCLE_TIMEOUT_KILL_GRACE_MS: "200",
OPENCLAW_PLUGIN_LIFECYCLE_PHASE_TIMEOUT_MS: "250",
OPENCLAW_PLUGIN_LIFECYCLE_TIMEOUT_KILL_GRACE_MS: "50",
PID_FILE: pidFile,
},
timeout: 5000,

View File

@@ -1,7 +1,11 @@
// Plugin Sdk Surface Report tests cover plugin sdk surface report script behavior.
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { beforeAll, describe, expect, it } from "vitest";
import {
collectPluginSdkSurfaceReport,
evaluatePluginSdkSurfaceReport,
readPluginSdkSurfaceBudgets,
} from "../../scripts/plugin-sdk-surface-report.mjs";
const pluginSdkSurfaceBudgetEnvPattern = /^OPENCLAW_PLUGIN_SDK_MAX_/u;
@@ -29,50 +33,30 @@ type PublicSurfaceCounts = {
};
function readDefaultPublicSurfaceBudgets(): PublicSurfaceCounts {
const source = readFileSync("scripts/plugin-sdk-surface-report.mjs", "utf8");
const readFallback = (budgetKey: string) => {
const match = new RegExp(`${budgetKey}:\\s*readBudgetEnv\\(\\s*"[^"]+",\\s*(\\d+)`, "u").exec(
source,
);
if (match === null || match[1] === undefined) {
throw new Error(`failed to read default ${budgetKey} budget`);
}
return Number(match[1]);
};
const { budgets } = readPluginSdkSurfaceBudgets({});
return {
exports: readFallback("publicExports"),
callableExports: readFallback("publicFunctionExports"),
wildcardReexports: readFallback("publicWildcardReexports"),
exports: budgets.publicExports,
callableExports: budgets.publicFunctionExports,
wildcardReexports: budgets.publicWildcardReexports,
};
}
function readCurrentPublicSurfaceCounts(): PublicSurfaceCounts {
const result = runSurfaceReport({});
expect(result.status).toBe(0);
expect(result.stderr).toBe("");
type SurfaceReport = ReturnType<typeof collectPluginSdkSurfaceReport>;
let surfaceReport: SurfaceReport;
const totalsMatch =
/public package SDK entrypoints:[\s\S]*?\n exports: (\d+)\n callable exports: (\d+)/u.exec(
result.stdout,
);
const wildcardsMatch = /public wildcard reexports: (\d+)/u.exec(result.stdout);
if (
totalsMatch === null ||
totalsMatch[1] === undefined ||
totalsMatch[2] === undefined ||
wildcardsMatch === null ||
wildcardsMatch[1] === undefined
) {
throw new Error("failed to read current public surface counts");
}
function readCurrentPublicSurfaceCounts(): PublicSurfaceCounts {
return {
exports: Number(totalsMatch[1]),
callableExports: Number(totalsMatch[2]),
wildcardReexports: Number(wildcardsMatch[1]),
exports: surfaceReport.publicStats.totals.exports,
callableExports: surfaceReport.publicStats.totals.callableExports,
wildcardReexports: surfaceReport.publicWildcards.count,
};
}
describe("plugin SDK surface report", () => {
beforeAll(() => {
surfaceReport = collectPluginSdkSurfaceReport();
});
it("rejects unknown CLI options before collecting SDK stats", () => {
const result = spawnSync(
process.execPath,
@@ -132,26 +116,35 @@ describe("plugin SDK surface report", () => {
});
it("accepts exact deprecated export budget overrides by public entrypoint", () => {
const result = runSurfaceReport({
const budgetConfig = readPluginSdkSurfaceBudgets({
OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_DEPRECATED_EXPORTS_BY_ENTRYPOINT: JSON.stringify({ core: 2 }),
});
expect(result.status).toBe(0);
expect(result.stderr).toBe("");
expect(evaluatePluginSdkSurfaceReport(surfaceReport, budgetConfig)).not.toContain(
expect.stringContaining("public deprecated exports in core"),
);
});
it("keeps default public surface budgets pinned to current source counts", () => {
expect(readDefaultPublicSurfaceBudgets()).toEqual(readCurrentPublicSurfaceCounts());
});
it("ignores ambient CI budget overrides when checking default source counts", () => {
it("keeps generated package declarations out of source surface counts", () => {
const budget = readDefaultPublicSurfaceBudgets().callableExports;
const budgetConfig = readPluginSdkSurfaceBudgets({
OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_FUNCTION_EXPORTS: String(budget - 1),
});
expect(evaluatePluginSdkSurfaceReport(surfaceReport, budgetConfig)).toContain(
`public callable exports ${budget} > ${budget - 1}`,
);
});
it("strips ambient CI budget overrides from CLI checks", () => {
const original = process.env.OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS;
process.env.OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS = "1";
try {
const result = runSurfaceReport({});
expect(result.status).toBe(0);
expect(result.stderr).toBe("");
expect(baseSurfaceReportEnv()).not.toHaveProperty("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS");
} finally {
if (original === undefined) {
delete process.env.OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS;
@@ -161,22 +154,13 @@ describe("plugin SDK surface report", () => {
}
});
it("keeps generated package declarations out of source surface counts", () => {
const budget = readDefaultPublicSurfaceBudgets().callableExports;
const result = runSurfaceReport({
OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_FUNCTION_EXPORTS: String(budget - 1),
});
expect(result.status).toBe(1);
expect(result.stderr).toContain(`public callable exports ${budget} > ${budget - 1}`);
});
it("rejects deprecated export growth by public entrypoint", () => {
const result = runSurfaceReport({
const budgetConfig = readPluginSdkSurfaceBudgets({
OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_DEPRECATED_EXPORTS_BY_ENTRYPOINT: JSON.stringify({ core: 1 }),
});
expect(result.status).toBe(1);
expect(result.stderr).toContain("public deprecated exports in core 2 > 1");
expect(evaluatePluginSdkSurfaceReport(surfaceReport, budgetConfig)).toContain(
"public deprecated exports in core 2 > 1",
);
});
});

View File

@@ -246,6 +246,7 @@ describe("prepare-extension-package-boundary-artifacts", () => {
{
label: "abort-group-prep",
args: ["--eval", parentScript],
abortKillGraceMs: 100,
timeoutMs: 60_000,
},
]);
@@ -305,6 +306,7 @@ describe("prepare-extension-package-boundary-artifacts", () => {
{
label: "abort-group-drain",
args: ["--eval", parentScript],
abortKillGraceMs: 100,
timeoutMs: 60_000,
},
]);
@@ -413,7 +415,7 @@ describe("prepare-extension-package-boundary-artifacts", () => {
].join("\n");
const runnerScript = [
`import { runNodeStep } from ${JSON.stringify(moduleHref)};`,
`await runNodeStep("signal-group-prep", ["--eval", ${JSON.stringify(parentScript)}], 60_000);`,
`await runNodeStep("signal-group-prep", ["--eval", ${JSON.stringify(parentScript)}], 60_000, { abortKillGraceMs: 100 });`,
].join("\n");
const runner = spawn(process.execPath, ["--input-type=module", "--eval", runnerScript], {
stdio: "ignore",

View File

@@ -312,7 +312,8 @@ describe("scripts/profile-extension-memory", () => {
hookPath,
name: "timeout-descendant",
repoRoot: root,
timeoutMs: 1_000,
shutdownGraceMs: 100,
timeoutMs: 250,
});
await waitForCondition(() => existsSync(descendantPidPath));
@@ -371,6 +372,7 @@ describe("scripts/profile-extension-memory", () => {
` hookPath: ${JSON.stringify(hookPath)},`,
" name: 'parent-signal-descendant',",
` repoRoot: ${JSON.stringify(root)},`,
" shutdownGraceMs: 100,",
" timeoutMs: 30000,",
"});",
].join("\n"),

View File

@@ -1,5 +1,4 @@
// QA UX Matrix evidence producer tests cover operator-facing CLI behavior.
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
@@ -7,19 +6,21 @@ import { describe, expect, it, vi } from "vitest";
import {
ensureUxMatrixVideoDependencies,
launchUxMatrixChromium,
runUxMatrixEvidenceProducerCli,
} from "../../scripts/qa/ux-matrix-evidence-producer.js";
const repoRoot = path.resolve(__dirname, "../..");
function runCli(...args: string[]) {
return spawnSync(
process.execPath,
["--import", "tsx", "scripts/qa/ux-matrix-evidence-producer.ts", ...args],
{
cwd: repoRoot,
encoding: "utf8",
},
);
async function runCli(...args: string[]) {
const stdout: string[] = [];
const stderr: string[] = [];
const status = await runUxMatrixEvidenceProducerCli(args, {
error: (message) => stderr.push(message),
log: (message) => stdout.push(message),
});
return {
status,
stderr: stderr.length > 0 ? `${stderr.join("\n")}\n` : "",
stdout: stdout.length > 0 ? `${stdout.join("\n")}\n` : "",
};
}
function expectNoNodeStack(stderr: string) {
@@ -28,8 +29,8 @@ function expectNoNodeStack(stderr: string) {
}
describe("QA UX Matrix evidence producer CLI", () => {
it("prints help without generating evidence", () => {
const result = runCli("--help");
it("prints help without generating evidence", async () => {
const result = await runCli("--help");
expect(result.status).toBe(0);
expect(result.stdout).toContain(
@@ -38,8 +39,8 @@ describe("QA UX Matrix evidence producer CLI", () => {
expect(result.stderr).toBe("");
});
it("prints help after boolean options without consuming valued option slots", () => {
const result = runCli("--skip-visual-proof", "--help");
it("prints help after boolean options without consuming valued option slots", async () => {
const result = await runCli("--skip-visual-proof", "--help");
expect(result.status).toBe(0);
expect(result.stdout).toContain(
@@ -48,8 +49,8 @@ describe("QA UX Matrix evidence producer CLI", () => {
expect(result.stderr).toBe("");
});
it("reports invalid args without a Node stack trace", () => {
const result = runCli("--wat");
it("reports invalid args without a Node stack trace", async () => {
const result = await runCli("--wat");
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
@@ -57,8 +58,8 @@ describe("QA UX Matrix evidence producer CLI", () => {
expectNoNodeStack(result.stderr);
});
it("reports missing valued args without a Node stack trace", () => {
const result = runCli("--artifact-base", "--repo-root", ".");
it("reports missing valued args without a Node stack trace", async () => {
const result = await runCli("--artifact-base", "--repo-root", ".");
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
@@ -66,7 +67,7 @@ describe("QA UX Matrix evidence producer CLI", () => {
expectNoNodeStack(result.stderr);
});
it("reports duplicate evidence producer args without a Node stack trace", () => {
it("reports duplicate evidence producer args without a Node stack trace", async () => {
const duplicateCases = [
["--artifact-base", ["--artifact-base", ".artifacts/a", "--artifact-base", ".artifacts/b"]],
["--repo-root", ["--artifact-base", ".artifacts/a", "--repo-root", ".", "--repo-root", ".."]],
@@ -77,7 +78,7 @@ describe("QA UX Matrix evidence producer CLI", () => {
] satisfies Array<[string, string[]]>;
for (const [flag, args] of duplicateCases) {
const result = runCli(...args);
const result = await runCli(...args);
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
@@ -86,9 +87,14 @@ describe("QA UX Matrix evidence producer CLI", () => {
}
});
it("reports short flag values without treating them as help", () => {
const artifactBaseResult = runCli("--artifact-base", "-h");
const repoRootResult = runCli("--artifact-base", "/tmp/openclaw-ux-test", "--repo-root", "-h");
it("reports short flag values without treating them as help", async () => {
const artifactBaseResult = await runCli("--artifact-base", "-h");
const repoRootResult = await runCli(
"--artifact-base",
"/tmp/openclaw-ux-test",
"--repo-root",
"-h",
);
expect(artifactBaseResult.status).toBe(1);
expect(artifactBaseResult.stdout).toBe("");
@@ -100,11 +106,11 @@ describe("QA UX Matrix evidence producer CLI", () => {
expectNoNodeStack(repoRootResult.stderr);
});
it("sanitizes local checkout paths from generated evidence artifacts", () => {
it("sanitizes local checkout paths from generated evidence artifacts", async () => {
const artifactBase = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-ux-evidence-test-"));
const fakeRepoRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-ux-repo-test-"));
try {
const result = runCli(
const result = await runCli(
"--artifact-base",
artifactBase,
"--repo-root",

View File

@@ -5,7 +5,10 @@ import { createServer, type AddressInfo, type Socket } from "node:net";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { runReleaseUserJourneyAssertion } from "../../scripts/e2e/lib/release-user-journey/assertions.mjs";
import {
runReleaseUserJourneyAssertion,
waitForClickClackSocket,
} from "../../scripts/e2e/lib/release-user-journey/assertions.mjs";
import { withEnvAsync } from "../../src/test-utils/env.js";
const ASSERTIONS_SCRIPT = "scripts/e2e/lib/release-user-journey/assertions.mjs";
@@ -334,13 +337,14 @@ describe("release user journey assertions", () => {
const startedAt = Date.now();
await expect(
withEnvAsync({ HOME: home, OPENCLAW_RELEASE_USER_JOURNEY_HTTP_TIMEOUT_MS: "100" }, () =>
runReleaseUserJourneyAssertion("wait-clickclack-socket", [
`http://127.0.0.1:${server.port}`,
"1",
]),
waitForClickClackSocket({
baseUrl: `http://127.0.0.1:${server.port}`,
pollIntervalMs: 20,
timeoutMs: 150,
}),
),
).rejects.toThrow("Timed out waiting for ClickClack websocket connection");
expect(Date.now() - startedAt).toBeLessThan(2500);
expect(Date.now() - startedAt).toBeLessThan(750);
} finally {
await server.stop();
rmSync(root, { force: true, recursive: true });

View File

@@ -8,7 +8,6 @@ import { pathToFileURL } from "node:url";
import { afterEach, describe, expect, it, vi } from "vitest";
import { resolveWindowsTaskkillPath } from "../../scripts/lib/windows-taskkill.mjs";
import {
ARTIFACT_TARBALL_SCAN_MAX_ENTRIES,
assertExpectedSha256ForTest,
cleanupPackageSourceWorktreeForTest,
cleanPackedOpenClawTarballsForTest,
@@ -565,7 +564,7 @@ describe("resolve-openclaw-package-candidate", () => {
"const fs = require('node:fs');",
"process.on('SIGTERM', () => {",
" fs.writeFileSync(process.env.OPENCLAW_TEST_CHILD_CLEANUP, 'clean');",
" setTimeout(() => process.exit(0), 75);",
" setTimeout(() => process.exit(0), 25);",
"});",
"fs.writeFileSync(process.env.OPENCLAW_TEST_CHILD_READY, 'ready');",
"setInterval(() => {}, 1000);",
@@ -595,16 +594,16 @@ describe("resolve-openclaw-package-candidate", () => {
OPENCLAW_TEST_CHILD_PID: childPidPath,
OPENCLAW_TEST_CHILD_READY: readyPath,
},
killAfterMs: 1000,
timeoutMs: 1000,
killAfterMs: 250,
timeoutMs: 250,
}),
).rejects.toThrow(/timed out after 1000ms/u);
).rejects.toThrow(/timed out after 250ms/u);
await waitForFile(readyPath, 2_000);
await timeoutAssertion;
expect(readFileSync(cleanupPath, "utf8")).toBe("clean");
expect(Date.now() - startedAt).toBeLessThan(1_700);
expect(Date.now() - startedAt).toBeLessThan(900);
});
it("forwards external termination to package runner process groups", async () => {
@@ -1353,22 +1352,14 @@ describe("resolve-openclaw-package-candidate", () => {
it("rejects source artifact scans that exceed the filesystem entry limit", async () => {
const dir = await mkdtemp(path.join(tmpdir(), "openclaw-package-artifact-scan-"));
tempDirs.push(dir);
const maxEntries = 3;
// Keep the real 10,001-entry proof while avoiding serial filesystem setup.
const indexes = Array.from(
{ length: ARTIFACT_TARBALL_SCAN_MAX_ENTRIES + 1 },
(_, index) => index,
);
for (let start = 0; start < indexes.length; start += 128) {
await Promise.all(
indexes
.slice(start, start + 128)
.map((index) => writeFile(path.join(dir, `not-a-package-${index}.txt`), "x")),
);
for (let index = 0; index <= maxEntries; index += 1) {
await writeFile(path.join(dir, `not-a-package-${index}.txt`), "x");
}
await expect(findSingleTarballForTest(dir)).rejects.toThrow(
`source=artifact scan exceeded ${ARTIFACT_TARBALL_SCAN_MAX_ENTRIES} filesystem entries`,
await expect(findSingleTarballForTest(dir, maxEntries)).rejects.toThrow(
`source=artifact scan exceeded ${maxEntries} filesystem entries`,
);
});

View File

@@ -310,7 +310,7 @@ describe("run-oxlint", () => {
CHILD_PID_PATH: childPidPath,
OPENCLAW_OXLINT_SHARD_HEARTBEAT_MS: "0",
OPENCLAW_OXLINT_SHARD_KILL_GRACE_MS: "25",
OPENCLAW_OXLINT_SHARD_TIMEOUT_MS: "1000",
OPENCLAW_OXLINT_SHARD_TIMEOUT_MS: "250",
},
extraArgs: [],
runner,

View File

@@ -5,6 +5,7 @@ import fs from "node:fs";
import os from "node:os";
import nodePath from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { pathToFileURL } from "node:url";
import { describe, expect, it, vi } from "vitest";
import {
DEFAULT_EXTRA_LONG_RUNNING_VITEST_NO_OUTPUT_TIMEOUT_MS,
@@ -611,9 +612,8 @@ describe("scripts/run-vitest", () => {
posixIt("cleans delegated test-project children when the wrapper is signaled", async () => {
const fixturePath = nodePath.join(
"test",
"scripts",
`run-vitest-delegated-signal-${process.pid}-${Date.now()}.test.ts`,
os.tmpdir(),
`openclaw-run-vitest-delegated-signal-${process.pid}-${Date.now()}.mjs`,
);
const childPidPath = nodePath.join(
os.tmpdir(),
@@ -629,20 +629,23 @@ describe("scripts/run-vitest", () => {
[
'import { spawn } from "node:child_process";',
'import fs from "node:fs";',
'import { it } from "vitest";',
'it("waits for wrapper termination", async () => {',
' const child = spawn(process.execPath, ["-e", "process.on(\\\'SIGTERM\\\', () => {}); setInterval(() => {}, 1000);"], { stdio: "ignore" });',
" fs.writeFileSync(process.env.OPENCLAW_DELEGATED_SIGNAL_CHILD_PID!, String(process.pid));",
" fs.writeFileSync(process.env.OPENCLAW_DELEGATED_SIGNAL_DESCENDANT_PID!, String(child.pid));",
" await new Promise(() => {});",
"});",
'const child = spawn(process.execPath, ["-e", "process.on(\\\'SIGTERM\\\', () => {}); setInterval(() => {}, 1000);"], { stdio: "ignore" });',
"fs.writeFileSync(process.env.OPENCLAW_DELEGATED_SIGNAL_CHILD_PID, String(process.pid));",
"fs.writeFileSync(process.env.OPENCLAW_DELEGATED_SIGNAL_DESCENDANT_PID, String(child.pid));",
"await new Promise(() => {});",
"",
].join("\n"),
);
const runner = spawn(
process.execPath,
["scripts/run-vitest.mjs", fixturePath, "--reporter=verbose"],
[
"--input-type=module",
"--eval",
`import { runTestProjectsDelegation } from ${JSON.stringify(
pathToFileURL(nodePath.resolve("scripts/run-vitest.mjs")).href,
)}; runTestProjectsDelegation([], process.env, { runnerPath: ${JSON.stringify(fixturePath)} });`,
],
{
env: {
...process.env,

View File

@@ -494,7 +494,7 @@ describe("secret provider integration proof harness", () => {
OPENCLAW_ENTRY: fakeOpenClaw,
},
},
{ timeoutKillGraceMs: 50, timeoutMs: 2_000 },
{ timeoutKillGraceMs: 50, timeoutMs: 500 },
);
result.catch(() => {});
await waitFor(() => fs.existsSync(readyPath));
@@ -756,6 +756,7 @@ describe("secret provider integration proof harness", () => {
await expect(
proof.runCommand(process.execPath, [scriptPath], {
timeoutKillGraceMs: 50,
timeoutMs: 150,
}),
).rejects.toThrow(/command timed out/u);
@@ -935,7 +936,7 @@ describe("secret provider integration proof harness", () => {
`const proof = await import(${JSON.stringify(
`${pathToFileURL(proofScriptPath).href}?case=parent-signal-${Date.now()}`,
)});`,
`await proof.runCommand(process.execPath, [${JSON.stringify(scriptPath)}], { timeoutMs: 30_000 });`,
`await proof.runCommand(process.execPath, [${JSON.stringify(scriptPath)}], { timeoutKillGraceMs: 50, timeoutMs: 30_000 });`,
"",
].join("\n"),
);

View File

@@ -131,18 +131,22 @@ describe("telegram user Crabbox proof log polling", () => {
).toBe(4096);
});
it("rejects loose and out-of-range proof ports before remote setup", () => {
const looseGatewayPort = runProofCli(["--gateway-port", "1e3", "--dry-run"]);
expect(looseGatewayPort.status).toBe(1);
expect(looseGatewayPort.stderr).toContain("--gateway-port must be a positive integer.");
const highGatewayPort = runProofCli(["--gateway-port", "65536", "--dry-run"]);
expect(highGatewayPort.status).toBe(1);
expect(highGatewayPort.stderr).toContain("--gateway-port must be a TCP port from 1 to 65535.");
const highMockPort = runProofCli(["--mock-port", "65536", "--dry-run"]);
expect(highMockPort.status).toBe(1);
expect(highMockPort.stderr).toContain("--mock-port must be a TCP port from 1 to 65535.");
it.each([
["loose gateway", "--gateway-port", "1e3", "--gateway-port must be a positive integer."],
[
"out-of-range gateway",
"--gateway-port",
"65536",
"--gateway-port must be a TCP port from 1 to 65535.",
],
[
"out-of-range mock",
"--mock-port",
"65536",
"--mock-port must be a TCP port from 1 to 65535.",
],
])("rejects %s proof ports before remote setup", (_label, flag, value, message) => {
expect(() => parseArgs([flag, value, "--dry-run"])).toThrow(message);
});
it("rejects short flags as proof option values before dry-run planning", () => {
@@ -846,7 +850,7 @@ fs.writeFileSync(${JSON.stringify(recorderExitPath)}, "exited");
startDelayMs: 0,
target: "linux",
}),
delay(2_000).then(() => {
delay(500).then(() => {
throw new Error("recordProbeVideo hung after the recorder had already exited");
}),
]),

View File

@@ -373,8 +373,8 @@ setInterval(() => {}, 1000);
const startedAt = Date.now();
const runPromise = runCommand(process.execPath, ["-e", parentScript], dir, {
timeoutKillGraceMs: 1_000,
timeoutMs: 1_000,
timeoutKillGraceMs: 250,
timeoutMs: 300,
});
const runError = runPromise.catch((error: unknown) => error);
await waitForFile(readyPath, 2_000);
@@ -382,11 +382,11 @@ setInterval(() => {}, 1000);
await expect(runError).resolves.toMatchObject({
code: "ETIMEDOUT",
message: expect.stringContaining("timed out after 1000ms"),
message: expect.stringContaining("timed out after 300ms"),
});
expect(readFileSync(cleanupPath, "utf8")).toBe("clean");
expect(Date.now() - startedAt).toBeLessThan(1_700);
expect(Date.now() - startedAt).toBeLessThan(800);
} finally {
if (childPid !== undefined && isProcessAlive(childPid)) {
process.kill(childPid, "SIGKILL");

View File

@@ -18,8 +18,10 @@ import {
resolveFullSuiteVitestEnv,
resolveReportArtifactDirs,
resolveReportRunSpecs,
resolveReportVitestArgs,
resolveRunPlanConcurrency,
resolveRunPlans,
runReportPlans,
signalTestGroupReportChild,
spawnText,
} from "../../scripts/test-group-report.mjs";
@@ -249,7 +251,7 @@ describe("scripts/test-group-report aggregation", () => {
}
});
it("fails allow-failures runs that produce no JSON report", () => {
it("fails when every allow-failures run produces no JSON report", () => {
const tempDir = makeTempDir();
const missingConfig = path.join(tempDir, "missing-vitest.config.ts");
const output = path.join(tempDir, "group-report.json");
@@ -280,6 +282,125 @@ describe("scripts/test-group-report aggregation", () => {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it("continues allow-failures profiling after a config exits without JSON", async () => {
const tempDir = makeTempDir();
const reportDir = path.join(tempDir, "reports");
const calls: string[] = [];
try {
const result = await runReportPlans({
args: parseTestGroupReportArgs([
"--config",
"failed.config.ts",
"--config",
"passed.config.ts",
"--allow-failures",
"--no-rss",
]),
logDir: path.join(tempDir, "logs"),
reportDir,
runPlans: [
{ config: "failed.config.ts", forwardedArgs: [], label: "failed" },
{ config: "passed.config.ts", forwardedArgs: [], label: "passed" },
],
runVitestJsonReport: async (params: {
config: string;
label: string;
logPath: string;
reportPath: string;
}) => {
calls.push(params.label);
if (params.label === "passed") {
fs.mkdirSync(path.dirname(params.reportPath), { recursive: true });
fs.writeFileSync(
params.reportPath,
`${JSON.stringify({ testResults: [{ name: "passed.test.ts" }] })}\n`,
"utf8",
);
}
return {
config: params.config,
elapsedMs: 10,
label: params.label,
logPath: params.logPath,
maxRssBytes: null,
reportPath: params.reportPath,
status: params.label === "failed" ? 1 : 0,
};
},
});
expect(calls).toStrictEqual(["failed", "passed"]);
expect(result.failed).toBe(true);
expect(result.exitCode).toBe(0);
expect(result.runs.map((run) => [run.label, run.status])).toStrictEqual([
["failed", 1],
["passed", 0],
]);
expect(result.runEntries.map((entry) => entry.config)).toStrictEqual(["passed"]);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it("prints slow tests as soon as each config report completes", async () => {
const tempDir = makeTempDir();
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
try {
await runReportPlans({
args: parseTestGroupReportArgs([
"--config",
"slow.config.ts",
"--max-test-ms",
"1000",
"--no-rss",
]),
logDir: path.join(tempDir, "logs"),
reportDir: path.join(tempDir, "reports"),
runPlans: [{ config: "slow.config.ts", forwardedArgs: [], label: "slow" }],
runVitestJsonReport: async (params: {
config: string;
label: string;
logPath: string;
reportPath: string;
}) => {
fs.mkdirSync(path.dirname(params.reportPath), { recursive: true });
fs.writeFileSync(
params.reportPath,
`${JSON.stringify({
testResults: [
{
name: path.join(process.cwd(), "src", "slow.test.ts"),
assertionResults: [
{ duration: 1250, fullName: "finishes eventually", status: "passed" },
],
},
],
})}\n`,
"utf8",
);
return {
config: params.config,
elapsedMs: 10,
label: params.label,
logPath: params.logPath,
maxRssBytes: null,
reportPath: params.reportPath,
status: 0,
};
},
});
expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining(
"slow-test config=slow duration=1250.0ms file=src/slow.test.ts name=finishes eventually",
),
);
} finally {
logSpy.mockRestore();
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
});
describe("scripts/test-group-report comparison", () => {
@@ -627,9 +748,10 @@ describe("scripts/test-group-report arg parsing", () => {
["--kill-grace-ms", ["100", "200"]],
["--concurrency", ["2", "3"]],
]) {
const args = flag === "--compare"
? [flag, values[0], values[1], flag, values[2], values[3]]
: [flag, values[0], flag, values[1]];
const args =
flag === "--compare"
? [flag, values[0], values[1], flag, values[2], values[3]]
: [flag, values[0], flag, values[1]];
expect(() => parseTestGroupReportArgs(args)).toThrow(`${flag} was provided more than once`);
}
expect(parseTestGroupReportArgs(["--config", "a.ts", "--config", "b.ts"]).configs).toEqual([
@@ -920,7 +1042,7 @@ describe("scripts/test-group-report child process guard", () => {
" setTimeout(() => {",
` fs.writeFileSync(${JSON.stringify(cleanupPath)}, "clean");`,
" process.exit(0);",
" }, 75);",
" }, 25);",
"});",
`fs.writeFileSync(${JSON.stringify(readyPath)}, "ready");`,
"setInterval(() => {}, 1000);",
@@ -936,8 +1058,8 @@ describe("scripts/test-group-report child process guard", () => {
const runPromise = spawnText(process.execPath, ["--eval", parentScript], {
cwd: process.cwd(),
env: process.env,
killGraceMs: 1_000,
timeoutMs: 1_000,
killGraceMs: 250,
timeoutMs: 250,
});
await waitForFile(readyPath, 2_000);
@@ -950,7 +1072,7 @@ describe("scripts/test-group-report child process guard", () => {
timedOut: true,
});
expect(fs.readFileSync(cleanupPath, "utf8")).toBe("clean");
expect(Date.now() - startedAt).toBeLessThan(1_700);
expect(Date.now() - startedAt).toBeLessThan(900);
await waitForDead(childPid, 2_000);
} finally {
if (childPid !== undefined && isProcessAlive(childPid)) {
@@ -1057,6 +1179,25 @@ describe("scripts/test-group-report child process guard", () => {
});
describe("scripts/test-group-report run plans", () => {
it("isolates full-suite duration reports by default", () => {
expect(resolveReportVitestArgs(parseTestGroupReportArgs(["--full-suite"]))).toEqual([
"--isolate=true",
]);
expect(
resolveReportVitestArgs(parseTestGroupReportArgs(["--full-suite", "--", "--maxWorkers=1"])),
).toEqual(["--maxWorkers=1", "--isolate=true"]);
});
it("preserves explicit full-suite isolation choices and explicit-config defaults", () => {
expect(
resolveReportVitestArgs(parseTestGroupReportArgs(["--full-suite", "--", "--no-isolate"])),
).toEqual(["--no-isolate"]);
expect(
resolveReportVitestArgs(parseTestGroupReportArgs(["--full-suite", "--", "--isolate=false"])),
).toEqual(["--isolate=false"]);
expect(resolveReportVitestArgs(parseTestGroupReportArgs(["--config", "a.ts"]))).toEqual([]);
});
it("caps Vitest workers for full-suite profiling by default", () => {
expect(resolveFullSuiteVitestEnv(parseTestGroupReportArgs(["--full-suite"]), {})).toEqual({
OPENCLAW_VITEST_MAX_WORKERS: "2",
@@ -1113,6 +1254,7 @@ describe("scripts/test-group-report run plans", () => {
path.join("/repo", "node_modules", ".experimental-vitest-cache", "0-a.ts"),
path.join("/repo", "node_modules", ".experimental-vitest-cache", "1-b.ts"),
]);
expect(specs.map((spec) => spec.vitestArgs)).toEqual([[], []]);
});
it("uses leaf configs for full-suite profiling without requiring parallel env", () => {

View File

@@ -1163,7 +1163,7 @@ describe("scripts/test-projects changed-target routing", () => {
]) {
expect(resolveChangedTestTargetPlan([fixturePath]), fixturePath).toEqual({
mode: "targets",
targets: ["test/scripts/docs-i18n-behavior.test.ts"],
targets: ["test/scripts/docs-i18n.test.ts"],
});
}
});

View File

@@ -796,7 +796,7 @@ describe("runTsdownBuildInvocation", () => {
async () => {
const rootDir = createTempDir("openclaw-tsdown-timeout-");
const childPidPath = path.join(rootDir, "child.pid");
const timeoutMs = 1_000;
const timeoutMs = 250;
let childPid: number | undefined;
const childScript = "process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);";
const parentScript = [
@@ -860,7 +860,7 @@ describe("runTsdownBuildInvocation", () => {
" setTimeout(() => {",
` fs.writeFileSync(${JSON.stringify(cleanupPath)}, 'clean');`,
" process.exit(0);",
" }, 75);",
" }, 50);",
"});",
`fs.writeFileSync(${JSON.stringify(readyPath)}, 'ready');`,
"setInterval(() => {}, 1000);",
@@ -892,7 +892,7 @@ describe("runTsdownBuildInvocation", () => {
env: {
...process.env,
OPENCLAW_TSDOWN_HEARTBEAT_MS: "0",
OPENCLAW_TSDOWN_TIMEOUT_MS: "1000",
OPENCLAW_TSDOWN_TIMEOUT_MS: "250",
},
},
);
@@ -903,7 +903,7 @@ describe("runTsdownBuildInvocation", () => {
expect(result.timedOut).toBe(true);
expect(fs.readFileSync(cleanupPath, "utf8")).toBe("clean");
expect(Date.now() - startedAt).toBeLessThan(1_700);
expect(Date.now() - startedAt).toBeLessThan(900);
await waitForDead(childPid, 2_000);
} finally {
if (childPid && isProcessAlive(childPid)) {

View File

@@ -296,7 +296,7 @@ describe("scripts/ui windows spawn behavior", () => {
await waitFor(() => fs.existsSync(descendantPidFile), "UI runner descendant readiness");
descendantPid = Number(fs.readFileSync(descendantPidFile, "utf8"));
await new Promise<void>((resolve) => {
setTimeout(resolve, 300);
setTimeout(resolve, 25);
});
wrapper.kill("SIGTERM");

View File

@@ -315,7 +315,9 @@ describe("scripts/e2e/lib/upgrade-survivor/probe-gateway.mjs", () => {
"--out",
out,
"--timeout-ms",
"1000",
"200",
"--attempt-timeout-ms",
"100",
],
5_000,
{ OPENCLAW_UPGRADE_SURVIVOR_PROBE_MAX_BODY_BYTES: "64" },
@@ -325,7 +327,7 @@ describe("scripts/e2e/lib/upgrade-survivor/probe-gateway.mjs", () => {
expect(result.status).not.toBe(0);
expect(result.stderr).toContain(`${baseUrl}/healthz probe body exceeded 64 bytes`);
expect(fs.existsSync(out)).toBe(false);
expect(Date.now() - startedAt).toBeLessThan(3_500);
expect(Date.now() - startedAt).toBeLessThan(750);
} finally {
server.close();
}