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