perf(test): speed up boundary report checks

This commit is contained in:
Peter Steinberger 2026-04-28 19:00:18 +01:00
parent 53d34e7cde
commit 84154bb09c
No known key found for this signature in database
4 changed files with 107 additions and 78 deletions

View file

@ -1,6 +1,7 @@
#!/usr/bin/env node
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import {
pluginSdkEntrypoints,
publicPluginOwnedSdkEntrypoints,
@ -48,6 +49,12 @@ type CompatDebtRecord = {
eligibleForRemoval: boolean;
};
type WorkspaceTextFile = {
file: string;
relativeFile: string;
source: string;
};
type ReservedSdkImport = {
file: string;
specifier: string;
@ -114,6 +121,12 @@ type BoundaryReportSummary = {
};
};
export type PluginBoundaryReportResult = {
stdout: string;
stderr: string;
exitCode: number;
};
function collectTextFiles(dir: string): string[] {
const files: string[] = [];
if (!existsSync(dir)) {
@ -145,6 +158,14 @@ function repoRelative(file: string): string {
return relative(REPO_ROOT, file).replaceAll("\\", "/");
}
function collectWorkspaceTextFileSources(): WorkspaceTextFile[] {
return collectWorkspaceTextFiles().map((file) => ({
file,
relativeFile: repoRelative(file),
source: readFileSync(file, "utf8"),
}));
}
function isDocsFile(file: string): boolean {
return file.startsWith("docs/") || file === "README.md";
}
@ -243,15 +264,13 @@ function extractCompatTokens(record: PluginCompatRecord): string[] {
return [...tokens].toSorted();
}
function collectReferenceFiles(files: readonly string[], tokens: readonly string[]) {
function collectReferenceFiles(files: readonly WorkspaceTextFile[], tokens: readonly string[]) {
const codeReferenceFiles = new Set<string>();
const docReferenceFiles = new Set<string>();
for (const file of files) {
const relativeFile = repoRelative(file);
for (const { relativeFile, source } of files) {
if (relativeFile === "src/plugins/compat/registry.ts") {
continue;
}
const source = readFileSync(file, "utf8");
if (!tokens.some((token) => source.includes(token))) {
continue;
}
@ -267,11 +286,18 @@ function collectReferenceFiles(files: readonly string[], tokens: readonly string
};
}
function collectCompatDebt(files: readonly string[], today = new Date()): CompatDebtRecord[] {
function collectCompatDebt(
files: readonly WorkspaceTextFile[],
today = new Date(),
options: { includeReferenceFiles?: boolean } = {},
): CompatDebtRecord[] {
return PLUGIN_COMPAT_RECORDS.filter((record) => record.status === "deprecated")
.map((record) => {
const tokens = extractCompatTokens(record);
const references = collectReferenceFiles(files, tokens);
const references =
options.includeReferenceFiles === false
? { codeReferenceFiles: [], docReferenceFiles: [] }
: collectReferenceFiles(files, tokens);
const eligibleForRemoval = record.removeAfter
? new Date(`${record.removeAfter}T00:00:00Z`) <= today
: false;
@ -297,13 +323,11 @@ function collectCompatDebt(files: readonly string[], today = new Date()): Compat
);
}
function collectReservedSdkImports(files: readonly string[]): ReservedSdkImport[] {
function collectReservedSdkImports(files: readonly WorkspaceTextFile[]): ReservedSdkImport[] {
const reserved = new Set<string>(reservedBundledPluginSdkEntrypoints);
const pluginIds = collectBundledPluginIds();
const imports: ReservedSdkImport[] = [];
for (const file of files) {
const relativeFile = repoRelative(file);
const source = readFileSync(file, "utf8");
for (const { relativeFile, source } of files) {
for (const match of source.matchAll(PLUGIN_SDK_SPECIFIER_PATTERN)) {
const specifier = match[1];
const subpath = match[2];
@ -325,18 +349,18 @@ function collectReservedSdkImports(files: readonly string[]): ReservedSdkImport[
);
}
function collectMemoryHostBoundary(files: readonly string[]): BoundaryReport["memoryHostSdk"] {
function collectMemoryHostBoundary(
files: readonly WorkspaceTextFile[],
): BoundaryReport["memoryHostSdk"] {
const packageJson = JSON.parse(
readFileSync(resolve(REPO_ROOT, "packages/memory-host-sdk/package.json"), "utf8"),
) as { private?: boolean; exports?: Record<string, string> };
const sourceBridgeFiles: string[] = [];
const packageCoreReferenceFiles = new Set<string>();
for (const file of files) {
const relativeFile = repoRelative(file);
for (const { relativeFile, source } of files) {
if (!relativeFile.startsWith("packages/memory-host-sdk/src/")) {
continue;
}
const source = readFileSync(file, "utf8");
if (source.includes("src/memory-host-sdk/")) {
sourceBridgeFiles.push(relativeFile);
}
@ -419,12 +443,12 @@ function buildSummary(report: BoundaryReport, owner?: string): BoundaryReportSum
};
}
function buildReport(options: Pick<CliOptions, "owner"> = {}): BoundaryReport {
const files = collectWorkspaceTextFiles();
function buildReport(options: Pick<CliOptions, "owner" | "summary"> = {}): BoundaryReport {
const files = collectWorkspaceTextFileSources();
const pluginIds = collectBundledPluginIds();
const compatRecords = collectCompatDebt(files).filter((record) =>
matchesOwner(options.owner, record.owner),
);
const compatRecords = collectCompatDebt(files, new Date(), {
includeReferenceFiles: !options.summary,
}).filter((record) => matchesOwner(options.owner, record.owner));
const reservedImports = collectReservedSdkImports(files).filter(
(entry) =>
matchesOwner(options.owner, entry.owner) || matchesOwner(options.owner, entry.consumerOwner),
@ -539,35 +563,51 @@ function collectFailures(report: BoundaryReport, options: CliOptions): string[]
return failures;
}
let options: CliOptions;
try {
options = parseArgs(process.argv.slice(2));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`${message}\n\n${renderHelp()}\n`);
process.exitCode = 2;
process.exit();
export function createPluginBoundaryReport(args: readonly string[]): PluginBoundaryReportResult {
const options = parseArgs(args);
if (options.help) {
return {
stdout: `${renderHelp()}\n`,
stderr: "",
exitCode: 0,
};
}
const report = buildReport(options);
const summary = buildSummary(report, options.owner);
const body = options.json
? JSON.stringify(options.summary ? summary : report, null, 2)
: options.summary
? renderSummaryText(summary)
: renderText(report, options.owner);
const failures = collectFailures(report, options);
return {
stdout: `${body}\n`,
stderr:
failures.length > 0
? `${failures.map((failure) => `plugin-boundary-report: ${failure}`).join("\n")}\n`
: "",
exitCode: failures.length > 0 ? 1 : 0,
};
}
if (options.help) {
process.stdout.write(`${renderHelp()}\n`);
process.exit();
function runPluginBoundaryReportCli(args: readonly string[]): void {
let result: PluginBoundaryReportResult;
try {
result = createPluginBoundaryReport(args);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`${message}\n\n${renderHelp()}\n`);
process.exitCode = 2;
return;
}
process.stdout.write(result.stdout);
if (result.stderr) {
process.stderr.write(result.stderr);
}
process.exitCode = result.exitCode;
}
const report = buildReport(options);
const summary = buildSummary(report, options.owner);
if (options.json) {
process.stdout.write(`${JSON.stringify(options.summary ? summary : report, null, 2)}\n`);
} else if (options.summary) {
process.stdout.write(`${renderSummaryText(summary)}\n`);
} else {
process.stdout.write(`${renderText(report, options.owner)}\n`);
}
const failures = collectFailures(report, options);
if (failures.length > 0) {
process.stderr.write(
`${failures.map((failure) => `plugin-boundary-report: ${failure}`).join("\n")}\n`,
);
process.exitCode = 1;
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
runPluginBoundaryReportCli(process.argv.slice(2));
}

View file

@ -15,19 +15,21 @@ describe("openclaw test state", () => {
scenario: "minimal",
});
expect(state.home).toBe(path.join(state.root, "home"));
expect(state.stateDir).toBe(path.join(state.home, ".openclaw"));
expect(state.configPath).toBe(path.join(state.stateDir, "openclaw.json"));
expect(state.workspaceDir).toBe(path.join(state.home, "workspace"));
expect(state.env.HOME).toBe(state.home);
expect(state.env.OPENCLAW_HOME).toBe(state.home);
expect(state.env.OPENCLAW_STATE_DIR).toBe(state.stateDir);
expect(state.env.OPENCLAW_CONFIG_PATH).toBe(state.configPath);
expect(process.env.HOME).toBe(state.home);
expect(process.env.OPENCLAW_HOME).toBe(state.home);
expect(JSON.parse(await fs.readFile(state.configPath, "utf8"))).toEqual({});
await state.cleanup();
try {
expect(state.home).toBe(path.join(state.root, "home"));
expect(state.stateDir).toBe(path.join(state.home, ".openclaw"));
expect(state.configPath).toBe(path.join(state.stateDir, "openclaw.json"));
expect(state.workspaceDir).toBe(path.join(state.home, "workspace"));
expect(state.env.HOME).toBe(state.home);
expect(state.env.OPENCLAW_HOME).toBe(state.home);
expect(state.env.OPENCLAW_STATE_DIR).toBe(state.stateDir);
expect(state.env.OPENCLAW_CONFIG_PATH).toBe(state.configPath);
expect(process.env.HOME).toBe(state.home);
expect(process.env.OPENCLAW_HOME).toBe(state.home);
expect(JSON.parse(await fs.readFile(state.configPath, "utf8"))).toEqual({});
} finally {
await state.cleanup();
}
expect(process.env.HOME).toBe(previousHome);
expect(process.env.OPENCLAW_HOME).toBe(previousOpenClawHome);

View file

@ -1,30 +1,15 @@
import { execFileSync } from "node:child_process";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
const REPO_ROOT = resolve(import.meta.dirname, "../..");
function runBoundaryReport(...args: string[]): string {
return execFileSync(
process.execPath,
["--import", "tsx", "scripts/plugin-boundary-report.ts", ...args],
{
cwd: REPO_ROOT,
encoding: "utf8",
maxBuffer: 1024 * 1024,
},
);
}
import { createPluginBoundaryReport } from "../../scripts/plugin-boundary-report.js";
describe("plugin-boundary-report", () => {
it("emits compact CI-safe summary JSON", () => {
const output = runBoundaryReport(
const result = createPluginBoundaryReport([
"--summary",
"--json",
"--fail-on-cross-owner",
"--fail-on-unclassified-unused-reserved",
);
const summary = JSON.parse(output) as {
]);
const summary = JSON.parse(result.stdout) as {
pluginSdk?: {
crossOwnerReservedImportCount?: unknown;
unusedReservedCount?: unknown;
@ -34,6 +19,7 @@ describe("plugin-boundary-report", () => {
};
};
expect(result).toMatchObject({ exitCode: 0, stderr: "" });
expect(summary.pluginSdk?.crossOwnerReservedImportCount).toBe(0);
expect(summary.pluginSdk?.unusedReservedCount).toBe(0);
expect(["private-core-bridge", "private-package-core-integrated"]).toContain(

View file

@ -198,6 +198,7 @@ export const forcedUnitFastTestFiles = [
"src/terminal/table.test.ts",
"src/test-helpers/state-dir-env.test.ts",
"src/test-utils/env.test.ts",
"src/test-utils/openclaw-test-state.test.ts",
"src/test-utils/temp-home.test.ts",
"src/utils.test.ts",
"src/version.test.ts",