refactor(scripts): deslop shared tooling flows (#162824)

Consolidate script lifecycle and helper ownership while preserving CLI, generated-output, cleanup, and guard contracts. Reject inherited object keys in model-matrix option admission with the existing unknown-argument diagnostic.

Validation: 90 full sibling suites; original-fails parser regression; byte-identical native outputs; changed checks; SDK surface and API comparison; both zero-cycle checks; independent isolated review.
This commit is contained in:
Peter Steinberger 2026-10-01 14:30:19 -07:00 • committed by GitHub
parent 6f56df6b5a
commit 7cbaa6b36f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
71 changed files with 464 additions and 1003 deletions

View file

@ -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",

View file

@ -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("&lt;", "<")
@ -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,

View file

@ -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<string, number>();
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",

View file

@ -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<void>((resolve) => {
resolveAllTurnsStarted = resolve;
});
const { promise: allTurnsStarted, resolve: resolveAllTurnsStarted } = createDeferredCore();
if (!live) {
providerBeforeLoad = readProviderRequestLog(requestLogPath).length;
}

View file

@ -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<BenchmarkCaseId, Benchmar
};
}
return () => {
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<void> {
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 {

View file

@ -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") {

View file

@ -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<string, unknown> {
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<ReturnType<typeof readAssetMetrics>>) {
)[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<string, unknown> = isMetricsRecord(parsed) ? parsed : {};
const record: Record<string, unknown> = isRecord(parsed) ? parsed : {};
const { startupJsGzipBytes, reason, updatedAt } = record;
if (
typeof startupJsGzipBytes !== "number" ||

View file

@ -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));

View file

@ -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;
}

View file

@ -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);

View file

@ -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.`,
);
}

View file

@ -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) {

View file

@ -120,8 +120,6 @@ async function runPluginExtensionImportBoundaryCheck(): Promise<0 | 1> {
return 1;
}
async function main(): Promise<void> {
runAsScript(import.meta.url, async () => {
process.exitCode = await runPluginExtensionImportBoundaryCheck();
}
runAsScript(import.meta.url, main);
});

View file

@ -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<Value>(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,
})),

View file

@ -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;

View file

@ -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<string, CodeModeMatrixCellResult[]>();
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<void> {
}
}
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();
}

View file

@ -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"

View file

@ -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:-}" ] &&

View file

@ -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.

View file

@ -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))"
}

View file

@ -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"
}

View file

@ -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" \

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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)"

View file

@ -362,26 +362,7 @@ class MacosSmoke extends SmokeRunController<MacosOptions> {
}),
);
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<void> {
@ -422,25 +403,29 @@ class MacosSmoke extends SmokeRunController<MacosOptions> {
);
}
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<void> {
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));
}

View file

@ -280,17 +280,7 @@ class WindowsSmoke extends SmokeRunController<WindowsOptions> {
),
}),
);
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<void> {
@ -345,17 +335,21 @@ class WindowsSmoke extends SmokeRunController<WindowsOptions> {
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<void> {
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(

View file

@ -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}"

View file

@ -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.

View file

@ -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=""

View file

@ -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}"

View file

@ -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}"

View file

@ -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"

View file

@ -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}"

View file

@ -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}"

View file

@ -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)"

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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<void> {
for (let i = 0; i < 4; i += 1) {
await new Promise<void>((r) => {
setImmediate(r);
});
await setImmediate();
globalThis.gc?.();
}
await new Promise<void>((r) => {
setTimeout(r, 100);
});
await setTimeout(100);
globalThis.gc?.();
}
@ -276,7 +273,7 @@ async function main(): Promise<void> {
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"))

View file

@ -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 {

View file

@ -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);
}

View file

@ -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}"

View file

@ -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}`;
}

View file

@ -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<string, ModeResult[]>();
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]) => {

View file

@ -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")
);
}

View file

@ -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;
}

View file

@ -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<LaneBaseParams, "logsDir" | "providerConfig"> & {
lane: LaneState;
env: NodeJS.ProcessEnv;
cleanup: Cleanup[];
beforeGatewayStart?: () => Promise<void>;
afterGatewayReady?: () => Promise<void>;
},
) {
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 },
) {

View file

@ -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<Worker>();

View file

@ -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 {

View file

@ -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<string>();
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<T extends PluginGatewayEntry>(
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<string, JsonRecord[]>();
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 (

View file

@ -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;

View file

@ -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<WebSocketRoute>();
let sendRequest: { id: string; params: unknown } | undefined;
let sendRequestCount = 0;
let resolveSend: (() => void) | undefined;
const sent = new Promise<void>((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) => {

View file

@ -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");

View file

@ -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<ReturnType<typeof acquireQaLease>>;
@ -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 ?? "");

View file

@ -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<Error>((resolve) => {
stopForwarding = resolve;
});
const { promise: stopped, resolve: stopForwarding } = createDeferredCore<Error>();
const cancel = () => {
stopForwarding(new Error("Telegram proof forwarding stopped"));
for (const controller of readers) {

View file

@ -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';

View file

@ -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,

View file

@ -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<void> {
await writePackageDistInventoryForPublish(process.cwd());
}
export async function preparePrepackArtifacts(env: NodeJS.ProcessEnv = process.env): Promise<void> {
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),

View file

@ -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<string>((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";

View file

@ -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,

View file

@ -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<typeof setTimeout>;
type ChildKiller = (signal: NodeJS.Signals) => void;
type ProcessTreeChild = Pick<ChildProcess, "exitCode" | "kill" | "pid" | "signalCode">;
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<string>((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;

View file

@ -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<string, number>) {
@ -933,7 +919,7 @@ export function runShellCommand({
return new Promise<ShellCommandResult>((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<ShellCaptureResult>((resolve, reject) => {
const resolvedTimeoutMs = resolveOptionalTimerTimeoutMs(timeoutMs);
const resolvedTimeoutKillGraceMs = resolveDockerSchedulerTimeoutMs(
const resolvedTimeoutKillGraceMs = resolveTimerTimeoutMs(
timeoutKillGraceMs,
SHELL_TIMEOUT_KILL_GRACE_MS,
);

View file

@ -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",

View file

@ -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));
}

View file

@ -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`,

View file

@ -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,

View file

@ -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);
});

View file

@ -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 ;;

View file

@ -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", () => {