mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 18:04:45 +00:00
fix(test): avoid walking plugin sdk package guardrails
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import { dirname, join, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
deprecatedBarrelPluginSdkEntrypoints,
|
||||
deprecatedPublicPluginSdkEntrypoints,
|
||||
@@ -55,9 +56,57 @@ const MATRIX_RUNTIME_DEPS = [
|
||||
"matrix-js-sdk",
|
||||
"music-metadata",
|
||||
] as const;
|
||||
const trackedFilesByRoot = new Map<string, readonly string[] | null>();
|
||||
|
||||
function toRepoRelativePath(filePath: string): string {
|
||||
return relative(REPO_ROOT, filePath).replaceAll("\\", "/");
|
||||
}
|
||||
|
||||
function isSkippedTrackedPath(repoRelativePath: string): boolean {
|
||||
return repoRelativePath
|
||||
.split("/")
|
||||
.some((part) => part === "dist" || part === "node_modules" || part === ".git");
|
||||
}
|
||||
|
||||
function isCodeFile(filePath: string): boolean {
|
||||
return /\.(?:[cm]?ts|tsx|mts|cts)$/.test(filePath);
|
||||
}
|
||||
|
||||
function listTrackedFiles(root: string): string[] | null {
|
||||
const relativeRoot = toRepoRelativePath(root);
|
||||
if (!relativeRoot || relativeRoot.startsWith("..")) {
|
||||
return null;
|
||||
}
|
||||
if (trackedFilesByRoot.has(relativeRoot)) {
|
||||
const files = trackedFilesByRoot.get(relativeRoot);
|
||||
return files ? [...files] : null;
|
||||
}
|
||||
const result = spawnSync("git", ["ls-files", "--", relativeRoot], {
|
||||
cwd: REPO_ROOT,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
trackedFilesByRoot.set(relativeRoot, null);
|
||||
return null;
|
||||
}
|
||||
const files = result.stdout
|
||||
.split("\n")
|
||||
.map((line) => line.trim().replaceAll("\\", "/"))
|
||||
.filter((line) => line.length > 0 && !isSkippedTrackedPath(line))
|
||||
.map((line) => resolve(REPO_ROOT, line))
|
||||
.toSorted();
|
||||
trackedFilesByRoot.set(relativeRoot, files);
|
||||
return [...files];
|
||||
}
|
||||
|
||||
function listTrackedCodeFiles(root: string): string[] | null {
|
||||
const files = listTrackedFiles(root);
|
||||
return files?.filter(isCodeFile) ?? null;
|
||||
}
|
||||
|
||||
function collectPluginSdkPackageExports(): string[] {
|
||||
const packageJson = JSON.parse(readFileSync(resolve(REPO_ROOT, "package.json"), "utf8")) as {
|
||||
const packageJson = JSON.parse(fs.readFileSync(resolve(REPO_ROOT, "package.json"), "utf8")) as {
|
||||
exports?: Record<string, unknown>;
|
||||
};
|
||||
const exports = packageJson.exports ?? {};
|
||||
@@ -78,7 +127,7 @@ function collectPluginSdkPackageExports(): string[] {
|
||||
function collectPluginSdkSubpathReferences() {
|
||||
const references: Array<{ file: string; subpath: string }> = [];
|
||||
for (const file of PUBLIC_CONTRACT_REFERENCE_FILES) {
|
||||
const source = readFileSync(resolve(REPO_ROOT, file), "utf8");
|
||||
const source = fs.readFileSync(resolve(REPO_ROOT, file), "utf8");
|
||||
for (const match of source.matchAll(PLUGIN_SDK_SUBPATH_PATTERN)) {
|
||||
const subpath = match[1];
|
||||
if (!subpath) {
|
||||
@@ -91,7 +140,7 @@ function collectPluginSdkSubpathReferences() {
|
||||
}
|
||||
|
||||
function collectDocumentedSdkSubpaths(): Set<string> {
|
||||
const source = readFileSync(resolve(REPO_ROOT, SDK_SUBPATH_DOC_FILE), "utf8");
|
||||
const source = fs.readFileSync(resolve(REPO_ROOT, SDK_SUBPATH_DOC_FILE), "utf8");
|
||||
return new Set(
|
||||
[...source.matchAll(/`plugin-sdk\/([a-z0-9][a-z0-9-]*)`/g)]
|
||||
.map((match) => match[1])
|
||||
@@ -100,7 +149,20 @@ function collectDocumentedSdkSubpaths(): Set<string> {
|
||||
}
|
||||
|
||||
function collectBundledPluginIds(): string[] {
|
||||
return readdirSync(resolve(REPO_ROOT, "extensions"), { withFileTypes: true })
|
||||
const trackedFiles = listTrackedFiles(resolve(REPO_ROOT, "extensions"));
|
||||
if (trackedFiles) {
|
||||
return [
|
||||
...new Set(
|
||||
trackedFiles
|
||||
.map((file) => toRepoRelativePath(file).split("/"))
|
||||
.filter((parts) => parts.length > 2)
|
||||
.map((parts) => parts[1])
|
||||
.filter((pluginId): pluginId is string => Boolean(pluginId)),
|
||||
),
|
||||
].toSorted((a, b) => b.length - a.length || a.localeCompare(b));
|
||||
}
|
||||
return fs
|
||||
.readdirSync(resolve(REPO_ROOT, "extensions"), { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name)
|
||||
.toSorted((a, b) => b.length - a.length || a.localeCompare(b));
|
||||
@@ -142,7 +204,7 @@ function collectBundledFacadeSdkEntrypoints(): string[] {
|
||||
const entrypoints: string[] = [];
|
||||
for (const entrypoint of pluginSdkEntrypoints) {
|
||||
const filePath = resolve(REPO_ROOT, "src/plugin-sdk", `${entrypoint}.ts`);
|
||||
const source = readFileSync(filePath, "utf8");
|
||||
const source = fs.readFileSync(filePath, "utf8");
|
||||
if (BUNDLED_PLUGIN_FACADE_LOADER_PATTERN.test(source)) {
|
||||
entrypoints.push(entrypoint);
|
||||
}
|
||||
@@ -154,7 +216,7 @@ function collectPrivateBundledSdkSurfaceEntrypoints(): string[] {
|
||||
const entrypoints: string[] = [];
|
||||
for (const entrypoint of pluginSdkEntrypoints) {
|
||||
const filePath = resolve(REPO_ROOT, "src/plugin-sdk", `${entrypoint}.ts`);
|
||||
const source = readFileSync(filePath, "utf8");
|
||||
const source = fs.readFileSync(filePath, "utf8");
|
||||
if (PRIVATE_BUNDLED_SDK_SURFACE_PATTERN.test(source)) {
|
||||
entrypoints.push(entrypoint);
|
||||
}
|
||||
@@ -165,7 +227,7 @@ function collectPrivateBundledSdkSurfaceEntrypoints(): string[] {
|
||||
function collectGenericCoreOwnerNameLeaks(): Array<{ file: string; match: string }> {
|
||||
const leaks: Array<{ file: string; match: string }> = [];
|
||||
for (const file of GENERIC_CORE_HELPER_FILES) {
|
||||
const source = readFileSync(resolve(REPO_ROOT, file), "utf8");
|
||||
const source = fs.readFileSync(resolve(REPO_ROOT, file), "utf8");
|
||||
for (const match of source.matchAll(GENERIC_CORE_PLUGIN_OWNER_NAME_PATTERN)) {
|
||||
const ownerName = match[0];
|
||||
if (!ownerName) {
|
||||
@@ -182,7 +244,7 @@ function readRootPackageJson(): {
|
||||
optionalDependencies?: Record<string, string>;
|
||||
files?: string[];
|
||||
} {
|
||||
return JSON.parse(readFileSync(resolve(REPO_ROOT, "package.json"), "utf8")) as {
|
||||
return JSON.parse(fs.readFileSync(resolve(REPO_ROOT, "package.json"), "utf8")) as {
|
||||
dependencies?: Record<string, string>;
|
||||
optionalDependencies?: Record<string, string>;
|
||||
files?: string[];
|
||||
@@ -193,7 +255,9 @@ function readMatrixPackageJson(): {
|
||||
dependencies?: Record<string, string>;
|
||||
optionalDependencies?: Record<string, string>;
|
||||
} {
|
||||
return JSON.parse(readFileSync(resolve(REPO_ROOT, "extensions/matrix/package.json"), "utf8")) as {
|
||||
return JSON.parse(
|
||||
fs.readFileSync(resolve(REPO_ROOT, "extensions/matrix/package.json"), "utf8"),
|
||||
) as {
|
||||
dependencies?: Record<string, string>;
|
||||
optionalDependencies?: Record<string, string>;
|
||||
};
|
||||
@@ -210,7 +274,12 @@ function collectRuntimeDependencySpecs(packageJson: {
|
||||
}
|
||||
|
||||
function collectExtensionFiles(dir: string): string[] {
|
||||
const entries = readdirSync(dir, { withFileTypes: true });
|
||||
const trackedFiles = listTrackedCodeFiles(dir);
|
||||
if (trackedFiles) {
|
||||
return trackedFiles;
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
const files: string[] = [];
|
||||
for (const entry of entries) {
|
||||
if (entry.name === "dist" || entry.name === "node_modules") {
|
||||
@@ -252,7 +321,7 @@ function collectExtensionCoreImportLeaks(): Array<{ file: string; specifier: str
|
||||
}
|
||||
const extensionRootMatch = /^(.*?\/extensions\/[^/]+)/.exec(file.replaceAll("\\", "/"));
|
||||
const extensionRoot = extensionRootMatch?.[1];
|
||||
const source = readFileSync(file, "utf8");
|
||||
const source = fs.readFileSync(file, "utf8");
|
||||
for (const match of source.matchAll(importPattern)) {
|
||||
const specifier = match[1];
|
||||
if (!specifier) {
|
||||
@@ -283,7 +352,7 @@ function collectExtensionTestHelperImportLeaks(): Array<{ file: string; specifie
|
||||
if (isExtensionTestOrSupportPath(repoRelativePath)) {
|
||||
continue;
|
||||
}
|
||||
const source = readFileSync(file, "utf8");
|
||||
const source = fs.readFileSync(file, "utf8");
|
||||
for (const importPattern of importPatterns) {
|
||||
for (const match of source.matchAll(importPattern)) {
|
||||
const specifier = match[1];
|
||||
@@ -309,7 +378,7 @@ function collectDeprecatedExtensionSdkImports(): Array<{ file: string; specifier
|
||||
];
|
||||
for (const file of collectExtensionFiles(resolve(REPO_ROOT, "extensions"))) {
|
||||
const repoRelativePath = relative(REPO_ROOT, file).replaceAll("\\", "/");
|
||||
const source = readFileSync(file, "utf8");
|
||||
const source = fs.readFileSync(file, "utf8");
|
||||
for (const importPattern of importPatterns) {
|
||||
for (const match of source.matchAll(importPattern)) {
|
||||
const specifier = match[1];
|
||||
@@ -327,8 +396,13 @@ function collectDeprecatedExtensionSdkImports(): Array<{ file: string; specifier
|
||||
}
|
||||
|
||||
function collectCodeFiles(dir: string): string[] {
|
||||
const trackedFiles = listTrackedCodeFiles(dir);
|
||||
if (trackedFiles) {
|
||||
return trackedFiles;
|
||||
}
|
||||
|
||||
const files: string[] = [];
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.name === "dist" || entry.name === "node_modules" || entry.name === ".git") {
|
||||
continue;
|
||||
}
|
||||
@@ -358,7 +432,7 @@ function collectDeprecatedTestBarrelImports(): Array<{ file: string; specifier:
|
||||
if (DEPRECATED_TEST_BARREL_ALLOWED_REFERENCE_FILES.has(repoRelativePath)) {
|
||||
continue;
|
||||
}
|
||||
const source = readFileSync(file, "utf8");
|
||||
const source = fs.readFileSync(file, "utf8");
|
||||
for (const importPattern of importPatterns) {
|
||||
for (const match of source.matchAll(importPattern)) {
|
||||
const specifier = match[1];
|
||||
@@ -377,7 +451,7 @@ function collectDeprecatedTestBarrelImports(): Array<{ file: string; specifier:
|
||||
}
|
||||
|
||||
function collectDeprecatedPackageTestingBridgeDrift(): string[] {
|
||||
const source = readFileSync(
|
||||
const source = fs.readFileSync(
|
||||
resolve(REPO_ROOT, "packages/plugin-sdk/src/testing.ts"),
|
||||
"utf8",
|
||||
).trim();
|
||||
@@ -422,7 +496,7 @@ function collectWorkspaceCodeFiles(): string[] {
|
||||
const files: string[] = [];
|
||||
for (const root of ["src", "test", "extensions", "packages", "scripts"]) {
|
||||
const dir = resolve(REPO_ROOT, root);
|
||||
if (existsSync(dir)) {
|
||||
if (fs.existsSync(dir)) {
|
||||
files.push(...collectCodeFiles(dir));
|
||||
}
|
||||
}
|
||||
@@ -443,7 +517,7 @@ function collectUnusedExtensionTestApiExports(): Array<{ file: string; exportNam
|
||||
const exportNames = new Set<string>();
|
||||
|
||||
for (const file of testApiFiles) {
|
||||
const source = readFileSync(file, "utf8");
|
||||
const source = fs.readFileSync(file, "utf8");
|
||||
const namedExports = parseTestApiNamedExports(source);
|
||||
testApiExports.set(file, namedExports);
|
||||
for (const exportName of namedExports) {
|
||||
@@ -463,7 +537,7 @@ function collectUnusedExtensionTestApiExports(): Array<{ file: string; exportNam
|
||||
const selfReferenceCounts = new Map<string, Map<string, number>>();
|
||||
|
||||
for (const file of workspaceCodeFiles) {
|
||||
const source = readFileSync(file, "utf8");
|
||||
const source = fs.readFileSync(file, "utf8");
|
||||
const selfCounts = testApiExports.has(file) ? new Map<string, number>() : undefined;
|
||||
for (const match of source.matchAll(identifierPattern)) {
|
||||
const exportName = match[1];
|
||||
@@ -510,7 +584,7 @@ function collectCrossOwnerReservedSdkImports(): Array<{
|
||||
for (const file of collectExtensionFiles(resolve(REPO_ROOT, "extensions"))) {
|
||||
const repoRelativePath = relative(REPO_ROOT, file).replaceAll("\\", "/");
|
||||
const pluginId = repoRelativePath.split("/")[1];
|
||||
const source = readFileSync(file, "utf8");
|
||||
const source = fs.readFileSync(file, "utf8");
|
||||
for (const match of source.matchAll(importPattern)) {
|
||||
const subpath = match[1];
|
||||
if (!subpath || !reserved.has(subpath)) {
|
||||
@@ -541,7 +615,7 @@ function collectReservedSdkSubpathImports(): string[] {
|
||||
|
||||
for (const root of ["src", "test", "extensions", "packages", "scripts"]) {
|
||||
for (const file of collectCodeFiles(resolve(REPO_ROOT, root))) {
|
||||
const source = readFileSync(file, "utf8");
|
||||
const source = fs.readFileSync(file, "utf8");
|
||||
for (const importPattern of importPatterns) {
|
||||
for (const match of source.matchAll(importPattern)) {
|
||||
const subpath = match[1];
|
||||
@@ -557,7 +631,7 @@ function collectReservedSdkSubpathImports(): string[] {
|
||||
}
|
||||
|
||||
function hasWildcardReexport(entrypoint: string): boolean {
|
||||
const source = readFileSync(resolve(REPO_ROOT, "src/plugin-sdk", `${entrypoint}.ts`), "utf8");
|
||||
const source = fs.readFileSync(resolve(REPO_ROOT, "src/plugin-sdk", `${entrypoint}.ts`), "utf8");
|
||||
return /^\s*export\s+(?:type\s+)?\*\s+from\s+["'][^"']+["']/mu.test(source);
|
||||
}
|
||||
|
||||
@@ -574,7 +648,7 @@ function collectExtensionProductionSdkSubpathImports(subpaths: ReadonlySet<strin
|
||||
if (isExtensionTestOrSupportPath(repoRelativePath)) {
|
||||
continue;
|
||||
}
|
||||
const source = readFileSync(file, "utf8");
|
||||
const source = fs.readFileSync(file, "utf8");
|
||||
for (const importPattern of importPatterns) {
|
||||
for (const match of source.matchAll(importPattern)) {
|
||||
const subpath = match[1];
|
||||
@@ -589,6 +663,22 @@ function collectExtensionProductionSdkSubpathImports(subpaths: ReadonlySet<strin
|
||||
}
|
||||
|
||||
describe("plugin-sdk package contract guardrails", () => {
|
||||
it("lists package guardrail scan inputs from git without walking roots", () => {
|
||||
const readDir = vi.spyOn(fs, "readdirSync");
|
||||
try {
|
||||
const pluginIds = collectBundledPluginIds();
|
||||
const extensionFiles = collectExtensionFiles(resolve(REPO_ROOT, "extensions"));
|
||||
const workspaceFiles = collectWorkspaceCodeFiles();
|
||||
|
||||
expect(pluginIds.length).toBeGreaterThan(0);
|
||||
expect(extensionFiles.length).toBeGreaterThan(0);
|
||||
expect(workspaceFiles.length).toBeGreaterThan(extensionFiles.length);
|
||||
expect(readDir).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
readDir.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps plugin-sdk entrypoint metadata unique", () => {
|
||||
const counts = new Map<string, number>();
|
||||
for (const entrypoint of pluginSdkEntrypoints) {
|
||||
|
||||
Reference in New Issue
Block a user