docs: document script lib test helpers

This commit is contained in:
Peter Steinberger
2026-06-04 23:08:26 -04:00
parent 92cdcae500
commit f3abe61b78
7 changed files with 91 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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