Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/Java/Openclaw/extensions/discord/src/   (KI Agentensystem Version 22©)  Datei vom 26.3.2026 mit Größe 14 kB image not shown  

Quelle  channel.test.ts

  Sprache: JAVA
 

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

import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { PluginRuntime } from "../../../src/plugins/runtime/types.js";
import { createStartAccountContext } from "../../../test/helpers/plugins/start-account-context.js";
import type { ResolvedDiscordAccount } from "./accounts.js";
import type { OpenClawConfig } from "./runtime-api.js";
import * as sendModule from "./send.js";
import { EMPTY_DISCORD_TEST_CONFIG } from "./test-support/config.js";
let discordPlugin: typeof import("./channel.js").discordPlugin;
let setDiscordRuntime: typeof import("./runtime.js").setDiscordRuntime;

const probeDiscordMock = vi.hoisted(() => vi.fn());
const monitorDiscordProviderMock = vi.hoisted(() => vi.fn());
const auditDiscordChannelPermissionsMock = vi.hoisted(() => vi.fn());
const collectDiscordAuditChannelIdsMock = vi.hoisted(() =>
  vi.fn(() => ({ channelIds: [], unresolvedChannels: 0 })),
);
const sleepWithAbortMock = vi.hoisted(() => vi.fn(async () => undefined));

vi.mock("openclaw/plugin-sdk/runtime-env", async () => {
  const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/runtime-env")>(
    "openclaw/plugin-sdk/runtime-env",
  );
  return {
    ...actual,
    sleepWithAbort: sleepWithAbortMock,
  };
});

vi.mock("./probe.js", () => {
  return {
    probeDiscord: probeDiscordMock,
  };
});

vi.mock("./monitor/provider.runtime.js", () => {
  return {
    monitorDiscordProvider: monitorDiscordProviderMock,
  };
});

vi.mock("./audit.js", () => {
  return {
    auditDiscordChannelPermissions: auditDiscordChannelPermissionsMock,
    collectDiscordAuditChannelIds: collectDiscordAuditChannelIdsMock,
  };
});

function createCfg(): OpenClawConfig {
  return {
    channels: {
      discord: {
        enabled: true,
        token: "discord-token",
      },
    },
  } as OpenClawConfig;
}

function resolveAccount(cfg: OpenClawConfig, accountId = "default"): ResolvedDiscordAccount {
  return discordPlugin.config.resolveAccount(cfg, accountId);
}

function startDiscordAccount(cfg: OpenClawConfig, accountId = "default") {
  return discordPlugin.gateway!.startAccount!(
    createStartAccountContext({
      account: resolveAccount(cfg, accountId),
      cfg,
    }),
  );
}

function installDiscordRuntime(discord: Record<string, unknown>) {
  setDiscordRuntime({
    channel: {
      discord,
    },
    logging: {
      shouldLogVerbose: () => false,
    },
  } as unknown as PluginRuntime);
}

afterEach(() => {
  probeDiscordMock.mockReset();
  monitorDiscordProviderMock.mockReset();
  auditDiscordChannelPermissionsMock.mockReset();
  collectDiscordAuditChannelIdsMock.mockReset();
  collectDiscordAuditChannelIdsMock.mockReturnValue({
    channelIds: [],
    unresolvedChannels: 0,
  });
  sleepWithAbortMock.mockReset();
  sleepWithAbortMock.mockResolvedValue(undefined);
});

beforeEach(async () => {
  vi.useRealTimers();
  installDiscordRuntime({});
});

beforeAll(async () => {
  ({ discordPlugin } = await import("./channel.js"));
  ({ setDiscordRuntime } = await import("./runtime.js"));
});

