fix(plugin-sdk): avoid telegram config import side effects (#61061)

* fix(plugin-sdk): avoid telegram config import side effects

* fix(plugin-sdk): address telegram contract review

* test(plugin-sdk): tighten telegram contract guards
This commit is contained in:
Altay 2026-04-05 02:32:04 +03:00 committed by GitHub
parent d37e4a6c3a
commit 2ba3484d10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 101 additions and 24 deletions

View File

@ -1,2 +1,2 @@
0cd9a43c490bb5511890171543a3029754d44c9f1fe1ebf6f5c845fb49f44452 plugin-sdk-api-baseline.json
66e1a9dff2b6c170dd1caceef1f15ad63c18f89c897d98f502cac1f2f46d26c2 plugin-sdk-api-baseline.jsonl
884e6fd12b7a8086a11f547e15201f46dea0f2dc46735fad055d4f1b96d5fb82 plugin-sdk-api-baseline.json
100f6b29793abf858f94cb8c292afc0dc56573f4e264d27496a96e17f8de4c1e plugin-sdk-api-baseline.jsonl

View File

@ -1,22 +1,74 @@
import { beforeAll, describe, expect, it, vi } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { TELEGRAM_COMMAND_NAME_PATTERN as bundledTelegramCommandNamePattern } from "../../extensions/telegram/src/command-config.ts";
const getBundledChannelContractSurfaceModule = vi.fn(() => null);
vi.mock("../channels/plugins/contract-surfaces.js", () => ({
getBundledChannelContractSurfaceModule: vi.fn(() => null),
getBundledChannelContractSurfaceModule,
}));
let telegramCommandConfig: typeof import("./telegram-command-config.js");
beforeAll(async () => {
async function loadTelegramCommandConfig() {
vi.resetModules();
telegramCommandConfig = await import("./telegram-command-config.js");
});
getBundledChannelContractSurfaceModule.mockClear();
return import("./telegram-command-config.js");
}
describe("telegram command config fallback", () => {
it("keeps command validation available when the bundled contract surface is unavailable", () => {
expect(telegramCommandConfig.TELEGRAM_COMMAND_NAME_PATTERN.test("hello_world")).toBe(true);
expect(telegramCommandConfig.normalizeTelegramCommandName("/Hello-World")).toBe(
"hello_world",
it("keeps the fallback regex in parity with the bundled telegram contract", async () => {
const telegramCommandConfig = await loadTelegramCommandConfig();
expect(telegramCommandConfig.TELEGRAM_COMMAND_NAME_PATTERN.toString()).toBe(
bundledTelegramCommandNamePattern.toString(),
);
});
it("keeps import-time regex access side-effect free", async () => {
const telegramCommandConfig = await loadTelegramCommandConfig();
expect(getBundledChannelContractSurfaceModule).not.toHaveBeenCalled();
expect(telegramCommandConfig.TELEGRAM_COMMAND_NAME_PATTERN.test("hello_world")).toBe(true);
expect(getBundledChannelContractSurfaceModule).not.toHaveBeenCalled();
});
it("lazy-loads the contract pattern only when callers opt in", async () => {
const contractPattern = /^[a-z]+$/;
getBundledChannelContractSurfaceModule.mockReturnValueOnce({
TELEGRAM_COMMAND_NAME_PATTERN: contractPattern,
normalizeTelegramCommandName: (value: string) => `contract:${value.trim().toLowerCase()}`,
normalizeTelegramCommandDescription: (value: string) => `desc:${value.trim()}`,
resolveTelegramCustomCommands: () => ({
commands: [{ command: "from_contract", description: "from contract" }],
issues: [],
}),
});
const telegramCommandConfig = await loadTelegramCommandConfig();
expect(getBundledChannelContractSurfaceModule).not.toHaveBeenCalled();
expect(telegramCommandConfig.TELEGRAM_COMMAND_NAME_PATTERN.test("hello_world")).toBe(true);
expect(getBundledChannelContractSurfaceModule).not.toHaveBeenCalled();
expect(telegramCommandConfig.getTelegramCommandNamePattern()).toBe(contractPattern);
expect(getBundledChannelContractSurfaceModule).toHaveBeenCalledTimes(1);
expect(telegramCommandConfig.getTelegramCommandNamePattern()).toBe(
telegramCommandConfig.getTelegramCommandNamePattern(),
);
expect(telegramCommandConfig.normalizeTelegramCommandName(" Hello ")).toBe("contract:hello");
expect(telegramCommandConfig.normalizeTelegramCommandDescription(" hi ")).toBe("desc:hi");
expect(
telegramCommandConfig.resolveTelegramCustomCommands({
commands: [{ command: "/ignored", description: "ignored" }],
}),
).toEqual({
commands: [{ command: "from_contract", description: "from contract" }],
issues: [],
});
expect(getBundledChannelContractSurfaceModule).toHaveBeenCalledTimes(1);
});
it("keeps command validation available when the bundled contract surface is unavailable", async () => {
const telegramCommandConfig = await loadTelegramCommandConfig();
expect(telegramCommandConfig.TELEGRAM_COMMAND_NAME_PATTERN.test("hello_world")).toBe(true);
expect(telegramCommandConfig.normalizeTelegramCommandName("/Hello-World")).toBe("hello_world");
expect(telegramCommandConfig.normalizeTelegramCommandDescription(" hi ")).toBe("hi");
expect(
@ -48,5 +100,6 @@ describe("telegram command config fallback", () => {
},
],
});
expect(getBundledChannelContractSurfaceModule).toHaveBeenCalledTimes(1);
});
});

View File

@ -27,6 +27,7 @@ type TelegramCommandConfigContract = {
};
const FALLBACK_TELEGRAM_COMMAND_NAME_PATTERN = /^[a-z0-9_]{1,32}$/;
let cachedTelegramCommandConfigContract: TelegramCommandConfigContract | null = null;
function fallbackNormalizeTelegramCommandName(value: string): string {
const trimmed = value.trim();
@ -121,15 +122,23 @@ const FALLBACK_TELEGRAM_COMMAND_CONFIG_CONTRACT: TelegramCommandConfigContract =
};
function loadTelegramCommandConfigContract(): TelegramCommandConfigContract {
const contract = getBundledChannelContractSurfaceModule<TelegramCommandConfigContract>({
pluginId: "telegram",
preferredBasename: "contract-surfaces.ts",
});
return contract ?? FALLBACK_TELEGRAM_COMMAND_CONFIG_CONTRACT;
cachedTelegramCommandConfigContract ??=
getBundledChannelContractSurfaceModule<TelegramCommandConfigContract>({
pluginId: "telegram",
preferredBasename: "contract-surfaces.ts",
}) ?? FALLBACK_TELEGRAM_COMMAND_CONFIG_CONTRACT;
return cachedTelegramCommandConfigContract;
}
export const TELEGRAM_COMMAND_NAME_PATTERN =
loadTelegramCommandConfigContract().TELEGRAM_COMMAND_NAME_PATTERN;
export function getTelegramCommandNamePattern(): RegExp {
return loadTelegramCommandConfigContract().TELEGRAM_COMMAND_NAME_PATTERN;
}
/**
* @deprecated Use `getTelegramCommandNamePattern()` when you need the live
* bundled contract value. This export remains an import-time-safe fallback.
*/
export const TELEGRAM_COMMAND_NAME_PATTERN = FALLBACK_TELEGRAM_COMMAND_NAME_PATTERN;
export function normalizeTelegramCommandName(value: string): string {
return loadTelegramCommandConfigContract().normalizeTelegramCommandName(value);

View File

@ -162,6 +162,14 @@ function expectSourceOmitsImportPattern(subpath: string, specifier: string) {
expect(source).not.toMatch(new RegExp(`\\bimport\\(\\s*["']${escapedSpecifier}["']\\s*\\)`, "u"));
}
function isGeneratedBundledFacadeSubpath(subpath: string): boolean {
const source = readPluginSdkSource(subpath);
return (
source.startsWith("// Generated by scripts/generate-plugin-sdk-facades.mjs.") &&
sourceMentionsIdentifier(source, "loadBundledPluginPublicSurfaceModuleSync")
);
}
describe("plugin-sdk subpath exports", () => {
it("keeps the curated public list free of internal implementation subpaths", () => {
for (const deniedSubpath of [
@ -185,14 +193,21 @@ describe("plugin-sdk subpath exports", () => {
}
});
it("keeps removed bundled-channel prefixes out of the public sdk list", () => {
it("keeps removed bundled-channel aliases out of the public sdk list", () => {
const removedChannelAliases = new Set(["discord", "signal", "slack", "telegram", "whatsapp"]);
const banned = pluginSdkSubpaths.filter((subpath) => removedChannelAliases.has(subpath));
expect(banned).toEqual([]);
});
it("keeps generated bundled-channel facades out of the public sdk list", () => {
const bannedPrefixes = ["discord", "signal", "slack", "telegram", "whatsapp"];
const banned = pluginSdkSubpaths.filter((subpath) =>
bannedPrefixes.some(
(prefix) =>
subpath === prefix ||
subpath.startsWith(`${prefix}-`) ||
subpath.startsWith(`${prefix}.`),
(subpath === prefix ||
subpath.startsWith(`${prefix}-`) ||
subpath.startsWith(`${prefix}.`)) &&
isGeneratedBundledFacadeSubpath(subpath),
),
);
expect(banned).toEqual([]);