@openclaw/twitch 2026.5.2-beta.2 → 2026.5.3-beta.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 (54) hide show
  1. package/dist/api.js +3 -0
  2. package/dist/channel-plugin-api.js +2 -0
  3. package/dist/index.js +18 -0
  4. package/dist/markdown-MRdI1sR7.js +306 -0
  5. package/dist/monitor-DS0YTAPB.js +333 -0
  6. package/dist/plugin-BQX9GiIn.js +878 -0
  7. package/dist/runtime-QZ5I3GlI.js +8 -0
  8. package/dist/runtime-api.js +1 -0
  9. package/dist/setup-entry.js +11 -0
  10. package/dist/setup-plugin-api.js +2 -0
  11. package/dist/setup-surface-yArVgckI.js +400 -0
  12. package/dist/twitch-CklAMZL5.js +131 -0
  13. package/package.json +20 -3
  14. package/api.ts +0 -21
  15. package/channel-plugin-api.ts +0 -1
  16. package/index.test.ts +0 -13
  17. package/index.ts +0 -16
  18. package/runtime-api.ts +0 -22
  19. package/setup-entry.ts +0 -9
  20. package/setup-plugin-api.ts +0 -3
  21. package/src/access-control.test.ts +0 -388
  22. package/src/access-control.ts +0 -173
  23. package/src/actions.test.ts +0 -74
  24. package/src/actions.ts +0 -175
  25. package/src/client-manager-registry.ts +0 -87
  26. package/src/config-schema.test.ts +0 -46
  27. package/src/config-schema.ts +0 -88
  28. package/src/config.test.ts +0 -233
  29. package/src/config.ts +0 -177
  30. package/src/monitor.ts +0 -314
  31. package/src/outbound.test.ts +0 -468
  32. package/src/outbound.ts +0 -186
  33. package/src/plugin.test.ts +0 -77
  34. package/src/plugin.ts +0 -208
  35. package/src/probe.test.ts +0 -196
  36. package/src/probe.ts +0 -130
  37. package/src/resolver.ts +0 -139
  38. package/src/runtime.ts +0 -9
  39. package/src/send.test.ts +0 -309
  40. package/src/send.ts +0 -139
  41. package/src/setup-surface.test.ts +0 -511
  42. package/src/setup-surface.ts +0 -520
  43. package/src/status.test.ts +0 -237
  44. package/src/status.ts +0 -179
  45. package/src/test-fixtures.ts +0 -30
  46. package/src/token.test.ts +0 -192
  47. package/src/token.ts +0 -93
  48. package/src/twitch-client.test.ts +0 -582
  49. package/src/twitch-client.ts +0 -276
  50. package/src/types.ts +0 -104
  51. package/src/utils/markdown.ts +0 -98
  52. package/src/utils/twitch.ts +0 -84
  53. package/test/setup.ts +0 -7
  54. 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 OPENCLAW_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: OPENCLAW_TWITCH_ACCESS_TOKEN (default account only)
