diff --git a/docs/help/testing.md b/docs/help/testing.md index ce7d28a64b6..219cdacfa60 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -391,6 +391,10 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost): - Runs in CI - No real keys required - Should be fast and stable + - Resolver and public-surface loader tests must prove broad `api.js` and + `runtime-api.js` fallback behavior with generated tiny plugin fixtures, not + real bundled plugin source APIs. Real plugin API loads belong in + plugin-owned contract/integration suites. diff --git a/src/plugin-sdk/AGENTS.md b/src/plugin-sdk/AGENTS.md index 7968f928d45..66017d5c619 100644 --- a/src/plugin-sdk/AGENTS.md +++ b/src/plugin-sdk/AGENTS.md @@ -45,6 +45,9 @@ can affect bundled plugins and third-party plugins. `api.ts` or `runtime-api.ts` plus generic SDK capabilities. Do not add a provider-named `src/plugin-sdk/.ts` seam just to make core aware of a bundled channel's private helpers. +- Resolver/facade loader tests are the exception to broad source API coverage: + use generated tiny plugin fixtures for `api.js` / `runtime-api.js` fallback + behavior. Do not point those tests at real bundled plugin source APIs. - For provider work, prefer family-level seams over provider-specific seams. Shared helpers should describe a reusable behavior such as replay policy, tool-schema compat, payload normalization, stream-wrapper composition, or diff --git a/src/plugins/AGENTS.md b/src/plugins/AGENTS.md index ce08e832e51..ff5bec54c5a 100644 --- a/src/plugins/AGENTS.md +++ b/src/plugins/AGENTS.md @@ -75,6 +75,9 @@ assembly, and contract enforcement. - If setup, discovery, or doctor flows need plugin runtime, make that need explicit and narrow. Do not let cold control-plane paths quietly import broad runtime surfaces. +- Resolver and public-surface loader tests must use generated tiny plugin + fixtures for broad `api.js` / `runtime-api.js` fallback behavior. Do not point + those tests at real bundled plugin source APIs just to prove path resolution. ## Verification diff --git a/test/extension-test-boundary.test.ts b/test/extension-test-boundary.test.ts index 4833f926973..dd13d1196a8 100644 --- a/test/extension-test-boundary.test.ts +++ b/test/extension-test-boundary.test.ts @@ -9,6 +9,12 @@ 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; @@ -93,6 +99,41 @@ function getImportBasename(importPath: string): string { return importPath.split("/").at(-1) ?? importPath; } +function collectBundledPluginIds(): Set { + 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[] { + 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/") && @@ -190,6 +231,18 @@ describe("non-extension test boundaries", () => { 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))