mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
docs: document script lib test helpers
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user