From f3abe61b78faeac8ce0894c02abf83307e56f130 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 4 Jun 2026 23:08:26 -0400 Subject: [PATCH] docs: document script lib test helpers --- scripts/lib/ts-guard-utils.mjs | 31 +++++++++++++++++++++++ scripts/lib/tsdown-output-roots.mjs | 7 +++++ scripts/lib/tsgo-sparse-guard.mjs | 10 ++++++++ scripts/lib/vitest-batch-runner.mjs | 10 ++++++++ scripts/lib/vitest-report-cli-utils.mjs | 10 ++++++++ scripts/lib/vitest-shard-timings.mjs | 13 ++++++++++ scripts/lib/workspace-bootstrap-smoke.mjs | 10 ++++++++ 7 files changed, 91 insertions(+) diff --git a/scripts/lib/ts-guard-utils.mjs b/scripts/lib/ts-guard-utils.mjs index 51e271507090..fb39cbfb91c5 100644 --- a/scripts/lib/ts-guard-utils.mjs +++ b/scripts/lib/ts-guard-utils.mjs @@ -1,3 +1,4 @@ +// Shared TypeScript AST and source-file helpers for guard scripts. import { existsSync, promises as fs } from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; @@ -13,6 +14,9 @@ function getTypeScript() { const baseTestSuffixes = [".test.ts", ".test-utils.ts", ".test-harness.ts", ".e2e-harness.ts"]; +/** + * Resolves the repository root by walking upward from the caller module. + */ export function resolveRepoRoot(importMetaUrl) { // Walk up from the caller's directory until we find the repo root (.git). // This handles callers at any depth (scripts/*.mjs, scripts/lib/*.mjs, etc.) @@ -29,6 +33,9 @@ export function resolveRepoRoot(importMetaUrl) { return path.resolve(path.dirname(fileURLToPath(importMetaUrl)), "..", ".."); } +/** + * Converts repo-relative source roots into absolute paths. + */ export function resolveSourceRoots(repoRoot, relativeRoots) { return relativeRoots.map((root) => path.join(repoRoot, ...root.split("/").filter(Boolean))); } @@ -38,6 +45,9 @@ function isTestLikeTypeScriptFile(filePath, options = {}) { return [...baseTestSuffixes, ...extraTestSuffixes].some((suffix) => filePath.endsWith(suffix)); } +/** + * Recursively collects TypeScript files under a file or directory target. + */ export async function collectTypeScriptFiles(targetPath, options = {}) { const includeTests = options.includeTests ?? false; const extraTestSuffixes = options.extraTestSuffixes ?? []; @@ -92,6 +102,9 @@ export async function collectTypeScriptFiles(targetPath, options = {}) { return out; } +/** + * Collects TypeScript files from multiple roots, ignoring missing roots by default. + */ export async function collectTypeScriptFilesFromRoots(sourceRoots, options = {}) { return ( await Promise.all( @@ -106,6 +119,9 @@ export async function collectTypeScriptFilesFromRoots(sourceRoots, options = {}) ).flat(); } +/** + * Runs a guard's violation scanner across collected TypeScript source files. + */ export async function collectFileViolations(params) { const files = await collectTypeScriptFilesFromRoots(params.sourceRoots, { extraTestSuffixes: params.extraTestSuffixes, @@ -128,10 +144,16 @@ export async function collectFileViolations(params) { return violations; } +/** + * Returns the one-based source line for a TypeScript AST node. + */ export function toLine(sourceFile, node) { return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1; } +/** + * Extracts text from identifier, string, or numeric property names. + */ export function getPropertyNameText(name) { const ts = getTypeScript(); if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) { @@ -140,6 +162,9 @@ export function getPropertyNameText(name) { return null; } +/** + * Removes harmless expression wrappers before AST shape checks. + */ export function unwrapExpression(expression) { const ts = getTypeScript(); let current = expression; @@ -160,6 +185,9 @@ export function unwrapExpression(expression) { } } +/** + * Collects one-based line numbers for call expressions selected by a callback. + */ export function collectCallExpressionLines(ts, sourceFile, resolveLineNode) { const lines = []; const visit = (node) => { @@ -183,6 +211,9 @@ function isDirectExecution(importMetaUrl) { return path.resolve(entry) === fileURLToPath(importMetaUrl); } +/** + * Runs a script main function only when the module is the direct entrypoint. + */ export function runAsScript(importMetaUrl, main) { if (!isDirectExecution(importMetaUrl)) { return; diff --git a/scripts/lib/tsdown-output-roots.mjs b/scripts/lib/tsdown-output-roots.mjs index d1a66e32dcbd..8c981bb7b639 100644 --- a/scripts/lib/tsdown-output-roots.mjs +++ b/scripts/lib/tsdown-output-roots.mjs @@ -1,3 +1,4 @@ +// Lists package dist roots produced by tsdown builds. const TSDOWN_PACKAGE_NAMES = [ "agent-core", "gateway-client", @@ -16,8 +17,14 @@ const TSDOWN_PACKAGE_NAMES = [ "acp-core", ]; +/** + * Dist roots for all packages built through the shared tsdown pipeline. + */ export const TSDOWN_PACKAGE_OUTPUT_ROOTS = TSDOWN_PACKAGE_NAMES.map(packageOutputRoot); +/** + * Returns the dist root for a known tsdown package name. + */ export function tsdownPackageOutputRoot(packageName) { if (!TSDOWN_PACKAGE_NAMES.includes(packageName)) { throw new Error(`Unknown tsdown package output root: ${packageName}`); diff --git a/scripts/lib/tsgo-sparse-guard.mjs b/scripts/lib/tsgo-sparse-guard.mjs index 94d131f06e39..6c6d1fdf5fda 100644 --- a/scripts/lib/tsgo-sparse-guard.mjs +++ b/scripts/lib/tsgo-sparse-guard.mjs @@ -1,3 +1,4 @@ +// Detects sparse-checkout gaps before tsgo runs core TypeScript projects. import { spawnSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; @@ -50,11 +51,17 @@ const CORE_TEST_REQUIRED_PATHS = [ "ui/src/ui/gateway.ts", ]; +/** + * Reports whether the caller explicitly opted out of sparse tsgo guard errors. + */ export function shouldSkipSparseTsgoGuardError(env = process.env) { const value = env[TSGO_SPARSE_SKIP_ENV_KEY]?.trim().toLowerCase(); return value === "1" || value === "true"; } +/** + * Creates an environment that suppresses recursive sparse tsgo guard checks. + */ export function createSparseTsgoSkipEnv(baseEnv = process.env) { return { ...baseEnv, @@ -62,6 +69,9 @@ export function createSparseTsgoSkipEnv(baseEnv = process.env) { }; } +/** + * Builds the sparse-checkout diagnostic for core tsgo projects, when needed. + */ export function getSparseTsgoGuardError( args, { diff --git a/scripts/lib/vitest-batch-runner.mjs b/scripts/lib/vitest-batch-runner.mjs index 29474b1f70e6..1091b14565b7 100644 --- a/scripts/lib/vitest-batch-runner.mjs +++ b/scripts/lib/vitest-batch-runner.mjs @@ -1,3 +1,4 @@ +// Runs grouped Vitest batches through the repo pnpm wrapper. import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { spawnPnpmRunner } from "../pnpm-runner.mjs"; @@ -10,6 +11,9 @@ const scriptFile = fileURLToPath(import.meta.url); const scriptDir = path.dirname(scriptFile); const repoRoot = path.resolve(scriptDir, "../.."); +/** + * Runs one Vitest batch and forwards process-group cleanup signals. + */ export async function runVitestBatch(params) { return await new Promise((resolve, reject) => { let forwardedSignal; @@ -46,10 +50,16 @@ export async function runVitestBatch(params) { }); } +/** + * Builds pnpm arguments for a Vitest batch run. + */ export function buildVitestBatchPnpmArgs(params) { return ["exec", "vitest", "run", "--config", params.config, ...params.args, ...params.targets]; } +/** + * Checks whether a module URL is the current direct script entrypoint. + */ export function isDirectScriptRun(metaUrl) { const entryHref = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : ""; return metaUrl === entryHref; diff --git a/scripts/lib/vitest-report-cli-utils.mjs b/scripts/lib/vitest-report-cli-utils.mjs index b3e1807ba265..d0a1d446c6f5 100644 --- a/scripts/lib/vitest-report-cli-utils.mjs +++ b/scripts/lib/vitest-report-cli-utils.mjs @@ -1,6 +1,10 @@ +// Shared CLI parsing and formatting helpers for Vitest report scripts. import { readJsonFile, runVitestJsonReport } from "../test-report-utils.mjs"; import { intFlag, parseFlagArgs, stringFlag } from "./arg-utils.mjs"; +/** + * Parses common Vitest report flags with caller-provided defaults. + */ export function parseVitestReportArgs(argv, defaults) { return parseFlagArgs( argv, @@ -17,6 +21,9 @@ export function parseVitestReportArgs(argv, defaults) { ); } +/** + * Runs Vitest JSON reporting from parsed args and loads the generated report. + */ export function loadVitestReportFromArgs(args, prefix) { const reportPath = runVitestJsonReport({ config: args.config, @@ -26,6 +33,9 @@ export function loadVitestReportFromArgs(args, prefix) { return readJsonFile(reportPath); } +/** + * Formats milliseconds with a fixed decimal precision. + */ export function formatMs(value, digits = 1) { return `${value.toFixed(digits)}ms`; } diff --git a/scripts/lib/vitest-shard-timings.mjs b/scripts/lib/vitest-shard-timings.mjs index b94300fc87b7..f08bb8e45f8b 100644 --- a/scripts/lib/vitest-shard-timings.mjs +++ b/scripts/lib/vitest-shard-timings.mjs @@ -1,3 +1,4 @@ +// Persists per-shard Vitest timing samples for later scheduling. import { createHash } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; @@ -25,6 +26,9 @@ function resolveShardTimingsPath(cwd = process.cwd(), env = process.env) { return env[TIMINGS_FILE_ENV_KEY] || path.join(cwd, ".artifacts", "vitest-shard-timings.json"); } +/** + * Resolves the stable timing key for a Vitest shard specification. + */ export function resolveShardTimingKey(spec) { if (!Array.isArray(spec.includePatterns) || spec.includePatterns.length === 0) { return spec.config; @@ -40,6 +44,9 @@ export function resolveShardTimingKey(spec) { )}`; } +/** + * Creates a timing sample for completed non-watch Vitest shard runs. + */ export function createShardTimingSample(spec, durationMs) { if (spec.watchMode || !Number.isFinite(durationMs) || durationMs <= 0) { return null; @@ -54,6 +61,9 @@ export function createShardTimingSample(spec, durationMs) { }; } +/** + * Reads persisted shard timing averages, returning an empty map when disabled. + */ export function readShardTimings(cwd = process.cwd(), env = process.env) { if (!shouldUseShardTimings(env)) { return new Map(); @@ -78,6 +88,9 @@ export function readShardTimings(cwd = process.cwd(), env = process.env) { } } +/** + * Merges new shard timing samples into the persisted local timing artifact. + */ export function writeShardTimings(samples, cwd = process.cwd(), env = process.env) { if (!shouldUseShardTimings(env) || samples.length === 0) { return; diff --git a/scripts/lib/workspace-bootstrap-smoke.mjs b/scripts/lib/workspace-bootstrap-smoke.mjs index f7f1bef5e356..24c8fbf74928 100644 --- a/scripts/lib/workspace-bootstrap-smoke.mjs +++ b/scripts/lib/workspace-bootstrap-smoke.mjs @@ -1,8 +1,12 @@ +// Verifies installed packages can bootstrap the default OpenClaw workspace files. import { execFileSync } from "node:child_process"; import { existsSync, mkdtempSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; +/** + * Template pack files that must be present in installed packages. + */ export const WORKSPACE_TEMPLATE_PACK_PATHS = [ "docs/reference/templates/AGENTS.md", "docs/reference/templates/SOUL.md", @@ -26,6 +30,9 @@ const REQUIRED_BOOTSTRAP_WORKSPACE_FILES = [ const WORKSPACE_BOOTSTRAP_SMOKE_TIMEOUT_MS = 15_000; const SAFE_UNIX_SMOKE_PATH = "/usr/bin:/bin"; +/** + * Creates a minimal isolated environment for workspace bootstrap smoke runs. + */ export function createWorkspaceBootstrapSmokeEnv(env, homeDir, overrides = {}) { const allowlistedEnvEntries = [ "TMPDIR", @@ -90,6 +97,9 @@ function describeExecFailure(error) { return [error.message, stdout, stderr].filter(Boolean).join(" | "); } +/** + * Runs the installed CLI workspace bootstrap smoke and validates created files. + */ export function runInstalledWorkspaceBootstrapSmoke(params) { const tempRoot = mkdtempSync(join(tmpdir(), "openclaw-workspace-bootstrap-smoke-")); const homeDir = join(tempRoot, "home");