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
+25 -13
View File
@@ -455,7 +455,14 @@ async function getFreePort(): Promise<number> {
});
}
async function runDirectPrompt(prompt: string): Promise<PromptResult> {
async function runDirectPrompt(
prompt: string,
options: {
claudeBin?: string;
shutdownWaitMs?: number;
timeoutMs?: number;
} = {},
): Promise<PromptResult> {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-direct-prompt-probe-"));
const proxyPort = ENABLE_CAPTURE ? await getFreePort() : undefined;
const proxy =
@@ -466,17 +473,21 @@ async function runDirectPrompt(prompt: string): Promise<PromptResult> {
try {
const stdout: string[] = [];
const stderr: string[] = [];
const child = spawn(CLAUDE_BIN, [...DIRECT_CLAUDE_ARGS, prompt, USER_PROMPT], {
cwd: process.cwd(),
env: {
...process.env,
...(proxyPort ? { ANTHROPIC_BASE_URL: `http://127.0.0.1:${proxyPort}` } : {}),
ANTHROPIC_API_KEY: "",
ANTHROPIC_API_KEY_OLD: "",
const child = spawn(
options.claudeBin ?? CLAUDE_BIN,
[...DIRECT_CLAUDE_ARGS, prompt, USER_PROMPT],
{
cwd: process.cwd(),
env: {
...process.env,
...(proxyPort ? { ANTHROPIC_BASE_URL: `http://127.0.0.1:${proxyPort}` } : {}),
ANTHROPIC_API_KEY: "",
ANTHROPIC_API_KEY_OLD: "",
},
detached: process.platform !== "win32",
stdio: ["ignore", "pipe", "pipe"],
},
detached: process.platform !== "win32",
stdio: ["ignore", "pipe", "pipe"],
});
);
child.stdout.on("data", (chunk) => stdout.push(String(chunk)));
child.stderr.on("data", (chunk) => stderr.push(String(chunk)));
const exitPromise = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(
@@ -490,7 +501,7 @@ async function runDirectPrompt(prompt: string): Promise<PromptResult> {
await waitForGatewayPromptChildTreeExit(
child,
exitPromise.then(() => undefined),
1_500,
options.shutdownWaitMs ?? 1_500,
);
};
const removeParentSignalHandlers = installGatewayPromptParentSignalHandlers(
@@ -505,7 +516,7 @@ async function runDirectPrompt(prompt: string): Promise<PromptResult> {
void stopDirectChild("SIGKILL").finally(() => {
resolve({ code: null, signal: "SIGKILL" });
});
}, TIMEOUT_MS);
}, options.timeoutMs ?? TIMEOUT_MS);
}),
]).finally(() => {
if (timeoutTimer) {
@@ -963,6 +974,7 @@ export const testing = {
readLogTail,
readRequestBody,
resolveAnthropicUpstreamUrl,
runDirectPrompt,
stopGatewayPromptChild,
summarizeCapture,
summarizeText,
+11 -1
View File
@@ -105,10 +105,20 @@ type CliOptions = {
const DEFAULT_RUNS = 5;
const DEFAULT_WARMUP = 1;
const DEFAULT_TIMEOUT_MS = 30_000;
const TIMEOUT_KILL_GRACE_MS = 1_000;
const DEFAULT_TIMEOUT_KILL_GRACE_MS = 1_000;
const TIMEOUT_KILL_GRACE_MS = resolveTimeoutKillGraceMs(process.env);
const PROCESS_GROUP_EXIT_POLL_MS = 25;
const DEFAULT_ENTRY = "openclaw.mjs";
const MAX_RSS_MARKER = "__OPENCLAW_MAX_RSS_KB__=";
function resolveTimeoutKillGraceMs(env: NodeJS.ProcessEnv): number {
const raw = env.VITEST ? env.OPENCLAW_TEST_CLI_STARTUP_TIMEOUT_KILL_GRACE_MS : undefined;
if (!raw || !/^\d+$/u.test(raw)) {
return DEFAULT_TIMEOUT_KILL_GRACE_MS;
}
const parsed = Number(raw);
return Number.isSafeInteger(parsed) ? parsed : DEFAULT_TIMEOUT_KILL_GRACE_MS;
}
const VALUE_FLAGS = new Set([
"--case",
"--compare-baseline",
+19 -2
View File
@@ -22,6 +22,7 @@ import { fileURLToPath } from "node:url";
import { resolvePathEnvKey, resolveWindowsCmdExePath } from "./windows-cmd-helpers.mjs";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const CRABBOX_METADATA_PROBE_TIMEOUT_MS = 5_000;
const ignoreRepoBinary = process.env.OPENCLAW_CRABBOX_WRAPPER_IGNORE_REPO_BINARY === "1";
const repoLocal = ignoreRepoBinary ? null : resolveCrabboxBinary(process.env, process.platform);
const pathLocal = resolvePathBinary("crabbox", process.env, process.platform);
@@ -361,7 +362,7 @@ function checkedOutput(command, commandArgs) {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
timeout: 5_000,
timeout: resolveMetadataProbeTimeoutMs(process.env),
killSignal: "SIGKILL",
});
const timedOut = result.error?.name === "Error" && result.signal === "SIGKILL";
@@ -3467,7 +3468,7 @@ const child = spawn(childInvocation.command, childInvocation.args, {
env: childEnv,
windowsVerbatimArguments: childInvocation.windowsVerbatimArguments,
});
const childKillGraceMs = 5_000;
const childKillGraceMs = resolveChildKillGraceMs(process.env);
let childForceKillTimer;
let childTreeShutdownStarted = false;
if (fullCheckout) {
@@ -3604,3 +3605,19 @@ async function waitForChildTreeExit(childProcess, timeoutMs) {
}
return !childProcessTreeIsAlive(childProcess);
}
function resolveChildKillGraceMs(env) {
if (!env.VITEST || !env.OPENCLAW_TEST_CRABBOX_CHILD_KILL_GRACE_MS) {
return 5_000;
}
const value = Number.parseInt(env.OPENCLAW_TEST_CRABBOX_CHILD_KILL_GRACE_MS, 10);
return Number.isFinite(value) && value >= 0 ? value : 5_000;
}
function resolveMetadataProbeTimeoutMs(env) {
if (!env.VITEST || !env.OPENCLAW_TEST_CRABBOX_METADATA_PROBE_TIMEOUT_MS) {
return CRABBOX_METADATA_PROBE_TIMEOUT_MS;
}
const value = Number.parseInt(env.OPENCLAW_TEST_CRABBOX_METADATA_PROBE_TIMEOUT_MS, 10);
return Number.isFinite(value) && value > 0 ? value : CRABBOX_METADATA_PROBE_TIMEOUT_MS;
}
+12 -2
View File
@@ -551,14 +551,15 @@ async function shutdownActiveCommands(signal) {
return commandShutdownPromise;
}
const children = [...activeCommandChildren];
const killGraceMs = resolveCommandParentSignalKillGraceMs(process.env);
for (const child of children) {
signalProcessGroup(child, signal);
}
commandShutdownPromise = Promise.all(
children.map((child) =>
finishTimedOutCommandProcessTree(child, {
forceKillAt: Date.now() + COMMAND_PARENT_SIGNAL_KILL_GRACE_MS,
timeoutKillGraceMs: COMMAND_PARENT_SIGNAL_KILL_GRACE_MS,
forceKillAt: Date.now() + killGraceMs,
timeoutKillGraceMs: killGraceMs,
}),
),
).finally(() => {
@@ -568,6 +569,15 @@ async function shutdownActiveCommands(signal) {
return commandShutdownPromise;
}
function resolveCommandParentSignalKillGraceMs(env) {
const raw = env.VITEST && env.OPENCLAW_TEST_KITCHEN_SINK_PARENT_SIGNAL_KILL_GRACE_MS;
if (!raw) {
return COMMAND_PARENT_SIGNAL_KILL_GRACE_MS;
}
const value = Number.parseInt(raw, 10);
return Number.isFinite(value) && value >= 0 ? value : COMMAND_PARENT_SIGNAL_KILL_GRACE_MS;
}
async function waitForCommandProcessTreeExit(child, timeoutMs) {
const deadlineAt = Date.now() + timeoutMs;
while (Date.now() < deadlineAt) {
@@ -309,7 +309,11 @@ async function waitClickClackSocket() {
30,
"ClickClack websocket timeout seconds",
);
const deadline = Date.now() + timeoutSeconds * 1000;
await waitForClickClackSocket({ baseUrl, timeoutMs: timeoutSeconds * 1000 });
}
export async function waitForClickClackSocket({ baseUrl, timeoutMs, pollIntervalMs = 250 }) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const remainingMs = Math.max(1, deadline - Date.now());
const state = await withClickClackFixtureResponse(
@@ -329,7 +333,7 @@ async function waitClickClackSocket() {
}
}
await new Promise((resolve) => {
setTimeout(resolve, 250);
setTimeout(resolve, Math.min(pollIntervalMs, Math.max(0, deadline - Date.now())));
});
}
throw new Error(`Timed out waiting for ClickClack websocket connection at ${baseUrl}`);
+7 -10
View File
@@ -104,10 +104,7 @@ function readPositiveInt(raw, fallback, label) {
function clampSecretProofTimerTimeoutMs(valueMs) {
const value = Number.isFinite(valueMs) ? valueMs : 1;
return Math.min(
Math.max(1, Math.floor(value)),
MAX_SECRET_PROOF_TIMER_TIMEOUT_MS,
);
return Math.min(Math.max(1, Math.floor(value)), MAX_SECRET_PROOF_TIMER_TIMEOUT_MS);
}
function readPositiveTimerMs(raw, fallback, label) {
@@ -361,6 +358,9 @@ async function cleanupEnv(root, options = {}) {
function runCommand(command, args, options = {}) {
const timeoutMs = clampSecretProofTimerTimeoutMs(options.timeoutMs ?? COMMAND_TIMEOUT_MS);
const timeoutKillGraceMs = clampSecretProofTimerTimeoutMs(
options.timeoutKillGraceMs ?? COMMAND_TIMEOUT_KILL_GRACE_MS,
);
return new Promise((resolve, reject) => {
const usesProcessGroup = options.detached ?? process.platform !== "win32";
const child = childProcess.spawn(command, args, {
@@ -379,11 +379,8 @@ function runCommand(command, args, options = {}) {
let killTimer;
let forceKillAt;
const armForceKill = () => {
forceKillAt ??= Date.now() + COMMAND_TIMEOUT_KILL_GRACE_MS;
killTimer ??= setTimeout(
() => terminateProcessTree(child, "SIGKILL"),
COMMAND_TIMEOUT_KILL_GRACE_MS,
);
forceKillAt ??= Date.now() + timeoutKillGraceMs;
killTimer ??= setTimeout(() => terminateProcessTree(child, "SIGKILL"), timeoutKillGraceMs);
killTimer.unref();
};
const abort = () => {
@@ -421,7 +418,7 @@ function runCommand(command, args, options = {}) {
const finishTerminatedTree = async () => {
await finishTimedOutCommandProcessTree(child, {
forceKillAt,
timeoutKillGraceMs: COMMAND_TIMEOUT_KILL_GRACE_MS,
timeoutKillGraceMs,
});
if (killTimer) {
clearTimeout(killTimer);
+20 -8
View File
@@ -118,6 +118,22 @@ docker_build_run_command() {
"$@"
}
docker_build_maybe_print_heartbeat() {
local label="$1"
local elapsed_seconds="$2"
local next_heartbeat="$3"
local log_file="$4"
if [ "$elapsed_seconds" -lt "$next_heartbeat" ]; then
return 1
fi
local log_bytes="0"
if [ -f "$log_file" ]; then
log_bytes="$(wc -c <"$log_file" 2>/dev/null || echo 0)"
log_bytes="${log_bytes//[[:space:]]/}"
fi
echo "Docker build $label still running (${elapsed_seconds}s elapsed, ${log_bytes} log bytes captured)..."
}
docker_build_run_logged() {
local label="$1"
local timeout_value="$2"
@@ -195,18 +211,14 @@ docker_build_run_logged() {
docker_build_run_command "$timeout_value" "$@" >"$log_file" 2>&1 &
build_pid="$!"
while kill -0 "$build_pid" 2>/dev/null; do
/bin/sleep 1 &
# Poll promptly so short builds do not pay a one-second wrapper tax.
/bin/sleep 0.1 &
heartbeat_sleep_pid="$!"
wait "$heartbeat_sleep_pid" 2>/dev/null || true
heartbeat_sleep_pid=""
local elapsed_seconds=$((SECONDS - started_at))
if [ "$elapsed_seconds" -ge "$next_heartbeat" ] && kill -0 "$build_pid" 2>/dev/null; then
local log_bytes="0"
if [ -f "$log_file" ]; then
log_bytes="$(wc -c <"$log_file" 2>/dev/null || echo 0)"
log_bytes="${log_bytes//[[:space:]]/}"
fi
echo "Docker build $label still running (${elapsed_seconds}s elapsed, ${log_bytes} log bytes captured)..."
if kill -0 "$build_pid" 2>/dev/null && \
docker_build_maybe_print_heartbeat "$label" "$elapsed_seconds" "$next_heartbeat" "$log_file"; then
next_heartbeat=$((elapsed_seconds + heartbeat_seconds))
fi
done
+20 -8
View File
@@ -65,6 +65,22 @@ run_logged_print() {
rm -f "$log_file"
}
docker_e2e_maybe_print_log_heartbeat() {
local label="$1"
local elapsed_seconds="$2"
local next_heartbeat="$3"
local log_file="$4"
if [ "$elapsed_seconds" -lt "$next_heartbeat" ]; then
return 1
fi
local log_bytes="0"
if [ -f "$log_file" ]; then
log_bytes="$(wc -c <"$log_file" 2>/dev/null || echo 0)"
log_bytes="${log_bytes//[[:space:]]/}"
fi
echo "still running $label (${elapsed_seconds}s elapsed, ${log_bytes} log bytes captured)"
}
run_logged_print_heartbeat() {
local label="$1"
local interval_seconds="$2"
@@ -143,15 +159,11 @@ run_logged_print_heartbeat() {
local next_heartbeat=$interval_seconds
local status=0
while kill -0 "$command_pid" 2>/dev/null; do
/bin/sleep 1
# Poll promptly so short commands do not pay a one-second wrapper tax.
/bin/sleep 0.1
local elapsed_seconds=$((SECONDS - started_at))
if [ "$elapsed_seconds" -ge "$next_heartbeat" ] && kill -0 "$command_pid" 2>/dev/null; then
local log_bytes="0"
if [ -f "$log_file" ]; then
log_bytes="$(wc -c <"$log_file" 2>/dev/null || echo 0)"
log_bytes="${log_bytes//[[:space:]]/}"
fi
echo "still running $label (${elapsed_seconds}s elapsed, ${log_bytes} log bytes captured)"
if kill -0 "$command_pid" 2>/dev/null && \
docker_e2e_maybe_print_log_heartbeat "$label" "$elapsed_seconds" "$next_heartbeat" "$log_file"; then
next_heartbeat=$((elapsed_seconds + interval_seconds))
fi
done
+8 -4
View File
@@ -133,6 +133,10 @@ type ClawHubRequestOptions = {
requestTimeoutMs?: number;
};
type ClawHubRetryOptions = ClawHubRequestOptions & {
sleep?: (ms: number) => Promise<void>;
};
async function fetchClawHubRequest(
url: URL,
options: ClawHubRequestOptions = {},
@@ -485,10 +489,8 @@ async function doesClawHubPackageExist(
async function hasClawHubTrustedPublisher(
packageName: string,
options: {
fetchImpl?: typeof fetch;
options: ClawHubRetryOptions & {
registryBaseUrl?: string;
requestTimeoutMs?: number;
} = {},
): Promise<boolean> {
const url = new URL(
@@ -537,7 +539,7 @@ async function hasClawHubTrustedPublisher(
}
await response.body?.cancel().catch(() => undefined);
await delay(clawHubRetryDelayMs(response, attempt));
await (options.sleep ?? delay)(clawHubRetryDelayMs(response, attempt));
}
}
@@ -600,6 +602,7 @@ export async function collectPluginClawHubReleasePlan(params?: {
fetchImpl?: typeof fetch;
requestTimeoutMs?: number;
resolveLatestVersion?: NpmLatestVersionResolver;
sleep?: (ms: number) => Promise<void>;
}): Promise<PluginReleasePlan> {
const rootDir = params?.rootDir;
const selection = params?.selection ?? [];
@@ -648,6 +651,7 @@ export async function collectPluginClawHubReleasePlan(params?: {
registryBaseUrl: params?.registryBaseUrl,
fetchImpl: params?.fetchImpl,
requestTimeoutMs: params?.requestTimeoutMs,
sleep: params?.sleep,
})
: false;
const alreadyPublished = packageExists
+91 -43
View File
@@ -74,6 +74,7 @@ const SOURCE_ROOTS: Record<NativeI18nSurface, string[]> = {
const ANDROID_EXTENSIONS = new Set([".kt", ".kts"]);
const APPLE_EXTENSIONS = new Set([".swift", ".plist"]);
const NATIVE_FORMAT_RE = /%(?:\d+\$)?[@a-z]/giu;
const NATIVE_SOURCE_READ_CONCURRENCY = 32;
const APPLE_UI_MULTILINE_CALLS =
/(?:Text|Label|Button|TextField|SecureField|Picker|Section|LabeledContent|Toggle|Menu|ShareLink|Link|TextEditor|ProgressView|Gauge|DisclosureGroup|ControlGroup|DatePicker|Stepper)\s*\(\s*"""([\s\S]*?)"""/gu;
const APPLE_LOCALIZED_STRING_CALLS =
@@ -564,6 +565,27 @@ function normalizeSource(source: string): string {
return source;
}
function identifierBefore(source: string, offset: number): string | null {
let cursor = offset - 1;
while (cursor >= 0 && source.charCodeAt(cursor) <= 32) {
cursor -= 1;
}
const end = cursor + 1;
while (cursor >= 0 && (isAsciiAlphaNumeric(source[cursor]) || source[cursor] === "_")) {
cursor -= 1;
}
const start = cursor + 1;
if (
start === end ||
(!isAsciiLowercaseLetter(source[start]) &&
!isAsciiUppercaseLetter(source[start]) &&
source[start] !== "_")
) {
return null;
}
return source.slice(start, end);
}
function enclosingCallName(source: string, offset: number): string | null {
let depth = 0;
for (let index = offset - 1; index >= 0; index -= 1) {
@@ -578,7 +600,7 @@ function enclosingCallName(source: string, offset: number): string | null {
depth -= 1;
continue;
}
return source.slice(0, index).match(/([A-Za-z_][A-Za-z0-9_]*)\s*$/u)?.[1] ?? null;
return identifierBefore(source, index);
}
return null;
}
@@ -837,36 +859,31 @@ function extractCandidates(
return entries;
}
async function walkFiles(
root: string,
surface: NativeI18nSurface,
out: string[] = [],
): Promise<string[]> {
async function walkFiles(root: string, surface: NativeI18nSurface): Promise<string[]> {
const entries = await readdir(root, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(root, entry.name);
if (entry.isDirectory()) {
if (GENERATED_PATH_RE.test(fullPath) || EXCLUDED_PATH_RE.test(fullPath)) {
continue;
const nested = await Promise.all(
entries.map(async (entry): Promise<string[]> => {
const fullPath = path.join(root, entry.name);
if (entry.isDirectory()) {
if (GENERATED_PATH_RE.test(fullPath) || EXCLUDED_PATH_RE.test(fullPath)) {
return [];
}
return await walkFiles(fullPath, surface);
}
await walkFiles(fullPath, surface, out);
continue;
}
const extension = path.extname(entry.name);
const isAndroidValuesXml =
surface === "android" &&
extension === ".xml" &&
path.dirname(fullPath).endsWith(`${path.sep}res${path.sep}values`);
const allowed = surface === "apple" ? APPLE_EXTENSIONS : ANDROID_EXTENSIONS;
if (
entry.isFile() &&
(allowed.has(extension) || isAndroidValuesXml) &&
!EXCLUDED_FILE_RE.test(entry.name)
) {
out.push(fullPath);
}
}
return out;
const extension = path.extname(entry.name);
const isAndroidValuesXml =
surface === "android" &&
extension === ".xml" &&
path.dirname(fullPath).endsWith(`${path.sep}res${path.sep}values`);
const allowed = surface === "apple" ? APPLE_EXTENSIONS : ANDROID_EXTENSIONS;
return entry.isFile() &&
(allowed.has(extension) || isAndroidValuesXml) &&
!EXCLUDED_FILE_RE.test(entry.name)
? [fullPath]
: [];
}),
);
return nested.flat();
}
function withIds(entries: Candidate[]): NativeI18nEntry[] {
@@ -899,24 +916,55 @@ function withIds(entries: Candidate[]): NativeI18nEntry[] {
});
}
async function mapWithConcurrency<T, R>(
values: readonly T[],
limit: number,
run: (value: T) => Promise<R>,
): Promise<R[]> {
const results = Array<R>(values.length);
let nextIndex = 0;
const workerCount = Math.min(limit, values.length);
await Promise.all(
Array.from({ length: workerCount }, async () => {
for (;;) {
const index = nextIndex;
nextIndex += 1;
if (index >= values.length) {
return;
}
results[index] = await run(values[index]);
}
}),
);
return results;
}
export async function collectNativeI18nEntries(): Promise<NativeI18nEntry[]> {
const sources: Array<{
const roots = (["android", "apple"] as const).flatMap((surface) =>
SOURCE_ROOTS[surface].map((sourceRoot) => ({ sourceRoot, surface })),
);
const filesByRoot = await Promise.all(
roots.map(async ({ sourceRoot, surface }) => ({
files: (await walkFiles(sourceRoot, surface)).toSorted(),
surface,
})),
);
const sources = await mapWithConcurrency(
filesByRoot.flatMap(({ files, surface }) => files.map((filePath) => ({ filePath, surface }))),
NATIVE_SOURCE_READ_CONCURRENCY,
async ({ filePath, surface }) => ({
repoPath: path.relative(ROOT, filePath).split(path.sep).join("/"),
source: await readFile(filePath, "utf8"),
surface,
}),
);
const typedSources: Array<{
repoPath: string;
source: string;
surface: NativeI18nSurface;
}> = [];
for (const surface of ["android", "apple"] as const) {
for (const sourceRoot of SOURCE_ROOTS[surface]) {
const files = await walkFiles(sourceRoot, surface);
for (const filePath of files.toSorted()) {
const source = await readFile(filePath, "utf8");
const repoPath = path.relative(ROOT, filePath).split(path.sep).join("/");
sources.push({ repoPath, source, surface });
}
}
}
}> = sources;
const uiCallNames = new Set([...APPLE_BUILTIN_UI_TYPES, ...ANDROID_BUILTIN_UI_CALLS]);
for (const { source, surface } of sources) {
for (const { source, surface } of typedSources) {
if (surface === "android") {
for (const match of source.matchAll(ANDROID_COMPOSABLE_FUNCTION)) {
if (match[1]) {
@@ -933,7 +981,7 @@ export async function collectNativeI18nEntries(): Promise<NativeI18nEntry[]> {
}
}
}
const entries = sources.flatMap(({ repoPath, source, surface }) =>
const entries = typedSources.flatMap(({ repoPath, source, surface }) =>
extractCandidates(surface, repoPath, source, uiCallNames),
);
return withIds(entries);
@@ -0,0 +1,132 @@
// Lightweight CLI contract for the issue #78851 model-resolution profiler.
export type Issue78851ModelResolutionOptions = {
agentCount: number;
cpuProfDir?: string;
cpuProfOutput?: string;
json: boolean;
keepTemp: boolean;
lookupsPerRun: number;
modelsPerProvider: number;
output?: string;
providers: number;
runs: number;
runtimeHooks: boolean;
warmup: number;
};
const BOOLEAN_FLAGS = new Set(["--help", "-h", "--json", "--keep-temp", "--runtime-hooks"]);
const VALUE_FLAGS = new Set([
"--agents",
"--cpu-prof-dir",
"--cpu-prof-output",
"--lookups",
"--models-per-provider",
"--output",
"--providers",
"--runs",
"--warmup",
]);
export class Issue78851CliArgumentError extends Error {
override name = "Issue78851CliArgumentError";
}
function parseFlagValue(flag: string, args: readonly string[]): string | undefined {
const index = args.indexOf(flag);
if (index === -1) {
return undefined;
}
const value = args[index + 1];
if (!value || value.startsWith("-")) {
throw new Issue78851CliArgumentError(`${flag} requires a value`);
}
return value;
}
function parseInteger(
flag: string,
fallback: number,
args: readonly string[],
minimum: number,
label: string,
): number {
const raw = parseFlagValue(flag, args);
if (!raw) {
return fallback;
}
const value = Number(raw);
if (!Number.isInteger(value) || value < minimum) {
throw new Issue78851CliArgumentError(`${flag} must be a ${label} integer`);
}
return value;
}
function validateArgs(args: readonly string[]): void {
const seenValueFlags = new Set<string>();
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (BOOLEAN_FLAGS.has(arg)) {
continue;
}
if (!VALUE_FLAGS.has(arg)) {
throw new Issue78851CliArgumentError(`Unknown argument: ${arg}`);
}
if (seenValueFlags.has(arg)) {
throw new Issue78851CliArgumentError(`${arg} was provided more than once`);
}
seenValueFlags.add(arg);
const value = args[index + 1];
if (!value || value.startsWith("-")) {
throw new Issue78851CliArgumentError(`${arg} requires a value`);
}
index += 1;
}
}
export function issue78851ModelResolutionHelpRequested(args: readonly string[]): boolean {
return args.includes("--help") || args.includes("-h");
}
export function parseIssue78851ModelResolutionOptions(
args: readonly string[],
): Issue78851ModelResolutionOptions {
validateArgs(args);
return {
agentCount: parseInteger("--agents", 8, args, 1, "positive"),
cpuProfDir: parseFlagValue("--cpu-prof-dir", args),
cpuProfOutput: parseFlagValue("--cpu-prof-output", args),
json: args.includes("--json"),
keepTemp: args.includes("--keep-temp"),
lookupsPerRun: parseInteger("--lookups", 32, args, 1, "positive"),
modelsPerProvider: parseInteger("--models-per-provider", 16, args, 1, "positive"),
output: parseFlagValue("--output", args),
providers: parseInteger("--providers", 48, args, 1, "positive"),
runs: parseInteger("--runs", 8, args, 1, "positive"),
runtimeHooks: args.includes("--runtime-hooks"),
warmup: parseInteger("--warmup", 1, args, 0, "non-negative"),
};
}
export function issue78851ModelResolutionUsage(): string {
return `OpenClaw issue #78851 model-resolution profiler
Usage:
pnpm perf:issue-78851 -- [options]
node --import tsx scripts/perf/issue-78851-model-resolution.ts [options]
Options:
--providers <n> Synthetic configured providers (default: 48)
--models-per-provider <n> Models per provider (default: 16)
--agents <n> Agent configs/fallback chains (default: 8)
--lookups <n> resolveModelAsync calls per phase (default: 32)
--runs <n> Measured runs (default: 8)
--warmup <n> Warmup runs before measurement (default: 1)
--cpu-prof-dir <dir> Write a V8 .cpuprofile for the measured loop
--cpu-prof-output <path> Write the V8 .cpuprofile to this exact path
--runtime-hooks Include provider runtime hook resolution
--output <path> Write JSON report
--json Print JSON report
--keep-temp Keep generated temp state
--help, -h Show this text
`;
}
+11 -148
View File
@@ -10,38 +10,13 @@ import {
resetModelsJsonReadyCacheForTest,
} from "../../src/agents/models-config.js";
import type { OpenClawConfig } from "../../src/config/types.openclaw.js";
type Options = {
agentCount: number;
cpuProfDir?: string;
cpuProfOutput?: string;
json: boolean;
keepTemp: boolean;
lookupsPerRun: number;
modelsPerProvider: number;
output?: string;
providers: number;
runs: number;
runtimeHooks: boolean;
warmup: number;
};
const BOOLEAN_FLAGS = new Set(["--help", "-h", "--json", "--keep-temp", "--runtime-hooks"]);
const VALUE_FLAGS = new Set([
"--agents",
"--cpu-prof-dir",
"--cpu-prof-output",
"--lookups",
"--models-per-provider",
"--output",
"--providers",
"--runs",
"--warmup",
]);
class CliArgumentError extends Error {
override name = "CliArgumentError";
}
import {
Issue78851CliArgumentError,
issue78851ModelResolutionHelpRequested,
issue78851ModelResolutionUsage,
parseIssue78851ModelResolutionOptions,
type Issue78851ModelResolutionOptions as Options,
} from "./issue-78851-model-resolution-cli.js";
type PhaseSample = {
ensureMs: number;
@@ -85,117 +60,6 @@ type Report = {
cpuProfilePath?: string;
};
function parseFlagValue(flag: string, args = process.argv.slice(2)): string | undefined {
const index = args.indexOf(flag);
if (index === -1) {
return undefined;
}
const value = args[index + 1];
if (!value || value.startsWith("-")) {
throw new CliArgumentError(`${flag} requires a value`);
}
return value;
}
function hasFlag(flag: string, args = process.argv.slice(2)): boolean {
return args.includes(flag);
}
function parsePositiveInt(flag: string, fallback: number, args = process.argv.slice(2)): number {
const raw = parseFlagValue(flag, args);
if (!raw) {
return fallback;
}
const value = Number(raw);
if (!Number.isFinite(value) || value <= 0) {
throw new CliArgumentError(`${flag} must be a positive integer`);
}
if (!Number.isInteger(value)) {
throw new CliArgumentError(`${flag} must be a positive integer`);
}
return value;
}
function parseNonNegativeInt(flag: string, fallback: number, args = process.argv.slice(2)): number {
const raw = parseFlagValue(flag, args);
if (!raw) {
return fallback;
}
const value = Number(raw);
if (!Number.isFinite(value) || value < 0) {
throw new CliArgumentError(`${flag} must be a non-negative integer`);
}
if (!Number.isInteger(value)) {
throw new CliArgumentError(`${flag} must be a non-negative integer`);
}
return value;
}
function validateCliArgs(args = process.argv.slice(2)): void {
const seenValueFlags = new Set<string>();
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (BOOLEAN_FLAGS.has(arg)) {
continue;
}
if (VALUE_FLAGS.has(arg)) {
if (seenValueFlags.has(arg)) {
throw new CliArgumentError(`${arg} was provided more than once`);
}
seenValueFlags.add(arg);
const value = args[index + 1];
if (!value || value.startsWith("-")) {
throw new CliArgumentError(`${arg} requires a value`);
}
index += 1;
continue;
}
throw new CliArgumentError(`Unknown argument: ${arg}`);
}
}
function parseOptions(args = process.argv.slice(2)): Options {
validateCliArgs(args);
return {
agentCount: parsePositiveInt("--agents", 8, args),
cpuProfDir: parseFlagValue("--cpu-prof-dir", args),
cpuProfOutput: parseFlagValue("--cpu-prof-output", args),
json: hasFlag("--json", args),
keepTemp: hasFlag("--keep-temp", args),
lookupsPerRun: parsePositiveInt("--lookups", 32, args),
modelsPerProvider: parsePositiveInt("--models-per-provider", 16, args),
output: parseFlagValue("--output", args),
providers: parsePositiveInt("--providers", 48, args),
runs: parsePositiveInt("--runs", 8, args),
runtimeHooks: hasFlag("--runtime-hooks", args),
warmup: parseNonNegativeInt("--warmup", 1, args),
};
}
function printUsage(): void {
process.stdout.write(`OpenClaw issue #78851 model-resolution profiler
Usage:
pnpm perf:issue-78851 -- [options]
node --import tsx scripts/perf/issue-78851-model-resolution.ts [options]
Options:
--providers <n> Synthetic configured providers (default: 48)
--models-per-provider <n> Models per provider (default: 16)
--agents <n> Agent configs/fallback chains (default: 8)
--lookups <n> resolveModelAsync calls per phase (default: 32)
--runs <n> Measured runs (default: 8)
--warmup <n> Warmup runs before measurement (default: 1)
--cpu-prof-dir <dir> Write a V8 .cpuprofile for the measured loop
--cpu-prof-output <path> Write the V8 .cpuprofile to this exact path
--runtime-hooks Include provider runtime hook resolution
--output <path> Write JSON report
--json Print JSON report
--keep-temp Keep generated temp state
--help, -h Show this text
`);
}
function round(value: number): number {
return Math.round(value * 100) / 100;
}
@@ -478,12 +342,11 @@ function printHuman(report: Report, cpuProfilePath?: string): void {
async function main(): Promise<void> {
const args = process.argv.slice(2);
validateCliArgs(args);
if (hasFlag("--help", args) || hasFlag("-h", args)) {
printUsage();
const options = parseIssue78851ModelResolutionOptions(args);
if (issue78851ModelResolutionHelpRequested(args)) {
process.stdout.write(issue78851ModelResolutionUsage());
return;
}
const options = parseOptions(args);
const tempRoot = await mkdtemp(path.join(tmpdir(), "openclaw-issue-78851-"));
const workspaceDir = path.join(tempRoot, "workspace");
await mkdir(workspaceDir, { recursive: true });
@@ -547,7 +410,7 @@ async function main(): Promise<void> {
}
main().catch((error: unknown) => {
if (error instanceof CliArgumentError) {
if (error instanceof Issue78851CliArgumentError) {
process.stderr.write(`${error.message}\n`);
process.exit(1);
}
+209 -118
View File
@@ -3,7 +3,7 @@
// Reports plugin SDK export surface metadata.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";
import ts from "typescript";
import {
deprecatedBarrelPluginSdkEntrypoints,
@@ -25,7 +25,7 @@ Options:
`;
}
function parseArgs(argv) {
export function parsePluginSdkSurfaceReportArgs(argv) {
const args = { check: false, help: false };
for (const arg of argv) {
if (arg === "--check") {
@@ -40,28 +40,14 @@ function parseArgs(argv) {
}
return args;
}
let cliArgs;
try {
cliArgs = parseArgs(process.argv.slice(2));
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
if (cliArgs.help) {
process.stdout.write(usage());
process.exit(0);
}
const checkOnly = cliArgs.check;
const publicEntrypointSet = new Set(publicPluginSdkEntrypoints);
const localOnlyEntrypointSet = new Set(privateLocalOnlyPluginSdkEntrypoints);
const deprecatedPublicEntrypointSet = new Set(deprecatedPublicPluginSdkEntrypoints);
const deprecatedBarrelEntrypointSet = new Set(deprecatedBarrelPluginSdkEntrypoints);
const forbiddenPublicSubpaths = new Set(["test-utils"]);
function readBudgetEnv(name, fallback) {
const raw = process.env[name];
export function readPluginSdkSurfaceBudgetEnv(name, fallback, env = process.env) {
const raw = env[name];
if (raw === undefined) {
return fallback;
}
@@ -76,8 +62,8 @@ function readBudgetEnv(name, fallback) {
return parsed;
}
function readEntrypointBudgetEnv(name, fallback) {
const raw = process.env[name];
export function readPluginSdkEntrypointBudgetEnv(name, fallback, env = process.env) {
const raw = env[name];
if (raw === undefined) {
return fallback;
}
@@ -101,7 +87,7 @@ function readEntrypointBudgetEnv(name, fallback) {
return Object.freeze({ ...fallback, ...overrides });
}
const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({
export const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({
core: 2,
health: 1,
lmstudio: 1,
@@ -197,29 +183,40 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({
zod: 282,
});
let budgets;
let publicDeprecatedExportsByEntrypointBudget;
try {
budgets = {
publicEntrypoints: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_ENTRYPOINTS", 324),
publicExports: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS", 10429),
publicFunctionExports: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_FUNCTION_EXPORTS", 5206),
publicDeprecatedExports: readBudgetEnv(
export function readPluginSdkSurfaceBudgets(env = process.env) {
const budgets = {
publicEntrypoints: readPluginSdkSurfaceBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_ENTRYPOINTS",
324,
env,
),
publicExports: readPluginSdkSurfaceBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS",
10429,
env,
),
publicFunctionExports: readPluginSdkSurfaceBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_FUNCTION_EXPORTS",
5206,
env,
),
publicDeprecatedExports: readPluginSdkSurfaceBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_DEPRECATED_EXPORTS",
3261,
env,
),
publicWildcardReexports: readBudgetEnv(
publicWildcardReexports: readPluginSdkSurfaceBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_WILDCARD_REEXPORTS",
212,
env,
),
};
publicDeprecatedExportsByEntrypointBudget = readEntrypointBudgetEnv(
const publicDeprecatedExportsByEntrypointBudget = readPluginSdkEntrypointBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_DEPRECATED_EXPORTS_BY_ENTRYPOINT",
defaultPublicDeprecatedExportsByEntrypointBudget,
env,
);
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
return { budgets, publicDeprecatedExportsByEntrypointBudget };
}
function entrypointPath(entrypoint) {
@@ -360,6 +357,36 @@ function collectExportStats(entrypoints) {
return { byEntrypoint, totals };
}
function selectExportStats(scannedStats, entrypoints) {
const byEntrypoint = new Map();
const totals = {
entrypoints: entrypoints.length,
exports: 0,
callableExports: 0,
deprecatedExports: 0,
deprecatedCallableExports: 0,
uniqueExports: 0,
uniqueCallableExports: 0,
};
for (const entrypoint of entrypoints) {
const stats = scannedStats.byEntrypoint.get(entrypoint) ?? {
exports: 0,
callableExports: 0,
deprecatedExports: 0,
deprecatedCallableExports: 0,
};
byEntrypoint.set(entrypoint, stats);
totals.exports += stats.exports;
totals.callableExports += stats.callableExports;
totals.deprecatedExports += stats.deprecatedExports;
totals.deprecatedCallableExports += stats.deprecatedCallableExports;
}
// Export identities are entrypoint-qualified, so the selected totals are unique.
totals.uniqueExports = totals.exports;
totals.uniqueCallableExports = totals.callableExports;
return { byEntrypoint, totals };
}
function formatStats(label, stats) {
return [
`${label}:`,
@@ -372,10 +399,10 @@ function formatStats(label, stats) {
].join("\n");
}
function collectDeprecatedEntrypointBudgetFailures(byEntrypoint) {
function collectDeprecatedEntrypointBudgetFailures(byEntrypoint, entrypointBudgets) {
const failures = [];
for (const [entrypoint, stats] of byEntrypoint) {
const budget = publicDeprecatedExportsByEntrypointBudget[entrypoint] ?? 0;
const budget = entrypointBudgets[entrypoint] ?? 0;
if (stats.deprecatedExports > budget) {
failures.push(
`public deprecated exports in ${entrypoint} ${stats.deprecatedExports} > ${budget}`,
@@ -385,95 +412,159 @@ function collectDeprecatedEntrypointBudgetFailures(byEntrypoint) {
return failures;
}
const allStats = collectExportStats(pluginSdkEntrypoints);
const publicStats = collectExportStats(publicPluginSdkEntrypoints);
const localOnlyStats = collectExportStats(privateLocalOnlyPluginSdkEntrypoints);
const publicWildcards = countWildcardReexports(publicPluginSdkEntrypoints);
const packageExportedSubpaths = readPackageExportedSubpaths();
const leakedForbiddenExports = packageExportedSubpaths.filter((subpath) =>
forbiddenPublicSubpaths.has(subpath),
);
const localOnlyStillPublic = privateLocalOnlyPluginSdkEntrypoints.filter((entrypoint) =>
publicEntrypointSet.has(entrypoint),
);
const localOnlyMissingFromInventory = [...localOnlyEntrypointSet].filter(
(entrypoint) => !pluginSdkEntrypoints.includes(entrypoint),
);
const deprecatedMissingFromPublic = [...deprecatedPublicEntrypointSet].filter(
(entrypoint) => !publicEntrypointSet.has(entrypoint),
);
const deprecatedBarrelMissingFromInventory = [...deprecatedBarrelEntrypointSet].filter(
(entrypoint) => !pluginSdkEntrypoints.includes(entrypoint),
);
const deprecatedBarrelWithoutWildcard = [...deprecatedBarrelEntrypointSet].filter((entrypoint) => {
const source = fs.readFileSync(entrypointPath(entrypoint), "utf8");
return !/^\s*export\s+(?:type\s+)?\*\s+from\s+["'][^"']+["']/mu.test(source);
});
console.log(formatStats("all SDK entrypoints", allStats.totals));
console.log(formatStats("public package SDK entrypoints", publicStats.totals));
console.log(formatStats("local-only SDK entrypoints", localOnlyStats.totals));
console.log(`deprecated public subpaths: ${deprecatedPublicPluginSdkEntrypoints.length}`);
console.log(`deprecated barrel subpaths: ${deprecatedBarrelPluginSdkEntrypoints.length}`);
console.log(`public wildcard reexports: ${publicWildcards.count}`);
console.log(`package-exported forbidden subpaths: ${leakedForbiddenExports.length}`);
const failures = [];
if (publicPluginSdkEntrypoints.length > budgets.publicEntrypoints) {
failures.push(
`public entrypoints ${publicPluginSdkEntrypoints.length} > ${budgets.publicEntrypoints}`,
export function collectPluginSdkSurfaceReport() {
const scannedEntrypoints = [
...new Set([
...pluginSdkEntrypoints,
...publicPluginSdkEntrypoints,
...privateLocalOnlyPluginSdkEntrypoints,
]),
];
const scannedStats = collectExportStats(scannedEntrypoints);
const allStats = selectExportStats(scannedStats, pluginSdkEntrypoints);
const publicStats = selectExportStats(scannedStats, publicPluginSdkEntrypoints);
const localOnlyStats = selectExportStats(scannedStats, privateLocalOnlyPluginSdkEntrypoints);
const publicWildcards = countWildcardReexports(publicPluginSdkEntrypoints);
const leakedForbiddenExports = readPackageExportedSubpaths().filter((subpath) =>
forbiddenPublicSubpaths.has(subpath),
);
}
if (publicStats.totals.exports > budgets.publicExports) {
failures.push(`public exports ${publicStats.totals.exports} > ${budgets.publicExports}`);
}
if (publicStats.totals.callableExports > budgets.publicFunctionExports) {
failures.push(
`public callable exports ${publicStats.totals.callableExports} > ${budgets.publicFunctionExports}`,
const localOnlyStillPublic = privateLocalOnlyPluginSdkEntrypoints.filter((entrypoint) =>
publicEntrypointSet.has(entrypoint),
);
}
if (publicStats.totals.deprecatedExports > budgets.publicDeprecatedExports) {
failures.push(
`public deprecated exports ${publicStats.totals.deprecatedExports} > ${budgets.publicDeprecatedExports}`,
const localOnlyMissingFromInventory = [...localOnlyEntrypointSet].filter(
(entrypoint) => !pluginSdkEntrypoints.includes(entrypoint),
);
}
failures.push(...collectDeprecatedEntrypointBudgetFailures(publicStats.byEntrypoint));
if (publicWildcards.count > budgets.publicWildcardReexports) {
failures.push(
`public wildcard reexports ${publicWildcards.count} > ${budgets.publicWildcardReexports}`,
const deprecatedMissingFromPublic = [...deprecatedPublicEntrypointSet].filter(
(entrypoint) => !publicEntrypointSet.has(entrypoint),
);
}
if (leakedForbiddenExports.length > 0) {
failures.push(`forbidden public subpaths: ${leakedForbiddenExports.join(", ")}`);
}
if (localOnlyStillPublic.length > 0) {
failures.push(`local-only entrypoints still public: ${localOnlyStillPublic.join(", ")}`);
}
if (localOnlyMissingFromInventory.length > 0) {
failures.push(
`local-only entrypoints missing from inventory: ${localOnlyMissingFromInventory.join(", ")}`,
const deprecatedBarrelMissingFromInventory = [...deprecatedBarrelEntrypointSet].filter(
(entrypoint) => !pluginSdkEntrypoints.includes(entrypoint),
);
}
if (deprecatedMissingFromPublic.length > 0) {
failures.push(
`deprecated public entrypoints missing from package surface: ${deprecatedMissingFromPublic.join(", ")}`,
);
}
if (deprecatedBarrelMissingFromInventory.length > 0) {
failures.push(
`deprecated barrel entrypoints missing from inventory: ${deprecatedBarrelMissingFromInventory.join(", ")}`,
);
}
if (deprecatedBarrelWithoutWildcard.length > 0) {
failures.push(
`deprecated barrel entrypoints without wildcard exports: ${deprecatedBarrelWithoutWildcard.join(", ")}`,
const deprecatedBarrelWithoutWildcard = [...deprecatedBarrelEntrypointSet].filter(
(entrypoint) => {
const source = fs.readFileSync(entrypointPath(entrypoint), "utf8");
return !/^\s*export\s+(?:type\s+)?\*\s+from\s+["'][^"']+["']/mu.test(source);
},
);
return {
allStats,
deprecatedBarrelMissingFromInventory,
deprecatedBarrelWithoutWildcard,
deprecatedMissingFromPublic,
leakedForbiddenExports,
localOnlyMissingFromInventory,
localOnlyStats,
localOnlyStillPublic,
publicStats,
publicWildcards,
};
}
if (checkOnly && failures.length > 0) {
console.error("plugin SDK surface budget failed:");
for (const failure of failures) {
console.error(`- ${failure}`);
export function evaluatePluginSdkSurfaceReport(
report,
{ budgets, publicDeprecatedExportsByEntrypointBudget },
) {
const failures = [];
if (publicPluginSdkEntrypoints.length > budgets.publicEntrypoints) {
failures.push(
`public entrypoints ${publicPluginSdkEntrypoints.length} > ${budgets.publicEntrypoints}`,
);
}
if (report.publicStats.totals.exports > budgets.publicExports) {
failures.push(`public exports ${report.publicStats.totals.exports} > ${budgets.publicExports}`);
}
if (report.publicStats.totals.callableExports > budgets.publicFunctionExports) {
failures.push(
`public callable exports ${report.publicStats.totals.callableExports} > ${budgets.publicFunctionExports}`,
);
}
if (report.publicStats.totals.deprecatedExports > budgets.publicDeprecatedExports) {
failures.push(
`public deprecated exports ${report.publicStats.totals.deprecatedExports} > ${budgets.publicDeprecatedExports}`,
);
}
failures.push(
...collectDeprecatedEntrypointBudgetFailures(
report.publicStats.byEntrypoint,
publicDeprecatedExportsByEntrypointBudget,
),
);
if (report.publicWildcards.count > budgets.publicWildcardReexports) {
failures.push(
`public wildcard reexports ${report.publicWildcards.count} > ${budgets.publicWildcardReexports}`,
);
}
if (report.leakedForbiddenExports.length > 0) {
failures.push(`forbidden public subpaths: ${report.leakedForbiddenExports.join(", ")}`);
}
if (report.localOnlyStillPublic.length > 0) {
failures.push(`local-only entrypoints still public: ${report.localOnlyStillPublic.join(", ")}`);
}
if (report.localOnlyMissingFromInventory.length > 0) {
failures.push(
`local-only entrypoints missing from inventory: ${report.localOnlyMissingFromInventory.join(", ")}`,
);
}
if (report.deprecatedMissingFromPublic.length > 0) {
failures.push(
`deprecated public entrypoints missing from package surface: ${report.deprecatedMissingFromPublic.join(", ")}`,
);
}
if (report.deprecatedBarrelMissingFromInventory.length > 0) {
failures.push(
`deprecated barrel entrypoints missing from inventory: ${report.deprecatedBarrelMissingFromInventory.join(", ")}`,
);
}
if (report.deprecatedBarrelWithoutWildcard.length > 0) {
failures.push(
`deprecated barrel entrypoints without wildcard exports: ${report.deprecatedBarrelWithoutWildcard.join(", ")}`,
);
}
return failures;
}
function renderPluginSdkSurfaceReport(report) {
return [
formatStats("all SDK entrypoints", report.allStats.totals),
formatStats("public package SDK entrypoints", report.publicStats.totals),
formatStats("local-only SDK entrypoints", report.localOnlyStats.totals),
`deprecated public subpaths: ${deprecatedPublicPluginSdkEntrypoints.length}`,
`deprecated barrel subpaths: ${deprecatedBarrelPluginSdkEntrypoints.length}`,
`public wildcard reexports: ${report.publicWildcards.count}`,
`package-exported forbidden subpaths: ${report.leakedForbiddenExports.length}`,
].join("\n");
}
function main(argv = process.argv.slice(2), env = process.env) {
const cliArgs = parsePluginSdkSurfaceReportArgs(argv);
if (cliArgs.help) {
process.stdout.write(usage());
return 0;
}
const budgetConfig = readPluginSdkSurfaceBudgets(env);
const report = collectPluginSdkSurfaceReport();
process.stdout.write(`${renderPluginSdkSurfaceReport(report)}\n`);
const failures = evaluatePluginSdkSurfaceReport(report, budgetConfig);
if (cliArgs.check && failures.length > 0) {
process.stderr.write(`plugin SDK surface budget failed:\n`);
for (const failure of failures) {
process.stderr.write(`- ${failure}\n`);
}
return 1;
}
return 0;
}
const isMain =
typeof process.argv[1] === "string" &&
process.argv[1].length > 0 &&
import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
if (isMain) {
try {
process.exitCode = main();
} catch (error) {
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
process.exitCode = 1;
}
process.exit(1);
}
@@ -17,7 +17,7 @@ const ROOT_SHIMS_MAX_OLD_SPACE_SIZE =
process.env.OPENCLAW_ROOT_SHIMS_MAX_OLD_SPACE_SIZE?.trim() || "8192";
const ROOT_SHIMS_NODE_OPTIONS =
`${process.env.NODE_OPTIONS ?? ""} --max-old-space-size=${ROOT_SHIMS_MAX_OLD_SPACE_SIZE}`.trim();
const NODE_STEP_ABORT_KILL_GRACE_MS = 1_000;
const DEFAULT_NODE_STEP_ABORT_KILL_GRACE_MS = 1_000;
const MAX_TIMER_TIMEOUT_MS = 2_147_000_000;
const NODE_STEP_PARENT_SIGNALS = ["SIGHUP", "SIGINT", "SIGTERM"];
const NODE_STEP_PARENT_SIGNAL_EXIT_CODES = new Map([
@@ -25,7 +25,7 @@ const NODE_STEP_PARENT_SIGNAL_EXIT_CODES = new Map([
["SIGINT", 130],
["SIGTERM", 143],
]);
const ACTIVE_NODE_STEP_KILLERS = new Set();
const ACTIVE_NODE_STEP_KILLERS = new Map();
let nodeStepParentSignalForwardersInstalled = false;
let exitingAfterParentSignal = false;
let parentSignalExitCode = 1;
@@ -475,11 +475,17 @@ export function signalNodeStep(
}
function signalActiveNodeSteps(signal) {
for (const killNodeStep of ACTIVE_NODE_STEP_KILLERS) {
for (const killNodeStep of ACTIVE_NODE_STEP_KILLERS.keys()) {
killNodeStep(signal);
}
}
function activeNodeStepKillGraceMs() {
return ACTIVE_NODE_STEP_KILLERS.size > 0
? Math.max(...ACTIVE_NODE_STEP_KILLERS.values())
: DEFAULT_NODE_STEP_ABORT_KILL_GRACE_MS;
}
function installNodeStepParentSignalForwarders() {
if (nodeStepParentSignalForwardersInstalled) {
return;
@@ -497,7 +503,7 @@ function installNodeStepParentSignalForwarders() {
signalActiveNodeSteps(signal);
parentSignalExitTimer ??= setTimeout(
() => process.exit(parentSignalExitCode),
NODE_STEP_ABORT_KILL_GRACE_MS,
activeNodeStepKillGraceMs(),
);
});
}
@@ -519,6 +525,10 @@ function resolveNodeStepTimerTimeoutMs(valueMs) {
*/
export function runNodeStep(label, args, timeoutMs, params = {}) {
const resolvedTimeoutMs = resolveNodeStepTimerTimeoutMs(timeoutMs);
const abortKillGraceMs = Math.max(
0,
Math.floor(params.abortKillGraceMs ?? DEFAULT_NODE_STEP_ABORT_KILL_GRACE_MS),
);
const abortController = params.abortController;
const spawnImpl = params.spawnImpl ?? spawn;
installNodeStepParentSignalForwarders();
@@ -571,18 +581,18 @@ export function runNodeStep(label, args, timeoutMs, params = {}) {
await waitForProcessGroupExit(100);
}
};
ACTIVE_NODE_STEP_KILLERS.add(killNodeStep);
ACTIVE_NODE_STEP_KILLERS.set(killNodeStep, abortKillGraceMs);
const abortStep = () => {
if (settled || canceled) {
return;
}
canceled = true;
killNodeStep("SIGTERM");
killDeadlineAt = Date.now() + NODE_STEP_ABORT_KILL_GRACE_MS;
killDeadlineAt = Date.now() + abortKillGraceMs;
killTimer = setTimeout(() => {
killTimer = undefined;
killNodeStep("SIGKILL");
}, NODE_STEP_ABORT_KILL_GRACE_MS);
}, abortKillGraceMs);
killTimer.unref?.();
};
function cleanup() {
@@ -667,7 +677,11 @@ export async function runNodeStepsInParallel(steps) {
const abortController = new AbortController();
const results = await Promise.allSettled(
steps.map((step) =>
runNodeStep(step.label, step.args, step.timeoutMs, { abortController, env: step.env }),
runNodeStep(step.label, step.args, step.timeoutMs, {
abortController,
abortKillGraceMs: step.abortKillGraceMs,
env: step.env,
}),
),
);
const firstFailure = results.find((result) => result.status === "rejected");
+17 -11
View File
@@ -15,6 +15,7 @@ import { formatErrorMessage } from "./lib/error-format.mjs";
const DEFAULT_CONCURRENCY = 6;
const DEFAULT_TIMEOUT_MS = 90_000;
const DEFAULT_COMBINED_TIMEOUT_MS = 180_000;
const DEFAULT_CHILD_SHUTDOWN_GRACE_MS = 1_000;
const DEFAULT_TOP = 10;
const OUTPUT_CAPTURE_MAX_CHARS = 128 * 1024;
const STDERR_PREVIEW_MAX_CHARS = 8 * 1024;
@@ -24,7 +25,7 @@ const PARENT_SIGNAL_EXIT_CODES = new Map([
["SIGINT", 130],
["SIGTERM", 143],
]);
const activeCaseChildren = new Set();
const activeCaseChildren = new Map();
const parentSignalHandlers = new Map();
let parentSignalHandlersInstalled = false;
let parentSignalShutdownStarted = false;
@@ -203,6 +204,7 @@ export async function runCase({
name,
body,
timeoutMs,
shutdownGraceMs = DEFAULT_CHILD_SHUTDOWN_GRACE_MS,
spawnImpl = spawn,
}) {
return await new Promise((resolve) => {
@@ -216,7 +218,7 @@ export async function runCase({
stdio: ["ignore", "pipe", "pipe"],
},
);
trackActiveCaseChild(child);
trackActiveCaseChild(child, shutdownGraceMs);
let stdout = createOutputCapture();
let stderr = createOutputCapture();
@@ -265,7 +267,7 @@ export async function runCase({
child.on("close", (code, signal) => {
void (async () => {
if (timedOut) {
await waitForChildProcessTreeExit(child, 1_000);
await waitForChildProcessTreeExit(child, shutdownGraceMs);
}
const stderrText = formatCapturedOutput(stderr);
settle({
@@ -321,8 +323,8 @@ function childProcessTreeIsAlive(child) {
}
}
function trackActiveCaseChild(child) {
activeCaseChildren.add(child);
function trackActiveCaseChild(child, shutdownGraceMs) {
activeCaseChildren.set(child, shutdownGraceMs);
installParentSignalHandlers();
}
@@ -365,7 +367,7 @@ function removeInstalledParentSignalHandlers() {
function handleParentSignal(signal) {
if (parentSignalShutdownStarted) {
for (const child of activeCaseChildren) {
for (const child of activeCaseChildren.keys()) {
signalChildProcessTree(child, "SIGKILL");
}
return;
@@ -375,17 +377,21 @@ function handleParentSignal(signal) {
}
async function cleanupActiveCaseChildrenForParentSignal(signal) {
const children = [...activeCaseChildren];
for (const child of children) {
const children = [...activeCaseChildren.entries()];
for (const [child] of children) {
signalChildProcessTree(child, signal);
}
await Promise.all(children.map((child) => waitForChildProcessTreeExit(child, 1_000)));
for (const child of children) {
await Promise.all(
children.map(([child, shutdownGraceMs]) => waitForChildProcessTreeExit(child, shutdownGraceMs)),
);
for (const [child] of children) {
if (childProcessTreeIsAlive(child)) {
signalChildProcessTree(child, "SIGKILL");
}
}
await Promise.all(children.map((child) => waitForChildProcessTreeExit(child, 1_000)));
await Promise.all(
children.map(([child, shutdownGraceMs]) => waitForChildProcessTreeExit(child, shutdownGraceMs)),
);
removeInstalledParentSignalHandlers();
process.exit(PARENT_SIGNAL_EXIT_CODES.get(signal) ?? 1);
}
+25 -17
View File
@@ -128,6 +128,30 @@ function parseOptions(argv: readonly string[]): ProducerOptions {
};
}
type ProducerCliOutput = {
error: (message: string) => void;
log: (message: string) => void;
};
export async function runUxMatrixEvidenceProducerCli(
argv: readonly string[],
output: ProducerCliOutput = console,
): Promise<number> {
try {
if (isHelpRequest(argv)) {
output.log(usage());
return 0;
}
const result = await runUxMatrixEvidenceProducer(parseOptions(argv));
output.log(`UX Matrix evidence: ${path.join(result.artifactBase, QA_EVIDENCE_FILENAME)}`);
output.log(`UX Matrix entries: ${result.evidence.entries.length}`);
return 0;
} catch (error) {
output.error(error instanceof Error ? error.message : String(error));
return 1;
}
}
async function writeJson(filePath: string, value: unknown) {
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
@@ -784,21 +808,5 @@ export async function runUxMatrixEvidenceProducer(options: ProducerOptions) {
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
(async () => {
const cliArgs = process.argv.slice(2);
if (isHelpRequest(cliArgs)) {
console.log(usage());
return;
}
const result = await runUxMatrixEvidenceProducer(parseOptions(cliArgs));
console.log(`UX Matrix evidence: ${path.join(result.artifactBase, QA_EVIDENCE_FILENAME)}`);
console.log(`UX Matrix entries: ${result.evidence.entries.length}`);
})()
.then(() => {
process.exitCode = 0;
})
.catch((error: unknown) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
process.exitCode = await runUxMatrixEvidenceProducerCli(process.argv.slice(2));
}
@@ -25,6 +25,7 @@ export const ARTIFACT_TARBALL_SCAN_MAX_ENTRIES = 10_000;
const COMMAND_STDOUT_CAPTURE_MAX_CHARS = 8 * 1024 * 1024;
const COMMAND_STDERR_CAPTURE_MAX_CHARS = 128 * 1024;
const COMMAND_TIMEOUT_KILL_AFTER_MS = 5_000;
const FORWARDED_SIGNAL_KILL_AFTER_MS = 250;
const COMMAND_PROCESS_TREE_EXIT_POLL_MS = 50;
const MAX_TIMER_TIMEOUT_MS = 2_147_000_000;
const ACTIVE_CHILD_KILLERS = new Set();
@@ -58,7 +59,7 @@ for (const signal of Object.keys(SIGNAL_EXIT_CODES)) {
killChild("SIGKILL");
}
process.exit(forwardedSignalExitCode);
}, COMMAND_TIMEOUT_KILL_AFTER_MS);
}, FORWARDED_SIGNAL_KILL_AFTER_MS);
});
}
export const OPENCLAW_PACKAGE_SPEC_RE =
@@ -457,7 +458,7 @@ async function assertExpectedSha256(file, expected) {
export const assertExpectedSha256ForTest = assertExpectedSha256;
async function findSingleTarball(dir) {
async function findSingleTarball(dir, maxEntries = ARTIFACT_TARBALL_SCAN_MAX_ENTRIES) {
const root = path.resolve(ROOT_DIR, dir);
const pending = [root];
const tarballs = [];
@@ -471,9 +472,9 @@ async function findSingleTarball(dir) {
const handle = await fs.opendir(currentDir);
for await (const entry of handle) {
scannedEntries += 1;
if (scannedEntries > ARTIFACT_TARBALL_SCAN_MAX_ENTRIES) {
if (scannedEntries > maxEntries) {
throw new Error(
`source=artifact scan exceeded ${ARTIFACT_TARBALL_SCAN_MAX_ENTRIES} filesystem entries under ${dir}; provide a smaller artifact directory containing exactly one .tgz.`,
`source=artifact scan exceeded ${maxEntries} filesystem entries under ${dir}; provide a smaller artifact directory containing exactly one .tgz.`,
);
}
+3 -2
View File
@@ -6,9 +6,10 @@ import { performance } from "node:perf_hooks";
const DEFAULT_CHECK_TIMEOUT_MS = 10 * 60 * 1000;
const DEFAULT_OUTPUT_MAX_BYTES = 512 * 1024;
const TIMEOUT_KILL_GRACE_MS = 5_000;
// Boundary checks are disposable subprocesses; bound descendant cleanup after timeout.
const TIMEOUT_KILL_GRACE_MS = 250;
const PROCESS_GROUP_EXIT_POLL_MS = 25;
const POST_FORCE_KILL_WAIT_MS = 1_000;
const POST_FORCE_KILL_WAIT_MS = 250;
const MAX_TIMER_TIMEOUT_MS = 2_147_000_000;
/** Ordered list of supplemental boundary checks used by CI sharding. */
+27 -22
View File
@@ -998,9 +998,9 @@ export function resolveTestProjectsRunnerSpawnParams(env, platform = process.pla
};
}
function spawnTestProjectsRunner(argv, env) {
function spawnTestProjectsRunner(argv, env, options = {}) {
let forwardedSignal = null;
const child = spawn(process.execPath, [testProjectsRunnerPath, ...argv], {
const child = spawn(process.execPath, [options.runnerPath ?? testProjectsRunnerPath, ...argv], {
...resolveTestProjectsRunnerSpawnParams(env),
});
const teardown = installVitestProcessGroupCleanup({
@@ -1014,6 +1014,30 @@ function spawnTestProjectsRunner(argv, env) {
return { child, getForwardedSignal: () => forwardedSignal, teardown };
}
export function runTestProjectsDelegation(argv, env, options = {}) {
const { child, getForwardedSignal, teardown } = spawnTestProjectsRunner(argv, env, options);
child.on("exit", (code, signal) => {
teardown();
const forwardedSignal = getForwardedSignal();
if (forwardedSignal) {
forceKillVitestProcessGroup(child);
process.kill(process.pid, forwardedSignal);
return;
}
if (signal) {
process.kill(process.pid, signal);
return;
}
process.exit(code ?? 1);
});
child.on("error", (error) => {
teardown();
console.error(error);
process.exit(1);
});
return child;
}
function main(argv = process.argv.slice(2), env = process.env) {
if (argv.length === 0) {
console.error("usage: node scripts/run-vitest.mjs <vitest args...>");
@@ -1033,26 +1057,7 @@ function main(argv = process.argv.slice(2), env = process.env) {
const delegatedArgs = resolveTestProjectsDelegationArgs(argv);
if (delegatedArgs) {
const { child, getForwardedSignal, teardown } = spawnTestProjectsRunner(delegatedArgs, env);
child.on("exit", (code, signal) => {
teardown();
const forwardedSignal = getForwardedSignal();
if (forwardedSignal) {
forceKillVitestProcessGroup(child);
process.kill(process.pid, forwardedSignal);
return;
}
if (signal) {
process.kill(process.pid, signal);
return;
}
process.exit(code ?? 1);
});
child.on("error", (error) => {
teardown();
console.error(error);
process.exit(1);
});
runTestProjectsDelegation(delegatedArgs, env);
return;
}
+30 -39
View File
@@ -438,7 +438,7 @@ async function writeTimingStore(timingStore, results) {
console.log(`==> Docker lane timings: ${timingStore.file}`);
}
async function writeRunSummary(logDir, summary) {
export async function writeRunSummary(logDir, summary) {
const file = path.join(logDir, "summary.json");
const payload = {
...summary,
@@ -594,7 +594,7 @@ export function runShellCommand({
env,
stdio: pipeOutput ? ["ignore", "pipe", "pipe"] : "inherit",
});
activeChildren.add(child);
activeChildren.set(child, resolvedTimeoutKillGraceMs);
let timedOut = false;
let noOutputTimedOut = false;
let killTimer;
@@ -614,10 +614,7 @@ export function runShellCommand({
}
terminateChild(child, "SIGTERM");
killAt = Date.now() + resolvedTimeoutKillGraceMs;
killTimer = setTimeout(
() => terminateChild(child, "SIGKILL"),
resolvedTimeoutKillGraceMs,
);
killTimer = setTimeout(() => terminateChild(child, "SIGKILL"), resolvedTimeoutKillGraceMs);
killTimer.unref?.();
};
const resetNoOutputTimer = () => {
@@ -725,7 +722,7 @@ export function runShellCaptureCommand({
env,
stdio: ["ignore", "pipe", "pipe"],
});
activeChildren.add(child);
activeChildren.set(child, resolvedTimeoutKillGraceMs);
let stdout = "";
let stderr = "";
let stdoutTruncated = false;
@@ -878,6 +875,25 @@ async function runCleanupSmoke(baseEnv, logDir, command, startedAtMs) {
};
}
export async function runCleanupSmokePhase(baseEnv, logDir, phases) {
const command = "pnpm test:docker:cleanup";
const startedAtMs = Date.now();
let failure;
try {
await runPhase(phases, CLEANUP_SMOKE_NAME, {}, async () => {
failure = await runCleanupSmoke(baseEnv, logDir, command, startedAtMs);
if (failure) {
throw new Error(
`Run cleanup smoke after parallel lanes failed with status ${failure.status}`,
);
}
});
} catch (error) {
failure ??= await recordCleanupSmokeFailure(error, baseEnv, logDir, command, startedAtMs);
}
return failure;
}
async function runForegroundGroup(entries, env) {
const failures = [];
for (const entry of entries) {
@@ -1298,7 +1314,7 @@ async function printFailureSummary(failures, tailLines) {
}
}
const activeChildren = new Set();
const activeChildren = new Map();
let activeChildrenShutdownPromise;
function shellCommandSkippedForShutdown() {
@@ -1376,7 +1392,7 @@ function terminateChild(child, signal) {
}
function terminateActiveChildren(signal) {
for (const child of activeChildren) {
for (const child of activeChildren.keys()) {
terminateChild(child, signal);
}
}
@@ -1386,13 +1402,13 @@ async function shutdownActiveChildren(signal, exitCode) {
terminateActiveChildren("SIGKILL");
return activeChildrenShutdownPromise;
}
const children = [...activeChildren];
const children = [...activeChildren.entries()];
terminateActiveChildren(signal);
activeChildrenShutdownPromise = Promise.all(
children.map((child) =>
children.map(([child, timeoutKillGraceMs]) =>
finishTimedOutShellProcessTree(child, {
killAt: Date.now() + SHELL_TIMEOUT_KILL_GRACE_MS,
timeoutKillGraceMs: SHELL_TIMEOUT_KILL_GRACE_MS,
killAt: Date.now() + timeoutKillGraceMs,
timeoutKillGraceMs,
}),
),
).finally(() => {
@@ -1717,32 +1733,7 @@ async function main() {
}
if (profile === DEFAULT_PROFILE && selectedLaneNames.length === 0) {
const cleanupSmokeCommand = "pnpm test:docker:cleanup";
const cleanupStartedAtMs = Date.now();
let cleanupFailure;
try {
await runPhase(phases, CLEANUP_SMOKE_NAME, {}, async () => {
cleanupFailure = await runCleanupSmoke(
baseEnv,
logDir,
cleanupSmokeCommand,
cleanupStartedAtMs,
);
if (cleanupFailure) {
throw new Error(
`Run cleanup smoke after parallel lanes failed with status ${cleanupFailure.status}`,
);
}
});
} catch (error) {
cleanupFailure ??= await recordCleanupSmokeFailure(
error,
baseEnv,
logDir,
cleanupSmokeCommand,
cleanupStartedAtMs,
);
}
const cleanupFailure = await runCleanupSmokePhase(baseEnv, logDir, phases);
if (cleanupFailure) {
failures.push(cleanupFailure);
}
+60 -9
View File
@@ -596,6 +596,9 @@ async function runVitestJsonReport(params) {
env: {
...process.env,
...params.env,
// The JSON reporter can stay silent for the entire config. The profiler
// owns the wall-clock timeout and process-group cleanup for this child.
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "0",
NODE_OPTIONS: [
(params.env?.NODE_OPTIONS ?? process.env.NODE_OPTIONS)?.trim(),
...resolveVitestNodeArgs({ ...process.env, ...params.env }).filter(
@@ -908,15 +911,35 @@ export function resolveRunPlanConcurrency(args, runPlanCount) {
return Math.min(2, runPlanCount);
}
function hasExplicitIsolationArg(args) {
return args.some(
(arg) => arg === "--isolate" || arg === "--no-isolate" || arg.startsWith("--isolate="),
);
}
/**
* Gives full-suite duration reports one process lifetime per test file.
* This prevents unrelated retained module graphs and GC pauses from being
* attributed to whichever assertion happens to run next in a shared worker.
*/
export function resolveReportVitestArgs(args) {
if (!args.fullSuite || hasExplicitIsolationArg(args.vitestArgs)) {
return args.vitestArgs;
}
return [...args.vitestArgs, "--isolate=true"];
}
/**
* Builds concrete report run specs from parsed args and config plans.
*/
export function resolveReportRunSpecs(args, runPlans, params = {}) {
const concurrency = params.concurrency ?? resolveRunPlanConcurrency(args, runPlans.length);
const env = params.env ?? process.env;
const vitestArgs = resolveReportVitestArgs(args);
const specs = runPlans.map((plan) => ({
...plan,
env: resolveFullSuiteVitestEnv(args, env, plan.label),
vitestArgs,
}));
if (concurrency <= 1) {
return specs;
@@ -933,11 +956,34 @@ function printRunLine(run) {
);
}
async function runReportPlans(params) {
function printSlowTestsForRun(entry, maxTestMs) {
if (maxTestMs === null || !fs.existsSync(entry.reportPath)) {
return;
}
const input = readReportInputs([entry]).reports[0];
if (!input) {
return;
}
const report = buildGroupedTestReport({
groupBy: "area",
maxTestMs,
reports: [input],
});
for (const test of report.slowTests) {
console.log(
`[test-group-report] slow-test config=${test.config} duration=${formatMs(test.durationMs)} file=${test.file} name=${test.fullName}`,
);
}
}
export async function runReportPlans(params) {
const concurrency = resolveRunPlanConcurrency(params.args, params.runPlans.length);
const runSpecs = resolveReportRunSpecs(params.args, params.runPlans, { concurrency });
const runVitest = params.runVitestJsonReport ?? runVitestJsonReport;
const results = [];
results.length = runSpecs.length;
const runs = [];
runs.length = runSpecs.length;
let nextIndex = 0;
let failed = false;
let exitCode = 0;
@@ -948,7 +994,7 @@ async function runReportPlans(params) {
nextIndex += 1;
const plan = runSpecs[index];
const slug = sanitizePathSegment(plan.label);
const run = await runVitestJsonReport({
const run = await runVitest({
config: plan.config,
forwardedArgs: plan.forwardedArgs,
env: plan.env,
@@ -958,8 +1004,9 @@ async function runReportPlans(params) {
rss: params.args.rss,
timeoutMs: params.args.timeoutMs,
killGraceMs: params.args.killGraceMs,
vitestArgs: params.args.vitestArgs,
vitestArgs: plan.vitestArgs,
});
runs[index] = run;
printRunLine(run);
let includeEntry = true;
if (run.status !== 0) {
@@ -968,7 +1015,6 @@ async function runReportPlans(params) {
console.error(
`[test-group-report] missing JSON report for failed config; see ${run.logPath}`,
);
exitCode = 1;
includeEntry = false;
} else {
console.error(
@@ -976,12 +1022,14 @@ async function runReportPlans(params) {
);
}
if (!params.args.allowFailures) {
exitCode = run.status;
exitCode = run.status || 1;
}
}
results[index] = includeEntry
? { config: plan.label, reportPath: run.reportPath, run }
: null;
const entry = includeEntry ? { config: plan.label, reportPath: run.reportPath, run } : null;
results[index] = entry;
if (entry) {
printSlowTestsForRun(entry, params.args.maxTestMs);
}
}
}
@@ -995,6 +1043,7 @@ async function runReportPlans(params) {
failed,
exitCode,
runEntries: results.filter(Boolean),
runs: runs.filter(Boolean),
};
}
@@ -1030,6 +1079,7 @@ async function main() {
const { reportDir, logDir } = resolveReportArtifactDirs(output);
const runEntries = [];
const runs = [];
const runPlans = resolveRunPlans(args);
let failed = false;
let exitCode = 0;
@@ -1046,6 +1096,7 @@ async function main() {
failed = result.failed;
exitCode = result.exitCode;
runEntries.push(...result.runEntries);
runs.push(...result.runs);
}
if (exitCode !== 0) {
@@ -1083,7 +1134,7 @@ async function main() {
...report,
command: "test-group-report",
failed,
runs: reportInputs.map((entry) => entry.run).filter(Boolean),
runs: runs.length > 0 ? runs : reportInputs.map((entry) => entry.run).filter(Boolean),
system: {
node: process.version,
platform: process.platform,
+1 -1
View File
@@ -3186,7 +3186,7 @@ function resolveDocsI18nBehaviorTargets(changedPath) {
if (!/^scripts\/docs-i18n\/testdata\/behavior\/[^/]+\/[^/]+$/u.test(changedPath)) {
return null;
}
return ["test/scripts/docs-i18n-behavior.test.ts"];
return ["test/scripts/docs-i18n.test.ts"];
}
function resolveDocsI18nGoTargets(changedPath) {
+3 -2
View File
@@ -34,9 +34,10 @@ const CGROUP_MEMORY_LIMIT_PATHS = [
"/sys/fs/cgroup/memory/memory.limit_in_bytes",
];
const PROC_MEMINFO_PATH = "/proc/meminfo";
const TERMINATION_GRACE_MS = 5_000;
// Build descendants get a short cleanup window; a timed-out build must not hold CI for seconds.
const TERMINATION_GRACE_MS = 250;
const PROCESS_GROUP_EXIT_POLL_MS = 25;
const POST_FORCE_KILL_WAIT_MS = 1_000;
const POST_FORCE_KILL_WAIT_MS = 250;
const ROOT_TSDOWN_OUTPUT_ROOTS = ["dist", "dist-runtime"];
const PRESERVED_TSDOWN_OUTPUT_FILES = ["dist/cli-startup-metadata.json"];
const PRESERVE_CLI_STARTUP_METADATA_ENV = "OPENCLAW_PRESERVE_CLI_STARTUP_METADATA";
+2 -1
View File
@@ -13,6 +13,7 @@ const repoRoot = path.resolve(here, "..");
const uiDir = path.join(repoRoot, "ui");
const WINDOWS_CMD_EXE_EXTENSIONS = new Set([".cmd", ".bat"]);
const FORWARDED_SIGNAL_KILL_GRACE_MS = 250;
function usage() {
// keep this tiny; it's invoked from npm scripts too
@@ -140,7 +141,7 @@ function runSpawnCall(spawnCall, label) {
forwardedSignalDrainTimer = setInterval(waitForForwardedSignalChildren, 25);
forceKillTimer = setTimeout(() => {
signalProcessTree(child, "SIGKILL", forwardedSignalPids);
}, 5_000);
}, FORWARDED_SIGNAL_KILL_GRACE_MS);
forceKillTimer.unref?.();
}
},