describe("discordPlugin outbound", () => {
  it("avoids local require calls for bundled-only sibling modules", async () => {
    const source = await readFile(
      resolve(process.cwd(), "extensions/discord/src/channel.ts"),
      "utf8",
    );
    expect(source).not.toContain('require("./ui.js")');
    expect(source).not.toContain('require("./channel-actions.js")');
  });

  it("prefers final assistant text for text-only cron announce delivery", () => {
    expect(discordPlugin.outbound?.preferFinalAssistantVisibleText).toBe(true);
  });

  it("honors per-account replyToMode overrides", () => {
    const resolveReplyToMode = discordPlugin.threading?.resolveReplyToMode;
    if (!resolveReplyToMode) {
      throw new Error("Expected discordPlugin.threading.resolveReplyToMode to be defined");
    }

    const cfg = {
      channels: {
        discord: {
          replyToMode: "all",
          token: "discord-token",
          accounts: {
            work: {
              token: "discord-token-work",
              replyToMode: "first",
            },
          },
        },
      },
    } as OpenClawConfig;

    expect(resolveReplyToMode({ cfg, accountId: "work" })).toBe("first");
    expect(resolveReplyToMode({ cfg, accountId: "default" })).toBe("all");
  });

  it("forwards full media send context to sendMessageDiscord", async () => {
    const sendMessageDiscord = vi.fn(async () => ({ messageId: "m1" }));
    const mediaReadFile = vi.fn(async () => Buffer.from("media"));

    const result = await discordPlugin.outbound!.sendMedia!({
      cfg: EMPTY_DISCORD_TEST_CONFIG,
      to: "channel:123",
      text: "hi",
      mediaUrl: "/tmp/image.png",
      mediaLocalRoots: ["/tmp/agent-root"],
      mediaReadFile,
      accountId: "work",
      threadId: "thread-123",
      replyToId: "reply-123",
      deps: {
        discord: sendMessageDiscord,
      },
    });

    expect(sendMessageDiscord).toHaveBeenCalledWith(
      "channel:thread-123",
      "hi",
      expect.objectContaining({
        mediaUrl: "/tmp/image.png",
        mediaLocalRoots: ["/tmp/agent-root"],
        mediaReadFile,
        replyTo: "reply-123",
      }),
    );
    expect(result).toMatchObject({ channel: "discord", messageId: "m1" });
  });

  it("splits text and video into separate sends for attached outbound delivery", async () => {
    const sendMessageDiscord = vi
      .fn()
      .mockResolvedValueOnce({ messageId: "text-1" })
      .mockResolvedValueOnce({ messageId: "video-1" });

    const result = await discordPlugin.outbound!.sendMedia!({
      cfg: EMPTY_DISCORD_TEST_CONFIG,
      to: "channel:123",
      text: "done - tiny cyber-lobster clip incoming",
      mediaUrl: "/tmp/molty.mp4",
      accountId: "work",
      replyToId: "reply-123",
      threadId: "thread-123",
      deps: {
        discord: sendMessageDiscord,
      },
    });

    expect(sendMessageDiscord).toHaveBeenCalledTimes(2);
    expect(sendMessageDiscord).toHaveBeenNthCalledWith(
      1,
      "channel:thread-123",
      "done - tiny cyber-lobster clip incoming",
      expect.objectContaining({
        replyTo: "reply-123",
      }),
    );
    expect(sendMessageDiscord).toHaveBeenNthCalledWith(
      2,
      "channel:thread-123",
      "",
      expect.objectContaining({
        mediaUrl: "/tmp/molty.mp4",
      }),
    );
    expect(result).toMatchObject({ channel: "discord", messageId: "video-1" });
  });

  it("threads poll sends through the thread target", async () => {
    const sendPollDiscord = vi.fn(async () => ({
      channelId: "channel:thread-123",
      messageId: "poll-1",
    }));
    const sendPollSpy = vi.spyOn(sendModule, "sendPollDiscord").mockImplementation(sendPollDiscord);
    try {
      const result = await discordPlugin.outbound!.sendPoll!({
        cfg: EMPTY_DISCORD_TEST_CONFIG,
        to: "channel:123",
        poll: {
          question: "Best shell?",
          options: ["molty", "molter"],
        },
        accountId: "work",
        threadId: "thread-123",
      });

      expect(sendPollDiscord).toHaveBeenCalledWith(
        "channel:thread-123",
        {
          question: "Best shell?",
          options: ["molty", "molter"],
        },
        expect.objectContaining({
          accountId: "work",
        }),
      );
      expect(result).toMatchObject({ channel: "discord", messageId: "poll-1" });
    } finally {
      sendPollSpy.mockRestore();
    }
  });

  it("forwards heartbeat typing through the run config and attached target", async () => {
    const sendTypingDiscord = vi.fn(async () => ({ ok: true, channelId: "thread-123" }));
    const sendTypingSpy = vi
      .spyOn(sendModule, "sendTypingDiscord")
      .mockImplementation(sendTypingDiscord);
    try {
      const cfg = createCfg();

      await discordPlugin.heartbeat!.sendTyping!({
        cfg,
        to: "channel:123",
        accountId: "work",
        threadId: "thread-123",
      });

      expect(sendTypingDiscord).toHaveBeenCalledWith("thread-123", {
        cfg,
        accountId: "work",
      });
    } finally {
      sendTypingSpy.mockRestore();
    }
  });

  it("uses direct Discord probe helpers for status probes", async () => {
    const runtimeProbeDiscord = vi.fn(async () => {
      throw new Error("runtime Discord probe should not be used");
    });
    installDiscordRuntime({
      probeDiscord: runtimeProbeDiscord,
    });
    probeDiscordMock.mockResolvedValue({
      ok: true,
      bot: { username: "Bob" },
      application: {
        intents: {
          messageContent: "limited",
          guildMembers: "disabled",
          presence: "disabled",
        },
      },
      elapsedMs: 1,
    });

    const cfg = createCfg();
    const account = resolveAccount(cfg);

    await discordPlugin.status!.probeAccount!({
      account,
      timeoutMs: 5000,
      cfg,
    });

    expect(probeDiscordMock).toHaveBeenCalledWith("discord-token", 5000, {
      includeApplication: true,
    });
    expect(runtimeProbeDiscord).not.toHaveBeenCalled();
  });

  it("uses direct Discord startup helpers before monitoring", async () => {
    const runtimeProbeDiscord = vi.fn(async () => {
      throw new Error("runtime Discord probe should not be used");
    });
    const runtimeMonitorDiscordProvider = vi.fn(async () => {
      throw new Error("runtime Discord monitor should not be used");
    });
    installDiscordRuntime({
      probeDiscord: runtimeProbeDiscord,
      monitorDiscordProvider: runtimeMonitorDiscordProvider,
    });
    probeDiscordMock.mockResolvedValue({
      ok: true,
      bot: { username: "Bob" },
      application: {
        intents: {
          messageContent: "limited",
          guildMembers: "disabled",
          presence: "disabled",
        },
      },
      elapsedMs: 1,
    });
    monitorDiscordProviderMock.mockResolvedValue(undefined);

    const cfg = createCfg();
    await startDiscordAccount(cfg);

    expect(probeDiscordMock).toHaveBeenCalledWith("discord-token", 2500, {
      includeApplication: true,
    });
    expect(monitorDiscordProviderMock).toHaveBeenCalledWith(
      expect.objectContaining({
        token: "discord-token",
        accountId: "default",
      }),
    );
    expect(sleepWithAbortMock).not.toHaveBeenCalled();
    expect(runtimeProbeDiscord).not.toHaveBeenCalled();
    expect(runtimeMonitorDiscordProvider).not.toHaveBeenCalled();
  });

  it("stagger starts later accounts in multi-bot setups", async () => {
    probeDiscordMock.mockResolvedValue({
      ok: true,
      bot: { username: "Cherry" },
      application: {
        intents: {
          messageContent: "limited",
          guildMembers: "disabled",
          presence: "disabled",
        },
      },
      elapsedMs: 1,
    });
    monitorDiscordProviderMock.mockResolvedValue(undefined);

    const cfg = {
      channels: {
        discord: {
          accounts: {
            // "alpha" sorts before "zeta" so alpha is index 0, zeta is index 1
            alpha: { token: "Bot alpha-token", enabled: true },
            zeta: { token: "Bot zeta-token", enabled: true },
          },
        },
      },
    } as OpenClawConfig;

    // First account (index 0) — no delay
    await startDiscordAccount(cfg, "alpha");
    expect(sleepWithAbortMock).not.toHaveBeenCalled();

    // Second account (index 1) — 10s delay
    await startDiscordAccount(cfg, "zeta");
    expect(sleepWithAbortMock).toHaveBeenCalledWith(10_000, expect.any(Object));
  });
});

