Skip to content

Commit 5d5ded7

Browse files
Auto-merge upstream openclaw/openclaw
2 parents ba8d8a4 + 04b64e4 commit 5d5ded7

6 files changed

Lines changed: 138 additions & 30 deletions

File tree

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
0cd9a43c490bb5511890171543a3029754d44c9f1fe1ebf6f5c845fb49f44452 plugin-sdk-api-baseline.json
2-
66e1a9dff2b6c170dd1caceef1f15ad63c18f89c897d98f502cac1f2f46d26c2 plugin-sdk-api-baseline.jsonl
1+
884e6fd12b7a8086a11f547e15201f46dea0f2dc46735fad055d4f1b96d5fb82 plugin-sdk-api-baseline.json
2+
100f6b29793abf858f94cb8c292afc0dc56573f4e264d27496a96e17f8de4c1e plugin-sdk-api-baseline.jsonl

src/plugin-sdk/telegram-command-config.test.ts

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,88 @@
1-
import { beforeAll, describe, expect, it, vi } from "vitest";
1+
import { describe, expect, it, vi } from "vitest";
2+
import { TELEGRAM_COMMAND_NAME_PATTERN as bundledTelegramCommandNamePattern } from "../../extensions/telegram/src/command-config.ts";
3+
4+
type BundledChannelContractSurfaceParams = Parameters<
5+
(typeof import("../channels/plugins/contract-surfaces.js"))["getBundledChannelContractSurfaceModule"]
6+
>[0];
7+
8+
type TelegramCommandConfigContract = Pick<
9+
typeof import("./telegram-command-config.js"),
10+
| "TELEGRAM_COMMAND_NAME_PATTERN"
11+
| "normalizeTelegramCommandName"
12+
| "normalizeTelegramCommandDescription"
13+
| "resolveTelegramCustomCommands"
14+
>;
15+
16+
const getBundledChannelContractSurfaceModule = vi.fn<
17+
(params: BundledChannelContractSurfaceParams) => TelegramCommandConfigContract | null
18+
>(() => null);
219

320
vi.mock("../channels/plugins/contract-surfaces.js", () => ({
4-
getBundledChannelContractSurfaceModule: vi.fn(() => null),
21+
getBundledChannelContractSurfaceModule,
522
}));
623

7-
let telegramCommandConfig: typeof import("./telegram-command-config.js");
8-
9-
beforeAll(async () => {
24+
async function loadTelegramCommandConfig() {
1025
vi.resetModules();
11-
telegramCommandConfig = await import("./telegram-command-config.js");
12-
});
26+
getBundledChannelContractSurfaceModule.mockClear();
27+
return import("./telegram-command-config.js");
28+
}
1329

