diff --git a/extensions/qa-lab/src/code-mode-model-matrix.options.test.ts b/extensions/qa-lab/src/code-mode-model-matrix.options.test.ts index 8f31a3aeff7c..6e4a3b57d692 100644 --- a/extensions/qa-lab/src/code-mode-model-matrix.options.test.ts +++ b/extensions/qa-lab/src/code-mode-model-matrix.options.test.ts @@ -146,9 +146,11 @@ describe("Code Mode model matrix options", () => { it("rejects ambiguous selectors and output paths", () => { expect(() => parseCodeModeMatrixOptions([])).toThrow("At least one --model"); expect(() => parseCodeModeMatrixOptions(["--model", "qwen3.5:9b"])).toThrow("provider/model"); - expect(() => - parseCodeModeMatrixOptions(["--model", "ollama/qwen3.5:9b", "--skip-build"]), - ).toThrow("Unknown argument"); + for (const arg of ["--skip-build", "constructor", "toString", "__proto__"]) { + expect(() => parseCodeModeMatrixOptions(["--model", "ollama/qwen3.5:9b", arg, "1"])).toThrow( + `Unknown argument: ${arg}`, + ); + } expect(() => parseCodeModeMatrixOptions([ "--model", diff --git a/scripts/android-app-i18n.ts b/scripts/android-app-i18n.ts index ebae7ee2db55..7b06d5259b4c 100644 --- a/scripts/android-app-i18n.ts +++ b/scripts/android-app-i18n.ts @@ -3,6 +3,7 @@ import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { expectDefined } from "../packages/normalization-core/src/expect.ts"; +import { compareAscii as compareText } from "./lib/canonical-json.mjs"; import { isDirectRunUrl } from "./lib/direct-run.mjs"; import { NATIVE_I18N_LOCALES } from "./native-i18n-locales.ts"; @@ -126,10 +127,6 @@ export type AndroidUiLiteralFinding = { source: string; }; -function compareText(left: string, right: string): number { - return left < right ? -1 : left > right ? 1 : 0; -} - function decodeXml(value: string): string { return value .replaceAll("<", "<") @@ -378,7 +375,7 @@ export function findUnusedAndroidResourceKeys( return [...keys].filter((key) => !references.has(key)); } -function lineNumber(source: string, offset: number): number { +export function lineNumber(source: string, offset: number): number { return source.slice(0, offset).split("\n").length; } @@ -602,7 +599,7 @@ function createDoubleQuoteScanner() { }; } -function findClosingDelimiter( +export function findClosingDelimiter( source: string, openingOffset: number, opening: string, diff --git a/scripts/apple-app-i18n.ts b/scripts/apple-app-i18n.ts index a9ccab08fa1d..8cc76ad089d5 100644 --- a/scripts/apple-app-i18n.ts +++ b/scripts/apple-app-i18n.ts @@ -2,6 +2,8 @@ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { decodeXml } from "../src/shared/xml.ts"; +import { selectDeterministicTranslation } from "./android-app-i18n.ts"; +import { compareAscii as compareCodeUnits } from "./lib/canonical-json.mjs"; import { NATIVE_I18N_LOCALES } from "./native-i18n-locales.ts"; const HERE = path.dirname(fileURLToPath(import.meta.url)); @@ -345,10 +347,6 @@ function formatTokens(value: string): string[] { return [...value.matchAll(FORMAT_RE)].map((match) => match[0]).toSorted(); } -function compareCodeUnits(left: string, right: string): number { - return left < right ? -1 : left > right ? 1 : 0; -} - export function serializeAppleCatalog(catalog: Catalog): string { const topLevelEntries = Object.entries(catalog); const lines = ["{"]; @@ -412,7 +410,7 @@ export function selectInfoPlistTranslation( (candidate) => candidate.trim() && candidate.trim() !== source.trim(), ); if (translatedCandidates.length > 0) { - return chooseTranslation(source, translatedCandidates); + return selectDeterministicTranslation(source, translatedCandidates); } return existing?.source === source && existing.value.trim() ? existing.value : source; } @@ -517,21 +515,6 @@ function appleCatalogValue(value: string): string { ); } -function chooseTranslation(source: string, translations: readonly string[]): string { - // Apple catalogs key by source, so duplicate native contexts must converge. - // Preserve shipped values first; otherwise choose deterministically and report every conflict. - const counts = new Map(); - for (const translation of translations) { - counts.set(translation, (counts.get(translation) ?? 0) + 1); - } - return ( - [...counts].toSorted(([leftValue, leftCount], [rightValue, rightCount]) => { - const sourcePenalty = Number(leftValue === source) - Number(rightValue === source); - return sourcePenalty || rightCount - leftCount || compareCodeUnits(leftValue, rightValue); - })[0]?.[0] ?? source - ); -} - function buildAppleCatalog( existingCatalog: Catalog, nativeSource: NativeSourceArtifact, @@ -589,7 +572,8 @@ function buildAppleCatalog( }; continue; } - const value = chooseTranslation(source, candidates); + const value = + candidates.length === 0 ? source : selectDeterministicTranslation(source, candidates); localizations[locale] = { stringUnit: { state: value === source ? "new" : "translated", diff --git a/scripts/bench-gateway-concurrency.ts b/scripts/bench-gateway-concurrency.ts index 0c77eafe886e..229e3b4424b9 100644 --- a/scripts/bench-gateway-concurrency.ts +++ b/scripts/bench-gateway-concurrency.ts @@ -24,6 +24,7 @@ import { isRecord } from "../packages/normalization-core/src/record-coerce.ts"; import { sliceUtf16Safe } from "../packages/normalization-core/src/utf16-slice.ts"; import type { createAgentTurnService } from "../src/gateway/agent-turn/agent-turn-service.js"; import type { SessionsListResult } from "../src/gateway/session-utils.types.js"; +import { createDeferredCore } from "../src/shared/deferred.ts"; import { applyMockOpenAiModelConfig } from "./e2e/lib/fixtures/mock-openai-config.mjs"; import { summarizeMockInferenceRequest, @@ -2161,34 +2162,21 @@ async function runGatewaySample( } } const historyClients = await concurrently( - Array.from({ length: options.historyClients }, async () => { - const historyClient = await connectGateway(port, setupDeadlineAt, protocolVersion, false); - ownClient(historyClient); - return historyClient; - }), + Array.from({ length: options.historyClients }, async () => + ownClient(await connectGateway(port, setupDeadlineAt, protocolVersion, false)), + ), ); const sessionUpdateClients = await concurrently( Array.from( { length: options.sessionUpdates > 0 ? options.sessionUpdateClients : 0 }, - async () => { - const updateClient = await connectGateway( - port, - setupDeadlineAt, - protocolVersion, - false, - ); - ownClient(updateClient); - return updateClient; - }, + async () => + ownClient(await connectGateway(port, setupDeadlineAt, protocolVersion, false)), ), ); const subscriptionProbeClient = options.subscribers > 0 - ? await connectGateway(port, setupDeadlineAt, protocolVersion, false) + ? ownClient(await connectGateway(port, setupDeadlineAt, protocolVersion, false)) : undefined; - if (subscriptionProbeClient) { - ownClient(subscriptionProbeClient); - } if (!live) { mockCheckpoints.push(await readMockRequests(mockPort, setupDeadlineAt)); } @@ -2272,10 +2260,7 @@ async function runGatewaySample( let browserDone = !browserProbe; const workloadDone = () => probesStopped || (turnsDone && updatesDone && browserDone); let startedTurnCount = 0; - let resolveAllTurnsStarted!: () => void; - const allTurnsStarted = new Promise((resolve) => { - resolveAllTurnsStarted = resolve; - }); + const { promise: allTurnsStarted, resolve: resolveAllTurnsStarted } = createDeferredCore(); if (!live) { providerBeforeLoad = readProviderRequestLog(requestLogPath).length; } diff --git a/scripts/bench-web-fetch.ts b/scripts/bench-web-fetch.ts index 53380973f67c..30ef731d0c14 100644 --- a/scripts/bench-web-fetch.ts +++ b/scripts/bench-web-fetch.ts @@ -1,10 +1,9 @@ // Web fetch benchmark covers direct response loading, HTML extraction, and fallback cleanup. -import { mkdir, writeFile } from "node:fs/promises"; -import path from "node:path"; import { performance } from "node:perf_hooks"; import type { OpenClawConfig } from "../src/config/types.openclaw.js"; import type { LookupFn } from "../src/infra/net/ssrf.js"; import * as cliArgs from "./lib/arg-utils.mts"; +import { writeReportArtifact } from "./lib/report-cli-helpers.mts"; type BenchmarkCaseId = (typeof ALL_CASE_IDS)[number]; @@ -285,74 +284,72 @@ async function loadCaseFactory(): Promise<() => Record { - return { - "tool-create": { - label: "create web_fetch tool", - run: () => { - createTool(); - }, + return () => ({ + "tool-create": { + label: "create web_fetch tool", + run: () => { + createTool(); }, - "tool-text": fetchCase("execute text/plain fetch", TEXT_BODY, "text/plain; charset=utf-8", { - url: "https://example.com/plain", - }), - "tool-markdown": fetchCase( - "execute text/markdown fetch", - MARKDOWN_BODY, - "text/markdown; charset=utf-8", - { url: "https://example.com/markdown" }, - ), - "tool-html-article": fetchCase( - "execute article HTML fetch", - ARTICLE_HTML, - "text/html; charset=utf-8", - { url: "https://example.com/article" }, - ), - "tool-html-article-text": fetchCase( - "execute article HTML fetch as text", - ARTICLE_HTML, - "text/html; charset=utf-8", - { url: "https://example.com/article-text", extractMode: "text" }, - ), - "tool-html-shell": fetchCase( - "execute shell HTML fallback fetch", - SHELL_HTML, - "text/html; charset=utf-8", - { url: "https://example.com/shell" }, - ), - "extract-readable-article": { - label: "extract readable article HTML", - run: async () => { - await extractReadableContent({ - html: ARTICLE_HTML, - url: "https://example.com/article", - extractMode: "markdown", - config: toolConfig, - }); - }, + }, + "tool-text": fetchCase("execute text/plain fetch", TEXT_BODY, "text/plain; charset=utf-8", { + url: "https://example.com/plain", + }), + "tool-markdown": fetchCase( + "execute text/markdown fetch", + MARKDOWN_BODY, + "text/markdown; charset=utf-8", + { url: "https://example.com/markdown" }, + ), + "tool-html-article": fetchCase( + "execute article HTML fetch", + ARTICLE_HTML, + "text/html; charset=utf-8", + { url: "https://example.com/article" }, + ), + "tool-html-article-text": fetchCase( + "execute article HTML fetch as text", + ARTICLE_HTML, + "text/html; charset=utf-8", + { url: "https://example.com/article-text", extractMode: "text" }, + ), + "tool-html-shell": fetchCase( + "execute shell HTML fallback fetch", + SHELL_HTML, + "text/html; charset=utf-8", + { url: "https://example.com/shell" }, + ), + "extract-readable-article": { + label: "extract readable article HTML", + run: async () => { + await extractReadableContent({ + html: ARTICLE_HTML, + url: "https://example.com/article", + extractMode: "markdown", + config: toolConfig, + }); }, - "extract-readable-article-text": { - label: "extract readable article HTML as text", - run: async () => { - await extractReadableContent({ - html: ARTICLE_HTML, - url: "https://example.com/article-text", - extractMode: "text", - config: toolConfig, - }); - }, + }, + "extract-readable-article-text": { + label: "extract readable article HTML as text", + run: async () => { + await extractReadableContent({ + html: ARTICLE_HTML, + url: "https://example.com/article-text", + extractMode: "text", + config: toolConfig, + }); }, - "extract-basic-shell": { - label: "extract basic shell HTML", - run: async () => { - await extractBasicHtmlContent({ - html: SHELL_HTML, - extractMode: "markdown", - }); - }, + }, + "extract-basic-shell": { + label: "extract basic shell HTML", + run: async () => { + await extractBasicHtmlContent({ + html: SHELL_HTML, + extractMode: "markdown", + }); }, - }; - }; + }, + }); } async function measureCase( @@ -414,10 +411,7 @@ async function main(): Promise { rssMb: Math.round((process.memoryUsage().rss / 1024 / 1024) * 10) / 10, }; }); - if (options.output) { - await mkdir(path.dirname(options.output), { recursive: true }); - await writeFile(options.output, `${JSON.stringify(report, null, 2)}\n`); - } + await writeReportArtifact(options.output ?? null, `${JSON.stringify(report, null, 2)}\n`); if (options.json) { console.log(JSON.stringify(report, null, 2)); } else { diff --git a/scripts/bench-workspace-computation.ts b/scripts/bench-workspace-computation.ts index 6708b8fb114d..0b221ab00a0a 100644 --- a/scripts/bench-workspace-computation.ts +++ b/scripts/bench-workspace-computation.ts @@ -213,7 +213,7 @@ async function createFixture(params: { await fs.mkdir(workspace); if (scenario === "inventory" || scenario === "delta" || scenario === "unchanged") { await exec("git", ["-c", `core.hooksPath=${os.devNull}`, "init", "--quiet", workspace], { - env: { ...isolatedEnv(root), GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: os.devNull }, + env: isolatedEnv(root), }); } if (scenario === "inventory" || scenario === "manifest") { diff --git a/scripts/check-control-ui-performance.mts b/scripts/check-control-ui-performance.mts index dd13fdc2bac7..d8955218aabe 100644 --- a/scripts/check-control-ui-performance.mts +++ b/scripts/check-control-ui-performance.mts @@ -10,10 +10,8 @@ import { } from "../src/gateway/control-ui-route-preloads.ts"; import { reportLimitViolations } from "./lib/check-limits.mts"; import { CONTROL_UI_LOCALE_ENTRIES } from "./lib/control-ui-i18n-config.ts"; - -function isMetricsRecord(value: unknown): value is Record { - return value !== null && typeof value === "object" && !Array.isArray(value); -} +import { isRecord } from "./lib/record-shared.mjs"; +import { escapeRegExp } from "./lib/regexp.mjs"; const KIB = 1024; const STARTUP_JS_BASELINE_RATCHET_BYTES = 4096; @@ -139,10 +137,6 @@ function largestAsset(assets: Array>) { )[0]!; } -function escapeRegExp(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); -} - function controlUiLocaleAssetIdentity( file: string, ): { locale: string; kind: "base" | "configHints" } | null { @@ -541,7 +535,7 @@ function isIsoDate(value: string): boolean { function readControlUiStartupBudgetBaseline(baselinePath: string): ControlUiStartupBudgetBaseline { try { const parsed: unknown = JSON.parse(fs.readFileSync(baselinePath, "utf8")); - const record: Record = isMetricsRecord(parsed) ? parsed : {}; + const record: Record = isRecord(parsed) ? parsed : {}; const { startupJsGzipBytes, reason, updatedAt } = record; if ( typeof startupJsGzipBytes !== "number" || diff --git a/scripts/check-database-worker-ratchet.mts b/scripts/check-database-worker-ratchet.mts index b0f2fdcedea9..6cb0fb0899d3 100644 --- a/scripts/check-database-worker-ratchet.mts +++ b/scripts/check-database-worker-ratchet.mts @@ -4,7 +4,6 @@ import { compareRatchetCounts, parseRatchetArgs, reportRatchetFailures, - reportRatchetSuccess, resolveRatchetBase, } from "./lib/shrink-ratchet.mts"; @@ -48,7 +47,7 @@ export function main(root = process.cwd(), argv = process.argv.slice(2)) { ) { return 1; } - reportRatchetSuccess("SQLite worker ratchet OK: no T1 call-count growth against " + base + "."); + console.log("SQLite worker ratchet OK: no T1 call-count growth against " + base + "."); return 0; } catch (error) { console.error(error instanceof Error ? error.message : String(error)); diff --git a/scripts/check-env-var-count.mts b/scripts/check-env-var-count.mts index 39cf229663f8..81f012a8ce52 100644 --- a/scripts/check-env-var-count.mts +++ b/scripts/check-env-var-count.mts @@ -8,7 +8,6 @@ import { loadRatchetSnapshot, loadRatchetSources, parseRatchetScalar, - reportRatchetSuccess, } from "./lib/shrink-ratchet.mts"; const BUDGET_PATH = "config/env-var-count-budget.txt"; @@ -161,7 +160,7 @@ export function main( throw new Error(messages.join("\n")); } if (messages.length === 0 && growth.length === 0) { - reportRatchetSuccess(`OPENCLAW_* count ${names.length}/${budget}`); + console.log(`OPENCLAW_* count ${names.length}/${budget}`); } return names.length; } diff --git a/scripts/check-extension-package-tsc-boundary.mts b/scripts/check-extension-package-tsc-boundary.mts index dc43932f347d..fbbe116e1913 100644 --- a/scripts/check-extension-package-tsc-boundary.mts +++ b/scripts/check-extension-package-tsc-boundary.mts @@ -21,7 +21,7 @@ import { resolveTimerTimeoutMs, } from "../packages/normalization-core/src/number-coercion.ts"; import { collectFilesSync } from "./check-file-utils.ts"; -import { appendBoundedTail } from "./lib/bounded-output-tail.mjs"; +import { appendBoundedTail, formatBoundedTail } from "./lib/bounded-output-tail.mjs"; import { portableRelativePath, readArtifactRecord, @@ -159,13 +159,6 @@ function createStepOutputCapture(): StepOutputCapture { return { text: "", truncatedChars: 0 }; } -function formatCapturedStepOutput(buffer: StepOutputCapture) { - if (buffer.truncatedChars === 0) { - return buffer.text; - } - return `[output truncated ${buffer.truncatedChars} chars; showing tail]\n${buffer.text}`; -} - function isPositiveFinite(value: number | undefined): value is number { return typeof value === "number" && Number.isFinite(value) && value > 0; } @@ -174,9 +167,6 @@ function isPositiveInteger(value: number | undefined): value is number { return typeof value === "number" && Number.isInteger(value) && value > 0; } -/** - * Formats the successful boundary compile summary. - */ export function formatBoundaryCheckSuccessSummary(params: BoundarySummaryParams = {}) { const lines = ["extension package boundary check passed"]; if (params.mode) { @@ -206,9 +196,6 @@ export function formatBoundaryCheckSuccessSummary(params: BoundarySummaryParams return `${lines.join("\n")}\n`; } -/** - * Formats skipped compile progress for fresh extension canaries. - */ export function formatSkippedCompileProgress(params: SkippedCompileParams = {}) { const skippedCount = params.skippedCount ?? 0; const totalCount = params.totalCount ?? 0; @@ -223,9 +210,6 @@ export function formatSkippedCompileProgress(params: SkippedCompileParams = {}) return `skipped ${skippedCount} fresh plugin compiles\n`; } -/** - * Formats slow extension compile diagnostics. - */ export function formatSlowCompileSummary(params: SlowCompileParams = {}) { const compileTimings = Array.isArray(params.compileTimings) ? params.compileTimings : []; if (compileTimings.length === 0) { @@ -242,9 +226,6 @@ export function formatSlowCompileSummary(params: SlowCompileParams = {}) { return `${lines.join("\n")}\n`; } -/** - * Formats a failed boundary-check child process step. - */ export function formatStepFailure(label: string, params: StepFailureParams = {}) { const stdoutSection = summarizeOutputSection("stdout", params.stdout ?? ""); const stderrSection = summarizeOutputSection("stderr", params.stderr ?? ""); @@ -363,8 +344,8 @@ export async function runNodeStepAsync( }); } return { - stdout: formatCapturedStepOutput(stdout), - stderr: formatCapturedStepOutput(stderr), + stdout: formatBoundedTail(stdout), + stderr: formatBoundedTail(stderr), elapsedMs: Date.now() - startedAt, }; } catch (error) { @@ -383,8 +364,8 @@ export async function runNodeStepAsync( ? "signal" : "spawn-error"; const detail = { - stdout: formatCapturedStepOutput(stdout), - stderr: formatCapturedStepOutput(stderr), + stdout: formatBoundedTail(stdout), + stderr: formatBoundedTail(stderr), kind, elapsedMs: Date.now() - startedAt, note: @@ -403,9 +384,6 @@ export async function runNodeStepAsync( } } -/** - * Runs boundary check steps with bounded concurrency. - */ export async function runNodeStepsWithConcurrency(steps: BoundaryStep[], concurrency: number) { const abortController = new AbortController(); let firstFailure: unknown = null; @@ -445,9 +423,6 @@ export async function runNodeStepsWithConcurrency(steps: BoundaryStep[], concurr } } -/** - * Resolves canary artifact paths for an extension boundary compile. - */ export function resolveCanaryArtifactPaths(extensionId: string, rootDir = repoRoot) { const extensionRoot = resolve(rootDir, "extensions", extensionId); return { @@ -457,9 +432,6 @@ export function resolveCanaryArtifactPaths(extensionId: string, rootDir = repoRo }; } -/** - * Removes canary artifacts for one extension. - */ function cleanupCanaryArtifacts(extensionId: string, rootDir = repoRoot) { const { canaryPath, tsconfigPath } = resolveCanaryArtifactPaths(extensionId, rootDir); rmSync(canaryPath, { force: true }); @@ -467,18 +439,12 @@ function cleanupCanaryArtifacts(extensionId: string, rootDir = repoRoot) { rmSync(resolveBoundaryInputReceiptPath(`${extensionId}-canary`, rootDir), { force: true }); } -/** - * Removes canary artifacts for multiple extensions. - */ function cleanupCanaryArtifactsForExtensions(extensionIds: string[], rootDir = repoRoot) { for (const extensionId of extensionIds) { cleanupCanaryArtifacts(extensionId, rootDir); } } -/** - * Installs signal/exit cleanup for extension canary artifacts. - */ export function installCanaryArtifactCleanup( extensionIds: string[], params: BoundaryCheckParams = {}, @@ -730,9 +696,6 @@ async function runCanaryCheck(extensionIds: string[]) { }; } -/** - * Runs the extension package TypeScript boundary check. - */ async function runBoundaryCheck(argv: string[]) { const startedAt = Date.now(); const mode = parseMode(argv); diff --git a/scripts/check-line-cap-ratchet.mts b/scripts/check-line-cap-ratchet.mts index 7f97ca01d1fc..960f8b912dcb 100644 --- a/scripts/check-line-cap-ratchet.mts +++ b/scripts/check-line-cap-ratchet.mts @@ -15,7 +15,6 @@ import { loadRatchetSnapshot, loadRatchetSources, parseRatchetArgs, - reportRatchetSuccess, resolveRatchetBase, } from "./lib/shrink-ratchet.mts"; @@ -204,7 +203,7 @@ export function main(root = process.cwd(), argv = process.argv.slice(2)) { return 1; } if (increased.length === 0) { - reportRatchetSuccess( + console.log( `Line-cap ratchet OK: ${paths.length} changed source files; no new violations or over-cap growth.`, ); } diff --git a/scripts/check-max-lines-ratchet.mts b/scripts/check-max-lines-ratchet.mts index 1f351dbd58d5..9fc527e33021 100644 --- a/scripts/check-max-lines-ratchet.mts +++ b/scripts/check-max-lines-ratchet.mts @@ -19,7 +19,6 @@ import { parseRatchetArgs, parseRatchetPaths, reportRatchetFailures, - reportRatchetSuccess, resolveRatchetBase, } from "./lib/shrink-ratchet.mts"; import { collectTypeScriptCommentRanges } from "./lib/ts-guard-utils.mts"; @@ -254,9 +253,7 @@ export function main( .filter((entry) => current.includes(entry)) .toSorted(compareStrings); writeBaseline(root, kept); - reportRatchetSuccess( - "Pruned " + BASELINE_PATH + ": " + baseline.size + " -> " + kept.length + ".", - ); + console.log("Pruned " + BASELINE_PATH + ": " + baseline.size + " -> " + kept.length + "."); return 0; } if ( @@ -272,9 +269,7 @@ export function main( } if (added.length + expanded.length + stale.length === 0) { - reportRatchetSuccess( - "max-lines ratchet OK: " + current.length + " grandfathered suppressions.", - ); + console.log("max-lines ratchet OK: " + current.length + " grandfathered suppressions."); } return 0; } catch (error) { diff --git a/scripts/check-plugin-extension-import-boundary.mts b/scripts/check-plugin-extension-import-boundary.mts index 774702680a91..cdf396f0e21a 100644 --- a/scripts/check-plugin-extension-import-boundary.mts +++ b/scripts/check-plugin-extension-import-boundary.mts @@ -120,8 +120,6 @@ async function runPluginExtensionImportBoundaryCheck(): Promise<0 | 1> { return 1; } -async function main(): Promise { +runAsScript(import.meta.url, async () => { process.exitCode = await runPluginExtensionImportBoundaryCheck(); -} - -runAsScript(import.meta.url, main); +}); diff --git a/scripts/check-plugin-gateway-gauntlet.mts b/scripts/check-plugin-gateway-gauntlet.mts index b2d3657531be..adcccc4019da 100644 --- a/scripts/check-plugin-gateway-gauntlet.mts +++ b/scripts/check-plugin-gateway-gauntlet.mts @@ -12,6 +12,7 @@ import { resolveTimerTimeoutMs, } from "../packages/normalization-core/src/number-coercion.ts"; import { normalizeCsvOrLooseStringList } from "../packages/normalization-core/src/string-normalization.ts"; +import { chunkItems } from "../src/utils/chunk-items.ts"; import { stripLeadingPackageManagerSeparator } from "./lib/arg-utils.mts"; import { hasUnjoinedWork, runManagedCommand } from "./lib/managed-child-process.mts"; import { @@ -371,14 +372,6 @@ function sourceOpenclawCommand(repoRoot: string, args: string[]) { }; } -function chunkArray(values: Value[], chunkSize: number) { - const chunks: Value[][] = []; - for (let index = 0; index < values.length; index += chunkSize) { - chunks.push(values.slice(index, index + chunkSize)); - } - return chunks; -} - export function toRepoRelativePath(repoRoot: string, absolutePath: string) { const relativePath = path.relative(repoRoot, absolutePath); if (!relativePath || relativePath.startsWith("..") || path.isAbsolute(relativePath)) { @@ -798,7 +791,7 @@ async function runSlashHelpProbes(params: GauntletContext) { async function runQaChunks(params: GauntletContext) { const chunks = [ ...(params.qaBaseline ? [{ label: "baseline", plugins: [] }] : []), - ...chunkArray(params.plugins, params.qaPluginChunkSize).map((plugins, index) => ({ + ...chunkItems(params.plugins, params.qaPluginChunkSize).map((plugins, index) => ({ label: `chunk-${String(index).padStart(2, "0")}`, plugins, })), diff --git a/scripts/close-duplicate-prs-after-merge.mjs b/scripts/close-duplicate-prs-after-merge.mjs index 82657682a3cc..d27e2ca30726 100644 --- a/scripts/close-duplicate-prs-after-merge.mjs +++ b/scripts/close-duplicate-prs-after-merge.mjs @@ -20,7 +20,7 @@ export function parsePrNumberList(value) { ...new Set( text .split(/[\s,]+/u) - .map((part) => part.trim().replace(/^#/u, "")) + .map((part) => part.replace(/^#/u, "")) .filter(Boolean) .map((part) => { if (!/^\d+$/u.test(part)) { @@ -62,7 +62,6 @@ export function parseArgs(argv, env = process.env) { } else if (arg === "--labels") { args.labels = next() .split(/[\s,]+/u) - .map((label) => label.trim()) .filter(Boolean); } else if (arg === "--help" || arg === "-h") { help = true; diff --git a/scripts/code-mode-model-matrix.ts b/scripts/code-mode-model-matrix.ts index 085c9333b51d..7566f817fc20 100644 --- a/scripts/code-mode-model-matrix.ts +++ b/scripts/code-mode-model-matrix.ts @@ -48,6 +48,7 @@ import { } from "./lib/code-mode-matrix-provider.ts"; import type { MatrixUsageAccounting } from "./lib/code-mode-matrix-usage.ts"; import { previewForDevToolLog, redactJsonValueForDevToolLog } from "./lib/dev-tooling-safety.ts"; +import { groupBy } from "./lib/group-by.mts"; export { validateQaEvidenceSummaryJson }; @@ -333,7 +334,7 @@ export function parseCodeModeMatrixOptions( "--max-known-cost-usd": "maxKnownCostUsd", "--max-wall-seconds": "maxWallSeconds", }; - const admissionKey = admissionKeys[arg]; + const admissionKey = Object.hasOwn(admissionKeys, arg) ? admissionKeys[arg] : undefined; if (admissionKey) { recordOnce(arg); const raw = requireOptionArgument(argv, index, arg); @@ -1377,13 +1378,7 @@ function summarizeMetric(values: (number | undefined)[]) { } function summarizeResults(results: CodeModeMatrixCellResult[]) { - const groups = new Map(); - for (const result of results) { - const key = `${result.model}\0${result.mode}\0${result.task}`; - const group = groups.get(key) ?? []; - group.push(result); - groups.set(key, group); - } + const groups = groupBy(results, (result) => `${result.model}\0${result.mode}\0${result.task}`); return [...groups.entries()].map(([key, group]) => { const [model, mode, task] = key.split("\0"); const passed = group.filter((result) => result.passed); @@ -1953,11 +1948,7 @@ async function main(): Promise { } } -function isCliEntrypoint(): boolean { - const entrypoint = process.argv[1]; - return Boolean(entrypoint && import.meta.url === pathToFileURL(path.resolve(entrypoint)).href); -} - -if (isCliEntrypoint()) { +const entrypoint = process.argv[1]; +if (entrypoint && import.meta.url === pathToFileURL(path.resolve(entrypoint)).href) { await main(); } diff --git a/scripts/e2e/codex-npm-plugin-live-docker.sh b/scripts/e2e/codex-npm-plugin-live-docker.sh index f33702c3086b..ff0e4d5c3ea8 100644 --- a/scripts/e2e/codex-npm-plugin-live-docker.sh +++ b/scripts/e2e/codex-npm-plugin-live-docker.sh @@ -14,7 +14,6 @@ TRUSTED_HARNESS_DIR="$(cd "$TRUSTED_HARNESS_DIR" && pwd)" CANDIDATE_ROOT="$(cd "$CANDIDATE_ROOT" && pwd)" ROOT_DIR="$TRUSTED_HARNESS_DIR" source "$TRUSTED_HARNESS_DIR/scripts/lib/docker-e2e-image.sh" -source "$TRUSTED_HARNESS_DIR/scripts/lib/docker-e2e-package.sh" IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-codex-npm-plugin-live-e2e" OPENCLAW_CODEX_NPM_PLUGIN_E2E_IMAGE)" DOCKER_TARGET="${OPENCLAW_CODEX_NPM_PLUGIN_DOCKER_TARGET:-bare}" @@ -101,22 +100,18 @@ trap cleanup EXIT docker_e2e_build_or_reuse "$IMAGE_NAME" codex-npm-plugin-live "$CANDIDATE_ROOT/scripts/e2e/Dockerfile" "$CANDIDATE_ROOT" "$DOCKER_TARGET" -prepare_package_tgz() { - if [ -n "$PACKAGE_TGZ" ]; then - PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz codex-npm-plugin-live "$PACKAGE_TGZ")" - return 0 - fi +if [ -n "$PACKAGE_TGZ" ]; then + PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz codex-npm-plugin-live "$PACKAGE_TGZ")" +else if [ "$HOST_BUILD" = "0" ] && [ -z "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}" ]; then echo "OPENCLAW_CODEX_NPM_PLUGIN_HOST_BUILD=0 requires OPENCLAW_CURRENT_PACKAGE_TGZ" >&2 exit 1 fi - local harness_root="$ROOT_DIR" - ROOT_DIR="$CANDIDATE_ROOT" - PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz codex-npm-plugin-live)" - ROOT_DIR="$harness_root" -} - -prepare_package_tgz + PACKAGE_TGZ="$( + ROOT_DIR="$CANDIDATE_ROOT" + docker_e2e_prepare_package_tgz codex-npm-plugin-live + )" +fi configure_codex_plugin_registry_candidate() { local source_path="$1" diff --git a/scripts/e2e/codex-on-demand-docker.sh b/scripts/e2e/codex-on-demand-docker.sh index 3adbf4bf8d18..a44f936ff318 100755 --- a/scripts/e2e/codex-on-demand-docker.sh +++ b/scripts/e2e/codex-on-demand-docker.sh @@ -9,7 +9,6 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" source "$ROOT_DIR/scripts/e2e/lib/prepublish-plugin-registry.sh" source "$ROOT_DIR/scripts/lib/frozen-target-compat.sh" @@ -53,19 +52,11 @@ trap cleanup EXIT docker_e2e_build_or_reuse "$IMAGE_NAME" codex-on-demand "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "$DOCKER_TARGET" -prepare_package_tgz() { - if [ -n "$PACKAGE_TGZ" ]; then - PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz codex-on-demand "$PACKAGE_TGZ")" - return 0 - fi - if [ "$HOST_BUILD" = "0" ] && [ -z "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}" ]; then - echo "OPENCLAW_CODEX_ON_DEMAND_HOST_BUILD=0 requires OPENCLAW_CURRENT_PACKAGE_TGZ" >&2 - exit 1 - fi - PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz codex-on-demand)" -} - -prepare_package_tgz +if [ -z "$PACKAGE_TGZ" ] && [ "$HOST_BUILD" = "0" ] && [ -z "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}" ]; then + echo "OPENCLAW_CODEX_ON_DEMAND_HOST_BUILD=0 requires OPENCLAW_CURRENT_PACKAGE_TGZ" >&2 + exit 1 +fi +PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz codex-on-demand "$PACKAGE_TGZ")" if [ -z "${OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR:-}" ] && [ -z "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}" ] && diff --git a/scripts/e2e/doctor-install-switch-docker.sh b/scripts/e2e/doctor-install-switch-docker.sh index a0b8310dc94c..29939b2ebcff 100755 --- a/scripts/e2e/doctor-install-switch-docker.sh +++ b/scripts/e2e/doctor-install-switch-docker.sh @@ -11,14 +11,10 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" TARGET_ROOT_DIR="$(cd "${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$ROOT_DIR}" && pwd)" TARGET_CONTRACT_DIR="$TARGET_ROOT_DIR/scripts/e2e/lib/doctor-install-switch" source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-doctor-install-switch-e2e" OPENCLAW_DOCTOR_INSTALL_SWITCH_E2E_IMAGE)" NPM_INSTALL_TIMEOUT="${OPENCLAW_E2E_NPM_INSTALL_TIMEOUT:-600s}" COMMAND_TIMEOUT="${OPENCLAW_DOCKER_DOCTOR_SWITCH_COMMAND_TIMEOUT:-900s}" -cleanup() { - docker_e2e_cleanup_package_tgz "${PACKAGE_TGZ:-}" -} -trap cleanup EXIT +trap 'docker_e2e_cleanup_package_tgz "${PACKAGE_TGZ:-}"' EXIT PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz doctor-switch "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")" # Bare lanes mount the package artifact instead of baking app sources into the image. diff --git a/scripts/e2e/lib/kitchen-sink-plugin/sweep.sh b/scripts/e2e/lib/kitchen-sink-plugin/sweep.sh index 066ed2fe2568..c062cb7735dd 100644 --- a/scripts/e2e/lib/kitchen-sink-plugin/sweep.sh +++ b/scripts/e2e/lib/kitchen-sink-plugin/sweep.sh @@ -141,61 +141,37 @@ start_kitchen_sink_clawhub_fixture_server() { return 1 } -scan_logs_for_unexpected_errors() { - node scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs scan-logs -} - -configure_kitchen_sink_runtime() { - node scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs configure-runtime -} - -remove_kitchen_sink_channel_config() { - node scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs remove-channel-config -} - -assert_kitchen_sink_installed() { - node scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs assert-installed -} - -assert_kitchen_sink_removed() { - node scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs assert-removed -} - -assert_kitchen_sink_cutover_preinstalled() { - node scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs assert-cutover-preinstalled -} - run_success_scenario() { echo "Testing ${KITCHEN_SINK_LABEL} install from ${KITCHEN_SINK_SPEC}..." local install_args=("$KITCHEN_SINK_SPEC") if [ -n "${KITCHEN_SINK_PREINSTALL_SPEC:-}" ]; then run_kitchen_sink_fixture_logged "kitchen-sink-preinstall-${KITCHEN_SINK_LABEL}" plugins install "$KITCHEN_SINK_PREINSTALL_SPEC" --force - assert_kitchen_sink_cutover_preinstalled + node scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs assert-cutover-preinstalled install_args+=("--force") fi run_kitchen_sink_fixture_logged "kitchen-sink-install-${KITCHEN_SINK_LABEL}" plugins install "${install_args[@]}" --force - configure_kitchen_sink_runtime + node scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs configure-runtime run_kitchen_sink_openclaw_logged "kitchen-sink-enable-${KITCHEN_SINK_LABEL}" plugins enable "$KITCHEN_SINK_ID" run_kitchen_sink_openclaw_capture "${KITCHEN_SINK_TMP_DIR}/kitchen-sink-${KITCHEN_SINK_LABEL}-plugins.json" plugins list --json run_kitchen_sink_openclaw_capture "${KITCHEN_SINK_TMP_DIR}/kitchen-sink-${KITCHEN_SINK_LABEL}-inspect.json" plugins inspect "$KITCHEN_SINK_ID" --runtime --json run_kitchen_sink_openclaw_capture "${KITCHEN_SINK_TMP_DIR}/kitchen-sink-${KITCHEN_SINK_LABEL}-inspect-all.json" plugins inspect --all --runtime --json - assert_kitchen_sink_installed + node scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs assert-installed if [ "$KITCHEN_SINK_SOURCE" = "clawhub" ]; then run_kitchen_sink_openclaw_logged "kitchen-sink-uninstall-${KITCHEN_SINK_LABEL}" plugins uninstall "$KITCHEN_SINK_SPEC" --force else run_kitchen_sink_openclaw_logged "kitchen-sink-uninstall-${KITCHEN_SINK_LABEL}" plugins uninstall "$KITCHEN_SINK_ID" --force fi - remove_kitchen_sink_channel_config + node scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs remove-channel-config run_kitchen_sink_openclaw_capture "${KITCHEN_SINK_TMP_DIR}/kitchen-sink-${KITCHEN_SINK_LABEL}-uninstalled.json" plugins list --json - assert_kitchen_sink_removed + node scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs assert-removed } run_failure_scenario() { echo "Testing expected ${KITCHEN_SINK_LABEL} install failure from ${KITCHEN_SINK_SPEC}..." run_expect_failure "install-${KITCHEN_SINK_LABEL}" openclaw_e2e_maybe_timeout "$KITCHEN_SINK_CLI_TIMEOUT" node "$OPENCLAW_ENTRY" plugins install "$KITCHEN_SINK_SPEC" --force - remove_kitchen_sink_channel_config + node scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs remove-channel-config run_kitchen_sink_openclaw_capture "${KITCHEN_SINK_TMP_DIR}/kitchen-sink-${KITCHEN_SINK_LABEL}-uninstalled.json" plugins list --json - assert_kitchen_sink_removed + node scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs assert-removed } run_kitchen_sink_sweep_main() { @@ -246,7 +222,7 @@ run_kitchen_sink_sweep_main() { exit 1 fi - scan_logs_for_unexpected_errors + node scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs scan-logs echo "kitchen-sink plugin Docker E2E passed (${scenario_count} scenario(s))" } diff --git a/scripts/e2e/lib/plugins/fixtures.sh b/scripts/e2e/lib/plugins/fixtures.sh index df4ac4763c11..9ef11bcba226 100644 --- a/scripts/e2e/lib/plugins/fixtures.sh +++ b/scripts/e2e/lib/plugins/fixtures.sh @@ -141,40 +141,6 @@ openclaw_plugins_fixture_exit_trap() { exit "$status" } -record_fixture_plugin_trust() { - local plugin_id="$1" - local plugin_root="$2" - local enabled="$3" - node scripts/e2e/lib/plugins/assertions.mjs record-fixture-plugin-trust "$plugin_id" "$plugin_root" "$enabled" -} - -write_demo_fixture_plugin() { - local dir="$1" - node scripts/e2e/lib/fixture.mjs plugin-demo "$dir" -} - -write_fixture_plugin() { - local dir="$1" - local id="$2" - local version="$3" - local method="$4" - local name="$5" - - node scripts/e2e/lib/fixture.mjs plugin "$dir" "$id" "$version" "$method" "$name" -} - -write_fixture_plugin_with_cli() { - local dir="$1" - local id="$2" - local version="$3" - local method="$4" - local name="$5" - local cli_root="$6" - local cli_output="$7" - - node scripts/e2e/lib/fixture.mjs plugin-cli "$dir" "$id" "$version" "$method" "$name" "$cli_root" "$cli_output" -} - pack_fixture_plugin_with_cli_registry_dependency() { local pack_dir="$1" local output_tgz="$2" @@ -199,16 +165,6 @@ pack_fake_is_number_package() { tar -czf "$output_tgz" -C "$pack_dir" package } -write_fixture_plugin_with_vendored_dependency() { - local dir="$1" - local id="$2" - local version="$3" - local method="$4" - local name="$5" - - node scripts/e2e/lib/fixture.mjs plugin-vendored-dep "$dir" "$id" "$version" "$method" "$name" -} - pack_fixture_plugin() { local pack_dir="$1" local output_tgz="$2" @@ -218,7 +174,7 @@ pack_fixture_plugin() { local name="$6" mkdir -p "$pack_dir/package" - write_fixture_plugin "$pack_dir/package" "$id" "$version" "$method" "$name" + node scripts/e2e/lib/fixture.mjs plugin "$pack_dir/package" "$id" "$version" "$method" "$name" tar -czf "$output_tgz" -C "$pack_dir" package } @@ -231,7 +187,7 @@ pack_fixture_plugin_with_invalid_extension_entry() { local name="$6" mkdir -p "$pack_dir/package" - write_fixture_plugin "$pack_dir/package" "$id" "$version" "$method" "$name" + node scripts/e2e/lib/fixture.mjs plugin "$pack_dir/package" "$id" "$version" "$method" "$name" node --input-type=module - "$pack_dir/package/package.json" <<'NODE' import fs from "node:fs"; @@ -279,9 +235,3 @@ start_npm_fixture_registry() { echo "Timed out waiting for npm fixture registry." >&2 return 1 } - -write_claude_bundle_fixture() { - local bundle_root="$1" - - node scripts/e2e/lib/fixture.mjs claude-bundle "$bundle_root" -} diff --git a/scripts/e2e/lib/plugins/marketplace.sh b/scripts/e2e/lib/plugins/marketplace.sh index 05da02ae9897..6481ffe4fb6e 100644 --- a/scripts/e2e/lib/plugins/marketplace.sh +++ b/scripts/e2e/lib/plugins/marketplace.sh @@ -2,13 +2,13 @@ run_plugins_marketplace_scenario() { echo "Testing marketplace install and update flows..." marketplace_root="$HOME/.claude/plugins/marketplaces/fixture-marketplace" mkdir -p "$HOME/.claude/plugins" "$marketplace_root/.claude-plugin" - write_fixture_plugin \ + node scripts/e2e/lib/fixture.mjs plugin \ "$marketplace_root/plugins/marketplace-shortcut" \ "marketplace-shortcut" \ "0.0.1" \ "demo.marketplace.shortcut.v1" \ "Marketplace Shortcut" - write_fixture_plugin \ + node scripts/e2e/lib/fixture.mjs plugin \ "$marketplace_root/plugins/marketplace-direct" \ "marketplace-direct" \ "0.0.1" \ @@ -30,7 +30,7 @@ run_plugins_marketplace_scenario() { node scripts/e2e/lib/plugins/assertions.mjs marketplace-records - write_fixture_plugin \ + node scripts/e2e/lib/fixture.mjs plugin \ "$marketplace_root/plugins/marketplace-shortcut" \ "marketplace-shortcut" \ "0.0.2" \ diff --git a/scripts/e2e/lib/plugins/sweep.sh b/scripts/e2e/lib/plugins/sweep.sh index 24f1ae761928..080d7e008962 100644 --- a/scripts/e2e/lib/plugins/sweep.sh +++ b/scripts/e2e/lib/plugins/sweep.sh @@ -134,8 +134,8 @@ OPENCLAW_PLUGIN_HOME="$HOME/.openclaw/$BUNDLED_PLUGIN_ROOT_DIR" demo_plugin_id="demo-plugin" demo_plugin_root="$OPENCLAW_PLUGIN_HOME/$demo_plugin_id" -write_demo_fixture_plugin "$demo_plugin_root" -record_fixture_plugin_trust "$demo_plugin_id" "$demo_plugin_root" 1 +node scripts/e2e/lib/fixture.mjs plugin-demo "$demo_plugin_root" +node scripts/e2e/lib/plugins/assertions.mjs record-fixture-plugin-trust "$demo_plugin_id" "$demo_plugin_root" 1 run_plugins_openclaw_capture "$OPENCLAW_PLUGINS_TMP_DIR/plugins.json" plugins list --json run_plugins_openclaw_capture "$OPENCLAW_PLUGINS_TMP_DIR/plugins-inspect.json" plugins inspect demo-plugin --runtime --json @@ -158,7 +158,7 @@ node scripts/e2e/lib/plugins/assertions.mjs plugin-tgz-removed echo "Testing install from local folder (plugins.load.paths)..." dir_plugin="$(mktemp -d "$OPENCLAW_PLUGINS_TMP_DIR/openclaw-plugin-dir.XXXXXX")" -write_fixture_plugin "$dir_plugin" demo-plugin-dir 0.0.1 demo.dir "Demo Plugin DIR" +node scripts/e2e/lib/fixture.mjs plugin "$dir_plugin" demo-plugin-dir 0.0.1 demo.dir "Demo Plugin DIR" run_plugins_fixture_logged install-dir plugins install "$dir_plugin" --force run_plugins_openclaw_capture "$OPENCLAW_PLUGINS_TMP_DIR/plugins3.json" plugins list --json @@ -175,7 +175,7 @@ node scripts/e2e/lib/plugins/assertions.mjs plugin-dir-removed echo "Testing install from local folder with preinstalled dependencies..." dir_deps_plugin="$(mktemp -d "$OPENCLAW_PLUGINS_TMP_DIR/openclaw-plugin-dir-deps.XXXXXX")" -write_fixture_plugin_with_vendored_dependency "$dir_deps_plugin" demo-plugin-dir-deps 0.0.1 demo.dir.deps "Demo Plugin DIR Deps" +node scripts/e2e/lib/fixture.mjs plugin-vendored-dep "$dir_deps_plugin" demo-plugin-dir-deps 0.0.1 demo.dir.deps "Demo Plugin DIR Deps" run_plugins_fixture_logged install-dir-deps plugins install "$dir_deps_plugin" --force run_plugins_openclaw_capture "$OPENCLAW_PLUGINS_TMP_DIR/plugins-dir-deps.json" plugins list --json @@ -189,7 +189,7 @@ node scripts/e2e/lib/plugins/assertions.mjs plugin-dir-deps-removed echo "Testing install from npm spec (file:)..." file_pack_dir="$(mktemp -d "$OPENCLAW_PLUGINS_TMP_DIR/openclaw-plugin-filepack.XXXXXX")" -write_fixture_plugin "$file_pack_dir/package" demo-plugin-file 0.0.1 demo.file "Demo Plugin FILE" +node scripts/e2e/lib/fixture.mjs plugin "$file_pack_dir/package" demo-plugin-file 0.0.1 demo.file "Demo Plugin FILE" run_plugins_fixture_logged install-file plugins install "file:$file_pack_dir/package" --force run_plugins_openclaw_capture "$OPENCLAW_PLUGINS_TMP_DIR/plugins4.json" plugins list --json @@ -252,7 +252,7 @@ echo "Testing install from git repo and plugin CLI execution..." git_fixture_root="$(mktemp -d "$OPENCLAW_PLUGINS_TMP_DIR/openclaw-plugin-git.XXXXXX")" git_repo="$git_fixture_root/repo" git_repo_url="file://$git_repo" -write_fixture_plugin_with_cli "$git_repo" demo-plugin-git 0.0.1 demo.git "Demo Plugin Git" demo-git "demo-plugin-git:pong" +node scripts/e2e/lib/fixture.mjs plugin-cli "$git_repo" demo-plugin-git 0.0.1 demo.git "Demo Plugin Git" demo-git "demo-plugin-git:pong" git -C "$git_repo" init -q git -C "$git_repo" config user.email "docker-e2e@openclaw.local" git -C "$git_repo" config user.name "OpenClaw Docker E2E" @@ -275,7 +275,7 @@ echo "Testing git plugin update from moving ref..." git_update_fixture_root="$(mktemp -d "$OPENCLAW_PLUGINS_TMP_DIR/openclaw-plugin-git-update.XXXXXX")" git_update_repo="$git_update_fixture_root/repo" git_update_repo_url="file://$git_update_repo" -write_fixture_plugin_with_cli "$git_update_repo" demo-plugin-git-update 0.0.1 demo.git.update.v1 "Demo Plugin Git Update" demo-git-update "demo-plugin-git-update:pong-v1" +node scripts/e2e/lib/fixture.mjs plugin-cli "$git_update_repo" demo-plugin-git-update 0.0.1 demo.git.update.v1 "Demo Plugin Git Update" demo-git-update "demo-plugin-git-update:pong-v1" git -C "$git_update_repo" init -q git -C "$git_update_repo" config user.email "docker-e2e@openclaw.local" git -C "$git_update_repo" config user.name "OpenClaw Docker E2E" @@ -285,7 +285,7 @@ git -C "$git_update_repo" commit -qm "test fixture v1" git_update_ref_v1="$(git -C "$git_update_repo" rev-parse HEAD)" run_plugins_fixture_logged install-git-update plugins install "git:$git_update_repo_url@main" --force -write_fixture_plugin_with_cli "$git_update_repo" demo-plugin-git-update 0.0.2 demo.git.update.v2 "Demo Plugin Git Update" demo-git-update "demo-plugin-git-update:pong-v2" +node scripts/e2e/lib/fixture.mjs plugin-cli "$git_update_repo" demo-plugin-git-update 0.0.2 demo.git.update.v2 "Demo Plugin Git Update" demo-git-update "demo-plugin-git-update:pong-v2" git -C "$git_update_repo" add -A git -C "$git_update_repo" commit -qm "test fixture v2" @@ -299,8 +299,8 @@ node scripts/e2e/lib/plugins/assertions.mjs plugin-git-updated "$git_update_ref_ echo "Testing Claude bundle enable and inspect flow..." bundle_plugin_id="claude-bundle-e2e" bundle_root="$OPENCLAW_PLUGIN_HOME/$bundle_plugin_id" -write_claude_bundle_fixture "$bundle_root" -record_fixture_plugin_trust "$bundle_plugin_id" "$bundle_root" 0 +node scripts/e2e/lib/fixture.mjs claude-bundle "$bundle_root" +node scripts/e2e/lib/plugins/assertions.mjs record-fixture-plugin-trust "$bundle_plugin_id" "$bundle_root" 0 run_plugins_openclaw_capture "$OPENCLAW_PLUGINS_TMP_DIR/plugins-bundle-disabled.json" plugins list --json node scripts/e2e/lib/plugins/assertions.mjs bundle-disabled @@ -311,7 +311,7 @@ node scripts/e2e/lib/plugins/assertions.mjs bundle-inspect echo "Testing plugin install visible after explicit restart..." slash_install_dir="$(mktemp -d "$OPENCLAW_PLUGINS_TMP_DIR/openclaw-plugin-slash-install.XXXXXX")" -write_fixture_plugin "$slash_install_dir" slash-install-plugin 0.0.1 demo.slash.install "Slash Install Plugin" +node scripts/e2e/lib/fixture.mjs plugin "$slash_install_dir" slash-install-plugin 0.0.1 demo.slash.install "Slash Install Plugin" run_plugins_fixture_logged install-slash-plugin plugins install "$slash_install_dir" --force run_plugins_openclaw_capture "$OPENCLAW_PLUGINS_TMP_DIR/plugin-command-install-show.json" plugins inspect slash-install-plugin --runtime --json diff --git a/scripts/e2e/live-plugin-tool-docker.sh b/scripts/e2e/live-plugin-tool-docker.sh index e207c4e29f31..9cec71881276 100755 --- a/scripts/e2e/live-plugin-tool-docker.sh +++ b/scripts/e2e/live-plugin-tool-docker.sh @@ -10,7 +10,6 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$ROOT_DIR/scripts/lib/openclaw-e2e-instance.sh" source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-live-plugin-tool-e2e" OPENCLAW_LIVE_PLUGIN_TOOL_E2E_IMAGE)" DOCKER_TARGET="${OPENCLAW_LIVE_PLUGIN_TOOL_DOCKER_TARGET:-bare}" @@ -23,15 +22,7 @@ AGENT_OUTPUT_DUMP_BYTES="$(openclaw_e2e_read_nonnegative_int_env OPENCLAW_LIVE_P SESSION_SCAN_MAX_ENTRIES="$(openclaw_e2e_read_positive_int_env OPENCLAW_LIVE_PLUGIN_TOOL_SESSION_SCAN_MAX_ENTRIES 50000)" run_log="" -cleanup() { - if [ -n "${PACKAGE_TGZ:-}" ]; then - docker_e2e_cleanup_package_tgz "$PACKAGE_TGZ" - fi - if [ -n "${run_log:-}" ]; then - rm -f "$run_log" - fi -} -trap cleanup EXIT +trap 'docker_e2e_cleanup_package_run "${PACKAGE_TGZ:-}" "${run_log:-}"' EXIT if [ ! -f "$PROFILE_FILE" ] && [ -f "$HOME/.profile" ]; then PROFILE_FILE="$HOME/.profile" @@ -39,19 +30,11 @@ fi docker_e2e_build_or_reuse "$IMAGE_NAME" live-plugin-tool "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "$DOCKER_TARGET" -prepare_package_tgz() { - if [ -n "$PACKAGE_TGZ" ]; then - PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz live-plugin-tool "$PACKAGE_TGZ")" - return 0 - fi - if [ "$HOST_BUILD" = "0" ] && [ -z "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}" ]; then - echo "OPENCLAW_LIVE_PLUGIN_TOOL_HOST_BUILD=0 requires OPENCLAW_CURRENT_PACKAGE_TGZ" >&2 - exit 1 - fi - PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz live-plugin-tool)" -} - -prepare_package_tgz +if [ -z "$PACKAGE_TGZ" ] && [ "$HOST_BUILD" = "0" ] && [ -z "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}" ]; then + echo "OPENCLAW_LIVE_PLUGIN_TOOL_HOST_BUILD=0 requires OPENCLAW_CURRENT_PACKAGE_TGZ" >&2 + exit 1 +fi +PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz live-plugin-tool "$PACKAGE_TGZ")" PROFILE_MOUNT=() PROFILE_STATUS="none" diff --git a/scripts/e2e/multi-node-update-docker.sh b/scripts/e2e/multi-node-update-docker.sh index b9468fc74960..7cbf48e5708b 100755 --- a/scripts/e2e/multi-node-update-docker.sh +++ b/scripts/e2e/multi-node-update-docker.sh @@ -21,7 +21,6 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-multi-node-update-e2e" OPENCLAW_MULTI_NODE_UPDATE_E2E_IMAGE)" SKIP_BUILD="${OPENCLAW_MULTI_NODE_UPDATE_E2E_SKIP_BUILD:-0}" @@ -31,10 +30,7 @@ ARTIFACT_DIR="${OPENCLAW_MULTI_NODE_ARTIFACT_DIR:-$ROOT_DIR/.artifacts/multi-nod mkdir -p "$ARTIFACT_DIR" chmod -R a+rwX "$ARTIFACT_DIR" || true -cleanup() { - docker_e2e_cleanup_package_tgz "${PACKAGE_TGZ:-}" -} -trap cleanup EXIT +trap 'docker_e2e_cleanup_package_tgz "${PACKAGE_TGZ:-}"' EXIT # Build the bare e2e image and prepare the package tarball. docker_e2e_build_or_reuse "$IMAGE_NAME" multi-node-update "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "bare" "$SKIP_BUILD" diff --git a/scripts/e2e/npm-onboard-channel-agent-docker.sh b/scripts/e2e/npm-onboard-channel-agent-docker.sh index 17e5b6e961de..ef8f0ec78fe8 100644 --- a/scripts/e2e/npm-onboard-channel-agent-docker.sh +++ b/scripts/e2e/npm-onboard-channel-agent-docker.sh @@ -9,7 +9,6 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" source "$ROOT_DIR/scripts/e2e/lib/prepublish-plugin-registry.sh" source "$ROOT_DIR/scripts/lib/frozen-target-compat.sh" @@ -40,15 +39,7 @@ STATUS_TEXT_MAX_BYTES="$( )" run_log="" -cleanup() { - if [ -n "${PACKAGE_TGZ:-}" ]; then - docker_e2e_cleanup_package_tgz "$PACKAGE_TGZ" - fi - if [ -n "${run_log:-}" ]; then - rm -f "$run_log" - fi -} -trap cleanup EXIT +trap 'docker_e2e_cleanup_package_run "${PACKAGE_TGZ:-}" "${run_log:-}"' EXIT case "$CHANNEL" in telegram | discord | slack) ;; @@ -60,19 +51,11 @@ esac docker_e2e_build_or_reuse "$IMAGE_NAME" npm-onboard-channel-agent "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "$DOCKER_TARGET" -prepare_package_tgz() { - if [ -n "$PACKAGE_TGZ" ]; then - PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz npm-onboard-channel-agent "$PACKAGE_TGZ")" - return 0 - fi - if [ "$HOST_BUILD" = "0" ] && [ -z "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}" ]; then - echo "OPENCLAW_NPM_ONBOARD_HOST_BUILD=0 requires OPENCLAW_CURRENT_PACKAGE_TGZ" >&2 - exit 1 - fi - PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz npm-onboard-channel-agent)" -} - -prepare_package_tgz +if [ -z "$PACKAGE_TGZ" ] && [ "$HOST_BUILD" = "0" ] && [ -z "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}" ]; then + echo "OPENCLAW_NPM_ONBOARD_HOST_BUILD=0 requires OPENCLAW_CURRENT_PACKAGE_TGZ" >&2 + exit 1 +fi +PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz npm-onboard-channel-agent "$PACKAGE_TGZ")" docker_e2e_package_mount_args "$PACKAGE_TGZ" run_log="$(docker_e2e_run_log npm-onboard-channel-agent)" diff --git a/scripts/e2e/parallels/macos-smoke.ts b/scripts/e2e/parallels/macos-smoke.ts index eb5037b93aed..90e571b953a5 100755 --- a/scripts/e2e/parallels/macos-smoke.ts +++ b/scripts/e2e/parallels/macos-smoke.ts @@ -362,26 +362,7 @@ class MacosSmoke extends SmokeRunController { }), ); await this.phases.phase("fresh.onboard-ref", 420, () => this.runRefOnboard()); - await this.phases.phase("fresh.gateway-start", 180, () => this.startManualGatewayIfNeeded()); - await this.phases.phase("fresh.gateway-status", 180, () => this.verifyGateway()); - this.status.freshGateway = "pass"; - await this.phases.phase("fresh.dashboard-load", 180, () => this.verifyDashboardLoad()); - this.status.freshDashboard = "pass"; - await this.phases.phase("fresh.first-agent-turn", this.agentTimeoutSeconds, () => - this.verifyTurn(), - ); - this.status.freshAgent = "pass"; - if (this.discordEnabled()) { - this.status.freshDiscord = "fail"; - await this.phases.phase("fresh.discord-config", 600, () => this.discord?.configure()); - await this.phases.phase("fresh.discord-gateway-ready", 180, () => - this.ensureDiscordGatewayReady(), - ); - await this.phases.phase("fresh.discord-roundtrip", 180, () => - this.runDiscordRoundtrip("fresh"), - ); - this.status.freshDiscord = "pass"; - } + await this.runGatewaySmoke("fresh"); } protected async runUpgradeLane(): Promise { @@ -422,25 +403,29 @@ class MacosSmoke extends SmokeRunController { ); } await this.phases.phase("upgrade.onboard-ref", 420, () => this.runRefOnboard()); - await this.phases.phase("upgrade.gateway-start", 180, () => this.startManualGatewayIfNeeded()); - await this.phases.phase("upgrade.gateway-status", 180, () => this.verifyGateway()); - this.status.upgradeGateway = "pass"; - await this.phases.phase("upgrade.dashboard-load", 180, () => this.verifyDashboardLoad()); - this.status.upgradeDashboard = "pass"; - await this.phases.phase("upgrade.first-agent-turn", this.agentTimeoutSeconds, () => + await this.runGatewaySmoke("upgrade"); + } + + private async runGatewaySmoke(lane: "fresh" | "upgrade"): Promise { + await this.phases.phase(`${lane}.gateway-start`, 180, () => this.startManualGatewayIfNeeded()); + await this.phases.phase(`${lane}.gateway-status`, 180, () => this.verifyGateway()); + this.status[`${lane}Gateway`] = "pass"; + await this.phases.phase(`${lane}.dashboard-load`, 180, () => this.verifyDashboardLoad()); + this.status[`${lane}Dashboard`] = "pass"; + await this.phases.phase(`${lane}.first-agent-turn`, this.agentTimeoutSeconds, () => this.verifyTurn(), ); - this.status.upgradeAgent = "pass"; + this.status[`${lane}Agent`] = "pass"; if (this.discordEnabled()) { - this.status.upgradeDiscord = "fail"; - await this.phases.phase("upgrade.discord-config", 600, () => this.discord?.configure()); - await this.phases.phase("upgrade.discord-gateway-ready", 180, () => + this.status[`${lane}Discord`] = "fail"; + await this.phases.phase(`${lane}.discord-config`, 600, () => this.discord?.configure()); + await this.phases.phase(`${lane}.discord-gateway-ready`, 180, () => this.ensureDiscordGatewayReady(), ); - await this.phases.phase("upgrade.discord-roundtrip", 180, () => - this.runDiscordRoundtrip("upgrade"), + await this.phases.phase(`${lane}.discord-roundtrip`, 180, () => + this.runDiscordRoundtrip(lane), ); - this.status.upgradeDiscord = "pass"; + this.status[`${lane}Discord`] = "pass"; } } @@ -461,15 +446,7 @@ exec node "$entry" ${argv}`, const prlctlDeadline = Date.now() + 45_000; const deadline = Date.now() + timeoutSeconds * 1000; while (Date.now() < prlctlDeadline && Date.now() < deadline) { - const result = run("prlctl", ["exec", this.options.vmName, "--current-user", "whoami"], { - check: false, - quiet: true, - timeoutMs: this.phases.remainingTimeoutMs(), - }); - const user = result.stdout.trim().replaceAll("\r", "").split("\n").at(-1) ?? ""; - if (result.status === 0 && /^[A-Za-z0-9._-]+$/.test(user)) { - this.guestUser = user; - this.guestTransport = "current-user"; + if (this.tryCurrentUser()) { return; } run("sleep", ["2"], { quiet: true }); @@ -484,15 +461,7 @@ exec node "$entry" ${argv}`, return; } while (Date.now() < deadline) { - const result = run("prlctl", ["exec", this.options.vmName, "--current-user", "whoami"], { - check: false, - quiet: true, - timeoutMs: this.phases.remainingTimeoutMs(), - }); - const user = result.stdout.trim().replaceAll("\r", "").split("\n").at(-1) ?? ""; - if (result.status === 0 && /^[A-Za-z0-9._-]+$/.test(user)) { - this.guestUser = user; - this.guestTransport = "current-user"; + if (this.tryCurrentUser()) { return; } run("sleep", ["2"], { quiet: true }); @@ -500,6 +469,21 @@ exec node "$entry" ${argv}`, throw new Error("guest current user did not become available"); } + private tryCurrentUser(): boolean { + const result = run("prlctl", ["exec", this.options.vmName, "--current-user", "whoami"], { + check: false, + quiet: true, + timeoutMs: this.phases.remainingTimeoutMs(), + }); + const user = result.stdout.trim().replaceAll("\r", "").split("\n").at(-1) ?? ""; + if (result.status !== 0 || !/^[A-Za-z0-9._-]+$/.test(user)) { + return false; + } + this.guestUser = user; + this.guestTransport = "current-user"; + return true; + } + private resolveDesktopUser(): string { return resolveMacosDesktopUser((args) => this.readDesktopUserOutput(args)); } diff --git a/scripts/e2e/parallels/windows-smoke.ts b/scripts/e2e/parallels/windows-smoke.ts index 1e39d5b8a244..731729bcd2d0 100755 --- a/scripts/e2e/parallels/windows-smoke.ts +++ b/scripts/e2e/parallels/windows-smoke.ts @@ -280,17 +280,7 @@ class WindowsSmoke extends SmokeRunController { ), }), ); - await this.phases.phase("fresh.onboard-ref", 720, () => this.runRefOnboard()); - await this.phases.phase("fresh.gateway-restart", 420, () => this.gatewayAction("restart")); - await this.phases.phase("fresh.gateway-status", 420, () => this.verifyGatewayReachable()); - this.status.freshGateway = "pass"; - await this.phases.phase("fresh.gateway-stop-before-local-agent", 420, () => - this.gatewayAction("stop"), - ); - await this.phases.phase("fresh.first-agent-turn", this.agentTimeoutSeconds, () => - this.verifyTurn(), - ); - this.status.freshAgent = "pass"; + await this.runGatewaySmoke("fresh"); } protected async runUpgradeLane(): Promise { @@ -345,17 +335,21 @@ class WindowsSmoke extends SmokeRunController { this.status.upgradeVersion = await this.extractLastVersion("upgrade.update-dev"); await this.phases.phase("upgrade.verify-dev-channel", 120, () => this.verifyDevChannelUpdate()); await this.phases.phase("upgrade.gateway-stop", 420, () => this.gatewayAction("stop")); - await this.phases.phase("upgrade.onboard-ref", 720, () => this.runRefOnboard()); - await this.phases.phase("upgrade.gateway-restart", 420, () => this.gatewayAction("restart")); - await this.phases.phase("upgrade.gateway-status", 420, () => this.verifyGatewayReachable()); - this.status.upgradeGateway = "pass"; - await this.phases.phase("upgrade.gateway-stop-before-local-agent", 420, () => + await this.runGatewaySmoke("upgrade"); + } + + private async runGatewaySmoke(lane: "fresh" | "upgrade"): Promise { + await this.phases.phase(`${lane}.onboard-ref`, 720, () => this.runRefOnboard()); + await this.phases.phase(`${lane}.gateway-restart`, 420, () => this.gatewayAction("restart")); + await this.phases.phase(`${lane}.gateway-status`, 420, () => this.verifyGatewayReachable()); + this.status[`${lane}Gateway`] = "pass"; + await this.phases.phase(`${lane}.gateway-stop-before-local-agent`, 420, () => this.gatewayAction("stop"), ); - await this.phases.phase("upgrade.first-agent-turn", this.agentTimeoutSeconds, () => + await this.phases.phase(`${lane}.first-agent-turn`, this.agentTimeoutSeconds, () => this.verifyTurn(), ); - this.status.upgradeAgent = "pass"; + this.status[`${lane}Agent`] = "pass"; } private guestPowerShell( diff --git a/scripts/e2e/plugin-lifecycle-matrix-docker.sh b/scripts/e2e/plugin-lifecycle-matrix-docker.sh index 9a5acb0b91d6..4ac358411c31 100755 --- a/scripts/e2e/plugin-lifecycle-matrix-docker.sh +++ b/scripts/e2e/plugin-lifecycle-matrix-docker.sh @@ -8,7 +8,6 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-plugin-lifecycle-matrix-e2e" OPENCLAW_PLUGIN_LIFECYCLE_MATRIX_E2E_IMAGE)" SKIP_BUILD="${OPENCLAW_PLUGIN_LIFECYCLE_MATRIX_E2E_SKIP_BUILD:-0}" diff --git a/scripts/e2e/plugin-update-unchanged-docker.sh b/scripts/e2e/plugin-update-unchanged-docker.sh index 3eea8e3d631e..f0aeb69b7192 100755 --- a/scripts/e2e/plugin-update-unchanged-docker.sh +++ b/scripts/e2e/plugin-update-unchanged-docker.sh @@ -9,14 +9,10 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-plugin-update-e2e" OPENCLAW_PLUGIN_UPDATE_E2E_IMAGE)" SKIP_BUILD="${OPENCLAW_PLUGIN_UPDATE_E2E_SKIP_BUILD:-0}" -cleanup() { - docker_e2e_cleanup_package_tgz "${PACKAGE_TGZ:-}" -} -trap cleanup EXIT +trap 'docker_e2e_cleanup_package_tgz "${PACKAGE_TGZ:-}"' EXIT PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz plugin-update "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")" # Bare lanes mount the package artifact instead of baking app sources into the image. diff --git a/scripts/e2e/published-driver-update-docker.sh b/scripts/e2e/published-driver-update-docker.sh index 742c85d614d1..2819b65286ba 100644 --- a/scripts/e2e/published-driver-update-docker.sh +++ b/scripts/e2e/published-driver-update-docker.sh @@ -15,7 +15,6 @@ export CELL_DEADLINE_EPOCH_SECONDS ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz published-driver-update "${1:-${OPENCLAW_CURRENT_PACKAGE_TGZ:-}}")" RUNTIME_VOLUME="" diff --git a/scripts/e2e/release-media-memory-docker.sh b/scripts/e2e/release-media-memory-docker.sh index 76aaa04dc542..bdc0f549809f 100755 --- a/scripts/e2e/release-media-memory-docker.sh +++ b/scripts/e2e/release-media-memory-docker.sh @@ -8,7 +8,6 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-release-media-memory-e2e" OPENCLAW_RELEASE_MEDIA_MEMORY_E2E_IMAGE)" SKIP_BUILD="${OPENCLAW_RELEASE_MEDIA_MEMORY_E2E_SKIP_BUILD:-0}" diff --git a/scripts/e2e/release-plugin-marketplace-docker.sh b/scripts/e2e/release-plugin-marketplace-docker.sh index 8838363418fd..5dff68b1d4d1 100755 --- a/scripts/e2e/release-plugin-marketplace-docker.sh +++ b/scripts/e2e/release-plugin-marketplace-docker.sh @@ -8,7 +8,6 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-release-plugin-marketplace-e2e" OPENCLAW_RELEASE_PLUGIN_MARKETPLACE_E2E_IMAGE)" SKIP_BUILD="${OPENCLAW_RELEASE_PLUGIN_MARKETPLACE_E2E_SKIP_BUILD:-0}" diff --git a/scripts/e2e/release-typed-onboarding-docker.sh b/scripts/e2e/release-typed-onboarding-docker.sh index 8d25b067143a..3ee92dbf2ba0 100755 --- a/scripts/e2e/release-typed-onboarding-docker.sh +++ b/scripts/e2e/release-typed-onboarding-docker.sh @@ -9,7 +9,6 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" TARGET_ROOT_DIR="$(cd "${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$ROOT_DIR}" && pwd)" source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" source "$ROOT_DIR/scripts/e2e/lib/prepublish-plugin-registry.sh" source "$ROOT_DIR/scripts/lib/frozen-target-compat.sh" diff --git a/scripts/e2e/release-upgrade-user-journey-docker.sh b/scripts/e2e/release-upgrade-user-journey-docker.sh index 754b6b6c1c70..b0602830cf3b 100755 --- a/scripts/e2e/release-upgrade-user-journey-docker.sh +++ b/scripts/e2e/release-upgrade-user-journey-docker.sh @@ -8,7 +8,6 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-release-upgrade-user-journey-e2e" OPENCLAW_RELEASE_UPGRADE_USER_JOURNEY_E2E_IMAGE)" SKIP_BUILD="${OPENCLAW_RELEASE_UPGRADE_USER_JOURNEY_E2E_SKIP_BUILD:-0}" diff --git a/scripts/e2e/release-user-journey-docker.sh b/scripts/e2e/release-user-journey-docker.sh index 5ebfdecf33ed..64293a59bff4 100755 --- a/scripts/e2e/release-user-journey-docker.sh +++ b/scripts/e2e/release-user-journey-docker.sh @@ -9,7 +9,6 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-release-user-journey-e2e" OPENCLAW_RELEASE_USER_JOURNEY_E2E_IMAGE)" SKIP_BUILD="${OPENCLAW_RELEASE_USER_JOURNEY_E2E_SKIP_BUILD:-0}" diff --git a/scripts/e2e/skill-install-docker.sh b/scripts/e2e/skill-install-docker.sh index ee0500775572..5c111b7caf60 100755 --- a/scripts/e2e/skill-install-docker.sh +++ b/scripts/e2e/skill-install-docker.sh @@ -9,13 +9,9 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-skill-install-e2e" OPENCLAW_SKILL_INSTALL_E2E_IMAGE)" -cleanup() { - docker_e2e_cleanup_package_tgz "${PACKAGE_TGZ:-}" -} -trap cleanup EXIT +trap 'docker_e2e_cleanup_package_tgz "${PACKAGE_TGZ:-}"' EXIT PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz skill-install "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")" OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 skill-install empty)" diff --git a/scripts/e2e/update-channel-switch-docker.sh b/scripts/e2e/update-channel-switch-docker.sh index f71562ebb490..4eb772c6428d 100755 --- a/scripts/e2e/update-channel-switch-docker.sh +++ b/scripts/e2e/update-channel-switch-docker.sh @@ -9,14 +9,10 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-update-channel-switch-e2e" OPENCLAW_UPDATE_CHANNEL_SWITCH_E2E_IMAGE)" SKIP_BUILD="${OPENCLAW_UPDATE_CHANNEL_SWITCH_E2E_SKIP_BUILD:-0}" -cleanup() { - docker_e2e_cleanup_package_tgz "${PACKAGE_TGZ:-}" -} -trap cleanup EXIT +trap 'docker_e2e_cleanup_package_tgz "${PACKAGE_TGZ:-}"' EXIT PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz update-channel-switch "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")" # Bare lanes mount the package artifact instead of baking app sources into the image. diff --git a/scripts/e2e/update-corrupt-plugin-docker.sh b/scripts/e2e/update-corrupt-plugin-docker.sh index 95a12fbaac58..6661faff2b99 100644 --- a/scripts/e2e/update-corrupt-plugin-docker.sh +++ b/scripts/e2e/update-corrupt-plugin-docker.sh @@ -10,7 +10,6 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" source "$ROOT_DIR/scripts/lib/frozen-target-compat.sh" TARGET_ROOT_DIR="$(cd "${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$ROOT_DIR}" && pwd)" @@ -20,10 +19,7 @@ CORRUPT_UPDATE_SCENARIO="$(openclaw_resolve_frozen_target_file "$TARGET_ROOT_DIR IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-update-corrupt-plugin-e2e" OPENCLAW_UPDATE_CORRUPT_PLUGIN_E2E_IMAGE)" SKIP_BUILD="${OPENCLAW_UPDATE_CORRUPT_PLUGIN_E2E_SKIP_BUILD:-0}" -cleanup() { - docker_e2e_cleanup_package_tgz "${PACKAGE_TGZ:-}" -} -trap cleanup EXIT +trap 'docker_e2e_cleanup_package_tgz "${PACKAGE_TGZ:-}"' EXIT PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz update-corrupt-plugin "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")" # Bare lanes mount the package artifact instead of baking app sources into the image. diff --git a/scripts/e2e/update-first-hop-compat-docker.sh b/scripts/e2e/update-first-hop-compat-docker.sh index fbd85e956321..2567d4dbe45a 100755 --- a/scripts/e2e/update-first-hop-compat-docker.sh +++ b/scripts/e2e/update-first-hop-compat-docker.sh @@ -8,7 +8,6 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" if [ "${OPENCLAW_QA_ALLOW_UPDATE_FIRST_HOP:-0}" != "1" ]; then echo "blocked destructive package self-update; set OPENCLAW_QA_ALLOW_UPDATE_FIRST_HOP=1 to run" >&2 diff --git a/scripts/embedded-run-abort-leak.ts b/scripts/embedded-run-abort-leak.ts index c6c8eef9e295..dd5667039a2b 100644 --- a/scripts/embedded-run-abort-leak.ts +++ b/scripts/embedded-run-abort-leak.ts @@ -18,9 +18,10 @@ */ import * as fs from "node:fs"; import * as path from "node:path"; +import { setImmediate, setTimeout } from "node:timers/promises"; import * as v8 from "node:v8"; import { expectDefined } from "../packages/normalization-core/src/expect.js"; -import { toErrorObject as toLintErrorObject } from "./lib/error-format.mts"; +import { coerceErrorMessage, toErrorObject as toLintErrorObject } from "./lib/error-format.mts"; import { parseNonNegativeInt, parsePositiveInt } from "./lib/numeric-options.mjs"; type Mode = "production" | "closure-extracted" | "closure-inline" | "synthetic-leak"; @@ -235,14 +236,10 @@ function runOnce(mode: Mode, scopeBytes: number, iter: number): void { async function settleAndGc(): Promise { for (let i = 0; i < 4; i += 1) { - await new Promise((r) => { - setImmediate(r); - }); + await setImmediate(); globalThis.gc?.(); } - await new Promise((r) => { - setTimeout(r, 100); - }); + await setTimeout(100); globalThis.gc?.(); } @@ -276,7 +273,7 @@ async function main(): Promise { try { opts = parseArgs(process.argv.slice(2)); } catch (error) { - fail(error instanceof Error ? error.message : String(error)); + fail(coerceErrorMessage(error)); } if (opts.mode === "production") { productionAbortable = (await import("../src/agents/embedded-agent-runner/run/abortable.js")) diff --git a/scripts/generate-base-config-schema.ts b/scripts/generate-base-config-schema.ts index c4a81dba9310..d2fa7fb9bff5 100644 --- a/scripts/generate-base-config-schema.ts +++ b/scripts/generate-base-config-schema.ts @@ -3,19 +3,15 @@ import { pathToFileURL } from "node:url"; import { computeBaseConfigSchemaResponse } from "../src/config/schema-base.js"; -function checkBaseConfigSchema(): void { - computeBaseConfigSchemaResponse({ - generatedAt: "2026-05-05T00:00:00.000Z", - }); -} - const args = new Set(process.argv.slice(2)); if (args.has("--check") && args.has("--write")) { throw new Error("Use either --check or --write, not both."); } if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { - checkBaseConfigSchema(); + computeBaseConfigSchemaResponse({ + generatedAt: "2026-05-05T00:00:00.000Z", + }); if (args.has("--write")) { console.log("[base-config-schema] runtime-computed; no generated file to write"); } else { diff --git a/scripts/github/real-behavior-proof-policy.mjs b/scripts/github/real-behavior-proof-policy.mjs index 065a58e7308b..d428b79b569d 100644 --- a/scripts/github/real-behavior-proof-policy.mjs +++ b/scripts/github/real-behavior-proof-policy.mjs @@ -1,5 +1,8 @@ // Shared PR context and evidence policy for GitHub checks and label decisions. -import { readBoundedResponseText } from "../lib/bounded-response.mjs"; +import { + createBoundedResponseTooLargeError, + readBoundedResponseText, +} from "../lib/bounded-response.mjs"; import { escapeRegExp } from "../lib/regexp.mjs"; import { createTimeoutError } from "../lib/timeout-error.mjs"; @@ -62,12 +65,6 @@ const legacyProofFieldNames = [ const missingValueRegex = /^(?:n\/?a|none|not applicable|tbd|todo|unknown|unsure|none provided|no evidence|not tested|untested|did not test|didn't test|could not test|couldn't test|-|(?:-{3,}|\*{3,}|_{3,})|\[[^\]]*\])\.?$/i; -function createTooLargeGitHubApiBodyError(label, maxBytes) { - const error = new Error(`${label} response body exceeded ${maxBytes} bytes`); - error.code = "ETOOBIG"; - return error; -} - async function withGitHubApiTimeout(label, timeoutMs, run) { const boundedTimeoutMs = Math.max(1, timeoutMs); const controller = new AbortController(); @@ -105,7 +102,8 @@ export async function readBoundedGitHubApiJson( ) { const text = await readBoundedResponseText(response, label, maxBytes, { ...options, - createTooLargeError: () => createTooLargeGitHubApiBodyError(label, maxBytes), + createTooLargeError: () => + createBoundedResponseTooLargeError(`${label} response body exceeded ${maxBytes} bytes`), }); return JSON.parse(text); } diff --git a/scripts/ios-validate-app-store-ipa.sh b/scripts/ios-validate-app-store-ipa.sh index 691f6e6d01c4..8250662f42f3 100755 --- a/scripts/ios-validate-app-store-ipa.sh +++ b/scripts/ios-validate-app-store-ipa.sh @@ -149,18 +149,6 @@ assert_plist_nonempty_string() { fi } -assert_plist_true() { - local plist="$1" - local key_path="$2" - local label="$3" - local actual - actual="$(plist_value "${plist}" "${key_path}")" - if [[ "${actual}" != "true" ]]; then - echo "Invalid IPA: ${label}; expected true, got ${actual:-missing}." >&2 - exit 1 - fi -} - assert_plist_key_absent() { local plist="$1" local key_path="$2" @@ -258,7 +246,7 @@ assert_plist_string "${entitlements_plist}" "application-identifier" "${EXPECTED assert_plist_string "${entitlements_plist}" "com.apple.developer.team-identifier" "${EXPECTED_TEAM_ID}" "signed team identifier mismatch" assert_plist_string "${entitlements_plist}" "aps-environment" "production" "signed APNs entitlement mismatch" assert_plist_string "${entitlements_plist}" "com.apple.developer.devicecheck.appattest-environment" "production" "signed App Attest entitlement mismatch" -assert_plist_true "${entitlements_plist}" "com.apple.developer.healthkit" "signed HealthKit entitlement mismatch" +assert_plist_string "${entitlements_plist}" "com.apple.developer.healthkit" "true" "signed HealthKit entitlement mismatch" assert_plist_array_contains "${entitlements_plist}" "com.apple.security.application-groups" "${EXPECTED_APP_GROUP}" "signed App Group entitlement mismatch" if ! "${SECURITY_BIN}" cms -D -i "${embedded_profile}" >"${profile_plist}" 2>"${tmp_dir}/security.err"; then @@ -272,7 +260,7 @@ assert_plist_array_contains "${profile_plist}" "TeamIdentifier" "${EXPECTED_TEAM assert_plist_string "${profile_plist}" "Entitlements:application-identifier" "${EXPECTED_TEAM_ID}.${EXPECTED_BUNDLE_ID}" "embedded profile application identifier mismatch" assert_plist_string "${profile_plist}" "Entitlements:aps-environment" "production" "embedded profile APNs entitlement mismatch" assert_plist_array_contains "${profile_plist}" "Entitlements:com.apple.developer.devicecheck.appattest-environment" "production" "embedded profile App Attest entitlement mismatch" -assert_plist_true "${profile_plist}" "Entitlements:com.apple.developer.healthkit" "embedded profile HealthKit entitlement mismatch" +assert_plist_string "${profile_plist}" "Entitlements:com.apple.developer.healthkit" "true" "embedded profile HealthKit entitlement mismatch" assert_plist_array_contains "${profile_plist}" "Entitlements:com.apple.security.application-groups" "${EXPECTED_APP_GROUP}" "embedded profile App Group entitlement mismatch" echo "Validated iOS App Store IPA: ${IPA_PATH}" diff --git a/scripts/lib/bounded-output-tail.mjs b/scripts/lib/bounded-output-tail.mjs index daab8c7865f8..7d4f9cc6f0c9 100644 --- a/scripts/lib/bounded-output-tail.mjs +++ b/scripts/lib/bounded-output-tail.mjs @@ -16,3 +16,11 @@ export function appendBoundedTail(state, chunk, maxChars) { truncatedChars: state.truncatedChars + droppedChars, }; } + +/** @param {BoundedTail} state */ +export function formatBoundedTail(state) { + if (state.truncatedChars === 0) { + return state.text; + } + return `[output truncated ${state.truncatedChars} chars; showing tail]\n${state.text}`; +} diff --git a/scripts/lib/code-mode-matrix-comparison.ts b/scripts/lib/code-mode-matrix-comparison.ts index fab79aef9365..981da7b9515a 100644 --- a/scripts/lib/code-mode-matrix-comparison.ts +++ b/scripts/lib/code-mode-matrix-comparison.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import { isDeepStrictEqual } from "node:util"; import { z } from "zod"; import type { CodeModeMatrixCellResult } from "../code-mode-model-matrix.ts"; +import { groupBy } from "./group-by.mts"; const comparableResult = z .object({ @@ -422,10 +423,9 @@ export function compareCodeModeMatrixModes(values: readonly unknown[]) { } } const grouped = (includeTask: boolean) => { - const subsets = new Map(); - for (const row of rows) { + const subsets = groupBy(rows, (row) => { const settings = row.workload!.settings; - const identity = canonicalJson({ + return canonicalJson({ model: row.model, ...(includeTask ? { task: row.task, settings } @@ -434,10 +434,7 @@ export function compareCodeModeMatrixModes(values: readonly unknown[]) { buildSha256: row.buildSha256, sourcePatchSha256: row.sourcePatchSha256, }); - const subset = subsets.get(identity) ?? []; - subset.push(row); - subsets.set(identity, subset); - } + }); return [...subsets] .toSorted(([a], [b]) => a.localeCompare(b)) .map(([identity, subset]) => { diff --git a/scripts/lib/config-boundary-guard.mts b/scripts/lib/config-boundary-guard.mts index 32f287055a13..9111ce528fe1 100644 --- a/scripts/lib/config-boundary-guard.mts +++ b/scripts/lib/config-boundary-guard.mts @@ -89,7 +89,6 @@ function isProductionExtensionFile(relPath: string) { if ( relPath.includes("/test-support/") || relPath.includes(".test.") || - relPath.includes(".live.test.") || relPath.includes(".test-d.") || relPath.includes(".test-harness.") || relPath.includes(".test-shared.") || @@ -105,17 +104,13 @@ function isProductionExtensionFile(relPath: string) { function isTestOrHarnessFile(relPath: string) { return ( relPath.includes("test-support") || - relPath.includes("/test-support/") || relPath.includes("/test-helpers/") || relPath.includes(".test.") || - relPath.includes(".live.test.") || relPath.includes(".test-d.") || relPath.includes(".test-harness.") || relPath.includes(".test-shared.") || relPath.endsWith(".test-helpers.ts") || - relPath.endsWith(".test-support.ts") || - relPath.endsWith("-test-helpers.ts") || - relPath.endsWith("-test-support.ts") + relPath.endsWith("-test-helpers.ts") ); } diff --git a/scripts/lib/cross-os-release-checks/config.ts b/scripts/lib/cross-os-release-checks/config.ts index dd49d77ab284..e6209fb92a8c 100644 --- a/scripts/lib/cross-os-release-checks/config.ts +++ b/scripts/lib/cross-os-release-checks/config.ts @@ -511,8 +511,7 @@ export function looksLikeCommitSha(ref: string) { } export function resolveExpectedDevUpdateRef(ref?: string) { - const trimmed = normalizeRequestedRef(ref) || "main"; - return trimmed || "main"; + return normalizeRequestedRef(ref) || "main"; } export function resolveDevUpdateVerificationRef(ref: string, sourceSha?: string) { @@ -540,13 +539,8 @@ export function buildRealUpdateEnv(env: NodeJS.ProcessEnv) { return updateEnv; } -function isExtendedStableBaselineVersion(baselineVersion: string | undefined) { - const parsed = baselineVersion ? parseReleaseVersion(baselineVersion) : null; - return parsed !== null && classifyReleaseTrain(parsed) === "extended-stable"; -} - -function isExtendedStableCandidateVersion(candidateVersion: string | undefined) { - const parsed = candidateVersion ? parseReleaseVersion(candidateVersion) : null; +function isExtendedStableVersion(version: string | undefined) { + const parsed = version ? parseReleaseVersion(version) : null; return parsed !== null && classifyReleaseTrain(parsed) === "extended-stable"; } @@ -554,10 +548,7 @@ function usesExtendedStableRegistryRoute( baselineVersion: string | undefined, candidateVersion: string | undefined, ) { - return ( - isExtendedStableBaselineVersion(baselineVersion) && - isExtendedStableCandidateVersion(candidateVersion) - ); + return isExtendedStableVersion(baselineVersion) && isExtendedStableVersion(candidateVersion); } function buildPackagedUpgradeUpdateEnv( @@ -576,10 +567,7 @@ function buildPackagedUpgradeUpdateEnv( return updateEnv; } -export function verifyPackagedUpgradeUpdateResult( - result: CommandResult, - _options?: { candidateVersion?: string }, -) { +export function verifyPackagedUpgradeUpdateResult(result: CommandResult) { if (result.exitCode === 0) { return; } diff --git a/scripts/lib/cross-os-release-checks/lanes.ts b/scripts/lib/cross-os-release-checks/lanes.ts index 3b1372f697e4..efe2abaad8e2 100644 --- a/scripts/lib/cross-os-release-checks/lanes.ts +++ b/scripts/lib/cross-os-release-checks/lanes.ts @@ -95,7 +95,6 @@ import { formatError, trimForSummary } from "./shared.ts"; export async function runFreshLane(params: LaneBaseParams & { build: CandidateBuild }) { const lane = createLaneState("fresh"); const cleanup: Cleanup[] = []; - const gatewayHolder: { current: GatewayHandle | null } = { current: null }; try { const env = buildLaneEnv(lane, params.providerConfig, params.providerSecretValue); await runTimedLanePhase(lane, "install-candidate", async () => { @@ -134,85 +133,34 @@ export async function runFreshLane(params: LaneBaseParams & { build: CandidateBu await installLaneCompanions({ ...params, lane, env }); - // Own the configured port through setup; release only when the gateway can claim it. - const gatewayPortReservation = await reserveGatewayPortForLane(lane); - cleanup.push(() => gatewayPortReservation.release()); - await runTimedLanePhase(lane, "onboard", async () => { - await runOnboard({ - lane, - env, - providerConfig: params.providerConfig, - logPath: join(params.logsDir, "fresh-onboard.log"), - }); - }); - - await runTimedLanePhase(lane, "models-set", async () => { - await runModelsSet({ - lane, - env, - providerConfig: params.providerConfig, - logPath: join(params.logsDir, "fresh-models-set.log"), - }); - }); - const authoredConfigPath = join(lane.stateDir, "openclaw.json"); const nestedPluginPath = "~/.openclaw/wiki"; - await runTimedLanePhase(lane, "seed-nested-plugin-path", async () => { - const config = JSON.parse(readFileSync(authoredConfigPath, "utf8")); - config.plugins ??= {}; - config.plugins.entries ??= {}; - // A disabled entry exercises generic path expansion without changing provider setup. - config.plugins.entries.wiki = { - enabled: false, - config: { store: { path: nestedPluginPath } }, - }; - writeFileSync(authoredConfigPath, `${JSON.stringify(config, null, 2)}\n`, "utf8"); + const agent = await runPackagedGatewaySmoke({ + ...params, + lane, + env, + cleanup, + beforeGatewayStart: () => + runTimedLanePhase(lane, "seed-nested-plugin-path", async () => { + const config = JSON.parse(readFileSync(authoredConfigPath, "utf8")); + config.plugins ??= {}; + config.plugins.entries ??= {}; + // A disabled entry exercises generic path expansion without changing provider setup. + config.plugins.entries.wiki = { + enabled: false, + config: { store: { path: nestedPluginPath } }, + }; + writeFileSync(authoredConfigPath, `${JSON.stringify(config, null, 2)}\n`, "utf8"); + }), + afterGatewayReady: () => + runTimedLanePhase(lane, "verify-nested-plugin-path", async () => { + const config = JSON.parse(readFileSync(authoredConfigPath, "utf8")); + if (config.plugins?.entries?.wiki?.config?.store?.path !== nestedPluginPath) { + throw new Error("Fresh Gateway startup changed the authored nested plugin path."); + } + }), }); - const gateway = await runTimedLanePhase(lane, "start-gateway", async () => { - await gatewayPortReservation.release(); - return startGateway({ - lane, - env, - logPath: join(params.logsDir, "fresh-gateway.log"), - }); - }); - gatewayHolder.current = gateway; - cleanup.push(() => stopGateway(gatewayHolder.current)); - - await runTimedLanePhase(lane, "wait-gateway", async () => { - await waitForGateway({ - lane, - env, - gatewayHolder, - gatewayLogPath: join(params.logsDir, "fresh-gateway.log"), - logPath: join(params.logsDir, "fresh-gateway-status.log"), - }); - }); - - await runTimedLanePhase(lane, "verify-nested-plugin-path", async () => { - const config = JSON.parse(readFileSync(authoredConfigPath, "utf8")); - if (config.plugins?.entries?.wiki?.config?.store?.path !== nestedPluginPath) { - throw new Error("Fresh Gateway startup changed the authored nested plugin path."); - } - }); - - await runTimedLanePhase(lane, "dashboard", async () => { - await runDashboardSmoke({ - lane, - logPath: join(params.logsDir, "fresh-dashboard.log"), - }); - }); - - const agent = await runTimedLanePhase(lane, "agent-turn", async () => - runAgentTurn({ - lane, - env, - label: "fresh", - logPath: join(params.logsDir, "fresh-agent.log"), - }), - ); - return { status: "pass", installedVersion: installed.version, @@ -244,7 +192,6 @@ export async function runUpgradeLane( } const lane = createLaneState("upgrade"); const cleanup: Cleanup[] = []; - const gatewayHolder: { current: GatewayHandle | null } = { current: null }; const result: LaneResult = { status: "pending", phaseTimings: lane.phaseTimings, @@ -409,9 +356,7 @@ export async function runUpgradeLane( ); }); } else { - verifyPackagedUpgradeUpdateResult(updateResult, { - candidateVersion: params.build.candidateVersion, - }); + verifyPackagedUpgradeUpdateResult(updateResult); } if ( @@ -442,64 +387,7 @@ export async function runUpgradeLane( verifyInstalledCandidate(installed, params.build); await installLaneCompanions({ ...params, lane, env }); - - // Own the configured port through setup; release only when the gateway can claim it. - const gatewayPortReservation = await reserveGatewayPortForLane(lane); - cleanup.push(() => gatewayPortReservation.release()); - await runTimedLanePhase(lane, "onboard", async () => { - await runOnboard({ - lane, - env, - providerConfig: params.providerConfig, - logPath: join(params.logsDir, "upgrade-onboard.log"), - }); - }); - - await runTimedLanePhase(lane, "models-set", async () => { - await runModelsSet({ - lane, - env, - providerConfig: params.providerConfig, - logPath: join(params.logsDir, "upgrade-models-set.log"), - }); - }); - - const gateway = await runTimedLanePhase(lane, "start-gateway", async () => { - await gatewayPortReservation.release(); - return startGateway({ - lane, - env, - logPath: join(params.logsDir, "upgrade-gateway.log"), - }); - }); - gatewayHolder.current = gateway; - cleanup.push(() => stopGateway(gatewayHolder.current)); - - await runTimedLanePhase(lane, "wait-gateway", async () => { - await waitForGateway({ - lane, - env, - gatewayHolder, - gatewayLogPath: join(params.logsDir, "upgrade-gateway.log"), - logPath: join(params.logsDir, "upgrade-gateway-status.log"), - }); - }); - - await runTimedLanePhase(lane, "dashboard", async () => { - await runDashboardSmoke({ - lane, - logPath: join(params.logsDir, "upgrade-dashboard.log"), - }); - }); - - const agent = await runTimedLanePhase(lane, "agent-turn", async () => - runAgentTurn({ - lane, - env, - label: "upgrade", - logPath: join(params.logsDir, "upgrade-agent.log"), - }), - ); + const agent = await runPackagedGatewaySmoke({ ...params, lane, env, cleanup }); return { ...result, @@ -522,6 +410,65 @@ export async function runUpgradeLane( } } +async function runPackagedGatewaySmoke( + params: Pick & { + lane: LaneState; + env: NodeJS.ProcessEnv; + cleanup: Cleanup[]; + beforeGatewayStart?: () => Promise; + afterGatewayReady?: () => Promise; + }, +) { + const { lane, env, cleanup } = params; + const gatewayHolder: { current: GatewayHandle | null } = { current: null }; + const logPath = (phase: string) => join(params.logsDir, `${lane.name}-${phase}.log`); + // Own the configured port through setup; release only when the gateway can claim it. + const gatewayPortReservation = await reserveGatewayPortForLane(lane); + cleanup.push(() => gatewayPortReservation.release()); + await runTimedLanePhase(lane, "onboard", async () => { + await runOnboard({ + lane, + env, + providerConfig: params.providerConfig, + logPath: logPath("onboard"), + }); + }); + await runTimedLanePhase(lane, "models-set", async () => { + await runModelsSet({ + lane, + env, + providerConfig: params.providerConfig, + logPath: logPath("models-set"), + }); + }); + if (params.beforeGatewayStart) { + await params.beforeGatewayStart(); + } + gatewayHolder.current = await runTimedLanePhase(lane, "start-gateway", async () => { + await gatewayPortReservation.release(); + return startGateway({ lane, env, logPath: logPath("gateway") }); + }); + cleanup.push(() => stopGateway(gatewayHolder.current)); + await runTimedLanePhase(lane, "wait-gateway", async () => { + await waitForGateway({ + lane, + env, + gatewayHolder, + gatewayLogPath: logPath("gateway"), + logPath: logPath("gateway-status"), + }); + }); + if (params.afterGatewayReady) { + await params.afterGatewayReady(); + } + await runTimedLanePhase(lane, "dashboard", async () => { + await runDashboardSmoke({ lane, logPath: logPath("dashboard") }); + }); + return runTimedLanePhase(lane, "agent-turn", async () => + runAgentTurn({ lane, env, label: lane.name, logPath: logPath("agent") }), + ); +} + export async function runInstallerFreshSuite( params: LaneBaseParams & { build: CandidateBuild; runDiscordRoundtrip: boolean }, ) { diff --git a/scripts/lib/gateway-bench-worker-profile.ts b/scripts/lib/gateway-bench-worker-profile.ts index bd082786a091..e84c5a152b48 100644 --- a/scripts/lib/gateway-bench-worker-profile.ts +++ b/scripts/lib/gateway-bench-worker-profile.ts @@ -2,6 +2,7 @@ import { writeFileSync } from "node:fs"; import type { Session } from "node:inspector/promises"; import { performance } from "node:perf_hooks"; import type { Worker } from "node:worker_threads"; +import { coerceErrorMessage as errorMessage } from "./error-format.mts"; import { GATEWAY_CPU_SAMPLE_INTERVAL_MICROS, GATEWAY_HEAP_SAMPLE_INTERVAL, @@ -44,10 +45,6 @@ type WorkerReply = { error?: { message: string }; }; -function errorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - /** Uses the private inspector channel; no worker factory changes or debug listener. */ export class GatewayBenchWorkerProfiler { private workers = new Set(); diff --git a/scripts/lib/packed-openclaw-tarballs.mts b/scripts/lib/packed-openclaw-tarballs.mts index 95dddda3a798..5f3f61683452 100644 --- a/scripts/lib/packed-openclaw-tarballs.mts +++ b/scripts/lib/packed-openclaw-tarballs.mts @@ -1,6 +1,12 @@ import fs from "node:fs/promises"; import path from "node:path"; +export function validatePackedTarballOutputName(value: string) { + if (!/^[A-Za-z0-9][A-Za-z0-9._-]*\.t(?:ar\.)?gz$/u.test(value)) { + throw new Error(`--output-name must be a tarball filename, not a path: ${value}`); + } +} + export async function cleanPackedOpenClawTarballs(outputDir: string) { let entries: string[]; try { diff --git a/scripts/lib/plugin-gateway-gauntlet.mts b/scripts/lib/plugin-gateway-gauntlet.mts index 05a4d87e6432..bd28e8603ce6 100644 --- a/scripts/lib/plugin-gateway-gauntlet.mts +++ b/scripts/lib/plugin-gateway-gauntlet.mts @@ -2,10 +2,12 @@ import fs from "node:fs"; import path from "node:path"; import JSON5 from "json5"; +import { normalizeTrimmedStringList } from "../../packages/normalization-core/src/string-normalization.ts"; import { NON_PACKAGED_BUNDLED_PLUGIN_DIRS } from "../../src/shared/non-packaged-plugin-dirs.ts"; import { collectBundledPluginBuildEntries } from "./bundled-plugin-build-entries.mjs"; +import { groupBy } from "./group-by.mts"; import { parsePositiveInt } from "./numeric-options.mjs"; -import { isRecord } from "./record-shared.mjs"; +import { isRecord, trimString } from "./record-shared.mjs"; type PluginGatewayEntry = { id: string; @@ -29,16 +31,6 @@ const ANSI_PATTERN = new RegExp(String.raw`\u001B\[[0-9;]*m`, "gu"); const QA_SUMMARY_MAX_BYTES_ENV = "OPENCLAW_PLUGIN_GATEWAY_GAUNTLET_QA_SUMMARY_MAX_BYTES"; const DEFAULT_QA_SUMMARY_MAX_BYTES = 2 * 1024 * 1024; -function normalizeStringOrEmpty(value: unknown) { - return typeof value === "string" ? value.trim() : ""; -} - -function normalizeStringArray(value: unknown) { - return Array.isArray(value) - ? value.map((entry) => normalizeStringOrEmpty(entry)).filter((entry) => entry.length > 0) - : []; -} - function readPluginManifest(manifestPath: string) { const raw = fs.readFileSync(manifestPath, "utf8"); const parsed: unknown = manifestPath.endsWith(".json5") ? JSON5.parse(raw) : JSON.parse(raw); @@ -95,20 +87,20 @@ function collectCommandAliasRecords(manifest: PluginManifest) { return aliases .map((alias) => { if (typeof alias === "string") { - const name = normalizeStringOrEmpty(alias); + const name = trimString(alias); return name ? { name, kind: "runtime-slash", cliCommand: null } : null; } if (!isRecord(alias)) { return null; } - const name = normalizeStringOrEmpty(alias.name); + const name = trimString(alias.name); if (!name) { return null; } return { name, - kind: normalizeStringOrEmpty(alias.kind) || "runtime-slash", - cliCommand: normalizeStringOrEmpty(alias.cliCommand) || null, + kind: trimString(alias.kind) || "runtime-slash", + cliCommand: trimString(alias.cliCommand) || null, }; }) .filter((alias) => alias !== null); @@ -117,14 +109,14 @@ function collectCommandAliasRecords(manifest: PluginManifest) { function collectAuthMethods(manifest: PluginManifest) { const auth = Array.isArray(manifest.auth) ? manifest.auth : []; return auth - .map((entry) => (isRecord(entry) ? normalizeStringOrEmpty(entry.method) : "")) + .map((entry) => (isRecord(entry) ? trimString(entry.method) : "")) .filter((method) => method.length > 0); } function collectOnboardingScopes(manifest: PluginManifest) { const scopes = new Set(); const addScopes = (value: unknown) => { - for (const scope of normalizeStringArray(value)) { + for (const scope of normalizeTrimmedStringList(value)) { scopes.add(scope); } }; @@ -146,17 +138,17 @@ function buildPluginMatrixEntry(repoRoot: string, manifestPath: string, manifest return { id: manifest.id, buildId: path.basename(pluginDir), - name: normalizeStringOrEmpty(manifest.name) || manifest.id, + name: trimString(manifest.name) || manifest.id, dir: path.relative(repoRoot, pluginDir), manifestPath: relativeManifestPath, enabledByDefault: manifest.enabledByDefault === true, activation: isRecord(manifest.activation) ? manifest.activation : {}, - providers: normalizeStringArray(manifest.providers), - channels: normalizeStringArray(manifest.channels), - skills: normalizeStringArray(manifest.skills), + providers: normalizeTrimmedStringList(manifest.providers), + channels: normalizeTrimmedStringList(manifest.channels), + skills: normalizeTrimmedStringList(manifest.skills), authMethods: collectAuthMethods(manifest), onboardingScopes: collectOnboardingScopes(manifest), - requiredPlugins: normalizeStringArray(manifest.requiresPlugins), + requiredPlugins: normalizeTrimmedStringList(manifest.requiresPlugins), hasConfigSchema: isRecord(manifest.configSchema), hasRequiredConfigFields: schemaHasRequiredFields(manifest.configSchema), commandAliases, @@ -194,7 +186,7 @@ function selectPluginEntries( entries: T[], options: { ids?: string[]; shardIndex?: number; shardTotal?: number; limit?: number } = {}, ) { - const ids = new Set(normalizeStringArray(options.ids)); + const ids = new Set(normalizeTrimmedStringList(options.ids)); let selected = ids.size > 0 ? entries.filter((entry) => ids.has(entry.id)) : [...entries]; const missingIds = [...ids].filter((id) => !entries.some((entry) => entry.id === id)); if (missingIds.length > 0) { @@ -281,17 +273,6 @@ function median(values: unknown[]) { : (lower + upper) / 2; } -function groupByPhase(rows: JsonRecord[]) { - const phases = new Map(); - for (const row of rows) { - const phase = normalizeStringOrEmpty(row.phase) || "unknown"; - const current = phases.get(phase) ?? []; - current.push(row); - phases.set(phase, current); - } - return phases; -} - function collectMetricObservations(rows: JsonRecord[], thresholds: MetricThresholds = {}) { const cpuCoreWarn = thresholds.cpuCoreWarn ?? 0.9; const hotWallWarnMs = thresholds.hotWallWarnMs ?? 30_000; @@ -300,7 +281,7 @@ function collectMetricObservations(rows: JsonRecord[], thresholds: MetricThresho const rssAnomalyMultiplier = thresholds.rssAnomalyMultiplier ?? 2.5; const firstWorkRow = rows.find((row) => row.phase !== "prebuild"); const observations = []; - for (const [phase, phaseRows] of groupByPhase(rows)) { + for (const [phase, phaseRows] of groupBy(rows, (row) => trimString(row.phase) || "unknown")) { const wallMedianMs = median(phaseRows.map((row) => row.wallMs)); const rssMedianMb = median(phaseRows.map((row) => row.maxRssMb)); for (const row of phaseRows) { @@ -437,7 +418,7 @@ function buildGauntletPrebuildEnv( includePrivateQa?: boolean; } = {}, ) { - const buildIds = new Set(normalizeStringArray(options.buildIds)); + const buildIds = new Set(normalizeTrimmedStringList(options.buildIds)); if (options.includePrivateQa) { for (const pluginId of NON_PACKAGED_BUNDLED_PLUGIN_DIRS) { buildIds.add(pluginId); @@ -492,7 +473,7 @@ function collectGatewayCpuObservations(params: { if (!isRecord(result)) { continue; } - const id = normalizeStringOrEmpty(result.id) || "unknown"; + const id = trimString(result.id) || "unknown"; const cpuCoreMax = metricMax(result.summary, "cpuCoreRatio"); const wallMax = metricMax(result.summary, "readyzMs") ?? metricMax(result.summary, "healthzMs"); if ( diff --git a/scripts/lib/shrink-ratchet.mts b/scripts/lib/shrink-ratchet.mts index f51f6f02b94c..6da0c9eb4aae 100644 --- a/scripts/lib/shrink-ratchet.mts +++ b/scripts/lib/shrink-ratchet.mts @@ -255,10 +255,6 @@ export function reportRatchetFailures( return active.length > 0; } -export function reportRatchetSuccess(message: string) { - console.log(message); -} - type PerFileCountRatchetOptions = { baselinePath: string; baselineHeader: string; @@ -366,7 +362,7 @@ export function runPerFileCountRatchet( } catch { if (args.prune && !args.staged && baseBaseline === null) { writeBaseline(current); - reportRatchetSuccess( + console.log( `Initialized ${baselinePath}: ${current.size} files, ${totalCount(current)} ${messages.countNoun}.`, ); return 0; @@ -376,7 +372,7 @@ export function runPerFileCountRatchet( if (args.prune && !args.staged && baseBaseline === null) { writeBaseline(current); - reportRatchetSuccess( + console.log( `Refreshed initial ${baselinePath}: ${current.size} files, ${totalCount(current)} ${messages.countNoun}.`, ); return 0; @@ -427,7 +423,7 @@ export function runPerFileCountRatchet( const oldFiles = baseline.size; const oldCount = totalCount(baseline); writeBaseline(current); - reportRatchetSuccess( + console.log( `Pruned ${baselinePath}: ${oldFiles} -> ${current.size} files; ${oldCount} -> ${totalCount(current)} ${messages.countNoun}.`, ); return 0; @@ -445,7 +441,7 @@ export function runPerFileCountRatchet( return 1; } - reportRatchetSuccess( + console.log( `${messages.successTitle}: ${current.size} files, ${totalCount(current)} grandfathered ${messages.countNoun}.`, ); return 0; diff --git a/scripts/mantis/observe-request-web-ui.mts b/scripts/mantis/observe-request-web-ui.mts index 457596720ddf..d965a91109d1 100644 --- a/scripts/mantis/observe-request-web-ui.mts +++ b/scripts/mantis/observe-request-web-ui.mts @@ -6,6 +6,7 @@ import path from "node:path"; import { Script } from "node:vm"; import { JSDOM } from "jsdom"; import { chromium, type WebSocketRoute } from "playwright"; +import { createDeferredCore } from "../../src/shared/deferred.ts"; import { createControlUiMockBootstrapConfig, createControlUiMockGatewayInitScript, @@ -37,10 +38,7 @@ new Script(createControlUiMockGatewayInitScript(scenario)).runInContext( const sockets = new Set(); let sendRequest: { id: string; params: unknown } | undefined; let sendRequestCount = 0; -let resolveSend: (() => void) | undefined; -const sent = new Promise((resolve) => { - resolveSend = resolve; -}); +const { promise: sent, resolve: resolveSend } = createDeferredCore(); let trafficBytes = 0; let protocolError = false; let captureArmed = false; @@ -142,7 +140,7 @@ try { if (frame.ok !== true) { protocolError = true; } - resolveSend?.(); + resolveSend(); } }); route.onMessage((message) => { diff --git a/scripts/mantis/run-request-telegram-qa.mts b/scripts/mantis/run-request-telegram-qa.mts index 67afd10edf99..ccabec4fe314 100644 --- a/scripts/mantis/run-request-telegram-qa.mts +++ b/scripts/mantis/run-request-telegram-qa.mts @@ -6,8 +6,8 @@ import { chmod, copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:f import path from "node:path"; import { setTimeout as sleep } from "node:timers/promises"; import { promisify } from "node:util"; -import { z } from "zod"; import { requestIdentitySchema } from "./request-proof.ts"; +import { proofImageInspectionSchema } from "./telegram-proof-storage.mts"; import { removeTelegramQaNetwork } from "./telegram-qa-cleanup.ts"; import { telegramQaExecutionSchema, @@ -43,15 +43,9 @@ const identity = requestIdentitySchema.parse({ const execute = promisify(execFile); const podman = async (args: string[]) => (await execute("podman", args, { timeout: 300_000, maxBuffer: 2 * 1024 * 1024 })).stdout; -const info = z - .array( - z.object({ - Id: z.string().regex(/^(?:sha256:)?[a-f0-9]{64}$/), - Config: z.object({ Labels: z.record(z.string(), z.string()) }), - }), - ) - .length(1) - .parse(JSON.parse(await podman(["image", "inspect", image])))[0]!; +const info = proofImageInspectionSchema.parse( + JSON.parse(await podman(["image", "inspect", image])), +)[0]!; if (info.Config.Labels["org.openclaw.mantis.candidate-sha"] !== candidate) { throw new Error("Candidate runtime identity mismatch"); } @@ -204,23 +198,18 @@ try { } catch (error) { errors.push(error); } finally { - for (const owned of [sut, observer]) { + for (const cleanup of [ + () => podman(["rm", "--force", "--ignore", sut]), + () => podman(["rm", "--force", "--ignore", observer]), + () => removeTelegramQaNetwork(podman, createdNetwork), + () => rm(scratch, { recursive: true, force: true }), + ]) { try { - await podman(["rm", "--force", "--ignore", owned]); + await cleanup(); } catch (error) { errors.push(error); } } - try { - await removeTelegramQaNetwork(podman, createdNetwork); - } catch (error) { - errors.push(error); - } - try { - await rm(scratch, { recursive: true, force: true }); - } catch (error) { - errors.push(error); - } } if (errors.length) { throw new AggregateError(errors, "Requested QA execution or owned environment cleanup failed"); diff --git a/scripts/mantis/run-request-telegram.mts b/scripts/mantis/run-request-telegram.mts index 6f40df91355c..b62ec1ad9c65 100644 --- a/scripts/mantis/run-request-telegram.mts +++ b/scripts/mantis/run-request-telegram.mts @@ -27,6 +27,7 @@ import { parseTelegramProofPlan, type TelegramProofPlan } from "./telegram-proof import { assertPodmanProofStorage, assertProofImage, + proofImageInspectionSchema, proofImageTag, } from "./telegram-proof-storage.mts"; import { telegramProofIdentitySchema } from "./telegram-request-proof.ts"; @@ -47,14 +48,6 @@ class TelegramProofStageError extends Error { const execute = promisify(execFile); const podman = async (args: string[]) => (await execute("podman", args, { maxBuffer: 2 * 1024 * 1024, timeout: 60_000 })).stdout; -const imageInfo = z - .array( - z.object({ - Id: z.string().regex(/^(?:sha256:)?[a-f0-9]{64}$/), - Config: z.object({ Labels: z.record(z.string(), z.string()) }), - }), - ) - .length(1); const skill = path.resolve(".agents/skills/telegram-e2e-userbot/scripts"); type QaLease = Awaited>; @@ -112,7 +105,9 @@ async function preflight(candidate: string, image: string) { if (!/^[a-f0-9]{40}$/.test(candidate) || !/^[a-z0-9][a-z0-9/.:@-]*$/.test(image)) { throw new Error("Invalid candidate/image selection"); } - const info = imageInfo.parse(JSON.parse(await podman(["image", "inspect", image])))[0]; + const info = proofImageInspectionSchema.parse( + JSON.parse(await podman(["image", "inspect", image])), + )[0]; if (!info || info.Config.Labels["org.openclaw.mantis.candidate-sha"] !== candidate) { throw new Error("Prepared runtime does not match exact candidate"); } @@ -546,7 +541,7 @@ async function run() { // field literally. Bind a private tag to the immutable image before creation, // then verify the created container's immutable image before Gateway startup. assertPodmanProofStorage(); - const tagged = imageInfo.parse( + const tagged = proofImageInspectionSchema.parse( JSON.parse(await podman(["image", "inspect", ready.imageTag])), )[0]; assertProofImage(ready.imageId, tagged?.Id ?? ""); diff --git a/scripts/mantis/telegram-proof-ingress.mts b/scripts/mantis/telegram-proof-ingress.mts index 664b88d8efc0..2b30190785a2 100644 --- a/scripts/mantis/telegram-proof-ingress.mts +++ b/scripts/mantis/telegram-proof-ingress.mts @@ -2,6 +2,7 @@ import { appendFile } from "node:fs/promises"; import http from "node:http"; import { z } from "zod"; import { startTelegramTestApiProxy } from "../../.agents/skills/telegram-e2e-userbot/scripts/telegram-test-api-proxy.mjs"; +import { createDeferredCore } from "../../src/shared/deferred.ts"; import type { TelegramFailureDiagnostic } from "./request-proof.ts"; import type { TelegramProofPlan } from "./telegram-proof-plan.ts"; @@ -95,10 +96,7 @@ export async function startTelegramProofIngress(options: { diagnostics.push({ sequence: diagnostics.length + 1, category }); } }; - let stopForwarding!: (error: Error) => void; - const stopped = new Promise((resolve) => { - stopForwarding = resolve; - }); + const { promise: stopped, resolve: stopForwarding } = createDeferredCore(); const cancel = () => { stopForwarding(new Error("Telegram proof forwarding stopped")); for (const controller of readers) { diff --git a/scripts/mantis/telegram-proof-storage.mts b/scripts/mantis/telegram-proof-storage.mts index 387845cb3701..8b75cd96b9fc 100644 --- a/scripts/mantis/telegram-proof-storage.mts +++ b/scripts/mantis/telegram-proof-storage.mts @@ -17,6 +17,14 @@ import { fileURLToPath } from "node:url"; import { z } from "zod"; export const proofStorageBytes = 32 * 1024 ** 3; +export const proofImageInspectionSchema = z + .array( + z.object({ + Id: z.string().regex(/^(?:sha256:)?[a-f0-9]{64}$/), + Config: z.object({ Labels: z.record(z.string(), z.string()) }), + }), + ) + .length(1); const containerConfiguration = '[containers]\nlog_driver = "k8s-file"\nlog_size_max = 1048576\n' + '[network]\ndefault_rootless_network_cmd = "slirp4netns"\n'; diff --git a/scripts/native-app-i18n.ts b/scripts/native-app-i18n.ts index 6b2eb172edaa..0f0076792d9b 100644 --- a/scripts/native-app-i18n.ts +++ b/scripts/native-app-i18n.ts @@ -7,8 +7,14 @@ import { expectDefined } from "../packages/normalization-core/src/expect.js"; import { isRecord } from "../packages/normalization-core/src/record-coerce.js"; import { sliceUtf16Safe } from "../packages/normalization-core/src/utf16-slice.ts"; import { decodeXml } from "../src/shared/xml.ts"; -import { collectToolDisplaySources, selectDeterministicTranslation } from "./android-app-i18n.ts"; +import { + collectToolDisplaySources, + findClosingDelimiter, + lineNumber, + selectDeterministicTranslation, +} from "./android-app-i18n.ts"; import { translateNativeEntries } from "./control-ui-i18n.ts"; +import { compareAscii as compareCodePoints } from "./lib/canonical-json.mjs"; import { NATIVE_I18N_LOCALES } from "./native-i18n-locales.ts"; type NativeI18nSurface = "android" | "apple"; @@ -396,52 +402,6 @@ function extractKotlinInterpolations(source: string): NativeInterpolation[] | nu return values; } -function compareCodePoints(left: string, right: string): number { - return left < right ? -1 : left > right ? 1 : 0; -} - -function lineNumber(source: string, offset: number): number { - return source.slice(0, offset).split("\n").length; -} - -function findClosingDelimiter( - source: string, - openingIndex: number, - opening: string, - closing: string, -): number | null { - let depth = 0; - let quoted = false; - let escaped = false; - for (let index = openingIndex; index < source.length; index += 1) { - const character = source[index]; - if (escaped) { - escaped = false; - continue; - } - if (quoted && character === "\\") { - escaped = true; - continue; - } - if (character === '"') { - quoted = !quoted; - continue; - } - if (quoted) { - continue; - } - if (character === opening) { - depth += 1; - } else if (character === closing) { - depth -= 1; - if (depth === 0) { - return index; - } - } - } - return null; -} - function readMultilineStringLiteral( source: string, openingQuote: number, diff --git a/scripts/openclaw-prepack.ts b/scripts/openclaw-prepack.ts index 09bc9ad3faf4..e923240eb7b1 100644 --- a/scripts/openclaw-prepack.ts +++ b/scripts/openclaw-prepack.ts @@ -266,22 +266,14 @@ function runPnpm(args: string[], env: NodeJS.ProcessEnv): void { run(command.command, command.args, { ...command.options, env }); } -function runBuildSmoke(): void { - run(process.execPath, ["--import", "tsx", "scripts/test-built-bundled-channel-entry-smoke.mts"]); -} - -async function writeDistInventory(): Promise { - await writePackageDistInventoryForPublish(process.cwd()); -} - export async function preparePrepackArtifacts(env: NodeJS.ProcessEnv = process.env): Promise { ensurePreparedArtifacts(); - runBuildSmoke(); + run(process.execPath, ["--import", "tsx", "scripts/test-built-bundled-channel-entry-smoke.mts"]); // The docs-map receipt serializes source-mutating pack lifecycles before the // changelog is touched, so concurrent packs cannot restore each other's files. await preparePackageDocsMap(process.cwd()); try { - await writeDistInventory(); + await writePackageDistInventoryForPublish(process.cwd()); await preparePackageManifest(process.cwd()); await preparePackageChangelog(process.cwd(), { allowUnreleased: resolvePrepackAllowUnreleasedChangelog(env), diff --git a/scripts/package-openclaw-for-docker.mts b/scripts/package-openclaw-for-docker.mts index 93f8772aaccb..e6480d0c4c36 100644 --- a/scripts/package-openclaw-for-docker.mts +++ b/scripts/package-openclaw-for-docker.mts @@ -7,6 +7,7 @@ import { createRequire } from "node:module"; import os from "node:os"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; +import { resolveTimerTimeoutMs } from "../packages/normalization-core/src/number-coercion.ts"; import { booleanFlag, parseFlagArgs, @@ -22,7 +23,10 @@ import { LEGACY_PACKAGE_INSTALL_GUARD_RELATIVE_PATH, PACKAGE_LIFECYCLE_PENDING_RELATIVE_PATH, } from "./lib/package-lifecycle-marker.mjs"; -import { cleanPackedOpenClawTarballs } from "./lib/packed-openclaw-tarballs.mts"; +import { + cleanPackedOpenClawTarballs, + validatePackedTarballOutputName, +} from "./lib/packed-openclaw-tarballs.mts"; import { isRecord } from "./lib/record-shared.mjs"; import { resolveNpmRunner } from "./npm-runner.mts"; import { preparePackageChangelog, restorePackageChangelog } from "./package-changelog.mjs"; @@ -38,7 +42,6 @@ const DEFAULT_TIMEOUT_KILL_AFTER_MS = 5_000; const PROCESS_GROUP_EXIT_POLL_MS = 25; const POST_FORCE_KILL_WAIT_MS = 1_000; const DEFAULT_CAPTURED_STDOUT_MAX_BYTES = 1024 * 1024; -const MAX_TIMER_TIMEOUT_MS = 2_147_000_000; const AI_RUNTIME_PACKAGE = "@openclaw/ai"; const AI_RUNTIME_BACKUP_DIR = ".openclaw-ai-package-backup"; @@ -174,30 +177,8 @@ function resolveTimeoutMs(envName: string, defaultValue: number) { return parsed; } -function numericTimerValueMs(valueMs: unknown) { - const value = Number(valueMs); - return Number.isFinite(value) ? Math.floor(value) : undefined; -} - -function resolvePackageBuildTimeoutMs( - valueMs: unknown, - fallbackMs: unknown = MAX_TIMER_TIMEOUT_MS, -) { - const value = numericTimerValueMs(valueMs) ?? numericTimerValueMs(fallbackMs); - return Math.min(Math.max(value ?? MAX_TIMER_TIMEOUT_MS, 1), MAX_TIMER_TIMEOUT_MS); -} - function resolveOptionalTimerTimeoutMs(valueMs: unknown) { - if (valueMs === undefined) { - return undefined; - } - return resolvePackageBuildTimeoutMs(valueMs, 1); -} - -function validateOutputName(value: string) { - if (!/^[A-Za-z0-9][A-Za-z0-9._-]*\.t(?:ar\.)?gz$/u.test(value)) { - throw new Error(`--output-name must be a tarball filename, not a path: ${value}`); - } + return valueMs === undefined ? undefined : resolveTimerTimeoutMs(Number(valueMs), 1); } function resolvePackedOpenClawFileName(value: string) { @@ -253,7 +234,7 @@ export function parseArgs(argv: string[]) { }, ); if (options.outputName) { - validateOutputName(options.outputName); + validatePackedTarballOutputName(options.outputName); } if (options.packJson && options.pnpmPack) { throw new Error("--pack-json cannot be combined with --pnpm-pack"); @@ -271,8 +252,8 @@ function run(command: string, args: string[], cwd: string, options: RunOptions = } return new Promise((resolve, reject) => { const resolvedTimeoutMs = resolveOptionalTimerTimeoutMs(options.timeoutMs); - const resolvedKillAfterMs = resolvePackageBuildTimeoutMs( - options.killAfterMs, + const resolvedKillAfterMs = resolveTimerTimeoutMs( + Number(options.killAfterMs), DEFAULT_TIMEOUT_KILL_AFTER_MS, ); const useProcessGroup = process.platform !== "win32"; diff --git a/scripts/profile-extension-memory.mts b/scripts/profile-extension-memory.mts index c2be23f87968..b90495c6f246 100644 --- a/scripts/profile-extension-memory.mts +++ b/scripts/profile-extension-memory.mts @@ -17,8 +17,8 @@ import { ensureExtensionMemoryBuild, findBuiltExtensionMemoryEntries, } from "./ensure-extension-memory-build.mts"; -import { stripLeadingPackageManagerSeparator } from "./lib/arg-utils.mts"; -import { appendBoundedTail } from "./lib/bounded-output-tail.mjs"; +import { requireOptionArgument, stripLeadingPackageManagerSeparator } from "./lib/arg-utils.mts"; +import { appendBoundedTail, formatBoundedTail } from "./lib/bounded-output-tail.mjs"; import { formatErrorMessage } from "./lib/error-format.mts"; import { captureImportIdentity, @@ -131,10 +131,7 @@ export function parseArgs(argv: string[]): { break parseArgv; case "--extension": case "-e": { - const next = args[index + 1]; - if (!next || next.startsWith("-")) { - throw new Error(`${arg} requires a value`); - } + const next = requireOptionArgument(args, index, arg); options.extensions.push(next); index += 1; break; @@ -154,10 +151,7 @@ export function parseArgs(argv: string[]): { break; } case "--json": { - const next = args[index + 1]; - if (!next || next.startsWith("-")) { - throw new Error(`${arg} requires a value`); - } + const next = requireOptionArgument(args, index, arg); options.jsonPath = path.resolve(next); index += 1; break; @@ -181,13 +175,6 @@ function createOutputCapture(): OutputCapture { return { text: "", truncatedChars: 0 }; } -function formatCapturedOutput(capture: OutputCapture): string { - if (capture.truncatedChars === 0) { - return capture.text; - } - return `[output truncated ${capture.truncatedChars} chars; showing tail]\n${capture.text}`; -} - function summarizeStderr(stderr: string, lines = 8, maxChars = STDERR_PREVIEW_MAX_CHARS): string { const text = stderr.trim().split("\n").filter(Boolean).slice(0, lines).join("\n"); if (text.length <= maxChars) { @@ -438,14 +425,14 @@ export function runCase({ } } } - const stderrText = formatCapturedOutput(stderr); + const stderrText = formatBoundedTail(stderr); const result: RunCaseResult = { name, code, signal, timedOut, error: null, - stdout: formatCapturedOutput(stdout), + stdout: formatBoundedTail(stdout), stderr: stderrText, maxRssMb: observation.resources ? observation.resources.maxRssKb / 1024 : null, resources: observation.resources, diff --git a/scripts/resolve-openclaw-package-candidate.mts b/scripts/resolve-openclaw-package-candidate.mts index 9a48bc55c0b4..33ae3cb7d870 100644 --- a/scripts/resolve-openclaw-package-candidate.mts +++ b/scripts/resolve-openclaw-package-candidate.mts @@ -17,13 +17,17 @@ import os from "node:os"; import path from "node:path"; import { pipeline } from "node:stream/promises"; import { fileURLToPath } from "node:url"; +import { resolveTimerTimeoutMs } from "../packages/normalization-core/src/number-coercion.ts"; import { isRecord as isJsonRecord } from "../packages/normalization-core/src/record-coerce.ts"; import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mts"; -import { appendBoundedTail } from "./lib/bounded-output-tail.mjs"; +import { appendBoundedTail, formatBoundedTail } from "./lib/bounded-output-tail.mjs"; import { toErrorObject } from "./lib/error-format.mts"; import { terminateManagedChild } from "./lib/managed-child-process.mts"; import { resolveNpmJsonEntries } from "./lib/npm-json-output.mts"; -import { cleanPackedOpenClawTarballs } from "./lib/packed-openclaw-tarballs.mts"; +import { + cleanPackedOpenClawTarballs, + validatePackedTarballOutputName, +} from "./lib/packed-openclaw-tarballs.mts"; import { resolveRepoRoot } from "./lib/repo-root.mjs"; import { resolveNpmRunner } from "./npm-runner.mts"; import { validatePackageSourceDir } from "./package-source-preflight.mjs"; @@ -40,15 +44,10 @@ const COMMAND_STDERR_CAPTURE_MAX_CHARS = 128 * 1024; const COMMAND_TIMEOUT_KILL_AFTER_MS = 5_000; const FORWARDED_SIGNAL_KILL_AFTER_MS = 250; const COMMAND_PROCESS_TREE_EXIT_POLL_MS = 50; -const MAX_TIMER_TIMEOUT_MS = 2_147_000_000; type ChildSignal = ChildProcess["signalCode"]; type TimerHandle = ReturnType; type ChildKiller = (signal: NodeJS.Signals) => void; type ProcessTreeChild = Pick; -type CommandOutputBuffer = { - text: string; - truncatedChars: number; -}; type RunOptions = { capture?: boolean; @@ -266,16 +265,10 @@ export function parseArgs(argv: readonly string[]) { }, }, ); - validateOutputName(options.outputName); + validatePackedTarballOutputName(options.outputName); return options; } -function validateOutputName(value: string) { - if (!/^[A-Za-z0-9][A-Za-z0-9._-]*\.t(?:ar\.)?gz$/u.test(value)) { - throw new Error(`--output-name must be a tarball filename, not a path: ${value}`); - } -} - function resolvePackedOpenClawTarballFilename(value: unknown) { const filename = typeof value === "string" ? value.trim() : ""; if ( @@ -324,31 +317,15 @@ export function resolveNpmPackageCandidatePackRunner( }); } -function numericTimerValueMs(valueMs: unknown) { - const value = Number(valueMs); - return Number.isFinite(value) ? Math.floor(value) : undefined; -} - -function resolvePackageCandidateTimeoutMs( - valueMs: unknown, - fallbackMs: unknown = MAX_TIMER_TIMEOUT_MS, -) { - const value = numericTimerValueMs(valueMs) ?? numericTimerValueMs(fallbackMs); - return Math.min(Math.max(value ?? MAX_TIMER_TIMEOUT_MS, 1), MAX_TIMER_TIMEOUT_MS); -} - function resolveOptionalTimerTimeoutMs(valueMs: unknown) { - if (valueMs === undefined) { - return undefined; - } - return resolvePackageCandidateTimeoutMs(valueMs, 1); + return valueMs === undefined ? undefined : resolveTimerTimeoutMs(Number(valueMs), 1); } function run(command: string, args: readonly string[], options: RunOptions = {}) { return new Promise((resolve, reject) => { const resolvedTimeoutMs = resolveOptionalTimerTimeoutMs(options.timeoutMs); - const resolvedKillAfterMs = resolvePackageCandidateTimeoutMs( - options.killAfterMs, + const resolvedKillAfterMs = resolveTimerTimeoutMs( + Number(options.killAfterMs), COMMAND_TIMEOUT_KILL_AFTER_MS, ); const useProcessGroup = process.platform !== "win32"; @@ -448,7 +425,7 @@ function run(command: string, args: readonly string[], options: RunOptions = {}) resolve(stdout.text); return; } - const stderrText = formatCapturedCommandOutput(stderr).trim(); + const stderrText = formatBoundedTail(stderr).trim(); const detail = stderrText ? `\n${stderrText}` : ""; reject(new Error(`${command} ${args.join(" ")} failed with ${status ?? signal}${detail}`)); }); @@ -519,13 +496,6 @@ async function waitForProcessTreeExit( return !processTreeIsAlive(child, useProcessGroup); } -function formatCapturedCommandOutput(buffer: CommandOutputBuffer) { - if (buffer.truncatedChars === 0) { - return buffer.text; - } - return `[output truncated ${buffer.truncatedChars} chars; showing tail]\n${buffer.text}`; -} - export const runCommandForTest = run; async function sha256(file: string) { @@ -1426,8 +1396,8 @@ async function openHttpsPackageDownloadResponse( async function openPackageDownloadResponse(url: string, options: PackageDownloadOptions) { const lookupHost = options.lookupHost ?? defaultLookupHost; - const timeoutMs = resolvePackageCandidateTimeoutMs( - options.timeoutMs, + const timeoutMs = resolveTimerTimeoutMs( + Number(options.timeoutMs), PACKAGE_URL_DOWNLOAD_TIMEOUT_MS, ); const maxRedirects = options.maxRedirects ?? PACKAGE_URL_MAX_REDIRECTS; diff --git a/scripts/test-docker-all.mts b/scripts/test-docker-all.mts index 606765e55c14..600cea952277 100644 --- a/scripts/test-docker-all.mts +++ b/scripts/test-docker-all.mts @@ -12,6 +12,10 @@ import { Readable } from "node:stream"; import { finished } from "node:stream/promises"; import { fileURLToPath } from "node:url"; import { coerceErrorMessage } from "@openclaw/normalization-core/error-coercion"; +import { + clampPositiveTimerTimeoutMs, + resolveTimerTimeoutMs, +} from "@openclaw/normalization-core/number-coercion"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { DEFAULT_E2E_BARE_IMAGE, @@ -65,7 +69,6 @@ const SHELL_POST_FORCE_KILL_WAIT_MS = 1_000; // Private QA subprocess contract. Ordinary lane/CLI failures remain 1; 130/143 // acknowledge joined signal cleanup. Only failed owner cleanup uses 2. const CLEANUP_FAILURE_EXIT_CODE = 2; -const MAX_TIMER_TIMEOUT_MS = 2_147_000_000; const DEFAULT_TIMINGS_FILE = path.join(ROOT_DIR, ".artifacts/docker-tests/lane-timings.json"); const DEFAULT_GITHUB_WORKFLOW = "openclaw-live-and-e2e-checks-reusable.yml"; const CANDIDATE_ENV_KEYS = @@ -267,25 +270,8 @@ function normalizeReleaseProfileEnv(raw: string | undefined) { ); } -function numericTimerValueMs(valueMs: unknown) { - const value = Number(valueMs); - return Number.isFinite(value) ? Math.floor(value) : undefined; -} - -function resolveDockerSchedulerTimeoutMs( - valueMs: unknown, - fallbackMs: unknown = MAX_TIMER_TIMEOUT_MS, -) { - const value = numericTimerValueMs(valueMs) ?? numericTimerValueMs(fallbackMs); - return Math.min(Math.max(value ?? MAX_TIMER_TIMEOUT_MS, 1), MAX_TIMER_TIMEOUT_MS); -} - function resolveOptionalTimerTimeoutMs(valueMs: unknown) { - const value = numericTimerValueMs(valueMs); - if (value === undefined || value <= 0) { - return undefined; - } - return resolveDockerSchedulerTimeoutMs(value); + return clampPositiveTimerTimeoutMs(Math.floor(Number(valueMs))); } function resourceLimitsSummary(resourceLimits: Record) { @@ -933,7 +919,7 @@ export function runShellCommand({ return new Promise((resolve, reject) => { const resolvedTimeoutMs = resolveOptionalTimerTimeoutMs(timeoutMs); const resolvedNoOutputTimeoutMs = resolveOptionalTimerTimeoutMs(noOutputTimeoutMs); - const resolvedTimeoutKillGraceMs = resolveDockerSchedulerTimeoutMs( + const resolvedTimeoutKillGraceMs = resolveTimerTimeoutMs( timeoutKillGraceMs, SHELL_TIMEOUT_KILL_GRACE_MS, ); @@ -1144,7 +1130,7 @@ export function runShellCaptureCommand({ } return new Promise((resolve, reject) => { const resolvedTimeoutMs = resolveOptionalTimerTimeoutMs(timeoutMs); - const resolvedTimeoutKillGraceMs = resolveDockerSchedulerTimeoutMs( + const resolvedTimeoutKillGraceMs = resolveTimerTimeoutMs( timeoutKillGraceMs, SHELL_TIMEOUT_KILL_GRACE_MS, ); diff --git a/test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts b/test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts index bb40cef9f921..1e001362944c 100644 --- a/test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts +++ b/test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts @@ -812,6 +812,8 @@ describe("package-openclaw-for-docker", () => { it("loads from a trusted harness checkout without installed dependencies", async () => { const tempRoot = tempDirs.make("openclaw-package-harness-"); const copiedFiles = [ + "packages/normalization-core/src/number-coercion.ts", + "packages/normalization-core/src/string-coerce.ts", "scripts/package-openclaw-for-docker.mts", "scripts/package-changelog.mjs", "scripts/package-docs-map.mjs", diff --git a/test/scripts/control-ui-performance-base.test.ts b/test/scripts/control-ui-performance-base.test.ts index ee5bef2216d2..3a081199cc3e 100644 --- a/test/scripts/control-ui-performance-base.test.ts +++ b/test/scripts/control-ui-performance-base.test.ts @@ -57,6 +57,7 @@ it("compares real UI builds with canonical compression and keeps artifacts after "lib/repo-root.mjs", "lib/output-root-guard.mjs", "lib/record-shared.mjs", + "lib/regexp.mjs", ]) { fs.copyFileSync(path.join(repoRoot, "scripts", script), path.join(root, "scripts", script)); } diff --git a/test/scripts/control-ui-performance.test.ts b/test/scripts/control-ui-performance.test.ts index f85bab812b4b..a5f564d2b908 100644 --- a/test/scripts/control-ui-performance.test.ts +++ b/test/scripts/control-ui-performance.test.ts @@ -89,18 +89,15 @@ function createCliFixture(startupCssGzipBytes = 15, deferredCssGzipBytes = 15) { path.resolve("src/gateway/control-ui-route-preloads.ts"), path.join(gatewayDir, "control-ui-route-preloads.ts"), ); - fs.copyFileSync( - path.resolve("scripts/lib/check-limits.mts"), - path.join(scriptLibDir, "check-limits.mts"), - ); - fs.copyFileSync( - path.resolve("scripts/lib/control-ui-i18n-config.ts"), - path.join(scriptLibDir, "control-ui-i18n-config.ts"), - ); - fs.copyFileSync( - path.resolve("scripts/lib/control-ui-i18n-config.json"), - path.join(scriptLibDir, "control-ui-i18n-config.json"), - ); + for (const file of [ + "check-limits.mts", + "control-ui-i18n-config.ts", + "control-ui-i18n-config.json", + "record-shared.mjs", + "regexp.mjs", + ]) { + fs.copyFileSync(path.resolve("scripts/lib", file), path.join(scriptLibDir, file)); + } fs.writeFileSync( path.join(scriptsDir, "tsx.mjs"), `await import(${JSON.stringify(tsxImport)});\n`, diff --git a/test/scripts/kitchen-sink-plugin-assertions.test.ts b/test/scripts/kitchen-sink-plugin-assertions.test.ts index 3952ab6b8f04..fb879a63e2c5 100644 --- a/test/scripts/kitchen-sink-plugin-assertions.test.ts +++ b/test/scripts/kitchen-sink-plugin-assertions.test.ts @@ -1040,7 +1040,7 @@ export KITCHEN_SINK_SPEC=npm:@openclaw/kitchen-sink@0.0.0 source scripts/e2e/lib/kitchen-sink-plugin/sweep.sh run_expect_failure "install/failure" bash -c 'printf "%s\\n" "npm ERR! No matching version @openclaw/kitchen-sink@0.0.0"; exit 1' test -f "$SCRATCH_ROOT/kitchen-sink-expected-failure-install_failure.log" -scan_logs_for_unexpected_errors +node scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs scan-logs `, { HOME_DIR: home, diff --git a/test/scripts/openclaw-cross-os-release-checks.test.ts b/test/scripts/openclaw-cross-os-release-checks.test.ts index 74f90090aa1b..92dbaeb93355 100644 --- a/test/scripts/openclaw-cross-os-release-checks.test.ts +++ b/test/scripts/openclaw-cross-os-release-checks.test.ts @@ -1803,19 +1803,16 @@ describe("scripts/openclaw-cross-os-release-checks", () => { it("rejects a successful packaged update followed by an old self-swapped process import miss", () => { expect(() => - verifyPackagedUpgradeUpdateResult( - { - exitCode: 1, - stdout: JSON.stringify({ - status: "ok", - after: { version: "2026.4.27" }, - steps: [{ name: "global update", exitCode: 0 }], - }), - stderr: - "[openclaw] Failed to start CLI: Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/tmp/prefix/lib/node_modules/openclaw/dist/memory-state-old.js'", - }, - { candidateVersion: "2026.4.27" }, - ), + verifyPackagedUpgradeUpdateResult({ + exitCode: 1, + stdout: JSON.stringify({ + status: "ok", + after: { version: "2026.4.27" }, + steps: [{ name: "global update", exitCode: 0 }], + }), + stderr: + "[openclaw] Failed to start CLI: Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/tmp/prefix/lib/node_modules/openclaw/dist/memory-state-old.js'", + }), ).toThrow(/Packaged upgrade failed/u); }); diff --git a/test/scripts/package-compat.test.ts b/test/scripts/package-compat.test.ts index e94fd4c01b06..16d34bc94630 100644 --- a/test/scripts/package-compat.test.ts +++ b/test/scripts/package-compat.test.ts @@ -102,17 +102,11 @@ openclaw_e2e_fixture_plugin_command openclaw_e2e_maybe_timeout 5s node "$OPENCLA const sweepFixtureLoader = ` source() { if [[ "$1" == scripts/e2e/lib/plugins/fixtures.sh ]]; then - write_fixture_plugin() { mkdir -p "$1"; } - write_demo_fixture_plugin() { write_fixture_plugin "$@"; } - write_fixture_plugin_with_cli() { write_fixture_plugin "$@"; } - write_fixture_plugin_with_vendored_dependency() { write_fixture_plugin "$@"; } - write_claude_bundle_fixture() { write_fixture_plugin "$@"; } pack_fixture_plugin() { :; } pack_fixture_plugin_with_cli_registry_dependency() { :; } pack_fake_is_number_package() { :; } pack_fixture_plugin_with_invalid_extension_entry() { :; } start_npm_fixture_registry() { :; } - record_fixture_plugin_trust() { :; } openclaw_plugins_cleanup_fixture_servers() { :; } else builtin source "$@" @@ -120,7 +114,8 @@ source() { } node() { case "$1" in - scripts/e2e/lib/plugins/assertions.mjs|scripts/e2e/lib/fixture.mjs) return 0 ;; + scripts/e2e/lib/fixture.mjs) mkdir -p "$3" ;; + scripts/e2e/lib/plugins/assertions.mjs) return 0 ;; *) command node "$@" ;; esac } @@ -131,11 +126,6 @@ source scripts/e2e/lib/plugins/sweep.sh const kitchenFixtureLoader = ` export KITCHEN_SINK_SWEEP_SOURCE_ONLY=1 source scripts/e2e/lib/kitchen-sink-plugin/sweep.sh -assert_kitchen_sink_cutover_preinstalled() { :; } -configure_kitchen_sink_runtime() { :; } -assert_kitchen_sink_installed() { :; } -assert_kitchen_sink_removed() { :; } -remove_kitchen_sink_channel_config() { :; } node() { case "$1" in scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs) return 0 ;; diff --git a/test/scripts/plugin-prerelease-test-plan.test.ts b/test/scripts/plugin-prerelease-test-plan.test.ts index 32442939297c..9951723173df 100644 --- a/test/scripts/plugin-prerelease-test-plan.test.ts +++ b/test/scripts/plugin-prerelease-test-plan.test.ts @@ -328,12 +328,12 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => { sweepScript.indexOf("run_success_scenario()"), sweepScript.indexOf("run_failure_scenario()"), ); - expect(successScenario.indexOf('plugins install "${install_args[@]}" --force')).toBeLessThan( - successScenario.indexOf("configure_kitchen_sink_runtime"), - ); - expect(successScenario.indexOf("configure_kitchen_sink_runtime")).toBeLessThan( - successScenario.indexOf('plugins enable "$KITCHEN_SINK_ID"'), - ); + const installIndex = successScenario.indexOf('plugins install "${install_args[@]}" --force'); + const configureIndex = successScenario.indexOf("assertions.mjs configure-runtime"); + const enableIndex = successScenario.indexOf('plugins enable "$KITCHEN_SINK_ID"'); + expect(installIndex).toBeGreaterThanOrEqual(0); + expect(configureIndex).toBeGreaterThan(installIndex); + expect(enableIndex).toBeGreaterThan(configureIndex); expect(successScenario).toContain('plugins inspect "$KITCHEN_SINK_ID" --runtime --json'); expect(successScenario).toContain("plugins inspect --all --runtime --json"); expect(sweepScript).toContain("run_failure_scenario"); @@ -355,7 +355,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => { ); expect(script).toContain("docker_e2e_sample_stats_until_exit"); expect(script).toContain("scripts/e2e/lib/docker-stats/assert-resource-ceiling.mjs"); - expect(sweepScript).toContain("scan_logs_for_unexpected_errors"); + expect(sweepScript).toContain("assertions.mjs scan-logs"); }); it("keeps kitchen-sink RPC coverage package-backed and resource-guarded", () => {