diff --git a/scripts/check-extension-wildcard-reexports.mjs b/scripts/check-extension-wildcard-reexports.mjs index 9260c55e9d9..0eaf265c1bd 100644 --- a/scripts/check-extension-wildcard-reexports.mjs +++ b/scripts/check-extension-wildcard-reexports.mjs @@ -6,13 +6,6 @@ import { fileURLToPath } from "node:url"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const GUARDED_PUBLIC_API_BARRELS = [ - "extensions/discord/api.ts", - "extensions/slack/api.ts", - "extensions/telegram/api.ts", - "extensions/whatsapp/api.ts", -]; - const LOCAL_WILDCARD_REEXPORT_PATTERN = /^\s*export\s+(?:type\s+)?\*\s+from\s+["'](?:\.{1,2}\/)/u; async function walkFiles(rootDir, predicate) { @@ -38,24 +31,11 @@ async function walkFiles(rootDir, predicate) { } async function listGuardedFiles(rootDir = repoRoot) { - const runtimeApiFiles = await walkFiles(path.join(rootDir, "extensions"), (filePath) => - filePath.endsWith(`${path.sep}runtime-api.ts`), + return walkFiles( + path.join(rootDir, "extensions"), + (filePath) => + filePath.endsWith(`${path.sep}runtime-api.ts`) || filePath.endsWith(`${path.sep}api.ts`), ); - const files = new Set(runtimeApiFiles); - for (const relativePath of GUARDED_PUBLIC_API_BARRELS) { - const filePath = path.join(rootDir, relativePath); - try { - const stat = await fs.stat(filePath); - if (stat.isFile()) { - files.add(filePath); - } - } catch (error) { - if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") { - throw error; - } - } - } - return [...files].toSorted((left, right) => left.localeCompare(right)); } export function findLocalWildcardReexports(source) { diff --git a/scripts/check-no-extension-test-core-imports.ts b/scripts/check-no-extension-test-core-imports.ts index afaed36c517..46a99b413ae 100644 --- a/scripts/check-no-extension-test-core-imports.ts +++ b/scripts/check-no-extension-test-core-imports.ts @@ -33,13 +33,26 @@ const FORBIDDEN_PATTERNS: Array<{ pattern: RegExp; hint: string }> = [ }, ]; +const FORBIDDEN_TEST_SUPPORT_PATTERNS: Array<{ pattern: RegExp; hint: string }> = [ + { + pattern: + /\b(?:import|export)\b[\s\S]*?\bfrom\s*["'](?:\.\.\/){2,}src\/(?:agents|channels|config|infra|plugins|routing|security|test-helpers|test-utils)\/[^"']+["']/, + hint: "Use openclaw/plugin-sdk/testing or a focused plugin-sdk test/runtime subpath instead of core internals.", + }, +]; + function isExtensionTestFile(filePath: string): boolean { return /\.test\.[cm]?[jt]sx?$/u.test(filePath) || /\.e2e\.test\.[cm]?[jt]sx?$/u.test(filePath); } +function isExtensionTestSupportFile(filePath: string): boolean { + return /(?:^|[/\\])test-support(?:[/\\]|$)/u.test(filePath) && /\.[cm]?[jt]sx?$/u.test(filePath); +} + function collectExtensionTestFiles(rootDir: string): string[] { return collectFilesSync(rootDir, { - includeFile: (filePath) => isExtensionTestFile(filePath), + includeFile: (filePath) => + isExtensionTestFile(filePath) || isExtensionTestSupportFile(filePath), }); } @@ -50,7 +63,10 @@ function main() { for (const file of files) { const content = fs.readFileSync(file, "utf8"); - for (const rule of FORBIDDEN_PATTERNS) { + const rules = isExtensionTestSupportFile(file) + ? [...FORBIDDEN_PATTERNS, ...FORBIDDEN_TEST_SUPPORT_PATTERNS] + : FORBIDDEN_PATTERNS; + for (const rule of rules) { if (!rule.pattern.test(content)) { continue; } @@ -70,7 +86,7 @@ function main() { } console.log( - `OK: extension test files avoid direct core test/internal imports (${files.length} checked).`, + `OK: extension test files and support helpers avoid direct core test/internal imports (${files.length} checked).`, ); } diff --git a/src/plugin-sdk/entrypoints.ts b/src/plugin-sdk/entrypoints.ts index 7fee9ef60a6..d3079fc955a 100644 --- a/src/plugin-sdk/entrypoints.ts +++ b/src/plugin-sdk/entrypoints.ts @@ -4,6 +4,8 @@ export const pluginSdkEntrypoints = [...pluginSdkEntryList]; export const pluginSdkSubpaths = pluginSdkEntrypoints.filter((entry) => entry !== "index"); +// Transitional compatibility/helper surfaces owned by their matching bundled plugin. +// Cross-owner extension imports are blocked by the package contract guardrails. export const reservedBundledPluginSdkEntrypoints = [ "bluebubbles", "bluebubbles-policy", @@ -58,6 +60,8 @@ export const reservedBundledPluginSdkEntrypoints = [ "zalouser", ] as const; +// Supported SDK facades backed by bundled plugins. These are intentionally public +// until they move to generic, plugin-neutral contracts. export const supportedBundledFacadeSdkEntrypoints = [ "lmstudio", "lmstudio-runtime", @@ -66,6 +70,7 @@ export const supportedBundledFacadeSdkEntrypoints = [ "tts-runtime", ] as const; +// Plugin-owned surfaces that are intentionally public and documented for third-party plugins. export const publicPluginOwnedSdkEntrypoints = [ "browser-config", "image-generation-core", diff --git a/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts b/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts index 9e4a00e726e..0ea2faafcfc 100644 --- a/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts +++ b/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts @@ -242,14 +242,6 @@ function collectCrossOwnerReservedSdkImports(): Array<{ for (const file of collectExtensionFiles(resolve(REPO_ROOT, "extensions"))) { const repoRelativePath = relative(REPO_ROOT, file).replaceAll("\\", "/"); - if ( - /(?:^|\/)(?:__tests__|tests|test-support)(?:\/|$)/.test(repoRelativePath) || - /(?:^|\/)test-support\.[cm]?tsx?$/.test(repoRelativePath) || - /\.test-support\.[cm]?tsx?$/.test(repoRelativePath) || - /\.test\.[cm]?tsx?$/.test(repoRelativePath) - ) { - continue; - } const pluginId = repoRelativePath.split("/")[1]; const source = readFileSync(file, "utf8"); for (const match of source.matchAll(importPattern)) {