refactor: consolidate coercion contracts (#122458)

* refactor: consolidate coercion contracts

Centralize exact string, record, numeric, date, Boolean, argument, and structured-error coercions while preserving call-site semantics.

Migrate canonical-name collisions and deprecated internal SDK bypasses, deleting 55 net production/tooling lines. Expand declaration ownership enforcement to 101 allowed helpers and add a narrow export-completeness audit.

* fix: preserve standalone script coercions

Keep copied Control UI tooling self-contained and retain the trusted release harness module-relative source seam when the harness runs against an old target cwd.
This commit is contained in:
Peter Steinberger
2026-08-11 23:26:37 -07:00
committed by GitHub
parent 66fe424590
commit b080dd1e76
276 changed files with 1685 additions and 1663 deletions
+5 -4
View File
@@ -11,6 +11,7 @@ import {
type WorkerResult,
type WorkerScenario,
} from "./bench-agent-concurrency.js";
import { classifyBoundedUnsignedDecimal } from "./lib/arg-utils.mts";
type WorkerOptions = {
scenario: WorkerScenario;
@@ -33,14 +34,14 @@ const SCENARIOS = new Set<WorkerScenario>([
]);
function parseInteger(raw: string | undefined, flag: string, min: number, max: number): number {
if (!raw || !/^\d+$/u.test(raw)) {
const result = classifyBoundedUnsignedDecimal(raw, min, max);
if (result.kind === "syntax") {
throw new Error(`${flag} must be an integer`);
}
const value = Number(raw);
if (value < min || value > max) {
if (result.kind !== "value") {
throw new Error(`${flag} must be between ${min} and ${max}`);
}
return value;
return result.value;
}
function parseOptions(argv: string[]): WorkerOptions {
+6 -5
View File
@@ -3,6 +3,7 @@ import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { classifyBoundedUnsignedDecimal } from "./lib/arg-utils.mts";
const DEFAULT_FANOUT = [1, 8, 32, 64];
const DEFAULT_SWEEP_ROWS = [32, 128, 512];
@@ -144,17 +145,17 @@ Options:
}
function parseInteger(raw: string, flag: string, min: number, max: number): number {
if (!/^\d+$/u.test(raw)) {
const result = classifyBoundedUnsignedDecimal(raw, min, max);
if (result.kind === "syntax") {
throw new Error(`${flag} must be an integer`);
}
const value = Number(raw);
if (value < min) {
if (result.kind === "below") {
throw new Error(`${flag} must be at least ${min}`);
}
if (value > max) {
if (result.kind === "above") {
throw new Error(`${flag} must be at most ${max}`);
}
return value;
return result.value;
}
function parseList(raw: string, flag: string, max: number): number[] {
+5 -8
View File
@@ -8,6 +8,7 @@ import path from "node:path";
import { performance } from "node:perf_hooks";
import { pathToFileURL } from "node:url";
import { PROTOCOL_VERSION } from "../packages/gateway-protocol/src/version.ts";
import { asFiniteNumber } from "../packages/normalization-core/src/number-coercion.ts";
import { applyMockOpenAiModelConfig } from "./e2e/lib/fixtures/mock-openai-config.mjs";
import { delay, stopChild } from "./lib/gateway-bench-child.ts";
import { getFreePort } from "./lib/gateway-bench-probes.ts";
@@ -299,10 +300,6 @@ async function requestHttp(params: {
});
}
function numberOrNull(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function describeProbeError(error: unknown): string {
const message = error instanceof Error ? error.message : String(error);
return message.slice(0, 500);
@@ -600,10 +597,10 @@ async function sampleGateway(params: {
ok: readyz.ok && readyz.status === 200,
status: readyz.status,
degraded: typeof eventLoop?.degraded === "boolean" ? eventLoop.degraded : null,
degradedSinceMs: numberOrNull(eventLoop?.degradedSinceMs),
delayP99Ms: numberOrNull(eventLoop?.delayP99Ms),
utilization: numberOrNull(eventLoop?.utilization),
cpuCoreRatio: numberOrNull(eventLoop?.cpuCoreRatio),
degradedSinceMs: asFiniteNumber(eventLoop?.degradedSinceMs) ?? null,
delayP99Ms: asFiniteNumber(eventLoop?.delayP99Ms) ?? null,
utilization: asFiniteNumber(eventLoop?.utilization) ?? null,
cpuCoreRatio: asFiniteNumber(eventLoop?.cpuCoreRatio) ?? null,
},
sessionsList: {
atMs,
+5 -4
View File
@@ -11,6 +11,7 @@ import {
type RetainedMemoryMetrics,
type WorkerResult,
} from "./bench-task-registry-sqlite.js";
import { classifyBoundedUnsignedDecimal } from "./lib/arg-utils.mts";
type WorkerOptions = {
size: number;
@@ -44,14 +45,14 @@ type TaskRegistryQueryApi = Pick<
>;
function parseInteger(raw: string | undefined, flag: string, min: number, max: number): number {
if (!raw || !/^\d+$/u.test(raw)) {
const result = classifyBoundedUnsignedDecimal(raw, min, max);
if (result.kind === "syntax") {
throw new Error(`${flag} must be an integer`);
}
const value = Number(raw);
if (value < min || value > max) {
if (result.kind !== "value") {
throw new Error(`${flag} must be between ${min} and ${max}`);
}
return value;
return result.value;
}
function parseOptions(argv: string[]): WorkerOptions {
+6 -5
View File
@@ -4,6 +4,7 @@ import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { classifyBoundedUnsignedDecimal } from "./lib/arg-utils.mts";
const DEFAULT_SIZES = [24, 64, 128];
const WORKER_TIMEOUT_MS = 300_000;
@@ -127,17 +128,17 @@ Options:
}
function parseInteger(raw: string, flag: string, min: number, max: number): number {
if (!/^\d+$/u.test(raw)) {
const result = classifyBoundedUnsignedDecimal(raw, min, max);
if (result.kind === "syntax") {
throw new Error(`${flag} must be an integer`);
}
const value = Number(raw);
if (value < min) {
if (result.kind === "below") {
throw new Error(`${flag} must be at least ${min}`);
}
if (value > max) {
if (result.kind === "above") {
throw new Error(`${flag} must be at most ${max}`);
}
return value;
return result.value;
}
function parseList(raw: string, flag: string): number[] {
+230 -6
View File
@@ -34,14 +34,18 @@ export const CANONICAL_COERCION_HELPER_OWNERS = [
file: "packages/normalization-core/src/string-coerce.ts",
kind: "function",
names: [
"hasNonEmptyString",
"lowercasePreservingWhitespace",
"localeLowercasePreservingWhitespace",
"normalizeBoundedOptionalString",
"normalizeFastMode",
"normalizeLowercaseStringOrEmpty",
"normalizeNullableString",
"normalizeOptionalLowercaseString",
"normalizeOptionalString",
"normalizeOptionalStringifiedId",
"normalizeOptionalThreadValue",
"normalizeStringifiedOptionalString",
"normalizeStringifiedEntries",
"readNonBlankString",
"readNonEmptyStringPreservingWhitespace",
@@ -52,7 +56,27 @@ export const CANONICAL_COERCION_HELPER_OWNERS = [
{
file: "packages/normalization-core/src/string-normalization.ts",
kind: "function",
names: ["filterStringEntries"],
names: [
"filterStringEntries",
"normalizeArrayBackedTrimmedStringList",
"normalizeAtHashSlug",
"normalizeCsvOrLooseStringList",
"normalizeHyphenSlug",
"normalizeOptionalTrimmedStringList",
"normalizeSingleOrTrimmedStringList",
"normalizeSortedUniqueStringEntries",
"normalizeSortedUniqueTrimmedStringList",
"normalizeStringEntries",
"normalizeStringEntriesLower",
"normalizeTrimmedStringList",
"normalizeUniqueSingleOrTrimmedStringList",
"normalizeUniqueStringEntries",
"normalizeUniqueStringEntriesLower",
"normalizeUniqueTrimmedStringList",
"sortUniqueStrings",
"uniqueStrings",
"uniqueValues",
],
},
{
file: "packages/normalization-core/src/number-coercion.ts",
@@ -66,6 +90,7 @@ export const CANONICAL_COERCION_HELPER_OWNERS = [
"asPositiveFiniteNumber",
"asPositiveSafeInteger",
"asSafeIntegerInRange",
"clampTimerTimeoutMs",
"clampPositiveTimerTimeoutMs",
"finiteSecondsToTimerSafeMilliseconds",
"isFutureDateTimestampMs",
@@ -76,6 +101,7 @@ export const CANONICAL_COERCION_HELPER_OWNERS = [
"parseStrictFiniteNumber",
"parseStrictInteger",
"parseStrictNonNegativeInteger",
"parseStrictPositiveInteger",
"positiveSecondsToSafeMilliseconds",
"resolveDateTimestampMs",
"resolveExpiresAtMsFromDurationMs",
@@ -86,11 +112,17 @@ export const CANONICAL_COERCION_HELPER_OWNERS = [
"resolveNonNegativeIntegerOption",
"resolveOptionalIntegerOption",
"resolvePositiveTimerTimeoutMs",
"resolveTimerTimeoutMs",
"resolveTimestampMsToIsoString",
"timestampMsToIsoFileStamp",
"timestampMsToIsoString",
],
},
{
file: "packages/normalization-core/src/boolean-coercion.ts",
kind: "function",
names: ["parseBoolean"],
},
{
file: "packages/normalization-core/src/record-coerce.ts",
kind: "function",
@@ -110,22 +142,37 @@ export const CANONICAL_COERCION_HELPER_OWNERS = [
{
file: "packages/normalization-core/src/json-coercion.ts",
kind: "function",
names: ["safeParseJsonRecord"],
names: ["safeParseJson", "safeParseJsonRecord"],
},
{
file: "packages/normalization-core/src/error-coercion.ts",
kind: "function",
names: ["coerceErrorMessage", "stringifyNonErrorCause", "toErrorObject", "toStringifiedError"],
names: [
"coerceErrorMessage",
"stringifyNonErrorCause",
"toErrorObject",
"toStringifiedError",
"toStructuredErrorObject",
],
},
{
file: "scripts/lib/error-format.mts",
kind: "function",
names: ["coerceErrorMessage", "toErrorObject"],
names: ["coerceErrorMessage", "toErrorObject", "toStringifiedError"],
},
{
file: "scripts/lib/arg-utils.runtime.mjs",
kind: "function",
names: [
"classifyBoundedUnsignedDecimal",
"parsePermissiveBooleanToken",
"parseStrictBooleanArg",
],
},
{
file: "src/utils/boolean.ts",
kind: "function",
names: ["parseBooleanValue"],
names: ["asBoolean", "parseBooleanValue"],
},
] as const satisfies readonly {
file: string;
@@ -133,6 +180,31 @@ export const CANONICAL_COERCION_HELPER_OWNERS = [
names: readonly string[];
}[];
export const CANONICAL_COERCION_MODULES = [
"packages/normalization-core/src/string-coerce.ts",
"packages/normalization-core/src/string-normalization.ts",
"packages/normalization-core/src/number-coercion.ts",
"packages/normalization-core/src/record-coerce.ts",
"packages/normalization-core/src/json-coercion.ts",
"packages/normalization-core/src/error-coercion.ts",
"packages/normalization-core/src/boolean-coercion.ts",
"scripts/lib/error-format.mts",
"src/utils/boolean.ts",
] as const;
export const DEFERRED_CANONICAL_COERCION_EXPORTS = [
{
file: "packages/normalization-core/src/error-coercion.ts",
name: "formatErrorMessage",
reason: "Structural formatter shares its public name with redacting owner adapters.",
},
{
file: "scripts/lib/error-format.mts",
name: "formatErrorMessage",
reason: "Dependency-light scripts retain a deliberately smaller formatting policy.",
},
] as const satisfies readonly { file: string; name: string; reason: string }[];
const EXCEPTIONAL_COERCION_HELPER_CARVE_OUTS = [
{
file: "ui/src/test-helpers/control-ui-e2e.ts",
@@ -208,6 +280,19 @@ export type CoercionHelperDeclaration = {
name: BannedCoercionHelperName;
};
export type CanonicalCoercionExportClassification = {
file: string;
name: string;
reason?: string;
status: "deferred" | "enforced";
};
export type CanonicalCoercionExportAudit = {
invalidClassifications: string[];
staleClassifications: CanonicalCoercionExportClassification[];
unclassifiedExports: Array<{ file: string; name: string }>;
};
export type CoercionHelperCarveOut = {
count: number;
file: string;
@@ -385,6 +470,98 @@ export function findBannedCoercionHelperDeclarations(
return declarations;
}
function hasExportModifier(node: ts.Node) {
return (ts.canHaveModifiers(node) ? (ts.getModifiers(node) ?? []) : []).some(
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword,
);
}
/** Finds directly declared callable exports in one selected canonical module. */
export function findExportedCallableNames(source: string, file = "source.ts") {
const scriptKind = file.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, scriptKind);
const callableLocals = new Set<string>();
const exportedNames = new Set<string>();
for (const statement of sourceFile.statements) {
if (ts.isFunctionDeclaration(statement) && statement.name) {
callableLocals.add(statement.name.text);
if (hasExportModifier(statement)) {
exportedNames.add(statement.name.text);
}
continue;
}
if (!ts.isVariableStatement(statement)) {
continue;
}
for (const declaration of statement.declarationList.declarations) {
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) {
continue;
}
const alias = unwrapDirectAliasInitializer(declaration.initializer);
if (
!isCallableInitializer(declaration.initializer) &&
(!alias || (!ts.isIdentifier(alias) && !ts.isPropertyAccessExpression(alias)))
) {
continue;
}
callableLocals.add(declaration.name.text);
if (hasExportModifier(statement)) {
exportedNames.add(declaration.name.text);
}
}
}
for (const statement of sourceFile.statements) {
if (
!ts.isExportDeclaration(statement) ||
statement.moduleSpecifier ||
!statement.exportClause ||
!ts.isNamedExports(statement.exportClause)
) {
continue;
}
for (const element of statement.exportClause.elements) {
const localName = element.propertyName?.text ?? element.name.text;
if (!element.isTypeOnly && callableLocals.has(localName)) {
exportedNames.add(element.name.text);
}
}
}
return [...exportedNames].toSorted();
}
/** Requires every selected callable export to be enforced or explicitly deferred. */
export function auditCanonicalCoercionExports(
exportsByFile: ReadonlyMap<string, readonly string[]>,
classifications: readonly CanonicalCoercionExportClassification[],
): CanonicalCoercionExportAudit {
const invalidClassifications: string[] = [];
const byKey = new Map<string, CanonicalCoercionExportClassification>();
for (const classification of classifications) {
const key = `${classification.file}\0${classification.name}`;
if (byKey.has(key)) {
invalidClassifications.push(
`${classification.file} [${classification.name}] is classified more than once`,
);
continue;
}
if (classification.status === "deferred" && !classification.reason?.trim()) {
invalidClassifications.push(
`${classification.file} [${classification.name}] needs a non-empty deferred reason`,
);
}
byKey.set(key, classification);
}
const unclassifiedExports = [...exportsByFile].flatMap(([file, names]) =>
names.flatMap((name) => (byKey.has(`${file}\0${name}`) ? [] : [{ file, name }])),
);
const staleClassifications = classifications.filter(
({ file, name }) => !(exportsByFile.get(file) ?? []).includes(name),
);
return { invalidClassifications, staleClassifications, unclassifiedExports };
}
/** Checks exact file/name/count carve-outs and rejects stale or excess entries. */
export function auditCoercionHelperDeclarations(
declarations: readonly CoercionHelperDeclaration[],
@@ -458,6 +635,28 @@ function writeLine(stream: ScriptIo["stdout"] | ScriptIo["stderr"], value: strin
stream.write(`${value}\n`);
}
function auditDefaultCanonicalExports(repoRoot: string): CanonicalCoercionExportAudit {
const canonicalModules = new Set<string>(CANONICAL_COERCION_MODULES);
const exportsByFile = new Map(
CANONICAL_COERCION_MODULES.map((file) => {
const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
return [file, findExportedCallableNames(source, file)] as const;
}),
);
const classifications: CanonicalCoercionExportClassification[] = [
...CANONICAL_COERCION_HELPER_OWNERS.filter(({ file }) => canonicalModules.has(file)).flatMap(
({ file, names }) => names.map((name) => ({ file, name, status: "enforced" as const })),
),
...DEFERRED_CANONICAL_COERCION_EXPORTS.map(({ file, name, reason }) => ({
file,
name,
reason,
status: "deferred" as const,
})),
];
return auditCanonicalCoercionExports(exportsByFile, classifications);
}
/** Runs the full tracked-source declaration guard. */
export function runCoercionHelperDeclarationGuard(
options: {
@@ -481,10 +680,17 @@ export function runCoercionHelperDeclarationGuard(
return findBannedCoercionHelperDeclarations(fs.readFileSync(absolutePath, "utf8"), file);
});
const audit = auditCoercionHelperDeclarations(declarations, carveOuts);
const exportAudit =
options.carveOuts === undefined
? auditDefaultCanonicalExports(repoRoot)
: { invalidClassifications: [], staleClassifications: [], unclassifiedExports: [] };
const failed =
audit.excessDeclarations.length > 0 ||
audit.invalidCarveOuts.length > 0 ||
audit.staleCarveOuts.length > 0;
audit.staleCarveOuts.length > 0 ||
exportAudit.invalidClassifications.length > 0 ||
exportAudit.staleClassifications.length > 0 ||
exportAudit.unclassifiedExports.length > 0;
if (!failed) {
writeLine(
io.stdout,
@@ -517,6 +723,24 @@ export function runCoercionHelperDeclarationGuard(
);
}
}
if (exportAudit.invalidClassifications.length > 0) {
writeLine(io.stderr, "Invalid canonical-export classifications:");
for (const message of exportAudit.invalidClassifications) {
writeLine(io.stderr, `- ${message}`);
}
}
if (exportAudit.unclassifiedExports.length > 0) {
writeLine(io.stderr, "Unclassified canonical callable exports:");
for (const entry of exportAudit.unclassifiedExports) {
writeLine(io.stderr, `- ${entry.file} [${entry.name}]`);
}
}
if (exportAudit.staleClassifications.length > 0) {
writeLine(io.stderr, "Stale canonical-export classifications:");
for (const entry of exportAudit.staleClassifications) {
writeLine(io.stderr, `- ${entry.file} [${entry.name}] (${entry.status})`);
}
}
writeLine(
io.stderr,
"Core/package/UI/workspace-script code: use the matching @openclaw/normalization-core coercion subpath.",
+5 -14
View File
@@ -9,6 +9,7 @@ import path from "node:path";
import { performance } from "node:perf_hooks";
import { pathToFileURL } from "node:url";
import { gte as semverGte, valid as validSemver } from "semver";
import { coerceErrorMessage } from "./lib/error-format.mts";
import { LOCAL_BUILD_METADATA_DIST_PATHS } from "./lib/local-build-metadata-paths.mts";
import {
collectPackageDistImports,
@@ -77,7 +78,7 @@ let cliArgs: ReturnType<typeof parseArgs>;
try {
cliArgs = parseArgs(process.argv.slice(2));
} catch (error) {
fail(error instanceof Error ? error.message : String(error));
fail(coerceErrorMessage(error));
}
if (cliArgs.help) {
console.log(usage());
@@ -209,11 +210,7 @@ function collectBundledPackageRuntimeErrors({
try {
bundledPackageJson = JSON.parse(readText(manifestPath)) as Record<string, unknown>;
} catch (error) {
errors.push(
`unreadable bundled ${name} package.json: ${
error instanceof Error ? error.message : String(error)
}`,
);
errors.push(`unreadable bundled ${name} package.json: ${coerceErrorMessage(error)}`);
return errors;
}
if (bundledPackageJson.name !== name) {
@@ -587,9 +584,7 @@ if (shouldValidateShrinkwrap) {
);
}
} catch (error) {
errors.push(
`unreadable npm-shrinkwrap.json: ${error instanceof Error ? error.message : String(error)}`,
);
errors.push(`unreadable npm-shrinkwrap.json: ${coerceErrorMessage(error)}`);
}
}
if (!entrySet.has(PACKAGE_INSTALL_GUARD_RELATIVE_PATH)) {
@@ -682,11 +677,7 @@ if (entrySet.has("dist/postinstall-inventory.json")) {
}
}
} catch (error) {
errors.push(
`unreadable dist/postinstall-inventory.json: ${
error instanceof Error ? error.message : String(error)
}`,
);
errors.push(`unreadable dist/postinstall-inventory.json: ${coerceErrorMessage(error)}`);
}
}
+2 -2
View File
@@ -2,7 +2,7 @@
import { execFileSync } from "node:child_process";
import { pathToFileURL } from "node:url";
import { isRecord } from "./lib/record-shared.mjs";
function normalizeStringifiedOptionalString(value) {
function normalizeDuplicatePrListInput(value) {
if (
typeof value !== "string" &&
typeof value !== "number" &&
@@ -29,7 +29,7 @@ each duplicate has either a shared referenced issue or overlapping changed hunks
* Parses comma-separated PR numbers from CLI/env input.
*/
export function parsePrNumberList(value) {
const text = normalizeStringifiedOptionalString(value) ?? "";
const text = normalizeDuplicatePrListInput(value) ?? "";
return [
...new Set(
text
+3 -3
View File
@@ -2274,9 +2274,9 @@ function parsePosixProcessRows(stdout: string) {
) {
continue;
}
const processId = parseStrictPositiveInteger(pidRaw);
const processId = parsePositivePosixProcessToken(pidRaw);
const parentProcessId = parseStrictUnsignedInteger(ppidRaw);
const rssKb = parseStrictPositiveInteger(rssKbRaw);
const rssKb = parsePositivePosixProcessToken(rssKbRaw);
const cpuPercent = parseStrictNonNegativeDecimal(cpuRaw);
if (
!Number.isInteger(processId) ||
@@ -2320,7 +2320,7 @@ function parseStrictUnsignedInteger(raw: string | undefined) {
return Number.isSafeInteger(parsed) ? parsed : null;
}
function parseStrictPositiveInteger(raw: string | undefined) {
function parsePositivePosixProcessToken(raw: string | undefined) {
const parsed = parseStrictUnsignedInteger(raw);
return parsed && parsed > 0 ? parsed : null;
}
+2 -2
View File
@@ -11,7 +11,7 @@ function parseExpectedStatus(raw) {
return Number(raw);
}
function resolveTimerTimeoutMs(valueMs, fallbackMs) {
function resolveOpenWebUiHttpProbeTimeoutMs(valueMs, fallbackMs) {
const value = Number.isFinite(valueMs) ? valueMs : fallbackMs;
return Math.min(Math.max(Math.floor(value), 1), MAX_TIMER_TIMEOUT_MS);
}
@@ -27,7 +27,7 @@ export async function probeHttpStatus({
throw new Error("usage: http-probe.mjs <url> [status|lt500]");
}
const expectedStatus = expectedRaw === "lt500" ? undefined : parseExpectedStatus(expectedRaw);
const resolvedTimeoutMs = resolveTimerTimeoutMs(timeoutMs, 30_000);
const resolvedTimeoutMs = resolveOpenWebUiHttpProbeTimeoutMs(timeoutMs, 30_000);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), resolvedTimeoutMs);
let res;
@@ -50,17 +50,17 @@ function readPositiveNumberEnv(name, fallback) {
const MAX_TIMER_TIMEOUT_MS = 2_147_000_000;
function clampTimerTimeoutMs(valueMs) {
function clampPluginLifecycleTimerMs(valueMs) {
return Math.min(Math.max(Math.floor(valueMs), 1), MAX_TIMER_TIMEOUT_MS);
}
const pollMs = clampTimerTimeoutMs(
const pollMs = clampPluginLifecycleTimerMs(
readPositiveIntEnv("OPENCLAW_PLUGIN_LIFECYCLE_METRIC_POLL_MS", 100),
);
const timeoutMs = clampTimerTimeoutMs(
const timeoutMs = clampPluginLifecycleTimerMs(
readPositiveIntEnv("OPENCLAW_PLUGIN_LIFECYCLE_PHASE_TIMEOUT_MS", 300000),
);
const timeoutKillGraceMs = clampTimerTimeoutMs(
const timeoutKillGraceMs = clampPluginLifecycleTimerMs(
readPositiveIntEnv("OPENCLAW_PLUGIN_LIFECYCLE_TIMEOUT_KILL_GRACE_MS", 2000),
);
const maxRssKbThreshold = readPositiveIntEnv(
+3 -3
View File
@@ -9,7 +9,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import type { QaProviderMode } from "../../extensions/qa-lab/src/run-config.ts";
import type { QaSuiteRoundTripProbe } from "../../extensions/qa-lab/src/suite-round-trip.ts";
function parseBoolean(value: string | undefined) {
function isTruthyNpmTelegramEnvValue(value: string | undefined) {
const normalized = value?.trim().toLowerCase();
return normalized === "1" || normalized === "true" || normalized === "yes";
}
@@ -147,7 +147,7 @@ async function shouldFailPackageTelegramRun(
result: { summaryPath: string },
env: NodeJS.ProcessEnv = process.env,
) {
if (parseBoolean(env.OPENCLAW_NPM_TELEGRAM_ALLOW_FAILURES)) {
if (isTruthyNpmTelegramEnvValue(env.OPENCLAW_NPM_TELEGRAM_ALLOW_FAILURES)) {
return false;
}
const { readQaSuiteFailedOrSkippedScenarioCountFromFile } =
@@ -224,7 +224,7 @@ async function main() {
providerMode,
primaryModel,
alternateModel: process.env.OPENCLAW_NPM_TELEGRAM_ALT_MODEL,
fastMode: parseBoolean(process.env.OPENCLAW_NPM_TELEGRAM_FAST),
fastMode: isTruthyNpmTelegramEnvValue(process.env.OPENCLAW_NPM_TELEGRAM_FAST),
scenarioIds,
resolvedScenarioIds: prioritizeRoundTripProbeScenario(resolvedScenarioIds, rttOptions),
roundTripProbe: createRoundTripProbe(rttOptions),
+5 -5
View File
@@ -70,18 +70,18 @@ function readNonNegativeInt(name, fallback) {
return parsed;
}
function clampTimerTimeoutMs(valueMs, minMs = 1) {
function clampOpenWebUiTimerTimeoutMs(valueMs, minMs = 1) {
const min = Math.max(0, Math.floor(minMs));
const value = Number.isFinite(valueMs) ? valueMs : min;
return Math.min(Math.max(Math.floor(value), min), MAX_TIMER_TIMEOUT_MS);
}
function readPositiveTimerMs(name, fallback) {
return clampTimerTimeoutMs(readPositiveInt(name, fallback));
return clampOpenWebUiTimerTimeoutMs(readPositiveInt(name, fallback));
}
function readNonNegativeTimerMs(name, fallback) {
return clampTimerTimeoutMs(readNonNegativeInt(name, fallback), 0);
return clampOpenWebUiTimerTimeoutMs(readNonNegativeInt(name, fallback), 0);
}
function createTimeoutError(label, timeoutMs) {
@@ -91,7 +91,7 @@ function createTimeoutError(label, timeoutMs) {
}
async function withRequestTimeout(label, timeoutMs, run) {
const resolvedTimeoutMs = clampTimerTimeoutMs(timeoutMs);
const resolvedTimeoutMs = clampOpenWebUiTimerTimeoutMs(timeoutMs);
const controller = new AbortController();
const timeoutError = createTimeoutError(label, resolvedTimeoutMs);
let timer;
@@ -156,7 +156,7 @@ function buildAuthHeaders(token, cookie) {
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, clampTimerTimeoutMs(ms, 0));
setTimeout(resolve, clampOpenWebUiTimerTimeoutMs(ms, 0));
});
}
+4 -14
View File
@@ -14,6 +14,8 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { clampTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion";
import { sliceUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import { parseStrictBooleanArg } from "../lib/arg-utils.mts";
import { coerceErrorMessage } from "../lib/error-format.mts";
import { sleep } from "../lib/sleep.mjs";
import { resolveWindowsTaskkillPath } from "../lib/windows-taskkill.mjs";
import { createPnpmRunnerSpawnSpec } from "../pnpm-runner.mts";
@@ -303,16 +305,6 @@ function parseTcpPort(value: string, label: string) {
return parsed;
}
function parseBoolean(value: string, label: string) {
if (value === "true") {
return true;
}
if (value === "false") {
return false;
}
throw new Error(`${label} must be true or false.`);
}
function createTelegramProofRunId() {
return `${new Date().toISOString().replace(/[:.]/gu, "-")}-${randomUUID().slice(0, 8)}`;
}
@@ -417,7 +409,7 @@ export function parseArgs(argvInput: string[]): Options {
} else if (arg === "--keep-box") {
opts.keepBox = true;
} else if (arg === "--link-preview") {
opts.linkPreview = parseBoolean(readValue(), "--link-preview");
opts.linkPreview = parseStrictBooleanArg(readValue(), "--link-preview");
} else if (arg === "--mock-port") {
opts.mockPort = parseTcpPort(readValue(), "--mock-port");
} else if (arg === "--mock-response-file") {
@@ -1656,9 +1648,7 @@ function destroyLocalSutRuntime(sut: { containerName?: string; tempRoot?: string
}
function cleanupFailureMessage(message: string, cleanupErrors: unknown[]) {
const details = cleanupErrors.map((error) =>
error instanceof Error ? error.message : String(error),
);
const details = cleanupErrors.map(coerceErrorMessage);
return [message, ...details.map((detail) => `Cleanup failure: ${detail}`)].join("\n");
}
+4 -8
View File
@@ -5,6 +5,7 @@ import { existsSync as existsSyncImpl, realpathSync } from "node:fs";
import { isAbsolute, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { chromium } from "playwright";
import { parsePermissiveBooleanToken } from "./lib/arg-utils.mts";
import { resolveRepoRoot } from "./lib/repo-root.mjs";
import { resolvePnpmRunner, type PnpmRunnerParams } from "./pnpm-runner.mts";
@@ -91,11 +92,6 @@ export function resolvePlaywrightInstallRunner(options: PlaywrightRunnerOptions
});
}
function isTruthyEnvFlag(value: unknown) {
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
}
/**
* Reports whether Linux system dependencies should be installed with Chromium.
*/
@@ -112,9 +108,9 @@ export function shouldInstallPlaywrightSystemDependencies(
return true;
}
return (
isTruthyEnvFlag(env.CI) ||
isTruthyEnvFlag(env.GITHUB_ACTIONS) ||
isTruthyEnvFlag(env.OPENCLAW_TESTBOX)
parsePermissiveBooleanToken(env.CI) === true ||
parsePermissiveBooleanToken(env.GITHUB_ACTIONS) === true ||
parsePermissiveBooleanToken(env.OPENCLAW_TESTBOX) === true
);
}
@@ -2,6 +2,7 @@
// Generate Bundled Channel Config Metadata script supports OpenClaw repository automation.
import fs from "node:fs";
import path from "node:path";
import { asFiniteNumber } from "../packages/normalization-core/src/number-coercion.ts";
import { loadBundledPluginPublicArtifactModuleSync } from "../src/plugins/public-surface-loader.js";
import { isDirectRunUrl } from "./lib/direct-run.mjs";
import { loadChannelConfigSurfaceModule } from "./load-channel-config-surface.ts";
@@ -157,7 +158,7 @@ function resolveRootAliases(source: BundledPluginSource, channelId: string): str
function resolveRootOrder(source: BundledPluginSource, channelId: string): number | undefined {
const channelMeta = resolvePackageChannelMeta(source);
const order = channelMeta?.id === channelId ? channelMeta.order : undefined;
return typeof order === "number" && Number.isFinite(order) ? order : undefined;
return asFiniteNumber(order);
}
function resolveRootConfigurable(source: BundledPluginSource, channelId: string): boolean {
+69
View File
@@ -0,0 +1,69 @@
type StringOptions = {
allowEmpty?: boolean;
allowInline?: boolean;
missingValueMessage?: string;
rejectShortOptions?: boolean;
repeatable?: boolean;
transform?: (value: string) => unknown;
};
type ConsumedFlag<T extends Record<string, unknown>> = {
flag?: string;
nextIndex: number;
repeatable?: boolean;
apply(target: T): void;
};
type FlagSpec<T extends Record<string, unknown>> = {
consume(argv: readonly string[], index: number, args: T): ConsumedFlag<T> | null;
};
type ParseOptions<T extends Record<string, unknown>> = {
allowUnknownOptions?: boolean;
duplicateOptionMessage?: (flag: string) => string;
ignoreDoubleDash?: boolean;
onUnhandledArg?: (arg: string, args: T) => "handled" | void;
};
export type BoundedUnsignedDecimalResult =
| { kind: "syntax" }
| { kind: "below" }
| { kind: "above" }
| { kind: "value"; value: number };
export function readFlagValue(args: readonly string[], name: string): string | undefined;
export function stripLeadingPackageManagerSeparator(argv: string[]): string[];
export function parseStrictBooleanArg(value: unknown, label: string): boolean;
export function classifyBoundedUnsignedDecimal(
value: unknown,
min: number,
max: number,
): BoundedUnsignedDecimalResult;
export function parsePermissiveBooleanToken(value: unknown): boolean | undefined;
export function stringFlag<T extends Record<string, unknown>>(
flag: string,
key: string,
options?: StringOptions,
): FlagSpec<T>;
export function stringListFlag<T extends Record<string, unknown>>(
flag: string,
key: string,
options?: Omit<StringOptions, "repeatable" | "transform">,
): FlagSpec<T>;
export function intFlag<T extends Record<string, unknown>>(
flag: string,
key: string,
options?: { min?: number },
): FlagSpec<T>;
export function booleanFlag<T extends Record<string, unknown>>(
flag: string,
key: string,
value?: unknown,
options?: { repeatable?: boolean },
): FlagSpec<T>;
export function parseFlagArgs<T extends Record<string, unknown>>(
argv: readonly string[],
args: T,
specs: readonly FlagSpec<T>[],
options?: ParseOptions<T>,
): T;
+54
View File
@@ -47,6 +47,9 @@
* onUnhandledArg?: (arg: string, args: T) => "handled" | void,
* }} ParseOptions
*/
/**
* @typedef {{ kind: "syntax" } | { kind: "below" } | { kind: "above" } | { kind: "value", value: number }} BoundedUnsignedDecimalResult
*/
/** @param {string} message */
function failFlagParse(message) {
throw new Error(message);
@@ -170,6 +173,57 @@ function readFlagOptionValue(argv, index, flag) {
}
return { nextIndex: index + 1, value };
}
/**
* Parse the exact lowercase Boolean language used by strict script arguments.
* @param {unknown} value
* @param {string} label
*/
export function parseStrictBooleanArg(value, label) {
if (value === "true") {
return true;
}
if (value === "false") {
return false;
}
throw new Error(`${label} must be true or false.`);
}
/**
* Classify an ASCII unsigned-decimal token against inclusive bounds.
* @param {unknown} value
* @param {number} min
* @param {number} max
* @returns {BoundedUnsignedDecimalResult}
*/
export function classifyBoundedUnsignedDecimal(value, min, max) {
if (typeof value !== "string" || !/^\d+$/u.test(value)) {
return { kind: "syntax" };
}
const parsed = Number(value);
if (parsed < min) {
return { kind: "below" };
}
if (parsed > max) {
return { kind: "above" };
}
return { kind: "value", value: parsed };
}
const PERMISSIVE_BOOLEAN_TRUE_TOKENS = new Set(["1", "on", "true", "yes"]);
const PERMISSIVE_BOOLEAN_FALSE_TOKENS = new Set(["0", "false", "no", "off"]);
/**
* Parse the normalized Boolean token language shared by repository scripts.
* @param {unknown} value
* @returns {boolean | undefined}
*/
export function parsePermissiveBooleanToken(value) {
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
if (!normalized) {
return undefined;
}
if (PERMISSIVE_BOOLEAN_TRUE_TOKENS.has(normalized)) {
return true;
}
return PERMISSIVE_BOOLEAN_FALSE_TOKENS.has(normalized) ? false : undefined;
}
/**
* @param {string} raw
* @param {string} flag
@@ -1,5 +1,6 @@
import type { ChildProcess } from "node:child_process";
import { basename, dirname, resolve, win32 as pathWin32 } from "node:path";
import { parsePermissiveBooleanToken } from "../arg-utils.mts";
import { trimForSummary } from "./shared.ts";
type CrossOsSuite = "packaged-fresh" | "installer-fresh" | "packaged-upgrade" | "dev-update";
@@ -314,11 +315,9 @@ function parseBooleanEnv(name: string, fallback: boolean, env = process.env): bo
if (!raw) {
return fallback;
}
if (/^(1|true|yes|on)$/iu.test(raw)) {
return true;
}
if (/^(0|false|no|off)$/iu.test(raw)) {
return false;
const parsed = parsePermissiveBooleanToken(raw);
if (parsed !== undefined) {
return parsed;
}
throw new Error(`${name} must be a boolean. Got: ${JSON.stringify(raw)}`);
}
@@ -15,6 +15,7 @@ import {
import { dirname } from "node:path";
import { StringDecoder } from "node:string_decoder";
import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "../../windows-cmd-helpers.mjs";
import { toStringifiedError } from "../error-format.mts";
import { resolveWindowsTaskkillPath } from "../windows-taskkill.mjs";
import type {
Cleanup,
@@ -559,8 +560,7 @@ export async function startStaticFileServer(params: {
server.close((error) => {
void (async () => {
const closeLogError = await finishStaticFileServerLog(logStream, logStreamError).catch(
(logError: unknown): Error =>
logError instanceof Error ? logError : new Error(String(logError)),
(logError: unknown): Error => toStringifiedError(logError),
);
if (error) {
rejectPromise(error);
@@ -51,4 +51,8 @@ export const BANNED_INTERNAL_PLUGIN_SDK_FACADE_MODULES: BannedInternalPluginSdkF
modulePath: "src/plugin-sdk/inbound-envelope",
canonical: "openclaw/plugin-sdk/channel-inbound",
},
{
modulePath: "src/plugin-sdk/text-runtime",
canonical: "the focused typed public Plugin SDK subpath for the imported helper",
},
];
+5 -6
View File
@@ -2,6 +2,7 @@
import path from "node:path";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import { redactSensitiveText } from "../../src/logging/redact.js";
import { parsePermissiveBooleanToken } from "./arg-utils.mts";
export { parseStrictIntegerOption } from "./strict-integer-option.ts";
@@ -50,15 +51,13 @@ export function parseBooleanEnv(params: {
name: string;
raw: string | undefined;
}): boolean {
const raw = params.raw?.trim().toLowerCase();
const raw = params.raw?.trim();
if (!raw) {
return params.fallback;
}
if (["1", "true", "yes", "on"].includes(raw)) {
return true;
}
if (["0", "false", "no", "off"].includes(raw)) {
return false;
const parsed = parsePermissiveBooleanToken(raw);
if (parsed !== undefined) {
return parsed;
}
throw new Error(
`${params.name} must be one of 1,0,true,false,yes,no,on,off; got ${JSON.stringify(params.raw)}`,
+5
View File
@@ -12,6 +12,11 @@ export function coerceErrorMessage(value: unknown): string {
return value instanceof Error ? value.message : String(value);
}
/** Preserve Error values and stringify every other value without workspace dependencies. */
export function toStringifiedError(value: unknown): Error {
return value instanceof Error ? value : new Error(String(value));
}
/** Preserve structured non-Error failures without requiring built workspace packages. */
export function toErrorObject(value: unknown, fallbackMessage: string): Error {
if (value instanceof Error) {
+5 -6
View File
@@ -11,9 +11,9 @@ export type LocalVitestScheduling = {
};
import os from "node:os";
import { parsePermissiveBooleanToken } from "./arg-utils.mts";
const MAX_LOCAL_FULL_SUITE_PARALLELISM = 10;
const TRUTHY_ENV_VALUES = new Set(["1", "true", "yes", "on"]);
const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value));
@@ -37,13 +37,12 @@ function isSystemThrottleDisabled(env: Record<string, string | undefined>) {
return normalized === "1" || normalized === "true";
}
function isTruthyEnvValue(value: string | undefined) {
return TRUTHY_ENV_VALUES.has(value?.trim().toLowerCase() ?? "");
}
/** @internal Shared repository-script contract. */
export function isCiLikeEnv(env: Record<string, string | undefined> = process.env) {
return isTruthyEnvValue(env.CI) || isTruthyEnvValue(env.GITHUB_ACTIONS);
return (
parsePermissiveBooleanToken(env.CI) === true ||
parsePermissiveBooleanToken(env.GITHUB_ACTIONS) === true
);
}
/** @internal Shared repository-script contract. */
@@ -30,7 +30,7 @@ function parseJson(source: string): JsonValue {
}
function isJsonObject(value: JsonValue | undefined): value is JsonObject {
return value !== null && typeof value === "object" && !Array.isArray(value);
return isRecord(value);
}
function valueAt(value: JsonValue | undefined, ...keys: string[]): JsonValue | undefined {
@@ -1,16 +1,7 @@
#!/usr/bin/env -S node --import tsx
import { parseStrictBooleanArg } from "./lib/arg-utils.mts";
import { buildOpenClawReleaseClawHubRuntimeState } from "./lib/openclaw-release-clawhub-plan.ts";
function parseBoolean(value: string, label: string): boolean {
if (value === "true") {
return true;
}
if (value === "false") {
return false;
}
throw new Error(`${label} must be true or false.`);
}
function parseArgs(argv: string[]) {
const values = [...argv];
if (values[0] === "--") {
@@ -40,10 +31,10 @@ function parseArgs(argv: string[]) {
repository = next();
break;
case "--wait-for-clawhub":
waitForClawHub = parseBoolean(next(), "--wait-for-clawhub");
waitForClawHub = parseStrictBooleanArg(next(), "--wait-for-clawhub");
break;
case "--force-skip-clawhub":
forceSkipClawHub = parseBoolean(next(), "--force-skip-clawhub");
forceSkipClawHub = parseStrictBooleanArg(next(), "--force-skip-clawhub");
break;
case "--normal-run-id":
normalRunId = next();
@@ -52,7 +43,7 @@ function parseArgs(argv: string[]) {
bootstrapRunId = next();
break;
case "--bootstrap-completed":
bootstrapCompleted = parseBoolean(next(), "--bootstrap-completed");
bootstrapCompleted = parseStrictBooleanArg(next(), "--bootstrap-completed");
break;
default:
throw new Error(`Unknown argument: ${arg}`);
+6 -3
View File
@@ -166,7 +166,10 @@ function numericTimerValueMs(valueMs: unknown) {
return Number.isFinite(value) ? Math.floor(value) : undefined;
}
function resolveTimerTimeoutMs(valueMs: unknown, fallbackMs: unknown = MAX_TIMER_TIMEOUT_MS) {
function resolvePackageBuildTimeoutMs(
valueMs: unknown,
fallbackMs: unknown = MAX_TIMER_TIMEOUT_MS,
) {
const value = numericTimerValueMs(valueMs) ?? numericTimerValueMs(fallbackMs);
return Math.min(Math.max(value ?? MAX_TIMER_TIMEOUT_MS, 1), MAX_TIMER_TIMEOUT_MS);
}
@@ -175,7 +178,7 @@ function resolveOptionalTimerTimeoutMs(valueMs: unknown) {
if (valueMs === undefined) {
return undefined;
}
return resolveTimerTimeoutMs(valueMs, 1);
return resolvePackageBuildTimeoutMs(valueMs, 1);
}
function readOptionValue(argv: string[], index: number, optionName: string) {
@@ -304,7 +307,7 @@ export function parseArgs(argv: string[]) {
function run(command: string, args: string[], cwd: string, options: RunOptions = {}) {
return new Promise<string>((resolve, reject) => {
const resolvedTimeoutMs = resolveOptionalTimerTimeoutMs(options.timeoutMs);
const resolvedKillAfterMs = resolveTimerTimeoutMs(
const resolvedKillAfterMs = resolvePackageBuildTimeoutMs(
options.killAfterMs,
DEFAULT_TIMEOUT_KILL_AFTER_MS,
);
+3 -3
View File
@@ -708,7 +708,7 @@ function parsePositiveIntegerEnv(name, fallback) {
}
function resolveBulkAdvisoryRequestTimeoutMs() {
return clampTimerTimeoutMs(
return clampBulkAdvisoryTimeoutMs(
parsePositiveIntegerEnv(
"OPENCLAW_PNPM_AUDIT_BULK_TIMEOUT_MS",
BULK_ADVISORY_REQUEST_TIMEOUT_MS,
@@ -723,13 +723,13 @@ function resolveBulkAdvisoryResponseBodyMaxBytes() {
);
}
function clampTimerTimeoutMs(valueMs) {
function clampBulkAdvisoryTimeoutMs(valueMs) {
const value = Number.isFinite(valueMs) ? valueMs : BULK_ADVISORY_REQUEST_TIMEOUT_MS;
return Math.min(Math.max(Math.floor(value), 1), MAX_TIMER_TIMEOUT_MS);
}
async function withBulkAdvisoryTimeout({ label, timeoutMs, run }) {
const resolvedTimeoutMs = clampTimerTimeoutMs(timeoutMs);
const resolvedTimeoutMs = clampBulkAdvisoryTimeoutMs(timeoutMs);
const controller = new AbortController();
let timeout;
const timeoutPromise = new Promise((_resolve, reject) => {
+10 -4
View File
@@ -325,7 +325,10 @@ function numericTimerValueMs(valueMs: unknown) {
return Number.isFinite(value) ? Math.floor(value) : undefined;
}
function resolveTimerTimeoutMs(valueMs: unknown, fallbackMs: unknown = MAX_TIMER_TIMEOUT_MS) {
function resolvePackageCandidateTimeoutMs(
valueMs: unknown,
fallbackMs: unknown = MAX_TIMER_TIMEOUT_MS,
) {
const value = numericTimerValueMs(valueMs) ?? numericTimerValueMs(fallbackMs);
return Math.min(Math.max(value ?? MAX_TIMER_TIMEOUT_MS, 1), MAX_TIMER_TIMEOUT_MS);
}
@@ -334,13 +337,13 @@ function resolveOptionalTimerTimeoutMs(valueMs: unknown) {
if (valueMs === undefined) {
return undefined;
}
return resolveTimerTimeoutMs(valueMs, 1);
return resolvePackageCandidateTimeoutMs(valueMs, 1);
}
function run(command: string, args: readonly string[], options: RunOptions = {}) {
return new Promise<string>((resolve, reject) => {
const resolvedTimeoutMs = resolveOptionalTimerTimeoutMs(options.timeoutMs);
const resolvedKillAfterMs = resolveTimerTimeoutMs(
const resolvedKillAfterMs = resolvePackageCandidateTimeoutMs(
options.killAfterMs,
COMMAND_TIMEOUT_KILL_AFTER_MS,
);
@@ -1505,7 +1508,10 @@ async function openHttpsPackageDownloadResponse(
async function openPackageDownloadResponse(url: string, options: PackageDownloadOptions) {
const lookupHost = options.lookupHost ?? defaultLookupHost;
const timeoutMs = resolveTimerTimeoutMs(options.timeoutMs, PACKAGE_URL_DOWNLOAD_TIMEOUT_MS);
const timeoutMs = resolvePackageCandidateTimeoutMs(
options.timeoutMs,
PACKAGE_URL_DOWNLOAD_TIMEOUT_MS,
);
const maxRedirects = options.maxRedirects ?? PACKAGE_URL_MAX_REDIRECTS;
const trustedSource = options.trustedSource;
let parsed = new URL(url);
+4 -8
View File
@@ -10,6 +10,7 @@ import { embeddedAgentVitestProjectOwners } from "../test/vitest/vitest.agents-p
import { toolingIsolatedTestFiles } from "../test/vitest/vitest.tooling-isolated-paths.mjs";
import { isUiTestTarget } from "../test/vitest/vitest.ui-paths.mjs";
import { boundaryTestFiles } from "../test/vitest/vitest.unit-paths.mjs";
import { parsePermissiveBooleanToken } from "./lib/arg-utils.mts";
import { runWithFailedTrailer, writeFailedTrailer } from "./lib/failed-trailer.mts";
import { createGatewayServerTestTargetChunks } from "./lib/gateway-server-test-plan.mts";
import { signalExitCode } from "./lib/managed-child-process.mts";
@@ -35,7 +36,6 @@ type WatchdogStream = {
off(event: string, listener: (...args: unknown[]) => void): unknown;
};
const TRUTHY_ENV_VALUES = new Set(["1", "true", "yes", "on"]);
const ANSI_CSI_PREFIX = `${String.fromCharCode(27)}[`;
const ANSI_CSI_SUFFIX_RE = /^[0-?]*[ -/]*[@-~]/u;
const SUPPRESSED_VITEST_STDERR_PATTERNS = ["[PLUGIN_TIMINGS]"];
@@ -154,10 +154,6 @@ const UNBOUNDED_CONFIG_ONLY_OPTIONS = [
const require = createRequire(import.meta.url);
const repoRoot = resolveRepoRoot(import.meta.url);
function isTruthyEnvValue(value: string | undefined): boolean {
return TRUTHY_ENV_VALUES.has(value?.trim().toLowerCase() ?? "");
}
function parsePositiveInt(value: string | undefined): number | null {
const text = value?.trim();
if (!text || !/^\d+$/u.test(text)) {
@@ -171,7 +167,7 @@ function parsePositiveInt(value: string | undefined): number | null {
* Resolves default Node flags for Vitest, including the local Maglev opt-in.
*/
export function resolveVitestNodeArgs(env: NodeJS.ProcessEnv = process.env): string[] {
if (isTruthyEnvValue(env.OPENCLAW_VITEST_ENABLE_MAGLEV)) {
if (parsePermissiveBooleanToken(env.OPENCLAW_VITEST_ENABLE_MAGLEV) === true) {
return [];
}
@@ -484,7 +480,7 @@ export function resolveRunVitestSpawnEnv(
if (explicitMode === "watch") {
return baseEnv;
}
if (explicitMode !== "run" && !isTruthyEnvValue(baseEnv.CI)) {
if (explicitMode !== "run" && parsePermissiveBooleanToken(baseEnv.CI) !== true) {
return baseEnv;
}
const defaultTimeoutMs = resolveDefaultVitestNoOutputTimeoutMs(argv);
@@ -587,7 +583,7 @@ export function resolveBoundedVitestInvocations(
if (
!matchesVitestConfigPath(normalizedConfig, GATEWAY_SERVER_VITEST_CONFIG) ||
mode === "watch" ||
(mode !== "run" && !isTruthyEnvValue(env.CI)) ||
(mode !== "run" && parsePermissiveBooleanToken(env.CI) !== true) ||
hasNonRunVitestSubcommand(argv) ||
hasAlternateVitestRootArg(argv) ||
collectExplicitProjectRouterTargetArgs(argv, cwd).length > 0 ||
+7 -4
View File
@@ -255,7 +255,10 @@ function numericTimerValueMs(valueMs: unknown) {
return Number.isFinite(value) ? Math.floor(value) : undefined;
}
function resolveTimerTimeoutMs(valueMs: unknown, fallbackMs: unknown = MAX_TIMER_TIMEOUT_MS) {
function resolveDockerSchedulerTimeoutMs(
valueMs: unknown,
fallbackMs: unknown = MAX_TIMER_TIMEOUT_MS,
) {
const value = numericTimerValueMs(valueMs) ?? numericTimerValueMs(fallbackMs);
return Math.min(Math.max(value ?? MAX_TIMER_TIMEOUT_MS, 1), MAX_TIMER_TIMEOUT_MS);
}
@@ -265,7 +268,7 @@ function resolveOptionalTimerTimeoutMs(valueMs: unknown) {
if (value === undefined || value <= 0) {
return undefined;
}
return resolveTimerTimeoutMs(value);
return resolveDockerSchedulerTimeoutMs(value);
}
function resourceLimitsSummary(resourceLimits: Record<string, number>) {
@@ -819,7 +822,7 @@ export function runShellCommand({
return new Promise<ShellCommandResult>((resolve) => {
const resolvedTimeoutMs = resolveOptionalTimerTimeoutMs(timeoutMs);
const resolvedNoOutputTimeoutMs = resolveOptionalTimerTimeoutMs(noOutputTimeoutMs);
const resolvedTimeoutKillGraceMs = resolveTimerTimeoutMs(
const resolvedTimeoutKillGraceMs = resolveDockerSchedulerTimeoutMs(
timeoutKillGraceMs,
SHELL_TIMEOUT_KILL_GRACE_MS,
);
@@ -951,7 +954,7 @@ export function runShellCaptureCommand({
}
return new Promise<ShellCaptureResult>((resolve) => {
const resolvedTimeoutMs = resolveOptionalTimerTimeoutMs(timeoutMs);
const resolvedTimeoutKillGraceMs = resolveTimerTimeoutMs(
const resolvedTimeoutKillGraceMs = resolveDockerSchedulerTimeoutMs(
timeoutKillGraceMs,
SHELL_TIMEOUT_KILL_GRACE_MS,
);
+8 -15
View File
@@ -4,7 +4,9 @@ import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { asSafeIntegerInRange } from "../packages/normalization-core/src/number-coercion.ts";
import { isRecord as isUnknownRecord } from "../packages/normalization-core/src/record-coerce.ts";
import { parsePermissiveBooleanToken } from "./lib/arg-utils.mts";
import { spawnPnpmRunner, type PnpmRunnerParams } from "./pnpm-runner.mts";
import {
createVitestProcessCompletion,
@@ -444,18 +446,6 @@ function collectReportedLiveTestFiles(payload: unknown, repoRoot = process.cwd()
);
}
function readOptionalNonNegativeInt(value: unknown) {
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
}
function isTruthyEnvValue(value: string | undefined) {
if (typeof value !== "string") {
return false;
}
const normalized = value.trim().toLowerCase();
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
}
function isDisabledOptInAssertion(assertion: Record<string, unknown>) {
if (assertion.status !== "passed") {
return false;
@@ -496,8 +486,8 @@ function buildFilePassEvidence(result: Record<string, unknown>) {
return evidence;
}
evidence.passed =
readOptionalNonNegativeInt(result.numPassingTests) ??
readOptionalNonNegativeInt(result.numPassedTests) ??
asSafeIntegerInRange(result.numPassingTests, { min: 0 }) ??
asSafeIntegerInRange(result.numPassedTests, { min: 0 }) ??
0;
return evidence;
}
@@ -538,7 +528,10 @@ function isDisabledOptionalLiveShardFile(
env: NodeJS.ProcessEnv = process.env,
) {
const requiredEnvNames = OPTIONAL_LIVE_SHARD_FILE_ENVS.get(file);
if (!requiredEnvNames || requiredEnvNames.some((name) => isTruthyEnvValue(env[name]))) {
if (
!requiredEnvNames ||
requiredEnvNames.some((name) => parsePermissiveBooleanToken(env[name]) === true)
) {
return false;
}
const statuses = evidence?.statuses ?? [];
+2 -2
View File
@@ -84,6 +84,7 @@ import {
detectChangedLanes,
listChangedPathsFromGit as listChangedPathsFromGitSource,
} from "./changed-lanes.mts";
import { parsePermissiveBooleanToken } from "./lib/arg-utils.mts";
import { getChangedPathFacts } from "./lib/changed-path-facts.mjs";
import { createExtensionTestProcessTargetChunks } from "./lib/extension-test-plan.mts";
import {
@@ -2901,8 +2902,7 @@ function resolveToolingTestTargets(changedPath: string, cwd = process.cwd()) {
}
function shouldUseBroadChangedTargets(env = process.env) {
const value = env[BROAD_CHANGED_ENV_KEY]?.trim().toLowerCase();
return ["1", "true", "yes", "on"].includes(value ?? "");
return parsePermissiveBooleanToken(env[BROAD_CHANGED_ENV_KEY]) === true;
}
function isRoutableChangedTarget(changedPath: string) {
+2 -2
View File
@@ -2,6 +2,7 @@
import { execFileSync, spawnSync } from "node:child_process";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { parseDateStringTimestampMs } from "@openclaw/normalization-core/number-coercion";
import { isRecord, readStringField } from "@openclaw/normalization-core/record-coerce";
import { minimatch } from "minimatch";
import { parse } from "yaml";
@@ -288,8 +289,7 @@ function latestRun(runs: WorkflowRun[]) {
}
function runUpdatedAtMs(run: Pick<WorkflowRun, "updated_at"> | undefined) {
const value = Date.parse(run?.updated_at ?? "");
return Number.isFinite(value) ? value : null;
return parseDateStringTimestampMs(run?.updated_at) ?? null;
}
function isRecentRun(run: WorkflowRun | undefined, nowMs: number) {