From 358579b1369a95f32733ba6f729e7066c5667da7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Apr 2026 04:00:00 +0100 Subject: [PATCH] test: guard extension test api exports --- extensions/telegram/test-api.ts | 39 -------- src/plugins/compat/registry.test.ts | 4 +- src/plugins/compat/registry.ts | 6 +- ...in-sdk-package-contract-guardrails.test.ts | 88 ++++++++++++++++++- 4 files changed, 94 insertions(+), 43 deletions(-) diff --git a/extensions/telegram/test-api.ts b/extensions/telegram/test-api.ts index 7ee0f0155fd..ab21feaa137 100644 --- a/extensions/telegram/test-api.ts +++ b/extensions/telegram/test-api.ts @@ -1,41 +1,2 @@ -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/src/plugins/compat/registry.test.ts b/src/plugins/compat/registry.test.ts index 2977020732c..9e72eeca1e5 100644 --- a/src/plugins/compat/registry.test.ts +++ b/src/plugins/compat/registry.test.ts @@ -93,7 +93,7 @@ const knownDeprecatedSurfaceMarkers = [ { code: "plugin-sdk-test-utils-alias", file: "src/plugin-sdk/test-utils.ts", - marker: "focused openclaw/plugin-sdk/* test subpaths", + marker: "focused `openclaw/plugin-sdk/*` test subpaths", }, { code: "plugin-install-config-ledger", @@ -128,7 +128,7 @@ const knownDeprecatedSurfaceMarkers = [ { code: "plugin-sdk-testing-barrel", file: "src/plugin-sdk/testing.ts", - marker: "Broad legacy compatibility barrel for older plugin tests", + marker: "@deprecated Broad compatibility barrel", }, { code: "channel-route-key-aliases", diff --git a/src/plugins/compat/registry.ts b/src/plugins/compat/registry.ts index 62ab50f8777..03a577076ca 100644 --- a/src/plugins/compat/registry.ts +++ b/src/plugins/compat/registry.ts @@ -83,6 +83,7 @@ export const PLUGIN_COMPAT_RECORDS = [ surfaces: ["openclaw/plugin-sdk/testing"], diagnostics: ["plugin SDK compatibility warning"], tests: [ + "src/plugins/compat/registry.test.ts", "scripts/check-no-extension-test-core-imports.ts", "test/extension-test-boundary.test.ts", ], @@ -844,7 +845,10 @@ export const PLUGIN_COMPAT_RECORDS = [ docsPath: "/plugins/sdk-migration", surfaces: ["openclaw/plugin-sdk/test-utils"], diagnostics: ["plugin SDK compatibility warning"], - tests: ["src/plugins/contracts/plugin-sdk-subpaths.test.ts"], + tests: [ + "src/plugins/compat/registry.test.ts", + "src/plugins/contracts/plugin-sdk-subpaths.test.ts", + ], }, ] as const satisfies readonly PluginCompatRecord[]; 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 6f7852dc75f..25985e259f4 100644 --- a/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts +++ b/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts @@ -1,4 +1,4 @@ -import { readdirSync, readFileSync } from "node:fs"; +import { existsSync, readdirSync, readFileSync } from "node:fs"; import { dirname, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; @@ -361,6 +361,88 @@ function collectDeprecatedTestBarrelImports(): Array<{ file: string; specifier: return leaks; } +function parseTestApiNamedExports(source: string): string[] { + const exports = new Set(); + const declarationPattern = + /\bexport\s+(?:const|function|class|async\s+function|type|interface)\s+([A-Za-z_$][\w$]*)/g; + const exportListPattern = /\bexport\s*\{([^}]+)\}/g; + + for (const match of source.matchAll(declarationPattern)) { + const exportName = match[1]; + if (exportName) { + exports.add(exportName); + } + } + + for (const match of source.matchAll(exportListPattern)) { + const exportList = match[1]; + if (!exportList) { + continue; + } + for (const part of exportList.split(",")) { + const item = part.trim().replace(/^type\s+/, ""); + const aliasMatch = /\bas\s+([A-Za-z_$][\w$]*)$/u.exec(item); + const nameMatch = /^([A-Za-z_$][\w$]*)/u.exec(item); + const exportName = aliasMatch?.[1] ?? nameMatch?.[1]; + if (exportName && exportName !== "default") { + exports.add(exportName); + } + } + } + + return [...exports].toSorted(); +} + +function collectWorkspaceCodeFiles(): string[] { + const files: string[] = []; + for (const root of ["src", "test", "extensions", "packages", "scripts"]) { + const dir = resolve(REPO_ROOT, root); + if (existsSync(dir)) { + files.push(...collectCodeFiles(dir)); + } + } + return files; +} + +function countIdentifierReferences( + files: readonly string[], + excludedFile: string, + name: string, +): number { + let count = 0; + const pattern = new RegExp(`\\b${name}\\b`, "g"); + for (const file of files) { + if (file === excludedFile) { + continue; + } + const source = readFileSync(file, "utf8"); + count += [...source.matchAll(pattern)].length; + } + return count; +} + +function collectUnusedExtensionTestApiExports(): Array<{ file: string; exportName: string }> { + const leaks: Array<{ file: string; exportName: string }> = []; + const workspaceCodeFiles = collectWorkspaceCodeFiles(); + const testApiFiles = collectCodeFiles(resolve(REPO_ROOT, "extensions")).filter((file) => + file.endsWith("/test-api.ts"), + ); + + for (const file of testApiFiles) { + const repoRelativePath = relative(REPO_ROOT, file).replaceAll("\\", "/"); + const source = readFileSync(file, "utf8"); + for (const exportName of parseTestApiNamedExports(source)) { + if (countIdentifierReferences(workspaceCodeFiles, file, exportName) === 0) { + leaks.push({ file: repoRelativePath, exportName }); + } + } + } + + return leaks.toSorted( + (a, b) => a.file.localeCompare(b.file) || a.exportName.localeCompare(b.exportName), + ); +} + function collectCrossOwnerReservedSdkImports(): Array<{ file: string; specifier: string; @@ -532,6 +614,10 @@ describe("plugin-sdk package contract guardrails", () => { expect(collectDeprecatedTestBarrelImports()).toEqual([]); }); + it("keeps extension test-api exports consumed", () => { + expect(collectUnusedExtensionTestApiExports()).toEqual([]); + }); + it("keeps reserved SDK compatibility subpaths inside their owning bundled plugins", () => { expect(collectCrossOwnerReservedSdkImports()).toEqual([]); });