describe("discordPlugin bindings", () => {
  it("derives DM current conversation ids from direct sender context", () => {
    const result = discordPlugin.bindings?.resolveCommandConversation?.({
      accountId: "default",
      chatType: "direct",
      from: "discord:123456789012345678",
      originatingTo: "channel:dm-channel-1",
      fallbackTo: "channel:dm-channel-1",
    });

    expect(result).toEqual({
      conversationId: "user:123456789012345678",
    });
  });

  it("preserves user-prefixed current conversation ids for DM binds", () => {
    const result = discordPlugin.bindings?.resolveCommandConversation?.({
      accountId: "default",
      originatingTo: "user:123456789012345678",
    });

    expect(result).toEqual({
      conversationId: "user:123456789012345678",
    });
  });

  it("preserves channel-prefixed current conversation ids for channel binds", () => {
    const result = discordPlugin.bindings?.resolveCommandConversation?.({
      accountId: "default",
      originatingTo: "channel:987654321098765432",
    });

    expect(result).toEqual({
      conversationId: "channel:987654321098765432",
    });
  });

  it("preserves channel-prefixed parent ids for thread binds", () => {
    const result = discordPlugin.bindings?.resolveCommandConversation?.({
      accountId: "default",
      originatingTo: "channel:thread-42",
      threadId: "thread-42",
      threadParentId: "parent-9",
    });

    expect(result).toEqual({
      conversationId: "thread-42",
      parentConversationId: "channel:parent-9",
    });
  });
});

