test: guard extension test api exports

This commit is contained in:
Peter Steinberger
2026-04-28 04:00:00 +01:00
parent a812b8f919
commit 358579b136
4 changed files with 94 additions and 43 deletions

View File

@@ -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<ResolvedTelegramAccount>({
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<ResolvedTelegramAccount>,
"id" | "meta" | "capabilities" | "config" | "approvalCapability" | "pairing" | "allowlist"
>;

View File

@@ -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",

View File

@@ -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[];

View File

@@ -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<string>();
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([]);
});