























@@ -32,6 +32,8 @@ vi.mock("openclaw/plugin-sdk/agent-runtime", () => ({
32323333let listNativeCommandSpecs: typeof import("openclaw/plugin-sdk/command-auth").listNativeCommandSpecs;
3434let createDiscordNativeCommand: typeof import("./native-command.js").createDiscordNativeCommand;
35+let nativeCommandTesting: typeof import("./native-command.js").testing;
36+let resolveDiscordNativeAutocompleteAuthorized: typeof import("./native-command-auth.js").resolveDiscordNativeAutocompleteAuthorized;
3537let createNoopThreadBindingManager: typeof import("./thread-bindings.js").createNoopThreadBindingManager;
36383739function createNativeCommand(
@@ -116,6 +118,29 @@ function requireAutocomplete(option: CommandOption, errorMessage: string) {
116118return autocomplete as (interaction: unknown) => Promise<unknown>;
117119}
118120121+function createAllowedGuildAutocompleteConfig(
122+commands: NonNullable<OpenClawConfig["commands"]>,
123+): OpenClawConfig {
124+return {
125+ commands,
126+channels: {
127+discord: {
128+groupPolicy: "allowlist",
129+guilds: {
130+"guild-1": {
131+channels: {
132+"channel-1": {
133+enabled: true,
134+requireMention: false,
135+},
136+},
137+},
138+},
139+},
140+},
141+} as OpenClawConfig;
142+}
143+119144async function runAutocomplete(
120145autocomplete: (interaction: unknown) => Promise<unknown>,
121146params: {
@@ -156,10 +181,43 @@ async function runAutocomplete(
156181return respond;
157182}
158183184+async function resolveAutocompleteAuthorized(params: {
185+cfg: OpenClawConfig;
186+userId: string;
187+username?: string;
188+globalName?: string;
189+}) {
190+return await resolveDiscordNativeAutocompleteAuthorized({
191+cfg: params.cfg,
192+discordConfig: params.cfg.channels?.discord ?? {},
193+accountId: "default",
194+interaction: {
195+user: {
196+id: params.userId,
197+username: params.username ?? params.userId,
198+globalName: params.globalName ?? params.userId,
199+},
200+channel: {
201+type: ChannelType.GuildText,
202+id: "channel-1",
203+name: "general",
204+},
205+guild: { id: "guild-1" },
206+rawData: {
207+member: { roles: [] },
208+},
209+client: {},
210+} as never,
211+});
212+}
213+159214describe("createDiscordNativeCommand option wiring", () => {
160215beforeAll(async () => {
161216({ listNativeCommandSpecs } = await import("openclaw/plugin-sdk/command-auth"));
162-({ createDiscordNativeCommand } = await import("./native-command.js"));
217+({ createDiscordNativeCommand, testing: nativeCommandTesting } = await import(
218+"./native-command.js"
219+));
220+({ resolveDiscordNativeAutocompleteAuthorized } = await import("./native-command-auth.js"));
163221({ createNoopThreadBindingManager } = await import("./thread-bindings.js"));
164222});
165223@@ -230,6 +288,114 @@ describe("createDiscordNativeCommand option wiring", () => {
230288expect(respond).toHaveBeenCalledWith([]);
231289});
232290291+it("rejects autocomplete when commands.ownerAllowFrom rejects the sender", async () => {
292+await expect(
293+resolveAutocompleteAuthorized({
294+cfg: createAllowedGuildAutocompleteConfig({
295+ownerAllowFrom: ["user:owner-user"],
296+}),
297+userId: "blocked-user",
298+username: "blocked",
299+globalName: "Blocked",
300+}),
301+).resolves.toBe(false);
302+});
303+304+it("authorizes autocomplete for commands.allowFrom users when commands.ownerAllowFrom is configured", async () => {
305+await expect(
306+resolveAutocompleteAuthorized({
307+cfg: createAllowedGuildAutocompleteConfig({
308+ownerAllowFrom: ["user:owner-user"],
309+allowFrom: {
310+discord: ["user:allowed-user"],
311+},
312+}),
313+userId: "blocked-user",
314+username: "blocked",
315+globalName: "Blocked",
316+}),
317+).resolves.toBe(false);
318+await expect(
319+resolveAutocompleteAuthorized({
320+cfg: createAllowedGuildAutocompleteConfig({
321+ownerAllowFrom: ["user:owner-user"],
322+allowFrom: {
323+discord: ["user:allowed-user"],
324+},
325+}),
326+userId: "allowed-user",
327+username: "allowed",
328+globalName: "Allowed",
329+}),
330+).resolves.toBe(true);
331+});
332+333+it("keeps plugin command autocomplete aligned with dispatch owner checks", async () => {
334+const restoreMatchPluginCommand = nativeCommandTesting.setMatchPluginCommand((prompt) =>
335+prompt === "/pair" ? ({ command: { name: "pair" }, args: "" } as never) : null,
336+);
337+try {
338+const command = createDiscordNativeCommand({
339+command: {
340+name: "pair",
341+description: "Pair",
342+acceptsArgs: true,
343+args: [
344+{
345+name: "mode",
346+description: "Pairing mode",
347+type: "string",
348+preferAutocomplete: true,
349+choices: () => [
350+{ label: "fast", value: "fast" },
351+{ label: "secure", value: "secure" },
352+],
353+},
354+],
355+},
356+cfg: createAllowedGuildAutocompleteConfig({
357+ownerAllowFrom: ["user:owner-user"],
358+}),
359+discordConfig: {
360+groupPolicy: "allowlist",
361+guilds: {
362+"guild-1": {
363+channels: {
364+"channel-1": {
365+enabled: true,
366+requireMention: false,
367+},
368+},
369+},
370+},
371+},
372+accountId: "default",
373+sessionPrefix: "discord:slash",
374+ephemeralDefault: true,
375+threadBindings: createNoopThreadBindingManager("default"),
376+});
377+const mode = requireOption(command, "mode");
378+const autocomplete = requireAutocomplete(mode, "plugin mode option did not wire autocomplete");
379+const respond = await runAutocomplete(autocomplete, {
380+userId: "blocked-user",
381+username: "blocked",
382+globalName: "Blocked",
383+channelType: ChannelType.GuildText,
384+channelId: "channel-1",
385+channelName: "general",
386+guildId: "guild-1",
387+focusedValue: "",
388+});
389+390+expect(respond).toHaveBeenCalledWith([
391+{ name: "fast", value: "fast" },
392+{ name: "secure", value: "secure" },
393+]);
394+} finally {
395+nativeCommandTesting.setMatchPluginCommand(restoreMatchPluginCommand);
396+}
397+});
398+233399it("returns no autocomplete choices outside the Discord allowlist when commands.useAccessGroups is false and commands.allowFrom is not configured", async () => {
234400const command = createNativeCommand("think", {
235401cfg: {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。