Files
openclaw/test/extension-test-boundary.test.ts
2026-04-29 06:46:02 +01:00

319 lines
11 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { BUNDLED_PLUGIN_PATH_PREFIX } from "openclaw/plugin-sdk/test-fixtures";
import { describe, expect, it } from "vitest";
import { GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES } from "../src/plugin-sdk/test-helpers/public-artifacts.js";
const repoRoot = path.resolve(import.meta.dirname, "..");
const ALLOWED_EXTENSION_PUBLIC_SURFACE_BASENAMES = new Set(
GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES,
);
const CHANNEL_CONTRACT_TEST_HELPERS_PREFIX = "src/channels/plugins/contracts/test-helpers/";
const BUNDLED_PLUGIN_RESOLVER_TEST_FILES = [
"src/plugin-sdk/facade-loader.test.ts",
"src/plugins/public-surface-loader.test.ts",
"src/plugins/public-surface-runtime.test.ts",
] as const;
const BROAD_PUBLIC_SOURCE_ARTIFACT_BASENAMES = new Set(["api.js", "runtime-api.js"]);
const ROOTDIR_BOUNDARY_CANARY_RE =
/(^|\/)__rootdir_boundary_canary__\.(?:[cm]?ts|[cm]?js|tsx|jsx)$/u;
function walk(dir: string, entries: string[] = []): string[] {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git") {
continue;
}
walk(fullPath, entries);
continue;
}
if (!entry.name.endsWith(".test.ts") && !entry.name.endsWith(".test.tsx")) {
continue;
}
entries.push(path.relative(repoRoot, fullPath).replaceAll(path.sep, "/"));
}
return entries;
}
function walkCode(dir: string, entries: string[] = []): string[] {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git") {
continue;
}
walkCode(fullPath, entries);
continue;
}
if (!entry.name.endsWith(".ts") && !entry.name.endsWith(".tsx")) {
continue;
}
const relativePath = path.relative(repoRoot, fullPath).replaceAll(path.sep, "/");
if (ROOTDIR_BOUNDARY_CANARY_RE.test(relativePath)) {
continue;
}
entries.push(relativePath);
}
return entries;
}
function findExtensionImports(source: string): string[] {
return [
...source.matchAll(/from\s+["']((?:\.\.\/)+extensions\/[^"']+)["']/g),
...source.matchAll(/import\(\s*["']((?:\.\.\/)+extensions\/[^"']+)["']\s*\)/g),
].map((match) => match[1]);
}
function isAllowedExtensionPublicImport(specifier: string): boolean {
return /(?:^|\/)extensions\/[^/]+\/(?:api|index|runtime-api|setup-entry|login-qr-api)\.js$/u.test(
specifier,
);
}
function findPluginSdkImports(source: string): string[] {
return [
...source.matchAll(/from\s+["']((?:\.\.\/)+plugin-sdk\/[^"']+)["']/g),
...source.matchAll(/import\(\s*["']((?:\.\.\/)+plugin-sdk\/[^"']+)["']\s*\)/g),
].map((match) => match[1]);
}
function findBundledPluginPublicSurfaceImports(source: string): string[] {
return [
...source.matchAll(/from\s+["'](?:\.\.\/)+test-utils\/bundled-plugin-public-surface\.js["']/g),
...source.matchAll(
/import\(\s*["'](?:\.\.\/)+test-utils\/bundled-plugin-public-surface\.js["']\s*\)/g,
),
].map((match) => match[0]);
}
function findRelativeSrcImports(source: string): string[] {
return [
...source.matchAll(/from\s+["']((?:\.\.?\/)+src\/[^"']+)["']/g),
...source.matchAll(/import\(\s*["']((?:\.\.?\/)+src\/[^"']+)["']\s*\)/g),
...source.matchAll(/vi\.(?:mock|doMock)\s*\(\s*["']((?:\.\.?\/)+src\/[^"']+)["']/g),
].map((match) => match[1]);
}
function getImportBasename(importPath: string): string {
return importPath.split("/").at(-1) ?? importPath;
}
function collectBundledPluginIds(): Set<string> {
return new Set(
fs
.readdirSync(path.join(repoRoot, "extensions"), { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name),
);
}
function getLineNumber(source: string, index: number): number {
return source.slice(0, index).split("\n").length;
}
function findRealBroadSourceApiResolverReferences(
source: string,
pluginIds: Set<string>,
): string[] {
const offenders: string[] = [];
for (const match of source.matchAll(/\{[^{}]*\bdirName:\s*["'][^"']+["'][^{}]*\}/g)) {
const objectLiteral = match[0];
const dirName = objectLiteral.match(/\bdirName:\s*["']([^"']+)["']/)?.[1];
const artifactBasename = objectLiteral.match(/\bartifactBasename:\s*["']([^"']+)["']/)?.[1];
if (
dirName &&
artifactBasename &&
pluginIds.has(dirName) &&
BROAD_PUBLIC_SOURCE_ARTIFACT_BASENAMES.has(artifactBasename)
) {
offenders.push(`${dirName}/${artifactBasename}:${getLineNumber(source, match.index ?? 0)}`);
}
}
return offenders;
}
function isAllowedCoreContractSuite(file: string, imports: readonly string[]): boolean {
return (
file.startsWith("src/channels/plugins/contracts/") &&
file.endsWith(".contract.test.ts") &&
imports.every((entry) =>
ALLOWED_EXTENSION_PUBLIC_SURFACE_BASENAMES.has(getImportBasename(entry)),
)
);
}
describe("non-extension test boundaries", () => {
it("keeps plugin-owned behavior suites under the bundled plugin tree", () => {
const testFiles = [
...walk(path.join(repoRoot, "src")),
...walk(path.join(repoRoot, "test")),
...walk(path.join(repoRoot, "packages")),
].filter(
(file) =>
!file.startsWith(BUNDLED_PLUGIN_PATH_PREFIX) &&
!file.startsWith("test/helpers/") &&
!file.startsWith("ui/"),
);
const offenders = testFiles
.map((file) => {
const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
const imports = findExtensionImports(source).filter(
(specifier) => !isAllowedExtensionPublicImport(specifier),
);
if (imports.length === 0) {
return null;
}
if (isAllowedCoreContractSuite(file, imports)) {
return null;
}
return {
file,
imports,
};
})
.filter((value): value is { file: string; imports: string[] } => value !== null);
expect(offenders).toEqual([]);
});
it("keeps extension-owned onboard helper coverage out of the core onboard auth suite", () => {
const bannedPluginSdkModules = new Set<string>([
"../plugin-sdk/litellm.js",
"../plugin-sdk/minimax.js",
"../plugin-sdk/mistral.js",
"../plugin-sdk/opencode-go.js",
"../plugin-sdk/opencode.js",
"../plugin-sdk/openrouter.js",
"../plugin-sdk/synthetic.js",
"../plugin-sdk/xai.js",
"../plugin-sdk/xiaomi.js",
]);
const file = "src/commands/onboard-auth.test.ts";
const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
const imports = findPluginSdkImports(source).filter((entry) =>
bannedPluginSdkModules.has(entry),
);
expect(imports).toEqual([]);
});
it("keeps bundled plugin public-surface imports out of core source", () => {
const files = walkCode(path.join(repoRoot, "src")).filter(
(file) => !file.startsWith(CHANNEL_CONTRACT_TEST_HELPERS_PREFIX),
);
const offenders = files.filter((file) => {
const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
return findBundledPluginPublicSurfaceImports(source).length > 0;
});
expect(offenders).toEqual([]);
});
it("keeps bundled plugin sync test-api loaders out of core tests", () => {
const files = [
...walkCode(path.join(repoRoot, "src")),
...walkCode(path.join(repoRoot, "test")),
]
.filter((file) => !file.startsWith(BUNDLED_PLUGIN_PATH_PREFIX))
.filter((file) => !file.startsWith(CHANNEL_CONTRACT_TEST_HELPERS_PREFIX))
.filter((file) => !file.startsWith("test/helpers/"))
.filter((file) => file !== "test/extension-test-boundary.test.ts");
const offenders = files.filter((file) => {
const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
return source.includes("loadBundledPluginTestApiSync(");
});
expect(offenders).toEqual([]);
});
it("keeps resolver tests on generated fixtures for broad bundled plugin source APIs", () => {
const bundledPluginIds = collectBundledPluginIds();
const offenders = BUNDLED_PLUGIN_RESOLVER_TEST_FILES.flatMap((file) => {
const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
return findRealBroadSourceApiResolverReferences(source, bundledPluginIds).map(
(reference) => `${file}: ${reference}`,
);
});
expect(offenders).toEqual([]);
});
it("keeps bundled channel security collector coverage under extension tests", () => {
const files = [...walk(path.join(repoRoot, "src")), ...walk(path.join(repoRoot, "test"))]
.filter((file) => !file.startsWith(BUNDLED_PLUGIN_PATH_PREFIX))
.filter((file) => !file.startsWith("test/helpers/"))
.filter((file) => file !== "test/extension-test-boundary.test.ts");
const offenders = files.filter((file) => {
const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
return (
source.includes("test/helpers/channels/security-audit-contract.js") ||
source.includes("src/channels/plugins/contracts/test-helpers/security-audit-contract.js")
);
});
expect(offenders).toEqual([]);
});
it("keeps extension channel contract helpers on the public testing surface", () => {
const files = walkCode(path.join(repoRoot, "extensions"));
const offenders = files.filter((file) => {
const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
return source.includes("src/channels/plugins/contracts/test-helpers/");
});
expect(offenders).toEqual([]);
});
it("keeps extension tests off legacy broad testing barrels and repo helper bridges", () => {
const bannedPatterns = [
/["']openclaw\/plugin-sdk\/testing["']/u,
/["']openclaw\/plugin-sdk\/test-utils["']/u,
/["'](?:\.\.\/)+(?:test\/helpers\/channels\/)[^"']+["']/u,
/["'](?:\.\.\/)+(?:src\/channels\/plugins\/contracts\/test-helpers\/)[^"']+["']/u,
/["'](?:\.\.\/)+(?:test\/helpers\/plugins\/)[^"']+["']/u,
/["'](?:\.\.\/)+(?:test\/helpers\/)[^"']+["']/u,
];
const files = walkCode(path.join(repoRoot, "extensions"));
const offenders = files.filter((file) => {
const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
return bannedPatterns.some((pattern) => pattern.test(source));
});
expect(offenders).toEqual([]);
});
it("keeps extension root test-support helpers from reaching into private src trees", () => {
const files = walkCode(path.join(repoRoot, "extensions")).filter((file) =>
/^extensions\/[^/]+\/test-support(?:\.ts|\/)/u.test(file),
);
const offenders = files
.map((file) => {
const imports = findRelativeSrcImports(fs.readFileSync(path.join(repoRoot, file), "utf8"));
return imports.length === 0 ? null : { file, imports };
})
.filter((entry): entry is { file: string; imports: string[] } => entry !== null);
expect(offenders).toEqual([]);
});
it("keeps bundled extension sources off deprecated channel config schema aliases", () => {
const files = walkCode(path.join(repoRoot, "extensions"));
const offenders = files.filter((file) => {
const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
return source.includes("openclaw/plugin-sdk/channel-config-schema-legacy");
});
expect(offenders).toEqual([]);
});
});