docs: document test project scripts

This commit is contained in:
Peter Steinberger
2026-06-04 23:55:54 -04:00
parent 9b1a01e4f9
commit a59eba3ee1
17 changed files with 107 additions and 0 deletions

View File

@@ -1,3 +1,5 @@
// Dispatches Vitest project shards for explicit targets, changed files, or the
// full local suite.
import fs from "node:fs";
import { performance } from "node:perf_hooks";
import { formatMs } from "./lib/check-timing-summary.mjs";

View File

@@ -1,3 +1,6 @@
// Test-project planning helpers used by scripts/run-vitest.mjs,
// scripts/test-projects.mjs, and focused tests. Exports are intentionally
// granular so project selection stays testable without spawning Vitest.
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
@@ -250,6 +253,9 @@ function uniqueOrdered(values) {
return [...new Set(values)];
}
/**
* Orders full-suite specs so expensive shards start first in parallel runs.
*/
export function orderFullSuiteSpecsForParallelRun(specs, shardTimings = new Map()) {
const sortedSpecs = specs.toSorted((a, b) => {
const weightDelta =
@@ -831,7 +837,9 @@ const BROAD_CHANGED_ENV_KEY = "OPENCLAW_TEST_CHANGED_BROAD";
const VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS";
const VITEST_NO_OUTPUT_HEARTBEAT_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS";
const VITEST_NO_OUTPUT_RETRY_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_RETRY";
/** Default no-output timeout applied to test-projects Vitest children. */
export const DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_TIMEOUT_MS = String(900_000);
/** Default heartbeat interval applied to test-projects Vitest children. */
export const DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_HEARTBEAT_MS = String(
DEFAULT_VITEST_NO_OUTPUT_HEARTBEAT_MS,
);
@@ -1138,6 +1146,9 @@ function expandExplicitSourceTestTargets(targetArgs, cwd) {
});
}
/**
* Finds explicit test path targets that do not match any known project plan.
*/
export function findUnmatchedExplicitTestTargets(args, cwd = process.cwd()) {
const { targetArgs } = parseTestProjectsArgs(args, cwd);
if (targetArgs.length === 0) {
@@ -1775,6 +1786,9 @@ function resolvePreciseChangedTestTargets(changedPath, options) {
return null;
}
/**
* Maps changed repo paths to the smallest useful Vitest target plan.
*/
export function resolveChangedTestTargetPlan(changedPaths, options = {}) {
if (changedPaths.length === 0) {
return { mode: "none", targets: [] };

View File

@@ -1,3 +1,4 @@
// Shared helpers for running Vitest JSON reports and reading duration data.
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
@@ -6,6 +7,9 @@ import path from "node:path";
const normalizeRepoPath = (value) => value.split(path.sep).join("/");
const repoRoot = path.resolve(process.cwd());
/**
* Normalizes absolute or relative file names to repo-relative POSIX paths.
*/
export function normalizeTrackedRepoPath(value) {
const normalizedValue = typeof value === "string" ? value : String(value ?? "");
const repoRelative = path.isAbsolute(normalizedValue)
@@ -17,10 +21,16 @@ export function normalizeTrackedRepoPath(value) {
return normalizeRepoPath(repoRelative);
}
/**
* Reads and parses a JSON file.
*/
export function readJsonFile(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
/**
* Reads a JSON file or returns the provided fallback on failure.
*/
export function tryReadJsonFile(filePath, fallback) {
try {
return readJsonFile(filePath);
@@ -29,6 +39,9 @@ export function tryReadJsonFile(filePath, fallback) {
}
}
/**
* Runs Vitest with the JSON reporter unless an existing report was supplied.
*/
export function runVitestJsonReport({
config,
reportPath = "",
@@ -63,6 +76,9 @@ export function runVitestJsonReport({
return resolvedReportPath;
}
/**
* Extracts per-file durations from a Vitest JSON report.
*/
export function collectVitestFileDurations(report, normalizeFile = (value) => value) {
return (report.testResults ?? [])
.map((result) => {
@@ -79,6 +95,9 @@ export function collectVitestFileDurations(report, normalizeFile = (value) => va
.filter((entry) => entry.file.length > 0 && entry.durationMs > 0);
}
/**
* Extracts per-assertion durations from a Vitest JSON report.
*/
export function collectVitestAssertionDurations(report, normalizeFile = (value) => value) {
return (report.testResults ?? []).flatMap((result) => {
const file = typeof result.name === "string" ? normalizeFile(result.name) : "";

View File

@@ -1,3 +1,4 @@
// Reports which unit tests qualify for the unit-fast routing lane.
import {
collectBroadUnitFastTestCandidates,
collectUnitFastTestFileAnalysis,

View File

@@ -1,3 +1,4 @@
// Refreshes the checked-in CLI startup benchmark fixture.
import { spawnSync } from "node:child_process";
import { parseFlagArgs, stringFlag, intFlag } from "./lib/arg-utils.mjs";

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
// Runs the closed-loop voice-call test slice through the repo Vitest wrapper.
import { execFileSync } from "node:child_process";
import { bundledPluginFile } from "./lib/bundled-plugin-paths.mjs";

View File

@@ -1,5 +1,7 @@
#!/usr/bin/env node
// Reports transitive npm package manifest risks such as lifecycle scripts,
// exotic specs, and recently published versions.
import { readFile } from "node:fs/promises";
import path from "node:path";
import process from "node:process";
@@ -20,6 +22,7 @@ const PINNED_GITHUB_TARBALL_PATTERN =
const EXOTIC_SPEC_PATTERN = /^(?:git\+|github:|gitlab:|bitbucket:|https?:)/iu;
const RECENTLY_PUBLISHED_VERSION_TYPE = "recently-published-version";
const NPM_PACKUMENT_ACCEPT_HEADER = "application/json";
/** Maximum npm packument response size accepted by the risk scanner. */
export const NPM_PACKUMENT_RESPONSE_MAX_BYTES = 16 * 1024 * 1024;
function isAllowedPinnedSpec(spec) {

View File

@@ -1,5 +1,7 @@
#!/usr/bin/env node
// Runs the tsdown build with output cleanup, stale chunk pruning, and bounded
// child-process diagnostics.
import { spawn, spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
@@ -68,6 +70,9 @@ function pruneStaleRuntimeSymlinks() {
removeDistPluginNodeModulesSymlinks(path.join(cwd, "dist-runtime"));
}
/**
* Removes build output roots while preserving explicitly protected artifacts.
*/
export function cleanTsdownOutputRoots(params = {}) {
const cwd = params.cwd ?? process.cwd();
const fsImpl = params.fs ?? fs;

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env node
// Routes UI package commands through the repo's Node/pnpm wrappers.
import { spawn, spawnSync } from "node:child_process";
import fs from "node:fs";
import { createRequire } from "node:module";
@@ -18,6 +19,9 @@ function usage() {
process.stderr.write("Usage: node scripts/ui.js <install|dev|build|test> [...args]\n");
}
/**
* Returns whether Windows needs cmd.exe for a command shim.
*/
export function shouldUseCmdExeForCommand(cmd, platform = process.platform) {
if (platform !== "win32") {
return false;
@@ -26,6 +30,9 @@ export function shouldUseCmdExeForCommand(cmd, platform = process.platform) {
return WINDOWS_CMD_EXE_EXTENSIONS.has(extension);
}
/**
* Builds the spawn call for a UI command, including Windows cmd.exe wrapping.
*/
export function resolveSpawnCall(cmd, args, envOverride, params = {}) {
const platform = params.platform ?? process.platform;
const comSpec = params.comSpec ?? process.env.ComSpec ?? "cmd.exe";
@@ -54,6 +61,9 @@ export function resolveSpawnCall(cmd, args, envOverride, params = {}) {
};
}
/**
* Builds the pnpm-backed spawn call for UI package scripts.
*/
export function resolvePnpmSpawnCall(pnpmArgs, envOverride, params = {}) {
const env = envOverride ?? process.env;
const platform = params.platform ?? process.platform;

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env node
// Validates that a referenced release-publish workflow run is usable for approval.
import fs from "node:fs";
const run = JSON.parse(fs.readFileSync(0, "utf8"));

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
// Verifies Docker image attestations cover required platforms and predicates.
import { execFileSync } from "node:child_process";
import process from "node:process";
@@ -7,6 +8,9 @@ const ATTESTATION_REFERENCE_TYPE = "attestation-manifest";
const EXPECTED_ATTESTATION_ARTIFACT_TYPE = "application/vnd.docker.attestation.manifest.v1+json";
const REQUIRED_PREDICATES = ["https://spdx.dev/Document", "https://slsa.dev/provenance/v1"];
/**
* Rewrites an image reference to use the provided digest.
*/
export function imageRefForDigest(imageRef, digest) {
const atIndex = imageRef.indexOf("@");
if (atIndex >= 0) {
@@ -18,6 +22,9 @@ export function imageRefForDigest(imageRef, digest) {
return `${base}@${digest}`;
}
/**
* Parses os/architecture[/variant] platform strings.
*/
export function parsePlatform(value) {
const [os, architecture, variant] = value.split("/");
if (!os || !architecture || value.split("/").length > 3) {
@@ -49,6 +56,9 @@ function parseJson(raw, label) {
}
}
/**
* Collects missing/mismatched attestation errors for required image platforms.
*/
export function collectDockerAttestationErrors(params) {
const {
imageRef,

View File

@@ -1,5 +1,7 @@
#!/usr/bin/env node
// Verifies published plugin npm packages include built runtime entries and
// metadata expected by OpenClaw.
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";

View File

@@ -1,3 +1,4 @@
// Runs the broad verification graph used by Crabbox/Testbox: check then test.
import { performance } from "node:perf_hooks";
import { formatMs, printTimingSummary } from "./lib/check-timing-summary.mjs";
import { runManagedCommand } from "./lib/managed-child-process.mjs";
@@ -7,6 +8,9 @@ const stages = [
{ name: "test", args: ["test"] },
];
/**
* Renders CLI usage for the verification wrapper.
*/
export function usage() {
return [
"Usage: node scripts/verify.mjs",
@@ -18,6 +22,9 @@ export function usage() {
].join("\n");
}
/**
* Parses verify wrapper CLI args.
*/
export function parseVerifyArgs(argv) {
const args = { help: false };
for (const arg of argv) {
@@ -45,6 +52,9 @@ async function runStage(stage) {
};
}
/**
* Runs verification stages in order and stops at the first failure.
*/
export async function main(argv = process.argv.slice(2)) {
let args;
try {

View File

@@ -1,7 +1,11 @@
// Shared Vitest child process-group signal forwarding helpers.
export function shouldUseDetachedVitestProcessGroup(platform = process.platform) {
return platform !== "win32";
}
/**
* Resolves the PID or process-group target for Vitest signal forwarding.
*/
export function resolveVitestProcessGroupSignalTarget(params) {
const pid = params.childPid;
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
@@ -10,6 +14,9 @@ export function resolveVitestProcessGroupSignalTarget(params) {
return shouldUseDetachedVitestProcessGroup(params.platform) ? -pid : pid;
}
/**
* Forwards a signal to the Vitest child or process group.
*/
export function forwardSignalToVitestProcessGroup(params) {
const target = resolveVitestProcessGroupSignalTarget({
childPid: params.child.pid,
@@ -54,6 +61,9 @@ function ensureProcessListenerCapacity(processObject, eventName, additionalListe
}
}
/**
* Installs signal/exit cleanup handlers for a Vitest child process group.
*/
export function installVitestProcessGroupCleanup(params) {
const processObject = params.processObject ?? process;
const platform = params.platform ?? process.platform;

View File

@@ -1,4 +1,6 @@
#!/usr/bin/env node
// Watches dev source paths and restarts scripts/run-node.mjs when relevant
// files change.
import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import fs from "node:fs";
@@ -101,6 +103,7 @@ const sleep = (ms) =>
const createWatchLockKey = (cwd, args) =>
createHash("sha256").update(cwd).update("\0").update(args.join("\0")).digest("hex").slice(0, 12);
/** Resolves the lock path that prevents duplicate watch-node loops. */
export const resolveWatchLockPath = (cwd, args = []) =>
path.join(cwd, WATCH_LOCK_DIR, `${createWatchLockKey(cwd, args)}.json`);
@@ -258,6 +261,9 @@ const releaseWatchLock = (lockHandle) => {
* watchPaths?: string[];
* }} [params]
*/
/**
* Runs the watch loop and restarts the child process on relevant changes.
*/
export async function runWatchMain(params = {}) {
const deps = {
spawn: params.spawn ?? spawn,

View File

@@ -1,5 +1,9 @@
// Windows cmd.exe quoting helpers for npm/pnpm command shims.
const WINDOWS_UNSAFE_CMD_CHARS_RE = /[&|<>%\r\n]/;
/**
* Resolves the correctly cased PATH key in a Windows-style env object.
*/
export function resolvePathEnvKey(env) {
return Object.keys(env).find((key) => key.toLowerCase() === "path") ?? "PATH";
}
@@ -15,6 +19,9 @@ function escapeForCmdExe(arg) {
return `"${escaped.replace(/"/g, '""')}"`;
}
/**
* Builds a cmd.exe-safe command line or rejects unsafe shell metacharacters.
*/
export function buildCmdExeCommandLine(command, args) {
const escapedCommand = escapeForCmdExe(command);
const commandLine = [escapedCommand, ...args.map(escapeForCmdExe)].join(" ");

View File

@@ -1,3 +1,4 @@
// Builds the generated official channel catalog from publishable channel plugins.
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
@@ -5,6 +6,7 @@ import officialExternalChannelCatalog from "./lib/official-external-channel-cata
import { isRecord, trimString } from "./lib/record-shared.mjs";
import { writeTextFileIfChanged } from "./runtime-postbuild-shared.mjs";
/** Generated official channel catalog path in dist. */
export const OFFICIAL_CHANNEL_CATALOG_RELATIVE_PATH = "dist/channel-catalog.json";
function toCatalogInstall(value, packageName) {
@@ -61,6 +63,9 @@ function getCatalogChannelId(entry) {
return trimString(entry?.openclaw?.channel?.id) || trimString(entry?.name);
}
/**
* Collects publishable channel catalog entries from bundled and external channels.
*/
export function buildOfficialChannelCatalog(params = {}) {
const repoRoot = params.cwd ?? params.repoRoot ?? process.cwd();
const extensionsRoot = path.join(repoRoot, "extensions");