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

Quelle  outbound.test.ts

  Sprache: JAVA
 

/**
 * Tests for outbound.ts module
 *
 * Tests cover:
 * - resolveTarget with various modes (explicit, implicit, heartbeat)
 * - sendText with markdown stripping
 * - sendMedia delegation to sendText
 * - Error handling for missing accounts/channels
 * - Abort signal handling
 */


import { describe, expect, it, vi } from "vitest";
import { resolveTwitchAccountContext } from "./config.js";
import { twitchOutbound } from "./outbound.js";
import {
  BASE_TWITCH_TEST_ACCOUNT,
  installTwitchTestHooks,
  makeTwitchTestConfig,
} from "./test-fixtures.js";

// Mock dependencies
vi.mock("./config.js", () => ({
  DEFAULT_ACCOUNT_ID: "default",
  resolveTwitchAccountContext: vi.fn(),
}));

vi.mock("./send.js", () => ({
  sendMessageTwitchInternal: vi.fn(),
}));

vi.mock("./utils/markdown.js", () => ({
  chunkTextForTwitch: vi.fn((text) => text.split(/(.{500})/).filter(Boolean)),
}));

vi.mock("./utils/twitch.js", () => ({
  normalizeTwitchChannel: (channel: string) => channel.toLowerCase().replace(/^#/, ""),
  missingTargetError: (channel: string, hint: string) =>
    new Error(`Missing target for ${channel}. Provide ${hint}`),
}));

function assertResolvedTarget(
  result: ReturnType<NonNullable<typeof twitchOutbound.resolveTarget>>,
): string {
  if (!result.ok) {
    throw result.error;
  }
  return result.to;
}

function expectTargetError(
  resolveTarget: NonNullable<typeof twitchOutbound.resolveTarget>,
  params: Parameters<NonNullable<typeof twitchOutbound.resolveTarget>>[0],
  expectedMessage: string,
) {
  const result = resolveTarget(params);

  expect(result.ok).toBe(false);
  if (result.ok) {
    throw new Error("expected resolveTarget to fail");
  }
  expect(result.error.message).toContain(expectedMessage);
}

describe("outbound", () => {
  const mockAccount = {
    ...BASE_TWITCH_TEST_ACCOUNT,
    accessToken: "oauth:test123",
  };
  const resolveTarget = twitchOutbound.resolveTarget!;

  const mockConfig = makeTwitchTestConfig(mockAccount);
  installTwitchTestHooks();

  function setupAccountContext(params?: {
    account?: typeof mockAccount | null;
    availableAccountIds?: string[];
  }) {
    const account = params?.account === undefined ? mockAccount : params.account;
    vi.mocked(resolveTwitchAccountContext).mockImplementation((_cfg, accountId) => ({
      accountId: accountId?.trim() || "default",
      account,
      tokenResolution: { source: "config", token: account?.accessToken ?? "" },
      configured: account !== null,
      availableAccountIds: params?.availableAccountIds ?? ["default"],
    }));
  }

  describe("metadata", () => {
    it("should have direct delivery mode", () => {
      expect(twitchOutbound.deliveryMode).toBe("direct");
    });

    it("should have 500 character text chunk limit", () => {
      expect(twitchOutbound.textChunkLimit).toBe(500);
    });

    it("should chunk long messages at 500 characters", () => {
      const chunker = twitchOutbound.chunker;
      if (!chunker) {
        throw new Error("twitch outbound.chunker unavailable");
      }

      expect(chunker("a".repeat(600), 500)).toEqual(["a".repeat(500), "a".repeat(100)]);
    });
  });

  describe("resolveTarget", () => {
    it("should normalize and return target in explicit mode", () => {
      const result = resolveTarget({
        to: "#MyChannel",
        mode: "explicit",
        allowFrom: [],
      });

      expect(result.ok).toBe(true);
      expect(assertResolvedTarget(result)).toBe("mychannel");
    });

    it("should return target in implicit mode with wildcard allowlist", () => {
      const result = resolveTarget({
        to: "#AnyChannel",
        mode: "implicit",
        allowFrom: ["*"],
      });

      expect(result.ok).toBe(true);
      expect(assertResolvedTarget(result)).toBe("anychannel");
    });

    it("should return target in implicit mode when in allowlist", () => {
      const result = resolveTarget({
        to: "#allowed",
        mode: "implicit",
        allowFrom: ["#allowed""#other"],
      });

      expect(result.ok).toBe(true);
      expect(assertResolvedTarget(result)).toBe("allowed");
    });

    it("should error when target not in allowlist (implicit mode)", () => {
      expectTargetError(
        resolveTarget,
        {
          to: "#notallowed",
          mode: "implicit",
          allowFrom: ["#primary""#secondary"],
        },
        "Twitch",
      );
    });

    it("should accept any target when allowlist is empty", () => {
      const result = resolveTarget({
        to: "#anychannel",
        mode: "heartbeat",
        allowFrom: [],
      });

      expect(result.ok).toBe(true);
      expect(assertResolvedTarget(result)).toBe("anychannel");
    });

    it("should error when no target provided with allowlist", () => {
      expectTargetError(
        resolveTarget,
        {
          to: undefined,
          mode: "implicit",
          allowFrom: ["#fallback""#other"],
        },
        "Twitch",
      );
    });

    it("should return error when no target and no allowlist", () => {
      expectTargetError(
        resolveTarget,
        {
          to: undefined,
          mode: "explicit",
          allowFrom: [],
        },
        "Missing target",
      );
    });

    it("should handle whitespace-only target", () => {
      expectTargetError(
        resolveTarget,
        {
          to: "   ",
          mode: "explicit",
          allowFrom: [],
        },
        "Missing target",
      );
    });

    it("should error when target normalizes to empty string", () => {
      expectTargetError(
        resolveTarget,
        {
          to: "#",
          mode: "explicit",
          allowFrom: [],
        },
        "Twitch",
      );
    });

    it("should filter wildcard from allowlist when checking membership", () => {
      const result = resolveTarget({
        to: "#mychannel",
        mode: "implicit",
        allowFrom: ["*""#specific"],
      });

      // With wildcard, any target is accepted
      expect(result.ok).toBe(true);
      expect(assertResolvedTarget(result)).toBe("mychannel");
    });
  });

  describe("sendText", () => {
    it("should send message successfully", async () => {
      const { sendMessageTwitchInternal } = await import("./send.js");

      setupAccountContext();
      vi.mocked(sendMessageTwitchInternal).mockResolvedValue({
        ok: true,
        messageId: "twitch-msg-123",
      });

      const result = await twitchOutbound.sendText!({
        cfg: mockConfig,
        to: "#testchannel",
        text: "Hello Twitch!",
        accountId: "default",
      });

      expect(result.channel).toBe("twitch");
      expect(result.messageId).toBe("twitch-msg-123");
      expect(sendMessageTwitchInternal).toHaveBeenCalledWith(
        "testchannel",
        "Hello Twitch!",
        mockConfig,
        "default",
        true,
        console,
      );
      expect(result.timestamp).toBeGreaterThan(0);
    });

    it("should throw when account not found", async () => {
      setupAccountContext({ account: null });

      await expect(
        twitchOutbound.sendText!({
          cfg: mockConfig,
          to: "#testchannel",
          text: "Hello!",
          accountId: "nonexistent",
        }),
      ).rejects.toThrow("Twitch account not found: nonexistent");
    });

    it("should throw when no channel specified", async () => {
      const accountWithoutChannel = { ...mockAccount, channel: undefined as unknown as string };
      setupAccountContext({ account: accountWithoutChannel });

      await expect(
        twitchOutbound.sendText!({
          cfg: mockConfig,
          to: "",
          text: "Hello!",
          accountId: "default",
        }),
      ).rejects.toThrow("No channel specified");
    });

    it("should use account channel when target not provided", async () => {
      const { sendMessageTwitchInternal } = await import("./send.js");

      setupAccountContext();
      vi.mocked(sendMessageTwitchInternal).mockResolvedValue({
        ok: true,
        messageId: "msg-456",
      });

      await twitchOutbound.sendText!({
        cfg: mockConfig,
        to: "",
        text: "Hello!",
        accountId: "default",
      });

      expect(sendMessageTwitchInternal).toHaveBeenCalledWith(
        "testchannel",
        "Hello!",
        mockConfig,
        "default",
        true,
        console,
      );
    });

    it("uses configured defaultAccount when accountId is omitted", async () => {
      const { sendMessageTwitchInternal } = await import("./send.js");

      vi.mocked(resolveTwitchAccountContext)
        .mockImplementationOnce(() => ({
          accountId: "secondary",
          account: {
            ...mockAccount,
            channel: "secondary-channel",
          },
          tokenResolution: { source: "config", token: mockAccount.accessToken },
          configured: true,
          availableAccountIds: ["default""secondary"],
        }))
        .mockImplementation((_cfg, accountId) => ({
          accountId: accountId?.trim() || "secondary",
          account: {
            ...mockAccount,
            channel: "secondary-channel",
          },
          tokenResolution: { source: "config", token: mockAccount.accessToken },
          configured: true,
          availableAccountIds: ["default""secondary"],
        }));
      vi.mocked(sendMessageTwitchInternal).mockResolvedValue({
        ok: true,
        messageId: "msg-secondary",
      });

      await twitchOutbound.sendText!({
        cfg: {
          channels: {
            twitch: {
              defaultAccount: "secondary",
            },
          },
        } as typeof mockConfig,
        to: "#secondary-channel",
        text: "Hello!",
      });

      expect(sendMessageTwitchInternal).toHaveBeenCalledWith(
        "secondary-channel",
        "Hello!",
        expect.any(Object),
        "secondary",
        true,
        console,
      );
    });

    it("should handle abort signal", async () => {
      const abortController = new AbortController();
      abortController.abort();

      await expect(
        twitchOutbound.sendText!({
          cfg: mockConfig,
          to: "#testchannel",
          text: "Hello!",
          accountId: "default",
          signal: abortController.signal,
        } as Parameters<NonNullable<typeof twitchOutbound.sendText>>[0]),
      ).rejects.toThrow("Outbound delivery aborted");
    });

    it("should throw on send failure", async () => {
      const { sendMessageTwitchInternal } = await import("./send.js");

      setupAccountContext();
      vi.mocked(sendMessageTwitchInternal).mockResolvedValue({
        ok: false,
        messageId: "failed-msg",
        error: "Connection lost",
      });

      await expect(
        twitchOutbound.sendText!({
          cfg: mockConfig,
          to: "#testchannel",
          text: "Hello!",
          accountId: "default",
        }),
      ).rejects.toThrow("Connection lost");
    });
  });

  describe("sendMedia", () => {
    it("should combine text and media URL", async () => {
      const { sendMessageTwitchInternal } = await import("./send.js");

      setupAccountContext();
      vi.mocked(sendMessageTwitchInternal).mockResolvedValue({
        ok: true,
        messageId: "media-msg-123",
      });

      const result = await twitchOutbound.sendMedia!({
        cfg: mockConfig,
        to: "#testchannel",
        text: "Check this:",
        mediaUrl: "https://example.com/image.png",
        accountId: "default",
      });

      expect(result.channel).toBe("twitch");
      expect(result.messageId).toBe("media-msg-123");
      expect(sendMessageTwitchInternal).toHaveBeenCalledWith(
        expect.anything(),
        "Check this: https://example.com/image.png",
        expect.anything(),
        expect.anything(),
        expect.anything(),
        expect.anything(),
      );
    });

    it("should send media URL only when no text", async () => {
      const { sendMessageTwitchInternal } = await import("./send.js");

      setupAccountContext();
      vi.mocked(sendMessageTwitchInternal).mockResolvedValue({
        ok: true,
        messageId: "media-only-msg",
      });

      await twitchOutbound.sendMedia!({
        cfg: mockConfig,
        to: "#testchannel",
        text: "",
        mediaUrl: "https://example.com/image.png",
        accountId: "default",
      });

      expect(sendMessageTwitchInternal).toHaveBeenCalledWith(
        expect.anything(),
        "https://example.com/image.png",
        expect.anything(),
        expect.anything(),
        expect.anything(),
        expect.anything(),
      );
    });

    it("should handle abort signal", async () => {
      const abortController = new AbortController();
      abortController.abort();

      await expect(
        twitchOutbound.sendMedia!({
          cfg: mockConfig,
          to: "#testchannel",
          text: "Check this:",
          mediaUrl: "https://example.com/image.png",
          accountId: "default",
          signal: abortController.signal,
        } as Parameters<NonNullable<typeof twitchOutbound.sendMedia>>[0]),
      ).rejects.toThrow("Outbound delivery aborted");
    });
  });
});

Messung V0.5 in Prozent
C=99 H=98 G=98

¤ Dauer der Verarbeitung: 0.15 Sekunden  (vorverarbeitet am  2026-05-26) ¤

*© 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.