mirror of https://github.com/openclaw/openclaw.git
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:
parent
d37e4a6c3a
commit
2ba3484d10
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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([]);
|
||||
|
|
|
|||
Loading…
Reference in New Issue