10
- */
11
-
12
- import {
13
- DEFAULT_ACCOUNT_ID,
14
- normalizeAccountId,
15
- resolveNormalizedAccountEntry,
16
- } from "openclaw/plugin-sdk/account-resolution";
17
- import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
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: OPENCLAW_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 - OpenClaw config
52
- * @param opts - Options including accountId and optional envToken override
53
- * @returns Token resolution with source
54
- */
55
- export function resolveTwitchToken(
56
- cfg?: OpenClawConfig,
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.OPENCLAW_TWITCH_ACCESS_TOKEN)
87
- : undefined;
88
- if (envToken) {
89
- return { token: envToken, source: "env" };
90
- }
91
-
92
- return { token: "", source: "none" };
93
- }
@@ -1,582 +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 _client = 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(
142
- expect.stringContaining("Connected to Twitch as testbot"),
143
- );
144
- });
145
-
146
- it("should use account username as default channel when channel not specified", async () => {
147
- const accountWithoutChannel: TwitchAccountConfig = {
148
- ...testAccount,
149
- channel: "",
150
- } as unknown as TwitchAccountConfig;
151
-
152
- await manager.getClient(accountWithoutChannel);
153
-
154
- // New implementation: channel (testbot) is passed to constructor, not via join()
155
- expect(mockConnect).toHaveBeenCalledTimes(1);
156
- });
157
-
158
- it("should reuse existing client for same account", async () => {
159
- const client1 = await manager.getClient(testAccount);
160
- const client2 = await manager.getClient(testAccount);
161
-
162
- expect(client1).toBe(client2);
163
- expect(mockConnect).toHaveBeenCalledTimes(1);
164
- });
165
-
166
- it("should create separate clients for different accounts", async () => {
167
- await manager.getClient(testAccount);
168
- await manager.getClient(testAccount2);
169
-
170
- expect(mockConnect).toHaveBeenCalledTimes(2);
171
- });
172
-
173
- it("should normalize token by removing oauth: prefix", async () => {
174
- const accountWithPrefix: TwitchAccountConfig = {
175
- ...testAccount,
176
- accessToken: "oauth:actualtoken123",
177
- };
178
-
179
- // Override the mock to return a specific token for this test
180
- resolveTwitchTokenMock.mockReturnValue({
181
- token: "oauth:actualtoken123",
182
- source: "config" as const,
183
- });
184
-
185
- await manager.getClient(accountWithPrefix);
186
-
187
- expect(mockAuthProvider.constructor).toHaveBeenCalledWith("test-client-id", "actualtoken123");
188
- });
189
-
190
- it("should use token directly when no oauth: prefix", async () => {
191
- // Override the mock to return a token without oauth: prefix
192
- resolveTwitchTokenMock.mockReturnValue({
193
- token: "oauth:mock-token-from-tests",
194
- source: "config" as const,
195
- });
196
-
197
- await manager.getClient(testAccount);
198
-
199
- // Implementation strips oauth: prefix from all tokens
200
- expect(mockAuthProvider.constructor).toHaveBeenCalledWith(
201
- "test-client-id",
202
- "mock-token-from-tests",
203
- );
204
- });
205
-
206
- it("should throw error when clientId is missing", async () => {
207
- const accountWithoutClientId: TwitchAccountConfig = {
208
- ...testAccount,
209
- clientId: "" as unknown as string,
210
- } as unknown as TwitchAccountConfig;
211
-
212
- await expect(manager.getClient(accountWithoutClientId)).rejects.toThrow(
213
- "Missing Twitch client ID",
214
- );
215
-
216
- expect(mockLogger.error).toHaveBeenCalledWith(
217
- expect.stringContaining("Missing Twitch client ID"),
218
- );
219
- });
220
-
221
- it("should throw error when token is missing", async () => {
222
- // Override the mock to return empty token
223
- resolveTwitchTokenMock.mockReturnValue({
224
- token: "",
225
- source: "none" as const,
226
- });
227
-
228
- await expect(manager.getClient(testAccount)).rejects.toThrow("Missing Twitch token");
229
- });
230
-
231
- it("should set up message handlers on client connection", async () => {
232
- await manager.getClient(testAccount);
233
-
234
- expect(mockOnMessage).toHaveBeenCalled();
235
- expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining("Set up handlers for"));
236
- });
237
-
238
- it("should create separate clients for same account with different channels", async () => {
239
- const account1: TwitchAccountConfig = {
240
- ...testAccount,
241
- channel: "channel1",
242
- };
243
- const account2: TwitchAccountConfig = {
244
- ...testAccount,
245
- channel: "channel2",
246
- };
247
-
248
- await manager.getClient(account1);
249
- await manager.getClient(account2);
250
-
251
- expect(mockConnect).toHaveBeenCalledTimes(2);
252
- });
253
- });
254
-
255
- describe("onMessage", () => {
256
- it("should register message handler for account", () => {
257
- const handler = vi.fn();
258
- manager.onMessage(testAccount, handler);
259
-
260
- expect(handler).not.toHaveBeenCalled();
261
- });
262
-
263
- it("should replace existing handler for same account", () => {
264
- const handler1 = vi.fn();
265
- const handler2 = vi.fn();
266
-
267
- manager.onMessage(testAccount, handler1);
268
- manager.onMessage(testAccount, handler2);
269
-
270
- // Check the stored handler is handler2
271
- const key = manager.getAccountKey(testAccount);
272
- expect((manager as any).messageHandlers.get(key)).toBe(handler2);
273
- });
274
- });
275
-
276
- describe("disconnect", () => {
277
- it("should disconnect a connected client", async () => {
278
- await manager.getClient(testAccount);
279
- await manager.disconnect(testAccount);
280
-
281
- expect(mockQuit).toHaveBeenCalledTimes(1);
282
- expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining("Disconnected"));
283
- });
284
-
285
- it("should clear client and message handler", async () => {
286
- const handler = vi.fn();
287
- await manager.getClient(testAccount);
288
- manager.onMessage(testAccount, handler);
289
-
290
- await manager.disconnect(testAccount);
291
-
292
- const key = manager.getAccountKey(testAccount);
293
- expect((manager as any).clients.has(key)).toBe(false);
294
- expect((manager as any).messageHandlers.has(key)).toBe(false);
295
- });
296
-
297
- it("should handle disconnecting non-existent client gracefully", async () => {
298
- // disconnect doesn't throw, just does nothing
299
- await manager.disconnect(testAccount);
300
- expect(mockQuit).not.toHaveBeenCalled();
301
- });
302
-
303
- it("should only disconnect specified account when multiple accounts exist", async () => {
304
- await manager.getClient(testAccount);
305
- await manager.getClient(testAccount2);
306
-
307
- await manager.disconnect(testAccount);
308
-
309
- expect(mockQuit).toHaveBeenCalledTimes(1);
310
-
311
- const key2 = manager.getAccountKey(testAccount2);
312
- expect((manager as any).clients.has(key2)).toBe(true);
313
- });
314
- });
315
-
316
- describe("disconnectAll", () => {
317
- it("should disconnect all connected clients", async () => {
318
- await manager.getClient(testAccount);
319
- await manager.getClient(testAccount2);
320
-
321
- await manager.disconnectAll();
322
-
323
- expect(mockQuit).toHaveBeenCalledTimes(2);
324
- expect((manager as any).clients.size).toBe(0);
325
- expect((manager as any).messageHandlers.size).toBe(0);
326
- });
327
-
328
- it("should handle empty client list gracefully", async () => {
329
- // disconnectAll doesn't throw, just does nothing
330
- await manager.disconnectAll();
331
- expect(mockQuit).not.toHaveBeenCalled();
332
- });
333
- });
334
-
335
- describe("sendMessage", () => {
336
- beforeEach(async () => {
337
- await manager.getClient(testAccount);
338
- });
339
-
340
- it("should send message successfully", async () => {
341
- const result = await manager.sendMessage(testAccount, "testchannel", "Hello, world!");
342
-
343
- expect(result).toMatchObject({ ok: true });
344
- expect(result.messageId).toMatch(
345
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
346
- );
347
- expect(mockSay).toHaveBeenCalledWith("testchannel", "Hello, world!");
348
- });
349
-
350
- it("should generate unique message ID for each message", async () => {
351
- const result1 = await manager.sendMessage(testAccount, "testchannel", "First message");
352
- const result2 = await manager.sendMessage(testAccount, "testchannel", "Second message");
353
-
354
- expect(result1.messageId).not.toBe(result2.messageId);
355
- });
356
-
357
- it("should handle sending to account's default channel", async () => {
358
- const result = await manager.sendMessage(
359
- testAccount,
360
- testAccount.channel || testAccount.username,
361
- "Test message",
362
- );
363
-
364
- // Should use the account's channel or username
365
- expect(result.ok).toBe(true);
366
- expect(mockSay).toHaveBeenCalled();
367
- });
368
-
369
- it("should return error on send failure", async () => {
370
- mockSay.mockRejectedValueOnce(new Error("Rate limited"));
371
-
372
- const result = await manager.sendMessage(testAccount, "testchannel", "Test message");
373
-
374
- expect(result.ok).toBe(false);
375
- expect(result.error).toBe("Rate limited");
376
- expect(mockLogger.error).toHaveBeenCalledWith(
377
- expect.stringContaining("Failed to send message"),
378
- );
379
- });
380
-
381
- it("should handle unknown error types", async () => {
382
- mockSay.mockRejectedValueOnce("String error");
383
-
384
- const result = await manager.sendMessage(testAccount, "testchannel", "Test message");
385
-
386
- expect(result.ok).toBe(false);
387
- expect(result.error).toBe("String error");
388
- });
389
-
390
- it("should create client if not already connected", async () => {
391
- // Clear the existing client
392
- (manager as any).clients.clear();
393
-
394
- // Reset connect call count for this specific test
395
- const connectCallCountBefore = mockConnect.mock.calls.length;
396
-
397
- const result = await manager.sendMessage(testAccount, "testchannel", "Test message");
398
-
399
- expect(result.ok).toBe(true);
400
- expect(mockConnect.mock.calls.length).toBeGreaterThan(connectCallCountBefore);
401
- });
402
- });
403
-
404
- describe("message handling integration", () => {
405
- let capturedMessage: TwitchChatMessage | null = null;
406
-
407
- beforeEach(() => {
408
- capturedMessage = null;
409
-
410
- // Set up message handler before connecting
411
- manager.onMessage(testAccount, (message) => {
412
- capturedMessage = message;
413
- });
414
- });
415
-
416
- it("should handle incoming chat messages", async () => {
417
- await manager.getClient(testAccount);
418
-
419
- // Get the onMessage callback
420
- const onMessageCallback = messageHandlers[0];
421
- if (!onMessageCallback) {
422
- throw new Error("onMessageCallback not found");
423
- }
424
-
425
- // Simulate Twitch message
426
- onMessageCallback("#testchannel", "testuser", "Hello bot!", {
427
- userInfo: {
428
- userName: "testuser",
429
- displayName: "TestUser",
430
- userId: "12345",
431
- isMod: false,
432
- isBroadcaster: false,
433
- isVip: false,
434
- isSubscriber: false,
435
- },
436
- id: "msg123",
437
- });
438
-
439
- expect(capturedMessage).not.toBeNull();
440
- expect(capturedMessage?.username).toBe("testuser");
441
- expect(capturedMessage?.displayName).toBe("TestUser");
442
- expect(capturedMessage?.userId).toBe("12345");
443
- expect(capturedMessage?.message).toBe("Hello bot!");
444
- expect(capturedMessage?.channel).toBe("testchannel");
445
- expect(capturedMessage?.chatType).toBe("group");
446
- });
447
-
448
- it("should normalize channel names without # prefix", async () => {
449
- await manager.getClient(testAccount);
450
-
451
- const onMessageCallback = messageHandlers[0];
452
-
453
- onMessageCallback("testchannel", "testuser", "Test", {
454
- userInfo: {
455
- userName: "testuser",
456
- displayName: "TestUser",
457
- userId: "123",
458
- isMod: false,
459
- isBroadcaster: false,
460
- isVip: false,
461
- isSubscriber: false,
462
- },
463
- id: "msg1",
464
- });
465
-
466
- expect(capturedMessage?.channel).toBe("testchannel");
467
- });
468
-
469
- it("should include user role flags in message", async () => {
470
- await manager.getClient(testAccount);
471
-
472
- const onMessageCallback = messageHandlers[0];
473
-
474
- onMessageCallback("#testchannel", "moduser", "Test", {
475
- userInfo: {
476
- userName: "moduser",
477
- displayName: "ModUser",
478
- userId: "456",
479
- isMod: true,
480
- isBroadcaster: false,
481
- isVip: true,
482
- isSubscriber: true,
483
- },
484
- id: "msg2",
485
- });
486
-
487
- expect(capturedMessage?.isMod).toBe(true);
488
- expect(capturedMessage?.isVip).toBe(true);
489
- expect(capturedMessage?.isSub).toBe(true);
490
- expect(capturedMessage?.isOwner).toBe(false);
491
- });
492
-
493
- it("should handle broadcaster messages", async () => {
494
- await manager.getClient(testAccount);
495
-
496
- const onMessageCallback = messageHandlers[0];
497
-
498
- onMessageCallback("#testchannel", "broadcaster", "Test", {
499
- userInfo: {
500
- userName: "broadcaster",
501
- displayName: "Broadcaster",
502
- userId: "789",
503
- isMod: false,
504
- isBroadcaster: true,
505
- isVip: false,
506
- isSubscriber: false,
507
- },
508
- id: "msg3",
509
- });
510
-
511
- expect(capturedMessage?.isOwner).toBe(true);
512
- });
513
- });
514
-
515
- describe("edge cases", () => {
516
- it("should handle multiple message handlers for different accounts", async () => {
517
- const messages1: TwitchChatMessage[] = [];
518
- const messages2: TwitchChatMessage[] = [];
519
-
520
- manager.onMessage(testAccount, (msg) => messages1.push(msg));
521
- manager.onMessage(testAccount2, (msg) => messages2.push(msg));
522
-
523
- await manager.getClient(testAccount);
524
- await manager.getClient(testAccount2);
525
-
526
- // Simulate message for first account
527
- const onMessage1 = messageHandlers[0];
528
- if (!onMessage1) {
529
- throw new Error("onMessage1 not found");
530
- }
531
- onMessage1("#testchannel", "user1", "msg1", {
532
- userInfo: {
533
- userName: "user1",
534
- displayName: "User1",
535
- userId: "1",
536
- isMod: false,
537
- isBroadcaster: false,
538
- isVip: false,
539
- isSubscriber: false,
540
- },
541
- id: "1",
542
- });
543
-
544
- // Simulate message for second account
545
- const onMessage2 = messageHandlers[1];
546
- if (!onMessage2) {
547
- throw new Error("onMessage2 not found");
548
- }
549
- onMessage2("#testchannel2", "user2", "msg2", {
550
- userInfo: {
551
- userName: "user2",
552
- displayName: "User2",
553
- userId: "2",
554
- isMod: false,
555
- isBroadcaster: false,
556
- isVip: false,
557
- isSubscriber: false,
558
- },
559
- id: "2",
560
- });
561
-
562
- expect(messages1).toHaveLength(1);
563
- expect(messages2).toHaveLength(1);
564
- expect(messages1[0]?.message).toBe("msg1");
565
- expect(messages2[0]?.message).toBe("msg2");
566
- });
567
-
568
- it("should handle rapid client creation requests", async () => {
569
- const promises = [
570
- manager.getClient(testAccount),
571
- manager.getClient(testAccount),
572
- manager.getClient(testAccount),
573
- ];
574
-
575
- await Promise.all(promises);
576
-
577
- // Note: The implementation doesn't handle concurrent getClient calls,
578
- // so multiple connections may be created. This is expected behavior.
579
- expect(mockConnect).toHaveBeenCalled();
580
- });
581
- });
582
- });