@kodelyth/twitch 2026.5.42 → 2026.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/klaw.plugin.json +219 -2
  2. package/package.json +18 -1
  3. package/api.ts +0 -21
  4. package/channel-plugin-api.ts +0 -1
  5. package/index.test.ts +0 -13
  6. package/index.ts +0 -16
  7. package/runtime-api.ts +0 -22
  8. package/setup-entry.ts +0 -9
  9. package/setup-plugin-api.ts +0 -3
  10. package/src/access-control.test.ts +0 -373
  11. package/src/access-control.ts +0 -195
  12. package/src/actions.test.ts +0 -75
  13. package/src/actions.ts +0 -175
  14. package/src/client-manager-registry.ts +0 -87
  15. package/src/config-schema.test.ts +0 -46
  16. package/src/config-schema.ts +0 -88
  17. package/src/config.test.ts +0 -233
  18. package/src/config.ts +0 -177
  19. package/src/monitor.ts +0 -311
  20. package/src/outbound.test.ts +0 -572
  21. package/src/outbound.ts +0 -242
  22. package/src/plugin.lifecycle.test.ts +0 -86
  23. package/src/plugin.live.test.ts +0 -120
  24. package/src/plugin.test.ts +0 -77
  25. package/src/plugin.ts +0 -220
  26. package/src/probe.test.ts +0 -196
  27. package/src/probe.ts +0 -130
  28. package/src/resolver.ts +0 -139
  29. package/src/runtime.ts +0 -9
  30. package/src/send.test.ts +0 -342
  31. package/src/send.ts +0 -191
  32. package/src/setup-surface.test.ts +0 -529
  33. package/src/setup-surface.ts +0 -526
  34. package/src/status.test.ts +0 -298
  35. package/src/status.ts +0 -179
  36. package/src/test-fixtures.ts +0 -30
  37. package/src/token.test.ts +0 -198
  38. package/src/token.ts +0 -93
  39. package/src/twitch-client.test.ts +0 -574
  40. package/src/twitch-client.ts +0 -276
  41. package/src/types.ts +0 -104
  42. package/src/utils/markdown.ts +0 -98
  43. package/src/utils/twitch.ts +0 -81
  44. package/test/setup.ts +0 -7
  45. package/tsconfig.json +0 -16
