mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-11 03:26:05 +00:00
test: speed up and stabilize full suite
This commit is contained in:
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }),
|
||||
});
|
||||
|
||||
@@ -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);",
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 ?? ""}`,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
);
|
||||
|
||||
@@ -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");
|
||||
}),
|
||||
]),
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user