diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f84dab5d04d..402f57a2cb91 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1472,6 +1472,12 @@ jobs: echo "Current CI targets must provide the tsgo:scripts package script." >&2 exit 1 fi + if pnpm run --silent 2>/dev/null | grep -q '^ tsgo:test:root$'; then + pnpm tsgo:test:root + elif [[ "$HISTORICAL_TARGET" != "true" ]]; then + echo "Current CI targets must provide the tsgo:test:root package script." >&2 + exit 1 + fi ;; *) echo "Unsupported check task: $TASK" >&2 diff --git a/package.json b/package.json index dff5b23d3833..6cf27dc76eb3 100644 --- a/package.json +++ b/package.json @@ -1963,9 +1963,10 @@ "tsgo:prod": "pnpm tsgo:core && pnpm tsgo:extensions", "tsgo:profile": "node scripts/profile-tsgo.mjs", "tsgo:scripts": "node scripts/run-tsgo.mjs -p tsconfig.scripts.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/scripts.tsbuildinfo", - "tsgo:test": "pnpm tsgo:core:test && pnpm tsgo:extensions:test", + "tsgo:test": "pnpm tsgo:core:test && pnpm tsgo:extensions:test && pnpm tsgo:test:root", "tsgo:test:extensions": "pnpm tsgo:extensions:test", "tsgo:test:packages": "node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.test.packages.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/test-packages.tsbuildinfo", + "tsgo:test:root": "node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.test.root.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/test-root.tsbuildinfo", "tsgo:test:src": "node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.test.src.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/test-src.tsbuildinfo", "tsgo:test:ui": "node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.test.ui.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/test-ui.tsbuildinfo", "tui": "node scripts/run-node.mjs tui", diff --git a/scripts/audit-seams.d.mts b/scripts/audit-seams.d.mts new file mode 100644 index 000000000000..25decf264b5c --- /dev/null +++ b/scripts/audit-seams.d.mts @@ -0,0 +1,11 @@ +#!/usr/bin/env node +export function describeSeamKinds(relativePath: unknown, source: unknown): string[]; +export function determineSeamTestStatus( + seamKinds: unknown, + relatedTestMatches: unknown, +): { + status: string; + reason: string; +}; +export function main(argv?: string[]): Promise; +export const HELP_TEXT: "Usage: node scripts/audit-seams.mjs [--help]\n\nAudit repo seam inventory and emit JSON to stdout.\n\nSections:\n duplicatedSeamFamilies Plugin SDK seam families imported from multiple production files\n overlapFiles Production files that touch multiple seam families\n optionalClusterStaticLeaks Optional extension/plugin clusters referenced from the static graph\n missingPackages Workspace packages whose deps are not mirrored at the root\n seamTestInventory High-signal seam candidates with nearby-test gap signals,\n including cron orchestration seams for agent handoff,\n outbound/media delivery, heartbeat/followup handoff,\n and scheduler state crossings, plus subagent seams\n for spawn/session handoff, announce delivery,\n lifecycle registry, cleanup, and parent streaming\n\nNotes:\n - Output is JSON only.\n - For clean redirected JSON through package scripts, prefer:\n pnpm --silent audit:seams > seam-inventory.json\n"; diff --git a/scripts/bench-test-changed.d.mts b/scripts/bench-test-changed.d.mts new file mode 100644 index 000000000000..8247463c0f9a --- /dev/null +++ b/scripts/bench-test-changed.d.mts @@ -0,0 +1,24 @@ +export function parseArgs(argv: unknown): { + maxWorkers?: unknown; + cwd: string; + mode: unknown; + ref: unknown; + rss: unknown; +}; +export function parseMaxRssBytes(output: unknown): number | null; +export function formatRss(valueBytes: unknown): string; +export function resolveBenchRssResult({ + label, + output, + rss, + status, +}: { + label: unknown; + output: unknown; + rss: unknown; + status: unknown; +}): { + maxRssBytes: number | null; + output: unknown; + status: unknown; +}; diff --git a/scripts/build-all.d.mts b/scripts/build-all.d.mts new file mode 100644 index 000000000000..749e0591944e --- /dev/null +++ b/scripts/build-all.d.mts @@ -0,0 +1,92 @@ +import type fs from "node:fs"; + +export type BuildCacheEntry = + | string + | { + path: string; + extensions?: string[]; + recursive?: boolean; + }; + +export type BuildAllStep = { + label: string; + kind?: "node" | "pnpm"; + args?: string[]; + pnpmArgs?: string[]; + env?: NodeJS.ProcessEnv; + windowsNodeOptions?: string; + cache?: { + env?: string[]; + inputs: BuildCacheEntry[]; + outputs: BuildCacheEntry[]; + restore?: "always"; + }; +}; + +export type BuildAllCacheState = { + cacheable: boolean; + fresh: boolean; + restorable?: boolean; + reason: string; + signature?: string; + outputRoot?: string; + stampPath?: string; + inputFiles?: number; + outputFiles?: number; + relativeOutputFiles?: string[]; + stampedOutputs?: string[]; +}; + +export const BUILD_ALL_STEPS: BuildAllStep[]; +export const BUILD_ALL_PROFILES: Record; +export const BUILD_ALL_PROFILE_STEP_ENV: Record>; + +export function buildAllUsage(): string; +export function parseBuildAllArgs(argv: string[]): { help: boolean; profile: string }; +export function resolveBuildAllSteps(profile?: string): BuildAllStep[]; +export function resolveBuildAllEnvironment( + env?: NodeJS.ProcessEnv, + now?: () => Date, + readGitCommit?: () => string | null, +): { [key: string]: string | undefined; OPENCLAW_BUILD_TIMESTAMP: string }; +export function resolveBuildAllStep( + step: BuildAllStep, + params?: { + platform?: NodeJS.Platform; + env?: NodeJS.ProcessEnv; + nodeExecPath?: string; + npmExecPath?: string; + comSpec?: string; + }, +): { + command: string; + args: string[]; + options: { + stdio: "inherit"; + env: NodeJS.ProcessEnv; + shell?: boolean; + windowsVerbatimArguments?: boolean; + }; +}; +export function resolveBuildAllStepCacheState( + step: BuildAllStep, + params?: { rootDir?: string; fs?: typeof fs; env?: NodeJS.ProcessEnv }, +): BuildAllCacheState; +export function writeBuildAllStepCacheStamp( + step: BuildAllStep, + cacheState: BuildAllCacheState, + params?: { rootDir?: string; fs?: typeof fs }, +): void; +export function resolveBuildAllStepCacheStampState( + step: BuildAllStep, + cacheState: BuildAllCacheState, + params?: { rootDir?: string; fs?: typeof fs }, +): BuildAllCacheState; +export function restoreBuildAllStepCacheOutputs( + cacheState: BuildAllCacheState, + params?: { rootDir?: string; fs?: typeof fs }, +): boolean; +export function formatBuildAllDuration(durationMs: number): string; +export function formatBuildAllTimingSummary( + timings: Array<{ label: string; durationMs: number; status: string }>, +): string; diff --git a/scripts/build-diffs-viewer-runtime.d.mts b/scripts/build-diffs-viewer-runtime.d.mts new file mode 100644 index 000000000000..5d62e4f236ae --- /dev/null +++ b/scripts/build-diffs-viewer-runtime.d.mts @@ -0,0 +1,8 @@ +#!/usr/bin/env node +/** + * Creates the esbuild plugin that neutralizes Pierre diffs' browser side-effect import. + */ +export function createPierreDiffsSideEffectImportPlugin(): { + name: string; + setup(buildContext: unknown): void; +}; diff --git a/scripts/bundled-plugin-assets.d.mts b/scripts/bundled-plugin-assets.d.mts new file mode 100644 index 000000000000..cbbf787512df --- /dev/null +++ b/scripts/bundled-plugin-assets.d.mts @@ -0,0 +1,25 @@ +#!/usr/bin/env node +/** + * Reads bundled plugin asset hook commands for a build or copy phase. + */ +export function readBundledPluginAssetHooks(options?: Record): Promise< + { + aliases: string[]; + command: string; + packageName: unknown; + phase: unknown; + pluginDir: string; + pluginId: string; + }[] +>; +/** + * Runs bundled plugin asset hook commands for the selected phase/plugins. + */ +export function runBundledPluginAssetHooks(options?: Record): Promise; +/** + * Parses `--phase` and repeated `--plugin` flags for asset hook scripts. + */ +export function parseBundledPluginAssetArgs(argv: unknown): { + phase: unknown; + plugins: unknown[]; +}; diff --git a/scripts/changed-lanes.d.mts b/scripts/changed-lanes.d.mts new file mode 100644 index 000000000000..7a5beeca546b --- /dev/null +++ b/scripts/changed-lanes.d.mts @@ -0,0 +1,55 @@ +export type ChangedLane = + | "core" + | "coreTests" + | "extensions" + | "extensionTests" + | "scripts" + | "testRoot" + | "apps" + | "docs" + | "tooling" + | "liveDockerTooling" + | "releaseMetadata" + | "all"; + +export type ChangedLanes = Record; + +export type ChangedLaneResult = { + paths: string[]; + lanes: ChangedLanes; + extensionImpactFromCore: boolean; + docsOnly: boolean; + reasons: string[]; +}; + +export type DetectChangedLanesOptions = { + packageJsonChangeKind?: "liveDockerTooling" | "tooling" | null; +}; + +export function normalizeChangedPath(inputPath: unknown): string; +export function createEmptyChangedLanes(): ChangedLanes; +export function isChangedLaneTestPath(changedPath: string): boolean; +export function detectChangedLanes( + changedPaths: string[], + options?: DetectChangedLanesOptions, +): ChangedLaneResult; +export function detectChangedLanesForPaths(params: { + paths: string[]; + base: string; + head?: string; + staged?: boolean; + mergeHeadFirstParent?: boolean; +}): ChangedLaneResult; +export function listChangedPathsFromGit(params: { + base: string; + head?: string; + includeWorktree?: boolean; + cwd?: string; + mergeHeadFirstParent?: boolean; +}): string[]; +export function listStagedChangedPaths(cwd?: string): string[]; +export function isLiveDockerPackageScriptOnlyChange(before: string, after: string): boolean; +export function isPackageScriptOnlyChange(before: string, after: string): boolean; + +export const LIVE_DOCKER_AUTH_SHELL_TARGETS: string[]; +export const RELEASE_METADATA_PATHS: Set; diff --git a/scripts/changed-lanes.mjs b/scripts/changed-lanes.mjs index f0697d84326b..e0741c363246 100644 --- a/scripts/changed-lanes.mjs +++ b/scripts/changed-lanes.mjs @@ -15,6 +15,8 @@ const EXTENSION_PATH_RE = /^extensions\/[^/]+(?:\/|$)/u; const CORE_PATH_RE = /^(?:src\/|ui\/|packages\/)/u; const SCRIPTS_TYPECHECK_PATH_RE = /^(?:scripts\/.*\.(?:[cm]?ts|[cm]?tsx)|tsconfig\.scripts\.json)$/u; +const TEST_ROOT_TYPECHECK_PATH_RE = + /^(?:test\/(?!fixtures\/).*\.(?:[cm]?ts|[cm]?tsx)|test\/tsconfig\/tsconfig\.test\.root\.json)$/u; const TOOLING_PATH_RE = /^(?:scripts\/|test\/vitest\/|\.github\/|\.vscode\/|config\/|deploy\/|git-hooks\/|Dockerfile\.sandbox(?:-(?:browser|common))?$|Makefile$|docker-setup\.sh$|setup-podman\.sh$|openclaw\.podman\.env$|skills\/pyproject\.toml$|vitest(?:\..+)?\.config\.ts$|tsconfig.*\.json$|\.dockerignore$|\.gitignore$|\.jscpd\.json$|\.npmignore$|\.pre-commit-config\.yaml$|\.swiftformat$|\.swiftlint\.yml$|\.oxlint.*|\.oxfmt.*)/u; const ROOT_GLOBAL_PATH_RE = @@ -56,7 +58,7 @@ export const RELEASE_METADATA_PATHS = new Set([ "package.json", ]); -/** @typedef {"core" | "coreTests" | "extensions" | "extensionTests" | "scripts" | "apps" | "docs" | "tooling" | "liveDockerTooling" | "releaseMetadata" | "all"} ChangedLane */ +/** @typedef {"core" | "coreTests" | "extensions" | "extensionTests" | "scripts" | "testRoot" | "apps" | "docs" | "tooling" | "liveDockerTooling" | "releaseMetadata" | "all"} ChangedLane */ /** * @typedef {{ @@ -88,6 +90,7 @@ export function createEmptyChangedLanes() { extensions: false, extensionTests: false, scripts: false, + testRoot: false, apps: false, docs: false, tooling: false, @@ -145,6 +148,9 @@ export function detectChangedLanes(changedPaths, options = {}) { if (SCRIPTS_TYPECHECK_PATH_RE.test(changedPath)) { lanes.scripts = true; } + if (TEST_ROOT_TYPECHECK_PATH_RE.test(changedPath)) { + lanes.testRoot = true; + } if (DOCS_PATH_RE.test(changedPath)) { lanes.docs = true; @@ -299,12 +305,14 @@ export function listChangedPathsFromGit(params) { let rangePaths; let noMergeBase = false; try { + // oxlint-disable-next-line typescript/no-base-to-string, typescript/restrict-template-expressions -- resolveMergeHeadDiffBase returns a git ref string when present. rangePaths = runGitNameOnlyDiff([`${base}...${head}`], cwd); } catch (error) { if (!isGitNoMergeBaseError(error)) { throw error; } noMergeBase = true; + // oxlint-disable-next-line typescript/no-base-to-string, typescript/restrict-template-expressions -- resolveMergeHeadDiffBase returns a git ref string when present. rangePaths = runGitNameOnlyDiff([`${base}..${head}`], cwd); } if (params.includeWorktree === false) { diff --git a/scripts/check-architecture-smells.d.mts b/scripts/check-architecture-smells.d.mts new file mode 100644 index 000000000000..477d5ac5a77e --- /dev/null +++ b/scripts/check-architecture-smells.d.mts @@ -0,0 +1,18 @@ +#!/usr/bin/env node +/** + * Collects architecture smell findings from the configured source roots. + */ +export function collectArchitectureSmells(): Promise< + Array<{ + category: string; + file: string; + line: number; + kind: string; + specifier: string; + reason: string; + }> +>; +/** + * Runs the architecture smell check and writes human/JSON output. + */ +export function main(argv: unknown, io: unknown): Promise; diff --git a/scripts/check-changed.d.mts b/scripts/check-changed.d.mts new file mode 100644 index 000000000000..31353b3725d8 --- /dev/null +++ b/scripts/check-changed.d.mts @@ -0,0 +1,69 @@ +import type { ChangedLaneResult } from "./changed-lanes.mjs"; + +export type ChangedCheckCommand = { + name: string; + args: string[]; + bin?: string; + env?: NodeJS.ProcessEnv; +}; + +export type ChangedCheckPlan = { + commands: ChangedCheckCommand[]; + summary: string; +}; + +export type ChangedCheckPlanOptions = { + env?: NodeJS.ProcessEnv; + staged?: boolean; + base?: string; + head?: string; + platform?: NodeJS.Platform; + swiftlintAvailable?: boolean; +}; + +export type TargetedLintOptions = { + fileExists?: (path: string) => boolean; +}; + +export type TargetedLintCommand = Required< + Pick +>; + +export function createChangedCheckChildEnv(baseEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv; +export function shouldDelegateChangedCheckToCrabbox( + argv?: string[], + env?: NodeJS.ProcessEnv, +): boolean; +export function buildChangedCheckCrabboxArgs(argv?: string[], options?: { cwd?: string }): string[]; +export function shouldRunShrinkwrapGuard(paths: string[]): boolean; +export function shouldRunPromptSnapshotCheck(paths: string[]): boolean; +export function shouldRunPromptSnapshotOwnerTest(paths: string[]): boolean; +export function shouldRunRuntimeSidecarBaselineCheck(paths: string[]): boolean; +export function shouldRunCanvasA2uiNativeResourceCheck(paths: string[]): boolean; +export function shouldRunAppcastOwnerTest(paths: string[]): boolean; +export function shouldRunTestTempCreationReport(paths: string[]): boolean; +export function createShrinkwrapGuardCommand(paths: string[]): ChangedCheckCommand | null; +export function createChangedCheckPlan( + result: ChangedLaneResult, + options?: ChangedCheckPlanOptions, +): ChangedCheckPlan; +export function createTargetedCoreLintCommand( + paths: string[], + env?: NodeJS.ProcessEnv, + options?: TargetedLintOptions, +): TargetedLintCommand | null; +export function createTargetedExtensionLintCommand( + paths: string[], + env?: NodeJS.ProcessEnv, + options?: TargetedLintOptions, +): TargetedLintCommand | null; +export function createTargetedScriptLintCommand( + paths: string[], + env?: NodeJS.ProcessEnv, + options?: TargetedLintOptions, +): TargetedLintCommand | null; +export function createPnpmManagedCommand( + command: T, + env?: NodeJS.ProcessEnv, +): T & { bin: string; env: NodeJS.ProcessEnv }; +export function cleanupCorepackPnpmShimDir(): void; diff --git a/scripts/check-changed.mjs b/scripts/check-changed.mjs index 73f46fc8c318..c3e3b6c94d2c 100644 --- a/scripts/check-changed.mjs +++ b/scripts/check-changed.mjs @@ -394,6 +394,9 @@ export function createChangedCheckPlan(result, options = {}) { if (lanes.scripts) { addTypecheck("typecheck scripts", ["tsgo:scripts"]); } + if (lanes.testRoot) { + addTypecheck("typecheck test root", ["tsgo:test:root"]); + } if (lanes.core || lanes.coreTests) { const coreLintCommand = createTargetedCoreLintCommand(result.paths, baseEnv); diff --git a/scripts/check-changelog-attributions.d.mts b/scripts/check-changelog-attributions.d.mts new file mode 100644 index 000000000000..5716c4751a7a --- /dev/null +++ b/scripts/check-changelog-attributions.d.mts @@ -0,0 +1,20 @@ +#!/usr/bin/env node +/** + * Reports whether a handle is forbidden in changelog thanks text. + */ +export function isForbiddenChangelogThanksHandle( + handle: unknown, + options?: Record, +): boolean; +/** + * Reports whether a handle needs a separate human credit. + */ +export function requiresExplicitHumanChangelogThanks(handle: unknown): boolean; +/** + * Finds changelog lines that thank forbidden handles. + */ +export function findForbiddenChangelogThanks(content: unknown): unknown; +/** + * Runs the changelog attribution check. + */ +export function main(argv?: string[]): Promise; diff --git a/scripts/check-channel-agnostic-boundaries.d.mts b/scripts/check-channel-agnostic-boundaries.d.mts new file mode 100644 index 000000000000..d80ab4bb8e95 --- /dev/null +++ b/scripts/check-channel-agnostic-boundaries.d.mts @@ -0,0 +1,31 @@ +#!/usr/bin/env node +/** + * Finds channel-specific references inside channel-agnostic protected sources. + */ +export function findChannelAgnosticBoundaryViolations( + content: unknown, + fileName?: string, + options?: Record, +): unknown[]; +/** + * Finds reverse dependencies from channel core into plugin/runtime surfaces. + */ +export function findChannelCoreReverseDependencyViolations( + content: unknown, + fileName?: string, +): unknown[]; +/** + * Finds user-facing channel names in ACP-owned text sources. + */ +export function findAcpUserFacingChannelNameViolations( + content: unknown, + fileName?: string, +): unknown[]; +/** + * Finds raw system mark literals where shared constants should be used. + */ +export function findSystemMarkLiteralViolations(content: unknown, fileName?: string): unknown[]; +/** + * Runs all channel-agnostic boundary checks. + */ +export function main(): Promise; diff --git a/scripts/check-cli-startup-memory.d.mts b/scripts/check-cli-startup-memory.d.mts new file mode 100644 index 000000000000..e42900a6e7c5 --- /dev/null +++ b/scripts/check-cli-startup-memory.d.mts @@ -0,0 +1,71 @@ +#!/usr/bin/env node +export namespace testing { + export { cases }; + export { nodeImportSpecifierForPath }; + export { parseArgs }; + export { readPositiveIntEnv }; + export { readPositiveNumberEnv }; + export { repoRoot }; + export { resolveDefaultLimitsMb }; + export { runCase }; + export { runStartupMemoryCheck }; +} +declare const cases: { + id: string; + label: string; + args: string[]; + limitMb: unknown; +}[]; +declare function nodeImportSpecifierForPath(filePath: unknown): string; +declare function parseArgs(argv: unknown): { + jsonPath: string; + summaryPath: string; +}; +declare function readPositiveIntEnv( + name: unknown, + fallback: unknown, + env?: NodeJS.ProcessEnv, +): unknown; +declare function readPositiveNumberEnv( + name: unknown, + fallback: unknown, + env?: NodeJS.ProcessEnv, +): unknown; +declare const repoRoot: string; +declare function resolveDefaultLimitsMb(platform?: NodeJS.Platform): { + help: number; + pluginsList: number; + statusJson: number; + gatewayStatus: number; +}; +declare function runCase( + testCase: unknown, + params?: Record, +): { + id: unknown; + label: unknown; + command: string; + limitMb: unknown; + maxRssMb: number | null; + status: string; + exitCode: unknown; + signal: unknown; + error: string | null; +}; +declare function runStartupMemoryCheck( + argv?: string[], + params?: Record, +): { + skipped: boolean; + results: { + id: unknown; + label: unknown; + command: string; + limitMb: unknown; + maxRssMb: number | null; + status: string; + exitCode: unknown; + signal: unknown; + error: string | null; + }[]; +}; diff --git a/scripts/check-database-first-legacy-stores.d.mts b/scripts/check-database-first-legacy-stores.d.mts new file mode 100644 index 000000000000..10303f23e7cb --- /dev/null +++ b/scripts/check-database-first-legacy-stores.d.mts @@ -0,0 +1,19 @@ +#!/usr/bin/env node +export function collectDatabaseFirstLegacyStoreSourceFiles( + sourceRoots: string[], +): Promise; +/** + * Finds database-first legacy-store violations in one TypeScript/JavaScript source file. + */ +export function collectDatabaseFirstLegacyStoreViolations( + content: string, + relativePath?: string, + scanOptions?: Record, +): { + kind: string; + line: number; +}[]; +/** + * Runs the database-first legacy-store guard. + */ +export function main(): Promise; diff --git a/scripts/check-deadcode-unused-files.d.mts b/scripts/check-deadcode-unused-files.d.mts new file mode 100644 index 000000000000..1ea9e0c91aac --- /dev/null +++ b/scripts/check-deadcode-unused-files.d.mts @@ -0,0 +1,47 @@ +#!/usr/bin/env node +/** + * Parses compact knip output into unused file paths. + */ +export function parseKnipCompactUnusedFiles(output: unknown): unknown[]; +/** + * Compares detected unused files against the checked-in allowlist. + */ +export function compareUnusedFilesToAllowlist( + actualFiles: unknown, + allowlistFiles: unknown, + optionalAllowlistFiles?: unknown[], +): { + actual: unknown[]; + allowed: unknown[]; + unexpected: unknown[]; + stale: unknown[]; + duplicateAllowedCount: number; + allowlistIsSorted: boolean; +}; +/** + * Runs knip and returns parsed unused-file results. + */ +export function runKnipUnusedFiles(params?: Record): Promise; +/** + * Checks detected unused files against the current allowlist. + */ +export function checkUnusedFiles( + output: unknown, + allowlistFiles?: string[], + optionalAllowlistFiles?: string[], +): { + ok: boolean; + comparison: { + actual: unknown[]; + allowed: unknown[]; + unexpected: unknown[]; + stale: unknown[]; + duplicateAllowedCount: number; + allowlistIsSorted: boolean; + }; + message: string; +}; +/** + * Maximum buffered knip output retained for diagnostics. + */ +export const KNIP_MAX_BUFFER_BYTES: number; diff --git a/scripts/check-dependency-pins.d.mts b/scripts/check-dependency-pins.d.mts new file mode 100644 index 000000000000..85675b5f3798 --- /dev/null +++ b/scripts/check-dependency-pins.d.mts @@ -0,0 +1,9 @@ +#!/usr/bin/env node +/** + * Collects dependency pin violations for the current workspace. + */ +export function collectDependencyPinViolations(cwd?: string): unknown[]; +/** + * Runs the dependency pin check. + */ +export function main(): Promise; diff --git a/scripts/check-docs-i18n-glossary.d.mts b/scripts/check-docs-i18n-glossary.d.mts new file mode 100644 index 000000000000..25527eb5d9ae --- /dev/null +++ b/scripts/check-docs-i18n-glossary.d.mts @@ -0,0 +1,11 @@ +#!/usr/bin/env node +export function parseArgs(argv: unknown): { + base: string; + head: string; +}; +export type TermMatch = { + file: string; + line: number; + kind: "title" | "link label"; + term: string; +}; diff --git a/scripts/check-docs-mdx.d.mts b/scripts/check-docs-mdx.d.mts new file mode 100644 index 000000000000..3d86b954af63 --- /dev/null +++ b/scripts/check-docs-mdx.d.mts @@ -0,0 +1,9 @@ +#!/usr/bin/env node +/** + * Parses docs MDX check arguments. + */ +export function parseArgs(argv: unknown): { + roots: unknown[]; + jsonOut: string; + maxErrors: number; +}; diff --git a/scripts/check-dynamic-import-warts.d.mts b/scripts/check-dynamic-import-warts.d.mts new file mode 100644 index 000000000000..1516826d26fc --- /dev/null +++ b/scripts/check-dynamic-import-warts.d.mts @@ -0,0 +1,9 @@ +#!/usr/bin/env node +/** + * Finds dynamic import advisories in a single source file. + */ +export function findDynamicImportAdvisories(content: unknown, fileName?: string): unknown[]; +/** + * Runs the dynamic import advisory check. + */ +export function main(argv?: string[]): Promise; diff --git a/scripts/check-extension-package-tsc-boundary.d.mts b/scripts/check-extension-package-tsc-boundary.d.mts new file mode 100644 index 000000000000..aba5d6b473e6 --- /dev/null +++ b/scripts/check-extension-package-tsc-boundary.d.mts @@ -0,0 +1,89 @@ +#!/usr/bin/env node +/** + * Resolves the compile worker count from CLI/env/default settings. + */ +export function resolveCompileConcurrency( + env?: NodeJS.ProcessEnv, + availableParallelism?: number, +): number; +/** + * Appends child-process output while preserving only the diagnostic tail. + */ +export function appendBoundedStepOutput( + buffer: unknown, + chunk: unknown, + maxChars?: number, +): { + text: string; + truncatedChars: unknown; +}; +/** + * Formats the successful boundary compile summary. + */ +export function formatBoundaryCheckSuccessSummary(params?: Record): string; +/** + * Formats skipped compile progress for fresh extension canaries. + */ +export function formatSkippedCompileProgress(params?: Record): string; +/** + * Formats slow extension compile diagnostics. + */ +export function formatSlowCompileSummary(params?: Record): string; +/** + * Formats a failed boundary-check child process step. + */ +export function formatStepFailure(label: unknown, params?: Record): string; +/** + * Checks whether an extension boundary compile canary is still fresh. + */ +export function isBoundaryCompileFresh( + extensionId: unknown, + params?: Record, +): boolean; +/** + * Runs one node-based boundary check step with timeout and output capture. + */ +export function runNodeStepAsync( + label: unknown, + args: unknown, + timeoutMs: unknown, + params?: Record, +): Promise; +/** + * Runs boundary check steps with bounded concurrency. + */ +export function runNodeStepsWithConcurrency(steps: unknown, concurrency: unknown): Promise; +/** + * Resolves canary artifact paths for an extension boundary compile. + */ +export function resolveCanaryArtifactPaths( + extensionId: unknown, + rootDir?: string, +): { + extensionRoot: string; + canaryPath: string; + tsconfigPath: string; +}; +/** + * Removes canary artifacts for multiple extensions. + */ +export function cleanupCanaryArtifactsForExtensions(extensionIds: unknown, rootDir?: string): void; +/** + * Installs signal/exit cleanup for extension canary artifacts. + */ +export function installCanaryArtifactCleanup( + extensionIds: unknown, + params?: Record, +): () => void; +/** + * Resolves the local lock path for extension boundary checks. + */ +export function resolveBoundaryCheckLockPath(rootDir?: string): string; +/** + * Acquires the single-process lock for extension boundary checks. + */ +export function acquireBoundaryCheckLock(params?: Record): () => void; +/** + * Runs the extension package TypeScript boundary check. + */ +export function main(argv?: string[]): Promise; diff --git a/scripts/check-extension-plugin-sdk-boundary.d.mts b/scripts/check-extension-plugin-sdk-boundary.d.mts new file mode 100644 index 000000000000..3b3e48fe788d --- /dev/null +++ b/scripts/check-extension-plugin-sdk-boundary.d.mts @@ -0,0 +1,19 @@ +#!/usr/bin/env node +/** + * Reads the checked-in expected boundary inventory. + */ +export function readExpectedInventory(mode: unknown): Promise; +/** + * Diffs expected and actual boundary inventory entries. + */ +export function diffInventory( + expected: unknown, + actual: unknown, +): { + missing: unknown; + unexpected: unknown; +}; +/** + * Entrypoint wrapper for the extension plugin SDK boundary check. + */ +export function main(argv: unknown, io: unknown): Promise<1 | 0>; diff --git a/scripts/check-extension-wildcard-reexports.d.mts b/scripts/check-extension-wildcard-reexports.d.mts new file mode 100644 index 000000000000..3b81fc700d87 --- /dev/null +++ b/scripts/check-extension-wildcard-reexports.d.mts @@ -0,0 +1,9 @@ +#!/usr/bin/env node +/** + * Finds local wildcard re-export lines in a barrel source string. + */ +export function findLocalWildcardReexports(source: unknown): unknown; +/** + * Runs the extension wildcard re-export guard. + */ +export function main(argv?: string[], io?: NodeJS.Process): Promise<0 | 1>; diff --git a/scripts/check-gateway-cpu-scenarios.d.mts b/scripts/check-gateway-cpu-scenarios.d.mts new file mode 100644 index 000000000000..fe70ebc3b9b4 --- /dev/null +++ b/scripts/check-gateway-cpu-scenarios.d.mts @@ -0,0 +1,88 @@ +#!/usr/bin/env node +export namespace testing { + export { hasPrivateQaDist }; + export { parseArgs }; + export { readStartupReport }; + export { runGatewayCpuScenarios }; + export { validateStartupReport }; +} +declare function hasPrivateQaDist(repoRoot: unknown, fsImpl?: typeof fs): boolean; +declare function parseArgs(argv: string[]): { + outputDir: string; + startupCases: string[]; + qaScenarios: string[]; + runs: number; + warmup: number; + skipStartup: boolean; + skipQa: boolean; + cpuCoreWarn: number; + hotWallWarnMs: number; +}; +declare function readStartupReport(startupOutput: unknown): + | { + diagnosticFailure: string; + diagnosticDetail: string; + report: null; + } + | { + diagnosticFailure: null; + diagnosticDetail: null; + report: unknown; + }; +declare function runGatewayCpuScenarios( + options: unknown, + params?: Record, +): Promise<{ + exitCode: number; + summary: { + options: { + startupCases: unknown; + qaScenarios: unknown; + runs: unknown; + warmup: unknown; + cpuCoreWarn: unknown; + hotWallWarnMs: unknown; + qaStateDir: string | null; + }; + steps: { + error?: unknown; + name: unknown; + status: unknown; + signal: unknown; + }[]; + observations: ( + | { + kind: string; + id: unknown; + cpuCoreRatioMax: number; + wallMsMax: number; + cpuCoreRatio?: undefined; + wallMs?: undefined; + } + | { + kind: string; + id: string; + cpuCoreRatio: number; + wallMs: number; + cpuCoreRatioMax?: undefined; + wallMsMax?: undefined; + } + )[]; + qaSummaryFailure?: string | undefined; + qaSummaryFailureDetail?: string | null | undefined; + startupReportFailure?: string | undefined; + startupReportFailureDetail?: string | null | undefined; + generatedAt: string; + outputDir: unknown; + startupOutput: string | null; + qaSummary: string | null; + }; +}>; +declare function validateStartupReport( + report: unknown, +): + | "startup report must be a JSON Record" + | "startup report missing results array" + | "startup report has no measured results" + | null; +import fs from "node:fs"; diff --git a/scripts/check-gateway-watch-regression.d.mts b/scripts/check-gateway-watch-regression.d.mts new file mode 100644 index 000000000000..4fcb16722173 --- /dev/null +++ b/scripts/check-gateway-watch-regression.d.mts @@ -0,0 +1,156 @@ +#!/usr/bin/env node +/** + * Appends watch output while preserving only the diagnostic tail. + */ +export function appendBoundedWatchLog( + current: unknown, + chunk: unknown, + maxChars?: number, +): { + text: string; + truncated: boolean; +}; +/** + * Updates bounded watch-build detection state from new output. + */ +export function updateWatchBuildDetection( + state: unknown, + chunk: unknown, +): { + buffer: string; + triggered: unknown; + reason: unknown; +}; +/** + * Parses a safe non-negative integer CLI value. + */ +export function readNonNegativeInteger(value: unknown, label: unknown): number; +/** + * Parses gateway watch regression CLI arguments. + */ +export function parseArgs(argv: unknown): { + outputDir: string; + windowMs: number; + readyTimeoutMs: number; + readySettleMs: number; + sigkillGraceMs: number; + sigkillExitGraceMs: number; + cpuWarnMs: number; + cpuFailMs: number; + distRuntimeFileGrowthMax: number; + distRuntimeByteGrowthMax: number; + keepLogs: boolean; + skipBuild: boolean; +}; +/** + * Reports whether gateway watch output contains a ready marker. + */ +export function hasGatewayReadyLog(text: unknown): boolean; +export function resolveTimedWatchShell(deps?: Record): string; +export function buildTimedWatchCommand( + pidFilePath: unknown, + timeFilePath: unknown, + isolatedHomeDir: unknown, + port: unknown, + deps?: Record, +): + | { + command: string; + args: unknown[]; + env: { + OPENCLAW_DISABLE_BONJOUR: string; + OPENCLAW_SKIP_ACPX_RUNTIME: string; + OPENCLAW_SKIP_ACPX_RUNTIME_PROBE: string; + OPENCLAW_SKIP_BROWSER_CONTROL_SERVER: string; + OPENCLAW_SKIP_CANVAS_HOST: string; + OPENCLAW_SKIP_CHANNELS: string; + OPENCLAW_SKIP_CRON: string; + OPENCLAW_SKIP_GMAIL_WATCHER: string; + OPENCLAW_RUNTIME_POSTBUILD_STATIC_ASSETS: string; + OPENCLAW_TEST_MINIMAL_GATEWAY: string; + NODE_ENV: string; + OPENCLAW_WATCH_PID_FILE: unknown; + HOME: unknown; + OPENCLAW_HOME: unknown; + OPENCLAW_CONFIG_PATH: string; + OPENCLAW_STATE_DIR: string; + PATH: string; + XDG_CONFIG_HOME: string; + }; + } + | { + command: unknown; + args: string[]; + env: { + OPENCLAW_DISABLE_BONJOUR: string; + OPENCLAW_SKIP_ACPX_RUNTIME: string; + OPENCLAW_SKIP_ACPX_RUNTIME_PROBE: string; + OPENCLAW_SKIP_BROWSER_CONTROL_SERVER: string; + OPENCLAW_SKIP_CANVAS_HOST: string; + OPENCLAW_SKIP_CHANNELS: string; + OPENCLAW_SKIP_CRON: string; + OPENCLAW_SKIP_GMAIL_WATCHER: string; + OPENCLAW_RUNTIME_POSTBUILD_STATIC_ASSETS: string; + OPENCLAW_TEST_MINIMAL_GATEWAY: string; + NODE_ENV: string; + OPENCLAW_WATCH_PID_FILE: unknown; + HOME: unknown; + OPENCLAW_HOME: unknown; + OPENCLAW_CONFIG_PATH: string; + OPENCLAW_STATE_DIR: string; + PATH: string; + XDG_CONFIG_HOME: string; + }; + }; +/** + * Runs a bounded gateway watch process and captures timing/log artifacts. + */ +export function runTimedWatch( + options: unknown, + outputDir: unknown, + deps?: Record, +): Promise<{ + exit: unknown; + spawnError: unknown; + timingFileMissing: boolean; + timing: unknown; + readyBeforeWindow: boolean; + exitedBeforeReady: boolean; + exitedBeforeStop: boolean; + idleCpuMs: number | null; + stdoutPath: string; + stderrPath: string; + timeFilePath: string; + watchTriggeredBuild: boolean; + watchBuildReason: string | null; +}>; +/** + * Stops the timed watch child process with TERM/KILL fallback. + */ +export function stopTimedWatchChild( + child: unknown, + watchPid: unknown, + options: unknown, + deps?: Record, +): Promise; +/** + * Reports whether restored CI artifacts need fresh build stamps. + */ +export function shouldRefreshBuildStampForRestoredArtifacts(params: unknown): boolean; +/** + * Writes build and runtime-postbuild stamps for the current artifact set. + */ +export function writeBuildAndRuntimePostBuildStamps(params?: Record): void; +/** + * Collects pass/fail findings for the bounded gateway watch regression run. + */ +export function collectGatewayWatchFindings(params: unknown): { + failures: string[]; + warnings: string[]; +}; +export function shouldReportDuplicateDistRuntimeRegression(failures: unknown): unknown; +/** + * Maximum retained stdout/stderr text for gateway watch diagnostics. + */ +export const WATCH_LOG_CAPTURE_MAX_CHARS: number; +export const WATCH_LOG_FAILURE_TAIL_CHARS: 12000; diff --git a/scripts/check-memory-fd-repro.d.mts b/scripts/check-memory-fd-repro.d.mts new file mode 100644 index 000000000000..af84587803c5 --- /dev/null +++ b/scripts/check-memory-fd-repro.d.mts @@ -0,0 +1,192 @@ +#!/usr/bin/env node +/** + * Parses a safe non-negative integer option. + */ +export function readNumber(value: unknown, label: unknown): number; +/** + * Parses a safe positive integer option. + */ +export function readPositiveNumber(value: unknown, label: unknown): number; +/** + * Parses memory FD repro CLI arguments and environment fallbacks. + */ +export function parseArgs(argv: string[]): { + fileCount: number; + mode: "fixed" | "leak" | "report"; + maxWorkspaceRegFds: number; + minLeakedFds: number; + invokeTimeoutMs: number; + sampleDelayMs: number; + settleDelayMs: number; + outputDir: string; + keep: boolean; + allowNonDarwin: boolean; +}; +/** + * Writes isolated OpenClaw config for the synthetic memory workspace. + */ +export function writeConfig({ + homeDir, + workspaceDir, + port, + token, +}: { + homeDir: unknown; + workspaceDir: unknown; + port: unknown; + token: unknown; +}): string; +/** + * Updates bounded gateway-ready output state from a stdout/stderr chunk. + */ +export function updateGatewayReadyOutputState( + state: unknown, + chunk: unknown, + maxChars?: number, +): { + tail: string; + readySeen: boolean; +}; +/** + * Reports whether a spawned child has already exited. + */ +export function hasChildExited(child: unknown): boolean; +/** + * Waits until gateway output and listener state both indicate readiness. + */ +export function waitForGatewayReady({ + child, + port, + logPath, + timeoutMs, +}: { + child: unknown; + port: unknown; + logPath: unknown; + timeoutMs: unknown; +}): Promise; +/** + * Stops the gateway child using the default process/runtime hooks. + */ +export function stopGateway({ child, port }: { child: unknown; port: unknown }): Promise; +/** + * Stops the gateway child and unknown remaining listener process. + */ +export function stopGatewayWithRuntime({ + child, + childExitPollIntervalMs, + childExitPolls, + port, + findGatewayPidFn, + killProcess, + listenerSettleDelayMs, +}: { + child: unknown; + childExitPollIntervalMs?: number | undefined; + childExitPolls?: number | undefined; + port: unknown; + findGatewayPidFn: unknown; + killProcess: unknown; + listenerSettleDelayMs?: number | undefined; +}): Promise; +/** + * Classifies the memory_search HTTP response into success/error details. + */ +export function classifyMemorySearchInvokeResponse({ + httpOk, + status, + bodyText, +}: { + httpOk: unknown; + status: unknown; + bodyText: unknown; +}): + | { + ok: boolean; + httpOk: unknown; + status: unknown; + gatewayOk: boolean | undefined; + error: string; + } + | { + ok: boolean; + httpOk: unknown; + status: unknown; + error: string; + gatewayOk?: undefined; + } + | { + error?: string | undefined; + toolError?: string | undefined; + ok: boolean; + httpOk: unknown; + status: unknown; + gatewayOk: boolean | undefined; + resultCount: unknown; + toolDisabled: boolean; + toolUnavailable: boolean; + }; +export function invokeMemorySearch({ + port, + token, + timeoutMs, +}: { + port: unknown; + token: unknown; + timeoutMs: unknown; +}): Promise< + | { + durationMs: number; + bodyPreview: string; + ok: boolean; + httpOk: unknown; + status: unknown; + gatewayOk: boolean | undefined; + error: string; + aborted?: undefined; + } + | { + durationMs: number; + bodyPreview: string; + ok: boolean; + httpOk: unknown; + status: unknown; + error: string; + gatewayOk?: undefined; + aborted?: undefined; + } + | { + durationMs: number; + bodyPreview: string; + error?: string | undefined; + toolError?: string | undefined; + ok: boolean; + httpOk: unknown; + status: unknown; + gatewayOk: boolean | undefined; + resultCount: unknown; + toolDisabled: boolean; + toolUnavailable: boolean; + aborted?: undefined; + } + | { + ok: boolean; + aborted: boolean; + durationMs: number; + error: string; + } +>; +/** + * Maximum gateway-ready output tail retained while waiting for startup. + */ +export const GATEWAY_READY_OUTPUT_MAX_CHARS: number; +/** + * Maximum bytes read from the memory_search HTTP response. + */ +export const MEMORY_SEARCH_RESPONSE_MAX_BYTES: number; +/** + * Probe query expected to hit the synthetic top-level memory file. + */ +export const MEMORY_SEARCH_PROBE_QUERY: "Top-level memory file"; +export { readBoundedResponseText }; +import { readBoundedResponseText } from "./lib/bounded-response.mjs"; diff --git a/scripts/check-no-conflict-markers.d.mts b/scripts/check-no-conflict-markers.d.mts new file mode 100644 index 000000000000..ad1ceb752ca8 --- /dev/null +++ b/scripts/check-no-conflict-markers.d.mts @@ -0,0 +1,31 @@ +#!/usr/bin/env node +/** + * Returns one-based line numbers containing merge conflict markers. + */ +export function findConflictMarkerLines(content: unknown): unknown[]; +/** + * Lists tracked files in the repository. + */ +export function listTrackedFiles(cwd?: string): string[]; +/** + * Scans files for merge conflict markers, skipping binary content. + */ +export function findConflictMarkersInFiles( + filePaths: unknown, + readFile?: typeof fs.readFileSync, +): { + filePath: unknown; + lines: unknown[]; +}[]; +/** + * Finds merge conflict markers in tracked repository files. + */ +export function findConflictMarkersInTrackedFiles(cwd?: string): { + filePath: unknown; + lines: unknown[]; +}[]; +/** + * Runs the merge conflict marker check. + */ +export function main(): Promise; +import fs from "node:fs"; diff --git a/scripts/check-no-random-messaging-tmp.d.mts b/scripts/check-no-random-messaging-tmp.d.mts new file mode 100644 index 000000000000..f8f3604edd19 --- /dev/null +++ b/scripts/check-no-random-messaging-tmp.d.mts @@ -0,0 +1,13 @@ +#!/usr/bin/env node +/** + * Finds `os.tmpdir()` or imported `tmpdir()` call lines in source. + */ +export function findMessagingTmpdirCallLines(content: unknown, fileName?: string): unknown[]; +/** + * Runs the messaging tmpdir guard. + */ +export function main(): Promise; +/** + * Source roots scanned for unsafe messaging tmpdir usage. + */ +export const messagingTmpdirGuardSourceRoots: string[]; diff --git a/scripts/check-no-raw-window-open.d.mts b/scripts/check-no-raw-window-open.d.mts new file mode 100644 index 000000000000..c7dc15e9d141 --- /dev/null +++ b/scripts/check-no-raw-window-open.d.mts @@ -0,0 +1,9 @@ +#!/usr/bin/env node +/** + * Finds raw `window.open(...)` or `globalThis.open(...)` call lines. + */ +export function findRawWindowOpenLines(content: unknown, fileName?: string): unknown[]; +/** + * Runs the raw window.open guard. + */ +export function main(): Promise; diff --git a/scripts/check-package-patches.d.mts b/scripts/check-package-patches.d.mts new file mode 100644 index 000000000000..1c65f47d3dbc --- /dev/null +++ b/scripts/check-package-patches.d.mts @@ -0,0 +1,9 @@ +#!/usr/bin/env node +/** + * Collects disallowed package patch declarations and patch files. + */ +export function collectPackagePatchViolations(cwd?: string): unknown[]; +/** + * Runs the package patch guard. + */ +export function main(): Promise; diff --git a/scripts/check-plugin-extension-import-boundary.d.mts b/scripts/check-plugin-extension-import-boundary.d.mts new file mode 100644 index 000000000000..3ef786a3ee64 --- /dev/null +++ b/scripts/check-plugin-extension-import-boundary.d.mts @@ -0,0 +1,39 @@ +#!/usr/bin/env node +/** + * Diffs expected and actual plugin-extension boundary inventory entries. + */ +export function diffInventory( + expected: unknown, + actual: unknown, +): { + missing: unknown; + unexpected: unknown; +}; +/** + * Entrypoint wrapper for the plugin-extension import boundary check. + */ +export function main(argv: unknown, io: unknown): Promise<1 | 0>; +/** + * Cached inventory of src/plugins imports that cross into bundled extensions. + */ +export const collectPluginExtensionImportBoundaryInventory: () => Promise< + Array<{ + file: string; + line: number; + kind: string; + specifier: string; + reason: string; + }> +>; +/** + * Cached expected plugin-extension import inventory baseline. + */ +export const readExpectedInventory: () => Promise< + Array<{ + file: string; + line: number; + kind: string; + specifier: string; + reason: string; + }> +>; diff --git a/scripts/check-plugin-gateway-gauntlet.d.mts b/scripts/check-plugin-gateway-gauntlet.d.mts new file mode 100644 index 000000000000..542e2debadd4 --- /dev/null +++ b/scripts/check-plugin-gateway-gauntlet.d.mts @@ -0,0 +1,97 @@ +#!/usr/bin/env node +/** + * Parses plugin gateway gauntlet CLI arguments and env defaults. + */ +export function parseArgs(argv: string[]): { + repoRoot: string; + outputDir: string; + pluginIds: string[]; + shardTotal: number; + shardIndex: number; + limit: number | undefined; + skipPrebuild: boolean; + skipLifecycle: boolean; + skipQa: boolean; + qaBaseline: boolean; + skipSlashHelp: boolean; + qaScenarios: string[]; + qaPluginChunkSize: number; + cpuCoreWarn: number; + hotWallWarnMs: number; + maxRssWarnMb: number; + wallAnomalyMultiplier: number; + rssAnomalyMultiplier: number; + qaCpuRegressionMultiplier: number; + qaWallRegressionMultiplier: number; + commandTimeoutMs: number; + buildTimeoutMs: number; + qaTimeoutMs: number; + allowEmpty: boolean; + failOnObservation: boolean; + keepRunRoot: boolean; +}; +export function buildObservationGuardFailures(observations: unknown, enabled?: boolean): unknown; +/** + * Builds the command that prepares QA runtime artifacts before gauntlet probes. + */ +export function createGauntletPrebuildCommand(repoRoot: unknown): { + command: string; + args: string[]; +}; +/** + * Converts an output path to a repo-relative path, rejecting paths outside the repo. + */ +export function toRepoRelativePath(repoRoot: unknown, absolutePath: unknown): string; +/** + * Parses `/usr/bin/time` output into wall, CPU, and RSS metrics. + */ +export function parseTimedMetrics( + stderr: unknown, + wallMs: unknown, + mode: unknown, +): { + wallMs: unknown; + cpuMs: number | null; + cpuCoreRatio: number | null; + maxRssMb: number | null; +}; +/** + * Runs a measured command through the live process implementation. + */ +export type GauntletMeasuredRow = { + diagnosticFailure?: string; + logPath: string | null; + logWriteError?: string; + spawnError?: { code?: string }; + status: number; + timedOut: boolean; + wallMs: number; +}; +export type GauntletMeasuredCommandParams = { + [key: string]: unknown; + args: string[]; + command: string; + consoleOutputMaxBytes?: number; + cwd: string; + env: NodeJS.ProcessEnv; + label: string; + logDir: string; + maxBufferBytes?: number; + phase: string; + timeoutKillGraceMs?: number; + timeoutMs: number; + timeMode?: string; +}; +export function runMeasuredCommand( + params: GauntletMeasuredCommandParams, +): Promise; +/** + * Runs one command with optional timing wrapper, bounded output, and log capture. + */ +export function runMeasuredCommandLive( + params: GauntletMeasuredCommandParams, +): Promise; +/** + * Reports whether gauntlet result rows contain work beyond the prebuild step. + */ +export function hasGauntletWorkRows(rows: unknown): unknown; diff --git a/scripts/check-plugin-npm-runtime-builds.d.mts b/scripts/check-plugin-npm-runtime-builds.d.mts new file mode 100644 index 000000000000..7d490468d807 --- /dev/null +++ b/scripts/check-plugin-npm-runtime-builds.d.mts @@ -0,0 +1,21 @@ +#!/usr/bin/env node +export function parseArgs(argv: unknown): + | { + help: true; + packageDirs: string[]; + } + | { + packageDirs: string[]; + help?: undefined; + }; +/** + * Builds publishable plugin npm runtimes and verifies declared outputs exist. + */ +export function checkPluginNpmRuntimeBuilds(params?: Record): Promise< + { + pluginDir: string; + status: string; + entryCount: number; + copiedStaticAssets: string[]; + }[] +>; diff --git a/scripts/check-plugin-sdk-wildcard-reexports.d.mts b/scripts/check-plugin-sdk-wildcard-reexports.d.mts new file mode 100644 index 000000000000..38c946be1fd6 --- /dev/null +++ b/scripts/check-plugin-sdk-wildcard-reexports.d.mts @@ -0,0 +1,9 @@ +#!/usr/bin/env node +/** + * Finds wildcard plugin SDK re-export lines in an extension API barrel. + */ +export function findPluginSdkWildcardReexports(source: unknown): unknown; +/** + * Runs the plugin SDK wildcard re-export guard. + */ +export function main(argv?: string[], io?: NodeJS.Process): Promise<0 | 1>; diff --git a/scripts/check-protocol-event-coverage.d.mts b/scripts/check-protocol-event-coverage.d.mts new file mode 100644 index 000000000000..9c5f271308a6 --- /dev/null +++ b/scripts/check-protocol-event-coverage.d.mts @@ -0,0 +1,36 @@ +#!/usr/bin/env node +/** + * Extracts the gateway event name list from server-methods-list.ts source. + * Bare identifiers in the array (e.g. GATEWAY_EVENT_UPDATE_AVAILABLE) are + * resolved against constantsSource (src/gateway/events.ts). + */ +export function extractGatewayEventNames(listSource: unknown, constantsSource: unknown): string[]; +/** + * Extracts qualified static string constants declared at Swift type scope. + * Type qualification avoids resolving unrelated constants that share a short + * member name elsewhere in the app. + */ +export function extractSwiftStaticStringConstants(source: unknown): Map; +/** Extracts Swift gateway-event case labels, including qualified constants. */ +export function extractSwiftHandledEvents( + source: unknown, + constants?: Map, +): Set; +/** + * Extracts event names a Kotlin source handles: string-literal case labels of + * `when (event)` blocks plus `event == "..."` comparisons, both scoped to + * `fun handle*Event(...)` bodies. Scoping matters: bare `event == "..."` + * literals also appear in predicate helpers that are not called from the + * dispatch path (e.g. gatewayEventInvalidatesNodesDevices in NodeRuntime.kt), + * and counting those would silently mark events as covered. Swift extraction + * stays tree-wide because Swift consumption always reads `.event` off a + * received EventFrame, which does not have that false-positive shape. + */ +export function extractKotlinHandledEvents(source: unknown): Set; +/** + * Compares a client's handled events against the gateway catalog and its + * allowlist. Returns human-readable error strings. Client-only names (e.g. the + * client-synthesized "seqGap" pseudo-event) are intentionally ignored; this + * check only guards the server->client direction. + */ +export function compareEventCoverage(params: unknown): string[]; diff --git a/scripts/check-release-metadata-only.d.mts b/scripts/check-release-metadata-only.d.mts new file mode 100644 index 000000000000..ef8b24ea9a15 --- /dev/null +++ b/scripts/check-release-metadata-only.d.mts @@ -0,0 +1,8 @@ +#!/usr/bin/env node +export function parseArgs(argv: string[]): { + staged: boolean; + base: string; + head: string; + paths: string[]; +}; +export function main(argv?: string[]): void; diff --git a/scripts/check-runtime-sidecar-loaders.d.mts b/scripts/check-runtime-sidecar-loaders.d.mts new file mode 100644 index 000000000000..59a6310da1a3 --- /dev/null +++ b/scripts/check-runtime-sidecar-loaders.d.mts @@ -0,0 +1,14 @@ +export type RuntimeSidecarLoaderViolation = { + line: number; + specifier: string; + sourcePath: string; + reason: string; +}; +export function collectTsdownEntrySources( + config: Array<{ entry?: Record }>, +): Set; +export function findRuntimeSidecarLoaderViolations( + content: string, + importerPath: string, + explicitEntrySources: Set, +): RuntimeSidecarLoaderViolation[]; diff --git a/scripts/check-sdk-package-extension-import-boundary.d.mts b/scripts/check-sdk-package-extension-import-boundary.d.mts new file mode 100644 index 000000000000..6006c0abcdc4 --- /dev/null +++ b/scripts/check-sdk-package-extension-import-boundary.d.mts @@ -0,0 +1,5 @@ +#!/usr/bin/env node +/** + * Entrypoint for the SDK-package extension import boundary checker. + */ +export const main: (argv: unknown, io: unknown) => Promise<1 | 0>; diff --git a/scripts/check-session-accessor-boundary.d.mts b/scripts/check-session-accessor-boundary.d.mts new file mode 100644 index 000000000000..236c570a2828 --- /dev/null +++ b/scripts/check-session-accessor-boundary.d.mts @@ -0,0 +1,73 @@ +#!/usr/bin/env node +export function collectSessionStoreRuntimeFileBackedCompatExports( + content: unknown, + fileName?: string, +): Map; +export function findSessionStoreRuntimeFileBackedCompatExportViolations( + content: unknown, + fileName?: string, +): { + line: unknown; + reason: string; +}[]; +export function findSessionAccessorBoundaryViolations( + content: unknown, + fileName?: string, +): unknown[]; +export function findEmbeddedAgentSessionTargetViolations( + content: unknown, + fileName?: string, +): unknown[]; +export function findSessionAccessorWriteBoundaryViolations( + content: unknown, + fileName?: string, +): unknown[]; +export function findTranscriptWriterBoundaryViolations( + content: unknown, + fileName?: string, +): unknown[]; +export function findGatewaySessionCreateLifecycleViolations( + content: unknown, + fileName?: string, +): unknown[]; +export function findSessionCompactManualTrimBoundaryViolations( + content: unknown, + fileName?: string, +): unknown[]; +export function findSessionLifecycleCleanupBoundaryViolations( + content: unknown, + fileName?: string, +): unknown[]; +export function findMemoryHostSessionCorpusBoundaryViolations( + content: unknown, + fileName?: string, +): unknown[]; +/** Ratchet compare: counts above baseline are regressions, below are improvements. */ +export function compareSessionAccessorDebt( + currentCounts: unknown, + baselineCounts: unknown, +): { + regressions: { + concern: string; + path: string; + currentCount: unknown; + baselineCount: unknown; + }[]; + improvements: { + concern: string; + path: string; + currentCount: unknown; + baselineCount: unknown; + }[]; +}; +export function formatSessionAccessorDebtImprovements(improvements: unknown): unknown[]; +export function main(): Promise; +export const allowedSessionStoreRuntimeFileBackedCompatExports: Set; +export const migratedSessionAccessorFiles: Set; +export const migratedBundledPluginSessionAccessorFiles: Set; +export const migratedEmbeddedAgentSessionTargetFiles: Set; +export const migratedSessionAccessorWriteFiles: Set; +export const migratedTranscriptWriterFiles: Set; +export const migratedSessionCompactManualTrimFiles: Set; +export const migratedSessionLifecycleCleanupFiles: Set; +export const migratedMemoryHostSessionCorpusFiles: Set; diff --git a/scripts/check-session-transcript-reader-boundary.d.mts b/scripts/check-session-transcript-reader-boundary.d.mts new file mode 100644 index 000000000000..c789240f3996 --- /dev/null +++ b/scripts/check-session-transcript-reader-boundary.d.mts @@ -0,0 +1,7 @@ +#!/usr/bin/env node +export function findSessionTranscriptReaderBoundaryViolations( + content: unknown, + fileName?: string, +): unknown[]; +export function main(): Promise; +export const migratedSessionTranscriptReaderFiles: Set; diff --git a/scripts/check-src-extension-import-boundary.d.mts b/scripts/check-src-extension-import-boundary.d.mts new file mode 100644 index 000000000000..83ec8dc074b6 --- /dev/null +++ b/scripts/check-src-extension-import-boundary.d.mts @@ -0,0 +1,5 @@ +#!/usr/bin/env node +/** + * Entrypoint for the src extension import boundary checker. + */ +export const main: (argv: unknown, io: unknown) => Promise<1 | 0>; diff --git a/scripts/check-test-helper-extension-import-boundary.d.mts b/scripts/check-test-helper-extension-import-boundary.d.mts new file mode 100644 index 000000000000..6a07ef4f91ce --- /dev/null +++ b/scripts/check-test-helper-extension-import-boundary.d.mts @@ -0,0 +1,9 @@ +#!/usr/bin/env node +/** + * Collects test-helper extension import boundary inventory. + */ +export const collectTestHelperExtensionImportBoundaryInventory: () => Promise; +/** + * Entrypoint for the test-helper extension import boundary checker. + */ +export const main: (argv: unknown, io: unknown) => Promise<1 | 0>; diff --git a/scripts/check-web-fetch-provider-boundaries.d.mts b/scripts/check-web-fetch-provider-boundaries.d.mts new file mode 100644 index 000000000000..17f357b1a7a3 --- /dev/null +++ b/scripts/check-web-fetch-provider-boundaries.d.mts @@ -0,0 +1,16 @@ +#!/usr/bin/env node +/** + * Collects web-fetch provider boundary violations in core source files. + */ +export function collectWebFetchProviderBoundaryViolations(): Promise< + Array<{ + file: string; + line: number; + provider: string; + reason: string; + }> +>; +/** + * Runs the web-fetch provider boundary check. + */ +export function main(argv: unknown, io: unknown): Promise<1 | 0>; diff --git a/scripts/check-web-search-provider-boundaries.d.mts b/scripts/check-web-search-provider-boundaries.d.mts new file mode 100644 index 000000000000..f9305d7b6613 --- /dev/null +++ b/scripts/check-web-search-provider-boundaries.d.mts @@ -0,0 +1,37 @@ +#!/usr/bin/env node +/** + * Collects web-search provider boundary inventory from core source files. + */ +export function collectWebSearchProviderBoundaryInventory(): Promise< + Array<{ + file: string; + line: number; + provider: string; + reason: string; + }> +>; +/** + * Reads the expected web-search provider boundary inventory baseline. + */ +export function readExpectedInventory(): Promise< + Array<{ + file: string; + line: number; + provider: string; + reason: string; + }> +>; +/** + * Diffs expected and actual web-search provider boundary inventory entries. + */ +export function diffInventory( + expected: unknown, + actual: unknown, +): { + missing: unknown; + unexpected: unknown; +}; +/** + * Entrypoint wrapper for the web-search provider boundary check. + */ +export function main(argv: unknown, io: unknown): Promise<1 | 0>; diff --git a/scripts/check.d.mts b/scripts/check.d.mts new file mode 100644 index 000000000000..5244285a67ce --- /dev/null +++ b/scripts/check.d.mts @@ -0,0 +1,19 @@ +/** + * Returns command usage text for the aggregate check runner. + */ +export function usage(): string; +/** + * Runs selected repository check lanes. + */ +export function main(argv?: string[]): Promise; +/** + * Runs one managed check command and returns timing/status details. + */ +export function runCommand( + command: unknown, + runManagedCommandImpl?: (options: { args: string[]; bin: string }) => Promise, +): Promise<{ + name: unknown; + durationMs: number; + status: number; +}>; diff --git a/scripts/check.mjs b/scripts/check.mjs index 9a7fd4f1eea7..f5d99bb23642 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -119,6 +119,7 @@ export async function main(argv = process.argv.slice(2)) { : [ { name: "typecheck prod", args: ["tsgo:prod"] }, { name: "typecheck scripts", args: ["tsgo:scripts"] }, + { name: "typecheck test root", args: ["tsgo:test:root"] }, ], }, { diff --git a/scripts/ci-run-timings.d.mts b/scripts/ci-run-timings.d.mts new file mode 100644 index 000000000000..4057fcda16a4 --- /dev/null +++ b/scripts/ci-run-timings.d.mts @@ -0,0 +1,55 @@ +#!/usr/bin/env node +export function isRetryableGhJsonErrorMessage(message: unknown): boolean; +/** + * Flattens paginated GitHub run job responses. + */ +export function collectRunJobsFromPages( + pages: Array<{ jobs?: Array> }>, +): Array>; +/** + * Summarizes longest jobs and total timing for a workflow run. + */ +export function summarizeRunTimings( + run: unknown, + limit?: number, +): { + byDuration: Array<{ name: string; durationSeconds: number }>; + byQueue: Array<{ name: string; queueSeconds: number }>; + conclusion: unknown; + status: unknown; + wallSeconds: number | null; + badJobs: unknown; +}; +/** + * Summarizes pnpm store warmup overlap near run start. + */ +export function summarizePnpmStoreWarmupBarrier( + run: unknown, + windowSeconds?: number, +): { + activePostWarmupJobCount: unknown; + firstPostWarmupStartDelaySeconds: number | null; + postWarmupP95StartDelaySeconds: unknown; + postWarmupStartedWithinWindow: unknown; + preflightToWarmupCompleteSeconds: number | null; + preflightToWarmupStartSeconds: number | null; + warmupDurationSeconds: number | null; + warmupResult: string; + windowSeconds: number; +} | null; +/** + * Selects the latest main push CI run, optionally matching a head SHA. + */ +export function selectLatestMainPushCiRun( + runs: Array>, + headSha?: string | null, +): Record | null; +/** + * Parses CI run timing CLI arguments. + */ +export function parseRunTimingArgs(args: unknown): { + explicitRunId: unknown; + limit: number; + recentLimit: number | null; + useLatestMain: boolean; +}; diff --git a/scripts/close-duplicate-prs-after-merge.d.mts b/scripts/close-duplicate-prs-after-merge.d.mts new file mode 100644 index 000000000000..ba10fb850370 --- /dev/null +++ b/scripts/close-duplicate-prs-after-merge.d.mts @@ -0,0 +1,66 @@ +/** + * Parses comma-separated PR numbers from CLI/env input. + */ +export function parsePrNumberList(value: unknown): number[]; +/** + * Parses duplicate PR close workflow arguments. + */ +export function parseArgs( + argv: string[], + env?: NodeJS.ProcessEnv, +): + | { + apply: boolean; + duplicates: number[]; + help: true; + labels: string[]; + landedPr?: number; + repo: string; + } + | { + apply: boolean; + duplicates: number[]; + help?: undefined; + labels: string[]; + landedPr: number; + repo: string; + }; +/** + * Parses changed hunk ranges from unified diff text. + */ +export function parseUnifiedDiffRanges(diffText: unknown): Map; +/** + * Builds the close/skip plan for duplicate PR candidates. + */ +export function buildDuplicateClosePlan({ + candidates, + diffs, + landed, + repo, +}: { + candidates: unknown; + diffs: unknown; + landed: unknown; + repo: unknown; +}): Array>; +/** + * Applies labels/comments/closes for planned duplicate PR actions. + */ +export function applyClosePlan({ + labels, + plan, + repo, + runGh, +}: { + labels?: string[] | undefined; + plan: unknown; + repo: unknown; + runGh: unknown; +}): void; +/** + * Runs the duplicate PR close workflow. + */ +export function runDuplicateCloseWorkflow( + args: unknown, + runGh?: (args: string[], options?: Record) => string, +): Array>; diff --git a/scripts/crabbox-wrapper-providers.d.mts b/scripts/crabbox-wrapper-providers.d.mts new file mode 100644 index 000000000000..3f5bbb7fed3e --- /dev/null +++ b/scripts/crabbox-wrapper-providers.d.mts @@ -0,0 +1,3 @@ +export function canonicalProviderName(provider: unknown): unknown; +export function parseProvidersFromHelp(text: unknown): unknown[]; +export function isProviderAdvertised(provider: unknown, advertisedProviders: unknown): unknown; diff --git a/scripts/dependency-changes-report.d.mts b/scripts/dependency-changes-report.d.mts new file mode 100644 index 000000000000..1f358f4814a9 --- /dev/null +++ b/scripts/dependency-changes-report.d.mts @@ -0,0 +1,76 @@ +#!/usr/bin/env node +/** + * Creates a structured dependency diff report from base/head payloads. + */ +export function createDependencyChangesReport({ + basePayload, + headPayload, + dependencyFileChanges, + baseLabel, + headLabel, + generatedAt, +}: { + basePayload: unknown; + headPayload: unknown; + dependencyFileChanges?: + | Array<{ status: string; path: string; oldPath: string | null }> + | undefined; + baseLabel?: string | undefined; + headLabel?: string | undefined; + generatedAt?: string | undefined; +}): { + generatedAt: string; + baseLabel: string; + headLabel: string; + summary: { + basePackages: number; + headPackages: number; + addedPackages: number; + removedPackages: number; + changedPackages: number; + dependencyFileChanges: number; + }; + dependencyFileChanges: unknown[]; + addedPackages: { + packageName: string; + versions: unknown[]; + }[]; + removedPackages: { + packageName: string; + versions: unknown[]; + }[]; + changedPackages: { + packageName: string; + addedVersions: unknown[]; + removedVersions: unknown[]; + }[]; +}; +/** + * Reports whether a path is a dependency-related file. + */ +export function isDependencyFile(filePath: unknown): boolean; +/** + * Returns git pathspecs used for dependency diff collection. + */ +export function dependencyDiffPathspecs(): string[]; +export function parseArgs(argv: string[]): + | { + rootDir: string; + baseRef: string; + baseLockfile: null; + headLockfile: string; + jsonPath: string | null; + markdownPath: string | null; + } + | { + rootDir: string; + baseRef: null; + baseLockfile: string; + headLockfile: string; + jsonPath: string | null; + markdownPath: string | null; + }; +/** + * Runs the dependency changes report CLI. + */ +export function main(argv?: string[]): Promise; diff --git a/scripts/dependency-ownership-surface-report.d.mts b/scripts/dependency-ownership-surface-report.d.mts new file mode 100644 index 000000000000..a324c1db1f73 --- /dev/null +++ b/scripts/dependency-ownership-surface-report.d.mts @@ -0,0 +1,90 @@ +#!/usr/bin/env node +/** + * Extracts the package name from a pnpm lockfile package key. + */ +export function packageNameFromLockKey(lockKey: unknown): unknown; +/** + * Collects dependency ownership and transitive surface metadata. + */ +export function collectDependencyOwnershipSurfaceReport(params?: Record): { + schemaVersion: number; + generatedAt: string; + target: { + packageName: unknown; + packageVersion: unknown; + gitBranch: string | null; + gitCommit: string | null; + lockfile: string; + ownershipMetadata: string; + }; + summary: { + importerCount: number; + lockfilePackageCount: number; + rootDirectDependencyCount: number; + rootClosurePackageCount: number; + rootOwnershipRecordCount: number; + buildRiskPackageCount: number; + }; + ownershipGaps: string[]; + staleOwnershipRecords: string[]; + ownershipWarnings: { + name: string; + owner: unknown; + sourceSections: unknown[]; + message: string; + }[]; + buildRiskPackages: { + name: unknown; + lockKey: string; + requiresBuild: boolean; + hasBin: boolean; + platformRestricted: boolean; + }[]; + topRootDependencyCones: { + name: string; + specifier: unknown; + section: string; + resolved: string; + owner: unknown; + class: unknown; + risk: unknown; + sourceCategory: string | null; + sourceSections: unknown[]; + sourceFileCount: number; + closureSize: number; + missingSnapshotKeys: unknown[]; + }[]; + rootDependencies: { + name: string; + specifier: unknown; + section: string; + resolved: string; + owner: unknown; + class: unknown; + risk: unknown; + sourceCategory: string | null; + sourceSections: unknown[]; + sourceFileCount: number; + closureSize: number; + missingSnapshotKeys: unknown[]; + }[]; + importerClosures: { + importer: string; + directDependencyCount: number; + closureSize: number; + }[]; +}; +/** + * Collects policy errors from a dependency ownership surface report. + */ +export function collectDependencyOwnershipSurfaceCheckErrors(report: unknown): unknown; +/** + * Renders a dependency ownership surface report as Markdown. + */ +export function renderDependencyOwnershipSurfaceMarkdownReport(report: unknown): string; +export function parseArgs(argv: string[]): { + asJson: boolean; + check: boolean; + jsonPath: string | null; + markdownPath: string | null; +}; diff --git a/scripts/dependency-vulnerability-gate.d.mts b/scripts/dependency-vulnerability-gate.d.mts new file mode 100644 index 000000000000..3867cba0da23 --- /dev/null +++ b/scripts/dependency-vulnerability-gate.d.mts @@ -0,0 +1,60 @@ +#!/usr/bin/env node +/** + * Classifies npm advisory findings into report-only findings and hard blockers. + */ +export type VulnerabilityAdvisory = { + id: string; + severity: string; + title: string; + url: string; + vulnerable_versions: string; +}; +export type VulnerabilityFinding = VulnerabilityAdvisory & { + malware: boolean; + packageName: string; + production: boolean; +}; +export function classifyVulnerabilityFindings({ + allAdvisories, + productionAdvisories, +}: { + allAdvisories: Record; + productionAdvisories: Record; +}): { + blockers: VulnerabilityFinding[]; + findings: VulnerabilityFinding[]; +}; +/** + * Runs the dependency vulnerability gate against pnpm-lock.yaml. + */ +export function runDependencyVulnerabilityGate({ + rootDir, + fetchImpl, +}?: { + rootDir?: string | undefined; + fetchImpl?: typeof fetch | undefined; +}): Promise<{ + blockers: VulnerabilityFinding[]; + findings: VulnerabilityFinding[]; + generatedAt: string; + policy: { + blocks: string[]; + reports: string[]; + vulnerabilityExceptions: boolean; + }; + graphs: { + all: { + packages: number; + packageVersions: unknown; + }; + production: { + packages: number; + packageVersions: unknown; + }; + }; +}>; +/** + * Renders the dependency vulnerability gate report as Markdown. + */ +export function renderDependencyVulnerabilityGateMarkdownReport(report: unknown): string; +export function main(argv?: string[]): Promise<1 | 0>; diff --git a/scripts/docs-sync-publish.d.mts b/scripts/docs-sync-publish.d.mts new file mode 100644 index 000000000000..655b1e780b7a --- /dev/null +++ b/scripts/docs-sync-publish.d.mts @@ -0,0 +1,25 @@ +#!/usr/bin/env node +export function parseArgs(argv: unknown): { + target: string; + sourceRepo: string; + sourceSha: string; + clawhubRepo: string; + clawhubSourceRepo: string; + clawhubSourceSha: string; +}; +/** + * Resolves the local ClawHub repository path used for docs mirroring. + */ +export function resolveClawHubRepoPath(value?: string, options?: Record): string; +/** + * Mirrors ClawHub docs into the target docs tree. + */ +export function syncClawHubDocsTree( + targetDocsDir: unknown, + options?: Record, +): { + repository: unknown; + sha: unknown; + path: string; + files: number; +}; diff --git a/scripts/e2e/cron-mcp-cleanup-docker-client.ts b/scripts/e2e/cron-mcp-cleanup-docker-client.ts index 3f71930a5d5f..db5b1f8eacc1 100644 --- a/scripts/e2e/cron-mcp-cleanup-docker-client.ts +++ b/scripts/e2e/cron-mcp-cleanup-docker-client.ts @@ -162,7 +162,9 @@ async function runCronCleanupScenario(params: { gateway: GatewayRpcClient; pidPath: string; }): Promise<{ jobId: string; runId?: string; pid: number; status?: unknown }> { - const { assert, waitFor } = await loadMcpChannelsHarness(); + const harness = await loadMcpChannelsHarness(); + const assert: McpChannelsHarness["assert"] = harness.assert; + const { waitFor } = harness; const { gateway, pidPath } = params; const job = await gateway.request("cron.add", { name: "cron mcp cleanup docker e2e", @@ -245,7 +247,8 @@ async function runSubagentCleanupScenario(params: { pidsPath: string; exitPath: string; }): Promise<{ runId: string; exitedPids: number[]; pids: number[] }> { - const { assert } = await loadMcpChannelsHarness(); + const harness = await loadMcpChannelsHarness(); + const assert: McpChannelsHarness["assert"] = harness.assert; const { gateway, pidPath, pidsPath, exitPath } = params; await resetProbeFiles({ pidPath, pidsPath, exitPath }); @@ -295,7 +298,9 @@ async function runSubagentCleanupScenario(params: { } async function main() { - const { assert, connectGateway } = await loadMcpChannelsHarness(); + const harness = await loadMcpChannelsHarness(); + const assert: McpChannelsHarness["assert"] = harness.assert; + const { connectGateway } = harness; const gatewayUrl = process.env.GW_URL?.trim(); const gatewayToken = process.env.GW_TOKEN?.trim(); const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || path.join(os.homedir(), ".openclaw"); diff --git a/scripts/e2e/kitchen-sink-rpc-walk.d.mts b/scripts/e2e/kitchen-sink-rpc-walk.d.mts new file mode 100644 index 000000000000..a5df6b1bb35c --- /dev/null +++ b/scripts/e2e/kitchen-sink-rpc-walk.d.mts @@ -0,0 +1,195 @@ +export function shouldPrintHelp(argv: string[]): boolean; +export function validateCliArgs(argv: string[]): void; +export function readPositiveInt(raw: string | undefined, fallback: number, label?: string): number; +export function readPositiveTimerMs( + raw: string | undefined, + fallback: number, + label?: string, +): number; +export function resolveKitchenSinkRpcConfig(env?: NodeJS.ProcessEnv): { + commandMaxRssMiB: number; + commandTimeoutMs: number; + fetchBodyMaxBytes: number; + fetchTimeoutMs: number; + installTimeoutMs: number; + maxRssMiB: number; + outputCaptureChars: number; + readyTimeoutMs: number; + rpcTimeoutMs: number; +}; +export function resolveKitchenSinkRpcPort( + env?: NodeJS.ProcessEnv, + options?: { findAvailablePort?: () => Promise }, +): Promise; +export function makeEnv(): { + root: string; + env: { + HOME: string; + USERPROFILE: string; + OPENCLAW_HOME: string; + OPENCLAW_STATE_DIR: string; + OPENCLAW_CONFIG_PATH: string; + OPENCLAW_NO_ONBOARD: string; + OPENCLAW_SKIP_PROVIDERS: string; + OPENCLAW_KITCHEN_SINK_PERSONALITY: string; + }; +}; +export function cleanupKitchenSinkEnv( + root: string, + options?: { + attempts?: number; + delayMs?: number; + throwOnFailure?: boolean; + warn?: boolean; + }, +): Promise; +export function appendBoundedOutput( + buffer: { text: string; truncatedChars: number }, + chunk: string | Uint8Array, + maxChars?: number, +): { + text: string; + truncatedChars: number; +}; +export function runCommand( + command: string, + args: string[], + options?: Record, +): Promise<{ + stderr: string; + stdout: string; + stderrTruncatedChars: number; + stdoutTruncatedChars: number; +}>; +export function signalProcessGroup( + child: { pid?: number; kill(signal?: NodeJS.Signals): unknown }, + signal: NodeJS.Signals, + { + platform, + runTaskkill, + useProcessGroup, + }?: { + platform?: NodeJS.Platform | undefined; + runTaskkill?: + | (( + command: string, + args: string[], + options: { stdio: "ignore" }, + ) => { error?: Error; status: number | null }) + | undefined; + useProcessGroup?: boolean | undefined; + }, +): void; +export function parseJsonOutput(stdout: string): Record; +export function parseGatewayCliRequestFailure(error: unknown): + | (Error & { + retryAfterMs?: number; + retryable: boolean; + details?: Record; + name: string; + gatewayCode: string; + }) + | null; +export function unwrapRpcPayload(raw: unknown): Record; +export function createRpcCliRunOptions( + method: string, + options?: { env?: NodeJS.ProcessEnv; commandResourceOptions?: Record }, +): Record & { resourceLabel: string; timeoutMs: number }; +export function findDistCallGatewayModuleFiles(cwd?: string): string[]; +export function usesBuiltOpenClawEntry( + runner: unknown, + cwd?: string, + env?: NodeJS.ProcessEnv, +): boolean; +export function fetchJson( + url: unknown, + options?: Record, +): Promise<{ + ok: unknown; + status: unknown; + body: unknown; +}>; +export function readBoundedResponseText( + response: unknown, + byteLimit: number, + timeoutPromise?: Promise, +): Promise; +export function stopGateway(child: unknown, options?: Record): Promise; +export function hasChildExited(child: unknown): boolean; +export function signalGateway( + child: unknown, + signal: unknown, + killProcess?: typeof defaultKillProcess, + options?: Record, +): boolean; +export function createGatewayReadyLogScanner(logPath: unknown, marker?: string): () => boolean; +export function waitForGatewayReady( + child: unknown, + port: unknown, + logPath: unknown, + options?: Record, +): Promise; +export function extractPluginCommandNames(payload: unknown): unknown[]; +export function assertExpectedKitchenSinkToolEntries( + entries: unknown, + label: unknown, + { + requirePluginProvenance, + }?: { + requirePluginProvenance?: boolean | undefined; + }, +): unknown; +export function assertChannelAccountRunning(payload: unknown): unknown; +export function extractTtsProviderIds(payload: unknown, surface: unknown): unknown[]; +export function assertTtsProviderCoverage(payload: unknown, surface: unknown): void; +export function assertKitchenSinkSearchInvokeResult(payload: unknown): void; +export function assertKitchenSinkTextInvokeResult(payload: unknown): void; +export function assertKitchenSinkImageJobInvokeResult(payload: unknown): void; +export function listKitchenSinkToolInvokeNames(): string[]; +export function listKitchenSinkReadOnlyRpcProbeNames(): string[]; +export function listKitchenSinkAuthorizationRpcProbeNames(): string[]; +export function assertOperatorRpcDenied(probe: unknown, call: unknown): Promise; +export function assertCreatedKitchenSinkSession(payload: unknown, expectedKey?: string): unknown; +export function assertKitchenSinkUiDescriptors( + payload: unknown, + options?: Record, +): unknown; +export function assertDiagnosticStabilityClean(payload: unknown): void; +export function assertGatewayHealthPayload(payload: unknown): void; +export function assertGatewayStatusPayload(payload: unknown): void; +export function sampleProcess( + pid: unknown, + options?: Record, +): Promise<{ + aggregateRssMiB: number; + rssMiB: number; + cpuPercent: unknown; + processId: unknown; +} | null>; +export function summarizeProcessSamples(samples: unknown): unknown; +export function sampleWindowsProcessByPort( + port: unknown, + options?: Record, +): Promise< + | { + rssMiB: number; + aggregateRssMiB: number; + cpuPercent: null; + cpuSeconds: number | null; + processId: number; + } + | { + rssMiB: number; + cpuPercent: null; + cpuSeconds: null; + processId: number; + } + | null +>; +export function assertResourceCeiling(sample: unknown): void; +export function assertCommandResourceCeiling(sample: unknown): void; +export function findErrorLogFindings(logPath: string): Array<{ line: string; lineNumber: number }>; +export function tailFile(file: string, maxBytes?: number): string; +export function main(): Promise; +export const MAX_KITCHEN_SINK_TIMER_TIMEOUT_MS: 2147000000; +declare function defaultKillProcess(pid: unknown, signal: unknown): true; diff --git a/scripts/e2e/lib/agent-turn-output.d.mts b/scripts/e2e/lib/agent-turn-output.d.mts new file mode 100644 index 000000000000..b0fc22de4fc7 --- /dev/null +++ b/scripts/e2e/lib/agent-turn-output.d.mts @@ -0,0 +1,3 @@ +export function extractAgentReplyTexts(text: string): string[]; +export function assertAgentReplyContainsMarker(marker: string, outputPath: string): void; +export function assertOpenAiRequestLogUsed(requestLogPath: string, label?: string): void; diff --git a/scripts/e2e/lib/bounded-response-text.d.mts b/scripts/e2e/lib/bounded-response-text.d.mts new file mode 100644 index 000000000000..576616efcf0b --- /dev/null +++ b/scripts/e2e/lib/bounded-response-text.d.mts @@ -0,0 +1,6 @@ +export function readBoundedResponseText( + response: unknown, + label: unknown, + byteLimit: unknown, + timeoutPromise?: Promise, +): Promise; diff --git a/scripts/e2e/lib/codex-install-utils.d.mts b/scripts/e2e/lib/codex-install-utils.d.mts new file mode 100644 index 000000000000..2467507163d6 --- /dev/null +++ b/scripts/e2e/lib/codex-install-utils.d.mts @@ -0,0 +1,14 @@ +export function stateDir(): string; +export function configPath(): string; +export function managedNpmRoot(): string; +export function realPathMaybe(filePath: string): string; +export function assertPathInside(parentPath: string, childPath: string, label: string): void; +import type { PluginInstallRecord } from "./plugin-index-sqlite.mjs"; + +export function readInstallRecords( + fallbackRecords?: Record, +): Record; +export function npmProjectRootForInstalledPackage(installPath: string, packageName: string): string; +export function findPackageJson(packageName: string, roots: string[]): string | undefined; +export { readJson }; +import { readJson } from "./fixtures/common.mjs"; diff --git a/scripts/e2e/lib/codex-media-path/jsonl-request-tail.d.mts b/scripts/e2e/lib/codex-media-path/jsonl-request-tail.d.mts new file mode 100644 index 000000000000..e9609adcb27a --- /dev/null +++ b/scripts/e2e/lib/codex-media-path/jsonl-request-tail.d.mts @@ -0,0 +1,6 @@ +export function createJsonlRequestTailer( + filePath: string, + options?: { historyLimit?: number; maxReadBytes?: number; tailLineLimit?: number }, +): { + read(): unknown[]; +}; diff --git a/scripts/e2e/lib/codex-media-path/limits.d.mts b/scripts/e2e/lib/codex-media-path/limits.d.mts new file mode 100644 index 000000000000..3eabb0b76a56 --- /dev/null +++ b/scripts/e2e/lib/codex-media-path/limits.d.mts @@ -0,0 +1,6 @@ +export function readPositiveIntEnv( + name: unknown, + fallback: unknown, + env?: NodeJS.ProcessEnv, +): number; +export function readTcpPortEnv(name: unknown, fallback: unknown, env?: NodeJS.ProcessEnv): number; diff --git a/scripts/e2e/lib/codex-on-demand/assertions.mjs b/scripts/e2e/lib/codex-on-demand/assertions.mjs index 1a298807ea63..320f666b7e87 100644 --- a/scripts/e2e/lib/codex-on-demand/assertions.mjs +++ b/scripts/e2e/lib/codex-on-demand/assertions.mjs @@ -24,12 +24,12 @@ if (!codexRecord) { if (codexRecord.source !== "npm") { throw new Error(`expected npm codex install record, got ${codexRecord.source}`); } -if (!String(codexRecord.spec || "").includes("@openclaw/codex")) { +if (!codexRecord.spec?.includes("@openclaw/codex")) { throw new Error(`expected @openclaw/codex install spec, got ${codexRecord.spec}`); } const npmRoot = managedNpmRoot(); -const installPath = String(codexRecord.installPath || "").replace(/^~(?=$|\/)/u, process.env.HOME); +const installPath = (codexRecord.installPath || "").replace(/^~(?=$|\/)/u, process.env.HOME); if (!installPath) { throw new Error(`missing codex installPath: ${JSON.stringify(codexRecord)}`); } diff --git a/scripts/e2e/lib/config-reload/log-scanner.d.mts b/scripts/e2e/lib/config-reload/log-scanner.d.mts new file mode 100644 index 000000000000..4713210ed29e --- /dev/null +++ b/scripts/e2e/lib/config-reload/log-scanner.d.mts @@ -0,0 +1,6 @@ +export function createConfigReloadLogScanner( + logPath: string, + options?: { maxReadBytes?: number; tailLineLimit?: number }, +): { + scan(): { reloadLines: string[]; restartLines: string[]; tailLines: string[] }; +}; diff --git a/scripts/e2e/lib/fixtures/common.d.mts b/scripts/e2e/lib/fixtures/common.d.mts new file mode 100644 index 000000000000..f6d2ce998fc4 --- /dev/null +++ b/scripts/e2e/lib/fixtures/common.d.mts @@ -0,0 +1,11 @@ +export type FixtureJson = Record & { + bin?: string | { codex?: string }; + name?: string; +}; + +export function json(value: unknown): string; +export function readJson(file: string): FixtureJson; +export function write(file: string, contents: string | NodeJS.ArrayBufferView): void; +export function writeJson(file: string, value: unknown): void; +export function requireArg(value: string | undefined, name: string): string; +export function assert(condition: unknown, message: string): asserts condition; diff --git a/scripts/e2e/lib/fixtures/mock-openai-config.d.mts b/scripts/e2e/lib/fixtures/mock-openai-config.d.mts new file mode 100644 index 000000000000..057e37500922 --- /dev/null +++ b/scripts/e2e/lib/fixtures/mock-openai-config.d.mts @@ -0,0 +1,2 @@ +export function parseMockOpenAiPort(value: unknown, label?: string): number; +export function applyMockOpenAiModelConfig(cfg: unknown, params: unknown): void; diff --git a/scripts/e2e/lib/fixtures/workspace.mjs b/scripts/e2e/lib/fixtures/workspace.mjs index 00cdc48be415..c93cbba4db87 100644 --- a/scripts/e2e/lib/fixtures/workspace.mjs +++ b/scripts/e2e/lib/fixtures/workspace.mjs @@ -72,12 +72,14 @@ function assertAgentsDeleteResult([outputPath]) { const message = error instanceof Error ? error.message.split("\n").at(0) : String(error); throw new Error(`agents delete --json parse failed: ${message}`, { cause: error }); } - for (const [actual, expected, label] of [ + /** @type {Array<[unknown, unknown, string]>} */ + const comparisons = [ [parsed.agentId, "ops", "agentId"], [parsed.workspace, process.env.SHARED_WORKSPACE, "workspace"], [parsed.workspaceRetained, true, "workspaceRetained"], [parsed.workspaceRetainedReason, "shared", "workspaceRetainedReason"], - ]) { + ]; + for (const [actual, expected, label] of comparisons) { assert(actual === expected, `${label} mismatch: ${JSON.stringify(actual)}`); } assert( diff --git a/scripts/e2e/lib/gateway-frame-payload.d.mts b/scripts/e2e/lib/gateway-frame-payload.d.mts new file mode 100644 index 000000000000..8f16ba83c05e --- /dev/null +++ b/scripts/e2e/lib/gateway-frame-payload.d.mts @@ -0,0 +1 @@ +export function resolveGatewaySuccessPayload(frame: unknown): unknown; diff --git a/scripts/e2e/lib/gateway-network/client.d.mts b/scripts/e2e/lib/gateway-network/client.d.mts new file mode 100644 index 000000000000..8fed2dd26be3 --- /dev/null +++ b/scripts/e2e/lib/gateway-network/client.d.mts @@ -0,0 +1,37 @@ +export type GatewayFrame = { + error?: { + code?: string; + details?: Record; + message?: string; + retryable?: boolean; + }; + id?: string; + ok?: boolean; + payload?: Record; + type?: string; +}; +export type GatewaySocket = { close(): void; send(payload: string): void }; +export function assertReadySuspensionResponse( + response: Record, + now?: number, +): Record; +export function assertGatewaySuspendingError(response: GatewayFrame): void; +export function assertSuspendedProbes( + health: Record, + readiness: Record, +): void; +export function responseError(method: string, response: GatewayFrame): Error; +export function runGatewayNetworkClient( + options: { token: string; url: string; timeoutMs?: number }, + deps?: { + delay?: (ms: number) => Promise; + onceFrame?: ( + ws: GatewaySocket, + predicate: (message: GatewayFrame) => boolean, + timeoutMs?: number, + ) => Promise; + openSocket?: (url: string, timeoutMs?: number) => Promise; + protocolVersion?: number; + stdout?: (message: string) => void; + }, +): Promise; diff --git a/scripts/e2e/lib/gateway-network/limits.d.mts b/scripts/e2e/lib/gateway-network/limits.d.mts new file mode 100644 index 000000000000..dbfa02b38f4a --- /dev/null +++ b/scripts/e2e/lib/gateway-network/limits.d.mts @@ -0,0 +1 @@ +export function readGatewayNetworkClientConnectTimeoutMs(env?: NodeJS.ProcessEnv): number; diff --git a/scripts/e2e/lib/gateway-network/ws-frames.d.mts b/scripts/e2e/lib/gateway-network/ws-frames.d.mts new file mode 100644 index 000000000000..f4dfc9586cc8 --- /dev/null +++ b/scripts/e2e/lib/gateway-network/ws-frames.d.mts @@ -0,0 +1,5 @@ +export function onceFrame( + ws: unknown, + filter: (message: Record) => boolean, + timeoutMs?: number, +): Promise>; diff --git a/scripts/e2e/lib/incremental-line-reader.d.mts b/scripts/e2e/lib/incremental-line-reader.d.mts new file mode 100644 index 000000000000..27de94578a3d --- /dev/null +++ b/scripts/e2e/lib/incremental-line-reader.d.mts @@ -0,0 +1,10 @@ +export function resolvePositiveInteger(value: unknown, fallback: number): number; +export function createIncrementalLineReader( + filePath: string, + options?: { maxReadBytes?: number }, +): { + readLines(): { + lines: string[]; + reset: boolean; + }; +}; diff --git a/scripts/e2e/lib/live-plugin-tool/assertions.mjs b/scripts/e2e/lib/live-plugin-tool/assertions.mjs index 2475455761c5..dcc6c0a714cc 100644 --- a/scripts/e2e/lib/live-plugin-tool/assertions.mjs +++ b/scripts/e2e/lib/live-plugin-tool/assertions.mjs @@ -416,7 +416,7 @@ function pluginInstallPath() { if (record.source !== "npm" || record.artifactKind !== "npm-pack") { throw new Error(`expected npm-pack install record: ${JSON.stringify(record)}`); } - return String(record.installPath || "").replace(/^~(?=$|\/)/u, process.env.HOME); + return (record.installPath || "").replace(/^~(?=$|\/)/u, process.env.HOME); } function writeFixture() { diff --git a/scripts/e2e/lib/mock-openai-http.d.mts b/scripts/e2e/lib/mock-openai-http.d.mts new file mode 100644 index 000000000000..71478d5d5658 --- /dev/null +++ b/scripts/e2e/lib/mock-openai-http.d.mts @@ -0,0 +1,36 @@ +export function readMockOpenAiHttpLimits(env?: NodeJS.ProcessEnv): { + requestMaxBytes?: number; + requestLogBodyMaxBytes?: number; +}; +export function isRequestBodyTooLargeError(error: unknown): error is Error; +export function readBody( + req: unknown, + limits?: { + requestMaxBytes?: number; + requestLogBodyMaxBytes?: number; + }, +): Promise; +export function boundedRequestLogBody( + value: unknown, + bodyText: unknown, + limits?: { + requestMaxBytes?: number; + requestLogBodyMaxBytes: number; + }, +): unknown; +export function writeRequestLogEntryOrFail( + res: unknown, + { + requestLog, + entry, + label, + required, + }: { + requestLog: unknown; + entry: unknown; + label?: string | undefined; + required?: boolean | undefined; + }, +): boolean; +export function writeJson(res: unknown, status: unknown, body: unknown): void; +export function writeSse(res: unknown, events: unknown): void; diff --git a/scripts/e2e/lib/onboard/log-contains.d.mts b/scripts/e2e/lib/onboard/log-contains.d.mts new file mode 100644 index 000000000000..9d0746c53887 --- /dev/null +++ b/scripts/e2e/lib/onboard/log-contains.d.mts @@ -0,0 +1,2 @@ +export function readLogTail(file: unknown, maxBytes?: number): string; +export function logTailContains(file: unknown, needle: unknown, maxBytes?: number): unknown; diff --git a/scripts/e2e/lib/openai-web-search-minimal/client.d.mts b/scripts/e2e/lib/openai-web-search-minimal/client.d.mts new file mode 100644 index 000000000000..7701492befd5 --- /dev/null +++ b/scripts/e2e/lib/openai-web-search-minimal/client.d.mts @@ -0,0 +1,16 @@ +export namespace testing { + export { DEFAULT_GATEWAY_SCHEMA_ERROR }; + export { DEFAULT_RAW_SCHEMA_ERROR }; + export { SUCCESS_MARKER }; + export { extractSuccessReplyTexts }; + export { resolveGatewayPort }; + export { validateSuccessResult }; + export { validateRejectResult }; +} +declare const DEFAULT_GATEWAY_SCHEMA_ERROR: "provider rejected the request schema or tool payload"; +declare const DEFAULT_RAW_SCHEMA_ERROR: "400 The following tools cannot be used with reasoning.effort 'minimal': web_search."; +declare const SUCCESS_MARKER: "OPENCLAW_SCHEMA_E2E_OK"; +declare function extractSuccessReplyTexts(value: unknown): unknown[]; +declare function resolveGatewayPort(env?: NodeJS.ProcessEnv): number; +declare function validateSuccessResult(result: unknown, marker?: string): void; +declare function validateRejectResult(result: unknown, expectedRawSchemaError?: string): string; diff --git a/scripts/e2e/lib/openwebui/http-probe.d.mts b/scripts/e2e/lib/openwebui/http-probe.d.mts new file mode 100644 index 000000000000..87a39a91eccf --- /dev/null +++ b/scripts/e2e/lib/openwebui/http-probe.d.mts @@ -0,0 +1,13 @@ +export function probeHttpStatus({ + url, + expectedRaw, + timeoutMs, + bearer, + fetchImpl, +}: { + url: unknown; + expectedRaw?: string | undefined; + timeoutMs?: number | undefined; + bearer?: string | undefined; + fetchImpl?: typeof fetch | undefined; +}): Promise; diff --git a/scripts/e2e/lib/plugin-index-sqlite.d.mts b/scripts/e2e/lib/plugin-index-sqlite.d.mts new file mode 100644 index 000000000000..f12af51bca77 --- /dev/null +++ b/scripts/e2e/lib/plugin-index-sqlite.d.mts @@ -0,0 +1,23 @@ +export type PluginInstallRecord = Record & { + artifactKind?: string; + gitCommit?: string; + installPath?: string; + resolvedSpec?: string; + resolvedVersion?: string; + source?: string; + sourcePath?: string; + spec?: string; +}; + +export function stateDir(): string; +export function configPath(): string; +export function readPluginInstallIndex(options?: Record): unknown; +export function readPluginInstallRecords(options?: { + configPath?: string; + fallbackRecords?: Record; + stateDir?: string; +}): Record; +export function writePluginInstallIndexForE2E( + index: unknown, + options?: Record, +): void; diff --git a/scripts/e2e/lib/release-user-journey/assertions.d.mts b/scripts/e2e/lib/release-user-journey/assertions.d.mts new file mode 100644 index 000000000000..535cb25e2c0e --- /dev/null +++ b/scripts/e2e/lib/release-user-journey/assertions.d.mts @@ -0,0 +1,6 @@ +export function waitForClickClackSocket(params: { + baseUrl: string; + timeoutMs: number; + pollIntervalMs?: number; +}): Promise; +export function runReleaseUserJourneyAssertion(command: string, args?: string[]): Promise; diff --git a/scripts/e2e/lib/text-file-utils.d.mts b/scripts/e2e/lib/text-file-utils.d.mts new file mode 100644 index 000000000000..ffb13c4ba50e --- /dev/null +++ b/scripts/e2e/lib/text-file-utils.d.mts @@ -0,0 +1,8 @@ +export function tailText(text: string, maxBytes: number): string; +export function readTextFileTail(file: string, maxBytes: number): string; +export function readTextFileBounded( + file: string, + label: string, + maxBytes: number, + options?: { tailBytes?: number }, +): string; diff --git a/scripts/e2e/lib/upgrade-survivor/config-recipe.d.mts b/scripts/e2e/lib/upgrade-survivor/config-recipe.d.mts new file mode 100644 index 000000000000..8c925db91726 --- /dev/null +++ b/scripts/e2e/lib/upgrade-survivor/config-recipe.d.mts @@ -0,0 +1,37 @@ +#!/usr/bin/env node +export function isReleaseBefore(version: unknown, minimum: unknown): boolean; +export function resolveUpgradeSurvivorOpenClawCommand( + argv: unknown, + params?: Record, +): + | { + command: unknown; + args: string[]; + commandLabel: string; + shell: boolean; + windowsVerbatimArguments: boolean; + } + | { + command: string; + args: unknown; + commandLabel: string; + shell: boolean; + windowsVerbatimArguments?: undefined; + }; +export function runUpgradeSurvivorOpenClawStep( + step: unknown, + params?: Record, +): { + id: unknown; + intent: unknown; + command: string; + status: unknown; + signal: unknown; + ok: boolean; + errorCode: string | undefined; + errorMessage: string | undefined; + stdout: string; + stderr: string; +}; +export const CONFIG_COMMAND_TIMEOUT_MS: 120000; +export const CONFIG_COMMAND_MAX_BUFFER_BYTES: number; diff --git a/scripts/e2e/lib/websocket-open.d.mts b/scripts/e2e/lib/websocket-open.d.mts new file mode 100644 index 000000000000..73f0206b83c5 --- /dev/null +++ b/scripts/e2e/lib/websocket-open.d.mts @@ -0,0 +1,5 @@ +export function waitForWebSocketOpen( + ws: unknown, + timeoutMs: unknown, + message?: string, +): Promise; diff --git a/scripts/e2e/npm-telegram-live-runner.ts b/scripts/e2e/npm-telegram-live-runner.ts index fae5717c60f9..3a149df1d0b8 100644 --- a/scripts/e2e/npm-telegram-live-runner.ts +++ b/scripts/e2e/npm-telegram-live-runner.ts @@ -5,6 +5,7 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import { pathToFileURL } from "node:url"; +import type { QaProviderMode } from "../../extensions/qa-lab/src/run-config.ts"; function parseBoolean(value: string | undefined) { const normalized = value?.trim().toLowerCase(); @@ -132,7 +133,7 @@ async function main() { repoRoot, outputDir, sutOpenClawCommand, - providerMode: process.env.OPENCLAW_NPM_TELEGRAM_PROVIDER_MODE, + providerMode: process.env.OPENCLAW_NPM_TELEGRAM_PROVIDER_MODE as QaProviderMode | undefined, primaryModel: process.env.OPENCLAW_NPM_TELEGRAM_MODEL, alternateModel: process.env.OPENCLAW_NPM_TELEGRAM_ALT_MODEL, fastMode: parseBoolean(process.env.OPENCLAW_NPM_TELEGRAM_FAST), @@ -152,7 +153,13 @@ async function main() { async function formatRunnerErrorMessage(error: unknown) { try { - const { formatErrorMessage } = await import("../../dist/infra/errors.js"); + // Widen the specifier so the source-only test-root program does not try to + // resolve dist (TS2307); the docker-e2e boundary guard requires importing + // built dist here, so the cast stays structural instead of a src reference. + const distErrorsPath = "../../dist/infra/errors.js" as string; + const { formatErrorMessage } = (await import(distErrorsPath)) as { + formatErrorMessage: (err: unknown) => string; + }; return formatErrorMessage(error); } catch { return error instanceof Error ? error.message : String(error); diff --git a/scripts/e2e/parallels/common.ts b/scripts/e2e/parallels/common.ts index be1e3a86cda0..4f0ddbb7f865 100644 --- a/scripts/e2e/parallels/common.ts +++ b/scripts/e2e/parallels/common.ts @@ -6,6 +6,10 @@ export * from "./host-server.ts"; export * from "./lane-runner.ts"; export * from "./macos-users.ts"; export * from "./package-artifact.ts"; +// host-server.ts and package-artifact.ts both export a module-private `testing` hook, which +// star re-exports silently drop as ambiguous. Re-export one explicitly so the barrel +// typechecks (TS2308); import either module's `testing` directly, never through this barrel. +export { testing } from "./host-server.ts"; export * from "./parallels-vm.ts"; export * from "./plugin-isolation.ts"; export * from "./provider-auth.ts"; diff --git a/scripts/e2e/parallels/host-server.ts b/scripts/e2e/parallels/host-server.ts index b719927e0fe4..665f402e3846 100644 --- a/scripts/e2e/parallels/host-server.ts +++ b/scripts/e2e/parallels/host-server.ts @@ -1,17 +1,19 @@ // Host Server script supports OpenClaw repository automation. -import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; +import { spawn, type ChildProcess } from "node:child_process"; import { randomUUID } from "node:crypto"; import { rm } from "node:fs/promises"; import { createServer } from "node:http"; import { createConnection } from "node:net"; import { tmpdir } from "node:os"; import path from "node:path"; +import type { Readable } from "node:stream"; import { sleep as delay } from "../../lib/sleep.mjs"; import { die, run, say, sh, warn } from "./host-command.ts"; import type { HostServer, NpmRegistryPackage, NpmRegistryServer } from "./types.ts"; const HOST_SERVER_STDERR_LIMIT_BYTES = 64 * 1024; const HOST_SERVER_STDERR_DRAIN_MS = 5_000; +type HostServerChild = ChildProcess & { stderr: Readable }; export function resolveHostIp(explicit = ""): string { if (explicit) { @@ -132,7 +134,7 @@ export async function startNpmRegistryServer(input: { } async function stopHostServerChild( - child: ChildProcessWithoutNullStreams, + child: HostServerChild, terminateTimeoutMs = 2_000, killTimeoutMs = 1_500, ): Promise { @@ -147,10 +149,7 @@ async function stopHostServerChild( return await waitForChildExit(child, killTimeoutMs); } -async function waitForChildExit( - child: ChildProcessWithoutNullStreams, - timeoutMs: number, -): Promise { +async function waitForChildExit(child: HostServerChild, timeoutMs: number): Promise { if (hasHostServerChildExited(child)) { return true; } @@ -172,14 +171,11 @@ async function waitForChildExit( }); } -function hasHostServerChildExited(child: ChildProcessWithoutNullStreams): boolean { +function hasHostServerChildExited(child: HostServerChild): boolean { return child.exitCode != null || child.signalCode != null; } -async function waitForHostServer( - child: ChildProcessWithoutNullStreams, - port: number, -): Promise { +async function waitForHostServer(child: HostServerChild, port: number): Promise { let stderr = ""; child.stderr.on("data", (chunk: Buffer) => { stderr = appendBoundedOutput(stderr, chunk, HOST_SERVER_STDERR_LIMIT_BYTES); @@ -218,7 +214,7 @@ function appendBoundedOutput(previous: string, chunk: Buffer, limitBytes: number return combined.subarray(combined.byteLength - limitBytes).toString("utf8"); } -function formatHostServerExit(child: ChildProcessWithoutNullStreams): string { +function formatHostServerExit(child: HostServerChild): string { return child.signalCode ? `signal ${child.signalCode}` : `exit ${child.exitCode ?? "unknown"}`; } diff --git a/scripts/e2e/parallels/macos-users.ts b/scripts/e2e/parallels/macos-users.ts index 67b052f0fcc4..9770fb5d8921 100644 --- a/scripts/e2e/parallels/macos-users.ts +++ b/scripts/e2e/parallels/macos-users.ts @@ -4,10 +4,10 @@ export function parseMacosDsclUserHomeLine(line: string): { user: string; home: if (!match) { return null; } - return { user: match[1], home: match[2] }; + return { user: match[1]!, home: match[2]! }; } export function isLikelyMacosDesktopHome(home: string | undefined): boolean { const normalized = home?.trim(); - return Boolean(normalized) && /(?:^|\/)Users\/[^/]+$/u.test(normalized); + return normalized !== undefined && /(?:^|\/)Users\/[^/]+$/u.test(normalized); } diff --git a/scripts/e2e/parallels/npm-update-smoke.ts b/scripts/e2e/parallels/npm-update-smoke.ts index 26b0af6b20ae..21977a979440 100755 --- a/scripts/e2e/parallels/npm-update-smoke.ts +++ b/scripts/e2e/parallels/npm-update-smoke.ts @@ -571,7 +571,7 @@ export class NpmUpdateSmoke { private tgzDir = ""; private latestVersion = ""; private packageSpec = ""; - private currentHead = ""; + currentHead = ""; private currentHeadShort = ""; private harnessCheckoutVersion = ""; private harnessTargetFamily = ""; diff --git a/scripts/e2e/parallels/provider-auth.ts b/scripts/e2e/parallels/provider-auth.ts index cf09ddd0e5a4..6ffbfbcb98e7 100644 --- a/scripts/e2e/parallels/provider-auth.ts +++ b/scripts/e2e/parallels/provider-auth.ts @@ -7,7 +7,7 @@ import { die, run } from "./host-command.ts"; import type { Mode, Platform, Provider, ProviderAuth } from "./types.ts"; type ResolveLatestVersionDeps = { - createTempDir?: typeof mkdtempSync; + createTempDir?: (prefix: string) => string; removeDir?: typeof rmSync; runCommand?: typeof run; tempDir?: typeof tmpdir; diff --git a/scripts/e2e/telegram-user-crabbox-proof.ts b/scripts/e2e/telegram-user-crabbox-proof.ts index 6c6220e91536..be0a1e4c6d0c 100644 --- a/scripts/e2e/telegram-user-crabbox-proof.ts +++ b/scripts/e2e/telegram-user-crabbox-proof.ts @@ -630,7 +630,11 @@ export function signalCommandTree( useProcessGroup = platform !== "win32", }: { platform?: NodeJS.Platform; - runTaskkill?: typeof spawnSync; + runTaskkill?: ( + command: string, + args: readonly string[], + options: { stdio: "ignore" }, + ) => { error?: Error; status: number | null }; useProcessGroup?: boolean; } = {}, ) { @@ -1206,10 +1210,11 @@ export async function startLocalSut( cwd: params.repoRoot, env: mockServerEnv({ ...params, requestLog }), }); + const runningMock = mock; await waitForOutputReady( - mock.child, + runningMock.child, /mock-openai listening/u, - () => mock.output, + () => runningMock.output, "mock-openai", 10_000, ); @@ -1219,23 +1224,24 @@ export async function startLocalSut( repoRoot: params.repoRoot, }); gateway = spawnLoggedCommand(gatewaySpec.command, gatewaySpec.args, gatewaySpec.options); + const runningGateway = gateway; await waitForOutputReady( - gateway.child, + runningGateway.child, /\[gateway\] ready/u, - () => gateway.output, + () => runningGateway.output, "gateway", 60_000, ); return { ...config, drained, - gateway: gateway.child, + gateway: runningGateway.child, get gatewayLog() { - return gateway.output; + return runningGateway.output; }, - mock: mock.child, + mock: runningMock.child, get mockLog() { - return mock.output; + return runningMock.output; }, requestLog, }; @@ -1327,10 +1333,10 @@ async function startLocalSutDaemon(params: { gatewayPid = spawnDaemon({ args: gatewaySpec.args, command: gatewaySpec.command, - cwd: gatewaySpec.options.cwd ?? params.repoRoot, + cwd: (gatewaySpec.options.cwd ?? params.repoRoot) as string, env: gatewaySpec.options.env ?? gatewayEnvVars, logPath: gatewayLog, - shell: gatewaySpec.options.shell, + shell: gatewaySpec.options.shell as boolean | undefined, windowsVerbatimArguments: gatewaySpec.options.windowsVerbatimArguments, }); if (!gatewayPid) { diff --git a/scripts/e2e/telegram-user-credential-io.ts b/scripts/e2e/telegram-user-credential-io.ts index b509b695e116..29342a9563b4 100644 --- a/scripts/e2e/telegram-user-credential-io.ts +++ b/scripts/e2e/telegram-user-credential-io.ts @@ -5,6 +5,12 @@ import { resolveWindowsTaskkillPath } from "../lib/windows-taskkill.mjs"; export type JsonObject = Record; +type RunTaskkill = ( + command: string, + args: string[], + options: { stdio: "ignore" }, +) => { error?: Error; status: number | null }; + type FetchJsonParams = { fetchImpl?: (url: string, init: RequestInit) => Promise; init: RequestInit; @@ -256,7 +262,7 @@ export function signalChildProcessTree( useProcessGroup = platform !== "win32", }: { platform?: NodeJS.Platform; - runTaskkill?: typeof spawnSync; + runTaskkill?: RunTaskkill; useProcessGroup?: boolean; } = {}, ) { diff --git a/scripts/ensure-cli-startup-build.d.mts b/scripts/ensure-cli-startup-build.d.mts new file mode 100644 index 000000000000..fbd61fec3d40 --- /dev/null +++ b/scripts/ensure-cli-startup-build.d.mts @@ -0,0 +1,26 @@ +#!/usr/bin/env node +/** + * Resolves the CLI startup build timeout from environment. + */ +export type StartupBuildParams = { + env?: NodeJS.ProcessEnv; + nodeExecPath?: string; + rootDir?: string; + spawnSync?: ( + command: string, + args: string[], + options: Record, + ) => { error?: Error; status?: number | null }; + stdio?: "inherit" | "pipe"; +}; +export function resolveCliStartupBuildTimeoutMs(env?: NodeJS.ProcessEnv): number; +/** + * Reports whether required CLI startup build outputs exist. + */ +export function hasCliStartupBuild(params?: StartupBuildParams): boolean; +/** + * Builds CLI startup assets when required outputs are missing. + */ +export function ensureCliStartupBuild(params?: StartupBuildParams): { + built: boolean; +}; diff --git a/scripts/ensure-extension-memory-build.d.mts b/scripts/ensure-extension-memory-build.d.mts new file mode 100644 index 000000000000..a3d8eb82b6a5 --- /dev/null +++ b/scripts/ensure-extension-memory-build.d.mts @@ -0,0 +1,27 @@ +#!/usr/bin/env node +/** + * Resolves the extension memory build timeout from environment. + */ +export type ExtensionMemoryBuildParams = { + env?: NodeJS.ProcessEnv; + nodeExecPath?: string; + requiredExtensionIds?: string[]; + rootDir?: string; + spawnSync?: ( + command: string, + args: string[], + options: Record, + ) => { error?: Error; status: number | null }; + stdio?: "inherit" | "pipe"; +}; +export function resolveExtensionMemoryBuildTimeoutMs(env?: NodeJS.ProcessEnv): number; +/** + * Reports whether built memory extension entries exist. + */ +export function hasBuiltExtensionMemoryEntries(params?: ExtensionMemoryBuildParams): boolean; +/** + * Builds memory extension entries when required outputs are missing. + */ +export function ensureExtensionMemoryBuild(params?: ExtensionMemoryBuildParams): { + built: boolean; +}; diff --git a/scripts/ensure-playwright-chromium.d.mts b/scripts/ensure-playwright-chromium.d.mts index 9868c9024264..372fc9b73851 100644 --- a/scripts/ensure-playwright-chromium.d.mts +++ b/scripts/ensure-playwright-chromium.d.mts @@ -1,29 +1,32 @@ -import type { spawnSync, SpawnSyncOptions } from "node:child_process"; -import type { existsSync } from "node:fs"; import type { resolvePnpmRunner } from "./pnpm-runner.mjs"; type Getuid = typeof process.getuid; +type SpawnSyncLike = ( + command: string, + args: string[], + options?: Record, +) => { error?: Error; status: number | null }; type ChromiumInstallOptions = { comSpec?: string; cwd?: string; env?: NodeJS.ProcessEnv; executablePath?: string; - existsSync?: typeof existsSync; + existsSync?: (path: string) => boolean; getuid?: Getuid; log?: (message: string) => void; platform?: NodeJS.Platform; - spawnSync?: typeof spawnSync; - stdio?: SpawnSyncOptions["stdio"]; + spawnSync?: SpawnSyncLike; + stdio?: "ignore" | "inherit" | "pipe"; }; export const systemChromiumExecutableCandidates: readonly string[]; export function canRunChromiumExecutable( executablePath: string, - spawnSync?: typeof spawnSync, + spawnSync?: SpawnSyncLike, ): boolean; export function resolveSystemChromiumExecutablePath( - existsSync?: typeof existsSync, - spawnSync?: typeof spawnSync, + existsSync?: (path: string) => boolean, + spawnSync?: SpawnSyncLike, ): string; export function resolvePlaywrightInstallRunner(options?: { comSpec?: string; diff --git a/scripts/format-docs.d.mts b/scripts/format-docs.d.mts new file mode 100644 index 000000000000..2b3ae54c34dd --- /dev/null +++ b/scripts/format-docs.d.mts @@ -0,0 +1,41 @@ +#!/usr/bin/env node +export function docsFiles(root?: string, deps?: Record): string[]; +export function chunkFilesForCommand( + files: unknown, + prefixArgs: unknown, + maxBytes?: number, +): unknown[][]; +export function resolveOxfmtInvocation( + args: unknown, + params?: Record, +): + | { + command: string; + args: string[]; + shell: boolean; + windowsVerbatimArguments: boolean; + } + | { + command: string; + args: string[]; + shell: boolean; + windowsVerbatimArguments?: undefined; + } + | { + command: string; + args: string[]; + shell: boolean; + windowsVerbatimArguments?: undefined; + }; +export function runOxfmt( + files: unknown, + params?: Record, + deps?: Record, +): void; +export function formatDocs( + params?: Record, + deps?: Record, +): { + changed: unknown[]; + fileCount: number; +}; diff --git a/scripts/full-release-validation-at-sha.d.mts b/scripts/full-release-validation-at-sha.d.mts new file mode 100644 index 000000000000..014437319797 --- /dev/null +++ b/scripts/full-release-validation-at-sha.d.mts @@ -0,0 +1,16 @@ +#!/usr/bin/env node +export function parseArgs(argv: unknown): { + sha: string; + workflowSha: string; + keepBranch: boolean; + dryRun: boolean; + inputs: { + provider: string; + mode: string; + release_profile: string; + rerun_group: string; + reuse_evidence: string; + }; +}; +export function releaseEvidenceVerificationArgs(parentRunId: unknown): string[]; +export function releaseEvidenceVerifierPath(worktreeRoot: unknown): string; diff --git a/scripts/generate-dependency-release-evidence.d.mts b/scripts/generate-dependency-release-evidence.d.mts new file mode 100644 index 000000000000..10721bc3e442 --- /dev/null +++ b/scripts/generate-dependency-release-evidence.d.mts @@ -0,0 +1,133 @@ +#!/usr/bin/env node +/** + * Resolves the release tag when the release ref is a SHA or tag. + */ +export function resolveReleaseTag({ + releaseRef, + packageVersion, +}: { + releaseRef: unknown; + packageVersion: unknown; +}): unknown; +/** + * Resolves the previous reachable release tag for dependency diffs. + */ +export function resolvePreviousReleaseTag({ + rootDir, + execFileSyncImpl, + fetchOnMiss, +}?: { + rootDir?: string | undefined; + execFileSyncImpl?: ((command: string, args?: string[]) => string) | undefined; + fetchOnMiss?: boolean | undefined; +}): string; +/** + * Creates the dependency evidence manifest payload. + */ +export function createDependencyEvidenceManifest({ + generatedAt, + releaseTag, + releaseRef, + releaseSha, + npmDistTag, + packageVersion, + workflowRunId, + workflowRunAttempt, + dependencyChangeBaseRef, +}?: { + generatedAt?: string | undefined; + releaseTag?: string; + releaseRef?: string; + releaseSha?: string; + npmDistTag?: string; + packageVersion?: string; + dependencyChangeBaseRef?: string; + workflowRunId?: string | undefined; + workflowRunAttempt?: string | undefined; +}): { + schemaVersion: number; + generatedAt: string; + releaseTag: unknown; + releaseRef: unknown; + releaseSha: unknown; + npmDistTag: unknown; + packageName: string; + packageVersion: unknown; + workflowRunId: string; + workflowRunAttempt: string; + dependencyChangeBaseRef: unknown; + reports: { + name: string; + command: string; + policy: string; + json: string; + markdown: string; + }[]; +}; +/** + * Reads generated reports and collects summary counts. + */ +export function collectDependencyEvidenceSummaryCounts(evidenceDir: unknown): Promise<{ + vulnerabilityBlockers: unknown; + vulnerabilityFindings: unknown; + transitiveRiskSignals: unknown; + workspaceExcludedTransitiveSignals: unknown; + transitiveMetadataFailures: unknown; + ownershipLockfilePackages: unknown; + ownershipBuildRiskPackages: unknown; + dependencyFileChanges: unknown; + dependencyAddedPackages: unknown; + dependencyRemovedPackages: unknown; + dependencyChangedPackages: unknown; +}>; +/** + * Renders the dependency evidence Markdown summary. + */ +export function renderDependencyEvidenceSummary({ + releaseTag, + releaseSha, + baseRef, + counts, +}: { + releaseTag: unknown; + releaseSha: unknown; + baseRef: unknown; + counts: unknown; +}): string; +/** + * Renders the GitHub Actions step summary for dependency evidence. + */ +export function renderDependencyEvidenceStepSummary({ + evidenceArtifactName, + baseRef, + counts, +}: { + evidenceArtifactName: unknown; + baseRef: unknown; + counts: unknown; +}): string; +export function parseArgs(argv: string[]): { + help?: true; + rootDir: string; + outputDir: string | null; + releaseRef: string | null; + npmDistTag: string | null; + baseRef: string | null; + githubOutput: string | undefined; + githubStepSummary: string | undefined; +}; +/** + * Runs the dependency release evidence generator CLI. + */ +export function main(argv?: string[]): Promise; +/** + * Dependency evidence reports generated for release artifacts. + */ +export const DEPENDENCY_EVIDENCE_REPORTS: { + name: string; + command: string; + policy: string; + json: string; + markdown: string; +}[]; +import { execFileSync } from "node:child_process"; diff --git a/scripts/generate-docs-map.d.mts b/scripts/generate-docs-map.d.mts new file mode 100644 index 000000000000..5fd18ac7b806 --- /dev/null +++ b/scripts/generate-docs-map.d.mts @@ -0,0 +1,5 @@ +#!/usr/bin/env node +export namespace testing { + export { cleanHeadingText }; +} +declare function cleanHeadingText(value: unknown): unknown; diff --git a/scripts/generate-npm-shrinkwrap.d.mts b/scripts/generate-npm-shrinkwrap.d.mts new file mode 100644 index 000000000000..3f3a21c5150b --- /dev/null +++ b/scripts/generate-npm-shrinkwrap.d.mts @@ -0,0 +1,131 @@ +#!/usr/bin/env node +/** + * Resolves the npm command invocation used by shrinkwrap generation. + */ +export function createNpmShrinkwrapCommand( + args: string[], + options?: { + comSpec?: string; + env?: NodeJS.ProcessEnv; + execPath?: string; + existsSync?: (candidate: string) => boolean; + platform?: NodeJS.Platform; + }, +): { + args: string[]; + command: string; + env?: NodeJS.ProcessEnv; + shell: boolean; + windowsVerbatimArguments?: boolean; +}; +/** + * Reads a positive integer env override for shrinkwrap subprocess limits. + */ +export function readPositiveIntEnv( + name: unknown, + fallback: unknown, + env?: NodeJS.ProcessEnv, +): number; +/** + * Builds execFileSync options with bounded timeout and output buffer limits. + */ +export function createNpmShrinkwrapExecOptions( + invocation: unknown, + cwd: unknown, + env?: NodeJS.ProcessEnv, +): { + cwd: unknown; + env: unknown; + maxBuffer: number; + shell: unknown; + stdio: string[]; + timeout: number; + windowsVerbatimArguments: unknown; +}; +export function resolvePackageDirs(args: string[]): { + check: unknown; + changedPaths: string[]; + jobs: number; + packageDirs: unknown[]; +}; +export function runBoundedTasks( + items: Item[], + jobs: number, + runTask: (item: Item) => Promise, +): Promise; +export function resolveShrinkwrapJobs( + rawValue: unknown, + env?: NodeJS.ProcessEnv, + fallback?: number, +): number; +export function collectCurrentShrinkwrapOverrides( + shrinkwrap: unknown, + declaredDependencies?: Set, + pnpmLockPackages?: Set, +): unknown; +export function collectOverrideViolations( + lockfile: unknown, + overrideRules: unknown, +): { + path: string; + packageName: unknown; + actualVersion: unknown; + expectedVersion: unknown; + packagePath: { + name: unknown; + path: string; + }[]; +}[]; +export function collectPnpmLockViolations( + shrinkwrap: unknown, + pnpmLockPackages?: Set, +): { + path: string; + packageKey: string; +}[]; +export function disableShrinkwrappedOverrideConflictSources( + lockfile: unknown, + overrideRules: unknown, +): string[]; +export function exactOverrideRulesFromOverrides(overrides: unknown): unknown; +export function exactVersionFromOverrideSpec(spec: unknown): string | null; +export function mergeOverrides( + packageOverrides: unknown, + workspaceOverrides: unknown, + pnpmLockOverrides: unknown, +): unknown; +export function applyPackageExtensionPeerMetadata( + lockfile: unknown, + packageExtensions?: unknown, +): unknown; +export function normalizeNpmVersionDrift(lockfile: T): T; +export function packageJsonForShrinkwrap( + packageJson: Record, + shrinkwrapOverrides: Record, +): { + dependencies?: Record; + devDependencies?: Record; + peerDependencies?: Record; + [key: string]: unknown; +}; +export function packageDependencyInputsChanged(packageDir: unknown, changedPaths: unknown): unknown; +export function pnpmLockOverrideVersionForVersions(versions: unknown): unknown; +export function parsePnpmPackageKey(packageKey: unknown): { + name: string; + version: string; +} | null; +export function parseLockPackagePath(lockPath: unknown): { + name: unknown; + path: string; +}[]; +export function readShrinkwrapOverrides(): unknown; +export function restoreCurrentPnpmLockedPackages( + generated: unknown, + current: unknown, + pnpmLockPackages?: Set, +): unknown; +export function shouldUseLegacyPeerDepsForShrinkwrap( + packageJson: unknown, + packageExtensions?: unknown, +): boolean; +export function shrinkwrapPackageDirsForChangedPaths(changedPaths: string[]): string[]; diff --git a/scripts/github/barnacle-auto-response.d.mts b/scripts/github/barnacle-auto-response.d.mts new file mode 100644 index 000000000000..8690a05eef61 --- /dev/null +++ b/scripts/github/barnacle-auto-response.d.mts @@ -0,0 +1,21 @@ +export const managedLabelSpecs: Record; +export const candidateLabels: { + blankTemplate: string; + lowSignalDocs: string; + docsDiscoverability: string; + testOnlyNoBug: string; + refactorOnly: string; + needsPrContext: string; + dirtyCandidate: string; + riskyInfra: string; + externalPluginCandidate: string; +}; +export function classifyPullRequestCandidateLabels( + pullRequest: Record, + files: Array<{ filename: string; status: string }>, +): string[]; +export function runBarnacleAutoResponse(params: { + github: Record; + context: Record; + core?: Pick; +}): Promise; diff --git a/scripts/github/dependency-guard.d.mts b/scripts/github/dependency-guard.d.mts new file mode 100644 index 000000000000..450e1b2fd172 --- /dev/null +++ b/scripts/github/dependency-guard.d.mts @@ -0,0 +1,105 @@ +export const GITHUB_API_REQUEST_TIMEOUT_MS: number; +export const GITHUB_ERROR_BODY_MAX_BYTES: number; +export const GITHUB_RESPONSE_BODY_MAX_BYTES: number; +export const dependencyChangedLabel: "dependencies-changed"; + +type Comment = { + body?: string; + created_at?: string; + html_url?: string; + user?: { login?: string }; +}; + +type PullRequest = { + head?: { ref?: string; repo?: { full_name?: string }; sha?: string }; + maintainer_can_modify?: boolean; + user?: { login?: string }; +}; + +type ActorCandidate = { login: string; source: string }; + +export function isDependencyFile(filename: string): boolean; +export function isDependencyManifest(filename: string): boolean; +export function isPackageLockfile(filename: string): boolean; +export function dependencyFieldChanges( + baseManifest: Record, + headManifest: Record, +): string[]; +export function shouldAutoscrubDependencyLockfiles(options: { + dependencyFiles?: string[]; + lockfileChanges: unknown[]; + dependencyManifestChanges?: unknown[]; +}): boolean; +export function canAutoscrubPullRequest(options: { + owner: string; + repo: string; + pullRequest: PullRequest; +}): boolean; +export function sanitizeDisplayValue(value: unknown): string; +export function markdownCode(value: unknown): string; +export function readBoundedGitHubJson( + response: Response, + maxBytes?: number, + options?: { signal?: AbortSignal }, +): Promise; +export function findDependencyOverrideCommand(options: { + comments: Comment[]; + expectedSha: string; + isSecurityMember: (login: string) => boolean; + newerThan?: string; +}): { login: string; reason: string | null; sha: string; url?: string } | null; +export function findDependencyOverrideCommandAsync(options: { + comments: Comment[]; + expectedSha: string; + isSecurityMember: (login: string) => Promise; + newerThan: string; +}): Promise<{ login: string; reason: string | null; sha: string; url?: string } | null>; +export function dependencyGuardCommentHeadSha(comment: Comment): string | null; +export function dependencyOverrideExpectedSha( + existingGuardComment: Comment | null, + currentHeadSha: string, +): string | null; +export function isDependencyGuardAuthorizedForHead( + comment: Comment, + currentHeadSha: string, +): boolean; +export function isDependencyGuardTrustedForHead(comment: Comment, currentHeadSha: string): boolean; +export function securityApproverSet(value: unknown): Set; +export function dependencyGuardCommentAuthors(value?: unknown): Set; +export function isDependencyGuardMarkerComment( + comment: Comment, + marker: string, + trustedAuthors: Set, +): boolean; +export function renderAuthorizedDependencyComment(override: Record): string; +export function renderTrustedDependencyComment(options: { + actor: { login: string; reason: string }; + headSha: string; +}): string; +export function renderAutoscrubbedDependencyComment(options: { + baseBranch: string; + lockfileChanges: string[]; + commitSha: string; +}): string; +export function isAutoscrubbedDependencyComment(comment: Comment): boolean; +export function renderClearedDependencyGuardComment(options: { headSha: string }): string; +export function renderBlockedDependencyComment(options: Record): string; +export function dependencyGuardTrustedActorCandidates(options: { + pullRequest: PullRequest; + event: Record; + currentHeadSha: string; +}): ActorCandidate[]; +export function findTrustedDependencyGuardActor(options: { + candidates: ActorCandidate[]; + isDependencyApprover: (login: string) => Promise; +}): Promise<{ login: string; reason: string } | null>; +export function githubApi( + token: string, + options?: { + fetchImpl?: typeof fetch; + responseMaxBodyBytes?: number; + timeoutMs?: number; + }, +): { request(path: string, options?: Record): Promise }; +export function createAutoscrubCommit(...args: unknown[]): Promise; +export function readBoundedGitHubErrorText(...args: unknown[]): Promise; diff --git a/scripts/github/real-behavior-proof-policy.d.mts b/scripts/github/real-behavior-proof-policy.d.mts new file mode 100644 index 000000000000..b3cf914fe90c --- /dev/null +++ b/scripts/github/real-behavior-proof-policy.d.mts @@ -0,0 +1,40 @@ +export const PROOF_OVERRIDE_LABEL: "proof: override"; +export const PROOF_SUFFICIENT_LABEL: "proof: sufficient"; +export const NEEDS_PR_CONTEXT_LABEL: "triage: needs-pr-context"; +export const DEFAULT_GITHUB_API_TIMEOUT_MS: 30000; + +type PullRequest = Record; +type Comment = Record; +type Evaluation = { + status: string; + reason: string; + applies: boolean; + passed: boolean; + missingSections: string[]; +}; + +export function readBoundedGitHubApiJson( + response: Response, + label: string, + maxBytes?: number, + options?: { timeoutMs?: number }, +): Promise; +export function isMaintainerTeamMember(params?: { + token?: string; + org?: string; + login?: string; + teamSlug?: string; + fetch?: typeof globalThis.fetch; + timeoutMs?: number; +}): Promise; +export function hasAuthoredPullRequestSection(heading: string, body?: string): boolean; +export function hasClawSweeperExactHeadProof(params?: { + pullRequest?: PullRequest; + comments?: Comment[]; +}): boolean; +export function evaluateClawSweeperExactHeadProof(params?: { + pullRequest?: PullRequest; + comments?: Comment[]; +}): Evaluation; +export function evaluatePullRequestContext(params?: { pullRequest?: PullRequest }): Evaluation; +export function labelsForPullRequestContext(evaluation: Evaluation): string[]; diff --git a/scripts/github/security-sensitive-guard.d.mts b/scripts/github/security-sensitive-guard.d.mts new file mode 100644 index 000000000000..53a452a655c0 --- /dev/null +++ b/scripts/github/security-sensitive-guard.d.mts @@ -0,0 +1,76 @@ +export const GITHUB_API_REQUEST_TIMEOUT_MS: number; +export const GITHUB_ERROR_BODY_MAX_BYTES: number; +export const GITHUB_RESPONSE_BODY_MAX_BYTES: number; +export const allowSecuritySensitiveCommand: string; +export const securitySensitiveGuardMarker: string; +type Comment = { body?: string; created_at?: string; html_url?: string; user?: { login?: string } }; +type ActorCandidate = { login: string; source: string }; +export function securitySensitiveFileDefinitions(): Array<{ path: string; reason: string }>; +export function securitySensitiveFileDefinition( + filename: string, +): { path: string; reason: string } | undefined; +export function isSecuritySensitiveFile(filename: string): boolean; +export function sanitizeDisplayValue(value: unknown): string; +export function markdownCode(value: unknown): string; +export function findSecuritySensitiveOverrideCommand(options: { + comments: Comment[]; + expectedSha: string; + isSecurityMember: (login: string) => boolean; + newerThan?: string; +}): { login: string; reason: string | null; sha: string; url?: string } | null; +export function findSecuritySensitiveOverrideCommandAsync(options: { + comments: Comment[]; + expectedSha: string; + isSecurityMember: (login: string) => Promise; + newerThan?: string; +}): Promise<{ login: string; reason: string | null; sha: string; url?: string } | null>; +export function securitySensitiveGuardCommentHeadSha(comment: Comment): string | null; +export function securitySensitiveOverrideExpectedSha( + comment: Comment | null, + currentHeadSha: string, +): string | null; +export function isSecuritySensitiveGuardAuthorizedForHead( + comment: Comment, + currentHeadSha: string, +): boolean; +export function isSecuritySensitiveGuardTrustedForHead( + comment: Comment, + currentHeadSha: string, +): boolean; +export function securityApproverSet(value: unknown): Set; +export function securitySensitiveGuardCommentAuthors(value?: unknown): Set; +export function isSecuritySensitiveGuardMarkerComment( + comment: Comment, + trustedAuthors: Set, +): boolean; +export function collectSecuritySensitiveChanges( + files: Array<{ filename: string; previous_filename?: string; status?: string }>, +): Array>; +export function renderSecuritySensitiveAwarenessComment( + changes: Array>, +): string; +export function renderAuthorizedSecuritySensitiveComment(override: Record): string; +export function renderTrustedSecuritySensitiveComment(options: Record): string; +export function renderClearedSecuritySensitiveGuardComment(options: { headSha: string }): string; +export function renderBlockedSecuritySensitiveComment(options: Record): string; +export function securitySensitiveGuardTrustedActorCandidates( + options: Record, +): ActorCandidate[]; +export function findTrustedSecuritySensitiveGuardActor(options: { + candidates: ActorCandidate[]; + isSecuritySensitiveApprover: (login: string) => Promise; +}): Promise<{ login: string; reason: string } | null>; +export function githubApi( + token: string, + options?: { fetchImpl?: typeof fetch; responseMaxBodyBytes?: number; timeoutMs?: number }, +): { request(path: string, options?: Record): Promise }; +export function readBoundedGitHubErrorText( + response: Response, + maxBytes?: number, + options?: { signal?: AbortSignal }, +): Promise; +export function readBoundedGitHubJson( + response: Response, + maxBytes?: number, + options?: { signal?: AbortSignal }, +): Promise; diff --git a/scripts/lib/arg-utils.d.mts b/scripts/lib/arg-utils.d.mts index 2a1a765b43c9..82e07c106c95 100644 --- a/scripts/lib/arg-utils.d.mts +++ b/scripts/lib/arg-utils.d.mts @@ -5,10 +5,10 @@ type FlagSpec = { index: number, args: T, ): { - flag: string; + flag?: string; nextIndex: number; - repeatable: boolean; - apply(target: T): void; + repeatable?: boolean; + apply(target?: T): void; } | null; }; diff --git a/scripts/lib/bounded-response.d.mts b/scripts/lib/bounded-response.d.mts new file mode 100644 index 000000000000..0754f69b013d --- /dev/null +++ b/scripts/lib/bounded-response.d.mts @@ -0,0 +1,7 @@ +/** Read response text while enforcing max bytes before and during streaming. */ +export function readBoundedResponseText( + response: unknown, + label: unknown, + maxBytes: unknown, + options?: Record, +): Promise; diff --git a/scripts/lib/budget-number-args.d.mts b/scripts/lib/budget-number-args.d.mts new file mode 100644 index 000000000000..5a6311669a56 --- /dev/null +++ b/scripts/lib/budget-number-args.d.mts @@ -0,0 +1,16 @@ +export function parseBudgetNumber(raw: string | undefined, label: string): number | null; +export function readBudgetEnvNumber(name: string, env?: NodeJS.ProcessEnv): number | null; +export function budgetFloatFlag( + flag: string, + key: Key, +): { + consume( + argv: string[], + index: number, + ): { + flag: string; + nextIndex: number; + repeatable: false; + apply(target: Record): void; + } | null; +}; diff --git a/scripts/lib/bundled-plugin-source-utils.d.mts b/scripts/lib/bundled-plugin-source-utils.d.mts new file mode 100644 index 000000000000..efb00a90a21c --- /dev/null +++ b/scripts/lib/bundled-plugin-source-utils.d.mts @@ -0,0 +1,11 @@ +/** Read a UTF-8 file when it exists, returning null on missing/unreadable paths. */ +export function readIfExists(filePath: unknown): string | null; +/** Collect bundled plugin manifests and package metadata from git or the extensions directory. */ +export function collectBundledPluginSources(params?: Record): { + packageJsonPath?: string; + packageJson?: unknown; + dirName: string; + pluginDir: string; + manifestPath: string; + manifest: unknown; +}[]; diff --git a/scripts/lib/changed-extensions.d.mts b/scripts/lib/changed-extensions.d.mts new file mode 100644 index 000000000000..dde1a1a9b440 --- /dev/null +++ b/scripts/lib/changed-extensions.d.mts @@ -0,0 +1,11 @@ +/** List bundled extension ids available in git or the local extensions directory. */ +export function listAvailableExtensionIds(): string[]; +/** Map changed paths to bundled extension ids, ignoring unknown extension-like paths. */ +export function detectChangedExtensionIds(changedPaths: string[]): string[]; +/** List changed bundled extension ids between a resolved base and head revision. */ +export function listChangedExtensionIds(params?: { + base?: string; + cwd?: string; + head?: string; + unavailableBaseBehavior?: "all" | "empty" | "error"; +}): string[]; diff --git a/scripts/lib/channel-contract-test-plan.d.mts b/scripts/lib/channel-contract-test-plan.d.mts new file mode 100644 index 000000000000..61e0ffa001f5 --- /dev/null +++ b/scripts/lib/channel-contract-test-plan.d.mts @@ -0,0 +1,7 @@ +/** Create balanced channel contract test shards for CI check planning. */ +export function createChannelContractTestShards(): { + checkName: string; + includePatterns: string[]; + task: string; + runtime: string; +}[]; diff --git a/scripts/lib/ci-node-test-plan.d.mts b/scripts/lib/ci-node-test-plan.d.mts new file mode 100644 index 000000000000..f7e2df530756 --- /dev/null +++ b/scripts/lib/ci-node-test-plan.d.mts @@ -0,0 +1,37 @@ +export type NodeTestShardGroup = { + shard_name: string; + configs: string[]; + includePatterns?: string[]; + requiresDist: boolean; + runner: string; + env?: Record; +}; + +export type NodeTestShard = { + checkName: string; + shardName: string; + configs: string[]; + runner: string; + requiresDist: boolean; + includePatterns?: string[]; + env?: Record; + groups?: NodeTestShardGroup[]; + timeoutMinutes?: number; +}; + +export type NodeTestPlanOptions = { + includeReleaseOnlyPluginShards?: boolean; + compact?: boolean; + compactGroupCount?: number; + compactWholeGroupCount?: number; +}; + +export type CompactNodeTestShard = Omit & { + groups: NodeTestShardGroup[]; +}; + +export function createNodeTestShards(options?: NodeTestPlanOptions): NodeTestShard[]; +export function createNodeTestShardBundles( + options: NodeTestPlanOptions & { compact: true }, +): CompactNodeTestShard[]; +export function createNodeTestShardBundles(options?: NodeTestPlanOptions): NodeTestShard[]; diff --git a/scripts/lib/clawhub-bootstrap-artifact.d.mts b/scripts/lib/clawhub-bootstrap-artifact.d.mts new file mode 100644 index 000000000000..bf75b7ab941c --- /dev/null +++ b/scripts/lib/clawhub-bootstrap-artifact.d.mts @@ -0,0 +1,39 @@ +export type ClawHubBootstrapEntry = { + artifactPath: string; + packageName: string; + sha256: string; + size: number; + version: string; +}; +export type ClawHubBootstrapManifest = { + artifactName: string; + clawhubToolchainIntegrity: string; + clawhubToolchainSha256: string; + clawhubToolchainVersion: string; + entries: ClawHubBootstrapEntry[]; + repository: string; + runAttempt: string; + runId: string; + targetSha: string; + workflowSha: string; +}; +export function verifyClawHubPackedArtifactIdentity( + options: Record, +): Promise; +export function parseClawHubBootstrapManifestBytes( + inputBytes: Uint8Array, +): ClawHubBootstrapManifest; +export function readClawHubBootstrapManifest(path: string): ClawHubBootstrapManifest; +export function downloadClawHubBootstrapArtifact( + options: Record, +): Promise; +export function createClawHubBootstrapArtifactManifest( + options: Record & { + artifactRoot: string; + matrixPath: string; + outputPath: string; + }, +): Promise; +export function verifyClawHubBootstrapArtifactManifest( + options: Record & { artifactRoot: string; manifestPath: string }, +): Promise; diff --git a/scripts/lib/deprecated-plugin-sdk-usage.d.mts b/scripts/lib/deprecated-plugin-sdk-usage.d.mts new file mode 100644 index 000000000000..de133ae2aa89 --- /dev/null +++ b/scripts/lib/deprecated-plugin-sdk-usage.d.mts @@ -0,0 +1,2 @@ +/** Build fully qualified deprecated plugin SDK module specifiers from subpath metadata. */ +export function buildDeprecatedPluginSdkModuleSpecifiers(deprecatedSubpaths?: string[]): string[]; diff --git a/scripts/lib/direct-run.d.mts b/scripts/lib/direct-run.d.mts new file mode 100644 index 000000000000..0b1b5d76d6f3 --- /dev/null +++ b/scripts/lib/direct-run.d.mts @@ -0,0 +1,10 @@ +export function isDirectRunPath( + directPath: string | undefined, + modulePath: string | undefined, + platform?: NodeJS.Platform, +): boolean; +export function isDirectRunUrl( + directPath: string | undefined, + moduleUrl: string, + platform?: NodeJS.Platform, +): boolean; diff --git a/scripts/lib/docker-e2e-plan.d.mts b/scripts/lib/docker-e2e-plan.d.mts new file mode 100644 index 000000000000..63060c0dafde --- /dev/null +++ b/scripts/lib/docker-e2e-plan.d.mts @@ -0,0 +1,85 @@ +import type { + DockerE2eImageKind, + DockerE2eLane, + DockerE2eReleaseProfile, + DockerE2eReleaseProfileInput, +} from "./docker-e2e-scenarios.mjs"; + +export type DockerE2ePlanLane = { + command: string; + imageKind?: DockerE2eImageKind; + live: boolean; + name: string; + noOutputTimeoutMs?: number; + resources: string[]; + stateScenario?: string; + timeoutMs?: number; + weight: number; +}; + +export type DockerE2ePlanOptions = { + includeOpenWebUI: boolean; + liveMode: "all" | "only" | "skip"; + liveRetries: number; + orderLanes: (lanes: T[]) => T[]; + planReleaseAll: boolean; + profile: string; + releaseChunk: string; + releaseProfile?: DockerE2eReleaseProfileInput; + selectedLaneNames: string[]; + timingStore?: unknown; + upgradeSurvivorBaselines?: string; + upgradeSurvivorScenarios?: string; +}; + +export type DockerE2ePlan = { + chunk: string; + credentials: string[]; + imageKinds: DockerE2eImageKind[]; + includeOpenWebUI: boolean; + lanes: DockerE2ePlanLane[]; + mainLanes: DockerE2ePlanLane[]; + needs: { + bareImage: boolean; + e2eImage: boolean; + functionalImage: boolean; + liveImage: boolean; + package: boolean; + }; + profile: string; + releaseProfile?: DockerE2eReleaseProfile; + selectedLanes: DockerE2ePlanLane[]; + tailLanes: DockerE2ePlanLane[]; + version: number; +}; + +export const DEFAULT_LIVE_RETRIES: number; +export const DEFAULT_E2E_BARE_IMAGE: string; +export const DEFAULT_E2E_FUNCTIONAL_IMAGE: string; +export const DEFAULT_E2E_IMAGE: string; +export const DEFAULT_PARALLELISM: number; +export const DEFAULT_PROFILE: string; +export const DEFAULT_RESOURCE_LIMITS: Record; +export const DEFAULT_TAIL_PARALLELISM: number; +export const RELEASE_PATH_PROFILE: string; + +export function normalizeReleaseProfile(raw: unknown): DockerE2eReleaseProfile; +export function parseLaneSelection(raw: unknown): string[]; +export function normalizeUpgradeSurvivorBaselineSpec(raw: unknown): string | undefined; +export function parseLiveMode(raw: unknown): DockerE2ePlanOptions["liveMode"]; +export function parseProfile(raw: unknown): string; +export function laneWeight(poolLane: DockerE2eLane): number; +export function laneResources(poolLane: DockerE2eLane): string[]; +export function laneSummary(poolLane: DockerE2eLane): string; +export function lanesNeedE2eImageKind( + poolLanes: DockerE2eLane[], + kind: DockerE2eImageKind, +): boolean; +export function lanesNeedOpenClawPackage(poolLanes: DockerE2eLane[]): boolean; +export function findLaneByName(name: string): DockerE2eLane | undefined; +export function resolveDockerE2ePlan(options: DockerE2ePlanOptions): { + orderedLanes: DockerE2eLane[]; + orderedTailLanes: DockerE2eLane[]; + plan: DockerE2ePlan; + scheduledLanes: DockerE2eLane[]; +}; diff --git a/scripts/lib/docker-e2e-scenarios.d.mts b/scripts/lib/docker-e2e-scenarios.d.mts new file mode 100644 index 000000000000..79e752ba533e --- /dev/null +++ b/scripts/lib/docker-e2e-scenarios.d.mts @@ -0,0 +1,36 @@ +export type DockerE2eImageKind = "bare" | "functional"; +export type DockerE2eReleaseProfile = "beta" | "stable" | "full"; +export type DockerE2eReleaseProfileInput = "minimum" | DockerE2eReleaseProfile; + +export type DockerE2eLane = { + cacheKey?: string; + command: string; + e2eImageKind?: DockerE2eImageKind; + estimateSeconds?: number; + live: boolean; + name: string; + needsLiveImage?: boolean; + noOutputTimeoutMs?: number; + resources: string[]; + retries: number; + retryPatterns: RegExp[]; + stateScenario?: string; + timeoutMs?: number; + weight: number; +}; + +export const DEFAULT_LIVE_RETRIES: number; +export const BUNDLED_PLUGIN_INSTALL_UNINSTALL_SHARDS: number; +export const mainLanes: DockerE2eLane[]; +export const tailLanes: DockerE2eLane[]; +export function normalizeReleaseProfile( + raw: DockerE2eReleaseProfileInput | null | undefined, +): DockerE2eReleaseProfile; +export function releasePathChunkLanes( + chunk: string, + options?: { includeOpenWebUI?: boolean; releaseProfile?: DockerE2eReleaseProfileInput }, +): DockerE2eLane[]; +export function allReleasePathLanes(options?: { + includeOpenWebUI?: boolean; + releaseProfile?: DockerE2eReleaseProfileInput; +}): DockerE2eLane[]; diff --git a/scripts/lib/extension-import-boundary-checker.d.mts b/scripts/lib/extension-import-boundary-checker.d.mts new file mode 100644 index 000000000000..6e7e3fea9b76 --- /dev/null +++ b/scripts/lib/extension-import-boundary-checker.d.mts @@ -0,0 +1,5 @@ +/** Create a boundary checker with cached inventory collection and a CLI-style main function. */ +export function createExtensionImportBoundaryChecker(params: unknown): { + collectInventory: () => Promise; + main: (argv: unknown, io: unknown) => Promise<1 | 0>; +}; diff --git a/scripts/lib/extension-test-plan.d.mts b/scripts/lib/extension-test-plan.d.mts new file mode 100644 index 000000000000..87ddd5f8ed1c --- /dev/null +++ b/scripts/lib/extension-test-plan.d.mts @@ -0,0 +1,42 @@ +export type ExtensionTestPlanGroup = { + config: string; + estimatedCost: number; + extensionIds: string[]; + roots: string[]; + testFileCount: number; +}; + +export type ExtensionBatchPlan = { + extensionCount: number; + extensionIds: string[]; + estimatedCost: number; + hasTests: boolean; + noTestExtensionIds?: string[]; + planGroups: ExtensionTestPlanGroup[]; + testFileCount: number; +}; + +export type ExtensionTestShard = ExtensionBatchPlan & { + checkName: string; +}; + +export const DEFAULT_EXTENSION_TEST_SHARD_COUNT: number; +export function listTrackedTestFilesForRoots(roots: string[]): string[]; +export function resolveExtensionTestConfig(root: string): string; +export function resolveExtensionTestPlan(params?: { + cwd?: string; + targetArg?: string; +}): ExtensionTestPlanGroup & { + extensionDir: string; + extensionId: string; + hasTests: boolean; +}; +export function resolveExtensionBatchPlan(params?: { + cwd?: string; + extensionIds?: string[]; +}): ExtensionBatchPlan; +export function createExtensionTestShards(params?: { + cwd?: string; + extensionIds?: string[]; + shardCount?: number | string; +}): ExtensionTestShard[]; diff --git a/scripts/lib/extension-vitest-paths.d.mts b/scripts/lib/extension-vitest-paths.d.mts new file mode 100644 index 000000000000..c8f0e800962a --- /dev/null +++ b/scripts/lib/extension-vitest-paths.d.mts @@ -0,0 +1,3 @@ +export function normalizeRelativePath(inputPath: string, cwd?: string): string; +export function relativizeExtensionVitestPath(inputPath: string, cwd?: string): string; +export function relativizeExtensionVitestArgs(vitestArgs: string[], cwd?: string): string[]; diff --git a/scripts/lib/format-generated-module.d.mts b/scripts/lib/format-generated-module.d.mts new file mode 100644 index 000000000000..f4e1579af4b9 --- /dev/null +++ b/scripts/lib/format-generated-module.d.mts @@ -0,0 +1,38 @@ +/** Resolve the fastest available oxfmt command for a generated module path. */ +export function resolveGeneratedModuleFormatter(params: { + existsSync: (value: string) => boolean; + comSpec?: string; + env?: NodeJS.ProcessEnv; + npmExecPath?: string; + outputPath: string; + platform: NodeJS.Platform; + repoRoot: string; +}): + | { + args: string[]; + command: string; + env?: NodeJS.ProcessEnv; + shell: boolean; + windowsVerbatimArguments?: boolean; + } + | { + command: string; + args: unknown[]; + shell: boolean; + }; +/** Format generated source in a temporary file and return the formatter output. */ +export function formatGeneratedModule( + source: unknown, + { + repoRoot, + outputPath, + errorLabel, + }: { + repoRoot: unknown; + outputPath: unknown; + errorLabel: unknown; + }, + deps?: Record, +): string; +export const GENERATED_MODULE_FORMAT_TIMEOUT_MS: 30000; +export const GENERATED_MODULE_FORMAT_MAX_BUFFER_BYTES: number; diff --git a/scripts/lib/guard-inventory-utils.d.mts b/scripts/lib/guard-inventory-utils.d.mts new file mode 100644 index 000000000000..572fe0170a13 --- /dev/null +++ b/scripts/lib/guard-inventory-utils.d.mts @@ -0,0 +1,31 @@ +/** Convert an absolute file path to a repo-relative POSIX path. */ +export function normalizeRepoPath(repoRoot: unknown, filePath: unknown): string; +/** Resolve a relative or absolute module specifier to a repo-relative path. */ +export function resolveRepoSpecifier( + repoRoot: unknown, + specifier: unknown, + importerFile: unknown, +): string | null; +/** Visit static and dynamic module specifiers in a parsed TypeScript source file. */ +export function visitModuleSpecifiers(ts: unknown, sourceFile: unknown, visit: unknown): void; +/** Diff expected and actual inventory entries using JSON identity. */ +export function diffInventoryEntries( + expected: unknown, + actual: unknown, + compareEntries: unknown, +): { + missing: unknown; + unexpected: unknown; +}; +/** Write one line to a stream without each caller repeating newline handling. */ +export function writeLine(stream: unknown, text: unknown): void; +/** Collect import/export/dynamic-import references from source text without full parsing. */ +export function collectModuleReferencesFromSource(source: unknown): unknown[]; +/** Memoize an async factory while resetting the cache after failures. */ +export function createCachedAsync(factory: unknown): () => Promise; +/** Format grouped inventory entries for human-readable guard output. */ +export function formatGroupedInventoryHuman(params: unknown, inventory: unknown): string; +/** Parse TypeScript files and collect sorted inventory entries from each source file. */ +export function collectTypeScriptInventory(params: unknown): Promise; +/** Run a baseline inventory check and return the intended process exit code. */ +export function runBaselineInventoryCheck(params: unknown): Promise<1 | 0>; diff --git a/scripts/lib/kova-report-gate.d.mts b/scripts/lib/kova-report-gate.d.mts new file mode 100644 index 000000000000..26c76debc430 --- /dev/null +++ b/scripts/lib/kova-report-gate.d.mts @@ -0,0 +1,29 @@ +export function evaluateToleratedPartialKovaReport(report: unknown): + | { + ok: boolean; + reason?: undefined; + } + | { + ok: boolean; + reason: string; + }; +export function evaluateToleratedProfiledKovaReport(report: unknown): + | { + ok: boolean; + reason?: undefined; + } + | { + ok: boolean; + reason: string; + }; +export function evaluateToleratedKovaReport(report: unknown): + | { + ok: boolean; + classification: string; + reason?: undefined; + } + | { + ok: boolean; + reason: string; + classification?: undefined; + }; diff --git a/scripts/lib/kova-report-publish-files.d.mts b/scripts/lib/kova-report-publish-files.d.mts new file mode 100644 index 000000000000..559e60887fd0 --- /dev/null +++ b/scripts/lib/kova-report-publish-files.d.mts @@ -0,0 +1,15 @@ +#!/usr/bin/env node +export function copyBundleMetadata({ + bundleDir, + destinationDir, +}: { + bundleDir: unknown; + destinationDir: unknown; +}): string[]; +export function assertPublishedFileSizeLimit({ + publishRoot, + maxFileBytes, +}: { + publishRoot: unknown; + maxFileBytes: unknown; +}): number; diff --git a/scripts/lib/kova-report-selector.d.mts b/scripts/lib/kova-report-selector.d.mts new file mode 100644 index 000000000000..81612462f45a --- /dev/null +++ b/scripts/lib/kova-report-selector.d.mts @@ -0,0 +1,2 @@ +#!/usr/bin/env node +export function selectKovaReport(reportDir: unknown): string; diff --git a/scripts/lib/kova-workflow-evidence.d.mts b/scripts/lib/kova-workflow-evidence.d.mts new file mode 100644 index 000000000000..b0ed30eaae94 --- /dev/null +++ b/scripts/lib/kova-workflow-evidence.d.mts @@ -0,0 +1,24 @@ +export function validateKovaWorkflowEvidence({ + plan, + report, + profile, + target, + repeat, + includeFilters, + authMode, + expectedModel, +}: { + plan: unknown; + report: unknown; + profile: unknown; + target: unknown; + repeat: unknown; + includeFilters: unknown; + authMode: unknown; + expectedModel: unknown; +}): { + authMode: unknown; + pairCount: number; + recordCount: unknown; + repeat: unknown; +}; diff --git a/scripts/lib/local-heavy-check-runtime.d.mts b/scripts/lib/local-heavy-check-runtime.d.mts new file mode 100644 index 000000000000..3c7eeafb1aa2 --- /dev/null +++ b/scripts/lib/local-heavy-check-runtime.d.mts @@ -0,0 +1,44 @@ +/** Return whether local-heavy-check safeguards are enabled for an environment. */ +export type LocalHostResources = { + logicalCpuCount: number; + totalMemoryBytes: number; +}; +export function isLocalCheckEnabled(env: NodeJS.ProcessEnv): boolean; +/** Ensure local check runs opt into safeguard environment outside CI. */ +export function resolveLocalHeavyCheckEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv; +/** Apply local tsgo defaults for declaration skipping, caching, throttling, and profiling. */ +export function applyLocalTsgoPolicy( + args: string[], + env: NodeJS.ProcessEnv, + hostResources: LocalHostResources, +): { + env: NodeJS.ProcessEnv; + args: string[]; +}; +/** Apply local oxlint defaults for type-aware checking and throttled worker settings. */ +export function applyLocalOxlintPolicy( + args: string[], + env: NodeJS.ProcessEnv, + hostResources: LocalHostResources, +): { + env: NodeJS.ProcessEnv; + args: string[]; +}; +/** Decide whether an oxlint invocation needs the local heavy-check lock. */ +export function shouldAcquireLocalHeavyCheckLockForOxlint( + args: string[], + { + cwd, + env, + }?: { + cwd?: string | undefined; + env?: NodeJS.ProcessEnv | undefined; + }, +): boolean; +/** Decide whether a tsgo invocation needs the local heavy-check lock. */ +export function shouldAcquireLocalHeavyCheckLockForTsgo( + args: string[], + env?: NodeJS.ProcessEnv, +): boolean; +/** Acquire a filesystem lock for one local heavy check and return its release callback. */ +export function acquireLocalHeavyCheckLockSync(params: unknown): () => void; diff --git a/scripts/lib/managed-child-process.d.mts b/scripts/lib/managed-child-process.d.mts new file mode 100644 index 000000000000..77e1b4306060 --- /dev/null +++ b/scripts/lib/managed-child-process.d.mts @@ -0,0 +1,142 @@ +/** + * Return conventional shell exit code for a signal. + * + * @param {NodeJS.Signals} signal + * @returns {number} + */ +export function signalExitCode(signal: NodeJS.Signals): number; +/** + * @param {import("node:child_process").ChildProcess} child + * @param {NodeJS.Signals} [signal] + * @param {{ platform?: NodeJS.Platform; runTaskkill?: typeof spawnSync }} [options] + */ +export function terminateManagedChild( + child: Pick, + signal?: NodeJS.Signals, + { + platform, + runTaskkill, + }?: { + platform?: NodeJS.Platform; + runTaskkill?: (command: string, args?: string[]) => { error?: Error; status: number | null }; + }, +): void; +/** + * Run a child command while forwarding termination signals to the managed process group. + * + * @param {{ + * bin: string; + * args?: string[]; + * cwd?: string; + * env?: NodeJS.ProcessEnv; + * stdio?: import("node:child_process").StdioOptions; + * shell?: boolean; + * windowsVerbatimArguments?: boolean; + * platform?: NodeJS.Platform; + * comSpec?: string; + * onReady?: (child: import("node:child_process").ChildProcess) => void; + * }} options + * @returns {Promise} + */ +export function runManagedCommand({ + bin, + args, + cwd, + env, + stdio, + platform, + shell, + windowsVerbatimArguments, + comSpec, + onReady, +}: { + bin: string; + args?: string[]; + cwd?: string; + env?: NodeJS.ProcessEnv; + stdio?: import("node:child_process").StdioOptions; + shell?: boolean; + windowsVerbatimArguments?: boolean; + platform?: NodeJS.Platform; + comSpec?: string; + onReady?: (child: import("node:child_process").ChildProcess) => void; +}): Promise; +/** + * @param {{ + * bin: string; + * args?: string[]; + * cwd?: string; + * env?: NodeJS.ProcessEnv; + * stdio?: import("node:child_process").StdioOptions; + * shell?: boolean; + * windowsVerbatimArguments?: boolean; + * platform?: NodeJS.Platform; + * comSpec?: string; + * }} options + */ +export function createManagedCommandSpawnSpec({ + bin, + args, + cwd, + env, + stdio, + platform, + shell, + windowsVerbatimArguments, + comSpec, +}: { + bin: string; + args?: string[]; + cwd?: string; + env?: NodeJS.ProcessEnv; + stdio?: import("node:child_process").StdioOptions; + shell?: boolean; + windowsVerbatimArguments?: boolean; + platform?: NodeJS.Platform; + comSpec?: string; +}): { + args: string[]; + command: string; + options: { + cwd: string | undefined; + env: NodeJS.ProcessEnv | undefined; + stdio: import("node:child_process").StdioOptions; + shell: boolean; + detached: boolean; + windowsVerbatimArguments: boolean | undefined; + }; +}; +/** + * @param {{ + * bin: string; + * args?: string[]; + * env?: NodeJS.ProcessEnv; + * shell?: boolean; + * windowsVerbatimArguments?: boolean; + * platform?: NodeJS.Platform; + * comSpec?: string; + * }} options + */ +export function createManagedCommandInvocation({ + bin, + args, + env, + platform, + shell, + windowsVerbatimArguments, + comSpec, +}: { + bin: string; + args?: string[]; + env?: NodeJS.ProcessEnv; + shell?: boolean; + windowsVerbatimArguments?: boolean; + platform?: NodeJS.Platform; + comSpec?: string; +}): { + args: string[]; + command: string; + shell: boolean; + windowsVerbatimArguments: boolean | undefined; +}; +import { spawnSync } from "node:child_process"; diff --git a/scripts/lib/merge-head-diff-base.d.mts b/scripts/lib/merge-head-diff-base.d.mts new file mode 100644 index 000000000000..86ae6839f18b --- /dev/null +++ b/scripts/lib/merge-head-diff-base.d.mts @@ -0,0 +1,19 @@ +/** Resolve the git base ref to use when diffing a merge head. */ +export function resolveMergeHeadDiffBase({ + base, + head, + cwd, + maxBuffer, + preferFirstParent, +}: { + base: unknown; + head?: string | undefined; + cwd?: string | undefined; + maxBuffer?: number | undefined; + preferFirstParent?: boolean | undefined; +}): unknown; +export function parseArgs(argv: unknown): { + base: string; + head: string; + preferFirstParent: boolean; +}; diff --git a/scripts/lib/package-root-args.d.mts b/scripts/lib/package-root-args.d.mts new file mode 100644 index 000000000000..aaed7c657aa9 --- /dev/null +++ b/scripts/lib/package-root-args.d.mts @@ -0,0 +1,7 @@ +/** Parse `--package-root` or an environment fallback into an absolute package root. */ +export function parsePackageRootArg( + argv: unknown, + envName: unknown, +): { + packageRoot: string; +}; diff --git a/scripts/lib/plain-gh.d.mts b/scripts/lib/plain-gh.d.mts new file mode 100644 index 000000000000..0d4f4510e35f --- /dev/null +++ b/scripts/lib/plain-gh.d.mts @@ -0,0 +1,39 @@ +import type { + ExecFileSyncOptions, + ExecFileSyncOptionsWithBufferEncoding, + ExecFileSyncOptionsWithStringEncoding, + SpawnSyncOptions, + SpawnSyncOptionsWithBufferEncoding, + SpawnSyncOptionsWithStringEncoding, + SpawnSyncReturns, +} from "node:child_process"; + +export function plainGhEnv(env?: NodeJS.ProcessEnv): { + [key: string]: string | undefined; +}; +export function resolvePlainGhBin(env?: NodeJS.ProcessEnv, systemCandidates?: string[]): string; +export function execPlainGh( + args: readonly string[], + options: ExecFileSyncOptionsWithStringEncoding, +): string; +export function execPlainGh( + args: readonly string[], + options?: ExecFileSyncOptionsWithBufferEncoding, +): NonSharedBuffer; +export function execPlainGh( + args: readonly string[], + options?: ExecFileSyncOptions, +): string | NonSharedBuffer; +export function spawnPlainGh( + args: readonly string[], + options: SpawnSyncOptionsWithStringEncoding, +): SpawnSyncReturns; +export function spawnPlainGh( + args: readonly string[], + options?: SpawnSyncOptionsWithBufferEncoding, +): SpawnSyncReturns; +export function spawnPlainGh( + args: readonly string[], + options?: SpawnSyncOptions, +): SpawnSyncReturns; +export const PLAIN_GH_SYSTEM_CANDIDATES: string[]; diff --git a/scripts/lib/plugin-contract-test-plan.d.mts b/scripts/lib/plugin-contract-test-plan.d.mts new file mode 100644 index 000000000000..e6dd457ba761 --- /dev/null +++ b/scripts/lib/plugin-contract-test-plan.d.mts @@ -0,0 +1,7 @@ +/** Create balanced plugin contract test shards for CI check planning. */ +export function createPluginContractTestShards(): { + checkName: string; + includePatterns: string[]; + runtime: string; + task: string; +}[]; diff --git a/scripts/lib/plugin-gateway-gauntlet.d.mts b/scripts/lib/plugin-gateway-gauntlet.d.mts new file mode 100644 index 000000000000..e7bcef8af633 --- /dev/null +++ b/scripts/lib/plugin-gateway-gauntlet.d.mts @@ -0,0 +1,133 @@ +export function collectQaBaselineRegressionObservations( + rows: unknown, + thresholds?: Record, +): ( + | { + kind: string; + pluginId: unknown; + cpuCoreRatio: unknown; + baselineCpuCoreRatio: unknown; + multiplier: unknown; + wallMs?: undefined; + baselineWallMs?: undefined; + } + | { + kind: string; + pluginId: unknown; + wallMs: unknown; + baselineWallMs: unknown; + multiplier: unknown; + cpuCoreRatio?: undefined; + baselineCpuCoreRatio?: undefined; + } +)[]; +export type PluginGatewayEntry = { + id: string; + requiredPlugins?: string[]; +}; +export function collectPluginsWithRequiredEntries( + entries: T[], + plugins: T[], +): T[]; +export function collectRequiredPluginEntries( + entries: T[], + plugins: T[], +): T[]; +export function collectGatewayCpuObservations(params: unknown): ( + | { + kind: string; + id: string; + cpuCoreRatioMax: number; + wallMsMax: number; + cpuCoreRatio?: undefined; + wallMs?: undefined; + } + | { + kind: string; + id: string; + cpuCoreRatio: number; + wallMs: number; + cpuCoreRatioMax?: undefined; + wallMsMax?: undefined; + } +)[]; +export function collectMetricObservations( + rows: unknown, + thresholds?: Record, +): ( + | { + coldStart?: true | undefined; + kind: string; + pluginId: unknown; + phase: unknown; + cpuCoreRatio: number; + wallMs: number; + } + | { + coldStart?: true | undefined; + kind: string; + pluginId: unknown; + phase: unknown; + wallMs: unknown; + medianWallMs: unknown; + multiplier: unknown; + } + | { + coldStart?: true | undefined; + kind: string; + pluginId: unknown; + phase: unknown; + maxRssMb: unknown; + thresholdMb: number; + } + | { + coldStart?: true | undefined; + kind: string; + pluginId: unknown; + phase: unknown; + maxRssMb: unknown; + medianRssMb: unknown; + multiplier: unknown; + } +)[]; +export function buildGauntletPrebuildEnv(env: unknown, options?: Record): unknown; +export function detectCommandDiagnosticFailure( + stdout: unknown, + stderr: unknown, +): "plugin-load-failure" | null; +export function discoverBundledPluginManifests(repoRoot: unknown): { + id: unknown; + buildId: string; + name: unknown; + dir: string; + manifestPath: string; + enabledByDefault: boolean; + activation: unknown; + providers: string[]; + channels: string[]; + skills: string[]; + authMethods: unknown; + onboardingScopes: unknown[]; + requiredPlugins: string[]; + hasConfigSchema: boolean; + hasRequiredConfigFields: boolean; + commandAliases: unknown; + cliCommandAliases: unknown; + runtimeSlashAliases: unknown; +}[]; +export function readQaSuiteSummary(summaryPath: unknown): + | { + diagnosticFailure: string; + diagnosticDetail: string; + summary: unknown; + } + | { + diagnosticFailure: null; + diagnosticDetail: null; + summary: unknown; + }; +export function schemaHasRequiredFields(schema: unknown, seen?: Set): boolean; +export function selectPluginEntries( + entries: T[], + options?: { ids?: string[]; shardIndex?: number; shardTotal?: number }, +): T[]; diff --git a/scripts/lib/plugin-npm-package-manifest.d.mts b/scripts/lib/plugin-npm-package-manifest.d.mts new file mode 100644 index 000000000000..2643afb937d5 --- /dev/null +++ b/scripts/lib/plugin-npm-package-manifest.d.mts @@ -0,0 +1,66 @@ +/** Resolve an npm command invocation for plugin package scripts. */ +export function resolvePluginNpmCommand( + args: unknown, + params?: Record, +): { + args: string[]; + command: string; + env?: NodeJS.ProcessEnv; + shell: boolean; + windowsVerbatimArguments?: boolean; +}; +/** Build the package.json that should be used while packaging a plugin for npm. */ +export function resolveAugmentedPluginNpmPackageJson(params: unknown): + | { + packageJsonPath: string; + packageDir: unknown; + repoRoot: string; + changed: boolean; + packageJson: undefined; + reason: string; + pluginDir?: undefined; + bundleDependencies?: undefined; + } + | { + packageJsonPath: string; + packageDir: unknown; + repoRoot: string; + changed: boolean; + packageJson: Record; + pluginDir: string; + bundleDependencies: boolean; + reason: string; + }; +/** Read generated bundled channel config metadata keyed by plugin id. */ +export function readGeneratedBundledChannelConfigs(repoRoot: unknown): Map; +/** Merge generated channel config schemas into a plugin manifest without clobbering labels. */ +export function mergeGeneratedChannelConfigs( + manifest: unknown, + generatedChannelConfigs: unknown, +): unknown; +/** Build the plugin manifest that should be used while packaging a plugin for npm. */ +export function resolveAugmentedPluginNpmManifest(params: unknown): { + manifestPath: string; + pluginId: unknown; + changed: boolean; + manifest: unknown; + reason: string; +}; +/** Temporarily write augmented manifest/package metadata while a packaging callback runs. */ +export function withAugmentedPluginNpmManifestForPackage( + params: unknown, + callback: unknown, +): unknown; +export function parseRunArgs(argv: unknown): + | { + help: true; + packageDir: string; + command: string; + args: string[]; + } + | { + packageDir: string; + command: string; + args: string[]; + help?: undefined; + }; diff --git a/scripts/lib/plugin-npm-runtime-assets.d.mts b/scripts/lib/plugin-npm-runtime-assets.d.mts new file mode 100644 index 000000000000..f86b7b9aaf10 --- /dev/null +++ b/scripts/lib/plugin-npm-runtime-assets.d.mts @@ -0,0 +1,4 @@ +/** Run a package-local static asset build command when the plugin declares one. */ +export function runPackageAssetBuild(plan: unknown): string | null; +/** List static asset source paths referenced by a package but missing from disk. */ +export function listMissingPackageStaticAssetSources(plan: unknown): string[]; diff --git a/scripts/lib/plugin-npm-runtime-build.d.mts b/scripts/lib/plugin-npm-runtime-build.d.mts new file mode 100644 index 000000000000..ba4fe54cc87c --- /dev/null +++ b/scripts/lib/plugin-npm-runtime-build.d.mts @@ -0,0 +1,66 @@ +/** List extension package dirs whose package metadata enables artifact publishing. */ +export function listPublishablePluginPackageDirs(params?: Record): string[]; +/** List package-local runtime output files expected from a runtime build plan. */ +export function listPluginNpmRuntimeBuildOutputs(plan: unknown): string[]; +/** Resolve the package-local runtime build plan for one publishable plugin package. */ +export function resolvePluginNpmRuntimeBuildPlan(params: unknown): { + runtimeBuildOutputs: string[]; + packageFiles: string[]; + packagePeerMetadata: { + peerDependencies: { + openclaw: string; + }; + peerDependenciesMeta: { + openclaw: { optional: boolean }; + }; + }; + repoRoot: string; + packageDir: string; + pluginDir: string; + packageJson: { + openclaw: { compat: { pluginApi: string } }; + [key: string]: unknown; + }; + rootPackageJson: Record; + sourceEntries: string[]; + entry: { + [k: string]: string; + }; + outDir: string; + runtimeExtensions: string[]; + runtimeSetupEntry: string | undefined; +} | null; +/** Build package-local runtime files and static assets for one plugin package. */ +export function buildPluginNpmRuntime(params: unknown): Promise<{ + assetBuildCommand: string | null; + copiedStaticAssets: string[]; + runtimeBuildOutputs: string[]; + packageFiles: unknown[]; + packagePeerMetadata: { + peerDependencies: { + openclaw: string; + }; + peerDependenciesMeta: unknown; + }; + repoRoot: string; + packageDir: unknown; + pluginDir: string; + packageJson: unknown; + rootPackageJson: unknown; + sourceEntries: unknown[]; + entry: { + [k: string]: string; + }; + outDir: string; + runtimeExtensions: unknown; + runtimeSetupEntry: string | undefined; +} | null>; +export function parseArgs(argv: unknown): + | { + help: boolean; + packageDir: string; + } + | { + packageDir: unknown; + help?: undefined; + }; diff --git a/scripts/lib/plugin-package-dependencies.d.mts b/scripts/lib/plugin-package-dependencies.d.mts index 0ff21b4d946a..df9c5d1f92fe 100644 --- a/scripts/lib/plugin-package-dependencies.d.mts +++ b/scripts/lib/plugin-package-dependencies.d.mts @@ -1,6 +1,7 @@ export type RuntimeDependencyPackageJson = { - dependencies?: Record; - optionalDependencies?: Record; + dependencies?: Record; + devDependencies?: Record; + optionalDependencies?: Record; }; export function collectRuntimeDependencySpecs( packageJson?: RuntimeDependencyPackageJson, diff --git a/scripts/lib/plugin-prerelease-test-plan.d.mts b/scripts/lib/plugin-prerelease-test-plan.d.mts new file mode 100644 index 000000000000..fd6966e4cff4 --- /dev/null +++ b/scripts/lib/plugin-prerelease-test-plan.d.mts @@ -0,0 +1,18 @@ +export type PluginPrereleaseStaticCheck = { + check: string; + checkName: string; + command: string; + surfaces: string[]; +}; + +export type PluginPrereleaseTestPlan = { + dockerLanes: string[]; + staticChecks: PluginPrereleaseStaticCheck[]; + surfaces: string[]; +}; + +export const PLUGIN_PRERELEASE_REQUIRED_SURFACES: readonly string[]; +export function createPluginPrereleaseTestPlan(): PluginPrereleaseTestPlan; +export function assertPluginPrereleaseTestPlanComplete( + plan?: PluginPrereleaseTestPlan, +): PluginPrereleaseTestPlan; diff --git a/scripts/lib/plugin-sdk-declaration-budget.d.mts b/scripts/lib/plugin-sdk-declaration-budget.d.mts new file mode 100644 index 000000000000..7afab45de1b3 --- /dev/null +++ b/scripts/lib/plugin-sdk-declaration-budget.d.mts @@ -0,0 +1,14 @@ +export function isPrivateQaPluginSdkBuild(env: unknown): boolean; +export function evaluatePluginSdkDeclarationBudget({ + declarationBytes, + buildPrivateQa, +}: { + declarationBytes: unknown; + buildPrivateQa: unknown; +}): { + budgetBytes: number; + budgetKind: string; + shouldFail: boolean; +}; +export const MAX_PUBLIC_PLUGIN_SDK_DECLARATION_BYTES: 5050000; +export const MAX_PRIVATE_QA_PUBLIC_PLUGIN_SDK_DECLARATION_BYTES: 5075000; diff --git a/scripts/lib/release-upgrade-baseline.d.mts b/scripts/lib/release-upgrade-baseline.d.mts new file mode 100644 index 000000000000..94109e8183a1 --- /dev/null +++ b/scripts/lib/release-upgrade-baseline.d.mts @@ -0,0 +1,6 @@ +export function compareOpenClawVersions(leftVersion: unknown, rightVersion: unknown): number; +export function resolveDefaultReleaseUpgradeBaseline( + candidateVersion: unknown, + publishedVersions: unknown, +): string; +export function parseArgs(argv: unknown): Map; diff --git a/scripts/lib/report-cli-helpers.d.mts b/scripts/lib/report-cli-helpers.d.mts new file mode 100644 index 000000000000..8295921c67bc --- /dev/null +++ b/scripts/lib/report-cli-helpers.d.mts @@ -0,0 +1,9 @@ +export function parseReportCliArgs(argv: string[]): { + rootDir: string; + jsonPath: string | null; + markdownPath: string | null; +}; +/** + * Writes an optional report artifact, creating its parent directory first. + */ +export function writeReportArtifact(filePath: string | null, content: string): Promise; diff --git a/scripts/lib/source-file-scan-cache.d.mts b/scripts/lib/source-file-scan-cache.d.mts new file mode 100644 index 000000000000..c265958eb95f --- /dev/null +++ b/scripts/lib/source-file-scan-cache.d.mts @@ -0,0 +1,15 @@ +export function mapWithConcurrency( + items: Item[], + concurrency: number, + mapper: (item: Item, index: number) => Promise, +): Promise; + +export function collectSourceFileContents(params: { + repoRoot: string; + scanRoots: string[]; + scanExtensions: Set; + ignoredDirNames: Set; + maxConcurrentReads?: number; + maxFileBytes?: number; + readFile?: (filePath: string) => Promise; +}): Promise>; diff --git a/scripts/lib/stable-release-closeout.d.mts b/scripts/lib/stable-release-closeout.d.mts new file mode 100644 index 000000000000..f83c0f6568b3 --- /dev/null +++ b/scripts/lib/stable-release-closeout.d.mts @@ -0,0 +1,29 @@ +export function parseStableReleaseTag(tag: unknown): string; +export function extractStableChangelogSection(changelog: unknown, version: unknown): unknown; +export function verifyStableMainCloseout(params: unknown): + | { + errors: string[]; + manifest: null; + } + | { + errors: string[]; + manifest: { + version: number; + releaseTag: unknown; + releaseVersion: unknown; + releaseTagSha: unknown; + mainSha: unknown; + mainPackageVersion: string; + releaseTagPackageVersion: string; + changelogSha256: string; + appcastSha256: string; + fullReleaseValidationRunId: unknown; + fullReleaseValidationRunAttempt: unknown; + releasePublishRunId: unknown; + rollbackDrill: { + id: unknown; + date: unknown; + }; + githubReleaseAssets: unknown; + }; + }; diff --git a/scripts/lib/static-extension-assets.d.mts b/scripts/lib/static-extension-assets.d.mts new file mode 100644 index 000000000000..8a756b356a2d --- /dev/null +++ b/scripts/lib/static-extension-assets.d.mts @@ -0,0 +1,28 @@ +/** + * Discovers static asset copy specs from extension package metadata. + */ +export function discoverStaticExtensionAssets(params?: Record): { + pluginDir: unknown; + src: string; + dest: string; +}[]; +/** + * Lists generated dist output paths for declared static extension assets. + */ +export function listStaticExtensionAssetOutputs(params?: Record): unknown; +/** + * Lists source file paths for declared static extension assets. + */ +export function listStaticExtensionAssetSources(params?: Record): unknown; +/** + * Copies declared static extension assets from source packages into root dist. + */ +export function copyStaticExtensionAssets(params?: Record): void; +/** + * Copies static assets into the dist-runtime overlay from source or root dist. + */ +export function copyStaticExtensionAssetsToRuntimeOverlay(params?: Record): void; +/** + * Copies declared static assets for one package runtime build. + */ +export function copyStaticExtensionAssetsForPackage(params: unknown): string[]; diff --git a/scripts/lib/test-group-report.d.mts b/scripts/lib/test-group-report.d.mts new file mode 100644 index 000000000000..e76917f2e4ad --- /dev/null +++ b/scripts/lib/test-group-report.d.mts @@ -0,0 +1,64 @@ +export type GroupedCounter = { + configs: string[]; + durationMs: number; + fileCount: number; + key: string; + testCount: number; +}; + +export type GroupedFile = { + config: string; + durationMs: number; + file: string; + group: string; + testCount: number; +}; + +export type GroupedTestReport = { + configs: GroupedCounter[]; + generatedAt: string; + groupBy: string; + groups: GroupedCounter[]; + slowTests: Array<{ + config: string; + durationMs: number; + file: string; + fullName: string; + status: string; + }>; + topFiles: GroupedFile[]; + totals: { durationMs: number; fileCount: number; testCount: number }; +}; + +export type GroupedTestComparison = { + files: Array< + Record & { file: string; status: string; delta: Record } + >; + groups: Array & { key: string; delta: Record }>; + runs: Array & { key: string; delta: Record }>; + totals: { delta: { durationMs: number; fileCount: number; testCount: number } }; +}; + +export function formatBytesAsMb(valueBytes: number | null | undefined): string; +export function normalizeConfigLabel(config: string): string; +export function resolveTestArea(file: string): string; +export function resolveGroupKey(file: string, mode?: string): string; +export function buildGroupedTestReport(params: { + groupBy: string; + maxTestMs?: number; + reports: Array<{ config: string; report: unknown }>; +}): GroupedTestReport; +export function buildGroupedTestComparison(params: { + afterPath?: string; + beforePath?: string; + after: Record; + before: Record; +}): GroupedTestComparison; +export function renderGroupedTestComparison( + comparison: GroupedTestComparison, + options?: { limit?: number; topFiles?: number }, +): string; +export function renderGroupedTestReport( + report: GroupedTestReport, + options?: { limit?: number; topFiles?: number }, +): string; diff --git a/scripts/lib/ts-guard-utils.d.mts b/scripts/lib/ts-guard-utils.d.mts new file mode 100644 index 000000000000..c6d126efa5df --- /dev/null +++ b/scripts/lib/ts-guard-utils.d.mts @@ -0,0 +1,60 @@ +/** + * Resolves the repository root by walking upward from the caller module. + */ +export function resolveRepoRoot(importMetaUrl: string): string; +/** + * Converts repo-relative source roots into absolute paths. + */ +export function resolveSourceRoots(repoRoot: string, relativeRoots: string[]): string[]; +/** + * Recursively collects TypeScript files under a file or directory target. + */ +export function collectTypeScriptFiles( + targetPath: string, + options?: { + extraTestSuffixes?: string[]; + ignoreMissing?: boolean; + includeTests?: boolean; + skipNodeModules?: boolean; + }, +): Promise; +/** + * Collects TypeScript files from multiple roots, ignoring missing roots by default. + */ +export function collectTypeScriptFilesFromRoots( + sourceRoots: string[], + options?: { + extraTestSuffixes?: string[]; + includeTests?: boolean; + skipNodeModules?: boolean; + }, +): Promise; +/** + * Runs a guard's violation scanner across collected TypeScript source files. + */ +export function collectFileViolations(params: unknown): Promise; +/** + * Returns the one-based source line for a TypeScript AST node. + */ +export function toLine(sourceFile: ts.SourceFile, node: ts.Node): number; +/** + * Extracts text from identifier, string, or numeric property names. + */ +export function getPropertyNameText(name: ts.PropertyName): string | null; +/** + * Removes harmless expression wrappers before AST shape checks. + */ +export function unwrapExpression(expression: ts.Expression): ts.Expression; +/** + * Collects one-based line numbers for call expressions selected by a callback. + */ +export function collectCallExpressionLines( + tsImpl: typeof ts, + sourceFile: ts.SourceFile, + resolveLineNode: (call: ts.CallExpression) => ts.Node | null | undefined, +): number[]; +/** + * Runs a script main function only when the module is the direct entrypoint. + */ +export function runAsScript(importMetaUrl: string, main: () => Promise): void; +import type ts from "typescript"; diff --git a/scripts/lib/tsgo-sparse-guard.d.mts b/scripts/lib/tsgo-sparse-guard.d.mts new file mode 100644 index 000000000000..f8df50b62515 --- /dev/null +++ b/scripts/lib/tsgo-sparse-guard.d.mts @@ -0,0 +1,28 @@ +/** + * Reports whether the caller explicitly opted out of sparse tsgo guard errors. + */ +export function shouldSkipSparseTsgoGuardError(env?: NodeJS.ProcessEnv): boolean; +/** + * Creates an environment that suppresses recursive sparse tsgo guard checks. + */ +export function createSparseTsgoSkipEnv(baseEnv?: NodeJS.ProcessEnv): { + OPENCLAW_TSGO_SPARSE_SKIP: string; +}; +/** + * Builds the sparse-checkout diagnostic for core tsgo projects, when needed. + */ +export function getSparseTsgoGuardError( + args: unknown, + { + cwd, + fileExists, + isSparseCheckoutEnabled, + sparseCheckoutPatterns, + }?: { + cwd?: string | undefined; + fileExists?: typeof fs.existsSync | undefined; + isSparseCheckoutEnabled?: (options: { cwd: string }) => boolean; + sparseCheckoutPatterns?: string[]; + }, +): string | null; +import fs from "node:fs"; diff --git a/scripts/lib/vitest-batch-runner.d.mts b/scripts/lib/vitest-batch-runner.d.mts new file mode 100644 index 000000000000..6564f2b26c8c --- /dev/null +++ b/scripts/lib/vitest-batch-runner.d.mts @@ -0,0 +1,10 @@ +export type VitestBatchRunParams = { + args: string[]; + config: string; + env?: NodeJS.ProcessEnv; + targets: string[]; +}; + +export function runVitestBatch(params: VitestBatchRunParams): Promise; +export function buildVitestBatchPnpmArgs(params: VitestBatchRunParams): string[]; +export function isDirectScriptRun(metaUrl: string): boolean; diff --git a/scripts/lib/vitest-shard-timings.d.mts b/scripts/lib/vitest-shard-timings.d.mts new file mode 100644 index 000000000000..15f8a2dc0fc2 --- /dev/null +++ b/scripts/lib/vitest-shard-timings.d.mts @@ -0,0 +1,24 @@ +/** + * Resolves the stable timing key for a Vitest shard specification. + */ +export function resolveShardTimingKey(spec: unknown): unknown; +/** + * Creates a timing sample for completed non-watch Vitest shard runs. + */ +export function createShardTimingSample( + spec: unknown, + durationMs: unknown, +): { + baseConfig: unknown; + config: unknown; + durationMs: unknown; + includePatternCount: unknown; +} | null; +/** + * Reads persisted shard timing averages, returning an empty map when disabled. + */ +export function readShardTimings(cwd?: string, env?: NodeJS.ProcessEnv): Map; +/** + * Merges new shard timing samples into the persisted local timing artifact. + */ +export function writeShardTimings(samples: unknown, cwd?: string, env?: NodeJS.ProcessEnv): void; diff --git a/scripts/mantis/build-telegram-desktop-proof-evidence.d.mts b/scripts/mantis/build-telegram-desktop-proof-evidence.d.mts new file mode 100644 index 000000000000..f79b4a733286 --- /dev/null +++ b/scripts/mantis/build-telegram-desktop-proof-evidence.d.mts @@ -0,0 +1,161 @@ +#!/usr/bin/env node +/** + * Builds the manifest for paired baseline/candidate Telegram Desktop proof artifacts. + */ +export function buildTelegramDesktopProofManifest({ + baseline, + baselineRef, + baselineSha, + candidate, + candidateRef, + candidateSha, + scenarioLabel, +}: { + baseline: unknown; + baselineRef: unknown; + baselineSha: unknown; + candidate: unknown; + candidateRef: unknown; + candidateSha: unknown; + scenarioLabel: unknown; +}): { + schemaVersion: number; + id: string; + title: string; + summary: string; + scenario: unknown; + comparison: { + baseline: { + expected: string; + status: string; + ref?: unknown; + sha?: unknown; + }; + candidate: { + expected: string; + status: string; + fixed: boolean; + ref?: unknown; + sha?: unknown; + }; + pass: boolean; + }; + artifacts: ( + | { + alt: string; + inline: boolean; + kind: string; + label: string; + lane: string; + path: string; + targetPath: string; + width: number; + required?: undefined; + } + | { + kind: string; + label: string; + lane: string; + path: string; + required: boolean; + targetPath: string; + alt?: undefined; + inline?: undefined; + width?: undefined; + } + | { + alt: string; + inline: boolean; + kind: string; + label: string; + lane: string; + path: string; + required: boolean; + targetPath: string; + width?: undefined; + } + | { + kind: string; + label: string; + lane: string; + path: string; + targetPath: string; + alt?: undefined; + inline?: undefined; + width?: undefined; + required?: undefined; + } + )[]; +}; +export function writeTelegramDesktopProofEvidence(rawArgs?: string[]): { + manifest: { + schemaVersion: number; + id: string; + title: string; + summary: string; + scenario: unknown; + comparison: { + baseline: { + expected: string; + status: string; + ref?: unknown; + sha?: unknown; + }; + candidate: { + expected: string; + status: string; + fixed: boolean; + ref?: unknown; + sha?: unknown; + }; + pass: boolean; + }; + artifacts: ( + | { + alt: string; + inline: boolean; + kind: string; + label: string; + lane: string; + path: string; + targetPath: string; + width: number; + required?: undefined; + } + | { + kind: string; + label: string; + lane: string; + path: string; + required: boolean; + targetPath: string; + alt?: undefined; + inline?: undefined; + width?: undefined; + } + | { + alt: string; + inline: boolean; + kind: string; + label: string; + lane: string; + path: string; + required: boolean; + targetPath: string; + width?: undefined; + } + | { + kind: string; + label: string; + lane: string; + path: string; + targetPath: string; + alt?: undefined; + inline?: undefined; + width?: undefined; + required?: undefined; + } + )[]; + }; + manifestPath: string; +}; diff --git a/scripts/mantis/build-telegram-evidence.d.mts b/scripts/mantis/build-telegram-evidence.d.mts new file mode 100644 index 000000000000..1b4f9c8e3853 --- /dev/null +++ b/scripts/mantis/build-telegram-evidence.d.mts @@ -0,0 +1,133 @@ +#!/usr/bin/env node +/** + * Renders a self-contained Telegram evidence HTML report. + */ +export function renderTelegramEvidenceHtml({ + observedMessages, + summary, +}: { + observedMessages: unknown; + summary: unknown; +}): string; +export function buildTelegramEvidenceManifest({ + candidateRef, + candidateSha, + hasObservedMessages, + scenarioLabel, + summary, + summaryArtifactPath, +}: { + candidateRef: unknown; + candidateSha: unknown; + hasObservedMessages?: boolean | undefined; + scenarioLabel: unknown; + summary: unknown; + summaryArtifactPath?: string | undefined; +}): { + schemaVersion: number; + id: string; + title: string; + summary: string; + scenario: unknown; + comparison: { + candidate: { + expected: string; + status: string; + fixed: boolean; + ref?: unknown; + sha?: unknown; + }; + pass: boolean; + }; + artifacts: ( + | { + kind: string; + lane: string; + label: string; + path: string; + targetPath: string; + alt: string; + width: number; + inline: boolean; + required: boolean; + } + | { + kind: string; + lane: string; + label: string; + path: string; + targetPath: string; + required: boolean; + alt?: undefined; + width?: undefined; + inline?: undefined; + } + | { + kind: string; + lane: string; + label: string; + path: string; + targetPath: string; + alt?: undefined; + width?: undefined; + inline?: undefined; + required?: undefined; + } + )[]; +}; +export function writeTelegramEvidence(rawArgs?: string[]): { + manifest: { + schemaVersion: number; + id: string; + title: string; + summary: string; + scenario: unknown; + comparison: { + candidate: { + expected: string; + status: string; + fixed: boolean; + ref?: unknown; + sha?: unknown; + }; + pass: boolean; + }; + artifacts: ( + | { + kind: string; + lane: string; + label: string; + path: string; + targetPath: string; + alt: string; + width: number; + inline: boolean; + required: boolean; + } + | { + kind: string; + lane: string; + label: string; + path: string; + targetPath: string; + required: boolean; + alt?: undefined; + width?: undefined; + inline?: undefined; + } + | { + kind: string; + lane: string; + label: string; + path: string; + targetPath: string; + alt?: undefined; + width?: undefined; + inline?: undefined; + required?: undefined; + } + )[]; + }; + manifestPath: string; + transcriptPath: string; +}; diff --git a/scripts/mantis/build-web-ui-chat-evidence.d.mts b/scripts/mantis/build-web-ui-chat-evidence.d.mts new file mode 100644 index 000000000000..543f46eb4e9e --- /dev/null +++ b/scripts/mantis/build-web-ui-chat-evidence.d.mts @@ -0,0 +1,87 @@ +#!/usr/bin/env node +export function buildWebUiChatEvidenceManifest({ + candidateRef, + candidateSha, + status, +}: { + candidateRef: unknown; + candidateSha: unknown; + status: unknown; +}): { + schemaVersion: number; + id: string; + title: string; + summary: string; + scenario: string; + comparison: { + candidate: { + expected: string; + status: unknown; + fixed: boolean; + ref?: unknown; + sha?: unknown; + }; + pass: boolean; + }; + artifacts: ( + | { + alt?: unknown; + inline?: boolean | undefined; + width?: number | undefined; + kind: unknown; + lane: string; + label: unknown; + path: unknown; + targetPath: unknown; + required: unknown; + } + | { + kind: string; + lane: string; + label: string; + path: string; + targetPath: string; + } + )[]; +}; +export function writeWebUiChatEvidence(rawArgs?: string[]): { + manifest: { + schemaVersion: number; + id: string; + title: string; + summary: string; + scenario: string; + comparison: { + candidate: { + expected: string; + status: unknown; + fixed: boolean; + ref?: unknown; + sha?: unknown; + }; + pass: boolean; + }; + artifacts: ( + | { + alt?: unknown; + inline?: boolean | undefined; + width?: number | undefined; + kind: unknown; + lane: string; + label: unknown; + path: unknown; + targetPath: unknown; + required: unknown; + } + | { + kind: string; + lane: string; + label: string; + path: string; + targetPath: string; + } + )[]; + }; + manifestPath: string; + reportPath: string; +}; diff --git a/scripts/mantis/publish-pr-evidence.d.mts b/scripts/mantis/publish-pr-evidence.d.mts new file mode 100644 index 000000000000..649c39a8441f --- /dev/null +++ b/scripts/mantis/publish-pr-evidence.d.mts @@ -0,0 +1,83 @@ +#!/usr/bin/env node +/** + * Loads and validates an evidence manifest from disk. + */ +export type EvidenceArtifact = { + alt?: string; + inline?: boolean; + kind: string; + label: string; + lane: string; + path?: string; + required?: boolean; + source: string; + targetPath: string; + width?: number; +}; +export type EvidenceManifest = { + artifacts: EvidenceArtifact[]; + comparison: { + baseline?: Record; + candidate: Record & { sha?: string }; + pass?: boolean; + }; + id: string; + manifestDir: string; + scenario: string; + schemaVersion: number; + summary?: string; + title: string; +}; +export function loadEvidenceManifest(manifestPath: string): EvidenceManifest; +export function shouldPublishPrComment( + manifest: EvidenceManifest, + { requestSource }?: { requestSource?: string }, +): boolean; +export function renderEvidenceComment({ + artifactUrl: actionsArtifactUrl, + manifest, + marker, + rawBase, + requestSource, + runUrl, + treeUrl, +}: { + artifactUrl?: string; + artifactRoot?: string; + manifest: EvidenceManifest; + marker: string; + rawBase: string; + requestSource?: string; + runUrl?: string; + treeUrl?: string; +}): string; +export function publishArtifactFiles({ + artifactRoot, + fetchImpl, + manifest, + storageConfig, +}: { + artifactRoot: string; + fetchImpl?: + | (( + url: URL, + init: { body: Buffer; headers: HeadersInit; method: string }, + ) => Promise) + | undefined; + manifest: EvidenceManifest; + storageConfig?: + | { + accessKeyId: string; + bucket: string; + endpoint: string; + publicBaseUrl: string; + region: string; + secretAccessKey: string; + } + | undefined; +}): Promise<{ + artifactRoot: string; + rawBase: string; + treeUrl: string; +}>; +export function publishEvidence(rawArgs?: string[]): Promise; diff --git a/scripts/measure-rpc-rtt.d.mts b/scripts/measure-rpc-rtt.d.mts new file mode 100644 index 000000000000..5dc2cdee4254 --- /dev/null +++ b/scripts/measure-rpc-rtt.d.mts @@ -0,0 +1,164 @@ +export function parseArgs(argv: string[]): + | { + help: true; + iterations: number; + methods: string[]; + outputDir?: string; + repoRoot?: string; + } + | { + help: false; + iterations: number; + methods: string[]; + outputDir: string; + repoRoot?: string; + }; +/** + * Polls readiness endpoints while also failing fast if the child exits. + */ +export function waitForGatewayReady({ + child, + fetchImpl, + port, + probeTimeoutMs, + readyTimeoutMs, + sleepMs, + stderrPath, +}: { + child: unknown; + fetchImpl?: typeof fetch | undefined; + port: unknown; + probeTimeoutMs?: number | undefined; + readyTimeoutMs?: number | undefined; + sleepMs?: number | undefined; + stderrPath: unknown; +}): Promise; +/** + * Signals the gateway process tree so spawned package-manager children are cleaned up. + */ +export function signalGatewayProcess( + child: unknown, + signal: unknown, + killProcess?: typeof defaultKillProcess, + { + platform, + runTaskkill, + }?: { + platform?: NodeJS.Platform | undefined; + runTaskkill?: typeof defaultRunTaskkill | undefined; + }, +): unknown; +/** + * Checks process-group liveness without treating an already-exited child as an error. + */ +export function isGatewayProcessAlive( + child: unknown, + killProcess?: typeof defaultKillProcess, + { + platform, + }?: { + platform?: NodeJS.Platform | undefined; + }, +): boolean; +/** + * Installs parent-process cleanup handlers for a spawned gateway. + */ +export function installGatewayParentCleanup( + child: unknown, + { + killProcess, + platform, + processLike, + runTaskkill, + }?: { + killProcess?: typeof defaultKillProcess | undefined; + platform?: NodeJS.Platform | undefined; + processLike?: + | { + exitCode?: number; + kill(pid: number, signal?: NodeJS.Signals): boolean; + off(event: string, listener: (...args: unknown[]) => void): unknown; + on(event: string, listener: (...args: unknown[]) => void): unknown; + once(event: string, listener: (...args: unknown[]) => void): unknown; + pid: number; + } + | undefined; + runTaskkill?: typeof defaultRunTaskkill | undefined; + }, +): () => void; +/** + * Stops the gateway with SIGTERM first and SIGKILL after the grace window. + */ +export function stopGateway(child: unknown, options?: Record): Promise; +/** + * Starts an isolated loopback gateway with temp HOME/state directories. + */ +export function startGateway({ + configPath, + env, + openImpl, + port, + repoRoot, + sourceEntryExists, + spawnImpl, + stderrPath, + stdoutPath, + tempRoot, + token, +}: { + configPath: unknown; + env?: NodeJS.ProcessEnv | undefined; + openImpl?: typeof defaultOpen | undefined; + port: unknown; + repoRoot: unknown; + sourceEntryExists?: typeof existsSync | undefined; + spawnImpl?: typeof spawn | undefined; + stderrPath: unknown; + stdoutPath: unknown; + tempRoot: unknown; + token: unknown; +}): Promise; +/** + * Removes the temporary root used by the RPC RTT probe. + */ +export function cleanupTempRoot( + tempRoot: unknown, + { + rmImpl, + }?: { + rmImpl?: typeof fs.rm | undefined; + }, +): Promise; +export function summarizeRttSamples(samples: unknown): { + avgMs: number; + maxMs: number; + minMs: number; + p50Ms: number; + p95Ms: number; +}; +export function assertRpcSmokeResponse(method: unknown, response: unknown): void; +export function createGatewayClient({ + WebSocket, + openTimeoutMs, + url, +}: { + WebSocket: unknown; + openTimeoutMs?: number | undefined; + url: unknown; +}): { + close: () => void; + request: (method: unknown, params: unknown, timeoutMs?: number) => Promise; + waitOpen: () => Promise; +}; +/** Maximum time to wait for a spawned gateway to become reachable. */ +export const READY_TIMEOUT_MS: 120000; +declare function defaultKillProcess(pid: unknown, signal: unknown): boolean; +declare function defaultRunTaskkill( + command: string, + args: string[], + options: Record, +): { error?: Error; status: number | null }; +declare function defaultOpen(filePath: unknown, flags: unknown): Promise; +import { spawn } from "node:child_process"; +import { existsSync } from "node:fs"; +import fs from "node:fs/promises"; diff --git a/scripts/ocm-npm-workspace-deps.d.mts b/scripts/ocm-npm-workspace-deps.d.mts new file mode 100644 index 000000000000..8621e1727d7a --- /dev/null +++ b/scripts/ocm-npm-workspace-deps.d.mts @@ -0,0 +1,34 @@ +#!/usr/bin/env node +export function parseWorkspaceDependencyDirs(raw?: string, cwd?: string): string[]; +export function resolveWorkspaceInstallPlan( + args: unknown, + workspaceDirs: unknown, + cwd?: string, +): { + installArgs: unknown; + prefixDir: string; + rootArchive: string; +} | null; +export function buildInstallManifest( + rootArchive: unknown, + workspacePackages: unknown, +): { + private: boolean; + dependencies: { + openclaw: string; + }; +}; +export function resolveNpmEnvironment(args: unknown, env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv; +export function resolveRuntimePackPlan( + args: string[], + env?: NodeJS.ProcessEnv, +): { profile: string; packArgs: string[] } | null; +export function resolveRuntimePackEnvironment( + env?: NodeJS.ProcessEnv, + now?: () => Date, + readGitCommit?: () => string | null, +): NodeJS.ProcessEnv & { OPENCLAW_BUILD_TIMESTAMP: string; GIT_COMMIT?: string }; +export function rewriteWorkspaceDependencyVersions( + packageJson: unknown, + workspacePackages: unknown, +): number; diff --git a/scripts/openclaw-cross-os-release-checks.ts b/scripts/openclaw-cross-os-release-checks.ts index dc5147db5f62..ac205177a2a8 100644 --- a/scripts/openclaw-cross-os-release-checks.ts +++ b/scripts/openclaw-cross-os-release-checks.ts @@ -236,7 +236,9 @@ const RELEASE_SMOKE_PLUGIN_ALLOWLIST_BASE = [ "talk-voice", ]; -export function buildCrossOsReleaseSmokePluginAllowlist(providerMeta: ProviderConfig) { +export function buildCrossOsReleaseSmokePluginAllowlist( + providerMeta: Pick, +) { return [...new Set([providerMeta.extensionId, ...RELEASE_SMOKE_PLUGIN_ALLOWLIST_BASE])]; } @@ -1826,9 +1828,9 @@ function resolveExpectedDevUpdateRef(ref?: string) { return trimmed || "main"; } -export function resolveDevUpdateVerificationRef(ref: string, sourceSha: string) { +export function resolveDevUpdateVerificationRef(ref: string, sourceSha?: string) { if (resolveExpectedDevUpdateRef(ref) === "main" && looksLikeCommitSha(sourceSha ?? "")) { - return sourceSha.trim(); + return sourceSha!.trim(); } return resolveExpectedDevUpdateRef(ref); } diff --git a/scripts/openclaw-npm-extended-stable-release.d.mts b/scripts/openclaw-npm-extended-stable-release.d.mts new file mode 100644 index 000000000000..6954213e3d04 --- /dev/null +++ b/scripts/openclaw-npm-extended-stable-release.d.mts @@ -0,0 +1,78 @@ +#!/usr/bin/env node +export function parseExtendedStableGuardBypass(value?: string): boolean; +export function validateNpmPublishBoundary( + packageVersion: unknown, + npmDistTag: unknown, + { + bypassExtendedStableGuard, + }?: { + bypassExtendedStableGuard?: boolean | undefined; + }, +): import("./lib/npm-publish-plan.mjs").ParsedReleaseVersion; +export function validateExtendedStableNpmReleaseRequest(request: unknown): + | { + extendedStable: boolean; + releaseVersion?: undefined; + extendedStableBranch?: undefined; + bypassExtendedStableGuard?: undefined; + } + | { + extendedStable: boolean; + releaseVersion: string; + extendedStableBranch: string; + bypassExtendedStableGuard: boolean; + } + | { + extendedStable: boolean; + releaseVersion: string; + extendedStableBranch: string; + bypassExtendedStableGuard?: undefined; + }; +export function validateExtendedStableRunIdentity({ + run, + kind, + npmDistTag, + expectedBranch, + expectedSha, +}: { + run: unknown; + kind: unknown; + npmDistTag: unknown; + expectedBranch: unknown; + expectedSha: unknown; +}): unknown; +export function validateFullReleaseValidationManifest({ + manifest, + npmDistTag, + expectedWorkflowRef, + expectedSha, + expectedRunId, + expectedRunAttempt, +}: { + manifest: unknown; + npmDistTag: unknown; + expectedWorkflowRef: unknown; + expectedSha: unknown; + expectedRunId: unknown; + expectedRunAttempt: unknown; +}): unknown; +export function parsePriorExtendedStableSelector(stdout: unknown): string; +export function capturePriorExtendedStableSelector({ query }: { query: unknown }): string; +export function verifyExtendedStableRegistryReadback({ + expectedVersion, + query, + sleep, + attempts, + delayMs, +}: { + expectedVersion: unknown; + query: unknown; + sleep: unknown; + attempts?: number | undefined; + delayMs?: number | undefined; +}): Promise<{ + exactVersion: string; + extendedStableSelector: string; + attemptsUsed: number; +}>; +export function extendedStableSelectorRepairCommand(expectedVersion: unknown): string; diff --git a/scripts/openclaw-performance-source-summary.d.mts b/scripts/openclaw-performance-source-summary.d.mts new file mode 100644 index 000000000000..6f7e1d96742e --- /dev/null +++ b/scripts/openclaw-performance-source-summary.d.mts @@ -0,0 +1,7 @@ +#!/usr/bin/env node +export function parseArgs(argv: string[]): { + baselineSourceDir: string | null; + sourceDir: string; + output: string | null; +}; +export function buildMarkdown(sourceDir: unknown, baselineSourceDir: unknown): string; diff --git a/scripts/package-openclaw-for-docker.d.mts b/scripts/package-openclaw-for-docker.d.mts new file mode 100644 index 000000000000..277a354caa6f --- /dev/null +++ b/scripts/package-openclaw-for-docker.d.mts @@ -0,0 +1,36 @@ +#!/usr/bin/env node +export function parseArgs(argv: unknown): { + allowUnreleasedChangelog: boolean; + outputDir: string; + outputName: string; + packJson: string; + pnpmPack: boolean; + skipBuild: boolean; + sourceDir: string; +}; +export function buildPackageArtifacts( + sourceDir: unknown, + options?: Record, +): Promise; +export function prepareBundledAiRuntimePackage( + sourceDir: string, + outputDir: string, + runCaptureImpl?: ( + command: string, + args: string[], + cwd: string, + options?: Record, + ) => Promise, + options?: Record, +): Promise<() => Promise>; +export function packOpenClawPackageForDocker( + sourceDir: unknown, + outputDir: unknown, + options?: Record, +): Promise; +export function runCommandForTest( + command: unknown, + args: unknown, + cwd: unknown, + options?: Record, +): Promise; diff --git a/scripts/perf/summarize-cpuprofile.d.mts b/scripts/perf/summarize-cpuprofile.d.mts new file mode 100644 index 000000000000..8132486192f7 --- /dev/null +++ b/scripts/perf/summarize-cpuprofile.d.mts @@ -0,0 +1,11 @@ +#!/usr/bin/env node +export function usage(): string; +export function shouldPrintHelp(argv: unknown): boolean; +/** + * Parses CPU profile file paths and --limit. + */ +export function parseArgs(argv: unknown): { + files: unknown[]; + limit: number; +}; +export function summarizeProfile(file: unknown, limit: unknown): void; diff --git a/scripts/plan-release-workflow-matrix.d.mts b/scripts/plan-release-workflow-matrix.d.mts new file mode 100644 index 000000000000..8639ef920368 --- /dev/null +++ b/scripts/plan-release-workflow-matrix.d.mts @@ -0,0 +1,40 @@ +/** + * Creates the Docker E2E/live model matrix plan for a release profile. + */ +export type ReleaseMatrixEntry = { + chunk_id?: string; + label?: string; + timeout_minutes?: number; + provider_label?: string; + providers?: string; + profiles: string; + models?: string; + max_models?: string; +}; + +export type OmittedReleaseMatrixEntry = { id: string; label: string; reason: string }; + +export function createReleaseWorkflowMatrixPlan(options?: { + releaseProfile?: string; + includeReleasePathSuites?: boolean | string; + dockerLanes?: string; + includeLiveSuites?: boolean | string; + liveModelProviders?: string; + liveSuiteFilter?: string; +}): { + dockerE2e: { + count: number; + matrix: { + include: ReleaseMatrixEntry[]; + }; + omitted: OmittedReleaseMatrixEntry[]; + }; + liveModels: { + count: number; + matrix: { + include: ReleaseMatrixEntry[]; + }; + omitted: OmittedReleaseMatrixEntry[]; + }; + releaseProfile: string; +}; diff --git a/scripts/plan-targeted-docker-lane-groups.d.mts b/scripts/plan-targeted-docker-lane-groups.d.mts new file mode 100644 index 000000000000..fd6245b2acba --- /dev/null +++ b/scripts/plan-targeted-docker-lane-groups.d.mts @@ -0,0 +1,16 @@ +/** + * Groups selected Docker lanes and expands sharded upgrade-survivor baselines. + */ +export function planTargetedDockerLaneGroups({ + groupSize, + lanes, + upgradeSurvivorBaselines, +}?: { + groupSize?: number | string | undefined; + lanes?: string | undefined; + upgradeSurvivorBaselines?: string | undefined; +}): { + docker_lanes: string; + label: string; + published_upgrade_survivor_baselines: string; +}[]; diff --git a/scripts/plugin-publication-artifact.d.mts b/scripts/plugin-publication-artifact.d.mts new file mode 100644 index 000000000000..ce8bc9fb5447 --- /dev/null +++ b/scripts/plugin-publication-artifact.d.mts @@ -0,0 +1,185 @@ +#!/usr/bin/env node +export function inspectPackageTarballBytes( + inputBytes: unknown, + options?: Record, +): { + inventory: ( + | { + path: string; + sha256: string; + sizeBytes: unknown; + type: string; + } + | { + path: string; + sizeBytes: number; + type: string; + } + )[]; + packageManifest: unknown; + packageManifestSha256: string; + pluginManifest: unknown; + pluginManifestSha256: string; + tarballSizeBytes: number; + tarballSha256: string; + totalFileBytes: number; +}; +export function validatePluginPackageManifest(params: unknown, packageManifest: unknown): void; +export function createPluginPublicationArtifact(params: unknown): { + manifest: { + schema: string; + schemaVersion: number; + targetSha: unknown; + package: { + dir: unknown; + name: unknown; + version: unknown; + author: unknown; + contributors: unknown; + repository: unknown; + packageJsonSha256: unknown; + pluginManifestSha256: unknown; + sourcePackageJsonSha256: unknown; + }; + publication: + | { + route: unknown; + authMode: unknown; + capability: unknown; + reason: unknown; + tag: unknown; + publisherPolicy: unknown; + bootstrapMode?: undefined; + manualOverrideReason?: undefined; + requiresManualOverride?: undefined; + } + | { + route: unknown; + tag: unknown; + bootstrapMode: unknown; + manualOverrideReason: unknown; + requiresManualOverride: unknown; + authMode?: undefined; + capability?: undefined; + reason?: undefined; + publisherPolicy?: undefined; + }; + artifact: { + name: unknown; + tarball: unknown; + npmIntegrity: string; + npmShasum: string; + sha256: unknown; + sizeBytes: unknown; + inventory: unknown; + }; + }; + manifestPath: string; + tarballPath: string; +}; +export function verifyPluginPublicationArtifact(params: unknown): { + artifactDigest: string; + artifactId: unknown; + artifactName: string; + artifactSizeBytes: unknown; + artifactZipSha256: string; + manifest: { + schema: string; + schemaVersion: number; + targetSha: unknown; + package: { + dir: unknown; + name: unknown; + version: unknown; + author: unknown; + contributors: unknown; + repository: unknown; + packageJsonSha256: unknown; + pluginManifestSha256: unknown; + sourcePackageJsonSha256: unknown; + }; + publication: + | { + route: unknown; + authMode: unknown; + capability: unknown; + reason: unknown; + tag: unknown; + publisherPolicy: unknown; + bootstrapMode?: undefined; + manualOverrideReason?: undefined; + requiresManualOverride?: undefined; + } + | { + route: unknown; + tag: unknown; + bootstrapMode: unknown; + manualOverrideReason: unknown; + requiresManualOverride: unknown; + authMode?: undefined; + capability?: undefined; + reason?: undefined; + publisherPolicy?: undefined; + }; + artifact: { + name: unknown; + tarball: unknown; + npmIntegrity: string; + npmShasum: string; + sha256: unknown; + sizeBytes: unknown; + inventory: unknown; + }; + }; + npmIntegrity: string; + npmShasum: string; + packageJsonSha256: unknown; + pluginManifestSha256: unknown; + producerRunAttempt: unknown; + producerRunId: unknown; + sourcePackageJsonSha256: unknown; + tarballInventory: ( + | { + path: string; + sha256: string; + sizeBytes: unknown; + type: string; + } + | { + path: string; + sizeBytes: number; + type: string; + } + )[]; + tarballName: string; + tarballPath: string; + tarballSizeBytes: number; + tarballSha256: string; +}; +export function main(argv?: string[]): void; +export const CLAWHUB_PUBLICATION_TAR_LIMITS: Readonly<{ + maxArchiveBytes: number; + maxEntries: 10000; + maxEntryBytes: number; + maxExpandedBytes: number; + maxPathBytes: number; + maxTotalFileBytes: number; +}>; +import { downloadActionsArtifactArchive } from "./lib/actions-artifact-archive.mjs"; +import { describeActionsArtifactFiles } from "./lib/actions-artifact-archive.mjs"; +import { inspectActionsArtifactZip } from "./lib/actions-artifact-archive.mjs"; +import { inspectActionsArtifactZipWithPolicy } from "./lib/actions-artifact-archive.mjs"; +import { readBoundedRegularFile } from "./lib/actions-artifact-archive.mjs"; +import { readPublicationArtifactArchive } from "./lib/actions-artifact-archive.mjs"; +import { validateActionsArtifactBinding } from "./lib/actions-artifact-archive.mjs"; +import { validateActionsArtifactProducerJob } from "./lib/actions-artifact-archive.mjs"; +export { + downloadActionsArtifactArchive, + describeActionsArtifactFiles, + inspectActionsArtifactZip, + inspectActionsArtifactZipWithPolicy, + readBoundedRegularFile, + readPublicationArtifactArchive, + validateActionsArtifactBinding, + validateActionsArtifactProducerJob, +}; diff --git a/scripts/plugin-sdk-surface-report.d.mts b/scripts/plugin-sdk-surface-report.d.mts new file mode 100644 index 000000000000..d00f4e691256 --- /dev/null +++ b/scripts/plugin-sdk-surface-report.d.mts @@ -0,0 +1,69 @@ +#!/usr/bin/env node +export function readPluginSdkSurfaceBudgets(env?: NodeJS.ProcessEnv): { + budgets: { + publicEntrypoints: number; + publicExports: number; + publicFunctionExports: number; + publicDeprecatedExports: number; + publicWildcardReexports: number; + }; + publicDeprecatedExportsByEntrypointBudget: number; +}; +export function collectPluginSdkSurfaceReport(): { + allStats: { + byEntrypoint: Map; + totals: { + entrypoints: unknown; + exports: number; + callableExports: number; + deprecatedExports: number; + deprecatedCallableExports: number; + uniqueExports: number; + uniqueCallableExports: number; + }; + }; + deprecatedBarrelMissingFromInventory: string[]; + deprecatedBarrelWithoutWildcard: string[]; + deprecatedMissingFromPublic: string[]; + leakedForbiddenExports: string[]; + localOnlyMissingFromInventory: string[]; + localOnlyStats: { + byEntrypoint: Map; + totals: { + entrypoints: unknown; + exports: number; + callableExports: number; + deprecatedExports: number; + deprecatedCallableExports: number; + uniqueExports: number; + uniqueCallableExports: number; + }; + }; + localOnlyStillPublic: string[]; + publicStats: { + byEntrypoint: Map; + totals: { + entrypoints: unknown; + exports: number; + callableExports: number; + deprecatedExports: number; + deprecatedCallableExports: number; + uniqueExports: number; + uniqueCallableExports: number; + }; + }; + publicWildcards: { + count: number; + matches: string[]; + }; +}; +export function evaluatePluginSdkSurfaceReport( + report: unknown, + { + budgets, + publicDeprecatedExportsByEntrypointBudget, + }: { + budgets: unknown; + publicDeprecatedExportsByEntrypointBudget: unknown; + }, +): string[]; diff --git a/scripts/postinstall-bundled-plugins.d.mts b/scripts/postinstall-bundled-plugins.d.mts new file mode 100644 index 000000000000..3c0b8f03fe84 --- /dev/null +++ b/scripts/postinstall-bundled-plugins.d.mts @@ -0,0 +1,34 @@ +#!/usr/bin/env node +export function collectLegacyPluginRuntimeDepsStateRoots( + params?: Record, +): string[]; +export function pruneLegacyPluginRuntimeDepsState(params?: Record): string[]; +export function pruneInstalledPackageDist(params?: Record): unknown[]; +export function applyBaileysEncryptedStreamFinishHotfix(params?: Record): + | { + applied: boolean; + reason: string | undefined; + targetPath: string; + error?: undefined; + } + | { + applied: boolean; + reason: string; + targetPath?: undefined; + error?: undefined; + } + | { + applied: boolean; + reason: string; + targetPath: string; + error: string; + }; +export function runPluginRegistryPostinstallMigration( + params?: Record, +): Promise; +export function isSourceCheckoutRoot(params: unknown): unknown; +export function pruneBundledPluginSourceNodeModules(params?: Record): void; +export function pruneOpenClawCompileCache(params?: Record): void; +export function runBundledPluginPostinstall(params?: Record): void; +export function isDirectPostinstallInvocation(params?: Record): boolean; +export const MAX_INSTALLED_DIST_SCAN_ENTRIES: 100000; diff --git a/scripts/pre-commit/pnpm-audit-prod.d.mts b/scripts/pre-commit/pnpm-audit-prod.d.mts new file mode 100644 index 000000000000..8150c9358838 --- /dev/null +++ b/scripts/pre-commit/pnpm-audit-prod.d.mts @@ -0,0 +1,60 @@ +#!/usr/bin/env node +export function stripVersionDecorators(reference: unknown): unknown; +export function parseSnapshotKey(snapshotKey: unknown): { + packageName: unknown; + reference: unknown; + version: unknown; +}; +export function collectProdResolvedPackagesFromLockfile( + lockfileText: unknown, +): Map; +export function collectAllResolvedPackagesFromLockfile( + lockfileText: unknown, +): Map; +export function createBulkAdvisoryPayload(versionsByPackage: unknown): unknown; +export function filterFindingsBySeverity( + advisoriesByPackage: unknown, + minSeverity: unknown, + versionsByPackage?: unknown, +): { + packageName: string; + id: unknown; + severity: string; + title: unknown; + url: unknown; + vulnerableVersions: unknown; +}[]; +export function readBoundedBulkAdvisoryErrorText( + response: unknown, + maxChars?: number, + options?: Record, +): Promise; +export function fetchBulkAdvisories({ + payload, + fetchImpl, + registryBaseUrl, + responseBodyMaxBytes, + timeoutMs, +}: { + payload: unknown; + fetchImpl?: typeof fetch | undefined; + registryBaseUrl?: string | undefined; + responseBodyMaxBytes?: unknown; + timeoutMs?: number | undefined; +}): Promise; +export function runPnpmAuditProd({ + rootDir, + fetchImpl, + stdout, + stderr, + minSeverity, +}?: { + rootDir?: string | undefined; + fetchImpl?: typeof fetch | undefined; + stdout?: Pick | undefined; + stderr?: Pick | undefined; + minSeverity?: string | undefined; +}): Promise<0 | 1>; +export function parseArgs(argv: unknown): { + minSeverity: string; +}; diff --git a/scripts/preinstall-package-manager-warning.d.mts b/scripts/preinstall-package-manager-warning.d.mts new file mode 100644 index 000000000000..a6c5a49bad15 --- /dev/null +++ b/scripts/preinstall-package-manager-warning.d.mts @@ -0,0 +1,15 @@ +/** + * Detects the package manager running the current lifecycle script. + */ +export function detectLifecyclePackageManager(env?: NodeJS.ProcessEnv): string | null; +/** + * Builds the warning shown for non-pnpm lifecycle installs. + */ +export function createPackageManagerWarningMessage(packageManager: unknown): string | null; +/** + * Emits the non-pnpm lifecycle warning when needed. + */ +export function warnIfNonPnpmLifecycle( + env?: NodeJS.ProcessEnv, + warn?: (...data: unknown[]) => void, +): boolean; diff --git a/scripts/prepare-extension-package-boundary-artifacts.d.mts b/scripts/prepare-extension-package-boundary-artifacts.d.mts new file mode 100644 index 000000000000..f64cc9ce7bb3 --- /dev/null +++ b/scripts/prepare-extension-package-boundary-artifacts.d.mts @@ -0,0 +1,59 @@ +/** + * Lists entry-shim artifacts written by scripts/write-plugin-sdk-entry-dts.ts. + */ +export function resolveBoundaryEntryShimRequiredOutputs(env?: NodeJS.ProcessEnv): string[]; +/** + * Parses the artifact preparation mode from CLI arguments. + */ +export function parseMode(argv?: string[]): string; +/** + * Reads the root shim timeout override for long package-boundary builds. + */ +export function resolveBoundaryRootShimsTimeoutMs(env?: NodeJS.ProcessEnv): number; +/** + * Compares input and output mtimes to skip fresh generated artifacts. + */ +export function isArtifactSetFresh(params: unknown): boolean; +/** + * Prefixes streamed child output line-by-line without breaking partial chunks. + */ +export function createPrefixedOutputWriter( + label: unknown, + target: unknown, +): { + write(chunk: unknown): void; + flush(): void; +}; +export function signalNodeStep( + child: unknown, + signal: unknown, + { + platform, + runTaskkill, + useProcessGroup, + }?: { + platform?: NodeJS.Platform | undefined; + runTaskkill?: + | ((command: string, args?: string[]) => { error?: Error; status: number | null }) + | undefined; + useProcessGroup?: boolean | undefined; + }, +): void; +/** + * Runs one artifact step with timeout, abort propagation, and prefixed output. + */ +export function runNodeStep( + label: unknown, + args: unknown, + timeoutMs: unknown, + params?: Record, +): Promise; +/** + * Runs independent artifact steps together and aborts siblings on first failure. + */ +export function runNodeStepsInParallel(steps: unknown): Promise; +/** + * Chooses serial or parallel artifact execution based on local heavy-check policy. + */ +export function runNodeSteps(steps: unknown, env?: NodeJS.ProcessEnv): Promise; +import { spawnSync } from "node:child_process"; diff --git a/scripts/prepare-git-hooks.d.mts b/scripts/prepare-git-hooks.d.mts new file mode 100644 index 000000000000..61882eef372d --- /dev/null +++ b/scripts/prepare-git-hooks.d.mts @@ -0,0 +1,7 @@ +/** + * Installs the repo-local hooks path and returns a structured reason if skipped. + */ +export function configurePrepareGitHooks(params?: Record): { + configured: boolean; + reason: string; +}; diff --git a/scripts/profile-extension-memory.d.mts b/scripts/profile-extension-memory.d.mts new file mode 100644 index 000000000000..ad5da7ebb4dd --- /dev/null +++ b/scripts/profile-extension-memory.d.mts @@ -0,0 +1,36 @@ +#!/usr/bin/env node +/** + * Parses extension memory profiler options after pnpm's optional separator. + */ +export function parseArgs(argv: string[]): { + extensions: string[]; + concurrency: number; + timeoutMs: number; + combinedTimeoutMs: number; + top: number; + jsonPath: string | null; + skipCombined: boolean; +}; +/** + * Runs one import scenario in a child process and captures bounded output plus RSS. + */ +export function runCase({ + repoRoot, + env, + hookPath, + name, + body, + timeoutMs, + shutdownGraceMs, + spawnImpl, +}: { + repoRoot: unknown; + env: unknown; + hookPath: unknown; + name: unknown; + body: unknown; + timeoutMs: unknown; + shutdownGraceMs?: number | undefined; + spawnImpl?: typeof spawn | undefined; +}): Promise; +import { spawn } from "node:child_process"; diff --git a/scripts/release-candidate-checklist.d.mts b/scripts/release-candidate-checklist.d.mts new file mode 100644 index 000000000000..75088a514b15 --- /dev/null +++ b/scripts/release-candidate-checklist.d.mts @@ -0,0 +1,178 @@ +#!/usr/bin/env node +/** + * Parses release-candidate validation options and enforces publish-scope policy. + */ +export function parseArgs(argv: unknown): { + repo: string; + provider: string; + mode: string; + releaseProfile: string; + npmDistTag: string; + pluginPublishScope: string; + plugins: string; + skipDispatch: boolean; + skipLocalGeneratedCheck: boolean; + skipParallels: boolean; + skipTelegram: boolean; + telegramProviderMode: string; + tag: string; + workflowRef: string; + fullReleaseRunId: string; + npmPreflightRunId: string; + windowsNodeTag: string; + windowsNodeInstallerDigests: string; + outputDir: string; +}; +export function run(command: unknown, args: unknown, options?: Record): string; +export function buildReleaseCandidateState( + options: unknown, + { + targetSha, + toolingSha, + }: { + targetSha: unknown; + toolingSha: unknown; + }, +): { + version: number; + phase: string; + repo: unknown; + tag: unknown; + targetSha: unknown; + toolingSha: unknown; + workflowRef: unknown; + provider: unknown; + mode: unknown; + releaseProfile: unknown; + npmDistTag: unknown; + pluginPublishScope: unknown; + plugins: unknown; + windowsNodeTag: unknown; + skipParallels: unknown; + skipTelegram: unknown; + telegramProviderMode: unknown; + fullReleaseRunId: unknown; + npmPreflightRunId: unknown; +}; +export function reconcileReleaseCandidateState(saved: unknown, expected: unknown): unknown; +/** + * Calls the GitHub REST API with the gh-auth token and a bounded timeout. + */ +export function githubApi(path: unknown, options?: Record): Promise; +/** + * Validates the immutable Windows source release contract for a stable candidate. + */ +export function validateWindowsSourceRelease( + tag: unknown, + options?: Record, +): Promise<{ + tag: unknown; + url: unknown; + assets: { + name: string; + digest: unknown; + }[]; +}>; +export function validateCandidateCheckout({ + targetSha, + targetHeadSha, + targetTrackedStatus, + toolingSha, + trustedToolingSha, + toolingTrackedStatus, + workflowRef, +}: { + targetSha: unknown; + targetHeadSha: unknown; + targetTrackedStatus: unknown; + toolingSha: unknown; + trustedToolingSha: unknown; + toolingTrackedStatus: unknown; + workflowRef: unknown; +}): { + status: string; + targetSha: unknown; + toolingSha: unknown; + workflowRef: unknown; +}; +export function candidateCumulativeShippedPullRequests( + changelog: string, + label: string, +): Set; +export function validateCandidateReleaseNotes({ + changelog, + repository, + tag, +}: { + changelog: unknown; + repository: unknown; + tag: unknown; +}): { + status: string; + mode: string; + characters: number; + bytes: number; +}; +export function validateCandidateChangelogProvenance({ + changelog, + version, + tag, + targetSha, + isAncestor, + loadShippedBaseline, +}: { + changelog: unknown; + version: unknown; + tag: unknown; + targetSha: unknown; + isAncestor?: ((ancestor: string, target: string) => boolean) | undefined; + loadShippedBaseline?: typeof loadCandidateShippedBaseline | undefined; +}): + | { + status: string; + reason: string; + shippedBaselines: ShippedBaselineExclusion[]; + base?: undefined; + target?: undefined; + } + | { + status: string; + base: unknown; + target: unknown; + shippedBaselines: ShippedBaselineExclusion[]; + reason?: undefined; + }; +/** + * Chooses the expected artifact name, allowing one same-prefix fallback per run. + */ +export function resolveArtifactName( + artifacts: unknown, + preferredName: unknown, + prefix: unknown, +): unknown; +/** + * Extracts a GitHub Actions run id from gh workflow dispatch output. + */ +export function parseRunIdFromDispatchOutput(output: unknown): unknown; +export function requireRunIdFromDispatchOutput(output: unknown, workflowFile: unknown): unknown; +/** + * Builds the final release publish workflow command once validation evidence is ready. + */ +export function buildPublishCommand(options: unknown): string; +export function validatePreflightManifest(manifest: unknown, params: unknown): void; +export function validateFullManifest(manifest: unknown, params: unknown): void; +export function candidateParallelsArgs( + tarballPath: unknown, + dependencyTarballPaths?: unknown[], +): unknown[]; +export function candidateParallelsShellCommand( + tarballPath: unknown, + timeoutBin: unknown, + dependencyTarballPaths?: unknown[], +): string; +declare function gitIsAncestor(ancestor: unknown, target: unknown): boolean; +declare function loadCandidateShippedBaseline(ref: unknown): { + ref: unknown; + pullRequests: Set; +}; +import type { ShippedBaselineExclusion } from "./render-github-release-notes.mjs"; diff --git a/scripts/release-ci-summary.d.mts b/scripts/release-ci-summary.d.mts new file mode 100644 index 000000000000..c3b7159c1c3e --- /dev/null +++ b/scripts/release-ci-summary.d.mts @@ -0,0 +1,235 @@ +#!/usr/bin/env node +export function validateParentRunBinding( + parentView: unknown, + parentRest: unknown, + expectedRunId: unknown, +): unknown; +export function expectedChildDispatches( + parentRunId: unknown, + parentRunAttempt: unknown, + parentWorkflowRef: unknown, +): { + displayTitle: string; + headBranch: string; + manifestKey: string; + name: string; + parentJobName: string; + suffix: string; + trustedRef: string; + workflow: string; +}[]; +export function requiredChildKeysForRerunGroup(rerunGroup: unknown): Set; +export function expectedSelectedChildDispatches( + parentRunId: unknown, + parentRunAttempt: unknown, + parentWorkflowRef: unknown, + selectedKeys: unknown, +): { + displayTitle: string; + headBranch: string; + manifestKey: string; + name: string; + parentJobName: string; + suffix: string; + trustedRef: string; + workflow: string; +}[]; +export function selectExactChildRun( + runs: unknown, + expectedDisplayTitle: unknown, + expectedHeadBranch: unknown, +): unknown; +export function selectExactChildRunFromPages( + runPages: unknown, + expectedDisplayTitle: unknown, + expectedHeadBranch: unknown, +): unknown; +export function validateParentManifest( + value: unknown, + expected: unknown, +): { + childRunIds: { + normalCi: string; + npmTelegram: string; + pluginPrerelease: string; + productPerformance: string; + releaseChecks: string; + }; + controls: Record; + evidenceReuse: + | { + changedPaths: string[]; + evidenceSha: string; + policy: string; + runId: string; + selectedRunId: string; + } + | undefined; + releaseProfile: string; + rerunGroup: string; + runAttempt: number; + runId: string; + runReleaseSoak: string; + targetRef: string; + targetSha: string; + validationInputs: Record; + version: unknown; + workflowFullRef: string | undefined; + workflowSha: string | undefined; + workflowRef: unknown; + workflowRefType: string | undefined; +}; +export function validateEvidenceReuseChain( + currentManifest: unknown, + selectedManifest: unknown, + rootManifest: unknown, +): unknown; +export function selectedChildKeys(parentJobs: unknown): Set; +export function manifestChildEntries( + manifest: { childRunIds: Record }, + children: Child[], + selectedKeys: Set, +): Array<{ child: Child; runId: string }>; +export function resolveManifestChildOriginAttempt( + run: unknown, + child: unknown, + parentManifest: unknown, + parentJobs: unknown, +): unknown; +export function selectManifestParentJob( + parentJobs: unknown, + child: unknown, + parentManifest: unknown, + originAttempt: unknown, +): unknown; +export function validateManifestChildRun( + run: unknown, + child: unknown, + runId: unknown, + parentManifest: unknown, + parentJobs: unknown, + selectedParentJobLog: unknown, + repository?: string, +): unknown; +export function validatePerformanceArtifactOnlyJobs(jobs: unknown, runAttempt: unknown): unknown; +export function validateManifestArtifactIdentity( + artifact: unknown, + { + artifactDigest, + artifactId, + runAttempt, + runId, + }: { + artifactDigest: unknown; + artifactId: unknown; + runAttempt: unknown; + runId: unknown; + }, +): unknown; +export function selectManifestArtifact( + artifacts: unknown, + runId: unknown, + runAttempt: unknown, +): unknown; +export function validateManifestArtifactCompatibility( + artifact: unknown, + manifest: unknown, + runId: unknown, + runAttempt: unknown, +): unknown; +export function readManifestArtifactArchive(archivePath: unknown, expectedDigest: unknown): unknown; +export function createReleaseEvidenceClient(repository?: string): { + compareCommits(base: unknown, head: unknown): unknown; + getJobLog(jobId: unknown): string; + getParentJobs(runId: unknown): unknown[]; + getRun(runId: unknown): unknown; + getRunView(runId: unknown): unknown; + loadManifest( + runId: unknown, + runAttempt: unknown, + manifestPath: unknown, + ): + | { + artifact: unknown; + manifest: unknown; + } + | undefined; +}; +export function validateTrustedProducerIdentity( + evidence: unknown, + client: unknown, + verifier: unknown, + trustedWorkflowRef: unknown, +): { + producerOnTrustedMainLineage: boolean; + workflowFullRef: string; + workflowQualifiedPath: string; + workflowRefProof: string; + workflowRefType: string; + workflowRunPath: string; +}; +export function resolveVerifierIdentity( + sourceSha: unknown, + verifierSourceContent: unknown, + repositoryRoot?: string, +): { + schemaVersion: number; + script: string; + scriptSha256: string; + sourceSha: unknown; +}; +export type ReleaseRunEvidence = { + children: Array>; + directRoot: boolean; + evidenceReuse: Record | null; + producerOnTrustedMainLineage: boolean; + releaseProfile: string; + repository: string; + rerunGroup: string; + root: Record & { + artifact: Record; + targetSha: string; + workflowSha: string; + }; + runReleaseSoak: boolean; + schema: string; + trustedWorkflowFullRef: string; + trustedWorkflowRef: string; + valid: boolean; + verifier: { schemaVersion: number; sourceSha: string }; +}; +export function validateReleaseRunEvidence( + { + manifestPath, + repository, + runId, + trustedWorkflowRef, + verifierSourceContent, + verifierSourceSha, + }: { + manifestPath?: string; + repository?: string | undefined; + runId: string; + trustedWorkflowRef?: string | undefined; + verifierSourceContent?: string | Uint8Array; + verifierSourceSha: string; + }, + client?: unknown, +): ReleaseRunEvidence; +export function parseReleaseCiSummaryArgs(argv: string[]): { + intervalMs: number; + json: boolean; + manifestPath: string | undefined; + repository: string; + runId: string; + trustedWorkflowRef: string; + validate: boolean; + verifierSourceFile: string | undefined; + verifierSourceSha: string | undefined; + watch: boolean; +}; +export function releaseCiWatchFingerprint(parent: unknown): string; +export function watchReleaseCiRun( + options: unknown, + overrides?: Record, +): Promise; diff --git a/scripts/render-github-release-notes.d.mts b/scripts/render-github-release-notes.d.mts new file mode 100644 index 000000000000..acbc9ea86588 --- /dev/null +++ b/scripts/render-github-release-notes.d.mts @@ -0,0 +1,77 @@ +#!/usr/bin/env node +export function githubReleaseBodySize(body: unknown): { + characters: number; + bytes: number; +}; +export function fitsGithubReleaseBody(body: unknown): boolean; +export function extractChangelogReleaseSections(changelog: string): { + version: string; + source: string; +}[]; +export function extractChangelogSection(changelog: unknown, version: unknown): unknown; +export function releaseNotesVersionForTag(tag: unknown): unknown; +export function formatShippedBaselineExclusions(baselines: ShippedBaselineExclusion[]): string; +export function parseShippedBaselineExclusions(section: string): ShippedBaselineExclusion[]; +export function tagPinnedContributionRecordUrl(repository: unknown, tag: unknown): string; +export function dedicatedSectionVersionForTag(tag: unknown): unknown; +export function releaseNotesSectionForTag( + changelog: unknown, + version: unknown, + tag: unknown, +): unknown; +export function renderGithubReleaseNotes({ + changelog, + version, + tag, + repository, + verification, +}: { + changelog: unknown; + version: unknown; + tag: unknown; + repository: unknown; + verification?: string | undefined; +}): { + body: string; + mode: string; + size: { + characters: number; + bytes: number; + }; + verificationIncluded: boolean; + verificationOmitted: boolean; +}; +export function verifyGithubReleaseNotes({ + body, + changelog, + version, + tag, + repository, +}: { + body: unknown; + changelog: unknown; + version: unknown; + tag: unknown; + repository: unknown; +}): { + matches: boolean; + actualSize: { + characters: number; + bytes: number; + }; + body: unknown; + mode: string; + size: { + characters: number; + bytes: number; + }; + verificationIncluded: boolean; + verificationOmitted: boolean; +}; +export const GITHUB_RELEASE_BODY_MAX_CHARACTERS: 125000; +export const GITHUB_RELEASE_BODY_MAX_BYTES: 125000; +export type ShippedBaselineExclusion = { + ref: string; + count: number; + pullRequests: number[]; +}; diff --git a/scripts/report-test-temp-creations.d.mts b/scripts/report-test-temp-creations.d.mts new file mode 100644 index 000000000000..508df1742fbe --- /dev/null +++ b/scripts/report-test-temp-creations.d.mts @@ -0,0 +1,20 @@ +#!/usr/bin/env node +export function formatGithubWarning(finding: unknown): string; +export function collectTempCreationFindingsFromDiff( + diffText: unknown, + options?: Record, +): ( + | { + file: unknown; + line: unknown; + reason: string; + source: string; + } + | { + file: string; + line: number; + reason: string; + source: unknown; + } +)[]; +export function main(argv: unknown, io: unknown): Promise<1 | 0>; diff --git a/scripts/repro/limit-edge-case-live-proof.d.mts b/scripts/repro/limit-edge-case-live-proof.d.mts new file mode 100644 index 000000000000..0221575ffc23 --- /dev/null +++ b/scripts/repro/limit-edge-case-live-proof.d.mts @@ -0,0 +1 @@ +export function withProofTempRoot(callback: (root: string) => T | Promise): Promise; diff --git a/scripts/resolve-openclaw-package-candidate.d.mts b/scripts/resolve-openclaw-package-candidate.d.mts new file mode 100644 index 000000000000..676d2c82f04f --- /dev/null +++ b/scripts/resolve-openclaw-package-candidate.d.mts @@ -0,0 +1,105 @@ +#!/usr/bin/env node +export function parseArgs(argv: unknown): { + artifactDir: string; + githubOutput: string; + metadata: string; + outputDir: string; + outputName: string; + packageRef: string; + packageSha256: string; + packageSpec: string; + packageUrl: string; + source: string; + trustedSourceId: string; + trustedSourcePolicy: string; + help?: true; +}; +export function validateOpenClawPackageSpec(spec: unknown): void; +export function resolveNpmPackageCandidatePackRunner( + packageSpec: string, + outputDir: string, + params?: { + comSpec?: string; + env?: NodeJS.ProcessEnv; + execPath?: string; + existsSync?: (candidate: string) => boolean; + platform?: NodeJS.Platform; + }, +): { + args: string[]; + command: string; + env?: NodeJS.ProcessEnv; + shell: boolean; + windowsVerbatimArguments?: boolean; +}; +export function signalChildProcessTree( + child: unknown, + signal: unknown, + { + platform, + runTaskkill, + useProcessGroup, + }?: { + platform?: NodeJS.Platform | undefined; + runTaskkill?: + | (( + command: string, + args: readonly string[], + options: { stdio: "ignore" }, + ) => { error?: Error; status: number | null }) + | undefined; + useProcessGroup?: boolean | undefined; + }, +): void; +export function readArtifactPackageCandidateMetadata(dir: unknown): Promise; +export function loadTrustedPackageSource( + id: unknown, + policyPath?: string, +): Promise<{ + allowPrivateNetwork: boolean; + auth: unknown; + hosts: unknown[]; + id: unknown; + pathPrefixes: string[]; + ports: number[]; + redirectHosts: unknown[]; +}>; +export function downloadUrl( + url: unknown, + target: unknown, + options?: Record, +): Promise; +export function readPackageBuildSourceSha(tarball: unknown): Promise; +export function main(argv?: string[]): Promise; +export const ARTIFACT_TARBALL_SCAN_MAX_ENTRIES: 10000; +export const OPENCLAW_PACKAGE_SPEC_RE: RegExp; +export function runCommandForTest( + command: unknown, + args: unknown, + options?: Record, +): Promise; +export function assertExpectedSha256ForTest(file: unknown, expected: unknown): Promise; +export function findSingleTarballForTest(dir: unknown, maxEntries?: number): Promise; +export function cleanupPackageSourceWorktreeForTest( + sourceDir: unknown, + { + resolveError, + runImpl, + consoleError, + }?: { + runImpl?: typeof run | undefined; + consoleError?: ((message: string) => void) | undefined; + resolveError?: unknown; + }, +): Promise; +export function moveNewestPackedTarballForTest( + outputDir: unknown, + packOutput: unknown, + outputName: unknown, +): Promise; +export function cleanPackedOpenClawTarballsForTest(outputDir: unknown): Promise; +declare function run( + command: unknown, + args: unknown, + options?: Record, +): Promise; diff --git a/scripts/resolve-upgrade-survivor-baselines.d.mts b/scripts/resolve-upgrade-survivor-baselines.d.mts new file mode 100644 index 000000000000..e07ab4934fa7 --- /dev/null +++ b/scripts/resolve-upgrade-survivor-baselines.d.mts @@ -0,0 +1,5 @@ +export function parseArgs(argv: unknown): Map; +/** + * Expands requested baseline tokens into normalized package/version specs. + */ +export function resolveBaselines(args: unknown): unknown[]; diff --git a/scripts/root-dependency-ownership-audit.d.mts b/scripts/root-dependency-ownership-audit.d.mts new file mode 100644 index 000000000000..f7b1eda2cfd7 --- /dev/null +++ b/scripts/root-dependency-ownership-audit.d.mts @@ -0,0 +1,30 @@ +#!/usr/bin/env node +/** + * Collects static and simple constant-backed package specifiers from source text. + */ +export function collectModuleSpecifiers(source: unknown): Set; +/** + * Classifies whether a root dependency is core-owned, shared, or extension-local. + */ +export function classifyRootDependencyOwnership(record: unknown): { + category: string; + recommendation: string; +}; +/** + * Builds dependency ownership records from root package.json and scanned imports. + */ +export function collectRootDependencyOwnershipAudit(params?: Record): { + depName: string; + spec: unknown; + sections: unknown[]; + fileCount: number; + sampleFiles: unknown[]; + declaredInExtensions: string[]; + internalizedBundledRuntimeOwners: string[]; + category: string; + recommendation: string; +}[]; +/** + * Returns actionable errors for dependencies that should not remain root-owned. + */ +export function collectRootDependencyOwnershipCheckErrors(records: unknown): unknown; diff --git a/scripts/run-additional-boundary-checks.d.mts b/scripts/run-additional-boundary-checks.d.mts new file mode 100644 index 000000000000..d15c61b9689e --- /dev/null +++ b/scripts/run-additional-boundary-checks.d.mts @@ -0,0 +1,96 @@ +#!/usr/bin/env node +/** + * Resolves the configured boundary-check concurrency. + */ +export type BoundaryCheck = { args: string[]; command: string; label: string }; +export function resolveConcurrency(value: unknown, fallback?: number, label?: string): number; +/** + * Parses positive integer CLI/env options with a fallback. + */ +export function resolvePositiveInteger(value: unknown, fallback: number, label?: string): number; +/** + * Parses one N/TOTAL shard selector into zero-based index form. + */ +export function parseShardSpec(value: unknown): { + count: number; + index: number; + label: string; +} | null; +/** + * Parses a comma-separated list of N/TOTAL shard selectors. + */ +export function parseShardSelection(value: unknown): + | { + count: number; + index: number; + label: string; + }[] + | null; +/** + * Selects checks whose ordinal belongs to the requested shard set. + */ +export function selectChecksForShard(checks: BoundaryCheck[], shardSpec: string): BoundaryCheck[]; +/** + * Formats a check command for CI group output. + */ +export function formatCommand({ command, args }: { command: string; args: string[] }): string; +/** + * Keeps only the tail of noisy check output so failure logs stay bounded. + */ +export function createBoundedOutputBuffer(maxBytes?: number): { + append: (value: unknown) => void; + read(): string; +}; +/** + * Runs one boundary check with timeout and process-group termination. + */ +export function runSingleCheck( + check: BoundaryCheck, + { + activeChildren, + checkTimeoutMs, + cwd, + env, + outputMaxBytes, + }: { + activeChildren?: Set; + checkTimeoutMs?: number | undefined; + cwd: string; + env: NodeJS.ProcessEnv; + outputMaxBytes?: number | undefined; + }, +): Promise<{ code: number; durationMs: number; output: string; timedOut: boolean }>; +/** + * Runs boundary checks with bounded concurrency and returns the failure count. + */ +export function runChecks( + checks?: BoundaryCheck[], + { + checkTimeoutMs, + concurrency, + cwd, + env, + output, + outputMaxBytes, + }?: { + checkTimeoutMs?: number | undefined; + concurrency?: number | undefined; + cwd?: string | undefined; + env?: NodeJS.ProcessEnv | undefined; + output?: { write(chunk: string): boolean } | undefined; + outputMaxBytes?: number | undefined; + }, +): Promise; +export function parseCliArgs( + args: unknown, + env?: NodeJS.ProcessEnv, +): { + help: boolean; + shardSpec: string; +}; +/** Ordered list of supplemental boundary checks used by CI sharding. */ +export const BOUNDARY_CHECKS: { + label: string; + command: string; + args: string[]; +}[]; diff --git a/scripts/run-android-gradle.d.mts b/scripts/run-android-gradle.d.mts new file mode 100644 index 000000000000..b7b387fb9e12 --- /dev/null +++ b/scripts/run-android-gradle.d.mts @@ -0,0 +1,23 @@ +#!/usr/bin/env node +export function splitAndroidGradleArgs(argv: unknown): { + gradleArgs: unknown; + postArgs: unknown; +}; +export function shouldSkipLinuxArmAndroidGradle(options?: Record): boolean; +export function linuxArmAndroidGradleSkipMessage( + platform?: NodeJS.Platform, + arch?: NodeJS.Architecture, +): string; +export function resolveAndroidSdkEnv(options?: { + env?: NodeJS.ProcessEnv; + existsSync?: (path: string) => boolean; + homeDir?: string; + platform?: NodeJS.Platform; +}): NodeJS.ProcessEnv; +export function run( + command: unknown, + args: unknown, + cwd: unknown, + env?: NodeJS.ProcessEnv, +): Promise; +export function main(argv?: string[]): Promise; diff --git a/scripts/run-oxlint-shards.d.mts b/scripts/run-oxlint-shards.d.mts new file mode 100644 index 000000000000..0ddeab3f83ec --- /dev/null +++ b/scripts/run-oxlint-shards.d.mts @@ -0,0 +1,106 @@ +/** + * Builds the platform-specific oxlint shard list. + */ +export function createOxlintShards({ + cwd, + env, + platform, + readDir, + splitCore, +}?: { + cwd?: string | undefined; + env?: NodeJS.ProcessEnv | undefined; + platform?: NodeJS.Platform | undefined; + readDir?: ((target: string) => string[]) | undefined; + splitCore?: boolean | undefined; +}): { + name: string; + args: string[]; +}[]; +/** + * Chunks extension lint targets to avoid Windows command-line and memory limits. + */ +export function createWindowsExtensionShards({ + cwd, + env, + readDir, +}?: { + cwd?: string | undefined; + env?: NodeJS.ProcessEnv | undefined; + readDir?: ((target: string) => string[]) | undefined; +}): { + name: string; + args: string[]; +}[]; +/** + * Reads the Windows extension shard chunk size. + */ +export function resolveWindowsExtensionChunkSize(env?: NodeJS.ProcessEnv): unknown; +/** + * Chooses serial shard execution for constrained hosts or Windows. + */ +export function shouldRunOxlintShardsSerial({ + env, + platform, + hostResources, +}?: { + env?: NodeJS.ProcessEnv | undefined; + hostResources?: { logicalCpuCount: number; totalMemoryBytes: number }; + platform?: NodeJS.Platform | undefined; +}): boolean; +/** + * Runs selected oxlint shards and returns process-style success/failure. + */ +export function main(extraArgs?: string[], runtimeEnv?: NodeJS.ProcessEnv): Promise; +/** + * Parses shard-runner flags separately from forwarded oxlint args. + */ +export function parseShardRunnerArgs(args: string[]): { + only: Set; + oxlintArgs: string[]; + splitCore: boolean; +}; +/** + * Filters shards by an optional comma-separated shard name list. + */ +export function filterOxlintShards(shards: T[], only: Set): T[]; +/** + * Resolves shard concurrency from env, platform, and host resources. + */ +export function resolveOxlintShardConcurrency({ + env, + platform, + hostResources, + splitCore, +}?: { + env?: NodeJS.ProcessEnv | undefined; + hostResources?: { logicalCpuCount: number; totalMemoryBytes: number }; + platform?: NodeJS.Platform | undefined; + splitCore?: boolean | undefined; +}): number; +/** + * Runs one oxlint shard with bounded output, heartbeat, and forced cleanup. + */ +export function runShard({ + env, + extraArgs, + runner, + shard, +}: { + env: unknown; + extraArgs: unknown; + runner: unknown; + shard: unknown; +}): Promise; +/** + * Reads the shard heartbeat interval. + */ +export function resolveShardHeartbeatMs(env: unknown): unknown; +/** + * Reads the per-shard timeout. + */ +export function resolveShardTimeoutMs(env: unknown): unknown; +/** + * Reads the graceful shutdown window before SIGKILL. + */ +export function resolveShardKillGraceMs(env: unknown): unknown; diff --git a/scripts/run-oxlint.d.mts b/scripts/run-oxlint.d.mts new file mode 100644 index 000000000000..c334bb14517c --- /dev/null +++ b/scripts/run-oxlint.d.mts @@ -0,0 +1,31 @@ +/** + * Returns whether oxlint args need package-boundary declaration artifacts first. + */ +export function shouldPrepareExtensionPackageBoundaryArtifacts(args: unknown): boolean; +/** + * Drops tracked-but-missing sparse-checkout targets so narrow sparse checks can pass. + */ +export function filterSparseMissingOxlintTargets( + args: string[], + { + cwd, + fileExists, + isSparseCheckoutEnabled, + isTrackedPath, + }?: { + cwd?: string | undefined; + fileExists?: ((target: string) => boolean) | undefined; + isSparseCheckoutEnabled?: ((params: { cwd: string }) => boolean) | undefined; + isTrackedPath?: ((params: { cwd: string; target: string }) => boolean) | undefined; + }, +): { + args: string[]; + hadExplicitTargets: boolean; + remainingExplicitTargets: number; + skippedTargets: string[]; + skippedConfigs: string[]; +}; +/** + * Applies wrapper policy and runs oxlint with the final argument list. + */ +export function main(argv?: string[], runtimeEnv?: NodeJS.ProcessEnv): Promise; diff --git a/scripts/run-vitest-profile.d.mts b/scripts/run-vitest-profile.d.mts new file mode 100644 index 000000000000..fb03a2910c0e --- /dev/null +++ b/scripts/run-vitest-profile.d.mts @@ -0,0 +1,65 @@ +export function parseArgs(argv: string[]): { + mode: "main" | "runner"; + outputDir: string; + vitestArgs: string[]; +}; +/** + * Resolves or creates the directory used for profiler artifacts. + */ +export function resolveVitestProfileDir({ + mode, + outputDir, +}: { + mode: unknown; + outputDir: unknown; +}): string; +/** + * Builds a profiler command without additional Vitest args. + */ +export function buildVitestProfileCommand({ + mode, + outputDir, +}: { + mode: unknown; + outputDir: unknown; +}): { + command: string; + args: unknown[]; +}; +/** + * Builds the profiler command for either Vitest main or worker-runner profiling. + */ +export function buildVitestProfileCommandWithArgs({ + mode, + outputDir, + vitestArgs, +}: { + mode: unknown; + outputDir: unknown; + vitestArgs: unknown; +}): { + command: string; + args: unknown[]; +}; +/** + * Converts a profiler plan into a spawn spec, routing pnpm through the wrapper. + */ +export function buildVitestProfileSpawnSpec( + plan: unknown, + runnerOptions?: Record, +): + | { + args: string[]; + command: string; + options: import("node:child_process").SpawnOptions; + } + | { + args: unknown; + command: unknown; + options: { + env: NodeJS.ProcessEnv; + stdio: string; + shell: boolean; + windowsVerbatimArguments?: boolean; + }; + }; diff --git a/scripts/run-vitest.d.mts b/scripts/run-vitest.d.mts new file mode 100644 index 000000000000..bdbe1e3e470a --- /dev/null +++ b/scripts/run-vitest.d.mts @@ -0,0 +1,76 @@ +import type { ChildProcess, SpawnOptions } from "node:child_process"; +type VitestFs = { + existsSync(path: string): boolean; + symlinkSync?(target: string, path: string, type: string): void; +}; + +export const DEFAULT_VITEST_NO_OUTPUT_TIMEOUT_MS: 120000; +export const DEFAULT_VITEST_NO_OUTPUT_HEARTBEAT_MS: 30000; +export const DEFAULT_LONG_RUNNING_VITEST_NO_OUTPUT_TIMEOUT_MS: 300000; +export const DEFAULT_EXTRA_LONG_RUNNING_VITEST_NO_OUTPUT_TIMEOUT_MS: 2400000; +export const VITEST_CONFIG_NO_OUTPUT_TIMEOUT_MS: Map; +export const TOOLING_EXCLUDED_TESTS: Set; + +export function resolveVitestNodeArgs(env?: NodeJS.ProcessEnv): string[]; +export function resolveMissingVitestDependencyMessage(baseDir?: string, fsImpl?: VitestFs): string; +export function resolveVitestCliEntry(params?: { + baseDir?: string; + env?: NodeJS.ProcessEnv; + fsImpl?: VitestFs; + platform?: NodeJS.Platform; + requireResolve?: (specifier: string, options?: { paths?: string[] }) => string; +}): string; +export function resolveVitestNoOutputTimeoutMs(env?: NodeJS.ProcessEnv): number | null; +export function resolveVitestNoOutputHeartbeatMs(env?: NodeJS.ProcessEnv): number | null; +export function resolveRunVitestSpawnEnv( + env?: NodeJS.ProcessEnv, + argv?: string[], +): NodeJS.ProcessEnv; +export function resolveDefaultVitestNoOutputTimeoutMs(argv?: string[]): number; +export function resolveVitestSpawnParams( + env?: NodeJS.ProcessEnv, + platform?: NodeJS.Platform, +): SpawnOptions & { env: NodeJS.ProcessEnv; detached: boolean; stdio: string[] }; +export function shouldSuppressVitestStderrLine(line: string): boolean; +export function resolveDirectNodeVitestArgs(pnpmArgs: string[]): string[] | null; +export function resolveExplicitTestFileNoPassArgs(argv: string[]): string[]; +export function resolveTestProjectsDelegationArgs(argv: string[], cwd?: string): string[] | null; +export function resolveMissingExplicitTestFiles( + argv: string[], + cwd?: string, + fsImpl?: VitestFs, +): string[]; +export function resolveImplicitVitestArgs(argv: string[], cwd?: string): string[]; +export function installVitestNoOutputWatchdog(params: { + streams?: Array<{ on(event: string, listener: (...args: unknown[]) => void): unknown } | null>; + timeoutMs: number | null; + heartbeatMs?: number | null; + label?: string; + forceKillAfterMs?: number; + log?: (message: string) => void; + onTimeout?: () => void; + onForceKill?: () => void; + setTimeoutFn?: typeof setTimeout; + clearTimeoutFn?: typeof clearTimeout; +}): () => void; +export function spawnWatchedVitestProcess(params: { + pnpmArgs: string[]; + spawnParams: SpawnOptions; + env: NodeJS.ProcessEnv; + label?: string; + onNoOutputTimeout?: () => void; +}): { + child: ChildProcess; + getForwardedSignal: () => NodeJS.Signals | null; + teardown: () => void; +}; +export function resolveTestProjectsRunnerEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv; +export function resolveTestProjectsRunnerSpawnParams( + env: NodeJS.ProcessEnv, + platform?: NodeJS.Platform, +): SpawnOptions & { env: NodeJS.ProcessEnv; detached: boolean; stdio: "inherit" }; +export function runTestProjectsDelegation( + argv: string[], + env: NodeJS.ProcessEnv, + options?: { runnerPath?: string }, +): ChildProcess; diff --git a/scripts/run-with-env.d.mts b/scripts/run-with-env.d.mts new file mode 100644 index 000000000000..3ca23c7310e1 --- /dev/null +++ b/scripts/run-with-env.d.mts @@ -0,0 +1,48 @@ +/** + * Detects help requests before the command separator. + */ +export function isRunWithEnvHelpRequest(argv: unknown): boolean; +/** + * Parses KEY=value assignments and the command following --. + */ +export function parseRunWithEnvArgs(argv: unknown): { + env: Record; + command: unknown; + args: unknown; +}; +/** + * Resolves bare Node command names to the current executable so wrapper and child use the same + * runtime. Windows command lookup is case-insensitive; explicit paths remain caller-owned. + */ +export function resolveSpawnCommand( + command: unknown, + args: unknown, + execPath?: string, + platform?: NodeJS.Platform, +): { + command: unknown; + args: unknown; +}; +/** + * Reads the signal-forwarding force-kill grace period. + */ +export function resolveForceKillDelayMs(env?: NodeJS.ProcessEnv): number; +/** + * Signals the wrapped command tree when this small parent wrapper is stopped. + */ +export function signalRunWithEnvChild( + child: unknown, + signal: unknown, + { + platform, + runTaskkill, + useChildProcessGroup, + }?: { + platform?: NodeJS.Platform | undefined; + runTaskkill?: + | ((command: string, args?: string[]) => { error?: Error; status: number | null }) + | undefined; + useChildProcessGroup?: boolean | undefined; + }, +): void; +import { spawnSync } from "node:child_process"; diff --git a/scripts/runtime-postbuild-stamp.d.mts b/scripts/runtime-postbuild-stamp.d.mts new file mode 100644 index 000000000000..40c1ff9f6c29 --- /dev/null +++ b/scripts/runtime-postbuild-stamp.d.mts @@ -0,0 +1,5 @@ +#!/usr/bin/env node +export { + RUNTIME_POSTBUILD_STAMP_FILE, + writeRuntimePostBuildStamp, +} from "./lib/local-build-metadata.mjs"; diff --git a/scripts/sync-native-a2ui.d.mts b/scripts/sync-native-a2ui.d.mts new file mode 100644 index 000000000000..7547e4a0e5c9 --- /dev/null +++ b/scripts/sync-native-a2ui.d.mts @@ -0,0 +1,19 @@ +#!/usr/bin/env node +export function getNativeA2uiResourcePaths(repoRoot?: string): { + sourceDir: string; + nativeDir: string; +}; +export function syncNativeA2uiResources({ + sourceDir, + nativeDir, +}: { + sourceDir: unknown; + nativeDir: unknown; +}): Promise; +export function checkNativeA2uiResources({ + sourceDir, + nativeDir, +}: { + sourceDir: unknown; + nativeDir: unknown; +}): Promise; diff --git a/scripts/test-built-status-message-runtime.d.mts b/scripts/test-built-status-message-runtime.d.mts new file mode 100644 index 000000000000..fca392b30a1b --- /dev/null +++ b/scripts/test-built-status-message-runtime.d.mts @@ -0,0 +1,4 @@ +/** + * Finds the preferred built status-message runtime bundle under dist. + */ +export function findBuiltStatusMessageRuntimePath(distDir: unknown): string; diff --git a/scripts/test-docker-all.d.mts b/scripts/test-docker-all.d.mts new file mode 100644 index 000000000000..cec609d312e0 --- /dev/null +++ b/scripts/test-docker-all.d.mts @@ -0,0 +1,97 @@ +export function parseDockerAllCliArgs(argv: unknown): { + help: boolean; + planJson: boolean; +}; +export function describeDockerSchedulerLimits(parallelism: unknown, options: unknown): string; +export function canStartSchedulerLane( + candidate: unknown, + active: unknown, + parallelism: unknown, + options: unknown, +): boolean; +export function githubWorkflowRerunCommand( + laneNames: unknown, + ref: unknown, + env?: NodeJS.ProcessEnv, +): string; +export function writeRunSummary( + logDir: unknown, + summary: unknown, + env?: NodeJS.ProcessEnv, +): Promise; +export function dockerPreflightContainerNames(raw: unknown): unknown; +export function resolveDockerPreflightPlatform( + arch?: NodeJS.Architecture, +): "linux/arm64" | "linux/amd64"; +export function dockerPreflightSmokeCommand(arch?: NodeJS.Architecture): string; +export function runShellCommand({ + command, + env, + label, + logFile, + timeoutMs, + noOutputTimeoutMs, + timeoutKillGraceMs, +}: { + command: unknown; + env: unknown; + label: unknown; + logFile?: string; + timeoutMs?: number; + noOutputTimeoutMs?: number; + timeoutKillGraceMs?: number | undefined; +}): unknown; +export function appendBoundedShellCapture( + current: unknown, + chunk: unknown, + maxChars?: number, +): { + text: string; + truncated: boolean; +}; +export function runShellCaptureCommand({ + command, + env, + label, + timeoutMs, + timeoutKillGraceMs, +}: { + command: unknown; + env: unknown; + label: unknown; + timeoutMs: unknown; + timeoutKillGraceMs?: number | undefined; +}): unknown; +export function runCleanupSmokePhase( + baseEnv: unknown, + logDir: unknown, + phases: unknown, +): Promise< + | { + command: unknown; + attempts: { + attempt: number; + elapsedSeconds: number; + finishedAt: string; + noOutputTimedOut: boolean; + startedAt: string; + status: number; + timedOut: boolean; + }[]; + elapsedSeconds: number; + finishedAt: string; + image: unknown; + logFile: string; + name: string; + noOutputTimedOut: boolean; + rerunCommand: unknown; + startedAt: string; + status: number; + targetable: boolean; + timedOut: boolean; + } + | undefined +>; +export function tailFile(file: unknown, lines: unknown, maxBytes?: number): Promise; +export const SHELL_CAPTURE_MAX_CHARS: number; +export const LOG_TAIL_MAX_BYTES: number; diff --git a/scripts/test-extension-batch.d.mts b/scripts/test-extension-batch.d.mts new file mode 100644 index 000000000000..4dcf79b2f68a --- /dev/null +++ b/scripts/test-extension-batch.d.mts @@ -0,0 +1,18 @@ +import type { ExtensionBatchPlan } from "./lib/extension-test-plan.mjs"; +import type { VitestBatchRunParams } from "./lib/vitest-batch-runner.mjs"; + +export function parseExtensionIds(rawArgs: string[]): string[]; +export function resolveExtensionBatchParallelism( + groupCount: number, + env?: NodeJS.ProcessEnv, +): number; +export function parseExactVitestExcludePaths(vitestArgs: string[]): string[]; +export function runExtensionBatchPlan( + batchPlan: ExtensionBatchPlan, + params?: { + allowEmptyAfterExclude?: boolean; + env?: NodeJS.ProcessEnv; + runGroup?: (params: VitestBatchRunParams) => Promise; + vitestArgs?: string[]; + }, +): Promise; diff --git a/scripts/test-group-report.d.mts b/scripts/test-group-report.d.mts new file mode 100644 index 000000000000..191ebcfa6db8 --- /dev/null +++ b/scripts/test-group-report.d.mts @@ -0,0 +1,87 @@ +import type { ChildProcess } from "node:child_process"; + +export type TestGroupReportArgs = { + allowFailures: boolean; + compare: { after: string; before: string } | null; + concurrency: number | null; + configs: string[]; + fullSuite: boolean; + groupBy: string; + help?: boolean; + killGraceMs: number; + limit: number; + maxTestMs: number | null; + output: string | null; + reports: string[]; + rss: boolean; + timeoutMs: number; + topFiles: number; + vitestArgs: string[]; +}; + +export type TestGroupRunPlan = { + config: string; + forwardedArgs: string[]; + label: string; +}; + +export type TestGroupRunSpec = TestGroupRunPlan & { + env: NodeJS.ProcessEnv; + vitestArgs: string[]; +}; + +export type TestGroupRun = { + config: string; + elapsedMs: number; + label: string; + logPath: string; + maxRssBytes: number | null; + reportPath: string; + status: number; +}; + +export function parseTestGroupReportArgs(argv: string[]): TestGroupReportArgs; +export function signalTestGroupReportChild( + child: Pick, + signal: NodeJS.Signals, + options?: Record, +): void; +export function spawnText( + command: string, + args: string[], + options: Record, +): Promise<{ output: string; status: number; signal: NodeJS.Signals | null }>; +export function resolveReportArtifactDirs(outputPath: string): { + logDir: string; + reportDir: string; +}; +export function resolveRunPlans(args: TestGroupReportArgs): TestGroupRunPlan[]; +export function resolveFullSuiteVitestEnv( + args: TestGroupReportArgs, + env?: NodeJS.ProcessEnv, + label?: string, +): NodeJS.ProcessEnv; +export function resolveRunPlanConcurrency(args: TestGroupReportArgs, runPlanCount: number): number; +export function resolveReportVitestArgs(args: TestGroupReportArgs): string[]; +export function resolveReportRunSpecs( + args: TestGroupReportArgs, + runPlans: TestGroupRunPlan[], + params?: { cwd?: string; env?: NodeJS.ProcessEnv }, +): TestGroupRunSpec[]; +export function runReportPlans(params: { + args: TestGroupReportArgs; + logDir: string; + reportDir: string; + runPlans: TestGroupRunPlan[]; + runVitestJsonReport?: (params: { + config: string; + label: string; + logPath: string; + reportPath: string; + }) => Promise; +}): Promise<{ + exitCode: number; + failed: boolean; + runEntries: Array<{ config: string; report: unknown }>; + runs: TestGroupRun[]; +}>; diff --git a/scripts/test-live-shard.d.mts b/scripts/test-live-shard.d.mts new file mode 100644 index 000000000000..4aadb02f1eed --- /dev/null +++ b/scripts/test-live-shard.d.mts @@ -0,0 +1,75 @@ +#!/usr/bin/env node +/** + * Lists all live test files from git/find fallback paths. + */ +export function collectAllLiveTestFiles(repoRoot?: string): string[]; +/** + * Selects the live test files belonging to one shard name. + */ +export function selectLiveShardFiles(shard: string, files?: string[]): string[]; +/** + * Parses live-shard CLI args into shard name and Vitest passthrough args. + */ +export function parseLiveShardArgs(args: string[]): { + shard: string; + listOnly: boolean; + passthroughArgs: string[]; +}; +/** + * Builds pnpm/vitest args for selected live test files. + */ +export function buildLiveShardPnpmArgs(files: string[], passthroughArgs: string[]): string[]; +/** + * Resolves build profiles required by selected live tests. + */ +export function resolveLiveShardPreparation(files: unknown): { + env: { + OPENCLAW_BUILD_PRIVATE_QA: string; + }; + profile: string; + requiredArtifact: string; +} | null; +/** + * Builds the Vitest JSON report path used to prove that a live shard ran tests. + */ +export function buildLiveShardReportPath(shard: string, env?: NodeJS.ProcessEnv): string; +/** + * Adds reporters needed for both operator logs and machine-readable evidence. + */ +export function addLiveShardReportArgs(passthroughArgs: string[], reportPath: string): string[]; +/** + * Removes a previous JSON report before a shard run so stale success cannot be reused. + */ +export function removeLiveShardReportFile(reportPath: string): void; +/** + * Validates a Vitest JSON payload for live-shard proof. + */ +export function validateLiveShardReportPayload( + payload: unknown, + expectedFiles?: unknown[], + repoRoot?: string, + env?: NodeJS.ProcessEnv, +): + | { + ok: boolean; + reason: string; + } + | { + ok: boolean; + reason?: undefined; + }; +/** + * Builds spawn options for the live-shard Vitest child. + */ +export function buildLiveShardSpawnParams( + env?: NodeJS.ProcessEnv, + platform?: NodeJS.Platform, +): { + detached: boolean; + env: NodeJS.ProcessEnv; + stdio: string; +}; +/** Live-test shards included in release validation. */ +export const RELEASE_LIVE_TEST_SHARDS: readonly string[]; +/** All live-test shards, including broader local-only shard aliases. */ +export const LIVE_TEST_SHARDS: readonly string[]; diff --git a/scripts/test-live.d.mts b/scripts/test-live.d.mts new file mode 100644 index 000000000000..5167b47c8eaf --- /dev/null +++ b/scripts/test-live.d.mts @@ -0,0 +1,23 @@ +export type TestLiveArgs = { + forceCodexHarness: boolean; + forwardedArgs: string[]; + help: boolean; + quietOverride: string | undefined; +}; +export function parseTestLiveArgs(argv: string[]): TestLiveArgs; +export function buildTestLiveEnv( + args: TestLiveArgs, + baseEnv?: NodeJS.ProcessEnv, +): NodeJS.ProcessEnv; +export function resolveTestLiveHeartbeatMs(baseEnv?: NodeJS.ProcessEnv): number; +export function resolveTestLiveNoOutputTimeoutMs(baseEnv?: NodeJS.ProcessEnv): number | null; +export function buildTestLivePnpmArgs(args: TestLiveArgs): string[]; +export function buildTestLiveSpawnParams( + env: NodeJS.ProcessEnv, + platform?: NodeJS.Platform, +): { + detached: boolean; + env: NodeJS.ProcessEnv; + stdio: ["inherit", "pipe", "pipe"]; +}; +export function main(argv?: string[], baseEnv?: NodeJS.ProcessEnv): void; diff --git a/scripts/test-perf-budget.d.mts b/scripts/test-perf-budget.d.mts new file mode 100644 index 000000000000..dd6f84cd8fd6 --- /dev/null +++ b/scripts/test-perf-budget.d.mts @@ -0,0 +1,11 @@ +export namespace testing { + export { collectPerfReportStats }; + export { parseArgs }; + export { parseBudgetNumber }; +} +declare function collectPerfReportStats(reportPath: unknown): { + fileCount: number; + totalFileDurationMs: number; +}; +declare function parseArgs(argv: unknown, env?: NodeJS.ProcessEnv): unknown; +import { parseBudgetNumber } from "./lib/budget-number-args.mjs"; diff --git a/scripts/test-report-utils.d.mts b/scripts/test-report-utils.d.mts new file mode 100644 index 000000000000..9502f55f119e --- /dev/null +++ b/scripts/test-report-utils.d.mts @@ -0,0 +1,38 @@ +/** + * Normalizes absolute or relative file names to repo-relative POSIX paths. + */ +export function normalizeTrackedRepoPath(value: unknown): unknown; +/** + * Reads and parses a JSON file. + */ +export function readJsonFile(filePath: unknown): unknown; +/** + * Reads a JSON file or returns the provided fallback on failure. + */ +export function tryReadJsonFile(filePath: unknown, fallback: unknown): unknown; +/** + * Runs Vitest with the JSON reporter unless an existing report was supplied. + */ +export function runVitestJsonReport({ + config, + reportPath, + prefix, +}: { + config: unknown; + reportPath?: string | undefined; + prefix?: string | undefined; +}): string; +/** + * Extracts per-file durations from a Vitest JSON report. + */ +export function collectVitestFileDurations( + report: unknown, + normalizeFile?: (value: unknown) => unknown, +): unknown; +/** + * Extracts per-assertion durations from a Vitest JSON report. + */ +export function collectVitestAssertionDurations( + report: unknown, + normalizeFile?: (value: unknown) => unknown, +): unknown; diff --git a/scripts/testbox-lease-freshness.d.mts b/scripts/testbox-lease-freshness.d.mts new file mode 100644 index 000000000000..23d10a588e15 --- /dev/null +++ b/scripts/testbox-lease-freshness.d.mts @@ -0,0 +1,40 @@ +export function buildTestboxLeaseFingerprint( + repoRoot: unknown, + args: unknown, +): { + version: number; + baseSha: string; + headSha: string; + workingTreeClean: boolean; + dependencyDigest: string; + environmentDigest: string; + workflow: unknown; + job: unknown; + ref: unknown; +}; +export function testboxLeaseStaleReasons(saved: unknown, current: unknown): string[]; +export function prepareTestboxLeaseFreshness({ + args, + env, + provider, + repoRoot, +}: { + args: unknown; + env: unknown; + provider: unknown; + repoRoot: unknown; +}): { + current: { + version: number; + baseSha: string; + headSha: string; + workingTreeClean: boolean; + dependencyDigest: string; + environmentDigest: string; + workflow: unknown; + job: unknown; + ref: unknown; + }; + path: string; +} | null; +export function recordTestboxLeaseFreshness(prepared: unknown): void; diff --git a/scripts/transitive-manifest-risk-report.d.mts b/scripts/transitive-manifest-risk-report.d.mts new file mode 100644 index 000000000000..31ac4d16fa34 --- /dev/null +++ b/scripts/transitive-manifest-risk-report.d.mts @@ -0,0 +1,73 @@ +export type PackageVersion = { + packageName: string; + version: string; +}; + +export type Manifest = { + dependencies?: Record; + optionalDependencies?: Record; + scripts?: Record; +}; + +export type ManifestLoadResult = { + manifest: Manifest; + publishedAt: string | null; +}; + +export type ManifestFinding = { + type: string; + packageName: string; + version: string; + dependency?: { name: string; spec: string; section: string }; + source?: string; + script?: string; + publishedAt?: string; + minimumReleaseAgeMinutes?: number; + workspaceExcluded?: boolean; + workspaceExclusion?: string; +}; + +export type TransitiveManifestRiskReport = { + generatedAt: string; + packageVersions: number; + findingCount: number; + byType: Record; + workspacePolicy: { + minimumReleaseAgeMinutes: number | null; + minimumReleaseAgeExclude: string[]; + }; + workspaceExcludedFindingCount: number; + workspaceExcludedByType: Record; + workspaceExcludedFindings: ManifestFinding[]; + metadataFailures: Array<{ packageName: string; version: string; error: string }>; + findings: ManifestFinding[]; +}; + +export function readBoundedNpmRegistryText( + response: Response, + maxBytes?: number, + options?: { signal?: AbortSignal }, +): Promise; + +export function fetchNpmManifest(options: { + packageName: string; + version: string; + fetchImpl: typeof fetch; + registryBaseUrl: string; + maxBytes?: number; + timeoutMs?: number; +}): Promise; + +export function createTransitiveManifestRiskReport(options: { + packageVersions: PackageVersion[]; + manifestLoader: (entry: PackageVersion) => Promise; + now?: Date; + minimumReleaseAgeMinutes?: number | null; + minimumReleaseAgeExclude?: string[]; +}): Promise; + +export function renderTransitiveManifestRiskMarkdownReport( + report: TransitiveManifestRiskReport, +): string; + +export function main(argv?: string[]): Promise; diff --git a/scripts/tsdown-build.d.mts b/scripts/tsdown-build.d.mts new file mode 100644 index 000000000000..3da4ef4bb520 --- /dev/null +++ b/scripts/tsdown-build.d.mts @@ -0,0 +1,94 @@ +#!/usr/bin/env node +/** + * Removes build output roots while preserving explicitly protected artifacts. + */ +export function cleanTsdownOutputRoots(params?: Record): void; +export function pruneStaleRootChunkFiles(params?: Record): void; +export function listTsdownOutputRoots(): string[]; +export function pruneUntrackedGeneratedSourceDeclarations(params?: Record): number; +export function pruneSourceCheckoutBundledPluginNodeModules(params?: Record): void; +export function parseTsdownBuildArgs(argv: unknown): { + forwardedArgs: unknown; + help: boolean; +}; +export function createTsdownOutputScanner(params?: Record): { + append(chunk: unknown): void; + finish(): { + captured: string; + hasIneffectiveDynamicImport: boolean; + fatalUnresolvedImport: unknown; + }; +}; +export function resolveTsdownBuildInvocation(params?: Record): + | { + command: unknown; + args: unknown[]; + options: { + stdio: string[]; + shell: boolean; + windowsVerbatimArguments: undefined; + env: NodeJS.ProcessEnv; + }; + } + | { + command: string; + args: string[]; + options: { + stdio: string[]; + shell: boolean; + windowsVerbatimArguments: boolean | undefined; + env: NodeJS.ProcessEnv; + }; + }; +/** Builds AI package declarations first, then consumes them from the main graph. */ +export function resolveTsdownBuildInvocations(params?: Record): ( + | { + command: unknown; + args: unknown[]; + options: { + stdio: string[]; + shell: boolean; + windowsVerbatimArguments: undefined; + env: NodeJS.ProcessEnv; + }; + } + | { + command: string; + args: string[]; + options: { + stdio: string[]; + shell: boolean; + windowsVerbatimArguments: boolean | undefined; + env: NodeJS.ProcessEnv; + }; + } +)[]; +export function signalTsdownBuildProcessTree( + child: { pid?: number; kill(signal?: NodeJS.Signals): unknown }, + signal: NodeJS.Signals, + { + platform, + runTaskkill, + useProcessGroup, + }?: { + platform?: NodeJS.Platform | undefined; + runTaskkill?: + | (( + command: string, + args: string[], + options: { stdio: "ignore" }, + ) => { error?: Error; status: number | null }) + | undefined; + useProcessGroup?: boolean | undefined; + }, +): void; +export function runTsdownBuildInvocation( + invocation: unknown, + params?: Record, +): Promise<{ + captured: string; + hasIneffectiveDynamicImport: boolean; + signal: NodeJS.Signals | null; + status: number | null; + timedOut: boolean; +}>; diff --git a/scripts/validate-full-release-validation-evidence.d.mts b/scripts/validate-full-release-validation-evidence.d.mts new file mode 100644 index 000000000000..5fd5563c4ff5 --- /dev/null +++ b/scripts/validate-full-release-validation-evidence.d.mts @@ -0,0 +1,62 @@ +#!/usr/bin/env node +export function normalizeFullReleaseValidationRun(run: unknown): { + databaseId: string; + runAttempt: number; + workflowName: unknown; + workflowPath: string; + workflowQualifiedRef: string; + repository: unknown; + headBranch: unknown; + headSha: unknown; + event: unknown; + status: unknown; + conclusion: unknown; + url: unknown; +}; +export function isShaPinnedReleaseValidationBranch(branch: unknown): boolean; +export function validateFullReleaseValidationEvidence({ + run: rawRun, + manifest, + expectedRepository, + expectedRunId, + expectedTargetSha, + expectedWorkflowBranch, + isTrustedMainAncestor, + validateEvidenceReuseStrictly, +}: { + run: unknown; + manifest: unknown; + expectedRepository: unknown; + expectedRunId: unknown; + expectedTargetSha: unknown; + expectedWorkflowBranch: unknown; + isTrustedMainAncestor: unknown; + validateEvidenceReuseStrictly?: unknown; +}): { + run: { + databaseId: string; + runAttempt: number; + workflowName: unknown; + workflowPath: string; + workflowQualifiedRef: string; + repository: unknown; + headBranch: unknown; + headSha: unknown; + event: unknown; + status: unknown; + conclusion: unknown; + url: unknown; + }; + source: string; +}; +export function runStrictReleaseEvidenceValidation({ + repository, + runId, + validatorFile, + verifierSourceSha, +}: { + repository: unknown; + runId: unknown; + validatorFile?: string | undefined; + verifierSourceSha: unknown; +}): unknown; diff --git a/scripts/verify-clawhub-published-artifact.d.mts b/scripts/verify-clawhub-published-artifact.d.mts new file mode 100644 index 000000000000..00601a1f0206 --- /dev/null +++ b/scripts/verify-clawhub-published-artifact.d.mts @@ -0,0 +1,31 @@ +#!/usr/bin/env node +export function verifyPublishedClawHubArtifacts(options: unknown): Promise<{ + schemaVersion: number; + repository: unknown; + targetSha: unknown; + workflowSha: unknown; + runId: unknown; + producerRunAttempt: string; + terminalRunAttempt: string; + artifactName: unknown; + artifactId: string; + artifactDigest: unknown; + clawhubToolchainIntegrity: unknown; + clawhubToolchainSha256: unknown; + clawhubToolchainVersion: unknown; + requestedPlugins: unknown[]; + verificationMode: unknown; + packages: unknown[]; +}>; +export function verifyPublishedClawHubPackage(options: unknown): Promise<{ + schemaVersion: number; + verificationMode: string; + expectedArtifact: { + sha256: string; + size: unknown; + npmIntegrity: string; + npmShasum: string; + fileName: string; + }; + package: unknown; +}>; diff --git a/scripts/verify-docker-attestations.d.mts b/scripts/verify-docker-attestations.d.mts new file mode 100644 index 000000000000..906ac124aa40 --- /dev/null +++ b/scripts/verify-docker-attestations.d.mts @@ -0,0 +1,26 @@ +#!/usr/bin/env node +/** + * Rewrites an image reference to use the provided digest. + */ +export function imageRefForDigest(imageRef: unknown, digest: unknown): string; +/** + * Parses os/architecture[/variant] platform strings. + */ +export function parsePlatform(value: unknown): { + architecture: unknown; + os: unknown; + variant: unknown; +}; +/** + * Collects missing/mismatched attestation errors for required image platforms. + */ +export function collectDockerAttestationErrors(params: unknown): string[]; +export function parseArgs(argv: unknown): { + help: boolean; + imageRefs: unknown[]; + requiredPlatforms: { + architecture: unknown; + os: unknown; + variant: unknown; + }[]; +}; diff --git a/scripts/verify-plugin-npm-published-runtime.d.mts b/scripts/verify-plugin-npm-published-runtime.d.mts new file mode 100644 index 000000000000..8b73653efcaa --- /dev/null +++ b/scripts/verify-plugin-npm-published-runtime.d.mts @@ -0,0 +1,23 @@ +#!/usr/bin/env node +export function collectPluginNpmPublishedRuntimeErrors(params: unknown): string[]; +export function resolveNpmPackFilename(output: unknown): string; +export function readPositiveIntEnv( + name: unknown, + fallback: unknown, + env?: NodeJS.ProcessEnv, +): number; +export function readPluginNpmCommandOptions(env?: NodeJS.ProcessEnv): { + encoding: string; + killSignal: string; + maxBuffer: number; + stdio: string[]; + timeout: number; +}; +export function runPluginNpmCommand(args: unknown, params?: Record): unknown; +export function parseNpmReadmeMetadata(raw: unknown): string; +export function findPackedPackageReadmePath(files: unknown): unknown; +export function usage(): string; +export function parseVerifyPublishedPluginRuntimeArgs(argv: unknown): { + help: boolean; + spec: unknown; +}; diff --git a/scripts/verify-pr-hosted-gates.d.mts b/scripts/verify-pr-hosted-gates.d.mts new file mode 100644 index 000000000000..2d3a5af27eae --- /dev/null +++ b/scripts/verify-pr-hosted-gates.d.mts @@ -0,0 +1,59 @@ +#!/usr/bin/env node +export function parseArgs(argv: unknown): { + repo: string; + sha: string; + pr: number; + recentSha: string; + output: string; + changelogOnly: boolean; +}; +export function parseWorkflowRunPage(raw: unknown): { + totalCount: unknown; + workflowRuns: unknown; +}; +export function workflowRunPageCount(totalCount: unknown): number; +export function collectHostedGateEvidence({ + sha, + pr, + recentSha, + workflowRuns, + changelogOnly, + nowMs, +}: { + sha: string; + pr?: number; + recentSha?: string; + workflowRuns: Array>; + changelogOnly?: boolean | undefined; + nowMs?: number | undefined; +}): { + headSha: unknown; + workflows: { + id: unknown; + name: unknown; + event: unknown; + headSha: unknown; + headBranch: unknown; + status: unknown; + conclusion: unknown; + createdAt: unknown; + updatedAt: unknown; + url: unknown; + }[]; +}; +export function workflowRunQueryPaths( + repo: string, + { + sha, + recentSha, + headBranch, + }: { + sha: string; + recentSha: string; + headBranch?: string; + }, + page?: number, +): string[]; +export function main(argv?: string[]): void; +export const SCHEDULED_HOSTED_WORKFLOWS: string[]; +export const HOSTED_GATE_MAX_AGE_HOURS: 12; diff --git a/scripts/vitest-process-group.d.mts b/scripts/vitest-process-group.d.mts new file mode 100644 index 000000000000..d450cfaf630e --- /dev/null +++ b/scripts/vitest-process-group.d.mts @@ -0,0 +1,32 @@ +export function shouldUseDetachedVitestProcessGroup( + platform?: NodeJS.Platform, +): platform is + | "aix" + | "android" + | "darwin" + | "freebsd" + | "haiku" + | "linux" + | "openbsd" + | "sunos" + | "cygwin" + | "netbsd"; +/** + * Resolves the PID or process-group target for Vitest signal forwarding. + */ +export function resolveVitestProcessGroupSignalTarget(params: unknown): number | null; +/** + * Forwards a signal to the Vitest child or process group. + */ +export function forwardSignalToVitestProcessGroup(params: unknown): boolean; +/** + * Force-cleans unknown remaining processes in a Vitest child process group. + */ +export function forceKillVitestProcessGroup( + child: unknown, + kill?: (pid: number, signal?: string | number) => true, +): boolean; +/** + * Installs signal/exit cleanup handlers for a Vitest child process group. + */ +export function installVitestProcessGroupCleanup(params: unknown): () => void; diff --git a/scripts/watch-node.d.mts b/scripts/watch-node.d.mts index 88b136006634..9b431d46bc8f 100644 --- a/scripts/watch-node.d.mts +++ b/scripts/watch-node.d.mts @@ -1,3 +1,4 @@ +export function resolveWatchLockPath(cwd: string, args?: string[]): string; export function runWatchMain(params?: { spawn?: ( cmd: string, @@ -35,4 +36,7 @@ export function runWatchMain(params?: { args?: string[]; env?: NodeJS.ProcessEnv; now?: () => number; + lockDisabled?: boolean; + killProcessTree?: (pid: number, signal: NodeJS.Signals) => void; + signalProcess?: (pid: number, signal: NodeJS.Signals) => void; }): Promise; diff --git a/scripts/write-official-channel-catalog.d.mts b/scripts/write-official-channel-catalog.d.mts index e8e9c5c03612..9e67113c8806 100644 --- a/scripts/write-official-channel-catalog.d.mts +++ b/scripts/write-official-channel-catalog.d.mts @@ -5,7 +5,9 @@ export function buildOfficialChannelCatalog(params?: { repoRoot?: string; cwd?: name: string; version?: string; description?: string; + source?: string; openclaw: { + plugin?: Record; channel: Record; install: { clawhubSpec?: string; diff --git a/test/cli-json-stdout.e2e.test.ts b/test/cli-json-stdout.e2e.test.ts index 53dff52814eb..384f80f09f91 100644 --- a/test/cli-json-stdout.e2e.test.ts +++ b/test/cli-json-stdout.e2e.test.ts @@ -13,7 +13,7 @@ describe("cli json stdout contract", () => { await fs.mkdir(legacyDir, { recursive: true }); await fs.writeFile(path.join(legacyDir, "clawdbot.json"), "{}", "utf8"); - const env = { + const env: NodeJS.ProcessEnv = { ...process.env, HOME: tempHome, USERPROFILE: tempHome, diff --git a/test/e2e/qa-lab/plugins/plugin-lifecycle-probe-runtime.ts b/test/e2e/qa-lab/plugins/plugin-lifecycle-probe-runtime.ts index 5f4e70f409e4..79b632de91ac 100644 --- a/test/e2e/qa-lab/plugins/plugin-lifecycle-probe-runtime.ts +++ b/test/e2e/qa-lab/plugins/plugin-lifecycle-probe-runtime.ts @@ -1,5 +1,6 @@ // Plugin Lifecycle Probe tests cover QA Lab plugin lifecycle evidence. import { spawn, spawnSync } from "node:child_process"; +/* oxlint-disable eslint/no-shadow, eslint/prefer-const, eslint/no-promise-executor-return, typescript/restrict-template-expressions, typescript/no-base-to-string -- QA probe intentionally validates loosely typed external JSON and mirrors child-process callback shapes. */ import { randomBytes } from "node:crypto"; import fs from "node:fs"; import os from "node:os"; @@ -25,9 +26,9 @@ const tempDirs = (() => { }; })(); -type ProbeEnv = Pick; +type ProbeEnv = NodeJS.ProcessEnv; -type MatrixEnv = NodeJS.ProcessEnv & ProbeEnv; +type MatrixEnv = NodeJS.ProcessEnv; interface CommandOptions { env?: NodeJS.ProcessEnv; diff --git a/test/e2e/qa-lab/runtime/docker-e2e-lane.fixture.ts b/test/e2e/qa-lab/runtime/docker-e2e-lane.fixture.ts index 1e1bf83ed1c4..86e595341293 100644 --- a/test/e2e/qa-lab/runtime/docker-e2e-lane.fixture.ts +++ b/test/e2e/qa-lab/runtime/docker-e2e-lane.fixture.ts @@ -143,7 +143,7 @@ export function resolveQaDockerE2eLane( } const lane = QA_DOCKER_E2E_LANES[laneName]; return { - env: { ...env, ...lane.env?.(env) }, + env: { ...env, ...("env" in lane ? lane.env?.(env) : undefined) }, name: laneName, script: lane.script, }; diff --git a/test/e2e/qa-lab/runtime/gateway-mcp-real-transports.ts b/test/e2e/qa-lab/runtime/gateway-mcp-real-transports.ts index 50c9548e27c9..ebd31deb00e1 100644 --- a/test/e2e/qa-lab/runtime/gateway-mcp-real-transports.ts +++ b/test/e2e/qa-lab/runtime/gateway-mcp-real-transports.ts @@ -276,8 +276,10 @@ function resolveChannelMcpInvocation(params: { function parseJsonFrame(data: RawData): Record | null { try { const text = Array.isArray(data) - ? Buffer.concat(data).toString("utf8") - : Buffer.from(data).toString("utf8"); + ? Buffer.concat(data.map((chunk) => Buffer.from(chunk))).toString("utf8") + : Buffer.isBuffer(data) + ? data.toString("utf8") + : Buffer.from(data).toString("utf8"); const value = JSON.parse(text); return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) diff --git a/test/e2e/qa-lab/runtime/heartbeat-active-hours-runtime.ts b/test/e2e/qa-lab/runtime/heartbeat-active-hours-runtime.ts index 0e558fd21a71..2251d5d37708 100644 --- a/test/e2e/qa-lab/runtime/heartbeat-active-hours-runtime.ts +++ b/test/e2e/qa-lab/runtime/heartbeat-active-hours-runtime.ts @@ -73,7 +73,9 @@ async function waitForObservation( if (observations.slice(afterCount).some((entry) => entry.outcome === outcome)) { return; } - await new Promise((resolve) => setTimeout(resolve, 20)); + await new Promise((resolve) => { + setTimeout(resolve, 20); + }); } throw new Error(`heartbeat scheduler did not observe ${outcome} within ${timeoutMs}ms`); } @@ -110,7 +112,7 @@ export async function runHeartbeatActiveHoursRuntime(options: HeartbeatRuntimeOp cfg: currentConfig, readCurrentConfig: () => currentConfig, runOnce: async ({ cfg, heartbeat }) => { - const active = isWithinActiveHours(cfg, heartbeat); + const active = isWithinActiveHours(cfg!, heartbeat); const outcome = active ? "active-fire" : "quiet-hours-skip"; observations.push({ at: new Date().toISOString(), outcome }); writer.appendLog(`heartbeat-active-hours: ${outcome}\n`); diff --git a/test/e2e/qa-lab/runtime/mcp-channels-docker-client.ts b/test/e2e/qa-lab/runtime/mcp-channels-docker-client.ts index 97f30923c4ed..c0d52b441d62 100644 --- a/test/e2e/qa-lab/runtime/mcp-channels-docker-client.ts +++ b/test/e2e/qa-lab/runtime/mcp-channels-docker-client.ts @@ -134,7 +134,8 @@ async function main() { }), maybeApprovePairing: () => maybeApprovePendingBridgePairing(gateway), }); - const mcp = mcpHandle.client; + const connectedMcp = mcpHandle; + const mcp = connectedMcp.client; const callTool = (params: Parameters[0]) => mcp.callTool(params, undefined, { timeout: 240_000 }) as Promise; @@ -332,23 +333,20 @@ async function main() { let helpNotification: ClaudeChannelNotification; try { - helpNotification = await waitFor( - "Claude channel notification", - () => - mcpHandle.rawMessages - .map((entry) => ClaudeChannelNotificationSchema.safeParse(entry)) - .find( - (entry) => - entry.success && - entry.data.params.meta.session_key === "agent:main:main" && - entry.data.params.content === channelMessage, - )?.data.params, + helpNotification = await waitFor("Claude channel notification", () => + connectedMcp!.rawMessages + .map((entry) => ClaudeChannelNotificationSchema.safeParse(entry)) + .flatMap((entry) => (entry.success ? [entry.data.params] : [])) + .find( + (params) => + params.meta.session_key === "agent:main:main" && params.content === channelMessage, + ), ); } catch (error) { throw new Error( `timeout waiting for Claude channel notification: ${JSON.stringify( { - rawMessages: mcpHandle.rawMessages.slice(-10), + rawMessages: connectedMcp.rawMessages.slice(-10), }, null, 2, @@ -394,23 +392,22 @@ async function main() { await waitFor( "non-owner reply forwarded as an ordinary Claude channel message", () => - mcpHandle.rawMessages + connectedMcp!.rawMessages .map((entry) => ClaudeChannelNotificationSchema.safeParse(entry)) + .flatMap((entry) => (entry.success ? [entry.data.params] : [])) .find( - (entry) => - entry.success && - entry.data.params.meta.session_key === "agent:main:main" && - entry.data.params.content === "yes abcde", - )?.data.params, + (params) => + params.meta.session_key === "agent:main:main" && params.content === "yes abcde", + ), 60_000, ); await delay(NON_OWNER_PERMISSION_QUIET_WINDOW_MS); - const nonOwnerPermission = mcpHandle.rawMessages + const nonOwnerPermission = connectedMcp.rawMessages .map((entry) => ClaudePermissionNotificationSchema.safeParse(entry)) .find((entry) => entry.success && entry.data.params.request_id === "abcde"); assert(!nonOwnerPermission, "non-owner reply must not resolve the Claude permission"); - const ownerNotificationStart = mcpHandle.rawMessages.length; + const ownerNotificationStart = connectedMcp.rawMessages.length; await gateway.request("chat.send", { sessionKey: "agent:main:main", message: "yes abcde", @@ -421,18 +418,18 @@ async function main() { permission = await waitFor( "Claude permission notification", () => - mcpHandle.rawMessages + connectedMcp!.rawMessages .slice(ownerNotificationStart) .map((entry) => ClaudePermissionNotificationSchema.safeParse(entry)) - .find((entry) => entry.success && entry.data.params.request_id === "abcde")?.data - .params, + .flatMap((entry) => (entry.success ? [entry.data.params] : [])) + .find((params) => params.request_id === "abcde"), 60_000, ); } catch (error) { throw new Error( `timeout waiting for Claude permission notification: ${JSON.stringify( { - rawMessages: mcpHandle.rawMessages.slice(-10), + rawMessages: connectedMcp.rawMessages.slice(-10), recentGatewayEvents: gateway.events.slice(-10).map((entry) => ({ event: entry.event, sessionKey: entry.payload.sessionKey, @@ -455,7 +452,7 @@ async function main() { nonOwnerReplyForwarded: true, nonOwnerPermissionBlocked: true, ownerPermissionAllowed: permission.behavior === "allow", - rawNotifications: mcpHandle.rawMessages.filter( + rawNotifications: connectedMcp.rawMessages.filter( (entry) => ClaudeChannelNotificationSchema.safeParse(entry).success || ClaudePermissionNotificationSchema.safeParse(entry).success, diff --git a/test/e2e/qa-lab/runtime/mcp-channels.fixture.ts b/test/e2e/qa-lab/runtime/mcp-channels.fixture.ts index 9570c71a76f2..705e2813d8e4 100644 --- a/test/e2e/qa-lab/runtime/mcp-channels.fixture.ts +++ b/test/e2e/qa-lab/runtime/mcp-channels.fixture.ts @@ -7,9 +7,18 @@ import { setTimeout as delay } from "node:timers/promises"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { z } from "zod"; -import { PROTOCOL_VERSION } from "../../../../dist/gateway/protocol/index.js"; -import { formatErrorMessage } from "../../../../dist/infra/errors.js"; -import { readStringValue } from "../../../../dist/normalization-core/string-coerce.js"; +const protocolModulePath = "../../../../dist/gateway/protocol/index.js" as string; +const errorsModulePath = "../../../../dist/infra/errors.js" as string; +const stringCoerceModulePath = "../../../../dist/normalization-core/string-coerce.js" as string; +const [{ PROTOCOL_VERSION }, { formatErrorMessage }, { readStringValue }] = await Promise.all([ + import(protocolModulePath) as Promise< + typeof import("../../../../packages/gateway-protocol/src/index.js") + >, + import(errorsModulePath) as Promise, + import(stringCoerceModulePath) as Promise< + typeof import("../../../../packages/normalization-core/src/string-coerce.js") + >, +]); import { resolveGatewaySuccessPayload } from "../../../../scripts/e2e/lib/gateway-frame-payload.mjs"; import { readMcpChannelLimits } from "../../../../scripts/e2e/mcp-channel-limits.ts"; import { diff --git a/test/e2e/qa-lab/runtime/openai-image-auth-docker-client.ts b/test/e2e/qa-lab/runtime/openai-image-auth-docker-client.ts index af7aa3bbf5e4..5a570ea5b6c5 100644 --- a/test/e2e/qa-lab/runtime/openai-image-auth-docker-client.ts +++ b/test/e2e/qa-lab/runtime/openai-image-auth-docker-client.ts @@ -176,8 +176,11 @@ export async function main() { const records: RequestRecord[] = []; const mock = await startMockServer(records); try { - const { buildOpenAIImageGenerationProvider } = - await import("../../../../dist/extensions/openai/image-generation-provider.js"); + const providerModulePath = + "../../../../dist/extensions/openai/image-generation-provider.js" as string; + const { buildOpenAIImageGenerationProvider } = (await import( + providerModulePath + )) as typeof import("../../../../extensions/openai/image-generation-provider.js"); const provider = buildOpenAIImageGenerationProvider(); const directResult = await provider.generateImage({ diff --git a/test/e2e/qa-lab/runtime/qa-otel-smoke-runtime.ts b/test/e2e/qa-lab/runtime/qa-otel-smoke-runtime.ts index c91c0d32a841..ce70518124a9 100644 --- a/test/e2e/qa-lab/runtime/qa-otel-smoke-runtime.ts +++ b/test/e2e/qa-lab/runtime/qa-otel-smoke-runtime.ts @@ -1,6 +1,7 @@ // QA OTEL Smoke runtime supports OpenClaw repository automation. import { spawn } from "node:child_process"; +/* oxlint-disable typescript/unbound-method -- the original stream method is invoked with process.stdout through Reflect.apply below. */ import { randomUUID } from "node:crypto"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; @@ -1087,7 +1088,7 @@ async function stopDockerContainer(name: string): Promise { } type StartDockerOtelCollectorDeps = { - mkdtemp?: typeof mkdtemp; + mkdtemp?: (prefix: string) => Promise; platform?: NodeJS.Platform; randomUUID?: typeof randomUUID; reserveLocalPort?: typeof reserveLocalPort; @@ -1763,7 +1764,7 @@ async function main() { const originalStdoutWrite = process.stdout.write; process.stdout.write = ((chunk: string | Uint8Array, ...args: unknown[]) => { stdoutDiagnosticLogs.append(chunk); - return originalStdoutWrite.call(process.stdout, chunk, ...args); + return Reflect.apply(originalStdoutWrite, process.stdout, [chunk, ...args]) as boolean; }) as typeof process.stdout.write; try { await runDirectTelemetryProducer({ diff --git a/test/external-script-modules.d.ts b/test/external-script-modules.d.ts new file mode 100644 index 000000000000..d26baddf827d --- /dev/null +++ b/test/external-script-modules.d.ts @@ -0,0 +1,151 @@ +declare module "*security/opengrep/check-rule-metadata.mjs" { + export function validateRuleMetadata( + rules: Array<{ id: string; metadata?: Record }>, + ): string[]; +} + +declare module "*scripts/ui.js" { + type SpawnCall = { + command: string; + args: string[]; + options: { + cwd?: string; + env?: NodeJS.ProcessEnv; + shell: boolean; + stdio: string; + windowsVerbatimArguments?: boolean; + }; + }; + export function shouldUseCmdExeForCommand(cmd: string, platform?: NodeJS.Platform): boolean; + export function resolveSpawnCall( + cmd: string, + args: string[], + envOverride?: NodeJS.ProcessEnv, + params?: { comSpec?: string; cwd?: string; nodeExecPath?: string; platform?: NodeJS.Platform }, + ): SpawnCall; + export function resolvePnpmSpawnCall( + pnpmArgs: string[], + envOverride?: NodeJS.ProcessEnv, + params?: { comSpec?: string; cwd?: string; nodeExecPath?: string; platform?: NodeJS.Platform }, + ): SpawnCall; + export function isDirectScriptExecution( + entry?: string, + scriptPath?: string, + realpath?: (path: string) => string, + ): boolean; +} + +declare module "*openclaw-changelog-update/scripts/verify-release-notes.mjs" { + type ContributionRecord = { + externalReferences?: string[]; + references: number[]; + thanks: string[]; + }; + export function createGithubSnapshotState(params: Record): { + base: string; + checkpointEvery: number; + dirty: boolean; + filePath: string; + hits: number; + misses: number; + repository: string; + responses: Record; + target: string; + writesSincePersist: number; + }; + export function githubApiWithSnapshot( + args: string[], + fetchApi: (args: string[]) => unknown, + snapshotState: Record, + ): unknown; + export function persistGithubSnapshot(snapshotState: Record): void; + export function defaultGithubSnapshotPath( + base: string, + target: string, + gitCommonDir: string, + ): string; + export function renderContributionRecordEntry(entry: Record): string; + export function releaseNoteReferences( + sectionSource: string, + shippedBaselines: unknown[], + ): number[]; + export function standardRevertedHash(message: string): string | null; + export function contributionRecordFor(section: Record): { + legacyIssues: Map; + pullRequests: Map; + }; + export function cumulativeShippedPullRequests(changelog: unknown, label: string): Set; + export function subtractShippedPullRequests( + source: unknown, + baselines: unknown[], + ): { + baselines: unknown[]; + pullRequests: Set; + }; + export function withoutExcludedContributionRecords( + record: { + legacyIssues: Map; + pullRequests: Map; + }, + excluded: Set, + ): { + legacyIssues: Map; + pullRequests: Map; + }; + export function contaminatingPullRequestReferences(params: Record): unknown[]; + export function canonicalMainCommitMatches(commit: unknown, candidates: unknown[]): unknown[]; + export function canonicalPullRequests( + currentPullRequests: unknown[], + mainPullRequests: unknown[], + hasCanonicalMainCommit?: boolean, + ): unknown[]; + export function ledgerFor(...args: unknown[]): { + entries: unknown[]; + issues: unknown[]; + ledger: string; + pullRequests: unknown[]; + titleReferences: unknown[]; + }; + export function countTopLevelSectionBullets(sectionSource: string, heading: string): number; + export function highlightCountError(sectionSource: string): string | undefined; + export function ledgerChecks(...args: unknown[]): string[]; +} + +declare module "*openclaw-live-updater/scripts/update-main.mjs" { + type UpdateResult = Record & { + actions: Record; + buildBefore: Record; + changedPaths?: string[]; + macTarget?: Record; + release?: () => void; + }; + export function originMatches(remoteUrl: string): boolean; + export function classifyActions( + changedPaths: string[], + options: Record, + ): Record; + export function inspectBuildState(checkout: string, expectedSha: string): UpdateResult; + export function acquireMaintenanceLock( + checkout: string, + requestedPath?: string, + ): { + acquired: boolean; + owner: { pid: number; checkout?: string; startedAt?: string }; + release?: () => void; + }; + export function parseGatewayLogAudit(output: string, sinceMs: number): Record; + export function verifyGatewayReadiness( + runCommand: (command: string, args: string[], checkout: string) => unknown, + checkout: string, + expectedSha: string, + sleep?: (ms: number) => void, + ): void; + export function findExactMacTarget( + processes: string, + executable: string, + ): { executable: string; pid: number } | null; + export function maintainMain( + options: Record, + dependencies?: Record, + ): UpdateResult; +} diff --git a/test/global-setup.ts b/test/global-setup.ts index 4cb1850f2dc8..0f45b8fc89b9 100644 --- a/test/global-setup.ts +++ b/test/global-setup.ts @@ -1,5 +1,5 @@ // Global test setup installs shared environment before Vitest projects run. -import { installTestEnv } from "./test-env"; +import { installTestEnv } from "./test-env.js"; export default async () => { const { cleanup } = installTestEnv(); diff --git a/test/helpers/gateway-e2e-harness.test.ts b/test/helpers/gateway-e2e-harness.test.ts index 9fe4732267fc..11e701e14d47 100644 --- a/test/helpers/gateway-e2e-harness.test.ts +++ b/test/helpers/gateway-e2e-harness.test.ts @@ -1,5 +1,5 @@ // Gateway E2E harness tests cover helper server and probe behavior. -import { createServer, type Server } from "node:http"; +import { createServer, type RequestListener, type Server } from "node:http"; import { afterEach, describe, expect, it } from "vitest"; import { postJson } from "./gateway-e2e-harness.js"; @@ -21,7 +21,7 @@ afterEach(async () => { server = undefined; }); -async function listen(handler: Parameters[0]): Promise { +async function listen(handler: RequestListener): Promise { server = createServer(handler); await new Promise((resolve) => { server?.listen(0, "127.0.0.1", resolve); diff --git a/test/helpers/gateway-e2e-harness.ts b/test/helpers/gateway-e2e-harness.ts index 81ca7916f0b0..0a2a4f56b8eb 100644 --- a/test/helpers/gateway-e2e-harness.ts +++ b/test/helpers/gateway-e2e-harness.ts @@ -220,7 +220,9 @@ export async function waitForNodeStatus( } try { while (Date.now() < deadline) { - const list = await client.request("node.list", {}); + const list = (await client.request("node.list", {})) as { + nodes?: Array<{ nodeId: string; connected?: boolean; paired?: boolean }>; + }; const match = list.nodes?.find((n) => n.nodeId === nodeId); if (match?.connected && match?.paired) { return; diff --git a/test/helpers/openclaw-test-instance.ts b/test/helpers/openclaw-test-instance.ts index 49db75809133..79749d35f45c 100644 --- a/test/helpers/openclaw-test-instance.ts +++ b/test/helpers/openclaw-test-instance.ts @@ -213,7 +213,7 @@ const getFreePort = async () => { }; async function waitForPortOpen( - proc: OpenClawTestProcess, + proc: Pick, chunksOut: string[], chunksErr: string[], port: number, diff --git a/test/non-isolated-runner.ts b/test/non-isolated-runner.ts index 4525367b60a4..cde179cbafb9 100644 --- a/test/non-isolated-runner.ts +++ b/test/non-isolated-runner.ts @@ -1,7 +1,7 @@ // Non-isolated runner helps execute tests without Vitest isolation. import fs from "node:fs"; import path from "node:path"; -import { TestRunner, type RunnerTask, type RunnerTestSuite, vi } from "vitest"; +import { TestRunner, type RunnerTask, type RunnerTestFile, type RunnerTestSuite, vi } from "vitest"; type EvaluatedModuleNode = { promise?: unknown; @@ -14,6 +14,11 @@ type EvaluatedModules = { idToModuleMap: Map; }; +type TestRunnerInternals = { + moduleRunner?: { mocker?: { reset?: () => void } }; + workerState: { evaluatedModules: unknown }; +}; + const SHARED_TEST_SETUP = Symbol.for("openclaw.sharedTestSetup"); const EMBEDDED_RUN_STATE = Symbol.for("openclaw.embeddedRunState"); const REPLY_RUN_REGISTRY = Symbol.for("openclaw.replyRunRegistry"); @@ -181,6 +186,7 @@ function resetOpenClawGlobalRunState(): void { const cleanupError = runCleanupActions(cleanupActions); if (cleanupError) { + // oxlint-disable-next-line typescript/only-throw-error -- cleanup hooks may throw their original non-Error value; preserve that test-runner behavior. throw cleanupError; } @@ -202,7 +208,7 @@ function resetOpenClawGlobalRunState(): void { } export default class OpenClawNonIsolatedRunner extends TestRunner { - override onCollectStart(file: { filepath: string }) { + override onCollectStart(file: RunnerTestFile) { super.onCollectStart(file); restoreRealTimers(); restoreNativeTimerGlobals(); @@ -247,7 +253,8 @@ export default class OpenClawNonIsolatedRunner extends TestRunner { vi.clearAllMocks(); resetOpenClawGlobalRunState(); vi.resetModules(); - this.moduleRunner?.mocker?.reset?.(); - resetEvaluatedModules(this.workerState.evaluatedModules as EvaluatedModules, true); + const internals = this as unknown as TestRunnerInternals; + internals.moduleRunner?.mocker?.reset?.(); + resetEvaluatedModules(internals.workerState.evaluatedModules as EvaluatedModules, true); } } diff --git a/test/openclaw-npm-postpublish-verify.test.ts b/test/openclaw-npm-postpublish-verify.test.ts index 7f0d90d6fa45..dbfea78fad92 100644 --- a/test/openclaw-npm-postpublish-verify.test.ts +++ b/test/openclaw-npm-postpublish-verify.test.ts @@ -132,7 +132,7 @@ describe("npm registry provenance verification", () => { await expect( fetchRegistryJson("https://registry.example/openclaw", { - fetchImpl, + fetchImpl: fetchImpl as typeof fetch, timeoutMs: 1234, }), ).resolves.toEqual({ ok: true }); diff --git a/test/openclaw-npm-release-check.test.ts b/test/openclaw-npm-release-check.test.ts index 6499fc651364..86d799613b86 100644 --- a/test/openclaw-npm-release-check.test.ts +++ b/test/openclaw-npm-release-check.test.ts @@ -429,7 +429,7 @@ describe("resolveNpmCommandInvocation", () => { PATH: `${dir}${delimiter}${process.env.PATH ?? ""}`, }, windowsVerbatimArguments: invocation.windowsVerbatimArguments, - }); + } as { cwd: string; env: NodeJS.ProcessEnv }); expect(JSON.parse(readFileSync(outputPath, "utf8"))).toEqual([ "view", @@ -776,7 +776,6 @@ describe("collectReleaseTagErrors", () => { collectReleaseTagErrors({ packageVersion: "2026.3.10", releaseTag: "v2026.3.10-1", - now: new Date("2026-03-10T00:00:00Z"), }), ).toStrictEqual([]); }); @@ -786,7 +785,6 @@ describe("collectReleaseTagErrors", () => { collectReleaseTagErrors({ packageVersion: "2026.3.10-1", releaseTag: "v2026.3.10-1", - now: new Date("2026-03-10T00:00:00Z"), }), ).toStrictEqual([]); }); @@ -796,7 +794,6 @@ describe("collectReleaseTagErrors", () => { collectReleaseTagErrors({ packageVersion: "2026.3.10-beta.1", releaseTag: "v2026.3.10-1", - now: new Date("2026-03-10T00:00:00Z"), }), ).toStrictEqual([ "Release tag v2026.3.10-1 does not match package.json version 2026.3.10-beta.1; expected v2026.3.10-beta.1.", diff --git a/test/scripts/bench-gateway-child-test-support.ts b/test/scripts/bench-gateway-child-test-support.ts index 9c7778b53e3c..d53f2febcace 100644 --- a/test/scripts/bench-gateway-child-test-support.ts +++ b/test/scripts/bench-gateway-child-test-support.ts @@ -1,5 +1,4 @@ // Gateway benchmark child test support simulates child process behavior for script tests. -import type { spawnSync } from "node:child_process"; import { EventEmitter } from "node:events"; import { expect, it, vi } from "vitest"; import { resolveWindowsTaskkillPath } from "../../scripts/lib/windows-taskkill.mjs"; @@ -137,7 +136,7 @@ export function registerStopChildBehaviorTests(params: { params.stopChild(child as unknown as TChild, { killGraceMs: 1, platform: "win32", - runTaskkill, + runTaskkill: runTaskkill as unknown as typeof spawnSync, teardownGraceMs: 1, }), ).resolves.toEqual({ @@ -349,3 +348,4 @@ export function registerStopChildBehaviorTests(params: { }, ); } +import type { spawnSync } from "node:child_process"; diff --git a/test/scripts/bench-gateway-startup.test.ts b/test/scripts/bench-gateway-startup.test.ts index d451d14443ea..fd0939acdade 100644 --- a/test/scripts/bench-gateway-startup.test.ts +++ b/test/scripts/bench-gateway-startup.test.ts @@ -1,7 +1,7 @@ // Bench Gateway Startup tests cover bench gateway startup script behavior. import { spawnSync } from "node:child_process"; import fs from "node:fs"; -import { createServer } from "node:http"; +import { createServer, type RequestListener } from "node:http"; import os from "node:os"; import path from "node:path"; import { performance } from "node:perf_hooks"; @@ -9,7 +9,7 @@ import { beforeAll, describe, expect, it } from "vitest"; import { testing } from "../../scripts/bench-gateway-startup.ts"; import { registerStopChildBehaviorTests } from "./bench-gateway-child-test-support.js"; -async function listenOnLoopback(handler: Parameters[0]) { +async function listenOnLoopback(handler: RequestListener) { const server = createServer(handler); await new Promise((resolve, reject) => { server.once("error", reject); diff --git a/test/scripts/changed-lanes.test.ts b/test/scripts/changed-lanes.test.ts index fb662d3d703e..fb506a2fdd8b 100644 --- a/test/scripts/changed-lanes.test.ts +++ b/test/scripts/changed-lanes.test.ts @@ -46,7 +46,7 @@ const nestedGitEnvKeys = [ ] as const; function createNestedGitEnv(): NodeJS.ProcessEnv { - const env = { + const env: NodeJS.ProcessEnv = { ...process.env, GIT_CONFIG_NOSYSTEM: "1", GIT_TERMINAL_PROMPT: "0", @@ -611,6 +611,26 @@ describe("scripts/changed-lanes", () => { expect(plan.commands.map((command) => command.args[0])).toContain("tsgo:scripts"); }); + it.each([ + ["test/vitest/foo.config.ts", true, true], + ["test/vitest/vitest-runtime-helper.d.mts", true, true], + ["test/fixtures/foo.ts", false, true], + ["test/foo.mjs", false, true], + ["test/tsconfig/tsconfig.test.root.json", true, true], + ])( + "routes %s to testRoot=%s and tooling=%s", + (changedPath, expectedTestRoot, expectedTooling) => { + const result = detectChangedLanes([changedPath]); + const plan = createChangedCheckPlan(result); + + expect(result.lanes.testRoot).toBe(expectedTestRoot); + expect(result.lanes.tooling).toBe(expectedTooling); + expect(plan.commands.map((command) => command.args[0]).includes("tsgo:test:root")).toBe( + expectedTestRoot, + ); + }, + ); + it("falls back to full core lint for broad core diffs", () => { const targets = Array.from({ length: 9 }, (_, index) => `src/shared/file-${index}.ts`); const command = createTargetedCoreLintCommand(targets, { PATH: "/usr/bin" }); @@ -764,7 +784,7 @@ describe("scripts/changed-lanes", () => { { name: "conflict markers", args: ["check:no-conflict-markers"] }, { CI: "1", PATH: "/usr/bin" }, ); - const [shimDir] = String(command.env?.PATH ?? "").split(path.delimiter); + const [shimDir] = (command.env?.PATH ?? "").split(path.delimiter); expect(path.basename(shimDir)).toMatch(/^openclaw-corepack-pnpm-/u); expect(existsSync(path.join(shimDir, "pnpm"))).toBe(true); @@ -1560,6 +1580,7 @@ describe("scripts/changed-lanes", () => { const plan = createChangedCheckPlan(result); expectLanes(result.lanes, { + testRoot: true, tooling: true, }); expect(plan.commands.map((command) => command.args[0])).toContain("lint:scripts"); @@ -1630,6 +1651,7 @@ describe("scripts/changed-lanes", () => { }); expectLanes(result.lanes, { + testRoot: changedPath.endsWith(".ts"), tooling: true, }); expect(plan.commands.map((command) => command.args[0])).not.toContain("lint:apps"); @@ -1832,6 +1854,7 @@ describe("scripts/changed-lanes", () => { extensions: false, extensionTests: false, scripts: false, + testRoot: false, apps: false, docs: false, tooling: false, diff --git a/test/scripts/check-dependency-pins.test.ts b/test/scripts/check-dependency-pins.test.ts index 7922027fca9a..1cccfad5f58d 100644 --- a/test/scripts/check-dependency-pins.test.ts +++ b/test/scripts/check-dependency-pins.test.ts @@ -18,7 +18,7 @@ const nestedGitEnvKeys = [ ] as const; function createNestedGitEnv(): NodeJS.ProcessEnv { - const env = { + const env: NodeJS.ProcessEnv = { ...process.env, GIT_CONFIG_NOSYSTEM: "1", GIT_TERMINAL_PROMPT: "0", diff --git a/test/scripts/check-gateway-watch-regression.test.ts b/test/scripts/check-gateway-watch-regression.test.ts index 13f09b0c84cd..ab6123526c13 100644 --- a/test/scripts/check-gateway-watch-regression.test.ts +++ b/test/scripts/check-gateway-watch-regression.test.ts @@ -364,12 +364,14 @@ describe("check-gateway-watch-regression", () => { }), ); const waitForGatewayReady = vi.fn(async () => false); - const spawn = vi.fn(() => { - process.nextTick(() => { - child.emit("error", new Error("spawn failed")); - }); - return child; - }); + const spawn = vi.fn( + (_command: string, _args: string[], _options: { env: NodeJS.ProcessEnv }) => { + process.nextTick(() => { + child.emit("error", new Error("spawn failed")); + }); + return child; + }, + ); try { const result = await runTimedWatch( diff --git a/test/scripts/check-package-patches.test.ts b/test/scripts/check-package-patches.test.ts index 3f4c0a06da6d..152d42f254a7 100644 --- a/test/scripts/check-package-patches.test.ts +++ b/test/scripts/check-package-patches.test.ts @@ -18,7 +18,7 @@ const nestedGitEnvKeys = [ ] as const; function createNestedGitEnv(): NodeJS.ProcessEnv { - const env = { + const env: NodeJS.ProcessEnv = { ...process.env, GIT_CONFIG_NOSYSTEM: "1", GIT_TERMINAL_PROMPT: "0", diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index 9930adb5657e..e1f571d5e2ec 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -41,6 +41,13 @@ const MATURITY_GENERATED_PR_PATHS = [ "docs/maturity/taxonomy.md", ]; +type WorkflowStep = { + name?: string; + run?: string; + uses?: string; + with?: Record; +}; + function readCiWorkflow() { return parse(readFileSync(".github/workflows/ci.yml", "utf8")); } @@ -298,11 +305,11 @@ function readAndroidCompileSdk(relativePath: string): number { function findYamlFiles(directory: string): string[] { return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { - const path = `${directory}/${entry.name}`; + const entryPath = `${directory}/${entry.name}`; if (entry.isDirectory()) { - return findYamlFiles(path); + return findYamlFiles(entryPath); } - return entry.isFile() && /\.ya?ml$/u.test(entry.name) ? [path] : []; + return entry.isFile() && /\.ya?ml$/u.test(entry.name) ? [entryPath] : []; }); } @@ -543,7 +550,7 @@ describe("ci workflow guards", () => { ); const preflightSteps = workflow.jobs.preflight.steps; const validationStep = preflightSteps.find( - (step) => step.name === "Validate release-gate dispatch", + (step: WorkflowStep) => step.name === "Validate release-gate dispatch", ); expect(validationStep.if).toBe( "github.event_name == 'workflow_dispatch' && inputs.release_gate", @@ -698,9 +705,12 @@ describe("ci workflow guards", () => { ); expect(resolveStep.run).toContain('[[ ! "${sha}" =~ ^[0-9a-f]{40}$ ]]'); - const checkoutSteps = Object.values(ownerWorkflow.jobs).flatMap( - (job: { steps?: Array<{ uses?: string; with?: Record }> }) => - (job.steps ?? []).filter((step) => step.uses === CHECKOUT_V6), + const checkoutSteps = ( + Object.values(ownerWorkflow.jobs) as Array<{ + steps?: Array<{ uses?: string; with?: Record }>; + }> + ).flatMap((job: { steps?: Array<{ uses?: string; with?: Record }> }) => + (job.steps ?? []).filter((step: WorkflowStep) => step.uses === CHECKOUT_V6), ); expect(checkoutSteps.length).toBeGreaterThan(0); for (const checkoutStep of checkoutSteps) { @@ -1131,7 +1141,7 @@ describe("ci workflow guards", () => { for (const item of cases) { const workflow = parse(readFileSync(item.workflowPath, "utf8")); const uploadStep = workflow.jobs.scan.steps.find( - (step) => step.name === "Upload SARIF as workflow artifact", + (step: WorkflowStep) => step.name === "Upload SARIF as workflow artifact", ); expect(uploadStep.if, item.workflowPath).toBe("always()"); @@ -1148,7 +1158,7 @@ describe("ci workflow guards", () => { const workflow = readRealBehaviorProofWorkflow(); const source = readFileSync(".github/workflows/real-behavior-proof.yml", "utf8"); const checkout = workflow.jobs["real-behavior-proof"].steps.find( - (step) => step.uses === CHECKOUT_V6, + (step: WorkflowStep) => step.uses === CHECKOUT_V6, ); expect(checkout.with.ref).toBe("${{ github.workflow_sha }}"); @@ -1200,8 +1210,10 @@ describe("ci workflow guards", () => { expect(appCompileSdk).toBe(benchmarkCompileSdk); for (const job of sdkJobs) { - const cacheStep = job.steps.find((step) => step.name === "Cache Android SDK"); - const installStep = job.steps.find((step) => step.name === "Install Android SDK packages"); + const cacheStep = job.steps.find((step: WorkflowStep) => step.name === "Cache Android SDK"); + const installStep = job.steps.find( + (step: WorkflowStep) => step.name === "Install Android SDK packages", + ); expect(cacheStep.with.key).toContain(`platform-${appCompileSdk}.0-`); expect(installStep.run).toContain(`"${packageId}"`); @@ -1212,7 +1224,7 @@ describe("ci workflow guards", () => { const workflow = readCiWorkflow(); const source = readFileSync(".github/workflows/ci.yml", "utf8"); const runStep = workflow.jobs.android.steps.find( - (step) => step.name === "Run Android ${{ matrix.task }}", + (step: WorkflowStep) => step.name === "Run Android ${{ matrix.task }}", ); expect(source).toContain('{ check_name: "android-test-play", task: "test-play" }'); @@ -1300,7 +1312,9 @@ describe("ci workflow guards", () => { runner: "blacksmith-4vcpu-ubuntu-2404", }); - const runStep = additionalJob.steps.find((step) => step.name === "Run additional check shard"); + const runStep = additionalJob.steps.find( + (step: WorkflowStep) => step.name === "Run additional check shard", + ); expect(runStep.run).toContain("session-accessor-boundary)"); expect(runStep.run).toContain( 'run_check "lint:tmp:session-accessor-boundary" pnpm run lint:tmp:session-accessor-boundary', @@ -1317,7 +1331,9 @@ describe("ci workflow guards", () => { runner: "blacksmith-4vcpu-ubuntu-2404", }); - const runStep = additionalJob.steps.find((step) => step.name === "Run additional check shard"); + const runStep = additionalJob.steps.find( + (step: WorkflowStep) => step.name === "Run additional check shard", + ); expect(runStep.run).toContain("session-transcript-reader-boundary)"); expect(runStep.run).toContain( 'run_check "lint:tmp:session-transcript-reader-boundary" pnpm run lint:tmp:session-transcript-reader-boundary', @@ -1366,7 +1382,9 @@ describe("ci workflow guards", () => { const workflow = readCiWorkflow(); for (const jobName of ["preflight", "security-fast", "skills-python"]) { - const checkoutStep = workflow.jobs[jobName].steps.find((step) => step.name === "Checkout"); + const checkoutStep = workflow.jobs[jobName].steps.find( + (step: WorkflowStep) => step.name === "Checkout", + ); expect(checkoutStep.run, jobName).toContain( 'timeout --signal=TERM --kill-after=10s 120s git -C "$GITHUB_WORKSPACE"', @@ -1393,7 +1411,9 @@ describe("ci workflow guards", () => { const workflow = readWorkflowSanityWorkflow(); for (const jobName of ["no-tabs", "actionlint", "generated-doc-baselines"]) { - const checkoutStep = workflow.jobs[jobName].steps.find((step) => step.name === "Checkout"); + const checkoutStep = workflow.jobs[jobName].steps.find( + (step: WorkflowStep) => step.name === "Checkout", + ); expect(checkoutStep.run, jobName).toContain("fetch_checkout_ref()"); expect(checkoutStep.run, jobName).toContain("for attempt in 1 2 3"); @@ -1413,16 +1433,16 @@ describe("ci workflow guards", () => { it("runs plugin SDK API and surface drift checks in workflow sanity", () => { const workflow = readWorkflowSanityWorkflow(); const steps = workflow.jobs["generated-doc-baselines"].steps; - const stepNames = steps.map((step) => step.name); + const stepNames = steps.map((step: WorkflowStep) => step.name); expect(stepNames).toContain("Check plugin SDK API baseline drift"); expect(stepNames).toContain("Check plugin SDK surface budget"); expect(stepNames.indexOf("Check plugin SDK API baseline drift")).toBeLessThan( stepNames.indexOf("Check plugin SDK surface budget"), ); - expect(steps.find((step) => step.name === "Check plugin SDK surface budget").run).toBe( - "pnpm plugin-sdk:surface:check", - ); + expect( + steps.find((step: WorkflowStep) => step.name === "Check plugin SDK surface budget").run, + ).toBe("pnpm plugin-sdk:surface:check"); }); it("bounds platform checkout fetches without GNU timeout", () => { @@ -1434,7 +1454,9 @@ describe("ci workflow guards", () => { expect(source.match(/fetch_checkout_ref_once\(\)/gu) ?? []).toHaveLength(1); for (const jobName of ["checks-windows", "macos-node", "macos-swift", "ios-build"]) { - const checkoutStep = workflow.jobs[jobName].steps.find((step) => step.name === "Checkout"); + const checkoutStep = workflow.jobs[jobName].steps.find( + (step: WorkflowStep) => step.name === "Checkout", + ); expect(checkoutStep.run, jobName).toContain("fetch_checkout_ref()"); expect(checkoutStep.run, jobName).toContain("fetch_checkout_ref_once()"); @@ -1462,17 +1484,19 @@ describe("ci workflow guards", () => { it("resets SwiftPM state between macOS release build retries", () => { const workflow = readCiWorkflow(); const macosInstallStep = workflow.jobs["macos-swift"].steps.find( - (step) => step.name === "Install XcodeGen / SwiftLint / SwiftFormat", + (step: WorkflowStep) => step.name === "Install XcodeGen / SwiftLint / SwiftFormat", ); const iosInstallStep = workflow.jobs["ios-build"].steps.find( - (step) => step.name === "Install iOS Swift tooling", + (step: WorkflowStep) => step.name === "Install iOS Swift tooling", ); const macosLintStep = workflow.jobs["macos-swift"].steps.find( - (step) => step.name === "Swift lint", + (step: WorkflowStep) => step.name === "Swift lint", + ); + const iosLintStep = workflow.jobs["ios-build"].steps.find( + (step: WorkflowStep) => step.name === "Swift lint", ); - const iosLintStep = workflow.jobs["ios-build"].steps.find((step) => step.name === "Swift lint"); const buildStep = workflow.jobs["macos-swift"].steps.find( - (step) => step.name === "Swift build (release)", + (step: WorkflowStep) => step.name === "Swift build (release)", ); for (const installStep of [macosInstallStep, iosInstallStep]) { @@ -1578,10 +1602,10 @@ describe("ci workflow guards", () => { it("runs mobile protocol coverage for Node and native-only changes", () => { const workflow = readCiWorkflow(); const coverageStep = workflow.jobs.preflight.steps.find( - (step) => step.name === "Check mobile protocol event coverage", + (step: WorkflowStep) => step.name === "Check mobile protocol event coverage", ); const checkShardRun = workflow.jobs["check-shard"].steps.find( - (step) => step.name === "Run check shard", + (step: WorkflowStep) => step.name === "Run check shard", ).run; expect(coverageStep.run).toBe("node scripts/check-protocol-event-coverage.mjs"); @@ -1685,17 +1709,23 @@ describe("ci workflow guards", () => { it("does not rebuild Control UI after build:ci-artifacts", () => { const workflow = readCiWorkflow(); const buildArtifactSteps = workflow.jobs["build-artifacts"].steps; - const buildDistStep = buildArtifactSteps.find((step) => step.name === "Build dist"); + const buildDistStep = buildArtifactSteps.find( + (step: WorkflowStep) => step.name === "Build dist", + ); expect(buildDistStep.run).toBe("pnpm build:ci-artifacts"); - expect(buildArtifactSteps.map((step) => step.name)).not.toContain("Build Control UI"); - expect(buildArtifactSteps.some((step) => step.run === "pnpm ui:build")).toBe(false); + expect(buildArtifactSteps.map((step: WorkflowStep) => step.name)).not.toContain( + "Build Control UI", + ); + expect(buildArtifactSteps.some((step: WorkflowStep) => step.run === "pnpm ui:build")).toBe( + false, + ); }); it("keeps the hosted plugin-list memory allowance scoped to GitHub-hosted runners", () => { const workflow = readCiWorkflow(); const startupMemoryStep = workflow.jobs["build-artifacts"].steps.find( - (step) => step.name === "Check CLI startup memory", + (step: WorkflowStep) => step.name === "Check CLI startup memory", ); expect(startupMemoryStep.env.OPENCLAW_STARTUP_MEMORY_PLUGINS_LIST_MB).toBe( @@ -1706,10 +1736,16 @@ describe("ci workflow guards", () => { it("restores the dist build cache before building and saves only cache misses", () => { const workflow = readCiWorkflow(); const buildArtifactSteps = workflow.jobs["build-artifacts"].steps; - const stepNames = buildArtifactSteps.map((step) => step.name); - const restoreStep = buildArtifactSteps.find((step) => step.name === "Restore dist build cache"); - const buildDistStep = buildArtifactSteps.find((step) => step.name === "Build dist"); - const saveStep = buildArtifactSteps.find((step) => step.name === "Save dist build cache"); + const stepNames = buildArtifactSteps.map((step: WorkflowStep) => step.name); + const restoreStep = buildArtifactSteps.find( + (step: WorkflowStep) => step.name === "Restore dist build cache", + ); + const buildDistStep = buildArtifactSteps.find( + (step: WorkflowStep) => step.name === "Build dist", + ); + const saveStep = buildArtifactSteps.find( + (step: WorkflowStep) => step.name === "Save dist build cache", + ); expect(stepNames.indexOf("Restore dist build cache")).toBeLessThan( stepNames.indexOf("Build dist"), @@ -1731,20 +1767,27 @@ describe("ci workflow guards", () => { expect(saveStep.with.path).toContain("packages/*/dist/"); expect(restoreStep.with.key).toContain("dist-build-v3-"); expect( - buildArtifactSteps.find((step) => step.name === "Pack built runtime artifacts").run, + buildArtifactSteps.find((step: WorkflowStep) => step.name === "Pack built runtime artifacts") + .run, ).toContain("packages/*/dist"); expect(restoreStep.with.path).toContain("extensions/*/src/host/**/.bundle.hash"); expect(restoreStep.with.path).toContain("extensions/*/src/host/**/*.bundle.js"); - expect(buildArtifactSteps.map((step) => step.name)).not.toContain("Cache dist build"); + expect(buildArtifactSteps.map((step: WorkflowStep) => step.name)).not.toContain( + "Cache dist build", + ); }); it("keeps the AI runtime in Testbox build artifact caches", () => { const workflow = readBuildArtifactsTestboxWorkflow(); const steps = workflow.jobs["build-artifacts"].steps; - const resolveSeedsStep = steps.find((step) => step.name === "Resolve release dist cache seeds"); - const restoreStep = steps.find((step) => step.name === "Restore dist build cache"); - const verifyStep = steps.find((step) => step.name === "Verify build artifacts"); - const saveStep = steps.find((step) => step.name === "Save dist build cache"); + const resolveSeedsStep = steps.find( + (step: WorkflowStep) => step.name === "Resolve release dist cache seeds", + ); + const restoreStep = steps.find( + (step: WorkflowStep) => step.name === "Restore dist build cache", + ); + const verifyStep = steps.find((step: WorkflowStep) => step.name === "Verify build artifacts"); + const saveStep = steps.find((step: WorkflowStep) => step.name === "Save dist build cache"); expect(resolveSeedsStep.run).toContain('cache_prefix="${RUNNER_OS}-dist-build-v2-"'); expect(restoreStep.with.path).toContain("packages/*/dist/"); @@ -1758,7 +1801,7 @@ describe("ci workflow guards", () => { const workflow = readCiWorkflow(); const buildArtifactSteps = workflow.jobs["build-artifacts"].steps; const builtArtifactChecks = buildArtifactSteps.find( - (step) => step.name === "Run built artifact checks", + (step: WorkflowStep) => step.name === "Run built artifact checks", ); const run = builtArtifactChecks.run; @@ -1776,9 +1819,11 @@ describe("ci workflow guards", () => { it("keeps docs i18n CI on the patched preferred Go toolchain", () => { const workflow = readCiWorkflow(); const nodeTestJob = workflow.jobs["checks-node-core-test-nondist-shard"]; - const setupGoStep = nodeTestJob.steps.find((step) => step.name === "Setup Go for docs i18n"); + const setupGoStep = nodeTestJob.steps.find( + (step: WorkflowStep) => step.name === "Setup Go for docs i18n", + ); const verifyGoStep = nodeTestJob.steps.find( - (step) => step.name === "Verify docs i18n Go toolchain", + (step: WorkflowStep) => step.name === "Verify docs i18n Go toolchain", ); expect(setupGoStep).toMatchObject({ if: "matrix.requires_go == true", @@ -1802,9 +1847,13 @@ describe("ci workflow guards", () => { it("fails and retries quiet Node test shard stalls quickly", () => { const workflow = readCiWorkflow(); const preflightJob = workflow.jobs.preflight; - const manifestStep = preflightJob.steps.find((step) => step.name === "Build CI manifest"); + const manifestStep = preflightJob.steps.find( + (step: WorkflowStep) => step.name === "Build CI manifest", + ); const nodeTestJob = workflow.jobs["checks-node-core-test-nondist-shard"]; - const runStep = nodeTestJob.steps.find((step) => step.name === "Run Node test shard"); + const runStep = nodeTestJob.steps.find( + (step: WorkflowStep) => step.name === "Run Node test shard", + ); expect(JSON.stringify(preflightJob.steps)).toContain("timeout_minutes: shard.timeoutMinutes"); expect(manifestStep.run).toContain( @@ -1855,7 +1904,7 @@ describe("ci workflow guards", () => { expect(timingJob.if).toContain("!cancelled()"); const checkoutStep = timingJob.steps.find( - (step) => step.name === "Checkout timing summary helper", + (step: WorkflowStep) => step.name === "Checkout timing summary helper", ); expect(checkoutStep.uses).toBe(CHECKOUT_V6); expect(checkoutStep.with.ref).toBe( @@ -1863,14 +1912,18 @@ describe("ci workflow guards", () => { ); expect(checkoutStep.with["persist-credentials"]).toBe(false); - const writeStep = timingJob.steps.find((step) => step.name === "Write CI timing summary"); + const writeStep = timingJob.steps.find( + (step: WorkflowStep) => step.name === "Write CI timing summary", + ); expect(writeStep.env).toMatchObject({ GH_TOKEN: "${{ github.token }}" }); expect(writeStep.run).toContain( 'node scripts/ci-run-timings.mjs "$GITHUB_RUN_ID" --limit 25 > ci-timings-summary.txt', ); expect(writeStep.run).toContain('cat ci-timings-summary.txt >> "$GITHUB_STEP_SUMMARY"'); - const uploadStep = timingJob.steps.find((step) => step.name === "Upload CI timing summary"); + const uploadStep = timingJob.steps.find( + (step: WorkflowStep) => step.name === "Upload CI timing summary", + ); expect(uploadStep.uses).toBe(UPLOAD_ARTIFACT_V7); expect(uploadStep.with).toMatchObject({ name: "ci-timings-summary", @@ -1919,7 +1972,7 @@ describe("ci workflow guards", () => { maturityWorkflow.on.workflow_call.secrets.OPENCLAW_MATURITY_SCORECARD_AGENT_OPENAI_API_KEY .required, ).toBe(false); - expect(Object.keys(maturityWorkflow.on.workflow_call.secrets).sort()).toEqual([ + expect(Object.keys(maturityWorkflow.on.workflow_call.secrets).toSorted()).toEqual([ "CLAWSWEEPER_APP_PRIVATE_KEY", "MANTIS_GITHUB_APP_ID", "MANTIS_GITHUB_APP_PRIVATE_KEY", @@ -1939,14 +1992,14 @@ describe("ci workflow guards", () => { expect(qaEvidenceWorkflow.on.workflow_dispatch.inputs.qa_profile.default).toBe("all"); expect(qaEvidenceWorkflow.on.workflow_call.inputs.qa_profile.type).toBe("string"); const validateProfileStep = qaRunJob.steps.find( - (step) => step.name === "Validate QA profile input", + (step: WorkflowStep) => step.name === "Validate QA profile input", ); expect(validateProfileStep.run).toContain( "taxonomy.profiles.find((entry) => entry.id === requested)", ); expect(validateProfileStep.run).toContain("profile=${profile.id}"); const ensurePlaywrightStep = qaRunJob.steps.find( - (step) => step.name === "Ensure Playwright Chromium", + (step: WorkflowStep) => step.name === "Ensure Playwright Chromium", ); expect(ensurePlaywrightStep.run).toBe("node scripts/ensure-playwright-chromium.mjs"); expect(generateJob.needs).toEqual(["validate_selected_ref", "publisher_preflight"]); @@ -1963,13 +2016,13 @@ describe("ci workflow guards", () => { expect(generateJob.with).not.toHaveProperty("fail_on_qa_failure"); const workflowStep = maturityWorkflow.jobs.validate_selected_ref.steps.find( - (step) => step.name === "Resolve job workflow identity", + (step: WorkflowStep) => step.name === "Resolve job workflow identity", ); const authorizeStep = maturityWorkflow.jobs.validate_selected_ref.steps.find( - (step) => step.name === "Authorize workflow invocation", + (step: WorkflowStep) => step.name === "Authorize workflow invocation", ); const validateRefStep = maturityWorkflow.jobs.validate_selected_ref.steps.find( - (step) => step.name === "Validate selected ref", + (step: WorkflowStep) => step.name === "Validate selected ref", ); expect(workflowStep.env.JOB_CONTEXT).toBe("${{ toJSON(job) }}"); expect(workflowStep.run).toContain("job.workflow_sha must be a full lowercase commit SHA"); @@ -2031,10 +2084,10 @@ describe("ci workflow guards", () => { expect(publisherPreflight.needs).toBe("validate_selected_ref"); expect(publisherPreflight.if).toBe("${{ inputs.publish_pull_request }}"); const preflightCheckoutStep = publisherPreflight.steps.find( - (step) => step.name === "Checkout trusted workflow source", + (step: WorkflowStep) => step.name === "Checkout trusted workflow source", ); const preflightTokensStep = publisherPreflight.steps.find( - (step) => step.name === "Create generated PR tokens", + (step: WorkflowStep) => step.name === "Create generated PR tokens", ); expect(preflightCheckoutStep).toMatchObject({ uses: CHECKOUT_V6, @@ -2068,7 +2121,7 @@ describe("ci workflow guards", () => { ); const generatedDownloadStep = publishJob.steps.find( - (step) => step.name === "Download generated QA evidence artifact", + (step: WorkflowStep) => step.name === "Download generated QA evidence artifact", ); expect(generatedDownloadStep.if).toBe("${{ inputs.qa_evidence_run_id == '' }}"); expect(generatedDownloadStep.env.GENERATED_ARTIFACT_NAME).toBe( @@ -2079,12 +2132,12 @@ describe("ci workflow guards", () => { expect(generatedDownloadStep.run).not.toContain("--pattern"); const requireEvidenceStep = publishJob.steps.find( - (step) => step.name === "Require one QA evidence file", + (step: WorkflowStep) => step.name === "Require one QA evidence file", ); expect(requireEvidenceStep.run).toContain("Expected exactly one qa-evidence.json file"); const validateManifestStep = publishJob.steps.find( - (step) => step.name === "Validate QA evidence manifest", + (step: WorkflowStep) => step.name === "Validate QA evidence manifest", ); expect(validateManifestStep.run).toContain("qa-profile-evidence-manifest.json"); expect(validateManifestStep.run).toContain("qa-evidence.json profile must be all"); @@ -2093,28 +2146,32 @@ describe("ci workflow guards", () => { expect(qaRunJob.outputs.artifact_name).toBe("${{ steps.evidence.outputs.artifact_name }}"); const qaEvidenceStep = qaRunJob.steps.find( - (step) => step.name === "Validate QA profile evidence", + (step: WorkflowStep) => step.name === "Validate QA profile evidence", ); expect(qaEvidenceStep.env.ARTIFACT_NAME).toBe( "qa-profile-evidence-${{ steps.profile.outputs.profile }}-${{ needs.validate_selected_ref.outputs.selected_revision }}", ); expect(qaEvidenceStep.run).toContain("qa-profile-evidence-manifest.json"); - const qaUploadStep = qaRunJob.steps.find((step) => step.name === "Upload QA profile evidence"); + const qaUploadStep = qaRunJob.steps.find( + (step: WorkflowStep) => step.name === "Upload QA profile evidence", + ); expect(qaUploadStep.with).toMatchObject({ name: "qa-profile-evidence-${{ steps.profile.outputs.profile }}-${{ needs.validate_selected_ref.outputs.selected_revision }}", path: "${{ steps.run_profile.outputs.output_dir }}", "if-no-files-found": "error", }); - const qaFailStep = qaRunJob.steps.find((step) => step.name === "Fail if QA profile failed"); + const qaFailStep = qaRunJob.steps.find( + (step: WorkflowStep) => step.name === "Fail if QA profile failed", + ); expect(qaFailStep.if).toBe("always()"); const renderCheckoutStep = publishJob.steps.find( - (step) => step.name === "Checkout selected ref", + (step: WorkflowStep) => step.name === "Checkout selected ref", ); const generatedPrUploadStep = publishJob.steps.find( - (step) => step.name === "Upload generated PR files", + (step: WorkflowStep) => step.name === "Upload generated PR files", ); expect(renderCheckoutStep.with["fetch-depth"]).toBe(0); expect(generatedPrUploadStep).toMatchObject({ @@ -2139,16 +2196,16 @@ describe("ci workflow guards", () => { expect(publishPrJob.if).toContain(fragment); } const trustedPublishCheckoutStep = publishPrJob.steps.find( - (step) => step.name === "Checkout trusted workflow source", + (step: WorkflowStep) => step.name === "Checkout trusted workflow source", ); const selectedCheckoutStep = publishPrJob.steps.find( - (step) => step.name === "Checkout selected ref", + (step: WorkflowStep) => step.name === "Checkout selected ref", ); const downloadPrFilesStep = publishPrJob.steps.find( - (step) => step.name === "Download generated PR files", + (step: WorkflowStep) => step.name === "Download generated PR files", ); const openDocsPrStep = publishPrJob.steps.find( - (step) => step.name === "Open or update generated docs PR", + (step: WorkflowStep) => step.name === "Open or update generated docs PR", ); expect(trustedPublishCheckoutStep).toMatchObject({ uses: CHECKOUT_V6, @@ -2263,7 +2320,9 @@ describe("ci workflow guards", () => { () => { const valid = runMaturityArtifactCopyScenario(); expect(valid.status).toBe(0); - expect(valid.copied).toEqual(MATURITY_GENERATED_PR_PATHS.map((path) => `new ${path}\n`)); + expect(valid.copied).toEqual( + MATURITY_GENERATED_PR_PATHS.map((generatedPath) => `new ${generatedPath}\n`), + ); const extra = runMaturityArtifactCopyScenario({ extraFile: true }); expect(extra.status).not.toBe(0); @@ -2289,11 +2348,13 @@ describe("ci workflow guards", () => { const job = releaseWorkflow.jobs.maturity_scorecard_release_checks; const summaryJob = releaseWorkflow.jobs.summary; const verifyStep = summaryJob.steps.find( - (step) => step.name === "Verify release check results", + (step: WorkflowStep) => step.name === "Verify release check results", ); const inputs = releaseWorkflow.on.workflow_dispatch.inputs; const resolveJob = releaseWorkflow.jobs.resolve_target; - const summarizeStep = resolveJob.steps.find((step) => step.name === "Summarize validated ref"); + const summarizeStep = resolveJob.steps.find( + (step: WorkflowStep) => step.name === "Summarize validated ref", + ); expect(releaseWorkflow.jobs).not.toHaveProperty("qa_profile_release_evidence_release_checks"); expect(inputs.run_maturity_scorecard).toMatchObject({ @@ -2337,7 +2398,7 @@ describe("ci workflow guards", () => { it("keeps workflow guards in fast CI-routing checks", () => { const workflow = readCiWorkflow(); const preflightStep = workflow.jobs.preflight.steps.find( - (step) => step.name === "Build CI manifest", + (step: WorkflowStep) => step.name === "Build CI manifest", ); const taxonomy = parse(readFileSync("taxonomy.yaml", "utf8")) as { profiles: Array<{ id: string; categoryIds: string[] }>; @@ -2348,17 +2409,17 @@ describe("ci workflow guards", () => { } const fastCoreJob = workflow.jobs["checks-fast-core"]; const runStep = fastCoreJob.steps.find( - (step) => step.name === "Run ${{ matrix.task }} (${{ matrix.runtime }})", + (step: WorkflowStep) => step.name === "Run ${{ matrix.task }} (${{ matrix.runtime }})", ); const smokeProfileJob = workflow.jobs["qa-smoke-ci-profile"]; const smokeBuildStep = smokeProfileJob.steps.find( - (step) => step.name === "Build QA smoke runtime", + (step: WorkflowStep) => step.name === "Build QA smoke runtime", ); const smokeRunStep = smokeProfileJob.steps.find( - (step) => step.name === "Run smoke profile part", + (step: WorkflowStep) => step.name === "Run smoke profile part", ); const smokeUploadStep = smokeProfileJob.steps.find( - (step) => step.name === "Upload QA smoke profile evidence", + (step: WorkflowStep) => step.name === "Upload QA smoke profile evidence", ); const ciWorkflowText = readFileSync(".github/workflows/ci.yml", "utf8"); @@ -2384,10 +2445,9 @@ describe("ci workflow guards", () => { expect(workflow.jobs["qa-smoke-ci"]).toBeUndefined(); expect(smokeProfileJob.needs).toEqual(["preflight"]); expect(smokeProfileJob.strategy["max-parallel"]).toBe(2); - expect(smokeProfileJob.strategy.matrix.include.map((entry) => entry.slug)).toEqual([ - "profile-1-of-2", - "profile-2-of-2", - ]); + expect( + smokeProfileJob.strategy.matrix.include.map((entry: { slug: string }) => entry.slug), + ).toEqual(["profile-1-of-2", "profile-2-of-2"]); expect(smokeProfileJob["runs-on"]).toContain("blacksmith-16vcpu-ubuntu-2404"); expect(smokeRunStep.run).toContain("createQaSmokeCiPart"); expect(smokeRunStep.run).toContain("createQaSmokeCiMatrix"); diff --git a/test/scripts/clawhub-fixture-server.test.ts b/test/scripts/clawhub-fixture-server.test.ts index cd466dbd9e04..d04450d28ff8 100644 --- a/test/scripts/clawhub-fixture-server.test.ts +++ b/test/scripts/clawhub-fixture-server.test.ts @@ -1,7 +1,8 @@ // ClawHub Fixture Server tests cover the local package fixture HTTP contract. -import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process"; +import { spawn, spawnSync, type ChildProcessByStdio } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import path from "node:path"; +import type { Readable } from "node:stream"; import { setTimeout as delay } from "node:timers/promises"; import { afterEach, describe, expect, it } from "vitest"; import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js"; @@ -11,7 +12,8 @@ const PACKAGE_NAME = "@openclaw/kitchen-sink"; const PACKAGE_PATH = `/api/v1/packages/${encodeURIComponent(PACKAGE_NAME)}`; const KITCHEN_SINK_VERSION = "0.2.5"; const tempDirs: string[] = []; -const servers: ChildProcessWithoutNullStreams[] = []; +type FixtureServerChild = ChildProcessByStdio; +const servers: FixtureServerChild[] = []; afterEach(async () => { await Promise.all(servers.splice(0).map(stopServer)); @@ -27,7 +29,7 @@ function collectStream(stream: NodeJS.ReadableStream) { return () => text; } -async function stopServer(child: ChildProcessWithoutNullStreams) { +async function stopServer(child: FixtureServerChild) { if (child.exitCode !== null || child.signalCode !== null) { return; } diff --git a/test/scripts/crabbox-wrapper.test.ts b/test/scripts/crabbox-wrapper.test.ts index 1906d780af93..d39dd71d4be7 100644 --- a/test/scripts/crabbox-wrapper.test.ts +++ b/test/scripts/crabbox-wrapper.test.ts @@ -340,8 +340,8 @@ function makeFakeGit( ' rm -rf "$4"', " exit 0", "fi", - ...Object.entries(responses).flatMap(([key, response]) => { - const args = key.split("\u0000"); + ...Object.entries(responses).flatMap(([responseKey, response]) => { + const args = responseKey.split("\u0000"); return [ `if ${shellArgListCondition(args)}; then`, response.stdout ? ` printf "%s" ${shellSingleQuote(response.stdout)}` : "", @@ -864,7 +864,7 @@ describe("scripts/crabbox-wrapper", () => { ]); const output = parseFakeCrabboxOutput(result); - const remoteCommand = normalizeShellLineEndings(output.scriptContent); + const remoteCommand = normalizeShellLineEndings(output.scriptContent!); expect(result.status).toBe(0); expect(output.args.slice(0, 7)).toEqual([ "run", diff --git a/test/scripts/docker-e2e-helper-cli.test.ts b/test/scripts/docker-e2e-helper-cli.test.ts index 4a6acf4bb19a..48001b68a926 100644 --- a/test/scripts/docker-e2e-helper-cli.test.ts +++ b/test/scripts/docker-e2e-helper-cli.test.ts @@ -161,7 +161,7 @@ describe("Docker E2E helper CLIs", () => { it("rejects missing timings limits without a Node stack trace", () => { for (const limit of [undefined, "-h"]) { const args = ["scripts/docker-e2e-timings.mjs", "summary.json", "--limit"]; - const result = runHelper(...(limit === undefined ? args : [...args, limit])); + const result = runHelper(args[0]!, ...args.slice(1), ...(limit === undefined ? [] : [limit])); expect(result.status).toBe(1); expect(result.stdout).toBe(""); diff --git a/test/scripts/e2e-helper-env-limits.test.ts b/test/scripts/e2e-helper-env-limits.test.ts index 6717a085f02a..d417b1a1240a 100644 --- a/test/scripts/e2e-helper-env-limits.test.ts +++ b/test/scripts/e2e-helper-env-limits.test.ts @@ -66,12 +66,13 @@ async function listen(server: Server): Promise { async function allocatePort(): Promise { const server = createServer(); const url = await listen(server); - await new Promise((resolve) => server.close(() => resolve())); + await new Promise((resolve) => { + server.close(() => resolve()); + }); return Number(new URL(url).port); } async function waitForOutput( - child: ReturnType, matches: (text: string) => boolean, getOutput: () => string, ): Promise { @@ -80,7 +81,9 @@ async function waitForOutput( if (matches(getOutput())) { return; } - await new Promise((resolve) => setTimeout(resolve, 20)); + await new Promise((resolve) => { + setTimeout(resolve, 20); + }); } throw new Error(`timed out waiting for fixture output. Output: ${getOutput()}`); } @@ -142,7 +145,6 @@ describe("e2e helper numeric env limits", () => { }); try { await waitForOutput( - child, (text) => text.includes(`clickclack fixture listening on ${port}`), () => output.text(), ); diff --git a/test/scripts/gateway-network-client.test.ts b/test/scripts/gateway-network-client.test.ts index 40da068f9dce..918bbcfe9b9e 100644 --- a/test/scripts/gateway-network-client.test.ts +++ b/test/scripts/gateway-network-client.test.ts @@ -2,6 +2,7 @@ import { EventEmitter } from "node:events"; import { describe, expect, it, vi } from "vitest"; import { + type GatewayFrame, assertGatewaySuspendingError, assertReadySuspensionResponse, assertSuspendedProbes, @@ -133,7 +134,7 @@ describe("gateway network WebSocket open guard", () => { delay: async () => {}, onceFrame: async ( _ws: unknown, - predicate: (frame: unknown) => boolean, + predicate: (frame: GatewayFrame) => boolean, _timeoutMs?: number, ) => { const frame = { @@ -157,7 +158,7 @@ describe("gateway network WebSocket open guard", () => { const harness = createNetworkClientHarness([{ ok: true }, healthResponse()]); await runGatewayNetworkClient( - { token: "secret-token", url: "ws://127.0.0.1:12345", timeoutMs: 1000 }, + { token: "test-token", url: "ws://127.0.0.1:12345", timeoutMs: 1000 }, harness.deps, ); @@ -169,21 +170,23 @@ describe("gateway network WebSocket open guard", () => { it("bounds socket and frame waits by the client deadline", async () => { const harness = createNetworkClientHarness([{ ok: true }, healthResponse()]); const openSocket = vi.fn(harness.deps.openSocket); - const onceFrame = vi.fn(harness.deps.onceFrame); + const onceFrameMock = vi.fn(harness.deps.onceFrame); await runGatewayNetworkClient( - { token: "secret-token", url: "ws://127.0.0.1:12345", timeoutMs: 250 }, + { token: "test-token", url: "ws://127.0.0.1:12345", timeoutMs: 250 }, { ...harness.deps, - onceFrame, + onceFrame: onceFrameMock, openSocket, }, ); - expect(openSocket.mock.calls[0]?.[1]).toBeGreaterThan(0); - expect(openSocket.mock.calls[0]?.[1]).toBeLessThanOrEqual(250); - expect(onceFrame.mock.calls.map((call) => call[2])).toHaveLength(2); - for (const frameTimeoutMs of onceFrame.mock.calls.map((call) => call[2])) { + const openSocketCalls = openSocket.mock.calls as unknown as Array<[unknown, number]>; + const onceFrameCalls = onceFrameMock.mock.calls as unknown as Array<[unknown, unknown, number]>; + expect(openSocketCalls[0]?.[1]).toBeGreaterThan(0); + expect(openSocketCalls[0]?.[1]).toBeLessThanOrEqual(250); + expect(onceFrameCalls.map((call) => call[2])).toHaveLength(2); + for (const frameTimeoutMs of onceFrameCalls.map((call) => call[2])) { expect(frameTimeoutMs).toBeGreaterThan(0); expect(frameTimeoutMs).toBeLessThanOrEqual(250); } @@ -197,7 +200,7 @@ describe("gateway network WebSocket open guard", () => { try { await expect( runGatewayNetworkClient( - { token: "secret-token", url: "ws://127.0.0.1:12345", timeoutMs: 250 }, + { token: "test-token", url: "ws://127.0.0.1:12345", timeoutMs: 250 }, { delay: async (ms: number) => { delays.push(ms); @@ -224,7 +227,7 @@ describe("gateway network WebSocket open guard", () => { await expect( runGatewayNetworkClient( - { token: "secret-token", url: "ws://127.0.0.1:12345", timeoutMs: 1000 }, + { token: "test-token", url: "ws://127.0.0.1:12345", timeoutMs: 1000 }, harness.deps, ), ).rejects.toThrow("health failed: missing health summary payload"); @@ -242,7 +245,7 @@ describe("gateway network WebSocket open guard", () => { await expect( runGatewayNetworkClient( - { token: "secret-token", url: "ws://127.0.0.1:12345", timeoutMs: 1000 }, + { token: "test-token", url: "ws://127.0.0.1:12345", timeoutMs: 1000 }, harness.deps, ), ).rejects.toThrow("health failed: health unavailable"); diff --git a/test/scripts/gateway-ws-client.test.ts b/test/scripts/gateway-ws-client.test.ts index cbecbd360001..129595213aa2 100644 --- a/test/scripts/gateway-ws-client.test.ts +++ b/test/scripts/gateway-ws-client.test.ts @@ -1,5 +1,6 @@ // Gateway Ws Client tests cover gateway ws client script behavior. import { createServer, type Server } from "node:http"; +import type { Duplex } from "node:stream"; import { afterEach, describe, expect, it } from "vitest"; import { WebSocket, WebSocketServer } from "ws"; import { createGatewayWsClient } from "../../scripts/dev/gateway-ws-client.js"; @@ -47,7 +48,7 @@ async function listen(handler: (ws: WebSocket) => void): Promise { async function listenStalledUpgrade(): Promise<{ close: () => Promise; url: string }> { const stalledServer = createServer(); - const sockets = new Set(); + const sockets = new Set(); let closing = false; stalledServer.on("connection", (socket) => { sockets.add(socket); diff --git a/test/scripts/install-ps1.test.ts b/test/scripts/install-ps1.test.ts index cb210945d70b..df499d738715 100644 --- a/test/scripts/install-ps1.test.ts +++ b/test/scripts/install-ps1.test.ts @@ -4,7 +4,7 @@ import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "nod import { tmpdir } from "node:os"; import { join } from "node:path"; import { beforeAll, describe, expect, it } from "vitest"; -import { createScriptTestHarness } from "./test-helpers"; +import { createScriptTestHarness } from "./test-helpers.js"; const SCRIPT_PATH = "scripts/install.ps1"; const ENTRYPOINT_RE = diff --git a/test/scripts/ios-periphery-comment-workflow.test.ts b/test/scripts/ios-periphery-comment-workflow.test.ts index efc84e215467..ece95b229765 100644 --- a/test/scripts/ios-periphery-comment-workflow.test.ts +++ b/test/scripts/ios-periphery-comment-workflow.test.ts @@ -136,7 +136,7 @@ async function runScope(options: { "context", "core", "github", - ]) as (context: typeof context, core: typeof core, github: typeof github) => Promise; + ]) as (context: unknown, core: unknown, github: unknown) => Promise; await execute(context, core, github); return outputs.get("should-scan"); @@ -286,9 +286,9 @@ async function runCommenter( "github", ]) as ( require: NodeJS.Require, - context: typeof context, - core: typeof core, - github: typeof github, + context: unknown, + core: unknown, + github: unknown, ) => Promise; await execute(createRequire(import.meta.url), context, core, github); diff --git a/test/scripts/kitchen-sink-plugin-assertions.test.ts b/test/scripts/kitchen-sink-plugin-assertions.test.ts index 07121d85440a..402e05f8cae8 100644 --- a/test/scripts/kitchen-sink-plugin-assertions.test.ts +++ b/test/scripts/kitchen-sink-plugin-assertions.test.ts @@ -554,7 +554,7 @@ describe("kitchen-sink plugin assertions", () => { it("rejects kitchen-sink log scans without an isolated scratch root", () => { const parent = mkdtempSync(path.join(tmpdir(), "openclaw-kitchen-sink-scan-")); try { - const spawnEnv = { ...process.env, HOME: parent }; + const spawnEnv: NodeJS.ProcessEnv = { ...process.env, HOME: parent }; delete spawnEnv.KITCHEN_SINK_TMP_DIR; const result = spawnSync(process.execPath, [ASSERTIONS_SCRIPT, "scan-logs"], { encoding: "utf8", diff --git a/test/scripts/local-heavy-check-runtime.test.ts b/test/scripts/local-heavy-check-runtime.test.ts index 5639cca21dc2..4707e2fbf1f9 100644 --- a/test/scripts/local-heavy-check-runtime.test.ts +++ b/test/scripts/local-heavy-check-runtime.test.ts @@ -25,7 +25,7 @@ const ROOMY_HOST = { }; function makeEnv(overrides: Record = {}) { - const env = { + const env: NodeJS.ProcessEnv = { ...process.env, OPENCLAW_LOCAL_CHECK: "1", ...overrides, diff --git a/test/scripts/mantis-telegram-desktop-proof-workflow.test.ts b/test/scripts/mantis-telegram-desktop-proof-workflow.test.ts index 215bdc0d0cb7..dd78c9bd7f93 100644 --- a/test/scripts/mantis-telegram-desktop-proof-workflow.test.ts +++ b/test/scripts/mantis-telegram-desktop-proof-workflow.test.ts @@ -15,6 +15,7 @@ const TELEGRAM_PROOF_SKILL = ".agents/skills/telegram-crabbox-e2e-proof/SKILL.md const DOCS = ["docs/help/testing.md", "docs/concepts/qa-e2e-automation.md"]; type WorkflowStep = { + if?: string; env?: Record; name?: string; run?: string; diff --git a/test/scripts/openclaw-cross-os-release-checks.test.ts b/test/scripts/openclaw-cross-os-release-checks.test.ts index bf8a4b3536c5..3ae56c044e1e 100644 --- a/test/scripts/openclaw-cross-os-release-checks.test.ts +++ b/test/scripts/openclaw-cross-os-release-checks.test.ts @@ -223,7 +223,7 @@ describe("scripts/openclaw-cross-os-release-checks", () => { ["http://127.0.0.1:18789/assets/index.css", "http://127.0.0.1:18789/assets/index.js"], async (url) => new Response("", { - status: String(url).endsWith(".js") ? 404 : 200, + status: (url instanceof Request ? url.url : url.toString()).endsWith(".js") ? 404 : 200, }), ); @@ -1681,14 +1681,14 @@ describe("scripts/openclaw-cross-os-release-checks", () => { expect(await canConnectToLoopbackPort(1234.5)).toBe(false); const server = createNetServer(); - await new Promise((resolvePromise) => { + await new Promise((resolvePromise) => { server.listen(0, "127.0.0.1", resolvePromise); }); const address = server.address(); const port = typeof address === "object" && address ? address.port : 0; expect(await canConnectToLoopbackPort(port)).toBe(true); - await new Promise((resolvePromise) => { - server.close(resolvePromise); + await new Promise((resolvePromise) => { + server.close(() => resolvePromise()); }); for (let attempt = 0; attempt < 20; attempt += 1) { if (!(await canConnectToLoopbackPort(port, 100))) { diff --git a/test/scripts/openclaw-e2e-instance.test.ts b/test/scripts/openclaw-e2e-instance.test.ts index 5aa98d5ac46b..6badd7c9155a 100644 --- a/test/scripts/openclaw-e2e-instance.test.ts +++ b/test/scripts/openclaw-e2e-instance.test.ts @@ -65,7 +65,9 @@ function runSourcedHelper( } function expectShellSuccess(result: ReturnType) { - expect(result.status, result.stderr || result.stdout || result.error?.message).toBe(0); + expect(result.status, String(result.stderr || result.stdout || result.error?.message || "")).toBe( + 0, + ); } function writePackageFixture(packagePath: string): void { diff --git a/test/scripts/openclaw-npm-extended-stable-full-validation-workflow.test.ts b/test/scripts/openclaw-npm-extended-stable-full-validation-workflow.test.ts index 0866e52cd201..6a7396a47788 100644 --- a/test/scripts/openclaw-npm-extended-stable-full-validation-workflow.test.ts +++ b/test/scripts/openclaw-npm-extended-stable-full-validation-workflow.test.ts @@ -89,7 +89,7 @@ describe("extended-stable Full Release Validation workflow", () => { "refs/heads/extended-stable/2026.12.33", ]) { const result = runReleaseChecksTrustedRefGuard(valid); - expect(result.status, result.stderr).toBe(0); + expect(result.status, String(result.stderr)).toBe(0); } for (const invalid of [ diff --git a/test/scripts/openclaw-npm-extended-stable-release.test.ts b/test/scripts/openclaw-npm-extended-stable-release.test.ts index 2bac574dd421..7928cedc4863 100644 --- a/test/scripts/openclaw-npm-extended-stable-release.test.ts +++ b/test/scripts/openclaw-npm-extended-stable-release.test.ts @@ -443,7 +443,7 @@ describe("extended-stable selector capture", () => { describe("extended-stable registry readback", () => { it("accepts eventual convergence and sleeps 10 seconds between attempts", async () => { let attempt = 0; - const sleep = vi.fn(async () => {}); + const sleep = vi.fn(async (_delay: number) => {}); const result = await verifyExtendedStableRegistryReadback({ expectedVersion: "2026.6.33", query: async (target: string) => { @@ -465,7 +465,7 @@ describe("extended-stable registry readback", () => { it("exhausts exactly 12 dual-query attempts on mismatch or failure", async () => { const query = vi.fn(async () => ({ status: 1, stdout: "" })); - const sleep = vi.fn(async () => {}); + const sleep = vi.fn(async (_delay: number) => {}); await expect( verifyExtendedStableRegistryReadback({ expectedVersion: "2026.6.33", query, sleep }), ).rejects.toThrow(/after 12 attempts/u); diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index 55a43780c84c..22a91c5e771f 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -1,7 +1,6 @@ // Package Acceptance Workflow tests cover package acceptance workflow script behavior. import { execFileSync, spawnSync } from "node:child_process"; -import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { readdirSync, readFileSync, statSync } from "node:fs"; import { afterEach, describe, expect, it } from "vitest"; import { parse } from "yaml"; import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js"; @@ -59,6 +58,7 @@ type WorkflowStep = { if?: string; name?: string; run?: string; + shell?: string; uses?: string; with?: Record; }; diff --git a/test/scripts/parallels-npm-update-smoke.test.ts b/test/scripts/parallels-npm-update-smoke.test.ts index 3811b2b60cab..caf3909c6658 100644 --- a/test/scripts/parallels-npm-update-smoke.test.ts +++ b/test/scripts/parallels-npm-update-smoke.test.ts @@ -153,6 +153,7 @@ describe("parallels npm update smoke", () => { await withEnvAsync({ OPENAI_API_KEY: "test-key" }, async () => { const smoke = new FailingNpmUpdateSmoke({ ...TEST_AUTH, + dependencyTarballs: [], json: false, packageSpec: "openclaw@latest", platforms: new Set(["linux"]), @@ -202,6 +203,7 @@ exit 1 () => { const smoke = new NpmUpdateSmoke({ ...TEST_AUTH, + dependencyTarballs: [], json: false, packageSpec: "openclaw@latest", platforms: new Set(["linux"]), @@ -540,6 +542,7 @@ exit 1 () => new NpmUpdateSmoke({ ...TEST_AUTH, + dependencyTarballs: [], json: false, packageSpec: "openclaw@latest", platforms: new Set(["linux"]), @@ -580,6 +583,7 @@ exit 1 () => new NpmUpdateSmoke({ ...TEST_AUTH, + dependencyTarballs: [], json: false, packageSpec: "openclaw@latest", platforms: new Set(["linux"]), @@ -649,7 +653,7 @@ exit 1 const decoded = decodePowerShellFromArgs(args); decodedCommands.push(decoded); if (options?.input) { - inputs.push(String(options.input)); + inputs.push(options.input); } if (decoded.includes('cmd.exe /d /s /c start "" /b powershell.exe')) { return { status: 0, stderr: "", stdout: "started\n" }; @@ -812,6 +816,7 @@ exit 7 () => { const smoke = new NpmUpdateSmoke({ ...TEST_AUTH, + dependencyTarballs: [], json: false, packageSpec: "openclaw@latest", platforms: new Set(["macos"]), @@ -853,6 +858,7 @@ exit 7 () => { const smoke = new NpmUpdateSmoke({ ...TEST_AUTH, + dependencyTarballs: [], json: false, packageSpec: "openclaw@latest", platforms: new Set(["macos"]), diff --git a/test/scripts/plugin-contract-test-plan.test.ts b/test/scripts/plugin-contract-test-plan.test.ts index a5c821e7dd75..4b3aa783864e 100644 --- a/test/scripts/plugin-contract-test-plan.test.ts +++ b/test/scripts/plugin-contract-test-plan.test.ts @@ -15,8 +15,10 @@ describe("scripts/lib/plugin-contract-test-plan.mjs", () => { it("keeps manual CI compatible with legacy target refs", () => { const workflow = readFileSync(".github/workflows/ci.yml", "utf8"); + // ci.yml imports the plan through the importTargetPlan fallback helper since + // 7ae5996bb3c so historical target refs without the module keep working. expect(workflow).toContain( - 'await import(\n "./scripts/lib/plugin-contract-test-plan.mjs"', + 'await importTargetPlan(\n "./scripts/lib/plugin-contract-test-plan.mjs"', ); expect(workflow).toContain("checks-fast-contracts-plugins-legacy"); expect(workflow).not.toContain( diff --git a/test/scripts/plugin-gateway-gauntlet.test.ts b/test/scripts/plugin-gateway-gauntlet.test.ts index b9f7e2920c54..26e1a695afa7 100644 --- a/test/scripts/plugin-gateway-gauntlet.test.ts +++ b/test/scripts/plugin-gateway-gauntlet.test.ts @@ -604,7 +604,7 @@ describe("plugin gateway gauntlet helpers", () => { expect(row.status).toBe(1); expect(row.spawnError?.code).toBe("ENOENT"); - await expect(fs.readFile(row.logPath, "utf8")).resolves.toContain("[spawn error] ENOENT"); + await expect(fs.readFile(row.logPath!, "utf8")).resolves.toContain("[spawn error] ENOENT"); }); it("clamps oversized measured command timers before scheduling", async () => { @@ -624,7 +624,7 @@ describe("plugin gateway gauntlet helpers", () => { expect(row.status).toBe(0); expect(row.timedOut).toBe(false); - await expect(fs.readFile(row.logPath, "utf8")).resolves.not.toContain("ETIMEDOUT"); + await expect(fs.readFile(row.logPath!, "utf8")).resolves.not.toContain("ETIMEDOUT"); }); it.runIf(process.platform !== "win32")( @@ -762,8 +762,8 @@ setInterval(() => {}, 1000); }); expect(row.status).toBe(0); - await expect(fs.readFile(row.logPath, "utf8")).resolves.toContain("live stdout"); - await expect(fs.readFile(row.logPath, "utf8")).resolves.toContain("live stderr"); + await expect(fs.readFile(row.logPath!, "utf8")).resolves.toContain("live stdout"); + await expect(fs.readFile(row.logPath!, "utf8")).resolves.toContain("live stderr"); }); it("returns a failed row when measured command log writing fails", async () => { @@ -1014,7 +1014,7 @@ process.exit(7); }); expect(row.status).toBe(0); - const log = await fs.readFile(row.logPath, "utf8"); + const log = await fs.readFile(row.logPath!, "utf8"); expect(log).toContain("x".repeat(12)); expect(log).toContain("[stdout truncated after 12 bytes]"); }); @@ -1046,7 +1046,7 @@ process.exit(7); expect(relayed).toContain("x".repeat(12)); expect(relayed).not.toContain("x".repeat(32)); expect(relayed).toContain("[stdout relay truncated after 12 bytes]"); - await expect(fs.readFile(row.logPath, "utf8")).resolves.toContain("x".repeat(32)); + await expect(fs.readFile(row.logPath!, "utf8")).resolves.toContain("x".repeat(32)); }); it("force kills timed-out live measured process groups that ignore SIGTERM", async () => { diff --git a/test/scripts/plugin-lifecycle-measure.test.ts b/test/scripts/plugin-lifecycle-measure.test.ts index f29f58d62d74..9f6706c719ec 100644 --- a/test/scripts/plugin-lifecycle-measure.test.ts +++ b/test/scripts/plugin-lifecycle-measure.test.ts @@ -99,7 +99,7 @@ describe("plugin lifecycle resource sampler", () => { dir, 'printf "%s\\n" "$1" >>"$GETCONF_LOG"\ncase "$1" in PAGESIZE) echo 16384 ;; CLK_TCK) echo 250 ;; esac', ); - const env = { + const env: NodeJS.ProcessEnv = { ...process.env, GETCONF_LOG: logPath, PATH: `${binDir}:${process.env.PATH ?? ""}`, @@ -291,7 +291,7 @@ describe("plugin lifecycle resource sampler", () => { const dir = makeTempDir(); const summary = path.join(dir, "summary.tsv"); const pidFile = path.join(dir, "descendant.pid"); - let descendantPid; + let descendantPid: number | undefined; try { const result = spawnSync( @@ -330,7 +330,7 @@ describe("plugin lifecycle resource sampler", () => { ); expect(waitForPidExit(descendantPid, 1000)).toBe(true); } finally { - if (descendantPid > 0 && pidExists(descendantPid)) { + if (descendantPid !== undefined && descendantPid > 0 && pidExists(descendantPid)) { process.kill(descendantPid, "SIGKILL"); } } @@ -343,7 +343,7 @@ describe("plugin lifecycle resource sampler", () => { const dir = makeTempDir(); const summary = path.join(dir, "summary.tsv"); const pidFile = path.join(dir, "descendant.pid"); - let descendantPid; + let descendantPid: number | undefined; try { const result = spawn( @@ -376,7 +376,7 @@ describe("plugin lifecycle resource sampler", () => { expect(close.signal).toBe("SIGTERM"); expect(waitForPidExit(descendantPid, 1000)).toBe(true); } finally { - if (descendantPid > 0 && pidExists(descendantPid)) { + if (descendantPid !== undefined && descendantPid > 0 && pidExists(descendantPid)) { process.kill(descendantPid, "SIGKILL"); } } diff --git a/test/scripts/plugin-npm-extended-stable-workflow.test.ts b/test/scripts/plugin-npm-extended-stable-workflow.test.ts index b63ad816039e..73f6aaf7234e 100644 --- a/test/scripts/plugin-npm-extended-stable-workflow.test.ts +++ b/test/scripts/plugin-npm-extended-stable-workflow.test.ts @@ -15,6 +15,7 @@ type Step = { with?: Record; }; type Job = { + name?: string; environment?: string; if?: string; needs?: string[] | string; diff --git a/test/scripts/plugin-prerelease-test-plan.test.ts b/test/scripts/plugin-prerelease-test-plan.test.ts index 645314af567e..011d1758127b 100644 --- a/test/scripts/plugin-prerelease-test-plan.test.ts +++ b/test/scripts/plugin-prerelease-test-plan.test.ts @@ -14,6 +14,18 @@ import { const CHECKOUT_V6 = "actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10"; const UPLOAD_ARTIFACT_V7 = "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a"; +type WorkflowStep = { + env?: Record; + name?: string; + run?: string; + uses?: string; + with?: Record; +}; + +type WorkflowMatrixEntry = { + check_name?: string; +}; + function readCiWorkflow() { return parse(readFileSync(".github/workflows/ci.yml", "utf8")); } @@ -245,22 +257,26 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => { const dockerSuite = pluginWorkflow.jobs["plugin-prerelease-docker-suite"]; const suite = pluginWorkflow.jobs["plugin-prerelease-suite"]; const releaseWorkflow = readFullReleaseValidationWorkflow(); - const manifestScript = preflight.steps.find((step) => step.name === "Build CI manifest").run; - const manifestEnv = preflight.steps.find((step) => step.name === "Build CI manifest").env; + const manifestScript = preflight.steps.find( + (step: WorkflowStep) => step.name === "Build CI manifest", + ).run; + const manifestEnv = preflight.steps.find( + (step: WorkflowStep) => step.name === "Build CI manifest", + ).env; const pluginManifestScript = pluginPreflight.steps.find( - (step) => step.name === "Build plugin prerelease manifest", + (step: WorkflowStep) => step.name === "Build plugin prerelease manifest", ).run; const pluginManifestEnv = pluginPreflight.steps.find( - (step) => step.name === "Build plugin prerelease manifest", + (step: WorkflowStep) => step.name === "Build plugin prerelease manifest", ).env; const normalCiScript = releaseWorkflow.jobs.normal_ci.steps.find( - (step) => step.name === "Dispatch and monitor CI", + (step: WorkflowStep) => step.name === "Dispatch and monitor CI", ).run; const pluginPrereleaseScript = releaseWorkflow.jobs.plugin_prerelease.steps.find( - (step) => step.name === "Dispatch and monitor plugin prerelease", + (step: WorkflowStep) => step.name === "Dispatch and monitor plugin prerelease", ).run; const buildDistStep = workflow.jobs["build-artifacts"].steps.find( - (step) => step.name === "Build dist", + (step: WorkflowStep) => step.name === "Build dist", ); expect(workflow.jobs["plugin-prerelease-static-shard"]).toBeUndefined(); @@ -373,7 +389,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => { expect(manifestScript).not.toContain("plugin-prerelease-test-plan.mjs"); expect( workflow.jobs["check-shard"].strategy.matrix.include.find( - (entry) => entry.check_name === "check-dependencies", + (entry: WorkflowMatrixEntry) => entry.check_name === "check-dependencies", ), ).toEqual({ check_name: "check-dependencies", @@ -381,7 +397,9 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => { runner: "blacksmith-4vcpu-ubuntu-2404", }); expect( - workflow.jobs["check-shard"].steps.find((step) => step.name === "Run check shard").run, + workflow.jobs["check-shard"].steps.find( + (step: WorkflowStep) => step.name === "Run check shard", + ).run, ).toContain("pnpm deadcode:ci"); expect(normalCiScript).toContain('args+=(-f historical_target_tag="$TARGET_REF")'); expect(normalCiScript).toContain('dispatch_and_wait ci.yml "$dispatch_run_name" "${args[@]}"'); @@ -459,11 +477,13 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => { expect(inspector["continue-on-error"]).toBe(true); expect(inspector["runs-on"]).toBe("ubuntu-24.04"); expect(inspector["timeout-minutes"]).toBe(30); - expect(inspector.steps.find((step) => step.name === "Setup Node environment").with).toEqual({ + expect( + inspector.steps.find((step: WorkflowStep) => step.name === "Setup Node environment").with, + ).toEqual({ "install-bun": "false", }); const inspectorRun = inspector.steps.find( - (step) => step.name === "Run plugin inspector advisory sweep", + (step: WorkflowStep) => step.name === "Run plugin inspector advisory sweep", ); expect(inspectorRun.env).toEqual({ OPENCLAW_PLUGIN_INSPECTOR_ROOT: ".artifacts/plugin-inspector", @@ -475,7 +495,9 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => { ); expect(inspectorRun.run).toContain("This job is informational"); expect( - inspector.steps.find((step) => step.name === "Upload plugin inspector advisory artifacts"), + inspector.steps.find( + (step: WorkflowStep) => step.name === "Upload plugin inspector advisory artifacts", + ), ).toEqual({ if: "always()", name: "Upload plugin inspector advisory artifacts", @@ -487,7 +509,9 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => { }, }); expect( - staticShard.steps.find((step) => step.name === "Run plugin prerelease static shard").run, + staticShard.steps.find( + (step: WorkflowStep) => step.name === "Run plugin prerelease static shard", + ).run, ).toContain('bash -c "$PLUGIN_PRERELEASE_COMMAND"'); expect(dockerSuite).toEqual({ if: "${{ inputs.full_release_validation && needs.preflight.outputs.run_plugin_prerelease_docker == 'true' }}", @@ -524,7 +548,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => { "plugin-prerelease-docker-suite", ]); expect( - suite.steps.find((step) => step.name === "Verify plugin prerelease suite").run, + suite.steps.find((step: WorkflowStep) => step.name === "Verify plugin prerelease suite").run, ).toContain("plugin-prerelease-inspector advisory result"); }); @@ -573,7 +597,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => { ); expect(fullReleaseWorkflow.jobs.docker_runtime_assets_preflight["timeout-minutes"]).toBe(20); const dockerPreflightStep = fullReleaseWorkflow.jobs.docker_runtime_assets_preflight.steps.find( - (step) => step.name === "Verify Docker runtime-assets prune path", + (step: WorkflowStep) => step.name === "Verify Docker runtime-assets prune path", ); expect(dockerPreflightStep).toBeDefined(); expect(dockerPreflightStep?.run).toContain("docker build"); @@ -584,7 +608,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => { ); expect( fullReleaseWorkflow.jobs.docker_runtime_assets_preflight.steps.some( - (step) => step.name === "Build and smoke test final Docker runtime image", + (step: WorkflowStep) => step.name === "Build and smoke test final Docker runtime image", ), ).toBe(false); expect(fullReleaseWorkflow.jobs.plugin_prerelease["timeout-minutes"]).toBe( diff --git a/test/scripts/plugin-publication-artifact.test.ts b/test/scripts/plugin-publication-artifact.test.ts index 2fc23cf2a133..1dbb5651de1d 100644 --- a/test/scripts/plugin-publication-artifact.test.ts +++ b/test/scripts/plugin-publication-artifact.test.ts @@ -1,4 +1,5 @@ import { createHash } from "node:crypto"; +/* oxlint-disable typescript/no-base-to-string -- fetch mocks normalize standard RequestInfo inputs exactly as the production fetch boundary does. */ import { existsSync, mkdirSync, @@ -794,7 +795,7 @@ describe("plugin publication artifact", () => { callCounts.archive += 1; return callCounts.archive === 1 ? new Response("retry", { status: 502 }) - : new Response(zip, { + : new Response(zip as unknown as BodyInit, { status: 200, headers: { "content-length": String(zip.length) }, }); @@ -888,7 +889,7 @@ describe("plugin publication artifact", () => { return Response.json(workflowJobs); } if (url.endsWith(`/actions/artifacts/${ARTIFACT_ID}/zip`)) { - return new Response(zip, { + return new Response(zip as unknown as BodyInit, { status: 200, headers: { "content-length": String(zip.length) }, }); diff --git a/test/scripts/pr-operation-lock.test.ts b/test/scripts/pr-operation-lock.test.ts index d8f74d545ba0..e162b0880081 100644 --- a/test/scripts/pr-operation-lock.test.ts +++ b/test/scripts/pr-operation-lock.test.ts @@ -1,3 +1,4 @@ +/* oxlint-disable eslint/prefer-const, eslint/no-promise-executor-return -- process-lifecycle tests retain timer initialization and callback expressions matching the exercised script. */ import { execFileSync, spawn, @@ -54,14 +55,18 @@ function createProcessGroupTimingPreload() { function spawnDetached(command: string, args: readonly string[], options: SpawnOptions = {}) { const child = spawn(command, args, { ...options, detached: true }); detachedChildren.add(child); - if (child.pid) goneProcessGroups.delete(child.pid); + if (child.pid) { + goneProcessGroups.delete(child.pid); + } return child; } function createRepo(nestedName?: string) { const tempRoot = tempDirs.make("openclaw-pr-operation-lock-"); const dir = nestedName ? join(tempRoot, nestedName) : tempRoot; - if (nestedName) mkdirSync(dir); + if (nestedName) { + mkdirSync(dir); + } execFileSync("git", ["init", "-q", "-b", "main"], { cwd: dir }); execFileSync("git", ["config", "user.name", "OpenClaw Test"], { cwd: dir }); execFileSync("git", ["config", "user.email", "test@openclaw.invalid"], { cwd: dir }); @@ -196,7 +201,7 @@ function runLockShell(repoDir: string, commands: string[]) { detached: true, encoding: "utf8", timeout: 10_000, - }); + } as { cwd: string; encoding: "utf8"; timeout: number }); } function spawnHolder(repoDir: string, statusFile: string, pr = 42, trapTerm = true) { @@ -250,7 +255,7 @@ function spawnHolderWithChild(repoDir: string, statusFile: string, childPidFile: "acquire_pr_operation_lock 42", `printf 'held\n' >'${statusFile}'`, "sleep 30 &", - `printf '%s\n' \"$!\" >'${childPidFile}'`, + `printf '%s\n' "$!" >'${childPidFile}'`, 'wait "$!"', ].join("\n"), ], @@ -261,7 +266,9 @@ function spawnHolderWithChild(repoDir: string, statusFile: string, childPidFile: async function waitFor(predicate: () => boolean, timeoutMs = 5000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { - if (predicate()) return true; + if (predicate()) { + return true; + } await new Promise((resolve) => setTimeout(resolve, 25)); } return false; @@ -272,7 +279,9 @@ function validProcessId(value: unknown): value is number { } function readProcessIdFile(path: string) { - if (!existsSync(path)) return undefined; + if (!existsSync(path)) { + return undefined; + } const value = Number(readFileSync(path, "utf8").trim()); return validProcessId(value) ? value : undefined; } @@ -295,7 +304,9 @@ function childStatus(child: ChildProcess) { } async function waitForExit(child: ChildProcess, timeoutMs = 5000) { - if (child.exitCode !== null || child.signalCode !== null) return; + if (child.exitCode !== null || child.signalCode !== null) { + return; + } await new Promise((resolve, reject) => { let timeout: NodeJS.Timeout; const onExit = () => { @@ -316,13 +327,17 @@ async function waitForExit(child: ChildProcess, timeoutMs = 5000) { } async function stopChild(child: ChildProcess, signal: NodeJS.Signals) { - if (child.exitCode !== null || child.signalCode !== null) return; + if (child.exitCode !== null || child.signalCode !== null) { + return; + } signalTestChild(child, signal); await waitForExit(child); } async function stopChildLeader(child: ChildProcess, signal: NodeJS.Signals) { - if (child.exitCode !== null || child.signalCode !== null) return; + if (child.exitCode !== null || child.signalCode !== null) { + return; + } child.kill(signal); await waitForExit(child); } @@ -330,7 +345,9 @@ async function stopChildLeader(child: ChildProcess, signal: NodeJS.Signals) { async function cleanupChildren(...children: Array) { const failures: unknown[] = []; for (const child of children) { - if (!child) continue; + if (!child) { + continue; + } try { if (child.exitCode === null && child.signalCode === null) { signalTestChild(child, "SIGKILL"); @@ -352,19 +369,25 @@ function signalTestChild(child: ChildProcess, signal: NodeJS.Signals) { return; } catch (error) { const code = (error as NodeJS.ErrnoException).code; - if (code !== "ESRCH" && code !== "EPERM") throw error; + if (code !== "ESRCH" && code !== "EPERM") { + throw error; + } } } child.kill(signal); } async function cleanupProcessGroup(pgid: number) { - if (!processGroupExists(pgid)) return; + if (!processGroupExists(pgid)) { + return; + } try { killProcessGroup(pgid, "SIGKILL"); } catch (error) { const code = (error as NodeJS.ErrnoException).code; - if (code === "ESRCH" || code === "EPERM") return; + if (code === "ESRCH" || code === "EPERM") { + return; + } throw error; } if (!(await waitFor(() => !processGroupExists(pgid), 2000))) { @@ -373,7 +396,9 @@ async function cleanupProcessGroup(pgid: number) { } function readOperationProcessGroup(repoDir: string) { - if (!refExists(repoDir)) return undefined; + if (!refExists(repoDir)) { + return undefined; + } try { const payload = execFileSync("git", ["cat-file", "blob", refOid(repoDir)], { cwd: repoDir, @@ -393,12 +418,16 @@ async function cleanupController( ) { let pgid = operationPgidFile ? readProcessIdFile(operationPgidFile) : undefined; pgid ??= readOperationProcessGroup(repoDir); - if (pgid) await cleanupProcessGroup(pgid); + if (pgid) { + await cleanupProcessGroup(pgid); + } await cleanupChildren(controller); pgid = operationPgidFile ? readProcessIdFile(operationPgidFile) : undefined; pgid ??= readOperationProcessGroup(repoDir); - if (pgid) await cleanupProcessGroup(pgid); + if (pgid) { + await cleanupProcessGroup(pgid); + } } function refOid(repoDir: string, ref = lockRef) { @@ -417,7 +446,9 @@ function processGroupExists(pgid: number) { if (!validProcessId(pgid)) { throw new Error(`refusing to probe invalid process group ${String(pgid)}`); } - if (goneProcessGroups.has(pgid)) return false; + if (goneProcessGroups.has(pgid)) { + return false; + } try { process.kill(-pgid, 0); return true; @@ -437,7 +468,9 @@ function killProcessGroup(pgid: number, signal: NodeJS.Signals) { if (!validProcessId(pgid)) { throw new Error(`refusing to signal invalid process group ${String(pgid)}`); } - if (!goneProcessGroups.has(pgid)) process.kill(-pgid, signal); + if (!goneProcessGroups.has(pgid)) { + process.kill(-pgid, signal); + } } describe("scripts/pr process-group platform guard", () => { @@ -447,7 +480,9 @@ describe("scripts/pr process-group platform guard", () => { expect(source).toContain("use WSL on Windows"); expect(source).toContain("const SIGNAL_GRACE_MS = 5000;"); expect(source).toContain("const KILL_DRAIN_MS = 5000;"); - if (process.platform !== "win32") return; + if (process.platform !== "win32") { + return; + } const result = spawnSync(process.execPath, [processGroupRunner, repoRoot, "unused"], { encoding: "utf8", @@ -717,7 +752,7 @@ describePosix("scripts/pr per-PR operation lock", () => { writeFileSync(stub, "#!/bin/sh\nexit 0\n"); chmodSync(stub, 0o755); } - const env = { + const env: NodeJS.ProcessEnv = { ...process.env, OPENCLAW_PR_DEDICATED_PROCESS_GROUP: "1", PATH: `${binDir}:${process.env.PATH ?? ""}`, @@ -1113,7 +1148,9 @@ describePosix("scripts/pr per-PR operation lock", () => { expect(refExists(repoDir)).toBe(false); } finally { daemonPgid ??= readProcessIdFile(daemonPidFile); - if (daemonPgid) await cleanupProcessGroup(daemonPgid); + if (daemonPgid) { + await cleanupProcessGroup(daemonPgid); + } } }); @@ -1173,7 +1210,9 @@ describePosix("scripts/pr per-PR operation lock", () => { expect(refExists(repoDir)).toBe(false); } finally { nestedPgid ??= readProcessIdFile(nestedPidFile); - if (nestedPgid) await cleanupProcessGroup(nestedPgid); + if (nestedPgid) { + await cleanupProcessGroup(nestedPgid); + } } }); @@ -1222,7 +1261,9 @@ describePosix("scripts/pr per-PR operation lock", () => { expect(refExists(repoDir)).toBe(false); } finally { nestedPgid ??= readProcessIdFile(nestedPidFile); - if (nestedPgid) await cleanupProcessGroup(nestedPgid); + if (nestedPgid) { + await cleanupProcessGroup(nestedPgid); + } } }, 15_000); @@ -1310,7 +1351,9 @@ describePosix("scripts/pr per-PR operation lock", () => { expect(refExists(repoDir)).toBe(false); } finally { holderPgid ??= readProcessIdFile(holderPidFile); - if (holderPgid) await cleanupProcessGroup(holderPgid); + if (holderPgid) { + await cleanupProcessGroup(holderPgid); + } await cleanupChildren(waiter); await cleanupController(repoDir, controller, operationPgidFile); } @@ -1348,7 +1391,9 @@ describePosix("scripts/pr per-PR operation lock", () => { expect(refExists(repoDir)).toBe(false); } finally { operationPgid ??= readProcessIdFile(operationPgidFile); - if (operationPgid) await cleanupProcessGroup(operationPgid); + if (operationPgid) { + await cleanupProcessGroup(operationPgid); + } } }); @@ -1381,7 +1426,9 @@ describePosix("scripts/pr per-PR operation lock", () => { expect(refExists(repoDir)).toBe(false); } finally { operationPgid ??= readProcessIdFile(operationPgidFile); - if (operationPgid) await cleanupProcessGroup(operationPgid); + if (operationPgid) { + await cleanupProcessGroup(operationPgid); + } } }); @@ -1744,7 +1791,9 @@ describePosix("scripts/pr per-PR operation lock", () => { expect(recovered.status, `${recovered.stdout}\n${recovered.stderr}`).toBe(0); expect(refExists(repoDir)).toBe(false); } finally { - if (nestedPgid) await cleanupProcessGroup(nestedPgid); + if (nestedPgid) { + await cleanupProcessGroup(nestedPgid); + } await cleanupController(repoDir, controller); } }); diff --git a/test/scripts/profile-extension-memory.test.ts b/test/scripts/profile-extension-memory.test.ts index 4aef096716ad..9ae8eb6afecf 100644 --- a/test/scripts/profile-extension-memory.test.ts +++ b/test/scripts/profile-extension-memory.test.ts @@ -259,7 +259,7 @@ describe("scripts/profile-extension-memory", () => { name: "spawn-error", body: "", timeoutMs: 30_000, - spawnImpl: () => { + spawnImpl: (() => { const child = new EventEmitter() as EventEmitter & { kill: () => boolean; stderr: EventEmitter; @@ -270,7 +270,7 @@ describe("scripts/profile-extension-memory", () => { child.kill = () => true; queueMicrotask(() => child.emit("error", new Error("spawn denied"))); return child; - }, + }) as unknown as typeof spawn, }); expect(Date.now() - startedAt).toBeLessThan(1000); diff --git a/test/scripts/release-beta-verifier.test.ts b/test/scripts/release-beta-verifier.test.ts index 121134b9e568..d2b3ec68f989 100644 --- a/test/scripts/release-beta-verifier.test.ts +++ b/test/scripts/release-beta-verifier.test.ts @@ -1,4 +1,5 @@ // Release Beta Verifier tests cover release beta verifier script behavior. +/* oxlint-disable typescript/no-base-to-string -- fetch mock normalizes standard RequestInfo inputs for URL assertions. */ import { createHash } from "node:crypto"; import { afterEach, describe, expect, it, vi } from "vitest"; import { @@ -454,7 +455,7 @@ describe("downloadClawHubBootstrapReadback", () => { return Response.json(attemptMetadata); } if (url.endsWith("/actions/artifacts/45/zip")) { - return new Response(archive, { + return new Response(archive as unknown as BodyInit, { headers: { "content-length": String(archive.length) }, }); } diff --git a/test/scripts/release-ci-summary.test.ts b/test/scripts/release-ci-summary.test.ts index 582e26d50d51..92b93753afac 100644 --- a/test/scripts/release-ci-summary.test.ts +++ b/test/scripts/release-ci-summary.test.ts @@ -137,7 +137,7 @@ function rawManifest({ workflowRefType, workflowSha, }: { - evidenceReuse?: Record; + evidenceReuse?: unknown; rerunGroup?: string; runId?: string; targetSha?: string; @@ -145,7 +145,25 @@ function rawManifest({ workflowFullRef?: string; workflowRefType?: "branch" | "tag"; workflowSha?: string; -}) { +}): { + childRuns: Record; + controls: Record; + evidenceReuse?: unknown; + releaseProfile: string; + rerunGroup: string; + runAttempt: string; + runId: string; + runReleaseSoak: string; + targetRef?: string; + targetSha: string; + validationInputs: Record; + version: 2 | 3; + workflowFullRef?: string; + workflowName: string; + workflowRef: string; + workflowRefType?: "branch" | "tag"; + workflowSha?: string; +} { return { childRuns: { normalCi: "101", @@ -312,10 +330,10 @@ function trustedMainPackageFixture({ return [parentJob]; }, getRun(requestedRunId: string) { - if (String(requestedRunId) === runId) { + if (requestedRunId === runId) { return parentRun; } - if (String(requestedRunId) === childRunId) { + if (requestedRunId === childRunId) { return childRun; } throw new Error(`unexpected run: ${requestedRunId}`); @@ -1152,7 +1170,7 @@ describe("release CI summary child correlation", () => { id: 999, }; const pages = Array.from({ length: 10 }, (_, pageIndex) => - Array.from({ length: 100 }, (_, runIndex) => ({ + Array.from({ length: 100 }, (_unused, runIndex) => ({ display_title: `decoy-${pageIndex}-${runIndex}`, event: "workflow_dispatch", head_branch: "main", diff --git a/test/scripts/release-no-push-workflow.test.ts b/test/scripts/release-no-push-workflow.test.ts index cec0dc2dbf49..215995380751 100644 --- a/test/scripts/release-no-push-workflow.test.ts +++ b/test/scripts/release-no-push-workflow.test.ts @@ -20,7 +20,6 @@ const PACKAGE_ACCEPTANCE = ".github/workflows/package-acceptance.yml"; const PLUGIN_PRERELEASE = ".github/workflows/plugin-prerelease.yml"; const LIVE_E2E = ".github/workflows/openclaw-live-and-e2e-checks-reusable.yml"; const INSTALL_SMOKE = ".github/workflows/install-smoke.yml"; -const INSTALL_SMOKE_REUSABLE = ".github/workflows/install-smoke-reusable.yml"; const SHARED_IMAGE_PUBLISHER = ".github/workflows/openclaw-shared-image-publish-reusable.yml"; const SCHEDULED_LIVE = ".github/workflows/openclaw-scheduled-live-checks.yml"; const DOCKER_RELEASE = ".github/workflows/docker-release.yml"; @@ -99,7 +98,7 @@ function permissionScopes(...permissions: Array): str } } } - return [...scopes].sort(); + return [...scopes].toSorted(); } function reusablePermissionViolations( diff --git a/test/scripts/release-workflow-matrix-plan.test.ts b/test/scripts/release-workflow-matrix-plan.test.ts index c179360edae9..ea711f0f751c 100644 --- a/test/scripts/release-workflow-matrix-plan.test.ts +++ b/test/scripts/release-workflow-matrix-plan.test.ts @@ -4,8 +4,10 @@ import { describe, expect, it } from "vitest"; import { parse } from "yaml"; import { createReleaseWorkflowMatrixPlan } from "../../scripts/plan-release-workflow-matrix.mjs"; -function workflow() { - return parse(readFileSync(".github/workflows/openclaw-live-and-e2e-checks-reusable.yml", "utf8")); +function workflow(): WorkflowDocument { + return parse( + readFileSync(".github/workflows/openclaw-live-and-e2e-checks-reusable.yml", "utf8"), + ) as WorkflowDocument; } const PROFILE_GATED_STATIC_MATRIX_ALLOWLIST = [ @@ -14,6 +16,40 @@ const PROFILE_GATED_STATIC_MATRIX_ALLOWLIST = [ "validate_live_media_provider_suites", ]; +type WorkflowStep = { + env?: Record; + if?: string; + name?: string; + run?: string; +}; + +type MatrixEntry = { + advisory?: boolean; + chunk_id?: string; + id?: string; + profiles?: string; + providers?: string; + suite_group?: string; + suite_id?: string; +}; + +type WorkflowJob = { + env: Record; + needs: string[]; + outputs: Record; + steps: WorkflowStep[]; + strategy: { matrix: { include: MatrixEntry[] } }; +}; + +type WorkflowDocument = { + env: Record; + jobs: Record; + on: { + workflow_call: { inputs: Record }; + workflow_dispatch: { inputs: Record }; + }; +}; + // Direct dispatches build from the selected ref. Only trusted workflow callers // may provide the complete immutable package artifact tuple. const WORKFLOW_CALL_ONLY_INPUTS = new Set([ @@ -96,7 +132,7 @@ function staticProfileMatrixJobs() { return Object.entries(workflow().jobs) .filter(([, job]) => { const entries = job.strategy?.matrix?.include; - return Array.isArray(entries) && entries.some((entry) => "profiles" in entry); + return Array.isArray(entries) && entries.some((entry: MatrixEntry) => "profiles" in entry); }) .map(([jobName]) => jobName) .toSorted((left, right) => left.localeCompare(right)); @@ -144,12 +180,12 @@ describe("scripts/plan-release-workflow-matrix.mjs", () => { "${{ inputs.allow_unreleased_changelog }}", ); const packageStep = definition.jobs.prepare_docker_e2e_image.steps.find( - (step) => step.name === "Pack OpenClaw package for Docker E2E", + (step: WorkflowStep) => step.name === "Pack OpenClaw package for Docker E2E", ); - expect(packageStep.env.ALLOW_UNRELEASED_CHANGELOG).toBe( + expect(packageStep!.env!.ALLOW_UNRELEASED_CHANGELOG).toBe( "${{ inputs.allow_unreleased_changelog }}", ); - expect(packageStep.run).toContain("package_args+=(--allow-unreleased-changelog)"); + expect(packageStep!.run).toContain("package_args+=(--allow-unreleased-changelog)"); }); it.each(PROFILE_EXPECTATIONS)( @@ -161,8 +197,10 @@ describe("scripts/plan-release-workflow-matrix.mjs", () => { releaseProfile: profile, }); - expect(plan.dockerE2e.matrix.include.map((entry) => entry.chunk_id)).toEqual(dockerE2eChunks); - expect(plan.liveModels.matrix.include.map((entry) => entry.providers)).toEqual( + expect(plan.dockerE2e.matrix.include.map((entry: MatrixEntry) => entry.chunk_id)).toEqual( + dockerE2eChunks, + ); + expect(plan.liveModels.matrix.include.map((entry: MatrixEntry) => entry.providers)).toEqual( liveModelProviders, ); }, @@ -175,8 +213,8 @@ describe("scripts/plan-release-workflow-matrix.mjs", () => { releaseProfile: "beta", }); - expect(plan.dockerE2e.omitted.map((entry) => entry.id)).toContain("core"); - expect(plan.liveModels.omitted.map((entry) => entry.id)).toContain("anthropic"); + expect(plan.dockerE2e.omitted.map((entry: MatrixEntry) => entry.id)).toContain("core"); + expect(plan.liveModels.omitted.map((entry: MatrixEntry) => entry.id)).toContain("anthropic"); }); it("keeps stable release jobs broad enough for stable-required lanes", () => { @@ -187,13 +225,13 @@ describe("scripts/plan-release-workflow-matrix.mjs", () => { }); expect(plan.dockerE2e.count).toBe(14); - expect(plan.liveModels.matrix.include.map((entry) => entry.providers)).toEqual([ + expect(plan.liveModels.matrix.include.map((entry: MatrixEntry) => entry.providers)).toEqual([ "anthropic", "google", "minimax", "openai", ]); - expect(plan.liveModels.omitted.map((entry) => entry.id)).toEqual([ + expect(plan.liveModels.omitted.map((entry: MatrixEntry) => entry.id)).toEqual([ "moonshot", "opencode-go", "openrouter", @@ -223,8 +261,8 @@ describe("scripts/plan-release-workflow-matrix.mjs", () => { const jobs = workflow().jobs; const dockerLiveJob = jobs.validate_live_docker_provider_suites; const anthropicEntries = dockerLiveJob.strategy.matrix.include - .filter((entry) => entry.suite_group === "live-gateway-anthropic-docker") - .map((entry) => ({ + .filter((entry: MatrixEntry) => entry.suite_group === "live-gateway-anthropic-docker") + .map((entry: MatrixEntry) => ({ advisory: entry.advisory, profiles: entry.profiles, suiteId: entry.suite_id, @@ -246,7 +284,7 @@ describe("scripts/plan-release-workflow-matrix.mjs", () => { expect.objectContaining({ suite_id: "live-gateway-anthropic-docker-full" }), ); - const conditionalSteps = dockerLiveJob.steps.filter((step) => step.if); + const conditionalSteps = dockerLiveJob.steps.filter((step: WorkflowStep) => step.if); expect(conditionalSteps.length).toBeGreaterThan(0); for (const step of conditionalSteps) { expect(step.if).toContain("inputs.live_suite_filter == matrix.suite_group"); diff --git a/test/scripts/render-maturity-docs.test.ts b/test/scripts/render-maturity-docs.test.ts index 779290ab3008..174e58ac9075 100644 --- a/test/scripts/render-maturity-docs.test.ts +++ b/test/scripts/render-maturity-docs.test.ts @@ -123,17 +123,18 @@ function allProfileScorecardFixture() { (surface.categories ?? []).map((category) => { const coverageIds = [ ...new Set((category.features ?? []).flatMap((feature) => feature.coverageIds ?? [])), - ].sort(); + ].toSorted(); + const features = category.features ?? []; return { id: `${surface.id}.${category.id}`, surfaceId: surface.id, name: category.name, status: "missing", features: { - total: category.features.length, + total: features.length, fulfilled: 0, partial: 0, - missing: category.features.length, + missing: features.length, fulfillmentPercent: 0, }, coverageIds: { diff --git a/test/scripts/report-test-temp-creations.test.ts b/test/scripts/report-test-temp-creations.test.ts index f2f7bd6419ce..8753334e2aea 100644 --- a/test/scripts/report-test-temp-creations.test.ts +++ b/test/scripts/report-test-temp-creations.test.ts @@ -20,7 +20,7 @@ const nestedGitEnvKeys = [ ] as const; function createNestedGitEnv(): NodeJS.ProcessEnv { - const env = { + const env: NodeJS.ProcessEnv = { ...process.env, GIT_CONFIG_NOSYSTEM: "1", GIT_TERMINAL_PROMPT: "0", diff --git a/test/scripts/security-sensitive-guard-script.test.ts b/test/scripts/security-sensitive-guard-script.test.ts index 0aed491ae78d..95984d4eef94 100644 --- a/test/scripts/security-sensitive-guard-script.test.ts +++ b/test/scripts/security-sensitive-guard-script.test.ts @@ -205,7 +205,7 @@ describe("security-sensitive guard script", () => { }); it("renders deterministic awareness, blocked, trusted, authorized, and cleared comments", () => { - const changes = [securitySensitiveFileDefinition(".gitignore")]; + const changes = [securitySensitiveFileDefinition(".gitignore")!]; const awarenessBody = renderSecuritySensitiveAwarenessComment(changes); const blockedBody = renderBlockedSecuritySensitiveComment({ changes, headSha }); const trustedBody = renderTrustedSecuritySensitiveComment({ diff --git a/test/scripts/stage-bundled-plugin-runtime.test.ts b/test/scripts/stage-bundled-plugin-runtime.test.ts index e1797f94bb2c..05e7dcb2108c 100644 --- a/test/scripts/stage-bundled-plugin-runtime.test.ts +++ b/test/scripts/stage-bundled-plugin-runtime.test.ts @@ -28,7 +28,7 @@ describe("stageBundledPluginRuntime", () => { vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const symlinkSpy = vi .spyOn(fs, "symlinkSync") - .mockImplementation((target, targetPath, type) => { + .mockImplementation((_target, targetPath, type) => { if ( String(targetPath).includes(`${path.sep}dist-runtime${path.sep}`) && type !== "junction" diff --git a/test/scripts/telegram-user-crabbox-proof.test.ts b/test/scripts/telegram-user-crabbox-proof.test.ts index efc6b547cc50..a9992514e303 100644 --- a/test/scripts/telegram-user-crabbox-proof.test.ts +++ b/test/scripts/telegram-user-crabbox-proof.test.ts @@ -290,7 +290,7 @@ describe("telegram user Crabbox proof log polling", () => { const stagedDir = stageFullSessionArtifacts(outputDir); expect(stagedDir).toBe(publishDir); - expect(fs.readdirSync(stagedDir).sort()).toEqual([ + expect(fs.readdirSync(stagedDir).toSorted()).toEqual([ "probe-2026-06-20T16-47-48-123Z.json", "probe.json", "status.json", @@ -684,6 +684,8 @@ process.exit(2); }), drainUpdates: async () => ({ drained: 0, + pendingAfter: undefined, + pendingBefore: undefined, webhookUrlSet: false, }), }, @@ -747,6 +749,8 @@ process.exit(2); }), drainUpdates: async () => ({ drained: 0, + pendingAfter: undefined, + pendingBefore: undefined, webhookUrlSet: false, }), waitForOutputReady: async (child, _pattern, output, label) => { diff --git a/test/scripts/test-extension.test.ts b/test/scripts/test-extension.test.ts index 07fb70d4b358..fff5c01f8708 100644 --- a/test/scripts/test-extension.test.ts +++ b/test/scripts/test-extension.test.ts @@ -1,4 +1,5 @@ // Test Extension tests cover test extension script behavior. +/* oxlint-disable typescript/no-unnecessary-type-parameters -- explicit call-site result types keep mock tuple extraction precise. */ import { spawn, spawnSync } from "node:child_process"; import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; @@ -45,7 +46,7 @@ function runScriptResult(args: string[], cwd = process.cwd()) { }); } -function requireFirstMockArg(mock: { mock: { calls: Array<[T, ...unknown[]]> } }): T { +function requireFirstMockArg(mock: { mock: { calls: readonly (readonly unknown[])[] } }): T { const [call] = mock.mock.calls; if (!call) { throw new Error("expected first mock call argument"); @@ -54,7 +55,7 @@ function requireFirstMockArg(mock: { mock: { calls: Array<[T, ...unknown[]]> if (arg === undefined) { throw new Error("expected first mock call argument"); } - return arg; + return arg as T; } function findExtensionWithoutTests() { @@ -522,7 +523,7 @@ describe("scripts/test-extension.mjs", () => { expect(shards).toHaveLength(DEFAULT_EXTENSION_TEST_SHARD_COUNT); expect(shards.map((shard) => shard.checkName)).toEqual( - shards.map((shard, index) => `checks-node-extensions-shard-${index + 1}`), + shards.map((_shard, index) => `checks-node-extensions-shard-${index + 1}`), ); const assigned = shards.flatMap((shard) => shard.extensionIds); @@ -593,7 +594,9 @@ describe("scripts/test-extension.mjs", () => { }, { env: { OPENCLAW_EXTENSION_BATCH_PARALLEL: "2" }, - runGroup, + runGroup: runGroup as NonNullable< + NonNullable[1]>["runGroup"] + >, vitestArgs: ["--reporter=dot"], }, ); diff --git a/test/scripts/test-group-report.test.ts b/test/scripts/test-group-report.test.ts index 9576f4858763..e153387e19c6 100644 --- a/test/scripts/test-group-report.test.ts +++ b/test/scripts/test-group-report.test.ts @@ -808,7 +808,9 @@ describe("scripts/test-group-report arg parsing", () => { flag === "--compare" ? [flag, values[0], values[1], flag, values[2], values[3]] : [flag, values[0], flag, values[1]]; - expect(() => parseTestGroupReportArgs(args)).toThrow(`${flag} was provided more than once`); + expect(() => parseTestGroupReportArgs(args as string[])).toThrow( + `${String(flag)} was provided more than once`, + ); } expect(parseTestGroupReportArgs(["--config", "a.ts", "--config", "b.ts"]).configs).toEqual([ "a.ts", diff --git a/test/scripts/ts-topology.test.ts b/test/scripts/ts-topology.test.ts index 8485ad839ed9..f219d7f34ee0 100644 --- a/test/scripts/ts-topology.test.ts +++ b/test/scripts/ts-topology.test.ts @@ -152,7 +152,9 @@ describe("ts-topology", () => { }); it("renders stable text summaries for the public-surface report", () => { - expect(renderTextReport({ ...publicSurfaceEnvelope, limit: 3 }, 3)).toMatchInlineSnapshot(` + expect( + renderTextReport({ ...publicSurfaceEnvelope, limit: 3 } as typeof publicSurfaceEnvelope, 3), + ).toMatchInlineSnapshot(` "Scope: custom Public exports analyzed: 6 Production-used exports: 3 diff --git a/test/scripts/verify-clawhub-published-artifact.test.ts b/test/scripts/verify-clawhub-published-artifact.test.ts index c50e568c8b48..2929ffe86bf9 100644 --- a/test/scripts/verify-clawhub-published-artifact.test.ts +++ b/test/scripts/verify-clawhub-published-artifact.test.ts @@ -1,4 +1,5 @@ import { createHash } from "node:crypto"; +/* oxlint-disable typescript/no-base-to-string -- fetch mocks normalize standard RequestInfo inputs for registry URL assertions. */ import { mkdirSync, mkdtempSync, @@ -93,7 +94,7 @@ function writeExpectedArtifact(artifact: Uint8Array) { return artifactDir; } -function artifactResponse(artifact: Uint8Array, body: BodyInit = artifact) { +function artifactResponse(artifact: Uint8Array, body: BodyInit = artifact as unknown as BodyInit) { const artifactIdentity = identity(artifact); return new Response(body, { headers: { @@ -310,7 +311,7 @@ describe("ClawHub published artifact verification", () => { it("rejects a missing configure-only tag before artifact or publisher requests", async () => { const artifact = new TextEncoder().encode("historical exact bytes"); - const fetchImpl = vi.fn(async () => + const fetchImpl = vi.fn(async (_input: RequestInfo | URL) => Response.json({ package: { tags: { beta: "2026.7.1-beta.2" } }, }), diff --git a/test/scripts/verify-pr-hosted-gates.test.ts b/test/scripts/verify-pr-hosted-gates.test.ts index 7233428cd4c9..c0f546fb89e4 100644 --- a/test/scripts/verify-pr-hosted-gates.test.ts +++ b/test/scripts/verify-pr-hosted-gates.test.ts @@ -42,7 +42,9 @@ function successfulRun(name: string, id: number, updatedAt: string) { }; } -function collectHostedGateEvidence(options: Parameters[0]) { +function collectHostedGateEvidence( + options: Omit[0], "nowMs" | "pr">, +) { return collectHostedGateEvidenceRaw({ nowMs, pr, ...options }); } diff --git a/test/scripts/vitest-process-group.test.ts b/test/scripts/vitest-process-group.test.ts index d1290df24647..120855b8a5ab 100644 --- a/test/scripts/vitest-process-group.test.ts +++ b/test/scripts/vitest-process-group.test.ts @@ -107,7 +107,7 @@ describe("vitest process group helpers", () => { expectListenerCount(listeners, "SIGTERM", 1); expectListenerCount(listeners, "exit", 1); - getListenerSet(listeners, "SIGTERM").values().next().value(); + getListenerSet(listeners, "SIGTERM").values().next().value!(); expect(onSignal).toHaveBeenCalledWith("SIGTERM"); expect(kill).toHaveBeenCalledWith(-4200, "SIGTERM"); @@ -138,7 +138,7 @@ describe("vitest process group helpers", () => { kill, }); - getListenerSet(listeners, "SIGTERM").values().next().value(); + getListenerSet(listeners, "SIGTERM").values().next().value!(); await Promise.resolve(); expect(kill).toHaveBeenNthCalledWith(1, -4200, "SIGTERM"); diff --git a/test/scripts/write-cli-startup-metadata.test.ts b/test/scripts/write-cli-startup-metadata.test.ts index c9606492af9a..2ac02bfc2bc6 100644 --- a/test/scripts/write-cli-startup-metadata.test.ts +++ b/test/scripts/write-cli-startup-metadata.test.ts @@ -158,7 +158,7 @@ describe("write-cli-startup-metadata", () => { failureMessage: "render failed", killGraceMs: 25, maxOutputBytes: 1024, - spawnProcess, + spawnProcess: spawnProcess as typeof spawn, timeoutMs: 5_000, }); @@ -182,7 +182,7 @@ describe("write-cli-startup-metadata", () => { failureMessage: "render failed", killGraceMs: 25, maxOutputBytes: 5, - spawnProcess, + spawnProcess: spawnProcess as typeof spawn, timeoutMs: 5_000, }); diff --git a/test/scripts/zai-fallback-repro.test.ts b/test/scripts/zai-fallback-repro.test.ts index c5a3ff1bf950..7f54caf09dac 100644 --- a/test/scripts/zai-fallback-repro.test.ts +++ b/test/scripts/zai-fallback-repro.test.ts @@ -79,12 +79,14 @@ describe("zai fallback repro command resolution", () => { }, error: () => {}, log: () => {}, - mkdtemp: async () => { + mkdtemp: (async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-zai-fallback-test-")); tempRoots.push(root); return root; - }, - randomUUID: () => "uuid-test", + }) as unknown as typeof fs.mkdtemp, + randomUUID: (() => "uuid-test") as unknown as NonNullable< + NonNullable[0]>["randomUUID"] + >, runCommand: async (label, _args, env) => { calls.push(label); if (label === "run1") { diff --git a/test/setup-openclaw-runtime.ts b/test/setup-openclaw-runtime.ts index 583d83eabea3..d557734b53fe 100644 --- a/test/setup-openclaw-runtime.ts +++ b/test/setup-openclaw-runtime.ts @@ -263,7 +263,9 @@ const createDefaultRegistry = () => plugin: createStubPlugin({ id: "discord", label: "Discord", - resolveReplyToMode: createTopLevelChannelReplyToModeResolverForTest("discord"), + resolveReplyToMode: createTopLevelChannelReplyToModeResolverForTest( + "discord", + ) as Parameters[0]["resolveReplyToMode"], }), source: "test", }, @@ -282,7 +284,9 @@ const createDefaultRegistry = () => ...createStubPlugin({ id: "telegram", label: "Telegram", - resolveReplyToMode: createTopLevelChannelReplyToModeResolverForTest("telegram"), + resolveReplyToMode: createTopLevelChannelReplyToModeResolverForTest( + "telegram", + ) as Parameters[0]["resolveReplyToMode"], }), status: { buildChannelSummary: async () => ({ diff --git a/test/test-env.test.ts b/test/test-env.test.ts index a0fae5de5c9a..6ee0ae134ada 100644 --- a/test/test-env.test.ts +++ b/test/test-env.test.ts @@ -197,8 +197,7 @@ describe("installTestEnv", () => { }; }; }; - const providers = copiedConfig.models?.providers; - requireRecord(providers, "model providers"); + const providers = requireRecord(copiedConfig.models?.providers, "model providers"); expect(providers.custom).toEqual({ baseUrl: "https://example.test/v1" }); const agentDefaults = requireRecord(copiedConfig.agents?.defaults, "agent defaults"); diff --git a/test/tsconfig/tsconfig.test.root.json b/test/tsconfig/tsconfig.test.root.json new file mode 100644 index 000000000000..1dbcd89f37b9 --- /dev/null +++ b/test/tsconfig/tsconfig.test.root.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.test.json", + "compilerOptions": { + "noUnusedLocals": true, + "noUnusedParameters": true, + "tsBuildInfoFile": ".artifacts/tsgo-cache/test-root.tsbuildinfo" + }, + "include": ["../../test/**/*", "../../src/**/*.d.ts", "../../packages/**/*.d.ts"], + "exclude": [ + "../../node_modules", + "../../dist", + "../../**/dist/**", + "../../test/fixtures/**", + // These Docker E2E clients statically import built dist/** artifacts and are + // validated by their Docker lanes, so they cannot join this source-only program + // (same rationale as the scripts/e2e exclusion in tsconfig.scripts.json). + "../../test/e2e/qa-lab/runtime/crestodian-first-run-docker-client.ts", + "../../test/e2e/qa-lab/runtime/agent-bundle-mcp-tools-docker-client.ts" + ] +} diff --git a/test/vitest-boundary-config.test.ts b/test/vitest-boundary-config.test.ts index c4f132267f5a..157a86b435ea 100644 --- a/test/vitest-boundary-config.test.ts +++ b/test/vitest-boundary-config.test.ts @@ -8,7 +8,7 @@ function requireTestConfig(config: ReturnType if (!config.test) { throw new Error("expected boundary vitest test config"); } - return config.test; + return config.test as typeof config.test & { passWithNoTests?: boolean }; } describe("boundary vitest config", () => { diff --git a/test/vitest-projects-config.test.ts b/test/vitest-projects-config.test.ts index 8f191345500c..bce275a2af34 100644 --- a/test/vitest-projects-config.test.ts +++ b/test/vitest-projects-config.test.ts @@ -41,10 +41,9 @@ function requireTestConfig(config: T): NonNullable return config.test as NonNullable; } -function requireWebOptimizer(testConfig: { - deps?: { optimizer?: { web?: { enabled?: boolean } } }; -}) { - const webOptimizer = testConfig.deps?.optimizer?.web; +function requireWebOptimizer(testConfig: unknown) { + const webOptimizer = (testConfig as { deps?: { optimizer?: { web?: { enabled?: boolean } } } }) + .deps?.optimizer?.web; if (!webOptimizer) { throw new Error("expected vitest web optimizer config"); } @@ -108,17 +107,19 @@ describe("projects vitest config", () => { }); it("keeps root projects on their expected pool defaults", () => { - expect(createGatewayVitestConfig().test.pool).toBe("threads"); - expect(createAgentsVitestConfig().test.pool).toBe("threads"); - expect(createAgentsCoreVitestConfig().test.pool).toBe("threads"); - expect(createAgentsEmbeddedVitestConfig().test.pool).toBe("threads"); - expect(createAgentsSupportVitestConfig().test.pool).toBe("threads"); - expect(createAgentsToolsVitestConfig().test.pool).toBe("threads"); - expect(createCommandsLightVitestConfig().test.pool).toBe("threads"); - expect(createCommandsVitestConfig().test.pool).toBe("forks"); - expect(createPluginSdkLightVitestConfig().test.pool).toBe("threads"); - expect(createUnitFastVitestConfig().test.pool).toBe("threads"); - expect(createContractsVitestConfig(pluginContractPatterns).test.pool).toBe("threads"); + expect(requireTestConfig(createGatewayVitestConfig()).pool).toBe("threads"); + expect(requireTestConfig(createAgentsVitestConfig()).pool).toBe("threads"); + expect(requireTestConfig(createAgentsCoreVitestConfig()).pool).toBe("threads"); + expect(requireTestConfig(createAgentsEmbeddedVitestConfig()).pool).toBe("threads"); + expect(requireTestConfig(createAgentsSupportVitestConfig()).pool).toBe("threads"); + expect(requireTestConfig(createAgentsToolsVitestConfig()).pool).toBe("threads"); + expect(requireTestConfig(createCommandsLightVitestConfig()).pool).toBe("threads"); + expect(requireTestConfig(createCommandsVitestConfig()).pool).toBe("forks"); + expect(requireTestConfig(createPluginSdkLightVitestConfig()).pool).toBe("threads"); + expect(requireTestConfig(createUnitFastVitestConfig()).pool).toBe("threads"); + expect(requireTestConfig(createContractsVitestConfig(pluginContractPatterns)).pool).toBe( + "threads", + ); }); it("honors explicit worker caps in CI vitest lanes", () => { @@ -156,9 +157,10 @@ describe("projects vitest config", () => { it("keeps contract shards on the non-isolated runner by default", () => { const config = createContractsVitestConfig(pluginContractPatterns); - expect(config.test.pool).toBe("threads"); - expect(config.test.isolate).toBe(false); - expect(normalizeConfigPath(config.test.runner)).toBe("test/non-isolated-runner.ts"); + const testConfig = requireTestConfig(config); + expect(testConfig.pool).toBe("threads"); + expect(testConfig.isolate).toBe(false); + expect(normalizeConfigPath(testConfig.runner)).toBe("test/non-isolated-runner.ts"); }); it("gives contract project configs unique names", () => { @@ -185,7 +187,7 @@ describe("projects vitest config", () => { "src/plugins/contracts/bundled-web-search.google.contract.test.ts", ]); - expect(config.test.include).toEqual([ + expect(requireTestConfig(config).include).toEqual([ "src/plugins/contracts/bundled-web-search.google.contract.test.ts", ]); }); @@ -204,7 +206,7 @@ describe("projects vitest config", () => { }, ); - expect(config.test.include).toEqual([ + expect(requireTestConfig(config).include).toEqual([ "src/channels/plugins/contracts/directory.registry-backed-shard-a.contract.test.ts", ]); }); @@ -223,23 +225,26 @@ describe("projects vitest config", () => { it("keeps the unit lane on the non-isolated runner by default", () => { const config = createUnitVitestConfig(); - expect(config.test.isolate).toBe(false); - expect(normalizeConfigPath(config.test.runner)).toBe("test/non-isolated-runner.ts"); + const testConfig = requireTestConfig(config); + expect(testConfig.isolate).toBe(false); + expect(normalizeConfigPath(testConfig.runner)).toBe("test/non-isolated-runner.ts"); }); it("keeps the unit-fast lane on shared workers without the reset-heavy runner", () => { const config = createUnitFastVitestConfig(); - expect(config.test.isolate).toBe(false); - expect(config.test.runner).toBeUndefined(); + const testConfig = requireTestConfig(config); + expect(testConfig.isolate).toBe(false); + expect(testConfig.runner).toBeUndefined(); }); it("keeps fake-timer unit-fast files serial with the non-isolated runner", () => { const config = createUnitFastFakeTimersVitestConfig(); - expect(config.test.isolate).toBe(false); - expect(normalizeConfigPath(config.test.runner)).toBe("test/non-isolated-runner.ts"); - expect(config.test.fileParallelism).toBe(false); - expect(config.test.maxWorkers).toBe(1); - expect(config.test.sequence).toMatchObject({ groupOrder: 1 }); + const testConfig = requireTestConfig(config); + expect(testConfig.isolate).toBe(false); + expect(normalizeConfigPath(testConfig.runner)).toBe("test/non-isolated-runner.ts"); + expect(testConfig.fileParallelism).toBe(false); + expect(testConfig.maxWorkers).toBe(1); + expect(testConfig.sequence).toMatchObject({ groupOrder: 1 }); }); it("keeps the bundled lane on thread workers with the non-isolated runner", () => { diff --git a/test/vitest-ui-package-config.test.ts b/test/vitest-ui-package-config.test.ts index 817658e630c2..c198b4db5c97 100644 --- a/test/vitest-ui-package-config.test.ts +++ b/test/vitest-ui-package-config.test.ts @@ -3,11 +3,18 @@ import { describe, expect, it } from "vitest"; import uiConfig from "../ui/vitest.config.ts"; import uiNodeConfig from "../ui/vitest.node.config.ts"; -function requireTestConfig(config: T): NonNullable { - if (!config.test) { +type ExpectedTestConfig = { + isolate?: boolean; + pool?: string; + projects?: unknown[]; + runner?: string; +}; + +function requireTestConfig(config: unknown): ExpectedTestConfig { + if (!config || typeof config !== "object" || !("test" in config) || !config.test) { throw new Error("expected ui package vitest test config"); } - return config.test as NonNullable; + return config.test as ExpectedTestConfig; } describe("ui package vitest config", () => { @@ -18,7 +25,7 @@ describe("ui package vitest config", () => { expect(testConfig.isolate).toBe(false); expect(testConfig.projects).toHaveLength(3); - for (const project of testConfig.projects) { + for (const project of testConfig.projects ?? []) { const projectTestConfig = requireTestConfig(project); expect(projectTestConfig.pool).toBe("threads"); expect(projectTestConfig.isolate).toBe(false); diff --git a/test/vitest/vitest.agents-paths.d.mts b/test/vitest/vitest.agents-paths.d.mts new file mode 100644 index 000000000000..6de0033a249b --- /dev/null +++ b/test/vitest/vitest.agents-paths.d.mts @@ -0,0 +1,6 @@ +export const agentsAllTestPatterns: string[]; +export const agentsCoreTestPatterns: string[]; +export const agentsEmbeddedTestPatterns: string[]; +export const agentsToolsTestPatterns: string[]; +export const agentsSupportTestPatterns: string[]; +export const agentsSupportExcludePatterns: string[]; diff --git a/test/vitest/vitest.auto-reply-reply.config.ts b/test/vitest/vitest.auto-reply-reply.config.ts index 8e0b0091084c..18b7c0827fa7 100644 --- a/test/vitest/vitest.auto-reply-reply.config.ts +++ b/test/vitest/vitest.auto-reply-reply.config.ts @@ -7,9 +7,6 @@ export function createAutoReplyReplyVitestConfig(env?: Record; const baseTest = sharedVitestConfig.test ?? {}; + const baseSequence = (baseTest as { sequence?: { groupOrder?: number } }).sequence; const scopedDir = options?.dir; const resolvedScopedDir = scopedDir ? path.join(repoRoot, scopedDir) : undefined; const env = options?.env; @@ -263,7 +264,7 @@ export function createScopedVitestConfig( ? {} : { sequence: { - ...baseTest.sequence, + ...baseSequence, groupOrder: scopedGroupOrder, }, }), diff --git a/test/vitest/vitest.test-shards.d.mts b/test/vitest/vitest.test-shards.d.mts new file mode 100644 index 000000000000..f741eac906b9 --- /dev/null +++ b/test/vitest/vitest.test-shards.d.mts @@ -0,0 +1,11 @@ +export type FullSuiteVitestShard = { + config: string; + name: string; + projects: string[]; +}; + +export const autoReplyCoreTestInclude: string[]; +export const autoReplyCoreTestExclude: string[]; +export const autoReplyTopLevelReplyTestInclude: string[]; +export const autoReplyReplySubtreeTestInclude: string[]; +export const fullSuiteVitestShards: FullSuiteVitestShard[]; diff --git a/test/vitest/vitest.tui-pty.config.ts b/test/vitest/vitest.tui-pty.config.ts index 3bf1a2a90e72..22a24ffc359b 100644 --- a/test/vitest/vitest.tui-pty.config.ts +++ b/test/vitest/vitest.tui-pty.config.ts @@ -27,6 +27,7 @@ function createTuiPtyVitestConfig(env?: Record) { loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", configEnv), ); const includeFromArgv = toTuiPtyIncludePatterns(narrowIncludePatternsForCli(targetableIncludes)); + const baseSequence = (baseTest as { sequence?: { groupOrder?: number } }).sequence; return defineConfig({ ...sharedVitestConfig, @@ -47,7 +48,7 @@ function createTuiPtyVitestConfig(env?: Record) { ), ], sequence: { - ...baseTest.sequence, + ...baseSequence, groupOrder: 95, }, }, diff --git a/test/vitest/vitest.unit-fast-fake-timers.config.ts b/test/vitest/vitest.unit-fast-fake-timers.config.ts index 1f25f8104372..df378d545df7 100644 --- a/test/vitest/vitest.unit-fast-fake-timers.config.ts +++ b/test/vitest/vitest.unit-fast-fake-timers.config.ts @@ -13,6 +13,7 @@ export function createUnitFastFakeTimersVitestConfig( options: { argv?: string[] } = {}, ) { const sharedTest = sharedVitestConfig.test ?? {}; + const sharedSequence = (sharedTest as { sequence?: { groupOrder?: number } }).sequence; const includeFromEnv = loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env); const unitFastTimerTestFiles = getUnitFastTimerTestFiles(); const cliInclude = narrowIncludePatternsForCli(unitFastTimerTestFiles, options.argv); @@ -31,7 +32,7 @@ export function createUnitFastFakeTimersVitestConfig( maxWorkers: 1, fileParallelism: false, sequence: { - ...sharedTest.sequence, + ...sharedSequence, groupOrder: 1, }, passWithNoTests: true, diff --git a/test/vitest/vitest.unit-fast-paths.d.mts b/test/vitest/vitest.unit-fast-paths.d.mts new file mode 100644 index 000000000000..4f991d61f09e --- /dev/null +++ b/test/vitest/vitest.unit-fast-paths.d.mts @@ -0,0 +1,29 @@ +export type UnitFastFileAnalysis = { + file: string; + unitFast: boolean; + forced: boolean; + reasons: string[]; +}; + +export type UnitFastAnalysisOptions = { + scope?: "default" | "broad"; +}; + +export const forcedUnitFastTestFiles: string[]; +export function classifyUnitFastTestFileContent(source: string): string[]; +export function collectUnitFastTestCandidates(cwd?: string): string[]; +export function collectBroadUnitFastTestCandidates(cwd?: string): string[]; +export function collectUnitFastTestFileAnalysis( + cwd?: string, + options?: UnitFastAnalysisOptions, +): UnitFastFileAnalysis[]; +export function getUnitFastTestFilesForIncludePatterns( + includePatterns: string[], + options?: { dir?: string }, +): string[]; +export function getUnitFastTestFiles(): string[]; +export function getUnitFastTimerTestFiles(): string[]; +export function isUnitFastTestFile(file: string): boolean; +export function isUnitFastTimerTestFile(file: string): boolean; +export function resolveUnitFastTestIncludePattern(file: string): string | null; +export function resolveUnitFastTimerTestIncludePattern(file: string): string | null; diff --git a/test/vitest/vitest.unit-paths.d.mts b/test/vitest/vitest.unit-paths.d.mts new file mode 100644 index 000000000000..d97115e09734 --- /dev/null +++ b/test/vitest/vitest.unit-paths.d.mts @@ -0,0 +1,7 @@ +export const unitTestIncludePatterns: string[]; +export const boundaryTestFiles: string[]; +export const bundledPluginDependentUnitTestFiles: string[]; +export const unitTestAdditionalExcludePatterns: string[]; +export function isUnitConfigTestFile(file: string): boolean; +export function isBundledPluginDependentUnitTestFile(file: string): boolean; +export function isBoundaryTestFile(file: string): boolean; diff --git a/tsconfig.projects.json b/tsconfig.projects.json index 476b8a158850..199fe46da306 100644 --- a/tsconfig.projects.json +++ b/tsconfig.projects.json @@ -3,6 +3,7 @@ "references": [ { "path": "./tsconfig.core.projects.json" }, { "path": "./tsconfig.extensions.projects.json" }, - { "path": "./tsconfig.scripts.json" } + { "path": "./tsconfig.scripts.json" }, + { "path": "./test/tsconfig/tsconfig.test.root.json" } ] }