mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor: consolidate coercion ownership (#122692)
* refactor: consolidate coercion ownership * test: align shard check with weighted planning * chore: refresh plugin SDK API baseline
This commit is contained in:
committed by
GitHub
parent
7c58151445
commit
c23d66e3b5
+7
-14
@@ -9,6 +9,7 @@ import { performance } from "node:perf_hooks";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { asRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import prettyMilliseconds from "pretty-ms";
|
||||
import { resolveBuildIdentityEnvironment } from "./lib/build-identity.mts";
|
||||
import {
|
||||
listPluginSdkDeclarationOutputs,
|
||||
pluginSdkEntrypoints,
|
||||
@@ -59,7 +60,6 @@ type BuildAllStepParams = {
|
||||
comSpec?: string;
|
||||
};
|
||||
type BuildAllCacheParams = { rootDir?: string; fs?: BuildAllFs; env?: NodeJS.ProcessEnv };
|
||||
const FULL_GIT_COMMIT_RE = /^[0-9a-f]{40}$/iu;
|
||||
const BUILD_CACHE_VERSION = 4;
|
||||
const TSDOWN_DECLARATION_EXTENSIONS = [".d.ts", ".d.mts", ".d.cts"];
|
||||
const TSDOWN_SOURCE_EXTENSIONS = [
|
||||
@@ -491,19 +491,12 @@ export function resolveBuildAllEnvironment(
|
||||
now: () => Date = () => new Date(),
|
||||
readGitCommit: () => string | null = readCurrentGitCommit,
|
||||
) {
|
||||
const explicitTimestamp = env.OPENCLAW_BUILD_TIMESTAMP?.trim();
|
||||
const explicitCommit = env.GIT_COMMIT?.trim() || env.GIT_SHA?.trim();
|
||||
const checkedOutCommit = explicitCommit ? null : readGitCommit()?.trim();
|
||||
// GITHUB_SHA names the workflow invocation and can differ from a checked-out tag.
|
||||
const commit = explicitCommit || checkedOutCommit || env.GITHUB_SHA?.trim();
|
||||
if (commit && !FULL_GIT_COMMIT_RE.test(commit)) {
|
||||
throw new Error("build commit must be a full 40-character hexadecimal SHA");
|
||||
}
|
||||
return {
|
||||
...env,
|
||||
OPENCLAW_BUILD_TIMESTAMP: explicitTimestamp || now().toISOString(),
|
||||
...(commit ? { GIT_COMMIT: commit.toLowerCase() } : {}),
|
||||
};
|
||||
return resolveBuildIdentityEnvironment({
|
||||
commitLabel: "build commit",
|
||||
env,
|
||||
now,
|
||||
readGitCommit,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveStepEnv(step: BuildAllStep, env: NodeJS.ProcessEnv, platform: NodeJS.Platform) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
||||
import { stableStringify } from "../packages/normalization-core/src/stable-stringify.ts";
|
||||
import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mts";
|
||||
import { getChangedPathFacts, normalizeChangedPath } from "./lib/changed-path-facts.mjs";
|
||||
import { isDirectRunUrl } from "./lib/direct-run.mjs";
|
||||
@@ -448,8 +449,8 @@ export function isLiveDockerPackageScriptOnlyChange(before: string, after: strin
|
||||
const afterStripped = stripLiveDockerPackageScripts(afterPackage);
|
||||
|
||||
return (
|
||||
stableJson(beforeStripped) === stableJson(afterStripped) &&
|
||||
stableJson(beforeAllowed) !== stableJson(afterAllowed)
|
||||
stableStringify(beforeStripped) === stableStringify(afterStripped) &&
|
||||
stableStringify(beforeAllowed) !== stableStringify(afterAllowed)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -469,8 +470,8 @@ export function isPackageScriptOnlyChange(before: string, after: string): boolea
|
||||
const afterStripped = stripPackageScripts(afterPackage);
|
||||
|
||||
return (
|
||||
stableJson(beforeStripped) === stableJson(afterStripped) &&
|
||||
stableJson(beforeScripts) !== stableJson(afterScripts)
|
||||
stableStringify(beforeStripped) === stableStringify(afterStripped) &&
|
||||
stableStringify(beforeScripts) !== stableStringify(afterScripts)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -542,19 +543,6 @@ function stripPackageScripts(packageJson: Record<string, unknown>) {
|
||||
return clone;
|
||||
}
|
||||
|
||||
function stableJson(value: unknown): string {
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.map(stableJson).join(",")}]`;
|
||||
}
|
||||
if (isRecord(value)) {
|
||||
return `{${Object.keys(value)
|
||||
.toSorted((left, right) => left.localeCompare(right))
|
||||
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
||||
.join(",")}}`;
|
||||
}
|
||||
return JSON.stringify(value) ?? "undefined";
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes changed-lane booleans to the GitHub Actions output file.
|
||||
*/
|
||||
|
||||
+11
-11
@@ -20,7 +20,12 @@ import {
|
||||
listStagedChangedPaths,
|
||||
} from "./changed-lanes.mts";
|
||||
import type { ChangedLaneResult } from "./changed-lanes.mts";
|
||||
import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mts";
|
||||
import {
|
||||
booleanFlag,
|
||||
isOpenEndedTruthyValue,
|
||||
parseFlagArgs,
|
||||
stringFlag,
|
||||
} from "./lib/arg-utils.mts";
|
||||
import { getChangedPathFacts, normalizeChangedPath } from "./lib/changed-path-facts.mjs";
|
||||
import { printTimingSummary } from "./lib/check-timing-summary.mts";
|
||||
import { isDirectRunUrl } from "./lib/direct-run.mjs";
|
||||
@@ -157,11 +162,6 @@ export function createChangedCheckChildEnv(baseEnv: NodeJS.ProcessEnv = process.
|
||||
};
|
||||
}
|
||||
|
||||
function isTruthyEnvFlag(value: string | undefined) {
|
||||
const normalized = (value ?? "").trim().toLowerCase();
|
||||
return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "no";
|
||||
}
|
||||
|
||||
function hasAndroidVersionSyncPath(paths: string[]) {
|
||||
return paths.some((changedPath) =>
|
||||
ANDROID_VERSION_SYNC_PATHS.has(normalizeChangedPath(changedPath)),
|
||||
@@ -231,10 +231,10 @@ export function shouldDelegateChangedCheckToCrabbox(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
options: ChangedCheckDelegateOptions = {},
|
||||
) {
|
||||
if (isTruthyEnvFlag(env.OPENCLAW_CHECK_CHANGED_REMOTE_CHILD)) {
|
||||
if (isOpenEndedTruthyValue(env.OPENCLAW_CHECK_CHANGED_REMOTE_CHILD)) {
|
||||
return false;
|
||||
}
|
||||
if (isTruthyEnvFlag(env.CI) || isTruthyEnvFlag(env.GITHUB_ACTIONS)) {
|
||||
if (isOpenEndedTruthyValue(env.CI) || isOpenEndedTruthyValue(env.GITHUB_ACTIONS)) {
|
||||
return false;
|
||||
}
|
||||
if (argv.includes("--dry-run")) {
|
||||
@@ -247,7 +247,7 @@ export function shouldDelegateChangedCheckToCrabbox(
|
||||
if (result.paths.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (isTruthyEnvFlag(env.OPENCLAW_TESTBOX)) {
|
||||
if (isOpenEndedTruthyValue(env.OPENCLAW_TESTBOX)) {
|
||||
return true;
|
||||
}
|
||||
// Release metadata plans diff the supplied commits after classification. A missing
|
||||
@@ -708,7 +708,7 @@ export function createChangedCheckPlan(
|
||||
add("package patch guard", ["deps:patches:check"]);
|
||||
if (
|
||||
hasDeadcodeScannedSource(result.paths) &&
|
||||
!isTruthyEnvFlag(baseEnv.OPENCLAW_CHECK_CHANGED_SKIP_DEADCODE)
|
||||
!isOpenEndedTruthyValue(baseEnv.OPENCLAW_CHECK_CHANGED_SKIP_DEADCODE)
|
||||
) {
|
||||
addCommand(
|
||||
"dead export scan (skip with OPENCLAW_CHECK_CHANGED_SKIP_DEADCODE=1)",
|
||||
@@ -1066,7 +1066,7 @@ export function createPnpmManagedCommand<T extends ChangedCheckCommand>(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
) {
|
||||
const commandEnv = command.env ?? resolveLocalHeavyCheckEnv(env);
|
||||
if (isTruthyEnvFlag(commandEnv.CI) || isTruthyEnvFlag(commandEnv.GITHUB_ACTIONS)) {
|
||||
if (isOpenEndedTruthyValue(commandEnv.CI) || isOpenEndedTruthyValue(commandEnv.GITHUB_ACTIONS)) {
|
||||
const shimmedEnv = prependCorepackPnpmShim(commandEnv);
|
||||
return {
|
||||
...command,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { isCodeFile, listRepoFilesSync } from "./check-file-utils.js";
|
||||
import { isDirectRunUrl } from "./lib/direct-run.mjs";
|
||||
import { runWithFailedTrailer } from "./lib/failed-trailer.mts";
|
||||
import { resolveRepoRoot } from "./lib/repo-root.mjs";
|
||||
import { toLine, unwrapExpression } from "./lib/ts-guard-utils.mts";
|
||||
import { getPropertyNameText, toLine, unwrapExpression } from "./lib/ts-guard-utils.mts";
|
||||
|
||||
const ABSOLUTE_LEGACY_COERCION_HELPER_NAMES = [
|
||||
"asObject",
|
||||
@@ -30,6 +30,11 @@ export type CoercionHelperDeclarationKind =
|
||||
| "variable";
|
||||
|
||||
export const CANONICAL_COERCION_HELPER_OWNERS = [
|
||||
{
|
||||
file: "packages/normalization-core/src/agent-id.ts",
|
||||
kind: "function",
|
||||
names: ["isValidAgentId", "normalizeAgentId"],
|
||||
},
|
||||
{
|
||||
file: "packages/normalization-core/src/string-coerce.ts",
|
||||
kind: "function",
|
||||
@@ -181,6 +186,7 @@ export const CANONICAL_COERCION_HELPER_OWNERS = [
|
||||
}[];
|
||||
|
||||
export const CANONICAL_COERCION_MODULES = [
|
||||
"packages/normalization-core/src/agent-id.ts",
|
||||
"packages/normalization-core/src/string-coerce.ts",
|
||||
"packages/normalization-core/src/string-normalization.ts",
|
||||
"packages/normalization-core/src/number-coercion.ts",
|
||||
@@ -192,6 +198,8 @@ export const CANONICAL_COERCION_MODULES = [
|
||||
"src/utils/boolean.ts",
|
||||
] as const;
|
||||
|
||||
const MIXED_CANONICAL_COERCION_MODULES = ["scripts/lib/arg-utils.runtime.mjs"] as const;
|
||||
|
||||
export const DEFERRED_CANONICAL_COERCION_EXPORTS = [
|
||||
{
|
||||
file: "packages/normalization-core/src/error-coercion.ts",
|
||||
@@ -210,28 +218,24 @@ const EXCEPTIONAL_COERCION_HELPER_CARVE_OUTS = [
|
||||
file: "ui/src/test-helpers/control-ui-e2e.ts",
|
||||
name: "isRecord",
|
||||
kind: "function",
|
||||
count: 1,
|
||||
reason: "Serialized mock Gateway closure cannot capture module imports.",
|
||||
},
|
||||
{
|
||||
file: "scripts/lib/kova-report-gate.mts",
|
||||
name: "isRecord",
|
||||
kind: "function",
|
||||
count: 1,
|
||||
reason: "Copied standalone report gate cannot rely on workspace package resolution.",
|
||||
},
|
||||
{
|
||||
file: "scripts/lib/record-shared.mjs",
|
||||
name: "isRecord",
|
||||
kind: "function",
|
||||
count: 1,
|
||||
reason: "Plain-Node shared helper serves MJS and E2E callers without package resolution.",
|
||||
},
|
||||
{
|
||||
file: "scripts/pr-lib/process-group-runner.mjs",
|
||||
name: "toError",
|
||||
kind: "function",
|
||||
count: 1,
|
||||
reason:
|
||||
"Bootstrap process supervisor preserves fallback errors without workspace dependencies.",
|
||||
},
|
||||
@@ -239,11 +243,9 @@ const EXCEPTIONAL_COERCION_HELPER_CARVE_OUTS = [
|
||||
file: "scripts/lib/bounded-response.mjs",
|
||||
name: "toLintErrorObject",
|
||||
kind: "function",
|
||||
count: 1,
|
||||
reason: "Standalone copied response reader cannot resolve workspace packages.",
|
||||
},
|
||||
] as const satisfies readonly {
|
||||
count: number;
|
||||
file: string;
|
||||
kind: CoercionHelperDeclarationKind;
|
||||
name: string;
|
||||
@@ -294,7 +296,6 @@ export type CanonicalCoercionExportAudit = {
|
||||
};
|
||||
|
||||
export type CoercionHelperCarveOut = {
|
||||
count: number;
|
||||
file: string;
|
||||
kind: CoercionHelperDeclarationKind;
|
||||
name: BannedCoercionHelperName;
|
||||
@@ -308,7 +309,6 @@ function canonicalOwnerCarveOuts(
|
||||
file: owner.file,
|
||||
kind: owner.kind,
|
||||
name,
|
||||
count: 1,
|
||||
reason: "Canonical coercion helper owned by this module.",
|
||||
}));
|
||||
}
|
||||
@@ -318,14 +318,10 @@ export const COERCION_HELPER_CARVE_OUTS: readonly CoercionHelperCarveOut[] = [
|
||||
...EXCEPTIONAL_COERCION_HELPER_CARVE_OUTS,
|
||||
];
|
||||
|
||||
type CarveOutMismatch = CoercionHelperCarveOut & {
|
||||
actualCount: number;
|
||||
};
|
||||
|
||||
type CoercionHelperAudit = {
|
||||
excessDeclarations: CoercionHelperDeclaration[];
|
||||
invalidCarveOuts: string[];
|
||||
staleCarveOuts: CarveOutMismatch[];
|
||||
staleCarveOuts: CoercionHelperCarveOut[];
|
||||
};
|
||||
|
||||
type ScriptIo = {
|
||||
@@ -362,16 +358,6 @@ export function isGovernedCoercionHelperPath(filePath: string) {
|
||||
);
|
||||
}
|
||||
|
||||
function propertyNameText(name: ts.PropertyName | undefined): string | undefined {
|
||||
if (!name) {
|
||||
return undefined;
|
||||
}
|
||||
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
||||
return name.text;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isCallableInitializer(expression: ts.Expression): boolean {
|
||||
const initializer = unwrapCallableInitializer(expression);
|
||||
return ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer);
|
||||
@@ -434,7 +420,7 @@ export function findBannedCoercionHelperDeclarations(
|
||||
});
|
||||
}
|
||||
} else if (ts.isMethodDeclaration(node)) {
|
||||
const name = propertyNameText(node.name);
|
||||
const name = getPropertyNameText(node.name);
|
||||
if (name && BANNED_HELPER_NAMES.has(name)) {
|
||||
declarations.push({
|
||||
file,
|
||||
@@ -444,7 +430,7 @@ export function findBannedCoercionHelperDeclarations(
|
||||
});
|
||||
}
|
||||
} else if (ts.isPropertyDeclaration(node) && node.initializer) {
|
||||
const name = propertyNameText(node.name);
|
||||
const name = getPropertyNameText(node.name);
|
||||
if (name && BANNED_HELPER_NAMES.has(name) && isCallableInitializer(node.initializer)) {
|
||||
declarations.push({
|
||||
file,
|
||||
@@ -454,7 +440,7 @@ export function findBannedCoercionHelperDeclarations(
|
||||
});
|
||||
}
|
||||
} else if (ts.isPropertyAssignment(node)) {
|
||||
const name = propertyNameText(node.name);
|
||||
const name = getPropertyNameText(node.name);
|
||||
if (name && BANNED_HELPER_NAMES.has(name) && isCallableInitializer(node.initializer)) {
|
||||
declarations.push({
|
||||
file,
|
||||
@@ -562,7 +548,7 @@ export function auditCanonicalCoercionExports(
|
||||
return { invalidClassifications, staleClassifications, unclassifiedExports };
|
||||
}
|
||||
|
||||
/** Checks exact file/name/count carve-outs and rejects stale or excess entries. */
|
||||
/** Checks exact file/name/kind carve-outs and rejects stale or excess entries. */
|
||||
export function auditCoercionHelperDeclarations(
|
||||
declarations: readonly CoercionHelperDeclaration[],
|
||||
carveOuts: readonly CoercionHelperCarveOut[],
|
||||
@@ -583,9 +569,6 @@ export function auditCoercionHelperDeclarations(
|
||||
`${carveOut.file} [${carveOut.name}] has invalid kind ${carveOut.kind}`,
|
||||
);
|
||||
}
|
||||
if (!Number.isInteger(carveOut.count) || carveOut.count < 1) {
|
||||
invalidCarveOuts.push(`${carveOut.file} [${carveOut.name}] must have a positive count`);
|
||||
}
|
||||
if (!carveOut.reason.trim()) {
|
||||
invalidCarveOuts.push(`${carveOut.file} [${carveOut.name}] needs a non-empty reason`);
|
||||
}
|
||||
@@ -602,22 +585,15 @@ export function auditCoercionHelperDeclarations(
|
||||
|
||||
const excessDeclarations: CoercionHelperDeclaration[] = [];
|
||||
for (const [key, actual] of declarationsByKey) {
|
||||
const allowedCount = carveOutByKey.get(key)?.count ?? 0;
|
||||
if (actual.length > allowedCount) {
|
||||
excessDeclarations.push(...actual.slice(allowedCount));
|
||||
if (carveOutByKey.has(key)) {
|
||||
excessDeclarations.push(...actual.slice(1));
|
||||
} else {
|
||||
excessDeclarations.push(...actual);
|
||||
}
|
||||
}
|
||||
const staleCarveOuts = carveOuts
|
||||
.map((carveOut): CarveOutMismatch | null => {
|
||||
const actual = declarationsByKey.get(carveOutKey(carveOut)) ?? [];
|
||||
return actual.length < carveOut.count
|
||||
? {
|
||||
...carveOut,
|
||||
actualCount: actual.length,
|
||||
}
|
||||
: null;
|
||||
})
|
||||
.filter((entry): entry is CarveOutMismatch => entry !== null);
|
||||
const staleCarveOuts = carveOuts.filter(
|
||||
(carveOut) => !declarationsByKey.has(carveOutKey(carveOut)),
|
||||
);
|
||||
|
||||
return {
|
||||
excessDeclarations: excessDeclarations.toSorted(
|
||||
@@ -637,15 +613,28 @@ function writeLine(stream: ScriptIo["stdout"] | ScriptIo["stderr"], value: strin
|
||||
|
||||
function auditDefaultCanonicalExports(repoRoot: string): CanonicalCoercionExportAudit {
|
||||
const canonicalModules = new Set<string>(CANONICAL_COERCION_MODULES);
|
||||
const mixedModules = new Set<string>(MIXED_CANONICAL_COERCION_MODULES);
|
||||
const auditedModules = [...CANONICAL_COERCION_MODULES, ...MIXED_CANONICAL_COERCION_MODULES];
|
||||
const exportsByFile = new Map(
|
||||
CANONICAL_COERCION_MODULES.map((file) => {
|
||||
auditedModules.map((file) => {
|
||||
const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
|
||||
return [file, findExportedCallableNames(source, file)] as const;
|
||||
const exportedNames = findExportedCallableNames(source, file);
|
||||
if (!mixedModules.has(file)) {
|
||||
return [file, exportedNames] as const;
|
||||
}
|
||||
const registeredNames = new Set<string>(
|
||||
CANONICAL_COERCION_HELPER_OWNERS.filter((owner) => owner.file === file).flatMap(
|
||||
(owner) => owner.names,
|
||||
),
|
||||
);
|
||||
return [file, exportedNames.filter((name) => registeredNames.has(name))] 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 })),
|
||||
...CANONICAL_COERCION_HELPER_OWNERS.filter(
|
||||
({ file }) => canonicalModules.has(file) || mixedModules.has(file),
|
||||
).flatMap(({ file, names }) =>
|
||||
names.map((name) => ({ file, name, status: "enforced" as const })),
|
||||
),
|
||||
...DEFERRED_CANONICAL_COERCION_EXPORTS.map(({ file, name, reason }) => ({
|
||||
file,
|
||||
@@ -719,7 +708,7 @@ export function runCoercionHelperDeclarationGuard(
|
||||
for (const carveOut of audit.staleCarveOuts) {
|
||||
writeLine(
|
||||
io.stderr,
|
||||
`- ${carveOut.file} [${carveOut.name}] expected ${carveOut.count} ${carveOut.kind} declaration(s), found ${carveOut.actualCount}; remove or reduce the carve-out`,
|
||||
`- ${carveOut.file} [${carveOut.name}] has no ${carveOut.kind} declaration; remove the carve-out`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -743,11 +732,11 @@ export function runCoercionHelperDeclarationGuard(
|
||||
}
|
||||
writeLine(
|
||||
io.stderr,
|
||||
"Core/package/UI/workspace-script code: use the matching @openclaw/normalization-core coercion subpath.",
|
||||
"Core/package/UI/workspace-script code: use the matching @openclaw/normalization-core export or module.",
|
||||
);
|
||||
writeLine(
|
||||
io.stderr,
|
||||
"Plugin production code: use openclaw/plugin-sdk/string-coerce-runtime, number-runtime, or error-runtime.",
|
||||
"Bundled plugin production code: use the matching openclaw/plugin-sdk runtime; number-runtime is bundled/private-local, not a third-party typed contract.",
|
||||
);
|
||||
writeLine(
|
||||
io.stderr,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
// Inventories extension imports to enforce plugin SDK boundary rules.
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import {
|
||||
BUNDLED_PLUGIN_PATH_PREFIX,
|
||||
BUNDLED_PLUGIN_ROOT_DIR,
|
||||
@@ -22,7 +20,6 @@ import { runAsScript } from "./lib/ts-guard-utils.mts";
|
||||
const DEFAULT_REPO_ROOT = resolveRepoRoot(import.meta.url);
|
||||
type BoundaryMode =
|
||||
| "src-outside-plugin-sdk"
|
||||
| "plugin-sdk-internal"
|
||||
| "relative-outside-package"
|
||||
| "normalization-core-bypass";
|
||||
type ModuleReference = { kind: string; line: number; specifier: string };
|
||||
@@ -36,7 +33,6 @@ type BoundaryCheckIo = {
|
||||
|
||||
const MODES = new Set<BoundaryMode>([
|
||||
"src-outside-plugin-sdk",
|
||||
"plugin-sdk-internal",
|
||||
"relative-outside-package",
|
||||
"normalization-core-bypass",
|
||||
]);
|
||||
@@ -44,27 +40,16 @@ const MODES = new Set<BoundaryMode>([
|
||||
const ruleTextByMode: Record<BoundaryMode, string> = {
|
||||
"src-outside-plugin-sdk":
|
||||
"Rule: production bundled plugins must not import src/** outside src/plugin-sdk/**",
|
||||
"plugin-sdk-internal":
|
||||
"Rule: production bundled plugins must not import src/plugin-sdk-internal/**",
|
||||
"relative-outside-package":
|
||||
"Rule: production bundled plugins must not use relative imports that escape their own package root",
|
||||
"normalization-core-bypass":
|
||||
"Rule: production bundled plugins must not import normalization-core directly; use the matching openclaw/plugin-sdk coercion runtime",
|
||||
};
|
||||
|
||||
type BaselineBoundaryMode = "plugin-sdk-internal" | "src-outside-plugin-sdk";
|
||||
const NORMALIZATION_CORE_PACKAGE = "@openclaw/normalization-core";
|
||||
const NORMALIZATION_CORE_ROOT = "packages/normalization-core";
|
||||
const DIRECT_COERCION_OWNER_PATHS = new Set(["src/infra/errors", "src/utils/boolean"]);
|
||||
|
||||
function baselinePathForMode(repoRoot: string, mode: BaselineBoundaryMode): string {
|
||||
const fileName =
|
||||
mode === "src-outside-plugin-sdk"
|
||||
? "extension-src-outside-plugin-sdk-inventory.json"
|
||||
: "extension-plugin-sdk-internal-inventory.json";
|
||||
return path.join(repoRoot, "test", "fixtures", fileName);
|
||||
}
|
||||
|
||||
function stripModuleExtension(filePath: string): string {
|
||||
return filePath.replace(/\.(?:[cm]?[jt]s|tsx|jsx)$/u, "");
|
||||
}
|
||||
@@ -127,9 +112,12 @@ function classifyReason(mode: BoundaryMode, kind: string, resolved: string, spec
|
||||
: "imports";
|
||||
if (mode === "normalization-core-bypass") {
|
||||
const facade = recommendedCoercionFacade(resolved);
|
||||
if (facade === "openclaw/plugin-sdk/number-runtime") {
|
||||
return `${verb} ${specifier} directly; bundled plugin production code must use bundled/private-local ${facade}`;
|
||||
}
|
||||
return facade
|
||||
? `${verb} ${specifier} directly; plugin production code must use ${facade}`
|
||||
: `${verb} ${specifier} directly; plugin production code must use the matching public openclaw/plugin-sdk facade, adding a narrow public SDK seam if needed`;
|
||||
? `${verb} ${specifier} directly; bundled plugin production code must use ${facade}`
|
||||
: `${verb} ${specifier} directly; bundled plugin production code must use the matching openclaw/plugin-sdk facade, adding a narrow public SDK seam if needed`;
|
||||
}
|
||||
if (mode === "relative-outside-package") {
|
||||
if (resolved.startsWith("src/plugin-sdk/")) {
|
||||
@@ -143,9 +131,6 @@ function classifyReason(mode: BoundaryMode, kind: string, resolved: string, spec
|
||||
}
|
||||
return `${verb} relative path ${specifier} outside the extension package`;
|
||||
}
|
||||
if (mode === "plugin-sdk-internal") {
|
||||
return `${verb} src/plugin-sdk-internal from an extension`;
|
||||
}
|
||||
if (resolved.startsWith("src/plugin-sdk/")) {
|
||||
return `${verb} allowed plugin-sdk path`;
|
||||
}
|
||||
@@ -163,57 +148,6 @@ function compareEntries(left: BoundaryEntry, right: BoundaryEntry): number {
|
||||
);
|
||||
}
|
||||
|
||||
function isBoundaryEntry(value: unknown): value is BoundaryEntry {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
typeof value.file === "string" &&
|
||||
typeof value.line === "number" &&
|
||||
typeof value.kind === "string" &&
|
||||
typeof value.specifier === "string" &&
|
||||
typeof value.resolvedPath === "string" &&
|
||||
typeof value.reason === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function isBoundaryEntryArray(value: unknown): value is BoundaryEntry[] {
|
||||
return Array.isArray(value) && value.every(isBoundaryEntry);
|
||||
}
|
||||
|
||||
async function readExpectedInventoryAtRoot(
|
||||
repoRoot: string,
|
||||
mode: BaselineBoundaryMode,
|
||||
): Promise<BoundaryEntry[]> {
|
||||
const baselinePath = baselinePathForMode(repoRoot, mode);
|
||||
try {
|
||||
const inventory: unknown = JSON.parse(await fs.readFile(baselinePath, "utf8"));
|
||||
if (!isBoundaryEntryArray(inventory)) {
|
||||
throw new Error(`Invalid boundary inventory: ${baselinePath}`);
|
||||
}
|
||||
return inventory;
|
||||
} catch (error) {
|
||||
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Diffs expected and actual boundary inventory entries.
|
||||
*/
|
||||
export function diffInventory(expected: BoundaryEntry[], actual: BoundaryEntry[]) {
|
||||
const expectedKeys = new Set(expected.map((entry) => JSON.stringify(entry)));
|
||||
const actualKeys = new Set(actual.map((entry) => JSON.stringify(entry)));
|
||||
return {
|
||||
missing: expected
|
||||
.filter((entry) => !actualKeys.has(JSON.stringify(entry)))
|
||||
.toSorted(compareEntries),
|
||||
unexpected: actual
|
||||
.filter((entry) => !expectedKeys.has(JSON.stringify(entry)))
|
||||
.toSorted(compareEntries),
|
||||
};
|
||||
}
|
||||
|
||||
const formatInventoryHuman = (mode: BoundaryMode, inventory: BoundaryEntry[]): string =>
|
||||
formatGroupedInventoryHuman(
|
||||
{
|
||||
@@ -254,9 +188,6 @@ export function createExtensionPluginSdkBoundaryChecker(options: { repoRoot?: st
|
||||
if (resolvedPath.startsWith("src/") && !resolvedPath.startsWith("src/plugin-sdk/")) {
|
||||
modes.push("src-outside-plugin-sdk");
|
||||
}
|
||||
if (resolvedPath.startsWith("src/plugin-sdk-internal/")) {
|
||||
modes.push("plugin-sdk-internal");
|
||||
}
|
||||
if (isNormalizationCoreBypass(specifier, resolvedPath)) {
|
||||
modes.push("normalization-core-bypass");
|
||||
}
|
||||
@@ -347,42 +278,16 @@ export function createExtensionPluginSdkBoundaryChecker(options: { repoRoot?: st
|
||||
}
|
||||
|
||||
const actual = await collectInventory(mode);
|
||||
const strictMode = mode === "normalization-core-bypass" || mode === "relative-outside-package";
|
||||
if (json) {
|
||||
writeLine(streams.stdout, JSON.stringify(actual, null, 2));
|
||||
return strictMode && actual.length > 0 ? 1 : 0;
|
||||
return actual.length > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
writeLine(streams.stdout, formatInventoryHuman(mode, actual));
|
||||
if (strictMode) {
|
||||
if (actual.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
writeLine(
|
||||
streams.stderr,
|
||||
`${ruleTextByMode[mode]} violations found (${actual.length}); this strict mode has no baseline.`,
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const expected = await readExpectedInventoryAtRoot(repoRoot, mode);
|
||||
const diff = diffInventory(expected, actual);
|
||||
if (diff.missing.length === 0 && diff.unexpected.length === 0) {
|
||||
writeLine(streams.stdout, `Baseline matches (${actual.length} entries).`);
|
||||
if (actual.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (diff.missing.length > 0) {
|
||||
writeLine(streams.stderr, `Missing baseline entries (${diff.missing.length}):`);
|
||||
for (const entry of diff.missing) {
|
||||
writeLine(streams.stderr, ` - ${entry.file}:${entry.line} ${entry.reason}`);
|
||||
}
|
||||
}
|
||||
if (diff.unexpected.length > 0) {
|
||||
writeLine(streams.stderr, `Unexpected inventory entries (${diff.unexpected.length}):`);
|
||||
for (const entry of diff.unexpected) {
|
||||
writeLine(streams.stderr, ` - ${entry.file}:${entry.line} ${entry.reason}`);
|
||||
}
|
||||
}
|
||||
writeLine(streams.stderr, `${ruleTextByMode[mode]} violations found (${actual.length}).`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -391,11 +296,6 @@ export function createExtensionPluginSdkBoundaryChecker(options: { repoRoot?: st
|
||||
|
||||
const defaultBoundaryChecker = createExtensionPluginSdkBoundaryChecker();
|
||||
|
||||
/** Reads the checked-in expected boundary inventory from the real repository. */
|
||||
export async function readExpectedInventory(mode: BaselineBoundaryMode): Promise<BoundaryEntry[]> {
|
||||
return await readExpectedInventoryAtRoot(DEFAULT_REPO_ROOT, mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entrypoint wrapper for the extension plugin SDK boundary check.
|
||||
*/
|
||||
|
||||
@@ -14,9 +14,12 @@ import {
|
||||
writeBuildStamp,
|
||||
writeRuntimePostBuildStamp,
|
||||
} from "./lib/local-build-metadata.mts";
|
||||
import { parseStrictNonNegativeDecimal as readNonNegativeInteger } from "./lib/numeric-options.mjs";
|
||||
import { sleep } from "./lib/sleep.mjs";
|
||||
import { resolveBuildRequirement } from "./run-node.mts";
|
||||
|
||||
export { readNonNegativeInteger };
|
||||
|
||||
const DEFAULTS = {
|
||||
outputDir: path.join(process.cwd(), ".local", "gateway-watch-regression"),
|
||||
windowMs: 10_000,
|
||||
@@ -52,7 +55,6 @@ const WATCH_GATEWAY_SKIP_ENV = {
|
||||
export const WATCH_LOG_CAPTURE_MAX_CHARS = 2 * 1024 * 1024;
|
||||
export const WATCH_LOG_FAILURE_TAIL_CHARS = 12_000;
|
||||
const WATCH_BUILD_DETECTION_MAX_CHARS = 4096;
|
||||
const NON_NEGATIVE_INTEGER_PATTERN = /^(0|[1-9]\d*)$/u;
|
||||
const ANSI_ESCAPE_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g");
|
||||
|
||||
type WatchOptions = typeof DEFAULTS;
|
||||
@@ -204,21 +206,6 @@ export function updateWatchBuildDetection(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a safe non-negative integer CLI value.
|
||||
*/
|
||||
export function readNonNegativeInteger(value: unknown, label: string): number {
|
||||
const raw = String(value).trim();
|
||||
if (!NON_NEGATIVE_INTEGER_PATTERN.test(raw)) {
|
||||
throw new Error(`${label} must be a non-negative integer`);
|
||||
}
|
||||
const parsed = Number(raw);
|
||||
if (!Number.isSafeInteger(parsed)) {
|
||||
throw new Error(`${label} must be a safe integer`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses gateway watch regression CLI arguments.
|
||||
*/
|
||||
|
||||
@@ -8,10 +8,15 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { safeParseJson } from "../packages/normalization-core/src/json-coercion.ts";
|
||||
import { resolveTimerTimeoutMs } from "../packages/normalization-core/src/number-coercion.ts";
|
||||
import { asNullableRecord as asRecord } from "../packages/normalization-core/src/record-coerce.ts";
|
||||
import { readNonBlankString } from "../packages/normalization-core/src/string-coerce.ts";
|
||||
import { stripLeadingPackageManagerSeparator } from "./lib/arg-utils.mts";
|
||||
import { readBoundedResponseText } from "./lib/bounded-response.mjs";
|
||||
import { parseStrictNonNegativeDecimal as parseNonNegativeInteger } from "./lib/numeric-options.mjs";
|
||||
|
||||
export { parseNonNegativeInteger };
|
||||
|
||||
const ISSUE_FILE_COUNTS = [
|
||||
["memory/transcripts", 9394],
|
||||
@@ -99,7 +104,6 @@ Options:
|
||||
`.trim();
|
||||
}
|
||||
|
||||
const NON_NEGATIVE_INTEGER_PATTERN = /^(0|[1-9]\d*)$/u;
|
||||
const ARGUMENT_FLAGS = new Set([
|
||||
"--allow-non-darwin",
|
||||
"--expect-leak",
|
||||
@@ -123,21 +127,6 @@ function stripPackageManagerSeparatorForKnownFlags(argv: string[]) {
|
||||
: argv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a safe non-negative integer option.
|
||||
*/
|
||||
export function parseNonNegativeInteger(value: unknown, label: string) {
|
||||
const raw = String(value).trim();
|
||||
if (!NON_NEGATIVE_INTEGER_PATTERN.test(raw)) {
|
||||
throw new Error(`${label} must be a non-negative integer`);
|
||||
}
|
||||
const parsed = Number(raw);
|
||||
if (!Number.isSafeInteger(parsed)) {
|
||||
throw new Error(`${label} must be a safe integer`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a safe positive integer option.
|
||||
*/
|
||||
@@ -615,19 +604,6 @@ async function waitForChildExit(
|
||||
return hasChildExited(child);
|
||||
}
|
||||
|
||||
function parseJsonValue(text: string): unknown {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readStringProperty(record: Record<string, unknown> | null, key: string) {
|
||||
const value = record?.[key];
|
||||
return typeof value === "string" && value.trim() ? value : undefined;
|
||||
}
|
||||
|
||||
function parseToolTextContent(result: Record<string, unknown> | null) {
|
||||
const content = Array.isArray(result?.content) ? result.content : [];
|
||||
for (const entry of content) {
|
||||
@@ -636,7 +612,7 @@ function parseToolTextContent(result: Record<string, unknown> | null) {
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
const parsed = asRecord(parseJsonValue(text));
|
||||
const parsed = asRecord(safeParseJson(text));
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
@@ -652,7 +628,7 @@ export function classifyMemorySearchInvokeResponse({
|
||||
status,
|
||||
bodyText,
|
||||
}: InvokeResponseOptions) {
|
||||
const parsedBody = parseJsonValue(bodyText);
|
||||
const parsedBody = safeParseJson(bodyText);
|
||||
const body = asRecord(parsedBody);
|
||||
if (!httpOk) {
|
||||
const errorRecord = asRecord(body?.error);
|
||||
@@ -662,8 +638,8 @@ export function classifyMemorySearchInvokeResponse({
|
||||
status,
|
||||
gatewayOk: body?.ok === true ? true : body?.ok === false ? false : undefined,
|
||||
error:
|
||||
readStringProperty(errorRecord, "message") ??
|
||||
readStringProperty(body, "error") ??
|
||||
readNonBlankString(errorRecord?.message) ??
|
||||
readNonBlankString(body?.error) ??
|
||||
`memory_search HTTP request failed with status ${status}`,
|
||||
};
|
||||
}
|
||||
@@ -685,8 +661,8 @@ export function classifyMemorySearchInvokeResponse({
|
||||
status,
|
||||
gatewayOk,
|
||||
error:
|
||||
readStringProperty(errorRecord, "message") ??
|
||||
readStringProperty(body, "error") ??
|
||||
readNonBlankString(errorRecord?.message) ??
|
||||
readNonBlankString(body.error) ??
|
||||
"memory_search gateway invocation failed",
|
||||
};
|
||||
}
|
||||
@@ -711,7 +687,7 @@ export function classifyMemorySearchInvokeResponse({
|
||||
const resultCount = Array.isArray(payload.results) ? payload.results.length : undefined;
|
||||
const toolDisabled = payload.disabled === true;
|
||||
const toolUnavailable = payload.unavailable === true;
|
||||
const toolError = readStringProperty(payload, "error");
|
||||
const toolError = readNonBlankString(payload.error);
|
||||
const ok = gatewayOk === true && !toolDisabled && !toolUnavailable && !toolError;
|
||||
|
||||
return {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
MAX_TIMER_TIMEOUT_MS,
|
||||
resolveTimerTimeoutMs,
|
||||
} from "../packages/normalization-core/src/number-coercion.ts";
|
||||
import { normalizeCsvOrLooseStringList } from "../packages/normalization-core/src/string-normalization.ts";
|
||||
import { stripLeadingPackageManagerSeparator } from "./lib/arg-utils.mts";
|
||||
import {
|
||||
parseNonNegativeInt,
|
||||
@@ -161,7 +162,7 @@ export function parseArgs(argv: string[]) {
|
||||
failOnObservation: process.env.OPENCLAW_PLUGIN_GATEWAY_GAUNTLET_FAIL_ON_OBSERVATION === "1",
|
||||
keepRunRoot: process.env.OPENCLAW_PLUGIN_GATEWAY_GAUNTLET_KEEP_RUN_ROOT === "1",
|
||||
};
|
||||
const envIds = normalizeCsv(process.env.OPENCLAW_PLUGIN_GATEWAY_GAUNTLET_IDS);
|
||||
const envIds = normalizeCsvOrLooseStringList(process.env.OPENCLAW_PLUGIN_GATEWAY_GAUNTLET_IDS);
|
||||
options.pluginIds.push(...envIds);
|
||||
const seenSingleValueFlags = new Set<string>();
|
||||
parseArgv: for (let index = 0; index < args.length; index += 1) {
|
||||
@@ -331,15 +332,6 @@ Environment:
|
||||
`);
|
||||
}
|
||||
|
||||
function normalizeCsv(raw: string | undefined) {
|
||||
return raw
|
||||
? raw
|
||||
.split(",")
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => entry.length > 0)
|
||||
: [];
|
||||
}
|
||||
|
||||
function assertNoDuplicateValues(values: string[], label: string) {
|
||||
const seen = new Set();
|
||||
for (const value of values) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { execFileSync } from "node:child_process";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { stableStringify } from "../packages/normalization-core/src/stable-stringify.ts";
|
||||
import { RELEASE_METADATA_PATHS } from "./changed-lanes.mts";
|
||||
|
||||
const DEFAULT_GIT_TIMEOUT_MS = 60_000;
|
||||
@@ -150,20 +151,7 @@ function stripPackageVersion(raw: string) {
|
||||
throw new Error("package.json must contain an object");
|
||||
}
|
||||
delete parsed.version;
|
||||
return stableJson(parsed);
|
||||
}
|
||||
|
||||
function stableJson(value: unknown): string | undefined {
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.map(stableJson).join(",")}]`;
|
||||
}
|
||||
if (isRecord(value)) {
|
||||
return `{${Object.keys(value)
|
||||
.toSorted((left, right) => left.localeCompare(right))
|
||||
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
||||
.join(",")}}`;
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
return stableStringify(parsed);
|
||||
}
|
||||
|
||||
function normalizeVersionText(raw: string) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
verifyControlUiGeneratedCatalogs,
|
||||
verifyRuntimeLocaleConfig,
|
||||
} from "./control-ui-i18n-verify.ts";
|
||||
import { isStrictAffirmativeValue } from "./lib/arg-utils.mts";
|
||||
import {
|
||||
hashControlUiTranslationText,
|
||||
loadControlUiTranslationMemory,
|
||||
@@ -487,8 +488,7 @@ export function isProviderAuthError(error: Error): boolean {
|
||||
}
|
||||
|
||||
function isProviderAuthOptional(): boolean {
|
||||
const raw = process.env[ENV_AUTH_OPTIONAL]?.trim().toLowerCase();
|
||||
return raw === "1" || raw === "true" || raw === "yes";
|
||||
return isStrictAffirmativeValue(process.env[ENV_AUTH_OPTIONAL]);
|
||||
}
|
||||
|
||||
function resolvePromptTimeoutMs(): number {
|
||||
|
||||
@@ -13,6 +13,7 @@ import process from "node:process";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { asRecord, isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { hasNonEmptyString } from "@openclaw/normalization-core/string-coerce";
|
||||
import {
|
||||
createBoundedResponseTooLargeError,
|
||||
readBoundedResponseText,
|
||||
@@ -885,8 +886,8 @@ function createGatewayClientRequestError(requestError: unknown): GatewayRequestE
|
||||
const candidate = asRecord(requestError);
|
||||
if (
|
||||
candidate.type !== "gateway_request_error" ||
|
||||
!isNonEmptyString(candidate.code) ||
|
||||
!isNonEmptyString(candidate.message) ||
|
||||
!hasNonEmptyString(candidate.code) ||
|
||||
!hasNonEmptyString(candidate.message) ||
|
||||
typeof candidate.retryable !== "boolean" ||
|
||||
(candidate.retryAfterMs !== undefined &&
|
||||
(typeof candidate.retryAfterMs !== "number" ||
|
||||
@@ -1710,7 +1711,7 @@ export function extractPluginCommandNames(payload: unknown) {
|
||||
}
|
||||
}
|
||||
return names
|
||||
.filter(isNonEmptyString)
|
||||
.filter(hasNonEmptyString)
|
||||
.map((name) => name.replace(/^\//u, ""))
|
||||
.filter((name, index, all) => all.indexOf(name) === index)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
@@ -1744,7 +1745,7 @@ export function assertExpectedKitchenSinkToolEntries(
|
||||
options: { requirePluginProvenance?: boolean } = {},
|
||||
) {
|
||||
const { requirePluginProvenance = false } = options;
|
||||
const ids = entries.map((entry) => asRecord(entry).id).filter(isNonEmptyString);
|
||||
const ids = entries.map((entry) => asRecord(entry).id).filter(hasNonEmptyString);
|
||||
assertIncludesAll(ids, EXPECTED_TOOLS, label);
|
||||
if (requirePluginProvenance) {
|
||||
const wrongProvenance = entries
|
||||
@@ -1772,7 +1773,7 @@ export function assertChannelAccountRunning(payload: unknown) {
|
||||
const accounts = Array.isArray(channelAccounts[CHANNEL_ID]) ? channelAccounts[CHANNEL_ID] : [];
|
||||
const account = accounts.find((entry) => asRecord(entry).accountId === CHANNEL_ACCOUNT_ID);
|
||||
if (!account) {
|
||||
const accountIds = accounts.map((entry) => asRecord(entry).accountId).filter(isNonEmptyString);
|
||||
const accountIds = accounts.map((entry) => asRecord(entry).accountId).filter(hasNonEmptyString);
|
||||
throw new Error(
|
||||
`Kitchen Sink channel account ${CHANNEL_ACCOUNT_ID} was not reported. Available account ids: ${boundedJsonPreview(
|
||||
accountIds,
|
||||
@@ -1801,12 +1802,12 @@ export function assertTtsProviderCoverage(payload: unknown, surface: "providers"
|
||||
`tts.${surface} returned invalid provider list: ${boundedJsonPreview(payload)}`,
|
||||
);
|
||||
}
|
||||
const ids = entries.map((entry) => asRecord(entry).id).filter(isNonEmptyString);
|
||||
const ids = entries.map((entry) => asRecord(entry).id).filter(hasNonEmptyString);
|
||||
assertIncludesAny(ids, EXPECTED_SPEECH_PROVIDERS, `tts.${surface}`);
|
||||
const configuredEntry = entries.find((entry) => {
|
||||
const provider = asRecord(entry);
|
||||
return (
|
||||
isNonEmptyString(provider.id) &&
|
||||
hasNonEmptyString(provider.id) &&
|
||||
EXPECTED_SPEECH_PROVIDERS.includes(provider.id) &&
|
||||
provider.configured === true
|
||||
);
|
||||
@@ -1990,7 +1991,7 @@ export async function assertOperatorRpcDenied(
|
||||
|
||||
export function assertCreatedKitchenSinkSession(payload: unknown, expectedKey = SESSION_KEY) {
|
||||
const created = assertObjectPayload(payload, "sessions.create");
|
||||
if (created.ok !== true || created.key !== expectedKey || !isNonEmptyString(created.sessionId)) {
|
||||
if (created.ok !== true || created.key !== expectedKey || !hasNonEmptyString(created.sessionId)) {
|
||||
throw new Error(
|
||||
`sessions.create did not return the requested Kitchen Sink session: ${boundedJsonPreview(
|
||||
payload,
|
||||
@@ -2076,11 +2077,11 @@ export function assertGatewayHealthPayload(payload: unknown) {
|
||||
[Number.isFinite(health.durationMs), "numeric durationMs"],
|
||||
[isRecord(health.channels), "channels object"],
|
||||
[Array.isArray(health.channelOrder), "channelOrder array"],
|
||||
[isNonEmptyString(health.defaultAgentId), "defaultAgentId"],
|
||||
[hasNonEmptyString(health.defaultAgentId), "defaultAgentId"],
|
||||
[Array.isArray(health.agents), "agents array"],
|
||||
[
|
||||
isRecord(sessions) &&
|
||||
isNonEmptyString(sessions.path) &&
|
||||
hasNonEmptyString(sessions.path) &&
|
||||
Number.isFinite(sessions.count) &&
|
||||
Array.isArray(sessions.recent),
|
||||
"sessions summary",
|
||||
@@ -2099,7 +2100,7 @@ export function assertGatewayStatusPayload(payload: unknown) {
|
||||
const problems = failedPayloadChecks([
|
||||
[
|
||||
isRecord(heartbeat) &&
|
||||
isNonEmptyString(heartbeat.defaultAgentId) &&
|
||||
hasNonEmptyString(heartbeat.defaultAgentId) &&
|
||||
Array.isArray(heartbeat.agents),
|
||||
"heartbeat summary",
|
||||
],
|
||||
@@ -2730,10 +2731,6 @@ function tailText(text: string) {
|
||||
return text.split(/\r?\n/u).slice(-120).join("\n");
|
||||
}
|
||||
|
||||
function isNonEmptyString(value: unknown): value is string {
|
||||
return typeof value === "string" && value.trim().length > 0;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const config = resolveKitchenSinkRpcConfig();
|
||||
let runner = resolveOpenClawRunner();
|
||||
|
||||
@@ -1,21 +1,2 @@
|
||||
// Limits shared by Codex media-path E2E fixtures.
|
||||
export function readPositiveIntEnv(name, fallback, env = process.env) {
|
||||
const text = String(env[name] ?? fallback).trim();
|
||||
if (!/^\d+$/u.test(text)) {
|
||||
throw new Error(`invalid ${name}: ${text}`);
|
||||
}
|
||||
const value = Number(text);
|
||||
if (!Number.isSafeInteger(value) || value <= 0) {
|
||||
throw new Error(`invalid ${name}: ${text}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function readTcpPortEnv(name, fallback, env = process.env) {
|
||||
const value = readPositiveIntEnv(name, fallback, env);
|
||||
if (value > 65_535) {
|
||||
const text = String(env[name] ?? fallback).trim();
|
||||
throw new Error(`invalid ${name}: ${text}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
// Compatibility path for Codex media-path E2E fixtures.
|
||||
export { readPositiveIntEnv, readTcpPortEnv } from "../env-limits.mjs";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Workspace fixture writer commands for E2E scenarios.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { readPositiveIntEnv } from "../env-limits.mjs";
|
||||
import { readTextFileTail } from "../text-file-utils.mjs";
|
||||
import { assert, readJson, requireArg, write, writeJson } from "./common.mjs";
|
||||
|
||||
@@ -10,18 +11,6 @@ const AGENTS_DELETE_OUTPUT_MAX_BYTES = readPositiveIntEnv(
|
||||
);
|
||||
const ERROR_DETAIL_TAIL_BYTES = 16 * 1024;
|
||||
|
||||
function readPositiveIntEnv(name, fallback) {
|
||||
const text = String(process.env[name] ?? fallback).trim();
|
||||
if (!/^\d+$/u.test(text)) {
|
||||
throw new Error(`invalid ${name}: ${text}`);
|
||||
}
|
||||
const value = Number(text);
|
||||
if (!Number.isSafeInteger(value) || value <= 0) {
|
||||
throw new Error(`invalid ${name}: ${text}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function writeOpenWebUiWorkspace() {
|
||||
const workspace =
|
||||
process.env.OPENCLAW_WORKSPACE_DIR || path.join(process.env.HOME, ".openclaw", "workspace");
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
// Limits shared by gateway network E2E fixtures.
|
||||
function readPositiveIntEnv(name: string, fallback: number, env: NodeJS.ProcessEnv) {
|
||||
const text = String(env[name] ?? fallback).trim();
|
||||
if (!/^\d+$/u.test(text)) {
|
||||
throw new Error(`invalid ${name}: ${text}`);
|
||||
}
|
||||
const value = Number(text);
|
||||
if (!Number.isSafeInteger(value) || value <= 0) {
|
||||
throw new Error(`invalid ${name}: ${text}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
import { readPositiveIntEnv } from "../env-limits.mjs";
|
||||
|
||||
export function readGatewayNetworkClientConnectTimeoutMs(env: NodeJS.ProcessEnv = process.env) {
|
||||
if (env.OPENCLAW_GATEWAY_NETWORK_CLIENT_CONNECT_TIMEOUT_MS != null) {
|
||||
|
||||
@@ -4,24 +4,13 @@ import path from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { isRecord } from "../../../lib/record-shared.mjs";
|
||||
import { extractAgentReplyTexts } from "../agent-turn-output.mjs";
|
||||
import { readPositiveIntEnv } from "../env-limits.mjs";
|
||||
import { readPluginInstallRecords } from "../plugin-index-sqlite.mjs";
|
||||
import { readTextFileTail, tailText } from "../text-file-utils.mjs";
|
||||
|
||||
const command = process.argv[2];
|
||||
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
|
||||
function readPositiveIntEnv(name, fallback) {
|
||||
const text = String(process.env[name] ?? fallback).trim();
|
||||
if (!/^\d+$/u.test(text)) {
|
||||
throw new Error(`invalid ${name}: ${text}`);
|
||||
}
|
||||
const value = Number(text);
|
||||
if (!Number.isSafeInteger(value) || value <= 0) {
|
||||
throw new Error(`invalid ${name}: ${text}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const agentTurnTimeoutSeconds = readPositiveIntEnv(
|
||||
"OPENCLAW_LIVE_PLUGIN_TOOL_TIMEOUT_SECONDS",
|
||||
300,
|
||||
|
||||
@@ -8,18 +8,8 @@ import { pathToFileURL } from "node:url";
|
||||
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 isTruthyNpmTelegramEnvValue(value: string | undefined) {
|
||||
const normalized = value?.trim().toLowerCase();
|
||||
return normalized === "1" || normalized === "true" || normalized === "yes";
|
||||
}
|
||||
|
||||
function splitCsv(value: string | undefined) {
|
||||
return (value ?? "")
|
||||
.split(",")
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => entry.length > 0);
|
||||
}
|
||||
import { normalizeCsvOrLooseStringList } from "../../packages/normalization-core/src/string-normalization.ts";
|
||||
import { isStrictAffirmativeValue } from "../lib/arg-utils.mts";
|
||||
|
||||
function parsePositiveIntegerEnv(env: NodeJS.ProcessEnv, name: string) {
|
||||
const raw = env[name]?.trim();
|
||||
@@ -89,7 +79,7 @@ function resolvePackageConfigMutation(env: NodeJS.ProcessEnv = process.env) {
|
||||
}
|
||||
|
||||
function resolveRttOptions(env: NodeJS.ProcessEnv, selectedScenarioIds: readonly string[] = []) {
|
||||
const explicitCheckIds = splitCsv(env.OPENCLAW_NPM_TELEGRAM_RTT_CHECKS);
|
||||
const explicitCheckIds = normalizeCsvOrLooseStringList(env.OPENCLAW_NPM_TELEGRAM_RTT_CHECKS);
|
||||
const checkIds = explicitCheckIds.length > 0 ? explicitCheckIds : [DEFAULT_RTT_CHECK_ID];
|
||||
const unknownCheckIds = checkIds.filter((checkId) => checkId !== DEFAULT_RTT_CHECK_ID);
|
||||
if (unknownCheckIds.length > 0) {
|
||||
@@ -147,7 +137,7 @@ async function shouldFailPackageTelegramRun(
|
||||
result: { summaryPath: string },
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
) {
|
||||
if (isTruthyNpmTelegramEnvValue(env.OPENCLAW_NPM_TELEGRAM_ALLOW_FAILURES)) {
|
||||
if (isStrictAffirmativeValue(env.OPENCLAW_NPM_TELEGRAM_ALLOW_FAILURES)) {
|
||||
return false;
|
||||
}
|
||||
const { readQaSuiteFailedOrSkippedScenarioCountFromFile } =
|
||||
@@ -204,7 +194,7 @@ async function main() {
|
||||
|
||||
const repoRoot = path.resolve(process.env.OPENCLAW_NPM_TELEGRAM_REPO_ROOT ?? process.cwd());
|
||||
const outputDir = resolvePackageTelegramOutputDir(process.env, repoRoot);
|
||||
const scenarioIds = splitCsv(process.env.OPENCLAW_NPM_TELEGRAM_SCENARIOS);
|
||||
const scenarioIds = normalizeCsvOrLooseStringList(process.env.OPENCLAW_NPM_TELEGRAM_SCENARIOS);
|
||||
const providerMode =
|
||||
(process.env.OPENCLAW_NPM_TELEGRAM_PROVIDER_MODE as QaProviderMode | undefined) ??
|
||||
DEFAULT_QA_LIVE_PROVIDER_MODE;
|
||||
@@ -224,7 +214,7 @@ async function main() {
|
||||
providerMode,
|
||||
primaryModel,
|
||||
alternateModel: process.env.OPENCLAW_NPM_TELEGRAM_ALT_MODEL,
|
||||
fastMode: isTruthyNpmTelegramEnvValue(process.env.OPENCLAW_NPM_TELEGRAM_FAST),
|
||||
fastMode: isStrictAffirmativeValue(process.env.OPENCLAW_NPM_TELEGRAM_FAST),
|
||||
scenarioIds,
|
||||
resolvedScenarioIds: prioritizeRoundTripProbeScenario(resolvedScenarioIds, rttOptions),
|
||||
roundTripProbe: createRoundTripProbe(rttOptions),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { mkdir, readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { stripLeadingPackageManagerSeparator } from "../../lib/arg-utils.mts";
|
||||
import { posixAgentWorkspaceScript } from "./agent-workspace.ts";
|
||||
import {
|
||||
die,
|
||||
@@ -248,10 +249,6 @@ export function parseArgs(argv: string[]): LinuxOptions {
|
||||
return options;
|
||||
}
|
||||
|
||||
function stripLeadingPackageManagerSeparator(argv: string[]): string[] {
|
||||
return argv[0] === "--" ? argv.slice(1) : argv;
|
||||
}
|
||||
|
||||
class LinuxSmoke extends SmokeRunController<LinuxOptions> {
|
||||
private auth: ProviderAuth;
|
||||
private disableBonjour = parseBoolEnv(process.env.OPENCLAW_PARALLELS_LINUX_DISABLE_BONJOUR);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { readFile, rm } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { stripLeadingPackageManagerSeparator } from "../../lib/arg-utils.mts";
|
||||
import { posixAgentWorkspaceScript } from "./agent-workspace.ts";
|
||||
import {
|
||||
die,
|
||||
@@ -268,10 +269,6 @@ export function parseArgs(argv: string[]): MacosOptions {
|
||||
return options;
|
||||
}
|
||||
|
||||
function stripLeadingPackageManagerSeparator(argv: string[]): string[] {
|
||||
return argv[0] === "--" ? argv.slice(1) : argv;
|
||||
}
|
||||
|
||||
class MacosSmoke {
|
||||
private agentTimeoutSeconds: number;
|
||||
private auth: ProviderAuth;
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { readStringValue } from "@openclaw/normalization-core/string-coerce";
|
||||
import prettyMilliseconds from "pretty-ms";
|
||||
import { stripLeadingPackageManagerSeparator } from "../../lib/arg-utils.mts";
|
||||
import {
|
||||
die,
|
||||
ensureValue,
|
||||
@@ -525,10 +526,6 @@ export function parseArgs(argv: string[]): NpmUpdateOptions {
|
||||
return options;
|
||||
}
|
||||
|
||||
function stripLeadingPackageManagerSeparator(argv: string[]): string[] {
|
||||
return argv[0] === "--" ? argv.slice(1) : argv;
|
||||
}
|
||||
|
||||
function platformRecord<T>(value: T): Record<Platform, T> {
|
||||
return { linux: value, macos: value, windows: value };
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Windows Smoke script supports OpenClaw repository automation.
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { stripLeadingPackageManagerSeparator } from "../../lib/arg-utils.mts";
|
||||
import { windowsAgentWorkspaceScript } from "./agent-workspace.ts";
|
||||
import {
|
||||
die,
|
||||
@@ -246,10 +247,6 @@ export function parseArgs(argv: string[]): WindowsOptions {
|
||||
return options;
|
||||
}
|
||||
|
||||
function stripLeadingPackageManagerSeparator(argv: string[]): string[] {
|
||||
return argv[0] === "--" ? argv.slice(1) : argv;
|
||||
}
|
||||
|
||||
class WindowsSmoke extends SmokeRunController<WindowsOptions> {
|
||||
private auth: ProviderAuth;
|
||||
private agentTimeoutSeconds = readPositiveIntEnv(
|
||||
|
||||
@@ -15,7 +15,7 @@ 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 { coerceErrorMessage, toStringifiedError } from "../lib/error-format.mts";
|
||||
import { sleep } from "../lib/sleep.mjs";
|
||||
import { resolveWindowsTaskkillPath } from "../lib/windows-taskkill.mjs";
|
||||
import { createPnpmRunnerSpawnSpec } from "../pnpm-runner.mts";
|
||||
@@ -965,8 +965,7 @@ export function runCommand(params: {
|
||||
timeoutKillGraceMs,
|
||||
}).then(
|
||||
() => reject(error),
|
||||
(cleanupError: unknown) =>
|
||||
reject(cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError))),
|
||||
(cleanupError: unknown) => reject(toStringifiedError(cleanupError)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -2070,12 +2069,12 @@ function sshArgs(inspect: CrabboxInspect, sshPort = inspect.sshPort?.trim() || "
|
||||
}
|
||||
|
||||
function isTransientSshFailure(error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const message = coerceErrorMessage(error);
|
||||
return /Connection (?:closed|reset)|Operation timed out|Connection timed out/u.test(message);
|
||||
}
|
||||
|
||||
function isSshConnectionFailure(error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const message = coerceErrorMessage(error);
|
||||
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
||||
return (
|
||||
code === "ETIMEDOUT" ||
|
||||
@@ -3177,7 +3176,7 @@ async function finishSession(root: string, opts: Options, outputDir: string) {
|
||||
}
|
||||
desktopSessionTerminationAttempted = true;
|
||||
await terminateRemoteDesktopSession(root, session.crabbox.inspect).catch((error: unknown) => {
|
||||
summary.desktopSessionTerminateError = error instanceof Error ? error.message : String(error);
|
||||
summary.desktopSessionTerminateError = coerceErrorMessage(error);
|
||||
});
|
||||
};
|
||||
try {
|
||||
@@ -3254,37 +3253,37 @@ async function finishSession(root: string, opts: Options, outputDir: string) {
|
||||
await stopLocalSutDaemon(session.localSut);
|
||||
sutQuiesced = true;
|
||||
} catch (error) {
|
||||
summary.sutStopError = error instanceof Error ? error.message : String(error);
|
||||
summary.sutStopError = coerceErrorMessage(error);
|
||||
summary.status = "fail";
|
||||
}
|
||||
if (sutQuiesced) {
|
||||
try {
|
||||
preserveLocalSutRuntimeArtifacts(session.localSut, session.outputDir);
|
||||
} catch (error) {
|
||||
summary.runtimeArtifactError = error instanceof Error ? error.message : String(error);
|
||||
summary.runtimeArtifactError = coerceErrorMessage(error);
|
||||
summary.status = "fail";
|
||||
}
|
||||
}
|
||||
try {
|
||||
destroyLocalSutRuntime(session.localSut);
|
||||
} catch (error) {
|
||||
summary.sutDestroyError = error instanceof Error ? error.message : String(error);
|
||||
summary.sutDestroyError = coerceErrorMessage(error);
|
||||
summary.status = "fail";
|
||||
}
|
||||
if (session.localSut.funnelBridge) {
|
||||
await stopTailscaleFunnelBridge(root, session.localSut.funnelBridge).catch(
|
||||
(error: unknown) => {
|
||||
summary.funnelResetError = error instanceof Error ? error.message : String(error);
|
||||
summary.funnelResetError = coerceErrorMessage(error);
|
||||
},
|
||||
);
|
||||
}
|
||||
await terminateDesktopSession();
|
||||
await releaseCredential(root, opts, session.credential.leaseFile).catch((error: unknown) => {
|
||||
summary.credentialReleaseError = error instanceof Error ? error.message : String(error);
|
||||
summary.credentialReleaseError = coerceErrorMessage(error);
|
||||
});
|
||||
if (session.crabbox.createdLease && !opts.keepBox) {
|
||||
await stopCrabbox(root, opts, session.crabbox.id).catch((error: unknown) => {
|
||||
summary.crabboxStopError = error instanceof Error ? error.message : String(error);
|
||||
summary.crabboxStopError = coerceErrorMessage(error);
|
||||
});
|
||||
}
|
||||
if (opts.keepBox) {
|
||||
@@ -3613,12 +3612,12 @@ async function main() {
|
||||
killTree(localSut?.mock);
|
||||
if (credential) {
|
||||
await releaseCredential(root, opts, credential.leaseFile).catch((error: unknown) => {
|
||||
summary.credentialReleaseError = error instanceof Error ? error.message : String(error);
|
||||
summary.credentialReleaseError = coerceErrorMessage(error);
|
||||
});
|
||||
}
|
||||
if (leaseId && createdLease && !opts.keepBox) {
|
||||
await stopCrabbox(root, opts, leaseId).catch((error: unknown) => {
|
||||
summary.crabboxStopError = error instanceof Error ? error.message : String(error);
|
||||
summary.crabboxStopError = coerceErrorMessage(error);
|
||||
});
|
||||
}
|
||||
if (opts.keepBox && leaseId) {
|
||||
@@ -3685,7 +3684,7 @@ function isMainModule(): boolean {
|
||||
|
||||
if (isMainModule()) {
|
||||
main().catch((error: unknown) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
console.error(coerceErrorMessage(error));
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
createIssueMutationHelpers,
|
||||
guardTrustedActorCandidates,
|
||||
isCommentNewerThan,
|
||||
normalizeGuardLoginSet,
|
||||
readBoundedGitHubErrorText,
|
||||
readBoundedGitHubJson,
|
||||
} from "./guard-shared.mjs";
|
||||
@@ -264,21 +265,11 @@ export function isDependencyGuardTrustedForHead(comment, currentHeadSha) {
|
||||
}
|
||||
|
||||
export function securityApproverSet(value) {
|
||||
return new Set(
|
||||
String(value ?? "")
|
||||
.split(/[\s,]+/u)
|
||||
.map((login) => login.trim().toLowerCase())
|
||||
.filter(Boolean),
|
||||
);
|
||||
return normalizeGuardLoginSet(value);
|
||||
}
|
||||
|
||||
export function dependencyGuardCommentAuthors(value) {
|
||||
return new Set(
|
||||
String(value ?? "github-actions[bot]")
|
||||
.split(/[\s,]+/u)
|
||||
.map((login) => login.trim().toLowerCase())
|
||||
.filter(Boolean),
|
||||
);
|
||||
return normalizeGuardLoginSet(value, "github-actions[bot]");
|
||||
}
|
||||
|
||||
export function isDependencyGuardMarkerComment(comment, marker, trustedAuthors) {
|
||||
|
||||
@@ -8,6 +8,19 @@ export const GITHUB_API_REQUEST_TIMEOUT_MS = 30_000;
|
||||
const githubApiRetryStatuses = new Set([502, 503, 504]);
|
||||
const githubApiRetryDelaysMs = [1_000, 2_000, 4_000];
|
||||
|
||||
/**
|
||||
* @param {string | null | undefined} value
|
||||
* @param {string} [fallback]
|
||||
*/
|
||||
export function normalizeGuardLoginSet(value, fallback = "") {
|
||||
return new Set(
|
||||
(value ?? fallback)
|
||||
.split(/[\s,]+/u)
|
||||
.map((login) => login.trim().toLowerCase())
|
||||
.filter(Boolean),
|
||||
);
|
||||
}
|
||||
|
||||
export function guardTrustedActorCandidates({ pullRequest, event, currentHeadSha }) {
|
||||
const eventHeadSha = event?.pull_request?.head?.sha;
|
||||
const eventAfterSha = event?.after;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
guardCommentHeadSha,
|
||||
guardTrustedActorCandidates,
|
||||
isCommentNewerThan,
|
||||
normalizeGuardLoginSet,
|
||||
readBoundedGitHubErrorText,
|
||||
readBoundedGitHubJson,
|
||||
} from "./guard-shared.mjs";
|
||||
@@ -170,21 +171,11 @@ export function isSecuritySensitiveGuardTrustedForHead(comment, currentHeadSha)
|
||||
}
|
||||
|
||||
export function securityApproverSet(value) {
|
||||
return new Set(
|
||||
String(value ?? "")
|
||||
.split(/[\s,]+/u)
|
||||
.map((login) => login.trim().toLowerCase())
|
||||
.filter(Boolean),
|
||||
);
|
||||
return normalizeGuardLoginSet(value);
|
||||
}
|
||||
|
||||
export function securitySensitiveGuardCommentAuthors(value) {
|
||||
return new Set(
|
||||
String(value ?? "github-actions[bot]")
|
||||
.split(/[\s,]+/u)
|
||||
.map((login) => login.trim().toLowerCase())
|
||||
.filter(Boolean),
|
||||
);
|
||||
return normalizeGuardLoginSet(value, "github-actions[bot]");
|
||||
}
|
||||
|
||||
export function isSecuritySensitiveGuardMarkerComment(comment, trustedAuthors) {
|
||||
|
||||
@@ -8,13 +8,13 @@ type StringOptions = {
|
||||
};
|
||||
|
||||
type ConsumedFlag<T extends Record<string, unknown>> = {
|
||||
flag?: string;
|
||||
flag: string;
|
||||
nextIndex: number;
|
||||
repeatable?: boolean;
|
||||
apply(target: T): void;
|
||||
};
|
||||
|
||||
type FlagSpec<T extends Record<string, unknown>> = {
|
||||
export type FlagSpec<T extends Record<string, unknown>> = {
|
||||
consume(argv: readonly string[], index: number, args: T): ConsumedFlag<T> | null;
|
||||
};
|
||||
|
||||
@@ -40,6 +40,8 @@ export function classifyBoundedUnsignedDecimal(
|
||||
max: number,
|
||||
): BoundedUnsignedDecimalResult;
|
||||
export function parsePermissiveBooleanToken(value: unknown): boolean | undefined;
|
||||
export function isOpenEndedTruthyValue(value: string | undefined): boolean;
|
||||
export function isStrictAffirmativeValue(value: string | undefined): boolean;
|
||||
export function stringFlag<T extends Record<string, unknown>>(
|
||||
flag: string,
|
||||
key: string,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
/**
|
||||
* @template {Record<string, unknown>} T
|
||||
* @typedef {{
|
||||
* flag?: string,
|
||||
* flag: string,
|
||||
* nextIndex: number,
|
||||
* repeatable?: boolean,
|
||||
* apply: ApplyFlag<T>,
|
||||
@@ -224,6 +224,23 @@ export function parsePermissiveBooleanToken(value) {
|
||||
}
|
||||
return PERMISSIVE_BOOLEAN_FALSE_TOKENS.has(normalized) ? false : undefined;
|
||||
}
|
||||
const OPEN_ENDED_FALSE_TOKENS = new Set(["", "0", "false", "no"]);
|
||||
/**
|
||||
* Treat every non-empty token except the explicit false language as enabled.
|
||||
* @param {string | undefined} value
|
||||
*/
|
||||
export function isOpenEndedTruthyValue(value) {
|
||||
return !OPEN_ENDED_FALSE_TOKENS.has((value ?? "").trim().toLowerCase());
|
||||
}
|
||||
|
||||
const STRICT_AFFIRMATIVE_TOKENS = new Set(["1", "true", "yes"]);
|
||||
/**
|
||||
* Accept only the narrow affirmative token language used by script environment flags.
|
||||
* @param {string | undefined} value
|
||||
*/
|
||||
export function isStrictAffirmativeValue(value) {
|
||||
return STRICT_AFFIRMATIVE_TOKENS.has(value?.trim().toLowerCase() ?? "");
|
||||
}
|
||||
/**
|
||||
* @param {string} raw
|
||||
* @param {string} flag
|
||||
@@ -385,9 +402,6 @@ export function parseFlagArgs(argv, args, specs, options = {}) {
|
||||
if (!option) {
|
||||
continue;
|
||||
}
|
||||
if (typeof option.flag !== "string" || !option.flag) {
|
||||
failFlagParse("parseFlagArgs specs must declare a flag for consumed options");
|
||||
}
|
||||
if (option.repeatable !== true) {
|
||||
if (seenFlags.has(option.flag)) {
|
||||
failFlagParse(
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
const FULL_GIT_COMMIT_RE = /^[0-9a-f]{40}$/iu;
|
||||
|
||||
type BuildIdentityOptions = {
|
||||
commitLabel: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
now?: () => Date;
|
||||
readGitCommit: () => string | null;
|
||||
};
|
||||
|
||||
/** Pins one timestamp and source commit across every child in a build lifecycle. */
|
||||
export function resolveBuildIdentityEnvironment({
|
||||
commitLabel,
|
||||
env = process.env,
|
||||
now = () => new Date(),
|
||||
readGitCommit,
|
||||
}: BuildIdentityOptions): NodeJS.ProcessEnv {
|
||||
const explicitTimestamp = env.OPENCLAW_BUILD_TIMESTAMP?.trim();
|
||||
const explicitCommit = env.GIT_COMMIT?.trim() || env.GIT_SHA?.trim();
|
||||
const checkedOutCommit = explicitCommit ? null : readGitCommit()?.trim();
|
||||
// GITHUB_SHA names the workflow invocation and can differ from a checked-out tag.
|
||||
const commit = explicitCommit || checkedOutCommit || env.GITHUB_SHA?.trim();
|
||||
if (commit && !FULL_GIT_COMMIT_RE.test(commit)) {
|
||||
throw new Error(`${commitLabel} must be a full 40-character hexadecimal SHA`);
|
||||
}
|
||||
return {
|
||||
...env,
|
||||
OPENCLAW_BUILD_TIMESTAMP: explicitTimestamp || now().toISOString(),
|
||||
...(commit ? { GIT_COMMIT: commit.toLowerCase() } : {}),
|
||||
};
|
||||
}
|
||||
@@ -1047,7 +1047,6 @@ function resolveInfraShardName(file: string): string {
|
||||
name.startsWith("fixed-window") ||
|
||||
name.startsWith("format-time/") ||
|
||||
name.startsWith("http-body") ||
|
||||
name.startsWith("parse-finite-number") ||
|
||||
name.startsWith("plain-object") ||
|
||||
name.startsWith("prototype-keys") ||
|
||||
name.startsWith("retry") ||
|
||||
|
||||
@@ -58,6 +58,24 @@ export function parseNonNegativeInt(raw, label) {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a safe non-negative integer written in canonical decimal notation.
|
||||
* @param {unknown} raw
|
||||
* @param {string} label
|
||||
* @returns {number}
|
||||
*/
|
||||
export function parseStrictNonNegativeDecimal(raw, label) {
|
||||
const text = String(raw).trim();
|
||||
if (!/^(0|[1-9]\d*)$/u.test(text)) {
|
||||
throw new Error(`${label} must be a non-negative integer`);
|
||||
}
|
||||
const value = Number(text);
|
||||
if (!Number.isSafeInteger(value)) {
|
||||
throw new Error(`${label} must be a safe integer`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a finite positive number option.
|
||||
* @param {string | number} raw
|
||||
|
||||
@@ -110,6 +110,16 @@ export const deprecatedBarrelPluginSdkEntrypoints = pluginSdkSubpaths.filter((en
|
||||
deprecatedBarrelPluginSdkSubpathList.includes(entry),
|
||||
);
|
||||
|
||||
/** Supported SDK facades backed by bundled plugins until generic contracts replace them. */
|
||||
export const supportedBundledFacadeSdkEntrypoints = [
|
||||
"discord",
|
||||
"matrix",
|
||||
"telegram-account",
|
||||
] as const;
|
||||
|
||||
/** Plugin-owned surfaces intentionally public and documented for third-party plugins. */
|
||||
export const publicPluginOwnedSdkEntrypoints = ["memory-core-host-engine-foundation"] as const;
|
||||
|
||||
/**
|
||||
* Build tsdown entry source paths for plugin SDK entrypoints.
|
||||
* @internal Shared repository-script contract.
|
||||
|
||||
@@ -5,6 +5,7 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { isRecord as isJsonRecord } from "../../packages/normalization-core/src/record-coerce.ts";
|
||||
import { normalizeOptionalString } from "../../packages/normalization-core/src/string-coerce.ts";
|
||||
import { readPublicationArtifactArchive, sha256Digest } from "./actions-artifact-archive.mjs";
|
||||
import { readBoundedResponseText } from "./bounded-response.mjs";
|
||||
import { collectClawHubPublishablePluginPackages } from "./plugin-clawhub-release.ts";
|
||||
@@ -93,16 +94,12 @@ const TRUSTED_TOOLING_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".
|
||||
const NPM_VIEW_ATTEMPTS = 30;
|
||||
const NPM_VIEW_RETRY_MAX_DELAY_MS = 10_000;
|
||||
|
||||
function normalizeOptionalText(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function compareCodeUnits(left: string, right: string): number {
|
||||
return left < right ? -1 : left > right ? 1 : 0;
|
||||
}
|
||||
|
||||
function requireString(value: unknown, label: string): string {
|
||||
const stringValue = normalizeOptionalText(value);
|
||||
const stringValue = normalizeOptionalString(value);
|
||||
if (stringValue === undefined) {
|
||||
throw new Error(`${label} is missing.`);
|
||||
}
|
||||
@@ -197,10 +194,10 @@ export function parseNpmViewFields(raw: string, distTag: string): NpmViewFields
|
||||
const parsed = parseJson(raw, "npm view");
|
||||
if (Array.isArray(parsed)) {
|
||||
return {
|
||||
version: normalizeOptionalText(parsed[0]),
|
||||
distTagVersion: normalizeOptionalText(parsed[1]),
|
||||
integrity: normalizeOptionalText(parsed[2]),
|
||||
tarball: normalizeOptionalText(parsed[3]),
|
||||
version: normalizeOptionalString(parsed[0]),
|
||||
distTagVersion: normalizeOptionalString(parsed[1]),
|
||||
integrity: normalizeOptionalString(parsed[2]),
|
||||
tarball: normalizeOptionalString(parsed[3]),
|
||||
};
|
||||
}
|
||||
if (!isJsonRecord(parsed)) {
|
||||
@@ -209,13 +206,14 @@ export function parseNpmViewFields(raw: string, distTag: string): NpmViewFields
|
||||
const distTags = isJsonRecord(parsed["dist-tags"]) ? parsed["dist-tags"] : undefined;
|
||||
const dist = isJsonRecord(parsed.dist) ? parsed.dist : undefined;
|
||||
return {
|
||||
version: normalizeOptionalText(parsed.version),
|
||||
version: normalizeOptionalString(parsed.version),
|
||||
distTagVersion:
|
||||
normalizeOptionalText(parsed[`dist-tags.${distTag}`]) ??
|
||||
normalizeOptionalText(distTags?.[distTag]),
|
||||
normalizeOptionalString(parsed[`dist-tags.${distTag}`]) ??
|
||||
normalizeOptionalString(distTags?.[distTag]),
|
||||
integrity:
|
||||
normalizeOptionalText(parsed["dist.integrity"]) ?? normalizeOptionalText(dist?.integrity),
|
||||
tarball: normalizeOptionalText(parsed["dist.tarball"]) ?? normalizeOptionalText(dist?.tarball),
|
||||
normalizeOptionalString(parsed["dist.integrity"]) ?? normalizeOptionalString(dist?.integrity),
|
||||
tarball:
|
||||
normalizeOptionalString(parsed["dist.tarball"]) ?? normalizeOptionalString(dist?.tarball),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -612,19 +610,19 @@ function verifyWorkflowRun(params: {
|
||||
if (!isJsonRecord(run)) {
|
||||
throw new Error(`${params.label}: workflow run returned an unsupported JSON shape.`);
|
||||
}
|
||||
const workflowName = normalizeOptionalText(run.workflowName);
|
||||
const workflowName = normalizeOptionalString(run.workflowName);
|
||||
if (workflowName !== params.expectedWorkflowName) {
|
||||
throw new Error(
|
||||
`${params.label}: run ${params.id} workflow is ${workflowName ?? "<missing>"}, expected ${params.expectedWorkflowName}.`,
|
||||
);
|
||||
}
|
||||
const event = normalizeOptionalText(run.event);
|
||||
const event = normalizeOptionalString(run.event);
|
||||
if (event !== "workflow_dispatch") {
|
||||
throw new Error(
|
||||
`${params.label}: run ${params.id} event is ${event ?? "<missing>"}, expected workflow_dispatch.`,
|
||||
);
|
||||
}
|
||||
const headBranch = normalizeOptionalText(run.headBranch);
|
||||
const headBranch = normalizeOptionalString(run.headBranch);
|
||||
const allowedHeadBranches =
|
||||
params.allowedHeadBranches ??
|
||||
(params.expectedHeadBranch !== undefined ? [params.expectedHeadBranch] : []);
|
||||
@@ -633,11 +631,11 @@ function verifyWorkflowRun(params: {
|
||||
`${params.label}: run ${params.id} branch is ${headBranch ?? "<missing>"}, expected ${allowedHeadBranches.join(" or ")}.`,
|
||||
);
|
||||
}
|
||||
const status = normalizeOptionalText(run.status);
|
||||
const conclusion = normalizeOptionalText(run.conclusion);
|
||||
const status = normalizeOptionalString(run.status);
|
||||
const conclusion = normalizeOptionalString(run.conclusion);
|
||||
const jobs = Array.isArray(run.jobs) ? run.jobs.filter(isJsonRecord) : [];
|
||||
const failedJobs = jobs.filter((job) => {
|
||||
const jobConclusion = normalizeOptionalText(job.conclusion);
|
||||
const jobConclusion = normalizeOptionalString(job.conclusion);
|
||||
return (
|
||||
jobConclusion !== undefined && jobConclusion !== "success" && jobConclusion !== "skipped"
|
||||
);
|
||||
@@ -650,14 +648,14 @@ function verifyWorkflowRun(params: {
|
||||
}
|
||||
if (status !== "completed" || conclusion !== "success" || failedJobs.length > 0) {
|
||||
const failedNames = failedJobs
|
||||
.map((job) => normalizeOptionalText(job.name) ?? "<unnamed>")
|
||||
.map((job) => normalizeOptionalString(job.name) ?? "<unnamed>")
|
||||
.join(", ");
|
||||
throw new Error(
|
||||
`${params.label}: run ${params.id} is ${status ?? "<missing>"}/${conclusion ?? "<missing>"}${failedNames ? `; failed jobs: ${failedNames}` : ""}.`,
|
||||
);
|
||||
}
|
||||
const createdAt = normalizeOptionalText(run.createdAt);
|
||||
const updatedAt = normalizeOptionalText(run.updatedAt);
|
||||
const createdAt = normalizeOptionalString(run.createdAt);
|
||||
const updatedAt = normalizeOptionalString(run.updatedAt);
|
||||
const createdMs = createdAt === undefined ? Number.NaN : Date.parse(createdAt);
|
||||
const updatedMs = updatedAt === undefined ? Number.NaN : Date.parse(updatedAt);
|
||||
const durationSeconds =
|
||||
@@ -667,7 +665,7 @@ function verifyWorkflowRun(params: {
|
||||
return {
|
||||
id: params.id,
|
||||
label: params.label,
|
||||
url: normalizeOptionalText(run.url),
|
||||
url: normalizeOptionalString(run.url),
|
||||
durationSeconds,
|
||||
};
|
||||
}
|
||||
@@ -676,7 +674,7 @@ function requirePositiveIntegerString(value: unknown, label: string): string {
|
||||
if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) {
|
||||
return String(value);
|
||||
}
|
||||
const stringValue = normalizeOptionalText(value);
|
||||
const stringValue = normalizeOptionalString(value);
|
||||
if (stringValue === undefined || !POSITIVE_INTEGER_PATTERN.test(stringValue)) {
|
||||
throw new Error(`${label} must be a positive integer.`);
|
||||
}
|
||||
@@ -1055,14 +1053,14 @@ export function validateClawHubBootstrapEvidence(params: {
|
||||
);
|
||||
}
|
||||
|
||||
const createdAt = normalizeOptionalText(runBinding.run.created_at);
|
||||
const updatedAt = normalizeOptionalText(runBinding.run.updated_at);
|
||||
const createdAt = normalizeOptionalString(runBinding.run.created_at);
|
||||
const updatedAt = normalizeOptionalString(runBinding.run.updated_at);
|
||||
const createdMs = createdAt === undefined ? Number.NaN : Date.parse(createdAt);
|
||||
const updatedMs = updatedAt === undefined ? Number.NaN : Date.parse(updatedAt);
|
||||
return {
|
||||
id: runId,
|
||||
label: "Plugin ClawHub New",
|
||||
url: normalizeOptionalText(runBinding.run.html_url),
|
||||
url: normalizeOptionalString(runBinding.run.html_url),
|
||||
durationSeconds:
|
||||
Number.isFinite(createdMs) && Number.isFinite(updatedMs)
|
||||
? Math.max(0, Math.round((updatedMs - createdMs) / 1000))
|
||||
|
||||
@@ -41,7 +41,7 @@ const CORE_PROD_REQUIRED_PATHS = [
|
||||
},
|
||||
{
|
||||
path: "scripts/lib/plugin-sdk-entrypoints.json",
|
||||
whenPresent: "src/plugin-sdk/entrypoints.ts",
|
||||
whenPresent: "scripts/lib/plugin-sdk-entries.mts",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSyn
|
||||
import { tmpdir } from "node:os";
|
||||
import { delimiter, join, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { resolveBuildIdentityEnvironment } from "./lib/build-identity.mts";
|
||||
|
||||
const WORKSPACE_DIRS_ENV = "OPENCLAW_OCM_WORKSPACE_DEPENDENCY_DIRS";
|
||||
const REAL_NPM_ENV = "OPENCLAW_OCM_REAL_NPM_BIN";
|
||||
@@ -12,7 +13,6 @@ const INTERNAL_NPM_BIN_ENV = "OCM_INTERNAL_NPM_BIN";
|
||||
const ALLOW_UNRELEASED_CHANGELOG_ENV = "OPENCLAW_PREPACK_ALLOW_UNRELEASED_CHANGELOG";
|
||||
const RUNTIME_BUILD_PROFILE_ENV = "OPENCLAW_OCM_RUNTIME_BUILD_PROFILE";
|
||||
const supportedRuntimeBuildProfiles = new Set(["sourcePerformance"]);
|
||||
const fullGitCommitPattern = /^[0-9a-f]{40}$/iu;
|
||||
|
||||
type WorkspacePackage = { name: string; version: string; tarball: string };
|
||||
type WorkspacePackageSource = Omit<WorkspacePackage, "tarball"> & { dir: string };
|
||||
@@ -118,18 +118,12 @@ export function resolveRuntimePackEnvironment(
|
||||
return result.status === 0 ? result.stdout.trim() : null;
|
||||
},
|
||||
) {
|
||||
const explicitTimestamp = env.OPENCLAW_BUILD_TIMESTAMP?.trim();
|
||||
const explicitCommit = env.GIT_COMMIT?.trim() || env.GIT_SHA?.trim();
|
||||
const checkedOutCommit = explicitCommit ? null : readGitCommit()?.trim();
|
||||
const commit = explicitCommit || checkedOutCommit || env.GITHUB_SHA?.trim();
|
||||
if (commit && !fullGitCommitPattern.test(commit)) {
|
||||
throw new Error("runtime pack commit must be a full 40-character hexadecimal SHA");
|
||||
}
|
||||
return {
|
||||
...env,
|
||||
OPENCLAW_BUILD_TIMESTAMP: explicitTimestamp || now().toISOString(),
|
||||
...(commit ? { GIT_COMMIT: commit.toLowerCase() } : {}),
|
||||
};
|
||||
return resolveBuildIdentityEnvironment({
|
||||
commitLabel: "runtime pack commit",
|
||||
env,
|
||||
now,
|
||||
readGitCommit,
|
||||
});
|
||||
}
|
||||
|
||||
function runTar(args: string[]) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { basename, delimiter, join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { formatErrorMessage } from "../src/infra/errors.ts";
|
||||
import { resolveBuildIdentityEnvironment } from "./lib/build-identity.mts";
|
||||
import { readPositiveEnvInt } from "./lib/numeric-options.mjs";
|
||||
import { writePackageDistInventoryForPublish } from "./lib/package-dist-inventory.ts";
|
||||
import { restorePrepackArtifacts } from "./openclaw-postpack.mjs";
|
||||
@@ -13,7 +14,6 @@ import { preparePackageChangelog } from "./package-changelog.mjs";
|
||||
import { preparePackageDocsMap } from "./package-docs-map.mjs";
|
||||
import { preparePackageManifest } from "./package-manifest.mjs";
|
||||
import { createPnpmRunnerSpawnSpec } from "./pnpm-runner.mts";
|
||||
const FULL_GIT_COMMIT_RE = /^[0-9a-f]{40}$/iu;
|
||||
const requiredPreparedPathGroups = [
|
||||
["dist/index.js", "dist/index.mjs"],
|
||||
["dist/control-ui/index.html"],
|
||||
@@ -249,22 +249,12 @@ export function resolvePrepackBuildEnvironment(
|
||||
return result.status === 0 ? result.stdout.trim() : null;
|
||||
},
|
||||
): NodeJS.ProcessEnv {
|
||||
const explicitTimestamp = env.OPENCLAW_BUILD_TIMESTAMP?.trim();
|
||||
const explicitCommit = env.GIT_COMMIT?.trim() || env.GIT_SHA?.trim();
|
||||
const checkedOutCommit = explicitCommit ? null : readGitCommit()?.trim();
|
||||
// GITHUB_SHA names the workflow invocation and can differ from a checked-out tag.
|
||||
const commit = explicitCommit || checkedOutCommit || env.GITHUB_SHA?.trim();
|
||||
if (commit && !FULL_GIT_COMMIT_RE.test(commit)) {
|
||||
throw new Error("build commit must be a full 40-character hexadecimal SHA");
|
||||
}
|
||||
const buildEnv: NodeJS.ProcessEnv = {
|
||||
...env,
|
||||
OPENCLAW_BUILD_TIMESTAMP: explicitTimestamp || now().toISOString(),
|
||||
};
|
||||
if (commit) {
|
||||
buildEnv.GIT_COMMIT = commit.toLowerCase();
|
||||
}
|
||||
return buildEnv;
|
||||
return resolveBuildIdentityEnvironment({
|
||||
commitLabel: "build commit",
|
||||
env,
|
||||
now,
|
||||
readGitCommit,
|
||||
});
|
||||
}
|
||||
|
||||
function runPnpm(args: string[], env: NodeJS.ProcessEnv): void {
|
||||
|
||||
@@ -3,14 +3,13 @@ import { spawnSync } from "node:child_process";
|
||||
import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { listPluginCompatRecords } from "../src/plugins/compat/registry.ts";
|
||||
import type { PluginCompatRecord } from "../src/plugins/compat/types.ts";
|
||||
import {
|
||||
pluginSdkEntrypoints,
|
||||
publicPluginOwnedSdkEntrypoints,
|
||||
reservedBundledPluginSdkEntrypoints,
|
||||
supportedBundledFacadeSdkEntrypoints,
|
||||
} from "../src/plugin-sdk/entrypoints.ts";
|
||||
import { listPluginCompatRecords } from "../src/plugins/compat/registry.ts";
|
||||
import type { PluginCompatRecord } from "../src/plugins/compat/types.ts";
|
||||
} from "./lib/plugin-sdk-entries.mts";
|
||||
|
||||
const REPO_ROOT = process.cwd();
|
||||
const SOURCE_ROOTS = ["src", "extensions", "packages", "scripts", "test", "docs"] as const;
|
||||
@@ -23,16 +22,11 @@ const SKIPPED_DIRS = new Set([
|
||||
"node_modules",
|
||||
]);
|
||||
const TEXT_FILE_PATTERN = /\.(?:[cm]?[jt]sx?|json|mdx?|ya?ml)$/u;
|
||||
const PLUGIN_SDK_SPECIFIER_PATTERN =
|
||||
/\b(?:from\s*["']|import\s*\(\s*["']|require\s*\(\s*["']|vi\.(?:mock|doMock)\s*\(\s*["'])(openclaw\/plugin-sdk\/([a-z0-9][a-z0-9-]*))["']/g;
|
||||
|
||||
type CliOptions = {
|
||||
json: boolean;
|
||||
summary: boolean;
|
||||
owner?: string;
|
||||
failOnCrossOwner: boolean;
|
||||
failOnEligibleCompat: boolean;
|
||||
failOnUnclassifiedUnusedReserved: boolean;
|
||||
help: boolean;
|
||||
};
|
||||
|
||||
@@ -73,15 +67,6 @@ type WorkspaceTextFile = {
|
||||
source: string;
|
||||
};
|
||||
|
||||
type ReservedSdkImport = {
|
||||
file: string;
|
||||
specifier: string;
|
||||
subpath: string;
|
||||
owner?: string;
|
||||
consumerOwner?: string;
|
||||
relation: "owner" | "cross-owner" | "workspace";
|
||||
};
|
||||
|
||||
type BoundaryReport = {
|
||||
generatedAt: string;
|
||||
compat: {
|
||||
@@ -94,12 +79,8 @@ type BoundaryReport = {
|
||||
};
|
||||
pluginSdk: {
|
||||
entrypointCount: number;
|
||||
reservedCount: number;
|
||||
supportedBundledFacadeCount: number;
|
||||
publicPluginOwnedCount: number;
|
||||
reservedImports: ReservedSdkImport[];
|
||||
crossOwnerReservedImports: ReservedSdkImport[];
|
||||
unusedReservedSubpaths: string[];
|
||||
};
|
||||
memoryHostSdk: {
|
||||
privatePackage: boolean;
|
||||
@@ -123,14 +104,8 @@ type BoundaryReportSummary = {
|
||||
};
|
||||
pluginSdk: {
|
||||
entrypointCount: number;
|
||||
reservedCount: number;
|
||||
supportedBundledFacadeCount: number;
|
||||
publicPluginOwnedCount: number;
|
||||
reservedImportCount: number;
|
||||
crossOwnerReservedImportCount: number;
|
||||
unusedReservedCount: number;
|
||||
unusedReservedSubpaths: string[];
|
||||
crossOwnerReservedImports: ReservedSdkImport[];
|
||||
};
|
||||
memoryHostSdk: {
|
||||
privatePackage: boolean;
|
||||
@@ -276,9 +251,7 @@ function parseArgs(args: readonly string[]): CliOptions {
|
||||
const options: CliOptions = {
|
||||
json: false,
|
||||
summary: false,
|
||||
failOnCrossOwner: false,
|
||||
failOnEligibleCompat: false,
|
||||
failOnUnclassifiedUnusedReserved: false,
|
||||
help: false,
|
||||
};
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
@@ -294,12 +267,8 @@ function parseArgs(args: readonly string[]): CliOptions {
|
||||
}
|
||||
options.owner = owner;
|
||||
index += 1;
|
||||
} else if (arg === "--fail-on-cross-owner") {
|
||||
options.failOnCrossOwner = true;
|
||||
} else if (arg === "--fail-on-eligible-compat") {
|
||||
options.failOnEligibleCompat = true;
|
||||
} else if (arg === "--fail-on-unclassified-unused-reserved") {
|
||||
options.failOnUnclassifiedUnusedReserved = true;
|
||||
} else if (arg === "--help" || arg === "-h") {
|
||||
options.help = true;
|
||||
} else {
|
||||
@@ -316,30 +285,11 @@ function renderHelp(): string {
|
||||
"Options:",
|
||||
" --summary Print compact counts only.",
|
||||
" --json Emit JSON instead of text.",
|
||||
" --owner <id> Filter compat/imports/reserved shims by owner id.",
|
||||
" --fail-on-cross-owner Exit non-zero on cross-owner reserved SDK imports.",
|
||||
" --owner <id> Filter compatibility records by owner id.",
|
||||
" --fail-on-eligible-compat Exit non-zero when deprecated compat is due for removal.",
|
||||
" --fail-on-unclassified-unused-reserved Exit non-zero on unused reserved SDK shims.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function collectBundledPluginIds(): string[] {
|
||||
return readdirSync(resolve(REPO_ROOT, "extensions"), { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name)
|
||||
.toSorted((left, right) => right.length - left.length || left.localeCompare(right));
|
||||
}
|
||||
|
||||
function resolvePluginOwner(entrypoint: string, pluginIds: readonly string[]): string | undefined {
|
||||
return pluginIds.find(
|
||||
(pluginId) => entrypoint === pluginId || entrypoint.startsWith(`${pluginId}-`),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveConsumerOwner(file: string): string | undefined {
|
||||
return /^extensions\/([^/]+)\//u.exec(file)?.[1];
|
||||
}
|
||||
|
||||
function extractCompatTokensFromValues(values: readonly (string | undefined)[]): string[] {
|
||||
const tokens = new Set<string>();
|
||||
for (const value of values) {
|
||||
@@ -481,32 +431,6 @@ function collectRemovalPendingDebt(
|
||||
);
|
||||
}
|
||||
|
||||
function collectReservedSdkImports(files: readonly WorkspaceTextFile[]): ReservedSdkImport[] {
|
||||
const reserved = new Set<string>(reservedBundledPluginSdkEntrypoints);
|
||||
const pluginIds = collectBundledPluginIds();
|
||||
const imports: ReservedSdkImport[] = [];
|
||||
for (const { relativeFile, source } of files) {
|
||||
for (const match of source.matchAll(PLUGIN_SDK_SPECIFIER_PATTERN)) {
|
||||
const specifier = match[1];
|
||||
const subpath = match[2];
|
||||
if (!specifier || !subpath || !reserved.has(subpath)) {
|
||||
continue;
|
||||
}
|
||||
const owner = resolvePluginOwner(subpath, pluginIds);
|
||||
const consumerOwner = resolveConsumerOwner(relativeFile);
|
||||
const relation =
|
||||
owner && consumerOwner ? (owner === consumerOwner ? "owner" : "cross-owner") : "workspace";
|
||||
imports.push({ file: relativeFile, specifier, subpath, owner, consumerOwner, relation });
|
||||
}
|
||||
}
|
||||
return imports.toSorted(
|
||||
(left, right) =>
|
||||
left.subpath.localeCompare(right.subpath) ||
|
||||
left.file.localeCompare(right.file) ||
|
||||
left.specifier.localeCompare(right.specifier),
|
||||
);
|
||||
}
|
||||
|
||||
function collectMemoryHostBoundary(
|
||||
files: readonly WorkspaceTextFile[],
|
||||
): BoundaryReport["memoryHostSdk"] {
|
||||
@@ -595,14 +519,8 @@ function buildSummary(report: BoundaryReport, owner?: string): BoundaryReportSum
|
||||
},
|
||||
pluginSdk: {
|
||||
entrypointCount: report.pluginSdk.entrypointCount,
|
||||
reservedCount: report.pluginSdk.reservedCount,
|
||||
supportedBundledFacadeCount: report.pluginSdk.supportedBundledFacadeCount,
|
||||
publicPluginOwnedCount: report.pluginSdk.publicPluginOwnedCount,
|
||||
reservedImportCount: report.pluginSdk.reservedImports.length,
|
||||
crossOwnerReservedImportCount: report.pluginSdk.crossOwnerReservedImports.length,
|
||||
unusedReservedCount: report.pluginSdk.unusedReservedSubpaths.length,
|
||||
unusedReservedSubpaths: report.pluginSdk.unusedReservedSubpaths,
|
||||
crossOwnerReservedImports: report.pluginSdk.crossOwnerReservedImports,
|
||||
},
|
||||
memoryHostSdk: {
|
||||
privatePackage: report.memoryHostSdk.privatePackage,
|
||||
@@ -618,25 +536,12 @@ function buildReport(options: Partial<Pick<CliOptions, "owner" | "summary">> = {
|
||||
const files = options.summary
|
||||
? collectSummaryWorkspaceTextFileSources()
|
||||
: collectWorkspaceTextFileSources();
|
||||
const pluginIds = collectBundledPluginIds();
|
||||
const compatRecords = collectCompatDebt(files, new Date(), {
|
||||
includeReferenceFiles: !options.summary,
|
||||
}).filter((record) => matchesOwner(options.owner, record.owner));
|
||||
const removalPending = collectRemovalPendingDebt(files).filter((record) =>
|
||||
matchesOwner(options.owner, record.owner),
|
||||
);
|
||||
const reservedImports = collectReservedSdkImports(files).filter(
|
||||
(entry) =>
|
||||
matchesOwner(options.owner, entry.owner) || matchesOwner(options.owner, entry.consumerOwner),
|
||||
);
|
||||
const usedReserved = new Set(reservedImports.map((entry) => entry.subpath));
|
||||
const unusedReservedSubpaths = (reservedBundledPluginSdkEntrypoints as readonly string[])
|
||||
.filter(
|
||||
(subpath) =>
|
||||
!usedReserved.has(subpath) &&
|
||||
matchesOwner(options.owner, resolvePluginOwner(subpath, pluginIds)),
|
||||
)
|
||||
.toSorted((a, b) => a.localeCompare(b));
|
||||
return {
|
||||
generatedAt: new Date().toISOString(),
|
||||
compat: {
|
||||
@@ -649,14 +554,8 @@ function buildReport(options: Partial<Pick<CliOptions, "owner" | "summary">> = {
|
||||
},
|
||||
pluginSdk: {
|
||||
entrypointCount: pluginSdkEntrypoints.length,
|
||||
reservedCount: reservedBundledPluginSdkEntrypoints.length,
|
||||
supportedBundledFacadeCount: supportedBundledFacadeSdkEntrypoints.length,
|
||||
publicPluginOwnedCount: publicPluginOwnedSdkEntrypoints.length,
|
||||
reservedImports,
|
||||
crossOwnerReservedImports: reservedImports.filter(
|
||||
(entry) => entry.relation === "cross-owner",
|
||||
),
|
||||
unusedReservedSubpaths,
|
||||
},
|
||||
memoryHostSdk: collectMemoryHostBoundary(files),
|
||||
};
|
||||
@@ -675,17 +574,8 @@ function renderSummaryText(summary: BoundaryReportSummary): string {
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
`plugin-sdk entrypoints=${summary.pluginSdk.entrypointCount} reserved=${summary.pluginSdk.reservedCount}`,
|
||||
`plugin-sdk entrypoints=${summary.pluginSdk.entrypointCount} supportedBundledFacade=${summary.pluginSdk.supportedBundledFacadeCount} publicPluginOwned=${summary.pluginSdk.publicPluginOwnedCount}`,
|
||||
);
|
||||
lines.push(
|
||||
` reservedImports=${summary.pluginSdk.reservedImportCount} crossOwnerReservedImports=${summary.pluginSdk.crossOwnerReservedImportCount} unusedReserved=${summary.pluginSdk.unusedReservedCount}`,
|
||||
);
|
||||
for (const subpath of summary.pluginSdk.unusedReservedSubpaths) {
|
||||
lines.push(` unused-reserved ${subpath}`);
|
||||
}
|
||||
for (const entry of summary.pluginSdk.crossOwnerReservedImports) {
|
||||
lines.push(` cross-owner ${entry.file}: ${entry.specifier} owner=${entry.owner ?? "unknown"}`);
|
||||
}
|
||||
lines.push(
|
||||
`memory-host-sdk implementation=${summary.memoryHostSdk.implementation} private=${summary.memoryHostSdk.privatePackage} exports=${summary.memoryHostSdk.exportedSubpathCount} sourceBridgeFiles=${summary.memoryHostSdk.sourceBridgeFileCount} coreReferenceFiles=${summary.memoryHostSdk.packageCoreReferenceFileCount}`,
|
||||
);
|
||||
@@ -714,17 +604,8 @@ function renderText(report: BoundaryReport, owner?: string): string {
|
||||
}
|
||||
lines.push("");
|
||||
lines.push(
|
||||
`plugin-sdk entrypoints=${report.pluginSdk.entrypointCount} reserved=${report.pluginSdk.reservedCount} supportedBundledFacade=${report.pluginSdk.supportedBundledFacadeCount} publicPluginOwned=${report.pluginSdk.publicPluginOwnedCount}`,
|
||||
`plugin-sdk entrypoints=${report.pluginSdk.entrypointCount} supportedBundledFacade=${report.pluginSdk.supportedBundledFacadeCount} publicPluginOwned=${report.pluginSdk.publicPluginOwnedCount}`,
|
||||
);
|
||||
lines.push(
|
||||
` reservedImports=${report.pluginSdk.reservedImports.length} crossOwnerReservedImports=${report.pluginSdk.crossOwnerReservedImports.length} unusedReserved=${report.pluginSdk.unusedReservedSubpaths.length}`,
|
||||
);
|
||||
for (const subpath of report.pluginSdk.unusedReservedSubpaths) {
|
||||
lines.push(` unused-reserved ${subpath}`);
|
||||
}
|
||||
for (const entry of report.pluginSdk.crossOwnerReservedImports) {
|
||||
lines.push(` cross-owner ${entry.file}: ${entry.specifier} owner=${entry.owner ?? "unknown"}`);
|
||||
}
|
||||
lines.push("");
|
||||
lines.push(
|
||||
`memory-host-sdk implementation=${resolveMemoryHostImplementation(report.memoryHostSdk)} private=${report.memoryHostSdk.privatePackage} exports=${report.memoryHostSdk.exportedSubpaths.length} sourceBridgeFiles=${report.memoryHostSdk.sourceBridgeFiles.length} coreReferenceFiles=${report.memoryHostSdk.packageCoreReferenceFiles.length}`,
|
||||
@@ -734,19 +615,6 @@ function renderText(report: BoundaryReport, owner?: string): string {
|
||||
|
||||
function collectFailures(report: BoundaryReport, options: CliOptions): string[] {
|
||||
const failures: string[] = [];
|
||||
if (options.failOnCrossOwner && report.pluginSdk.crossOwnerReservedImports.length > 0) {
|
||||
failures.push(
|
||||
`${report.pluginSdk.crossOwnerReservedImports.length} cross-owner reserved SDK import(s) found`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
options.failOnUnclassifiedUnusedReserved &&
|
||||
report.pluginSdk.unusedReservedSubpaths.length > 0
|
||||
) {
|
||||
failures.push(
|
||||
`${report.pluginSdk.unusedReservedSubpaths.length} unused reserved SDK subpath(s) found`,
|
||||
);
|
||||
}
|
||||
if (options.failOnEligibleCompat && report.compat.eligibleForRemovalCount > 0) {
|
||||
failures.push(
|
||||
`${report.compat.eligibleForRemovalCount} compatibility record(s) are due for removal`,
|
||||
|
||||
@@ -365,7 +365,8 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env
|
||||
// -1: infra-runtime now names its error exports explicitly.
|
||||
// -1: infra-runtime excludes the internal system-event receipt API.
|
||||
// -2: text-runtime names record and string coercion compatibility exports explicitly.
|
||||
76,
|
||||
// -1: infra-runtime re-exports number coercion directly from its canonical owner.
|
||||
75,
|
||||
env,
|
||||
),
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseArgs } from "node:util";
|
||||
import { parseStrictPositiveInteger } from "../src/infra/parse-finite-number.js";
|
||||
import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-coercion";
|
||||
|
||||
const options = {
|
||||
help: { type: "boolean", short: "h" },
|
||||
|
||||
@@ -5,7 +5,12 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import ts from "typescript";
|
||||
import { isChangedLaneTestPath } from "./changed-lanes.mts";
|
||||
import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mts";
|
||||
import {
|
||||
booleanFlag,
|
||||
isOpenEndedTruthyValue,
|
||||
parseFlagArgs,
|
||||
stringFlag,
|
||||
} from "./lib/arg-utils.mts";
|
||||
import { runAsScript } from "./lib/ts-guard-utils.mts";
|
||||
|
||||
type AddedLine = {
|
||||
@@ -100,11 +105,6 @@ function shouldInspectManualHelperUsage(filePath: string): boolean {
|
||||
return normalizedPath !== TEMP_DIR_HELPER_TEST_PATH && shouldInspectFile(normalizedPath);
|
||||
}
|
||||
|
||||
function isTruthyEnvFlag(value: string | undefined): boolean {
|
||||
const normalized = value?.trim().toLowerCase() ?? "";
|
||||
return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "no";
|
||||
}
|
||||
|
||||
function escapeGithubCommandValue(value: unknown): string {
|
||||
return String(value).replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
|
||||
}
|
||||
@@ -516,7 +516,7 @@ async function main(argv?: string[], io?: ScriptIo): Promise<0 | 1> {
|
||||
stdout.write(`${JSON.stringify(findings, null, 2)}\n`);
|
||||
} else if (findings.length === 0) {
|
||||
stderr.write("No new test temp-directory migration warnings found.\n");
|
||||
} else if (isTruthyEnvFlag(env.GITHUB_ACTIONS)) {
|
||||
} else if (isOpenEndedTruthyValue(env.GITHUB_ACTIONS)) {
|
||||
for (const finding of findings) {
|
||||
stderr.write(`${formatGithubWarning(finding)}\n`);
|
||||
}
|
||||
|
||||
@@ -100,11 +100,6 @@ export const BOUNDARY_CHECKS = (
|
||||
"pnpm",
|
||||
["run", "lint:extensions:no-normalization-core-bypass"],
|
||||
],
|
||||
[
|
||||
"extension-plugin-sdk-internal-boundary",
|
||||
"pnpm",
|
||||
["run", "lint:extensions:no-plugin-sdk-internal"],
|
||||
],
|
||||
[
|
||||
"extension-relative-outside-package-boundary",
|
||||
"pnpm",
|
||||
|
||||
@@ -6,7 +6,7 @@ import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import pMap from "p-map";
|
||||
import { coerceErrorMessage as formatSpawnError } from "./lib/error-format.mts";
|
||||
import { coerceErrorMessage } from "./lib/error-format.mts";
|
||||
import { parsePositiveInt } from "./lib/numeric-options.mjs";
|
||||
import {
|
||||
buildGroupedTestComparison,
|
||||
@@ -361,7 +361,7 @@ export function signalTestGroupReportChild(
|
||||
} catch (error) {
|
||||
if (error && !hasErrorCode(error, "ESRCH")) {
|
||||
appendDiagnostic(
|
||||
`[test-group-report] failed to send ${signal} to process group: ${formatSpawnError(error)}\n`,
|
||||
`[test-group-report] failed to send ${signal} to process group: ${coerceErrorMessage(error)}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -733,7 +733,7 @@ function readReportInputs(entries: ReportInputEntry[]) {
|
||||
} catch (error) {
|
||||
invalid.push({
|
||||
entry,
|
||||
reason: error instanceof Error ? error.message : String(error),
|
||||
reason: coerceErrorMessage(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1134,7 +1134,7 @@ export async function runReportPlans(params: {
|
||||
`[test-group-report] config failed; keeping partial report from ${run.reportPath}`,
|
||||
);
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : String(error);
|
||||
const reason = coerceErrorMessage(error);
|
||||
console.error(
|
||||
`[test-group-report] config failed; skipping unusable JSON report from ${run.reportPath} (${reason})`,
|
||||
);
|
||||
@@ -1283,7 +1283,7 @@ const isMain =
|
||||
|
||||
if (isMain) {
|
||||
main().catch((error: unknown) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
console.error(coerceErrorMessage(error));
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
// Runs a Vitest config and enforces wall-time regression budgets.
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { booleanFlag, parseFlagArgs, stringFlag, type FlagSpec } from "./lib/arg-utils.mts";
|
||||
import {
|
||||
booleanFlag,
|
||||
isStrictAffirmativeValue,
|
||||
parseFlagArgs,
|
||||
stringFlag,
|
||||
type FlagSpec,
|
||||
} from "./lib/arg-utils.mts";
|
||||
import {
|
||||
budgetFloatFlag,
|
||||
parseBudgetNumber,
|
||||
readBudgetEnvNumber,
|
||||
} from "./lib/budget-number-args.mts";
|
||||
import { coerceErrorMessage as formatErrorMessage } from "./lib/error-format.mts";
|
||||
import { coerceErrorMessage } from "./lib/error-format.mts";
|
||||
import { formatMs } from "./lib/vitest-report-cli-utils.mts";
|
||||
import { readJsonFile, runVitestJsonReport } from "./test-report-utils.mts";
|
||||
|
||||
function readBooleanEnv(name: string, env = process.env) {
|
||||
const normalized = env[name]?.trim().toLowerCase();
|
||||
return normalized === "1" || normalized === "true" || normalized === "yes";
|
||||
}
|
||||
|
||||
type PerfBudgetOptions = {
|
||||
baselineWallMs: number | null;
|
||||
config: string;
|
||||
@@ -61,7 +62,7 @@ function parseArgs(argv: readonly string[], env = process.env) {
|
||||
maxWallMs: readBudgetEnvNumber("OPENCLAW_TEST_PERF_MAX_WALL_MS", env),
|
||||
baselineWallMs: readBudgetEnvNumber("OPENCLAW_TEST_PERF_BASELINE_WALL_MS", env),
|
||||
maxRegressionPct: readBudgetEnvNumber("OPENCLAW_TEST_PERF_MAX_REGRESSION_PCT", env) ?? 10,
|
||||
reportOnly: readBooleanEnv("OPENCLAW_TEST_PERF_REPORT_ONLY", env),
|
||||
reportOnly: isStrictAffirmativeValue(env.OPENCLAW_TEST_PERF_REPORT_ONLY),
|
||||
},
|
||||
[
|
||||
stringFlag("--config", "config"),
|
||||
@@ -85,7 +86,7 @@ function collectPerfReportStats(reportPath: string) {
|
||||
report = readJsonFile(reportPath);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`[test-perf-budget] failed to read Vitest JSON report ${reportPath}: ${formatErrorMessage(
|
||||
`[test-perf-budget] failed to read Vitest JSON report ${reportPath}: ${coerceErrorMessage(
|
||||
error,
|
||||
)}`,
|
||||
{ cause: error },
|
||||
@@ -113,7 +114,7 @@ function main() {
|
||||
try {
|
||||
opts = parseArgs(process.argv.slice(2));
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
console.error(coerceErrorMessage(error));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -128,7 +129,7 @@ function main() {
|
||||
try {
|
||||
reportStats = collectPerfReportStats(reportPath);
|
||||
} catch (error) {
|
||||
console.error(formatErrorMessage(error));
|
||||
console.error(coerceErrorMessage(error));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,11 @@ import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import * as tar from "tar";
|
||||
import { readPositiveIntEnv } from "./e2e/lib/env-limits.mjs";
|
||||
import { sleep } from "./lib/sleep.mjs";
|
||||
|
||||
export { readPositiveIntEnv };
|
||||
|
||||
const DEFAULT_NPM_COMMAND_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
const DEFAULT_NPM_COMMAND_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
|
||||
|
||||
@@ -228,18 +231,6 @@ export function resolveNpmPackFilename(output: string) {
|
||||
return filename;
|
||||
}
|
||||
|
||||
export function readPositiveIntEnv(name: string, fallback: number, env = process.env) {
|
||||
const text = String(env[name] ?? fallback).trim();
|
||||
if (!/^\d+$/u.test(text)) {
|
||||
throw new Error(`invalid ${name}: ${text}`);
|
||||
}
|
||||
const value = Number(text);
|
||||
if (!Number.isSafeInteger(value) || value <= 0) {
|
||||
throw new Error(`invalid ${name}: ${text}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function readPluginNpmCommandOptions(env: NodeJS.ProcessEnv = process.env) {
|
||||
return {
|
||||
encoding: "utf8",
|
||||
|
||||
Reference in New Issue
Block a user