1430
describe("telegram command config fallback", () => {
15-
it("keeps command validation available when the bundled contract surface is unavailable", () => {
31+
it("keeps the fallback regex in parity with the bundled telegram contract", async () => {
32+
const telegramCommandConfig = await loadTelegramCommandConfig();
33+
34+
expect(telegramCommandConfig.TELEGRAM_COMMAND_NAME_PATTERN.toString()).toBe(
35+
bundledTelegramCommandNamePattern.toString(),
36+
);
37+
});
38+
39+
it("keeps import-time regex access side-effect free", async () => {
40+
const telegramCommandConfig = await loadTelegramCommandConfig();
41+
42+
expect(getBundledChannelContractSurfaceModule).not.toHaveBeenCalled();
1643
expect(telegramCommandConfig.TELEGRAM_COMMAND_NAME_PATTERN.test("hello_world")).toBe(true);
17-
expect(telegramCommandConfig.normalizeTelegramCommandName("/Hello-World")).toBe(
18-
"hello_world",
44+
expect(getBundledChannelContractSurfaceModule).not.toHaveBeenCalled();
45+
});
46+
47+
it("lazy-loads the contract pattern only when callers opt in", async () => {
48+
const contractPattern = /^[a-z]+$/;
49+
getBundledChannelContractSurfaceModule.mockReturnValueOnce({
50+
TELEGRAM_COMMAND_NAME_PATTERN: contractPattern,
51+
normalizeTelegramCommandName: (value: string) => `contract:${value.trim().toLowerCase()}`,
52+
normalizeTelegramCommandDescription: (value: string) => `desc:${value.trim()}`,
53+
resolveTelegramCustomCommands: () => ({
54+
commands: [{ command: "from_contract", description: "from contract" }],
55+
issues: [],
56+
}),
57+
});
58+
const telegramCommandConfig = await loadTelegramCommandConfig();
59+
60+
expect(getBundledChannelContractSurfaceModule).not.toHaveBeenCalled();
61+
expect(telegramCommandConfig.TELEGRAM_COMMAND_NAME_PATTERN.test("hello_world")).toBe(true);
62+
expect(getBundledChannelContractSurfaceModule).not.toHaveBeenCalled();
63+
expect(telegramCommandConfig.getTelegramCommandNamePattern()).toBe(contractPattern);
64+
expect(getBundledChannelContractSurfaceModule).toHaveBeenCalledTimes(1);
65+
expect(telegramCommandConfig.getTelegramCommandNamePattern()).toBe(
66+
telegramCommandConfig.getTelegramCommandNamePattern(),
1967
);
68+
expect(telegramCommandConfig.normalizeTelegramCommandName(" Hello ")).toBe("contract:hello");
69+
expect(telegramCommandConfig.normalizeTelegramCommandDescription(" hi ")).toBe("desc:hi");
70+
expect(
71+
telegramCommandConfig.resolveTelegramCustomCommands({
72+
commands: [{ command: "/ignored", description: "ignored" }],
73+
}),
74+
).toEqual({
75+
commands: [{ command: "from_contract", description: "from contract" }],
76+
issues: [],
77+
});
78+
expect(getBundledChannelContractSurfaceModule).toHaveBeenCalledTimes(1);
79+
});
80+
81+
it("keeps command validation available when the bundled contract surface is unavailable", async () => {
82+
const telegramCommandConfig = await loadTelegramCommandConfig();
83+
84+
expect(telegramCommandConfig.TELEGRAM_COMMAND_NAME_PATTERN.test("hello_world")).toBe(true);
85+
expect(telegramCommandConfig.normalizeTelegramCommandName("/Hello-World")).toBe("hello_world");
2086
expect(telegramCommandConfig.normalizeTelegramCommandDescription(" hi ")).toBe("hi");
2187

2288
expect(
@@ -48,5 +114,6 @@ describe("telegram command config fallback", () => {
48114
},
49115
],
50116
});
117+
expect(getBundledChannelContractSurfaceModule).toHaveBeenCalledTimes(1);
51118
});
52119
});

src/plugin-sdk/telegram-command-config.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type TelegramCommandConfigContract = {
2727
};
2828

2929
const FALLBACK_TELEGRAM_COMMAND_NAME_PATTERN = /^[a-z0-9_]{1,32}$/;
30+
let cachedTelegramCommandConfigContract: TelegramCommandConfigContract | null = null;
3031

3132
function fallbackNormalizeTelegramCommandName(value: string): string {
3233
const trimmed = value.trim();
@@ -121,15 +122,23 @@ const FALLBACK_TELEGRAM_COMMAND_CONFIG_CONTRACT: TelegramCommandConfigContract =
121122
};
122123

123124
function loadTelegramCommandConfigContract(): TelegramCommandConfigContract {
124-
const contract = getBundledChannelContractSurfaceModule<TelegramCommandConfigContract>({
125-
pluginId: "telegram",
126-
preferredBasename: "contract-surfaces.ts",
127-
});
128-
return contract ?? FALLBACK_TELEGRAM_COMMAND_CONFIG_CONTRACT;
125+
cachedTelegramCommandConfigContract ??=
126+
getBundledChannelContractSurfaceModule<TelegramCommandConfigContract>({
127+
pluginId: "telegram",
128+
preferredBasename: "contract-surfaces.ts",
129+
}) ?? FALLBACK_TELEGRAM_COMMAND_CONFIG_CONTRACT;
130+
return cachedTelegramCommandConfigContract;
129131
}
130132

131-
export const TELEGRAM_COMMAND_NAME_PATTERN =
132-
loadTelegramCommandConfigContract().TELEGRAM_COMMAND_NAME_PATTERN;
133+
export function getTelegramCommandNamePattern(): RegExp {
134+
return loadTelegramCommandConfigContract().TELEGRAM_COMMAND_NAME_PATTERN;
135+
}
136+
137+
/**
138+
* @deprecated Use `getTelegramCommandNamePattern()` when you need the live
139+
* bundled contract value. This export remains an import-time-safe fallback.
140+
*/
141+
export const TELEGRAM_COMMAND_NAME_PATTERN = FALLBACK_TELEGRAM_COMMAND_NAME_PATTERN;
133142

134143
export function normalizeTelegramCommandName(value: string): string {
135144
return loadTelegramCommandConfigContract().normalizeTelegramCommandName(value);

src/plugins/contracts/bundled-extension-config-api-guardrails.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ const BUNDLED_EXTENSION_CONFIG_IMPORT_GUARDS = [
3535
path: "extensions/googlechat/src/config-schema.ts",
3636
allowedSpecifier: "openclaw/plugin-sdk/googlechat",
3737
},
38+
// Teams keeps a package-local config barrel so production code does not
39+
// self-import via openclaw/plugin-sdk/msteams from inside the same extension.
3840
{
3941
path: "extensions/msteams/src/config-schema.ts",
40-
allowedSpecifier: "openclaw/plugin-sdk/msteams",
42+
allowedSpecifier: "../config-api.js",
4143
},
4244
] as const;
4345

src/plugins/contracts/plugin-sdk-subpaths.test.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,14 @@ function expectSourceOmitsImportPattern(subpath: string, specifier: string) {
162162
expect(source).not.toMatch(new RegExp(`\\bimport\\(\\s*["']${escapedSpecifier}["']\\s*\\)`, "u"));
163163
}
164164

165+
function isGeneratedBundledFacadeSubpath(subpath: string): boolean {
166+
const source = readPluginSdkSource(subpath);
167+
return (
168+
source.startsWith("// Generated by scripts/generate-plugin-sdk-facades.mjs.") &&
169+
sourceMentionsIdentifier(source, "loadBundledPluginPublicSurfaceModuleSync")
170+
);
171+
}
172+
165173
describe("plugin-sdk subpath exports", () => {
166174
it("keeps the curated public list free of internal implementation subpaths", () => {
167175
for (const deniedSubpath of [
@@ -185,14 +193,21 @@ describe("plugin-sdk subpath exports", () => {
185193
}
186194
});
187195

188-
it("keeps removed bundled-channel prefixes out of the public sdk list", () => {
196+
it("keeps removed bundled-channel aliases out of the public sdk list", () => {
197+
const removedChannelAliases = new Set(["discord", "signal", "slack", "telegram", "whatsapp"]);
198+
const banned = pluginSdkSubpaths.filter((subpath) => removedChannelAliases.has(subpath));
199+
expect(banned).toEqual([]);
200+
});
201+
202+
it("keeps generated bundled-channel facades out of the public sdk list", () => {
189203
const bannedPrefixes = ["discord", "signal", "slack", "telegram", "whatsapp"];
190204
const banned = pluginSdkSubpaths.filter((subpath) =>
191205
bannedPrefixes.some(
192206
(prefix) =>
193-
subpath === prefix ||
194-
subpath.startsWith(`${prefix}-`) ||
195-
subpath.startsWith(`${prefix}.`),
207+
(subpath === prefix ||
208+
subpath.startsWith(`${prefix}-`) ||
209+
subpath.startsWith(`${prefix}.`)) &&
210+
isGeneratedBundledFacadeSubpath(subpath),
196211
),
197212
);
198213
expect(banned).toEqual([]);

src/plugins/contracts/speech-vitest-registry.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,19 @@ function resolveNamedBuilder<T>(moduleExport: unknown, pattern: RegExp): (() =>
8484
return undefined;
8585
}
8686

87+
function resolveNamedBuilders<T>(moduleExport: unknown, pattern: RegExp): Array<() => T> {
88+
if (!moduleExport || typeof moduleExport !== "object") {
89+
return [];
90+
}
91+
const matches: Array<() => T> = [];
92+
for (const [key, value] of Object.entries(moduleExport as Record<string, unknown>)) {
93+
if (pattern.test(key) && typeof value === "function") {
94+
matches.push(value as () => T);
95+
}
96+
}
97+
return matches;
98+
}
99+
87100
function resolveNamedValues<T>(
88101
moduleExport: unknown,
89102
pattern: RegExp,
@@ -315,17 +328,19 @@ export function loadVitestImageGenerationProviderContractRegistry(): ImageGenera
315328
if (!fs.existsSync(testApiPath)) {
316329
continue;
317330
}
318-
const builder = resolveNamedBuilder<ImageGenerationProviderPlugin>(
331+
const builders = resolveNamedBuilders<ImageGenerationProviderPlugin>(
319332
createVitestCapabilityLoader(testApiPath)(testApiPath),
320333
/^build.+ImageGenerationProvider$/u,
321334
);
322-
if (!builder) {
335+
if (builders.length === 0) {
323336
continue;
324337
}
325-
registrations.push({
326-
pluginId: plugin.id,
327-
provider: builder(),
328-
});
338+
registrations.push(
339+
...builders.map((builder) => ({
340+
pluginId: plugin.id,
341+
provider: builder(),
342+
})),
343+
);
329344
unresolvedPluginIds.delete(plugin.id);
330345
}
331346

0 commit comments

Comments
 (0)