@gguf/claw
Version:
WhatsApp gateway CLI (Baileys web) with Pi RPC agent
196 lines (153 loc) • 6.21 kB
text/typescript
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { TwitchAccountConfig } from "./types.js";
import { probeTwitch } from "./probe.js";
// Mock Twurple modules - Vitest v4 compatible mocking
const mockUnbind = vi.fn();
// Event handler storage
let connectHandler: (() => void) | null = null;
let disconnectHandler: ((manually: boolean, reason?: Error) => void) | null = null;
// Event listener mocks that store handlers and return unbind function
const mockOnConnect = vi.fn((handler: () => void) => {
connectHandler = handler;
return { unbind: mockUnbind };
});
const mockOnDisconnect = vi.fn((handler: (manually: boolean, reason?: Error) => void) => {
disconnectHandler = handler;
return { unbind: mockUnbind };
});
const mockOnAuthenticationFailure = vi.fn((_handler: () => void) => {
return { unbind: mockUnbind };
});
// Connect mock that triggers the registered handler
const defaultConnectImpl = async () => {
// Simulate successful connection by calling the handler after a delay
if (connectHandler) {
await new Promise((resolve) => setTimeout(resolve, 1));
connectHandler();
}
};
const mockConnect = vi.fn().mockImplementation(defaultConnectImpl);
const mockQuit = vi.fn().mockResolvedValue(undefined);
vi.mock("@twurple/chat", () => ({
ChatClient: class {
connect = mockConnect;
quit = mockQuit;
onConnect = mockOnConnect;
onDisconnect = mockOnDisconnect;
onAuthenticationFailure = mockOnAuthenticationFailure;
},
}));
vi.mock("@twurple/auth", () => ({
StaticAuthProvider: class {},
}));
describe("probeTwitch", () => {
const mockAccount: TwitchAccountConfig = {
username: "testbot",
token: "oauth:test123456789",
channel: "testchannel",
};
beforeEach(() => {
vi.clearAllMocks();
// Reset handlers
connectHandler = null;
disconnectHandler = null;
});
it("returns error when username is missing", async () => {
const account = { ...mockAccount, username: "" };
const result = await probeTwitch(account, 5000);
expect(result.ok).toBe(false);
expect(result.error).toContain("missing credentials");
});
it("returns error when token is missing", async () => {
const account = { ...mockAccount, token: "" };
const result = await probeTwitch(account, 5000);
expect(result.ok).toBe(false);
expect(result.error).toContain("missing credentials");
});
it("attempts connection regardless of token prefix", async () => {
// Note: probeTwitch doesn't validate token format - it tries to connect with whatever token is provided
// The actual connection would fail in production with an invalid token
const account = { ...mockAccount, token: "raw_token_no_prefix" };
const result = await probeTwitch(account, 5000);
// With mock, connection succeeds even without oauth: prefix
expect(result.ok).toBe(true);
});
it("successfully connects with valid credentials", async () => {
const result = await probeTwitch(mockAccount, 5000);
expect(result.ok).toBe(true);
expect(result.connected).toBe(true);
expect(result.username).toBe("testbot");
expect(result.channel).toBe("testchannel"); // uses account's configured channel
});
it("uses custom channel when specified", async () => {
const account: TwitchAccountConfig = {
...mockAccount,
channel: "customchannel",
};
const result = await probeTwitch(account, 5000);
expect(result.ok).toBe(true);
expect(result.channel).toBe("customchannel");
});
it("times out when connection takes too long", async () => {
mockConnect.mockImplementationOnce(() => new Promise(() => {})); // Never resolves
const result = await probeTwitch(mockAccount, 100);
expect(result.ok).toBe(false);
expect(result.error).toContain("timeout");
// Reset mock
mockConnect.mockImplementation(defaultConnectImpl);
});
it("cleans up client even on failure", async () => {
mockConnect.mockImplementationOnce(async () => {
// Simulate connection failure by calling disconnect handler
// onDisconnect signature: (manually: boolean, reason?: Error) => void
if (disconnectHandler) {
await new Promise((resolve) => setTimeout(resolve, 1));
disconnectHandler(false, new Error("Connection failed"));
}
});
const result = await probeTwitch(mockAccount, 5000);
expect(result.ok).toBe(false);
expect(result.error).toContain("Connection failed");
expect(mockQuit).toHaveBeenCalled();
// Reset mocks
mockConnect.mockImplementation(defaultConnectImpl);
});
it("handles connection errors gracefully", async () => {
mockConnect.mockImplementationOnce(async () => {
// Simulate connection failure by calling disconnect handler
// onDisconnect signature: (manually: boolean, reason?: Error) => void
if (disconnectHandler) {
await new Promise((resolve) => setTimeout(resolve, 1));
disconnectHandler(false, new Error("Network error"));
}
});
const result = await probeTwitch(mockAccount, 5000);
expect(result.ok).toBe(false);
expect(result.error).toContain("Network error");
// Reset mock
mockConnect.mockImplementation(defaultConnectImpl);
});
it("trims token before validation", async () => {
const account: TwitchAccountConfig = {
...mockAccount,
token: " oauth:test123456789 ",
};
const result = await probeTwitch(account, 5000);
expect(result.ok).toBe(true);
});
it("handles non-Error objects in catch block", async () => {
mockConnect.mockImplementationOnce(async () => {
// Simulate connection failure by calling disconnect handler
// onDisconnect signature: (manually: boolean, reason?: Error) => void
if (disconnectHandler) {
await new Promise((resolve) => setTimeout(resolve, 1));
disconnectHandler(false, "String error" as unknown as Error);
}
});
const result = await probeTwitch(mockAccount, 5000);
expect(result.ok).toBe(false);
expect(result.error).toBe("String error");
// Reset mock
mockConnect.mockImplementation(defaultConnectImpl);
});
});