describe("discordPlugin security", () => {
  it("normalizes dm allowlist entries with trimmed prefixes and mentions", () => {
    const resolveDmPolicy = discordPlugin.security?.resolveDmPolicy;
    if (!resolveDmPolicy) {
      throw new Error("resolveDmPolicy unavailable");
    }

    const cfg = {
      channels: {
        discord: {
          token: "discord-token",
          dm: { policy: "allowlist", allowFrom: ["  discord:<@!123456789>  "] },
        },
      },
    } as OpenClawConfig;

    const result = resolveDmPolicy({
      cfg,
      account: discordPlugin.config.resolveAccount(cfg, "default"),
    });
    if (!result) {
      throw new Error("discord resolveDmPolicy returned null");
    }

    expect(result.policy).toBe("allowlist");
    expect(result.allowFrom).toEqual(["  discord:<@!123456789>  "]);
    expect(result.normalizeEntry?.("  discord:<@!123456789>  ")).toBe("123456789");
    expect(result.normalizeEntry?.("  user:987654321  ")).toBe("987654321");
  });
});

describe("discordPlugin groups", () => {
  it("uses plugin-owned group policy resolvers", () => {
    const cfg = {
      channels: {
        discord: {
          token: "discord-test",
          guilds: {
            guild1: {
              requireMention: false,
              tools: { allow: ["message.guild"] },
              channels: {
                "123": {
                  requireMention: true,
                  tools: { allow: ["message.channel"] },
                },
              },
            },
          },
        },
      },
    } as OpenClawConfig;

    expect(
      discordPlugin.groups?.resolveRequireMention?.({
        cfg,
        groupSpace: "guild1",
        groupId: "123",
      }),
    ).toBe(true);
    expect(
      discordPlugin.groups?.resolveToolPolicy?.({
        cfg,
        groupSpace: "guild1",
        groupId: "123",
      }),
    ).toEqual({ allow: ["message.channel"] });
  });
});

¤ Dauer der Verarbeitung: 0.26 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.