From 129b996a4e57b16659e041868c2f09c7a04fb3cf Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Apr 2026 03:52:16 +0100 Subject: [PATCH] refactor: tighten extension test support boundaries --- .../.generated/plugin-sdk-api-baseline.sha256 | 4 +- docs/plugins/sdk-testing.md | 3 + extensions/telegram/test-api.ts | 39 +++++++++++ extensions/telegram/test-support.ts | 39 ----------- .../zalo/src/monitor.image.polling.test.ts | 4 +- .../src/monitor.pairing.lifecycle.test.ts | 4 +- .../src/monitor.polling.media-reply.test.ts | 4 +- .../src/monitor.reply-once.lifecycle.test.ts | 4 +- extensions/zalo/src/monitor.webhook.test.ts | 14 ++-- .../test-support/lifecycle-test-support.ts | 2 +- .../monitor-mocks-test-support.ts | 18 ++--- .../check-no-extension-test-core-imports.ts | 55 ++++++++++++++++ src/plugin-sdk/test-utils.ts | 7 +- src/plugin-sdk/testing.ts | 8 ++- ...in-sdk-package-contract-guardrails.test.ts | 65 +++++++++++++++++++ test/extension-test-boundary.test.ts | 23 +++++++ 16 files changed, 223 insertions(+), 70 deletions(-) delete mode 100644 extensions/telegram/test-support.ts rename extensions/zalo/{ => src}/test-support/lifecycle-test-support.ts (99%) rename extensions/zalo/{ => src}/test-support/monitor-mocks-test-support.ts (91%) diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 73bf91a4476..34b1cc96718 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -6c33fafd66396c2c769b8f8d1f5d0792beb8b4b2d23b5f0323ff2bb2d0a79805 plugin-sdk-api-baseline.json -27336f313c029843be409d22d79caf6f45884cf37458d9f52c0a88c4e8b268fd plugin-sdk-api-baseline.jsonl +343a555f212dd5ebf26dccbefff1cb4b56a08e4dcc2c801ac7ab5fb98973192a plugin-sdk-api-baseline.json +02aaccbe13f261de2d41fcb4270fc9ae70b931966089e56d21a4ebc8e80c8821 plugin-sdk-api-baseline.jsonl diff --git a/docs/plugins/sdk-testing.md b/docs/plugins/sdk-testing.md index 495f4fa05af..25898ea4ef4 100644 --- a/docs/plugins/sdk-testing.md +++ b/docs/plugins/sdk-testing.md @@ -45,6 +45,9 @@ plugins. Prefer the focused subpaths below for new plugin tests. The broad `openclaw/plugin-sdk/testing` barrel is legacy compatibility only. +Repo guardrails reject new real imports from `plugin-sdk/testing` and +`plugin-sdk/test-utils`; those names remain only as deprecated compatibility +surfaces for external plugins and compatibility-record tests. ```typescript import { diff --git a/extensions/telegram/test-api.ts b/extensions/telegram/test-api.ts index ab21feaa137..7ee0f0155fd 100644 --- a/extensions/telegram/test-api.ts +++ b/extensions/telegram/test-api.ts @@ -1,2 +1,41 @@ +import { buildDmGroupAccountAllowlistAdapter } from "openclaw/plugin-sdk/allowlist-config-edit"; +import type { ChannelPlugin } from "openclaw/plugin-sdk/channel-core"; +import { getChatChannelMeta } from "openclaw/plugin-sdk/channel-plugin-common"; +import { resolveTelegramAccount, type ResolvedTelegramAccount } from "./src/accounts.js"; +import { telegramApprovalCapability } from "./src/approval-native.js"; +import { telegramConfigAdapter } from "./src/shared.js"; + export { sendMessageTelegram, sendPollTelegram, type TelegramApiOverride } from "./src/send.js"; export { resetTelegramThreadBindingsForTests } from "./src/thread-bindings.js"; + +export const telegramCommandTestPlugin = { + id: "telegram", + meta: getChatChannelMeta("telegram"), + capabilities: { + chatTypes: ["direct", "group", "channel", "thread"], + reactions: true, + threads: true, + media: true, + polls: true, + nativeCommands: true, + blockStreaming: true, + }, + config: telegramConfigAdapter, + approvalCapability: telegramApprovalCapability, + pairing: { + idLabel: "telegramUserId", + }, + allowlist: buildDmGroupAccountAllowlistAdapter({ + channelId: "telegram", + resolveAccount: resolveTelegramAccount, + normalize: ({ cfg, accountId, values }) => + telegramConfigAdapter.formatAllowFrom!({ cfg, accountId, allowFrom: values }), + resolveDmAllowFrom: (account) => account.config.allowFrom, + resolveGroupAllowFrom: (account) => account.config.groupAllowFrom, + resolveDmPolicy: (account) => account.config.dmPolicy, + resolveGroupPolicy: (account) => account.config.groupPolicy, + }), +} satisfies Pick< + ChannelPlugin, + "id" | "meta" | "capabilities" | "config" | "approvalCapability" | "pairing" | "allowlist" +>; diff --git a/extensions/telegram/test-support.ts b/extensions/telegram/test-support.ts deleted file mode 100644 index b708bb199fa..00000000000 --- a/extensions/telegram/test-support.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { buildDmGroupAccountAllowlistAdapter } from "openclaw/plugin-sdk/allowlist-config-edit"; -import type { ChannelPlugin } from "openclaw/plugin-sdk/channel-core"; -import { getChatChannelMeta } from "openclaw/plugin-sdk/channel-plugin-common"; -import type { ResolvedTelegramAccount } from "./src/accounts.js"; -import { resolveTelegramAccount } from "./src/accounts.js"; -import { telegramApprovalCapability } from "./src/approval-native.js"; -import { telegramConfigAdapter } from "./src/shared.js"; - -export const telegramCommandTestPlugin = { - id: "telegram", - meta: getChatChannelMeta("telegram"), - capabilities: { - chatTypes: ["direct", "group", "channel", "thread"], - reactions: true, - threads: true, - media: true, - polls: true, - nativeCommands: true, - blockStreaming: true, - }, - config: telegramConfigAdapter, - approvalCapability: telegramApprovalCapability, - pairing: { - idLabel: "telegramUserId", - }, - allowlist: buildDmGroupAccountAllowlistAdapter({ - channelId: "telegram", - resolveAccount: resolveTelegramAccount, - normalize: ({ cfg, accountId, values }) => - telegramConfigAdapter.formatAllowFrom!({ cfg, accountId, allowFrom: values }), - resolveDmAllowFrom: (account) => account.config.allowFrom, - resolveGroupAllowFrom: (account) => account.config.groupAllowFrom, - resolveDmPolicy: (account) => account.config.dmPolicy, - resolveGroupPolicy: (account) => account.config.groupPolicy, - }), -} satisfies Pick< - ChannelPlugin, - "id" | "meta" | "capabilities" | "config" | "approvalCapability" | "pairing" | "allowlist" ->; diff --git a/extensions/zalo/src/monitor.image.polling.test.ts b/extensions/zalo/src/monitor.image.polling.test.ts index 1122f692998..11bf24b9f6d 100644 --- a/extensions/zalo/src/monitor.image.polling.test.ts +++ b/extensions/zalo/src/monitor.image.polling.test.ts @@ -6,14 +6,14 @@ import { createLifecycleMonitorSetup, expectImageLifecycleDelivery, settleAsyncWork, -} from "../test-support/lifecycle-test-support.js"; +} from "./test-support/lifecycle-test-support.js"; import { getUpdatesMock, getZaloRuntimeMock, loadCachedLifecycleMonitorModule, resetLifecycleTestState, sendMessageMock, -} from "../test-support/monitor-mocks-test-support.js"; +} from "./test-support/monitor-mocks-test-support.js"; describe("Zalo polling image handling", () => { const { diff --git a/extensions/zalo/src/monitor.pairing.lifecycle.test.ts b/extensions/zalo/src/monitor.pairing.lifecycle.test.ts index c4bef3c4926..11d2d63ee1e 100644 --- a/extensions/zalo/src/monitor.pairing.lifecycle.test.ts +++ b/extensions/zalo/src/monitor.pairing.lifecycle.test.ts @@ -5,13 +5,13 @@ import { createTextUpdate, postWebhookReplay, settleAsyncWork, -} from "../test-support/lifecycle-test-support.js"; +} from "./test-support/lifecycle-test-support.js"; import { resetLifecycleTestState, sendMessageMock, setLifecycleRuntimeCore, startWebhookLifecycleMonitor, -} from "../test-support/monitor-mocks-test-support.js"; +} from "./test-support/monitor-mocks-test-support.js"; describe("Zalo pairing lifecycle", () => { const readAllowFromStoreMock = vi.fn(async () => [] as string[]); diff --git a/extensions/zalo/src/monitor.polling.media-reply.test.ts b/extensions/zalo/src/monitor.polling.media-reply.test.ts index ff2783dc8eb..982009b163b 100644 --- a/extensions/zalo/src/monitor.polling.media-reply.test.ts +++ b/extensions/zalo/src/monitor.polling.media-reply.test.ts @@ -9,14 +9,14 @@ import { createLifecycleMonitorSetup, createTextUpdate, settleAsyncWork, -} from "../test-support/lifecycle-test-support.js"; +} from "./test-support/lifecycle-test-support.js"; import { getUpdatesMock, loadCachedLifecycleMonitorModule, resetLifecycleTestState, sendPhotoMock, setLifecycleRuntimeCore, -} from "../test-support/monitor-mocks-test-support.js"; +} from "./test-support/monitor-mocks-test-support.js"; const prepareHostedZaloMediaUrlMock = vi.fn(); diff --git a/extensions/zalo/src/monitor.reply-once.lifecycle.test.ts b/extensions/zalo/src/monitor.reply-once.lifecycle.test.ts index 1203607dac9..4abecb0ffe9 100644 --- a/extensions/zalo/src/monitor.reply-once.lifecycle.test.ts +++ b/extensions/zalo/src/monitor.reply-once.lifecycle.test.ts @@ -6,13 +6,13 @@ import { createTextUpdate, postWebhookReplay, settleAsyncWork, -} from "../test-support/lifecycle-test-support.js"; +} from "./test-support/lifecycle-test-support.js"; import { resetLifecycleTestState, sendMessageMock, setLifecycleRuntimeCore, startWebhookLifecycleMonitor, -} from "../test-support/monitor-mocks-test-support.js"; +} from "./test-support/monitor-mocks-test-support.js"; describe("Zalo reply-once lifecycle", () => { const finalizeInboundContextMock = vi.fn((ctx: Record) => ctx); diff --git a/extensions/zalo/src/monitor.webhook.test.ts b/extensions/zalo/src/monitor.webhook.test.ts index 4f8cfb85230..08d56ff6569 100644 --- a/extensions/zalo/src/monitor.webhook.test.ts +++ b/extensions/zalo/src/monitor.webhook.test.ts @@ -6,13 +6,6 @@ import { import { withServer } from "openclaw/plugin-sdk/test-env"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig, PluginRuntime } from "../runtime-api.js"; -import { - createImageLifecycleCore, - createImageUpdate, - createTextUpdate, - expectImageLifecycleDelivery, - postWebhookReplay, -} from "../test-support/lifecycle-test-support.js"; import { handleZaloWebhookRequest } from "./monitor.js"; import type { ZaloRuntimeEnv } from "./monitor.types.js"; import { @@ -24,6 +17,13 @@ import { type ZaloWebhookProcessUpdate, ZaloRetryableWebhookError, } from "./monitor.webhook.js"; +import { + createImageLifecycleCore, + createImageUpdate, + createTextUpdate, + expectImageLifecycleDelivery, + postWebhookReplay, +} from "./test-support/lifecycle-test-support.js"; import type { ResolvedZaloAccount } from "./types.js"; const DEFAULT_ACCOUNT: ResolvedZaloAccount = { accountId: "default", diff --git a/extensions/zalo/test-support/lifecycle-test-support.ts b/extensions/zalo/src/test-support/lifecycle-test-support.ts similarity index 99% rename from extensions/zalo/test-support/lifecycle-test-support.ts rename to extensions/zalo/src/test-support/lifecycle-test-support.ts index 0148a6fe80d..b985e12b23a 100644 --- a/extensions/zalo/test-support/lifecycle-test-support.ts +++ b/extensions/zalo/src/test-support/lifecycle-test-support.ts @@ -1,7 +1,7 @@ import { request as httpRequest } from "node:http"; import { expect, vi } from "vitest"; import type { OpenClawConfig, PluginRuntime } from "../runtime-api.js"; -import type { ResolvedZaloAccount } from "../src/types.js"; +import type { ResolvedZaloAccount } from "../types.js"; export function createLifecycleConfig(params: { accountId: string; diff --git a/extensions/zalo/test-support/monitor-mocks-test-support.ts b/extensions/zalo/src/test-support/monitor-mocks-test-support.ts similarity index 91% rename from extensions/zalo/test-support/monitor-mocks-test-support.ts rename to extensions/zalo/src/test-support/monitor-mocks-test-support.ts index 8a03bbda993..6425171986a 100644 --- a/extensions/zalo/test-support/monitor-mocks-test-support.ts +++ b/extensions/zalo/src/test-support/monitor-mocks-test-support.ts @@ -6,17 +6,17 @@ import { } from "openclaw/plugin-sdk/plugin-test-runtime"; import { vi, type Mock } from "vitest"; import type { OpenClawConfig } from "../runtime-api.js"; -import type { ResolvedZaloAccount } from "../src/types.js"; +import type { ResolvedZaloAccount } from "../types.js"; -type MonitorModule = typeof import("../src/monitor.js"); -type SecretInputModule = typeof import("../src/secret-input.js"); -type WebhookModule = typeof import("../src/monitor.webhook.js"); +type MonitorModule = typeof import("../monitor.js"); +type SecretInputModule = typeof import("../secret-input.js"); +type WebhookModule = typeof import("../monitor.webhook.js"); -const monitorModuleUrl = new URL("../src/monitor.ts", import.meta.url).href; -const secretInputModuleUrl = new URL("../src/secret-input.ts", import.meta.url).href; -const webhookModuleUrl = new URL("../src/monitor.webhook.ts", import.meta.url).href; -const apiModuleId = new URL("../src/api.js", import.meta.url).pathname; -const runtimeModuleId = new URL("../src/runtime.js", import.meta.url).pathname; +const monitorModuleUrl = new URL("../monitor.ts", import.meta.url).href; +const secretInputModuleUrl = new URL("../secret-input.ts", import.meta.url).href; +const webhookModuleUrl = new URL("../monitor.webhook.ts", import.meta.url).href; +const apiModuleId = new URL("../api.js", import.meta.url).pathname; +const runtimeModuleId = new URL("../runtime.js", import.meta.url).pathname; type UnknownMock = Mock<(...args: unknown[]) => unknown>; type AsyncUnknownMock = Mock<(...args: unknown[]) => Promise>; diff --git a/scripts/check-no-extension-test-core-imports.ts b/scripts/check-no-extension-test-core-imports.ts index 3c42d2b471c..c894063f4c3 100644 --- a/scripts/check-no-extension-test-core-imports.ts +++ b/scripts/check-no-extension-test-core-imports.ts @@ -86,6 +86,8 @@ const MOCK_RELATIVE_MODULE_PATTERN = const RELATIVE_CORE_HINT = "Use a focused plugin-sdk test/runtime subpath instead of core internals."; +const ROOT_TEST_SUPPORT_LOCAL_SRC_HINT = + "Move this helper under the extension's src/test-support tree or expose a narrow test-api/runtime-api surface instead of reaching into private src from package-root test-support."; // Tombstones for retired repo-only plugin helper bridge files. Keep this list so // deleted bridges fail loudly if they are recreated instead of using SDK subpaths. @@ -203,6 +205,30 @@ function resolvesToRepoSrc(filePath: string, specifier: string): boolean { return repoRelative === "src" || repoRelative.startsWith("src/"); } +function getExtensionRootForFile(filePath: string): string | undefined { + const relativePath = path.relative(process.cwd(), filePath).replaceAll(path.sep, "/"); + const match = /^extensions\/[^/]+(?:\/|$)/u.exec(relativePath); + return match ? path.resolve(process.cwd(), match[0]) : undefined; +} + +function isRootExtensionTestSupportFile(filePath: string): boolean { + const relativePath = path.relative(process.cwd(), filePath).replaceAll(path.sep, "/"); + return /^extensions\/[^/]+\/test-support(?:\.[cm]?[jt]sx?|\/)/u.test(relativePath); +} + +function resolvesToExtensionLocalSrc(filePath: string, specifier: string): boolean { + if (!specifier.startsWith(".")) { + return false; + } + const extensionRoot = getExtensionRootForFile(filePath); + if (!extensionRoot) { + return false; + } + const resolved = path.resolve(path.dirname(filePath), specifier); + const localSrc = path.join(extensionRoot, "src"); + return resolved === localSrc || resolved.startsWith(`${localSrc}${path.sep}`); +} + function collectRelativeCoreImportOffenders( filePath: string, content: string, @@ -229,6 +255,34 @@ function collectRelativeCoreImportOffenders( return offenders; } +function collectRootTestSupportLocalSrcImportOffenders( + filePath: string, + content: string, +): Offender[] { + if (!isRootExtensionTestSupportFile(filePath)) { + return []; + } + const offenders: Offender[] = []; + const matches = [ + ...content.matchAll(STATIC_RELATIVE_MODULE_PATTERN), + ...content.matchAll(DYNAMIC_RELATIVE_MODULE_PATTERN), + ...content.matchAll(MOCK_RELATIVE_MODULE_PATTERN), + ]; + for (const match of matches) { + const specifier = match[1]; + if (!specifier || !resolvesToExtensionLocalSrc(filePath, specifier)) { + continue; + } + offenders.push({ + file: filePath, + hint: ROOT_TEST_SUPPORT_LOCAL_SRC_HINT, + line: lineNumberForOffset(content, match.index ?? 0), + specifier, + }); + } + return offenders; +} + function main() { const extensionsDir = path.join(process.cwd(), "extensions"); const pluginHelpersDir = path.join(process.cwd(), "test/helpers/plugins"); @@ -272,6 +326,7 @@ function main() { includeDynamic: true, }), ); + offenders.push(...collectRootTestSupportLocalSrcImportOffenders(file, content)); } for (const file of pluginHelperFiles) { diff --git a/src/plugin-sdk/test-utils.ts b/src/plugin-sdk/test-utils.ts index 01b154113d2..335e95c6b2f 100644 --- a/src/plugin-sdk/test-utils.ts +++ b/src/plugin-sdk/test-utils.ts @@ -1,4 +1,7 @@ -// Deprecated compatibility alias. -// Prefer focused openclaw/plugin-sdk/* test subpaths for public test helpers. +/** + * @deprecated Compatibility alias for the legacy `plugin-sdk/testing` barrel. + * + * Prefer focused `openclaw/plugin-sdk/*` test subpaths for public test helpers. + */ export * from "./testing.js"; diff --git a/src/plugin-sdk/testing.ts b/src/plugin-sdk/testing.ts index 2d5249867f4..72cd311d66b 100644 --- a/src/plugin-sdk/testing.ts +++ b/src/plugin-sdk/testing.ts @@ -1,5 +1,9 @@ -// Broad legacy compatibility barrel for older plugin tests. -// New tests should import focused plugin-sdk/* test subpaths. +/** + * @deprecated Broad compatibility barrel for older plugin tests. + * + * New tests should import focused `openclaw/plugin-sdk/*` test subpaths such as + * `plugin-test-runtime`, `channel-test-helpers`, `test-env`, or `test-fixtures`. + */ export { createAckReactionHandle, 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 d4a76548342..6f7852dc75f 100644 --- a/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts +++ b/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts @@ -31,6 +31,17 @@ const DEPRECATED_EXTENSION_SDK_SPECIFIERS = new Set([ "openclaw/plugin-sdk/testing", "openclaw/plugin-sdk/test-utils", ]); +const DEPRECATED_TEST_BARREL_SPECIFIERS = new Set([ + "openclaw/plugin-sdk/testing", + "openclaw/plugin-sdk/test-utils", +]); +const DEPRECATED_TEST_BARREL_ALLOWED_REFERENCE_FILES = new Set([ + "src/plugin-sdk/testing.ts", + "src/plugin-sdk/test-utils.ts", + "src/plugins/compat/registry.ts", + "src/plugins/contracts/plugin-entry-guardrails.test.ts", + "src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts", +]); function collectPluginSdkPackageExports(): string[] { const packageJson = JSON.parse(readFileSync(resolve(REPO_ROOT, "package.json"), "utf8")) as { @@ -300,6 +311,56 @@ function collectDeprecatedExtensionSdkImports(): Array<{ file: string; specifier return leaks; } +function collectCodeFiles(dir: string): string[] { + const files: string[] = []; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (entry.name === "dist" || entry.name === "node_modules" || entry.name === ".git") { + continue; + } + const nextPath = join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...collectCodeFiles(nextPath)); + continue; + } + if (!entry.isFile() || !/\.(?:[cm]?ts|tsx|mts|cts)$/.test(entry.name)) { + continue; + } + files.push(nextPath); + } + return files; +} + +function collectDeprecatedTestBarrelImports(): Array<{ file: string; specifier: string }> { + const leaks: Array<{ file: string; specifier: string }> = []; + const importPatterns = [ + /\b(?:import|export)\b[\s\S]*?\bfrom\s*["'](openclaw\/plugin-sdk\/(?:testing|test-utils))["']/g, + /\bimport\s*\(\s*["'](openclaw\/plugin-sdk\/(?:testing|test-utils))["']\s*\)/g, + /\bvi\.(?:mock|doMock)\s*\(\s*["'](openclaw\/plugin-sdk\/(?:testing|test-utils))["']/g, + ]; + for (const root of ["src", "test", "extensions", "packages"]) { + for (const file of collectCodeFiles(resolve(REPO_ROOT, root))) { + const repoRelativePath = relative(REPO_ROOT, file).replaceAll("\\", "/"); + if (DEPRECATED_TEST_BARREL_ALLOWED_REFERENCE_FILES.has(repoRelativePath)) { + continue; + } + const source = readFileSync(file, "utf8"); + for (const importPattern of importPatterns) { + for (const match of source.matchAll(importPattern)) { + const specifier = match[1]; + if (!specifier || !DEPRECATED_TEST_BARREL_SPECIFIERS.has(specifier)) { + continue; + } + leaks.push({ + file: repoRelativePath, + specifier, + }); + } + } + } + } + return leaks; +} + function collectCrossOwnerReservedSdkImports(): Array<{ file: string; specifier: string; @@ -467,6 +528,10 @@ describe("plugin-sdk package contract guardrails", () => { expect(collectDeprecatedExtensionSdkImports()).toEqual([]); }); + it("keeps real tests off deprecated plugin-sdk testing barrels", () => { + expect(collectDeprecatedTestBarrelImports()).toEqual([]); + }); + it("keeps reserved SDK compatibility subpaths inside their owning bundled plugins", () => { expect(collectCrossOwnerReservedSdkImports()).toEqual([]); }); diff --git a/test/extension-test-boundary.test.ts b/test/extension-test-boundary.test.ts index e5e1a21918c..678469bbe23 100644 --- a/test/extension-test-boundary.test.ts +++ b/test/extension-test-boundary.test.ts @@ -81,6 +81,14 @@ function findBundledPluginPublicSurfaceImports(source: string): string[] { ].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; } @@ -229,6 +237,21 @@ describe("non-extension test boundaries", () => { 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"));