test: guard broad plugin resolver fixtures

This commit is contained in:
Peter Steinberger
2026-04-29 06:45:57 +01:00
parent 83df409d94
commit 358b4f24cd
4 changed files with 63 additions and 0 deletions

View File

@@ -391,6 +391,10 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
- Runs in CI - Runs in CI
- No real keys required - No real keys required
- Should be fast and stable - 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.
<AccordionGroup> <AccordionGroup>
<Accordion title="Projects, shards, and scoped lanes"> <Accordion title="Projects, shards, and scoped lanes">

View File

@@ -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 `api.ts` or `runtime-api.ts` plus generic SDK capabilities. Do not add a
provider-named `src/plugin-sdk/<id>.ts` seam just to make core aware of a provider-named `src/plugin-sdk/<id>.ts` seam just to make core aware of a
bundled channel's private helpers. 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. - For provider work, prefer family-level seams over provider-specific seams.
Shared helpers should describe a reusable behavior such as replay policy, Shared helpers should describe a reusable behavior such as replay policy,
tool-schema compat, payload normalization, stream-wrapper composition, or tool-schema compat, payload normalization, stream-wrapper composition, or

View File

@@ -75,6 +75,9 @@ assembly, and contract enforcement.
- If setup, discovery, or doctor flows need plugin runtime, make that need - 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 explicit and narrow. Do not let cold control-plane paths quietly import broad
runtime surfaces. 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 ## Verification

View File

@@ -9,6 +9,12 @@ const ALLOWED_EXTENSION_PUBLIC_SURFACE_BASENAMES = new Set(
GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES, GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES,
); );
const CHANNEL_CONTRACT_TEST_HELPERS_PREFIX = "src/channels/plugins/contracts/test-helpers/"; 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 = const ROOTDIR_BOUNDARY_CANARY_RE =
/(^|\/)__rootdir_boundary_canary__\.(?:[cm]?ts|[cm]?js|tsx|jsx)$/u; /(^|\/)__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; 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 { function isAllowedCoreContractSuite(file: string, imports: readonly string[]): boolean {
return ( return (
file.startsWith("src/channels/plugins/contracts/") && file.startsWith("src/channels/plugins/contracts/") &&
@@ -190,6 +231,18 @@ describe("non-extension test boundaries", () => {
expect(offenders).toEqual([]); 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", () => { it("keeps bundled channel security collector coverage under extension tests", () => {
const files = [...walk(path.join(repoRoot, "src")), ...walk(path.join(repoRoot, "test"))] const files = [...walk(path.join(repoRoot, "src")), ...walk(path.join(repoRoot, "test"))]
.filter((file) => !file.startsWith(BUNDLED_PLUGIN_PATH_PREFIX)) .filter((file) => !file.startsWith(BUNDLED_PLUGIN_PATH_PREFIX))