mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
test: guard extension test api exports
This commit is contained in:
@@ -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"
|
||||
>;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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[];
|
||||
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user