Spaces:
Paused
Paused
| /** | |
| * Tests for send.ts module | |
| * | |
| * Tests cover: | |
| * - Message sending with valid configuration | |
| * - Account resolution and validation | |
| * - Channel normalization | |
| * - Markdown stripping | |
| * - Error handling for missing/invalid accounts | |
| * - Registry integration | |
| */ | |
| import type { OpenClawConfig } from "openclaw/plugin-sdk"; | |
| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; | |
| import { sendMessageTwitchInternal } from "./send.js"; | |
| // Mock dependencies | |
| vi.mock("./config.js", () => ({ | |
| DEFAULT_ACCOUNT_ID: "default", | |
| getAccountConfig: vi.fn(), | |
| })); | |
| vi.mock("./utils/twitch.js", () => ({ | |
| generateMessageId: vi.fn(() => "test-msg-id"), | |
| isAccountConfigured: vi.fn(() => true), | |
| normalizeTwitchChannel: (channel: string) => channel.toLowerCase().replace(/^#/, ""), | |
| })); | |
| vi.mock("./utils/markdown.js", () => ({ | |
| stripMarkdownForTwitch: vi.fn((text: string) => text.replace(/\*\*/g, "")), | |
| })); | |
| vi.mock("./client-manager-registry.js", () => ({ | |
| getClientManager: vi.fn(), | |
| })); | |
| describe("send", () => { | |
| const mockLogger = { | |
| info: vi.fn(), | |
| warn: vi.fn(), | |
| error: vi.fn(), | |
| debug: vi.fn(), | |
| }; | |
| const mockAccount = { | |
| username: "testbot", | |
| token: "oauth:test123", | |
| clientId: "test-client-id", | |
| channel: "#testchannel", | |
| }; | |
| const mockConfig = { | |
| channels: { | |
| twitch: { | |
| accounts: { | |
| default: mockAccount, | |
| }, | |
| }, | |
| }, | |
| } as unknown as OpenClawConfig; | |
| beforeEach(() => { | |
| vi.clearAllMocks(); | |
| }); | |
| afterEach(() => { | |
| vi.restoreAllMocks(); | |
| }); | |
| describe("sendMessageTwitchInternal", () => { | |
| it("should send a message successfully", async () => { | |
| const { getAccountConfig } = await import("./config.js"); | |
| const { getClientManager } = await import("./client-manager-registry.js"); | |
| const { stripMarkdownForTwitch } = await import("./utils/markdown.js"); | |
| vi.mocked(getAccountConfig).mockReturnValue(mockAccount); | |
| vi.mocked(getClientManager).mockReturnValue({ | |
| sendMessage: vi.fn().mockResolvedValue({ | |
| ok: true, | |
| messageId: "twitch-msg-123", | |
| }), | |
| } as ReturnType<typeof getClientManager>); | |
| vi.mocked(stripMarkdownForTwitch).mockImplementation((text) => text); | |
| const result = await sendMessageTwitchInternal( | |
| "#testchannel", | |
| "Hello Twitch!", | |
| mockConfig, | |
| "default", | |
| false, | |
| mockLogger as unknown as Console, | |
| ); | |
| expect(result.ok).toBe(true); | |
| expect(result.messageId).toBe("twitch-msg-123"); | |
| }); | |
| it("should strip markdown when enabled", async () => { | |
| const { getAccountConfig } = await import("./config.js"); | |
| const { getClientManager } = await import("./client-manager-registry.js"); | |
| const { stripMarkdownForTwitch } = await import("./utils/markdown.js"); | |
| vi.mocked(getAccountConfig).mockReturnValue(mockAccount); | |
| vi.mocked(getClientManager).mockReturnValue({ | |
| sendMessage: vi.fn().mockResolvedValue({ | |
| ok: true, | |
| messageId: "twitch-msg-456", | |
| }), | |
| } as ReturnType<typeof getClientManager>); | |
| vi.mocked(stripMarkdownForTwitch).mockImplementation((text) => text.replace(/\*\*/g, "")); | |
| await sendMessageTwitchInternal( | |
| "#testchannel", | |
| "**Bold** text", | |
| mockConfig, | |
| "default", | |
| true, | |
| mockLogger as unknown as Console, | |
| ); | |
| expect(stripMarkdownForTwitch).toHaveBeenCalledWith("**Bold** text"); | |
| }); | |
| it("should return error when account not found", async () => { | |
| const { getAccountConfig } = await import("./config.js"); | |
| vi.mocked(getAccountConfig).mockReturnValue(null); | |
| const result = await sendMessageTwitchInternal( | |
| "#testchannel", | |
| "Hello!", | |
| mockConfig, | |
| "nonexistent", | |
| false, | |
| mockLogger as unknown as Console, | |
| ); | |
| expect(result.ok).toBe(false); | |
| expect(result.error).toContain("Account not found: nonexistent"); | |
| }); | |
| it("should return error when account not configured", async () => { | |
| const { getAccountConfig } = await import("./config.js"); | |
| const { isAccountConfigured } = await import("./utils/twitch.js"); | |
| vi.mocked(getAccountConfig).mockReturnValue(mockAccount); | |
| vi.mocked(isAccountConfigured).mockReturnValue(false); | |
| const result = await sendMessageTwitchInternal( | |
| "#testchannel", | |
| "Hello!", | |
| mockConfig, | |
| "default", | |
| false, | |
| mockLogger as unknown as Console, | |
| ); | |
| expect(result.ok).toBe(false); | |
| expect(result.error).toContain("not properly configured"); | |
| }); | |
| it("should return error when no channel specified", async () => { | |
| const { getAccountConfig } = await import("./config.js"); | |
| const { isAccountConfigured } = await import("./utils/twitch.js"); | |
| // Set channel to undefined to trigger the error (bypassing type check) | |
| const accountWithoutChannel = { | |
| ...mockAccount, | |
| channel: undefined as unknown as string, | |
| }; | |
| vi.mocked(getAccountConfig).mockReturnValue(accountWithoutChannel); | |
| vi.mocked(isAccountConfigured).mockReturnValue(true); | |
| const result = await sendMessageTwitchInternal( | |
| "", | |
| "Hello!", | |
| mockConfig, | |
| "default", | |
| false, | |
| mockLogger as unknown as Console, | |
| ); | |
| expect(result.ok).toBe(false); | |
| expect(result.error).toContain("No channel specified"); | |
| }); | |
| it("should skip sending empty message after markdown stripping", async () => { | |
| const { getAccountConfig } = await import("./config.js"); | |
| const { isAccountConfigured } = await import("./utils/twitch.js"); | |
| const { stripMarkdownForTwitch } = await import("./utils/markdown.js"); | |
| vi.mocked(getAccountConfig).mockReturnValue(mockAccount); | |
| vi.mocked(isAccountConfigured).mockReturnValue(true); | |
| vi.mocked(stripMarkdownForTwitch).mockReturnValue(""); | |
| const result = await sendMessageTwitchInternal( | |
| "#testchannel", | |
| "**Only markdown**", | |
| mockConfig, | |
| "default", | |
| true, | |
| mockLogger as unknown as Console, | |
| ); | |
| expect(result.ok).toBe(true); | |
| expect(result.messageId).toBe("skipped"); | |
| }); | |
| it("should return error when client manager not found", async () => { | |
| const { getAccountConfig } = await import("./config.js"); | |
| const { isAccountConfigured } = await import("./utils/twitch.js"); | |
| const { getClientManager } = await import("./client-manager-registry.js"); | |
| vi.mocked(getAccountConfig).mockReturnValue(mockAccount); | |
| vi.mocked(isAccountConfigured).mockReturnValue(true); | |
| vi.mocked(getClientManager).mockReturnValue(undefined); | |
| const result = await sendMessageTwitchInternal( | |
| "#testchannel", | |
| "Hello!", | |
| mockConfig, | |
| "default", | |
| false, | |
| mockLogger as unknown as Console, | |
| ); | |
| expect(result.ok).toBe(false); | |
| expect(result.error).toContain("Client manager not found"); | |
| }); | |
| it("should handle send errors gracefully", async () => { | |
| const { getAccountConfig } = await import("./config.js"); | |
| const { isAccountConfigured } = await import("./utils/twitch.js"); | |
| const { getClientManager } = await import("./client-manager-registry.js"); | |
| vi.mocked(getAccountConfig).mockReturnValue(mockAccount); | |
| vi.mocked(isAccountConfigured).mockReturnValue(true); | |
| vi.mocked(getClientManager).mockReturnValue({ | |
| sendMessage: vi.fn().mockRejectedValue(new Error("Connection lost")), | |
| } as ReturnType<typeof getClientManager>); | |
| const result = await sendMessageTwitchInternal( | |
| "#testchannel", | |
| "Hello!", | |
| mockConfig, | |
| "default", | |
| false, | |
| mockLogger as unknown as Console, | |
| ); | |
| expect(result.ok).toBe(false); | |
| expect(result.error).toBe("Connection lost"); | |
| expect(mockLogger.error).toHaveBeenCalled(); | |
| }); | |
| it("should use account channel when channel parameter is empty", async () => { | |
| const { getAccountConfig } = await import("./config.js"); | |
| const { isAccountConfigured } = await import("./utils/twitch.js"); | |
| const { getClientManager } = await import("./client-manager-registry.js"); | |
| vi.mocked(getAccountConfig).mockReturnValue(mockAccount); | |
| vi.mocked(isAccountConfigured).mockReturnValue(true); | |
| const mockSend = vi.fn().mockResolvedValue({ | |
| ok: true, | |
| messageId: "twitch-msg-789", | |
| }); | |
| vi.mocked(getClientManager).mockReturnValue({ | |
| sendMessage: mockSend, | |
| } as ReturnType<typeof getClientManager>); | |
| await sendMessageTwitchInternal( | |
| "", | |
| "Hello!", | |
| mockConfig, | |
| "default", | |
| false, | |
| mockLogger as unknown as Console, | |
| ); | |
| expect(mockSend).toHaveBeenCalledWith( | |
| mockAccount, | |
| "testchannel", // normalized account channel | |
| "Hello!", | |
| mockConfig, | |
| "default", | |
| ); | |
| }); | |
| }); | |
| }); | |