mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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:
committed by
GitHub
parent
66fe424590
commit
b080dd1e76
@@ -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 {
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
@@ -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",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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)}`,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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 ?? [];
|
||||
|
||||
@@ -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,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) {
|
||||
|
||||
Reference in New Issue
Block a user