package/src/token.ts DELETED
@@ -1,93 +0,0 @@
1
- /**
2
- * Twitch access token resolution with environment variable support.
3
- *
4
- * Supports reading Twitch OAuth access tokens from config or environment variable.
5
- * The KLAW_TWITCH_ACCESS_TOKEN env var is only used for the default account.
6
- *
7
- * Token resolution priority:
8
- * 1. Account access token from merged config (accounts.{id} or base-level for default)
9
- * 2. Environment variable: KLAW_TWITCH_ACCESS_TOKEN (default account only)
10
- */
11
-
12
- import {
13
- DEFAULT_ACCOUNT_ID,
14
- normalizeAccountId,
15
- resolveNormalizedAccountEntry,
16
- } from "klaw/plugin-sdk/account-resolution";
17
- import type { KlawConfig } from "klaw/plugin-sdk/config-contracts";
18
-
19
- export type TwitchTokenSource = "env" | "config" | "none";
20
-
21
- export type TwitchTokenResolution = {
22
- token: string;
23
- source: TwitchTokenSource;
24
- };
25
-
26
- /**
27
- * Normalize a Twitch OAuth token - ensure it has the oauth: prefix
28
- */
29
- function normalizeTwitchToken(raw?: string | null): string | undefined {
30
- if (!raw) {
31
- return undefined;
32
- }
33
- const trimmed = raw.trim();
34
- if (!trimmed) {
35
- return undefined;
36
- }
37
- // Twitch tokens should have oauth: prefix
38
- return trimmed.startsWith("oauth:") ? trimmed : `oauth:${trimmed}`;
39
- }
40
-
41
- /**
42
- * Resolve Twitch access token from config or environment variable.
43
- *
44
- * Priority:
45
- * 1. Account access token (from merged config - base-level for default, or accounts.{accountId})
46
- * 2. Environment variable: KLAW_TWITCH_ACCESS_TOKEN (default account only)
47
- *
48
- * The getAccountConfig function handles merging base-level config with accounts.default,
49
- * so this logic works for both simplified and multi-account patterns.
50
- *
51
- * @param cfg - Klaw config
52
- * @param opts - Options including accountId and optional envToken override
53
- * @returns Token resolution with source
54
- */
55
- export function resolveTwitchToken(
56
- cfg?: KlawConfig,
57
- opts: { accountId?: string | null; envToken?: string | null } = {},
58
- ): TwitchTokenResolution {
59
- const accountId = normalizeAccountId(opts.accountId);
60
-
61
- // Get merged account config (handles both simplified and multi-account patterns)
62
- const twitchCfg = cfg?.channels?.twitch;
63
- const accounts = twitchCfg?.accounts as Record<string, Record<string, unknown>> | undefined;
64
- const accountCfg = resolveNormalizedAccountEntry(accounts, accountId, normalizeAccountId);
65
-
66
- // For default account, also check base-level config
67
- let token: string | undefined;
68
- if (accountId === DEFAULT_ACCOUNT_ID) {
69
- // Base-level config takes precedence
70
- token = normalizeTwitchToken(
71
- (typeof twitchCfg?.accessToken === "string" ? twitchCfg.accessToken : undefined) ||
72
- (accountCfg?.accessToken as string | undefined),
73
- );
74
- } else {
75
- // Non-default accounts only use accounts object
76
- token = normalizeTwitchToken(accountCfg?.accessToken as string | undefined);
77
- }
78
-
79
- if (token) {
80
- return { token, source: "config" };
81
- }
82
-
83
- // Environment variable (default account only)
84
- const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
85
- const envToken = allowEnv
86
- ? normalizeTwitchToken(opts.envToken ?? process.env.KLAW_TWITCH_ACCESS_TOKEN)
87
- : undefined;
88
- if (envToken) {
89
- return { token: envToken, source: "env" };
90
- }
91
-
92
- return { token: "", source: "none" };
93
- }
@@ -1,574 +0,0 @@
1
- /**
2
- * Tests for TwitchClientManager class
3
- *
4
- * Tests cover:
5
- * - Client connection and reconnection
6
- * - Message handling (chat)
7
- * - Message sending with rate limiting
8
- * - Disconnection scenarios
9
- * - Error handling and edge cases
10
- */
11
-
12
- import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
13
- import { resolveTwitchToken } from "./token.js";
14
- import { TwitchClientManager } from "./twitch-client.js";
15
- import type { ChannelLogSink, TwitchAccountConfig, TwitchChatMessage } from "./types.js";
16
-
17
- // Mock @twurple dependencies
18
- const mockConnect = vi.fn().mockResolvedValue(undefined);
19
- const mockJoin = vi.fn().mockResolvedValue(undefined);
20
- const mockSay = vi.fn().mockResolvedValue({ messageId: "test-msg-123" });
21
- const mockQuit = vi.fn();
22
- const mockUnbind = vi.fn();
23
-
24
- // Event handler storage for testing
25
- const messageHandlers: Array<(channel: string, user: string, message: string, msg: any) => void> =
26
- [];
27
-
28
- // Mock functions that track handlers and return unbind objects
29
- const mockOnMessage = vi.fn((handler: any) => {
30
- messageHandlers.push(handler);
31
- return { unbind: mockUnbind };
32
- });
33
-
34
- const mockAddUserForToken = vi.fn().mockResolvedValue("123456");
35
- const mockOnRefresh = vi.fn();
36
- const mockOnRefreshFailure = vi.fn();
37
-
38
- vi.mock("@twurple/chat", () => ({
39
- ChatClient: class {
40
- onMessage = mockOnMessage;
41
- connect = mockConnect;
42
- join = mockJoin;
43
- say = mockSay;
44
- quit = mockQuit;
45
- },
46
- LogLevel: {
47
- CRITICAL: "CRITICAL",
48
- ERROR: "ERROR",
49
- WARNING: "WARNING",
50
- INFO: "INFO",
51
- DEBUG: "DEBUG",
52
- TRACE: "TRACE",
53
- },
54
- }));
55
-
56
- const mockAuthProvider = {
57
- constructor: vi.fn(),
58
- };
59
-
60
- vi.mock("@twurple/auth", () => ({
61
- StaticAuthProvider: function StaticAuthProvider(...args: unknown[]) {
62
- mockAuthProvider.constructor(...args);
63
- },
64
- RefreshingAuthProvider: class {
65
- addUserForToken = mockAddUserForToken;
66
- onRefresh = mockOnRefresh;
67
- onRefreshFailure = mockOnRefreshFailure;
68
- },
69
- }));
70
-
71
- // Mock token resolution - must be after @twurple/auth mock
72
- vi.mock("./token.js", () => ({
73
- resolveTwitchToken: vi.fn(() => ({
74
- token: "oauth:mock-token-from-tests",
75
- source: "config" as const,
76
- })),
77
- DEFAULT_ACCOUNT_ID: "default",
78
- }));
79
-
80
- describe("TwitchClientManager", () => {
81
- let manager: TwitchClientManager;
82
- let mockLogger: ChannelLogSink;
83
- let resolveTwitchTokenMock: ReturnType<typeof vi.mocked<typeof resolveTwitchToken>>;
84
-
85
- const testAccount: TwitchAccountConfig = {
86
- username: "testbot",
87
- accessToken: "test123456",
88
- clientId: "test-client-id",
89
- channel: "testchannel",
90
- enabled: true,
91
- };
92
-
93
- const testAccount2: TwitchAccountConfig = {
94
- username: "testbot2",
95
- accessToken: "test789",
96
- clientId: "test-client-id-2",
97
- channel: "testchannel2",
98
- enabled: true,
99
- };
100
-
101
- beforeAll(() => {
102
- resolveTwitchTokenMock = vi.mocked(resolveTwitchToken);
103
- });
104
-
105
- beforeEach(() => {
106
- // Clear all mocks first
107
- vi.clearAllMocks();
108
-
109
- // Clear handler arrays
110
- messageHandlers.length = 0;
111
-
112
- // Re-set up the default token mock implementation after clearing
113
- resolveTwitchTokenMock.mockReturnValue({
114
- token: "oauth:mock-token-from-tests",
115
- source: "config" as const,
116
- });
117
-
118
- // Create mock logger
119
- mockLogger = {
120
- info: vi.fn(),
121
- warn: vi.fn(),
122
- error: vi.fn(),
123
- debug: vi.fn(),
124
- };
125
-
126
- // Create manager instance
127
- manager = new TwitchClientManager(mockLogger);
128
- });
129
-
130
- afterEach(() => {
131
- // Clean up manager to avoid side effects
132
- manager.clearForTest();
133
- });
134
-
135
- describe("getClient", () => {
136
- it("should create a new client connection", async () => {
137
- const clientForTest = await manager.getClient(testAccount);
138
-
139
- // New implementation: connect is called, channels are passed to constructor
140
- expect(mockConnect).toHaveBeenCalledTimes(1);
141
- expect(mockLogger.info).toHaveBeenCalledWith("Connected to Twitch as testbot");
142
- });
143
-
144
- it("should use account username as default channel when channel not specified", async () => {
145
- const accountWithoutChannel: TwitchAccountConfig = {
146
- ...testAccount,
147
- channel: "",
148
- } as unknown as TwitchAccountConfig;
149
-
150
- await manager.getClient(accountWithoutChannel);
151
-
152
- // New implementation: channel (testbot) is passed to constructor, not via join()
153
- expect(mockConnect).toHaveBeenCalledTimes(1);
154
- });
155
-
156
- it("should reuse existing client for same account", async () => {
157
- const client1 = await manager.getClient(testAccount);
158
- const client2 = await manager.getClient(testAccount);
159
-
160
- expect(client1).toBe(client2);
161
- expect(mockConnect).toHaveBeenCalledTimes(1);
162
- });
163
-
164
- it("should create separate clients for different accounts", async () => {
165
- await manager.getClient(testAccount);
166
- await manager.getClient(testAccount2);
167
-
168
- expect(mockConnect).toHaveBeenCalledTimes(2);
169
- });
170
-
171
- it("should normalize token by removing oauth: prefix", async () => {
172
- const accountWithPrefix: TwitchAccountConfig = {
173
- ...testAccount,
174
- accessToken: "oauth:actualtoken123",
175
- };
176
-
177
- // Override the mock to return a specific token for this test
178
- resolveTwitchTokenMock.mockReturnValue({
179
- token: "oauth:actualtoken123",
180
- source: "config" as const,
181
- });
182
-
183
- await manager.getClient(accountWithPrefix);
184
-
185
- expect(mockAuthProvider.constructor).toHaveBeenCalledWith("test-client-id", "actualtoken123");
186
- });
187
-
188
- it("should use token directly when no oauth: prefix", async () => {
189
- // Override the mock to return a token without oauth: prefix
190
- resolveTwitchTokenMock.mockReturnValue({
191
- token: "oauth:mock-token-from-tests",
192
- source: "config" as const,
193
- });
194
-
195
- await manager.getClient(testAccount);
196
-
197
- // Implementation strips oauth: prefix from all tokens
198
- expect(mockAuthProvider.constructor).toHaveBeenCalledWith(
199
- "test-client-id",
200
- "mock-token-from-tests",
201
- );
202
- });
203
-
204
- it("should throw error when clientId is missing", async () => {
205
- const accountWithoutClientId: TwitchAccountConfig = {
206
- ...testAccount,
207
- clientId: "" as unknown as string,
208
- } as unknown as TwitchAccountConfig;
209
-
210
- await expect(manager.getClient(accountWithoutClientId)).rejects.toThrow(
211
- "Missing Twitch client ID",
212
- );
213
-
214
- expect(mockLogger.error).toHaveBeenCalledWith("Missing Twitch client ID for account testbot");
215
- });
216
-
217
- it("should throw error when token is missing", async () => {
218
- // Override the mock to return empty token
219
- resolveTwitchTokenMock.mockReturnValue({
220
- token: "",
221
- source: "none" as const,
222
- });
223
-
224
- await expect(manager.getClient(testAccount)).rejects.toThrow("Missing Twitch token");
225
- });
226
-
227
- it("should set up message handlers on client connection", async () => {
228
- await manager.getClient(testAccount);
229
-
230
- expect(mockOnMessage).toHaveBeenCalled();
231
- expect(mockLogger.info).toHaveBeenCalledWith("Set up handlers for testbot:testchannel");
232
- });
233
-
234
- it("should create separate clients for same account with different channels", async () => {
235
- const account1: TwitchAccountConfig = {
236
- ...testAccount,
237
- channel: "channel1",
238
- };
239
- const account2: TwitchAccountConfig = {
240
- ...testAccount,
241
- channel: "channel2",
242
- };
243
-
244
- await manager.getClient(account1);
245
- await manager.getClient(account2);
246
-
247
- expect(mockConnect).toHaveBeenCalledTimes(2);
248
- });
249
- });
250
-
251
- describe("onMessage", () => {
252
- it("should register message handler for account", () => {
253
- const handler = vi.fn();
254
- manager.onMessage(testAccount, handler);
255
-
256
- expect(handler).not.toHaveBeenCalled();
257
- });
258
-
259
- it("should replace existing handler for same account", () => {
260
- const handler1 = vi.fn();
261
- const handler2 = vi.fn();
262
-
263
- manager.onMessage(testAccount, handler1);
264
- manager.onMessage(testAccount, handler2);
265
-
266
- // Check the stored handler is handler2
267
- const key = manager.getAccountKey(testAccount);
268
- expect((manager as any).messageHandlers.get(key)).toBe(handler2);
269
- });
270
- });
271
-
272
- describe("disconnect", () => {
273
- it("should disconnect a connected client", async () => {
274
- await manager.getClient(testAccount);
275
- await manager.disconnect(testAccount);
276
-
277
- expect(mockQuit).toHaveBeenCalledTimes(1);
278
- expect(mockLogger.info).toHaveBeenCalledWith("Disconnected testbot:testchannel");
279
- });
280
-
281
- it("should clear client and message handler", async () => {
282
- const handler = vi.fn();
283
- await manager.getClient(testAccount);
284
- manager.onMessage(testAccount, handler);
285
-
286
- await manager.disconnect(testAccount);
287
-
288
- const key = manager.getAccountKey(testAccount);
289
- expect((manager as any).clients.has(key)).toBe(false);
290
- expect((manager as any).messageHandlers.has(key)).toBe(false);
291
- });
292
-
293
- it("should handle disconnecting non-existent client gracefully", async () => {
294
- // Missing clients are ignored.
295
- await manager.disconnect(testAccount);
296
- expect(mockQuit).not.toHaveBeenCalled();
297
- });
298
-
299
- it("should only disconnect specified account when multiple accounts exist", async () => {
300
- await manager.getClient(testAccount);
301
- await manager.getClient(testAccount2);
302
-
303
- await manager.disconnect(testAccount);
304
-
305
- expect(mockQuit).toHaveBeenCalledTimes(1);
306
-
307
- const key2 = manager.getAccountKey(testAccount2);
308
- expect((manager as any).clients.has(key2)).toBe(true);
309
- });
310
- });
311
-
312
- describe("disconnectAll", () => {
313
- it("should disconnect all connected clients", async () => {
314
- await manager.getClient(testAccount);
315
- await manager.getClient(testAccount2);
316
-
317
- await manager.disconnectAll();
318
-
319
- expect(mockQuit).toHaveBeenCalledTimes(2);
320
- expect((manager as any).clients.size).toBe(0);
321
- expect((manager as any).messageHandlers.size).toBe(0);
322
- });
323
-
324
- it("should handle empty client list gracefully", async () => {
325
- // Empty client sets are ignored.
326
- await manager.disconnectAll();
327
- expect(mockQuit).not.toHaveBeenCalled();
328
- });
329
- });
330
-
331
- describe("sendMessage", () => {
332
- beforeEach(async () => {
333
- await manager.getClient(testAccount);
334
- });
335
-
336
- it("should send message successfully", async () => {
337
- const result = await manager.sendMessage(testAccount, "testchannel", "Hello, world!");
338
- const { messageId, ...resultRest } = result;
339
-
340
- expect(resultRest).toEqual({ ok: true });
341
- expect(messageId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
342
- expect(mockSay).toHaveBeenCalledWith("testchannel", "Hello, world!");
343
- });
344
-
345
- it("should generate unique message ID for each message", async () => {
346
- const result1 = await manager.sendMessage(testAccount, "testchannel", "First message");
347
- const result2 = await manager.sendMessage(testAccount, "testchannel", "Second message");
348
-
349
- expect(result1.messageId).not.toBe(result2.messageId);
350
- });
351
-
352
- it("should handle sending to account's default channel", async () => {
353
- const result = await manager.sendMessage(
354
- testAccount,
355
- testAccount.channel || testAccount.username,
356
- "Test message",
357
- );
358
-
359
- // Should use the account's channel or username
360
- expect(result.ok).toBe(true);
361
- expect(mockSay).toHaveBeenCalled();
362
- });
363
-
364
- it("should return error on send failure", async () => {
365
- mockSay.mockRejectedValueOnce(new Error("Rate limited"));
366
-
367
- const result = await manager.sendMessage(testAccount, "testchannel", "Test message");
368
-
369
- expect(result.ok).toBe(false);
370
- expect(result.error).toBe("Rate limited");
371
- expect(mockLogger.error).toHaveBeenCalledWith("Failed to send message: Rate limited");
372
- });
373
-
374
- it("should handle unknown error types", async () => {
375
- mockSay.mockRejectedValueOnce("String error");
376
-
377
- const result = await manager.sendMessage(testAccount, "testchannel", "Test message");
378
-
379
- expect(result.ok).toBe(false);
380
- expect(result.error).toBe("String error");
381
- });
382
-
383
- it("should create client if not already connected", async () => {
384
- // Clear the existing client
385
- (manager as any).clients.clear();
386
-
387
- // Reset connect call count for this specific test
388
- const connectCallCountBefore = mockConnect.mock.calls.length;
389
-
390
- const result = await manager.sendMessage(testAccount, "testchannel", "Test message");
391
-
392
- expect(result.ok).toBe(true);
393
- expect(mockConnect.mock.calls.length).toBeGreaterThan(connectCallCountBefore);
394
- });
395
- });
396
-
397
- describe("message handling integration", () => {
398
- let capturedMessage: TwitchChatMessage | null = null;
399
-
400
- beforeEach(() => {
401
- capturedMessage = null;
402
-
403
- // Set up message handler before connecting
404
- manager.onMessage(testAccount, (message) => {
405
- capturedMessage = message;
406
- });
407
- });
408
-
409
- it("should handle incoming chat messages", async () => {
410
- await manager.getClient(testAccount);
411
-
412
- // Get the onMessage callback
413
- const onMessageCallback = messageHandlers[0];
414
- if (!onMessageCallback) {
415
- throw new Error("onMessageCallback not found");
416
- }
417
-
418
- // Simulate Twitch message
419
- onMessageCallback("#testchannel", "testuser", "Hello bot!", {
420
- userInfo: {
421
- userName: "testuser",
422
- displayName: "TestUser",
423
- userId: "12345",
424
- isMod: false,
425
- isBroadcaster: false,
426
- isVip: false,
427
- isSubscriber: false,
428
- },
429
- id: "msg123",
430
- });
431
-
432
- expect(capturedMessage?.username).toBe("testuser");
433
- expect(capturedMessage?.displayName).toBe("TestUser");
434
- expect(capturedMessage?.userId).toBe("12345");
435
- expect(capturedMessage?.message).toBe("Hello bot!");
436
- expect(capturedMessage?.channel).toBe("testchannel");
437
- expect(capturedMessage?.chatType).toBe("group");
438
- });
439
-
440
- it("should normalize channel names without # prefix", async () => {
441
- await manager.getClient(testAccount);
442
-
443
- const onMessageCallback = messageHandlers[0];
444
-
445
- onMessageCallback("testchannel", "testuser", "Test", {
446
- userInfo: {
447
- userName: "testuser",
448
- displayName: "TestUser",
449
- userId: "123",
450
- isMod: false,
451
- isBroadcaster: false,
452
- isVip: false,
453
- isSubscriber: false,
454
- },
455
- id: "msg1",
456
- });
457
-
458
- expect(capturedMessage?.channel).toBe("testchannel");
459
- });
460
-
461
- it("should include user role flags in message", async () => {
462
- await manager.getClient(testAccount);
463
-
464
- const onMessageCallback = messageHandlers[0];
465
-
466
- onMessageCallback("#testchannel", "moduser", "Test", {
467
- userInfo: {
468
- userName: "moduser",
469
- displayName: "ModUser",
470
- userId: "456",
471
- isMod: true,
472
- isBroadcaster: false,
473
- isVip: true,
474
- isSubscriber: true,
475
- },
476
- id: "msg2",
477
- });
478
-
479
- expect(capturedMessage?.isMod).toBe(true);
480
- expect(capturedMessage?.isVip).toBe(true);
481
- expect(capturedMessage?.isSub).toBe(true);
482
- expect(capturedMessage?.isOwner).toBe(false);
483
- });
484
-
485
- it("should handle broadcaster messages", async () => {
486
- await manager.getClient(testAccount);
487
-
488
- const onMessageCallback = messageHandlers[0];
489
-
490
- onMessageCallback("#testchannel", "broadcaster", "Test", {
491
- userInfo: {
492
- userName: "broadcaster",
493
- displayName: "Broadcaster",
494
- userId: "789",
495
- isMod: false,
496
- isBroadcaster: true,
497
- isVip: false,
498
- isSubscriber: false,
499
- },
500
- id: "msg3",
501
- });
502
-
503
- expect(capturedMessage?.isOwner).toBe(true);
504
- });
505
- });
506
-
507
- describe("edge cases", () => {
508
- it("should handle multiple message handlers for different accounts", async () => {
509
- const messages1: TwitchChatMessage[] = [];
510
- const messages2: TwitchChatMessage[] = [];
511
-
512
- manager.onMessage(testAccount, (msg) => messages1.push(msg));
513
- manager.onMessage(testAccount2, (msg) => messages2.push(msg));
514
-
515
- await manager.getClient(testAccount);
516
- await manager.getClient(testAccount2);
517
-
518
- // Simulate message for first account
519
- const onMessage1 = messageHandlers[0];
520
- if (!onMessage1) {
521
- throw new Error("onMessage1 not found");
522
- }
523
- onMessage1("#testchannel", "user1", "msg1", {
524
- userInfo: {
525
- userName: "user1",
526
- displayName: "User1",
527
- userId: "1",
528
- isMod: false,
529
- isBroadcaster: false,
530
- isVip: false,
531
- isSubscriber: false,
532
- },
533
- id: "1",
534
- });
535
-
536
- // Simulate message for second account
537
- const onMessage2 = messageHandlers[1];
538
- if (!onMessage2) {
539
- throw new Error("onMessage2 not found");
540
- }
541
- onMessage2("#testchannel2", "user2", "msg2", {
542
- userInfo: {
543
- userName: "user2",
544
- displayName: "User2",
545
- userId: "2",
546
- isMod: false,
547
- isBroadcaster: false,
548
- isVip: false,
549
- isSubscriber: false,
550
- },
551
- id: "2",
552
- });
553
-
554
- expect(messages1).toHaveLength(1);
555
- expect(messages2).toHaveLength(1);
556
- expect(messages1[0]?.message).toBe("msg1");
557
- expect(messages2[0]?.message).toBe("msg2");
558
- });
559
-
560
- it("should handle rapid client creation requests", async () => {
561
- const promises = [
562
- manager.getClient(testAccount),
563
- manager.getClient(testAccount),
564
- manager.getClient(testAccount),
565
- ];
566
-
567
- await Promise.all(promises);
568
-
569
- // Note: The implementation doesn't handle concurrent getClient calls,
570
- // so multiple connections may be created. This is expected behavior.
571
- expect(mockConnect).toHaveBeenCalled();
572
- });
573
- });
574
- });