Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  monitor.test.ts

  Sprache: JAVA
 

Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

import type {
  ButtonInteraction,
  ComponentData,
  ModalInteraction,
  StringSelectMenuInteraction,
} from "@buape/carbon";
import { ChannelType } from "discord-api-types/v10";
import type { DiscordAccountConfig, OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { buildPluginBindingApprovalCustomId } from "openclaw/plugin-sdk/conversation-runtime";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { type DiscordComponentEntry, type DiscordModalEntry } from "../components.js";
import {
  dispatchPluginInteractiveHandlerMock,
  dispatchReplyMock,
  enqueueSystemEventMock,
  readSessionUpdatedAtMock,
  recordInboundSessionMock,
  resetDiscordComponentRuntimeMocks,
  resolveStorePathMock,
} from "../test-support/component-runtime.js";
import type { DiscordGuildEntryResolved } from "./allow-list.js";

type CreateDiscordComponentButton =
  typeof import("./agent-components.js").createDiscordComponentButton;
type CreateDiscordComponentModal =
  typeof import("./agent-components.js").createDiscordComponentModal;
type CreateDiscordComponentStringSelect =
  typeof import("./agent-components.js").createDiscordComponentStringSelect;
type DispatchReplyWithBufferedBlockDispatcherFn =
  typeof import("openclaw/plugin-sdk/reply-dispatch-runtime").dispatchReplyWithBufferedBlockDispatcher;
type DispatchReplyWithBufferedBlockDispatcherResult = Awaited<
  ReturnType<DispatchReplyWithBufferedBlockDispatcherFn>
>;

let createDiscordComponentButton: CreateDiscordComponentButton;
let createDiscordComponentStringSelect: CreateDiscordComponentStringSelect;
let createDiscordComponentModal: CreateDiscordComponentModal;
let clearDiscordComponentEntries: typeof import("../components-registry.js").clearDiscordComponentEntries;
let registerDiscordComponentEntries: typeof import("../components-registry.js").registerDiscordComponentEntries;
let resolveDiscordComponentEntry: typeof import("../components-registry.js").resolveDiscordComponentEntry;
let resolveDiscordModalEntry: typeof import("../components-registry.js").resolveDiscordModalEntry;
let sendComponents: typeof import("../send.components.js");

let lastDispatchCtx: Record<string, unknown> | undefined;

function getLastRecordedCtx(): Record<string, unknown> | undefined {
  const params = recordInboundSessionMock.mock.calls.at(-1)?.[0] as
    | { ctx?: Record<string, unknown> }
    | undefined;
  return params?.ctx;
}

describe("discord component interactions", () => {
  let editDiscordComponentMessageMock: ReturnType<typeof vi.spyOn>;
  const createCfg = (): OpenClawConfig =>
    ({
      channels: {
        discord: {
          replyToMode: "first",
        },
      },
    }) as OpenClawConfig;

  const createDiscordConfig = (overrides?: Partial<DiscordAccountConfig>): DiscordAccountConfig =>
    ({
      replyToMode: "first",
      ...overrides,
    }) as DiscordAccountConfig;

  type DispatchParams = Parameters<DispatchReplyWithBufferedBlockDispatcherFn>[0];

  type ComponentContext = Parameters<CreateDiscordComponentButton>[0];

  const createComponentContext = (overrides?: Partial<ComponentContext>) =>
    ({
      cfg: createCfg(),
      accountId: "default",
      dmPolicy: "allowlist",
      allowFrom: ["123456789"],
      discordConfig: createDiscordConfig(),
      token: "token",
      ...overrides,
    }) as ComponentContext;

  const createComponentInteractionBase = () => {
    const reply = vi.fn().mockResolvedValue(undefined);
    const defer = vi.fn().mockResolvedValue(undefined);
    const rest = {
      get: vi.fn().mockResolvedValue({ type: ChannelType.DM }),
      post: vi.fn().mockResolvedValue({}),
      patch: vi.fn().mockResolvedValue({}),
      delete: vi.fn().mockResolvedValue(undefined),
    };
    return {
      reply,
      defer,
      client: { rest },
      user: { id: "123456789", username: "AgentUser", discriminator: "0001" },
      message: { id: "msg-1" },
    };
  };

  const createComponentButtonInteraction = (overrides: Partial<ButtonInteraction> = {}) => {
    const base = createComponentInteractionBase();
    const interaction = {
      rawData: { channel_id: "dm-channel", id: "interaction-1" },
      customId: "occomp:cid=btn_1",
      ...base,
      ...overrides,
    } as unknown as ButtonInteraction;
    return { interaction, defer: base.defer, reply: base.reply };
  };

  const createComponentSelectInteraction = (
    overrides: Partial<StringSelectMenuInteraction> = {},
  ) => {
    const base = createComponentInteractionBase();
    const interaction = {
      rawData: { channel_id: "dm-channel", id: "interaction-select-1" },
      customId: "occomp:cid=sel_1",
      values: ["alpha"],
      ...base,
      ...overrides,
    } as unknown as StringSelectMenuInteraction;
    return { interaction, defer: base.defer, reply: base.reply };
  };

  const createModalInteraction = (overrides: Partial<ModalInteraction> = {}) => {
    const reply = vi.fn().mockResolvedValue(undefined);
    const acknowledge = vi.fn().mockResolvedValue(undefined);
    const rest = {
      get: vi.fn().mockResolvedValue({ type: ChannelType.DM }),
      post: vi.fn().mockResolvedValue({}),
      patch: vi.fn().mockResolvedValue({}),
      delete: vi.fn().mockResolvedValue(undefined),
    };
    const fields = {
      getText: (key: string) => (key === "fld_1" ? "Casey" : undefined),
      getStringSelect: (_key: string) => undefined,
      getRoleSelect: (_key: string) => [],
      getUserSelect: (_key: string) => [],
    };
    const interaction = {
      rawData: { channel_id: "dm-channel", id: "interaction-2" },
      user: { id: "123456789", username: "AgentUser", discriminator: "0001" },
      customId: "ocmodal:mid=mdl_1",
      fields,
      acknowledge,
      reply,
      client: { rest },
      ...overrides,
    } as unknown as ModalInteraction;
    return { interaction, acknowledge, reply };
  };

  const createButtonEntry = (
    overrides: Partial<DiscordComponentEntry> = {},
  ): DiscordComponentEntry => ({
    id: "btn_1",
    kind: "button",
    label: "Approve",
    messageId: "msg-1",
    sessionKey: "session-1",
    agentId: "agent-1",
    accountId: "default",
    ...overrides,
  });

  const createModalEntry = (overrides: Partial<DiscordModalEntry> = {}): DiscordModalEntry => ({
    id: "mdl_1",
    title: "Details",
    messageId: "msg-2",
    sessionKey: "session-2",
    agentId: "agent-2",
    accountId: "default",
    fields: [
      {
        id: "fld_1",
        name: "name",
        label: "Name",
        type: "text",
      },
    ],
    ...overrides,
  });

  const createGuildPluginButton = (allowFrom: string[]) =>
    createDiscordComponentButton(
      createComponentContext({
        cfg: {
          commands: { useAccessGroups: true },
          channels: { discord: { replyToMode: "first" } },
        } as OpenClawConfig,
        allowFrom,
      }),
    );

  const createGuildPluginButtonInteraction = (interactionId: string) =>
    createComponentButtonInteraction({
      rawData: {
        channel_id: "guild-channel",
        guild_id: "guild-1",
        id: interactionId,
        member: { roles: [] },
      } as unknown as ButtonInteraction["rawData"],
      guild: { id: "guild-1", name: "Test Guild" } as unknown as ButtonInteraction["guild"],
    });

  async function expectPluginGuildInteractionAuth(params: {
    allowFrom: string[];
    interactionId: string;
    isAuthorizedSender: boolean;
  }) {
    registerDiscordComponentEntries({
      entries: [createButtonEntry({ callbackData: "codex:approve" })],
      modals: [],
    });
    dispatchPluginInteractiveHandlerMock.mockResolvedValue({
      matched: true,
      handled: true,
      duplicate: false,
    });

    const button = createGuildPluginButton(params.allowFrom);
    const { interaction } = createGuildPluginButtonInteraction(params.interactionId);

    await button.run(interaction, { cid: "btn_1" } as ComponentData);

    expect(dispatchPluginInteractiveHandlerMock).toHaveBeenCalledWith(
      expect.objectContaining({
        ctx: expect.objectContaining({
          auth: { isAuthorizedSender: params.isAuthorizedSender },
        }),
      }),
    );
    expect(dispatchReplyMock).not.toHaveBeenCalled();
  }

  beforeAll(async () => {
    ({
      createDiscordComponentButton,
      createDiscordComponentStringSelect,
      createDiscordComponentModal,
    } = await import("./agent-components.js"));
    ({
      clearDiscordComponentEntries,
      registerDiscordComponentEntries,
      resolveDiscordComponentEntry,
      resolveDiscordModalEntry,
    } = await import("../components-registry.js"));
    sendComponents = await import("../send.components.js");
  });

  beforeEach(() => {
    editDiscordComponentMessageMock = vi
      .spyOn(sendComponents, "editDiscordComponentMessage")
      .mockResolvedValue({
        messageId: "msg-1",
        channelId: "dm-channel",
      });
    clearDiscordComponentEntries();
    resetDiscordComponentRuntimeMocks();
    lastDispatchCtx = undefined;
    enqueueSystemEventMock.mockClear();
    dispatchReplyMock
      .mockClear()
      .mockImplementation(
        async (params: DispatchParams): Promise<DispatchReplyWithBufferedBlockDispatcherResult> => {
          lastDispatchCtx = params.ctx;
          await params.dispatcherOptions.deliver({ text: "ok" }, { kind: "final" });
          return {
            queuedFinal: false,
            counts: {
              block: 0,
              final: 1,
              tool: 0,
            },
          };
        },
      );
    recordInboundSessionMock.mockClear().mockResolvedValue(undefined);
    readSessionUpdatedAtMock.mockClear().mockReturnValue(undefined);
    resolveStorePathMock.mockClear().mockReturnValue("/tmp/openclaw-sessions-test.json");
    dispatchPluginInteractiveHandlerMock.mockReset().mockResolvedValue({
      matched: false,
      handled: false,
      duplicate: false,
    });
  });

  it("routes button clicks with reply references", async () => {
    registerDiscordComponentEntries({
      entries: [createButtonEntry()],
      modals: [],
    });

    const button = createDiscordComponentButton(createComponentContext());
    const { interaction, reply } = createComponentButtonInteraction();

    await button.run(interaction, { cid: "btn_1" } as ComponentData);

    expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
    expect(lastDispatchCtx?.BodyForAgent).toBe('Clicked "Approve".');
    expect(dispatchReplyMock).toHaveBeenCalledTimes(1);
    expect(resolveDiscordComponentEntry({ id: "btn_1" })).toBeNull();
  });

  it("records DM component interactions with user originating targets", async () => {
    registerDiscordComponentEntries({
      entries: [createButtonEntry()],
      modals: [],
    });

    const button = createDiscordComponentButton(createComponentContext());
    const { interaction } = createComponentButtonInteraction();

    await button.run(interaction, { cid: "btn_1" } as ComponentData);

    expect(lastDispatchCtx?.OriginatingTo).toBe("user:123456789");
    expect(lastDispatchCtx?.To).toBe("channel:dm-channel");
    expect(getLastRecordedCtx()?.OriginatingTo).toBe("user:123456789");
    expect(getLastRecordedCtx()?.To).toBe("channel:dm-channel");
  });

  it("uses raw callbackData for built-in fallback when no plugin handler matches", async () => {
    registerDiscordComponentEntries({
      entries: [createButtonEntry({ callbackData: "/codex_resume --browse-projects" })],
      modals: [],
    });

    const button = createDiscordComponentButton(createComponentContext());
    const { interaction, reply } = createComponentButtonInteraction();

    await button.run(interaction, { cid: "btn_1" } as ComponentData);

    expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
    expect(lastDispatchCtx?.BodyForAgent).toBe("/codex_resume --browse-projects");
    expect(dispatchReplyMock).toHaveBeenCalledTimes(1);
  });

  it("preserves selected values for select fallback when no plugin handler matches", async () => {
    registerDiscordComponentEntries({
      entries: [
        {
          id: "sel_1",
          kind: "select",
          label: "Pick",
          messageId: "msg-1",
          sessionKey: "session-1",
          agentId: "agent-1",
          accountId: "default",
          callbackData: "/codex_resume",
          selectType: "string",
          options: [{ value: "alpha", label: "Alpha" }],
        },
      ],
      modals: [],
    });

    const select = createDiscordComponentStringSelect(createComponentContext());
    const { interaction, reply } = createComponentSelectInteraction();

    await select.run(interaction, { cid: "sel_1" } as ComponentData);

    expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
    expect(lastDispatchCtx?.BodyForAgent).toBe('Selected Alpha from "Pick".');
    expect(dispatchReplyMock).toHaveBeenCalledTimes(1);
  });

  it("keeps reusable buttons active after use", async () => {
    registerDiscordComponentEntries({
      entries: [createButtonEntry({ reusable: true })],
      modals: [],
    });

    const button = createDiscordComponentButton(createComponentContext());
    const { interaction } = createComponentButtonInteraction();
    await button.run(interaction, { cid: "btn_1" } as ComponentData);

    const { interaction: secondInteraction } = createComponentButtonInteraction({
      rawData: {
        channel_id: "dm-channel",
        id: "interaction-2",
      } as unknown as ButtonInteraction["rawData"],
    });
    await button.run(secondInteraction, { cid: "btn_1" } as ComponentData);

    expect(dispatchReplyMock).toHaveBeenCalledTimes(2);
    expect(resolveDiscordComponentEntry({ id: "btn_1", consume: false })).not.toBeNull();
  });

  it("blocks buttons when allowedUsers does not match", async () => {
    registerDiscordComponentEntries({
      entries: [createButtonEntry({ allowedUsers: ["999"] })],
      modals: [],
    });

    const button = createDiscordComponentButton(createComponentContext());
    const { interaction, reply } = createComponentButtonInteraction();

    await button.run(interaction, { cid: "btn_1" } as ComponentData);

    expect(reply).toHaveBeenCalledWith({
      content: "You are not authorized to use this button.",
      ephemeral: true,
    });
    expect(dispatchReplyMock).not.toHaveBeenCalled();
    expect(resolveDiscordComponentEntry({ id: "btn_1", consume: false })).not.toBeNull();
  });

  it("blocks buttons from guilds removed from the allowlist", async () => {
    registerDiscordComponentEntries({
      entries: [createButtonEntry()],
      modals: [],
    });

    const button = createDiscordComponentButton(
      createComponentContext({
        cfg: {
          channels: { discord: { replyToMode: "first", groupPolicy: "allowlist" } },
        } as OpenClawConfig,
        discordConfig: createDiscordConfig({ groupPolicy: "allowlist" }),
        guildEntries: {},
      }),
    );
    const { interaction, reply } = createComponentButtonInteraction({
      rawData: {
        channel_id: "guild-channel",
        guild_id: "gone",
        id: "interaction-guild-removed",
        member: { roles: [] },
      } as unknown as ButtonInteraction["rawData"],
      guild: { id: "gone", name: "Test Guild" } as unknown as ButtonInteraction["guild"],
    });

    await button.run(interaction, { cid: "btn_1" } as ComponentData);

    expect(reply).toHaveBeenCalledWith({
      content: "You are not authorized to use this button.",
      ephemeral: true,
    });
    expect(dispatchReplyMock).not.toHaveBeenCalled();
  });

  it.each([
    {
      title: "blocks buttons on disabled guild channels",
      guildId: "g1",
      interactionId: "interaction-guild-disabled",
      guildEntries: { g1: { channels: { "guild-channel": { enabled: false } } } },
    },
    {
      title: "blocks buttons on denied guild channels",
      guildId: "g1",
      interactionId: "interaction-guild-denied",
      guildEntries: { g1: { channels: { "guild-channel": { enabled: false } } } },
    },
  ])("$title", async ({ guildId, interactionId, guildEntries }) => {
    await expectBlockedGuildButton({ guildId, interactionId, guildEntries });
  });

  async function runModalSubmission(params?: { reusable?: boolean }) {
    registerDiscordComponentEntries({
      entries: [],
      modals: [createModalEntry({ reusable: params?.reusable ?? false })],
    });

    const modal = createDiscordComponentModal(
      createComponentContext({
        discordConfig: createDiscordConfig({ replyToMode: "all" }),
      }),
    );
    const { interaction, acknowledge } = createModalInteraction();

    await modal.run(interaction, { mid: "mdl_1" } as ComponentData);
    return { acknowledge };
  }

  async function expectBlockedGuildButton(params: {
    guildId: string;
    interactionId: string;
    guildEntries: Record<string, DiscordGuildEntryResolved>;
  }) {
    registerDiscordComponentEntries({
      entries: [createButtonEntry()],
      modals: [],
    });

    const button = createDiscordComponentButton(
      createComponentContext({
        cfg: {
          channels: { discord: { replyToMode: "first", groupPolicy: "allowlist" } },
        } as OpenClawConfig,
        discordConfig: createDiscordConfig({ groupPolicy: "allowlist" }),
        guildEntries: params.guildEntries,
      }),
    );
    const { interaction, reply } = createComponentButtonInteraction({
      rawData: {
        channel_id: "guild-channel",
        guild_id: params.guildId,
        id: params.interactionId,
        member: { roles: [] },
      } as unknown as ButtonInteraction["rawData"],
      guild: {
        id: params.guildId,
        name: "Test Guild",
      } as unknown as ButtonInteraction["guild"],
    });

    await button.run(interaction, { cid: "btn_1" } as ComponentData);

    expect(reply).toHaveBeenCalledWith({
      content: "You are not authorized to use this button.",
      ephemeral: true,
    });
    expect(dispatchReplyMock).not.toHaveBeenCalled();
  }

  async function expectGuildModalAuth(params: {
    allowFrom: string[];
    interactionId: string;
    expectedAuthorized: boolean;
  }) {
    registerDiscordComponentEntries({
      entries: [],
      modals: [createModalEntry()],
    });

    const modal = createDiscordComponentModal(
      createComponentContext({
        cfg: {
          commands: { useAccessGroups: true },
          channels: { discord: { replyToMode: "first" } },
        } as OpenClawConfig,
        allowFrom: params.allowFrom,
      }),
    );
    const { interaction, acknowledge } = createModalInteraction({
      rawData: {
        channel_id: "guild-channel",
        guild_id: "guild-1",
        id: params.interactionId,
        member: { roles: [] },
      } as unknown as ModalInteraction["rawData"],
      guild: { id: "guild-1", name: "Test Guild" } as unknown as ModalInteraction["guild"],
    });

    await modal.run(interaction, { mid: "mdl_1" } as ComponentData);

    expect(acknowledge).toHaveBeenCalledTimes(1);
    expect(dispatchReplyMock).toHaveBeenCalledTimes(1);
    expect(lastDispatchCtx?.CommandAuthorized).toBe(params.expectedAuthorized);
  }

  it("routes modal submissions with field values", async () => {
    const { acknowledge } = await runModalSubmission();

    expect(acknowledge).toHaveBeenCalledTimes(1);
    expect(lastDispatchCtx?.BodyForAgent).toContain('Form "Details" submitted.');
    expect(lastDispatchCtx?.BodyForAgent).toContain("- Name: Casey");
    expect(dispatchReplyMock).toHaveBeenCalledTimes(1);
    expect(resolveDiscordModalEntry({ id: "mdl_1" })).toBeNull();
  });

  it.each([
    {
      title: "does not mark guild modal events as command-authorized for non-allowlisted users",
      allowFrom: ["owner-1"],
      interactionId: "interaction-guild-1",
      expectedAuthorized: false,
    },
    {
      title: "marks guild modal events as command-authorized for allowlisted users",
      allowFrom: ["123456789"],
      interactionId: "interaction-guild-2",
      expectedAuthorized: true,
    },
  ])("$title", async ({ allowFrom, interactionId, expectedAuthorized }) => {
    await expectGuildModalAuth({ allowFrom, interactionId, expectedAuthorized });
  });

  it("keeps reusable modal entries active after submission", async () => {
    const { acknowledge } = await runModalSubmission({ reusable: true });

    expect(acknowledge).toHaveBeenCalledTimes(1);
    expect(resolveDiscordModalEntry({ id: "mdl_1", consume: false })).not.toBeNull();
  });

  it("passes false auth to plugin Discord interactions for non-allowlisted guild users", async () => {
    await expectPluginGuildInteractionAuth({
      allowFrom: ["owner-1"],
      interactionId: "interaction-guild-plugin-1",
      isAuthorizedSender: false,
    });
  });

  it("passes true auth to plugin Discord interactions for allowlisted guild users", async () => {
    await expectPluginGuildInteractionAuth({
      allowFrom: ["123456789"],
      interactionId: "interaction-guild-plugin-2",
      isAuthorizedSender: true,
    });
  });

  it("routes plugin Discord interactions in group DMs by channel id instead of sender id", async () => {
    registerDiscordComponentEntries({
      entries: [createButtonEntry({ callbackData: "codex:approve" })],
      modals: [],
    });
    dispatchPluginInteractiveHandlerMock.mockResolvedValue({
      matched: true,
      handled: true,
      duplicate: false,
    });

    const button = createDiscordComponentButton(
      createComponentContext({
        discordConfig: createDiscordConfig({
          dm: {
            groupEnabled: true,
            groupChannels: ["group-dm-1"],
          },
        }),
      }),
    );
    const { interaction } = createComponentButtonInteraction({
      rawData: {
        channel_id: "group-dm-1",
        id: "interaction-group-dm-1",
      } as unknown as ButtonInteraction["rawData"],
      channel: {
        id: "group-dm-1",
        type: ChannelType.GroupDM,
      } as unknown as ButtonInteraction["channel"],
    });

    await button.run(interaction, { cid: "btn_1" } as ComponentData);

    expect(dispatchPluginInteractiveHandlerMock).toHaveBeenCalledWith(
      expect.objectContaining({
        ctx: expect.objectContaining({
          conversationId: "channel:group-dm-1",
          senderId: "123456789",
        }),
      }),
    );
    expect(dispatchReplyMock).not.toHaveBeenCalled();
  });

  it("marks built-in Group DM component fallbacks with group metadata", async () => {
    registerDiscordComponentEntries({
      entries: [createButtonEntry()],
      modals: [],
    });

    const button = createDiscordComponentButton(
      createComponentContext({
        discordConfig: createDiscordConfig({
          dm: {
            groupEnabled: true,
            groupChannels: ["group-dm-1"],
          },
        }),
      }),
    );
    const { interaction, reply } = createComponentButtonInteraction({
      rawData: {
        channel_id: "group-dm-1",
        id: "interaction-group-dm-fallback",
      } as unknown as ButtonInteraction["rawData"],
      channel: {
        id: "group-dm-1",
        type: ChannelType.GroupDM,
        name: "incident-room",
      } as unknown as ButtonInteraction["channel"],
    });

    await button.run(interaction, { cid: "btn_1" } as ComponentData);

    expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
    expect(dispatchReplyMock).toHaveBeenCalledTimes(1);
    expect(lastDispatchCtx).toMatchObject({
      From: "discord:group:group-dm-1",
      ChatType: "group",
      ConversationLabel: "Group DM #incident-room channel id:group-dm-1",
    });
  });

  it("blocks Group DM modal triggers before showing the modal", async () => {
    registerDiscordComponentEntries({
      entries: [createButtonEntry({ kind: "modal-trigger", modalId: "mdl_1" })],
      modals: [createModalEntry()],
    });

    const button = createDiscordComponentButton(createComponentContext());
    const showModal = vi.fn().mockResolvedValue(undefined);
    const { interaction, reply } = createComponentButtonInteraction({
      rawData: {
        channel_id: "group-dm-1",
        id: "interaction-group-dm-modal-trigger",
      } as unknown as ButtonInteraction["rawData"],
      channel: {
        id: "group-dm-1",
        type: ChannelType.GroupDM,
        name: "incident-room",
      } as unknown as ButtonInteraction["channel"],
      showModal,
    });

    await button.run(interaction, { cid: "btn_1", mid: "mdl_1" } as ComponentData);

    expect(reply).toHaveBeenCalledWith({
      content: "Group DM interactions are disabled.",
      ephemeral: true,
    });
    expect(showModal).not.toHaveBeenCalled();
    expect(dispatchReplyMock).not.toHaveBeenCalled();
  });

  it("does not fall through to Claw when a plugin Discord interaction already replied", async () => {
    registerDiscordComponentEntries({
      entries: [createButtonEntry({ callbackData: "codex:approve" })],
      modals: [],
    });
    dispatchPluginInteractiveHandlerMock.mockImplementation(async (params: unknown) => {
      const typedParams = params as {
        respond: { reply: (payload: { text: string; ephemeral: boolean }) => Promise<void> };
      };
      await typedParams.respond.reply({ text: "✓", ephemeral: true });
      return {
        matched: true,
        handled: true,
        duplicate: false,
      };
    });

    const button = createDiscordComponentButton(createComponentContext());
    const { interaction, reply } = createComponentButtonInteraction();

    await button.run(interaction, { cid: "btn_1" } as ComponentData);

    expect(dispatchPluginInteractiveHandlerMock).toHaveBeenCalledTimes(1);
    expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
    expect(dispatchReplyMock).not.toHaveBeenCalled();
  });

  it("lets plugin Discord interactions clear components after acknowledging", async () => {
    registerDiscordComponentEntries({
      entries: [createButtonEntry({ callbackData: "codex:approve" })],
      modals: [],
    });
    dispatchPluginInteractiveHandlerMock.mockImplementation(async (params: unknown) => {
      const typedParams = params as {
        respond: {
          acknowledge: () => Promise<void>;
          clearComponents: (payload: { text: string }) => Promise<void>;
        };
      };
      await typedParams.respond.acknowledge();
      await typedParams.respond.clearComponents({ text: "Handled" });
      return {
        matched: true,
        handled: true,
        duplicate: false,
      };
    });

    const button = createDiscordComponentButton(createComponentContext());
    const acknowledge = vi.fn().mockResolvedValue(undefined);
    const reply = vi.fn().mockResolvedValue(undefined);
    const update = vi.fn().mockResolvedValue(undefined);
    const baseInteraction = createComponentButtonInteraction().interaction as unknown as Record<
      string,
      unknown
    >;
    const interaction = {
      ...baseInteraction,
      acknowledge,
      reply,
      update,
    } as unknown as ButtonInteraction;

    await button.run(interaction, { cid: "btn_1" } as ComponentData);

    expect(acknowledge).toHaveBeenCalledTimes(1);
    expect(reply).toHaveBeenCalledWith({
      content: "Handled",
      components: [],
    });
    expect(update).not.toHaveBeenCalled();
    expect(dispatchReplyMock).not.toHaveBeenCalled();
  });

  it("falls through to built-in Discord component routing when a plugin declines handling", async () => {
    registerDiscordComponentEntries({
      entries: [createButtonEntry({ callbackData: "codex:approve" })],
      modals: [],
    });
    dispatchPluginInteractiveHandlerMock.mockResolvedValue({
      matched: true,
      handled: false,
      duplicate: false,
    });

    const button = createDiscordComponentButton(createComponentContext());
    const { interaction, reply } = createComponentButtonInteraction();

    await button.run(interaction, { cid: "btn_1" } as ComponentData);

    expect(dispatchPluginInteractiveHandlerMock).toHaveBeenCalledTimes(1);
    expect(reply).toHaveBeenCalledWith({ content: "✓", ephemeral: true });
    expect(dispatchReplyMock).toHaveBeenCalledTimes(1);
  });

  it("resolves plugin binding approvals without falling through to Claw", async () => {
    registerDiscordComponentEntries({
      entries: [
        createButtonEntry({
          callbackData: buildPluginBindingApprovalCustomId("approval-1", "allow-once"),
        }),
      ],
      modals: [],
    });
    const button = createDiscordComponentButton(createComponentContext());
    const acknowledge = vi.fn().mockResolvedValue(undefined);
    const followUp = vi.fn().mockResolvedValue(undefined);
    const baseInteraction = createComponentButtonInteraction().interaction as unknown as Record<
      string,
      unknown
    >;
    const interaction = {
      ...baseInteraction,
      acknowledge,
      followUp,
    } as unknown as ButtonInteraction;

    await button.run(interaction, { cid: "btn_1" } as ComponentData);

    expect(acknowledge).toHaveBeenCalledTimes(1);
    expect(editDiscordComponentMessageMock).toHaveBeenCalledWith(
      "user:123456789",
      "msg-1",
      { text: expect.any(String) },
      expect.objectContaining({ accountId: "default" }),
    );
    expect(dispatchReplyMock).not.toHaveBeenCalled();
  });
});

¤ Dauer der Verarbeitung: 0.24 Sekunden  (vorverarbeitet am  2026-04-27) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge