@herdctl/discord 0.0.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 (137) hide show
  1. package/dist/__tests__/auto-mode-handler.test.d.ts +2 -0
  2. package/dist/__tests__/auto-mode-handler.test.d.ts.map +1 -0
  3. package/dist/__tests__/auto-mode-handler.test.js +362 -0
  4. package/dist/__tests__/auto-mode-handler.test.js.map +1 -0
  5. package/dist/__tests__/discord-connector.test.d.ts +2 -0
  6. package/dist/__tests__/discord-connector.test.d.ts.map +1 -0
  7. package/dist/__tests__/discord-connector.test.js +958 -0
  8. package/dist/__tests__/discord-connector.test.js.map +1 -0
  9. package/dist/__tests__/error-handler.test.d.ts +2 -0
  10. package/dist/__tests__/error-handler.test.d.ts.map +1 -0
  11. package/dist/__tests__/error-handler.test.js +509 -0
  12. package/dist/__tests__/error-handler.test.js.map +1 -0
  13. package/dist/__tests__/errors.test.d.ts +2 -0
  14. package/dist/__tests__/errors.test.d.ts.map +1 -0
  15. package/dist/__tests__/errors.test.js +152 -0
  16. package/dist/__tests__/errors.test.js.map +1 -0
  17. package/dist/__tests__/logger.test.d.ts +2 -0
  18. package/dist/__tests__/logger.test.d.ts.map +1 -0
  19. package/dist/__tests__/logger.test.js +282 -0
  20. package/dist/__tests__/logger.test.js.map +1 -0
  21. package/dist/__tests__/mention-handler.test.d.ts +2 -0
  22. package/dist/__tests__/mention-handler.test.d.ts.map +1 -0
  23. package/dist/__tests__/mention-handler.test.js +547 -0
  24. package/dist/__tests__/mention-handler.test.js.map +1 -0
  25. package/dist/auto-mode-handler.d.ts +145 -0
  26. package/dist/auto-mode-handler.d.ts.map +1 -0
  27. package/dist/auto-mode-handler.js +211 -0
  28. package/dist/auto-mode-handler.js.map +1 -0
  29. package/dist/commands/__tests__/command-manager.test.d.ts +2 -0
  30. package/dist/commands/__tests__/command-manager.test.d.ts.map +1 -0
  31. package/dist/commands/__tests__/command-manager.test.js +307 -0
  32. package/dist/commands/__tests__/command-manager.test.js.map +1 -0
  33. package/dist/commands/__tests__/help.test.d.ts +2 -0
  34. package/dist/commands/__tests__/help.test.d.ts.map +1 -0
  35. package/dist/commands/__tests__/help.test.js +105 -0
  36. package/dist/commands/__tests__/help.test.js.map +1 -0
  37. package/dist/commands/__tests__/reset.test.d.ts +2 -0
  38. package/dist/commands/__tests__/reset.test.d.ts.map +1 -0
  39. package/dist/commands/__tests__/reset.test.js +140 -0
  40. package/dist/commands/__tests__/reset.test.js.map +1 -0
  41. package/dist/commands/__tests__/status.test.d.ts +2 -0
  42. package/dist/commands/__tests__/status.test.d.ts.map +1 -0
  43. package/dist/commands/__tests__/status.test.js +205 -0
  44. package/dist/commands/__tests__/status.test.js.map +1 -0
  45. package/dist/commands/command-manager.d.ts +66 -0
  46. package/dist/commands/command-manager.d.ts.map +1 -0
  47. package/dist/commands/command-manager.js +191 -0
  48. package/dist/commands/command-manager.js.map +1 -0
  49. package/dist/commands/help.d.ts +8 -0
  50. package/dist/commands/help.d.ts.map +1 -0
  51. package/dist/commands/help.js +27 -0
  52. package/dist/commands/help.js.map +1 -0
  53. package/dist/commands/index.d.ts +12 -0
  54. package/dist/commands/index.d.ts.map +1 -0
  55. package/dist/commands/index.js +13 -0
  56. package/dist/commands/index.js.map +1 -0
  57. package/dist/commands/reset.d.ts +9 -0
  58. package/dist/commands/reset.d.ts.map +1 -0
  59. package/dist/commands/reset.js +28 -0
  60. package/dist/commands/reset.js.map +1 -0
  61. package/dist/commands/status.d.ts +9 -0
  62. package/dist/commands/status.d.ts.map +1 -0
  63. package/dist/commands/status.js +102 -0
  64. package/dist/commands/status.js.map +1 -0
  65. package/dist/commands/types.d.ts +87 -0
  66. package/dist/commands/types.d.ts.map +1 -0
  67. package/dist/commands/types.js +8 -0
  68. package/dist/commands/types.js.map +1 -0
  69. package/dist/discord-connector.d.ts +154 -0
  70. package/dist/discord-connector.d.ts.map +1 -0
  71. package/dist/discord-connector.js +638 -0
  72. package/dist/discord-connector.js.map +1 -0
  73. package/dist/error-handler.d.ts +237 -0
  74. package/dist/error-handler.d.ts.map +1 -0
  75. package/dist/error-handler.js +433 -0
  76. package/dist/error-handler.js.map +1 -0
  77. package/dist/errors.d.ts +61 -0
  78. package/dist/errors.d.ts.map +1 -0
  79. package/dist/errors.js +77 -0
  80. package/dist/errors.js.map +1 -0
  81. package/dist/index.d.ts +34 -0
  82. package/dist/index.d.ts.map +1 -0
  83. package/dist/index.js +36 -0
  84. package/dist/index.js.map +1 -0
  85. package/dist/logger.d.ts +119 -0
  86. package/dist/logger.d.ts.map +1 -0
  87. package/dist/logger.js +198 -0
  88. package/dist/logger.js.map +1 -0
  89. package/dist/mention-handler.d.ts +176 -0
  90. package/dist/mention-handler.d.ts.map +1 -0
  91. package/dist/mention-handler.js +236 -0
  92. package/dist/mention-handler.js.map +1 -0
  93. package/dist/session-manager/__tests__/errors.test.d.ts +2 -0
  94. package/dist/session-manager/__tests__/errors.test.d.ts.map +1 -0
  95. package/dist/session-manager/__tests__/errors.test.js +124 -0
  96. package/dist/session-manager/__tests__/errors.test.js.map +1 -0
  97. package/dist/session-manager/__tests__/session-manager.test.d.ts +2 -0
  98. package/dist/session-manager/__tests__/session-manager.test.d.ts.map +1 -0
  99. package/dist/session-manager/__tests__/session-manager.test.js +517 -0
  100. package/dist/session-manager/__tests__/session-manager.test.js.map +1 -0
  101. package/dist/session-manager/__tests__/types.test.d.ts +2 -0
  102. package/dist/session-manager/__tests__/types.test.d.ts.map +1 -0
  103. package/dist/session-manager/__tests__/types.test.js +169 -0
  104. package/dist/session-manager/__tests__/types.test.js.map +1 -0
  105. package/dist/session-manager/errors.d.ts +58 -0
  106. package/dist/session-manager/errors.d.ts.map +1 -0
  107. package/dist/session-manager/errors.js +70 -0
  108. package/dist/session-manager/errors.js.map +1 -0
  109. package/dist/session-manager/index.d.ts +11 -0
  110. package/dist/session-manager/index.d.ts.map +1 -0
  111. package/dist/session-manager/index.js +12 -0
  112. package/dist/session-manager/index.js.map +1 -0
  113. package/dist/session-manager/session-manager.d.ts +107 -0
  114. package/dist/session-manager/session-manager.d.ts.map +1 -0
  115. package/dist/session-manager/session-manager.js +347 -0
  116. package/dist/session-manager/session-manager.js.map +1 -0
  117. package/dist/session-manager/types.d.ts +167 -0
  118. package/dist/session-manager/types.d.ts.map +1 -0
  119. package/dist/session-manager/types.js +57 -0
  120. package/dist/session-manager/types.js.map +1 -0
  121. package/dist/types.d.ts +323 -0
  122. package/dist/types.d.ts.map +1 -0
  123. package/dist/types.js +8 -0
  124. package/dist/types.js.map +1 -0
  125. package/dist/utils/__tests__/formatting.test.d.ts +2 -0
  126. package/dist/utils/__tests__/formatting.test.d.ts.map +1 -0
  127. package/dist/utils/__tests__/formatting.test.js +571 -0
  128. package/dist/utils/__tests__/formatting.test.js.map +1 -0
  129. package/dist/utils/formatting.d.ts +211 -0
  130. package/dist/utils/formatting.d.ts.map +1 -0
  131. package/dist/utils/formatting.js +305 -0
  132. package/dist/utils/formatting.js.map +1 -0
  133. package/dist/utils/index.d.ts +5 -0
  134. package/dist/utils/index.d.ts.map +1 -0
  135. package/dist/utils/index.js +9 -0
  136. package/dist/utils/index.js.map +1 -0
  137. package/package.json +49 -0
@@ -0,0 +1,958 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ // =============================================================================
3
+ // Mock discord.js - Must be hoisted, factory cannot reference external variables
4
+ // =============================================================================
5
+ // Shared mock state - these will be accessed by tests
6
+ let mockLoginImpl = null;
7
+ // Mock REST EventEmitter for rate limit events
8
+ let mockRestEmitter = null;
9
+ // Mock discord.js module - factory must be self-contained
10
+ vi.mock("discord.js", () => {
11
+ const { EventEmitter } = require("events");
12
+ // Define mock user inside factory
13
+ const mockUser = {
14
+ id: "123456789",
15
+ username: "TestBot",
16
+ discriminator: "0001",
17
+ setActivity: vi.fn(),
18
+ };
19
+ // Define mock client inside factory
20
+ class MockClientClass extends EventEmitter {
21
+ user = mockUser;
22
+ rest = new EventEmitter(); // REST client for rate limit events
23
+ login = vi.fn().mockImplementation(async () => {
24
+ // Use the external mockLoginImpl if set, otherwise return success
25
+ if (mockLoginImpl) {
26
+ return mockLoginImpl();
27
+ }
28
+ return "token";
29
+ });
30
+ destroy = vi.fn();
31
+ constructor() {
32
+ super();
33
+ // Store reference to rest emitter for test access
34
+ mockRestEmitter = this.rest;
35
+ }
36
+ }
37
+ return {
38
+ Client: MockClientClass,
39
+ GatewayIntentBits: {
40
+ Guilds: 1,
41
+ GuildMessages: 2,
42
+ DirectMessages: 4,
43
+ MessageContent: 8,
44
+ },
45
+ Events: {
46
+ ClientReady: "ready",
47
+ ShardDisconnect: "shardDisconnect",
48
+ ShardReconnecting: "shardReconnecting",
49
+ ShardResume: "shardResume",
50
+ Error: "error",
51
+ Warn: "warn",
52
+ Debug: "debug",
53
+ },
54
+ };
55
+ });
56
+ // Mock @discordjs/rest for RESTEvents constant
57
+ vi.mock("@discordjs/rest", () => {
58
+ return {
59
+ RESTEvents: {
60
+ RateLimited: "rateLimited",
61
+ },
62
+ };
63
+ });
64
+ // Import after mock
65
+ import { DiscordConnector } from "../discord-connector.js";
66
+ import { AlreadyConnectedError, DiscordConnectionError, InvalidTokenError, } from "../errors.js";
67
+ // Helper to set the login behavior before connecting
68
+ function setMockLoginBehavior(behavior, errorMessage) {
69
+ if (behavior === "failure") {
70
+ mockLoginImpl = () => Promise.reject(new Error(errorMessage || "Login failed"));
71
+ }
72
+ else {
73
+ mockLoginImpl = null; // Use default success behavior
74
+ }
75
+ }
76
+ // =============================================================================
77
+ // Test Fixtures
78
+ // =============================================================================
79
+ function createMockAgentConfig() {
80
+ return {
81
+ name: "test-agent",
82
+ description: "Test agent for Discord connector tests",
83
+ };
84
+ }
85
+ function createMockDiscordConfig() {
86
+ return {
87
+ bot_token_env: "TEST_BOT_TOKEN",
88
+ session_expiry_hours: 24,
89
+ log_level: "standard",
90
+ guilds: [
91
+ {
92
+ id: "guild-123",
93
+ channels: [
94
+ {
95
+ id: "channel-456",
96
+ name: "#test",
97
+ mode: "mention",
98
+ context_messages: 10,
99
+ },
100
+ ],
101
+ },
102
+ ],
103
+ };
104
+ }
105
+ function createMockFleetManager() {
106
+ return {
107
+ trigger: vi.fn(),
108
+ getFleetStatus: vi.fn(),
109
+ };
110
+ }
111
+ function createMockSessionManager() {
112
+ return {
113
+ agentName: "test-agent",
114
+ getOrCreateSession: vi.fn().mockResolvedValue({ sessionId: "test-session", isNew: true }),
115
+ touchSession: vi.fn().mockResolvedValue(undefined),
116
+ getSession: vi.fn().mockResolvedValue(null),
117
+ clearSession: vi.fn().mockResolvedValue(true),
118
+ cleanupExpiredSessions: vi.fn().mockResolvedValue(0),
119
+ };
120
+ }
121
+ function createMockLogger() {
122
+ return {
123
+ debug: vi.fn(),
124
+ info: vi.fn(),
125
+ warn: vi.fn(),
126
+ error: vi.fn(),
127
+ };
128
+ }
129
+ // =============================================================================
130
+ // Constructor Tests
131
+ // =============================================================================
132
+ describe("DiscordConnector", () => {
133
+ let agentConfig;
134
+ let discordConfig;
135
+ let fleetManager;
136
+ let sessionManager;
137
+ let mockLogger;
138
+ beforeEach(() => {
139
+ vi.clearAllMocks();
140
+ setMockLoginBehavior("success"); // Reset to success by default
141
+ agentConfig = createMockAgentConfig();
142
+ discordConfig = createMockDiscordConfig();
143
+ fleetManager = createMockFleetManager();
144
+ sessionManager = createMockSessionManager();
145
+ mockLogger = createMockLogger();
146
+ });
147
+ describe("constructor", () => {
148
+ it("creates connector with valid options", () => {
149
+ const connector = new DiscordConnector({
150
+ agentConfig,
151
+ discordConfig,
152
+ botToken: "valid-token",
153
+ fleetManager,
154
+ sessionManager,
155
+ logger: mockLogger,
156
+ });
157
+ expect(connector.agentName).toBe("test-agent");
158
+ expect(connector.isConnected()).toBe(false);
159
+ });
160
+ it("throws InvalidTokenError for empty token", () => {
161
+ expect(() => new DiscordConnector({
162
+ agentConfig,
163
+ discordConfig,
164
+ botToken: "",
165
+ fleetManager,
166
+ sessionManager,
167
+ })).toThrow(InvalidTokenError);
168
+ });
169
+ it("throws InvalidTokenError for whitespace-only token", () => {
170
+ expect(() => new DiscordConnector({
171
+ agentConfig,
172
+ discordConfig,
173
+ botToken: " ",
174
+ fleetManager,
175
+ sessionManager,
176
+ })).toThrow(InvalidTokenError);
177
+ });
178
+ it("uses default logger when none provided", () => {
179
+ // Should not throw
180
+ const connector = new DiscordConnector({
181
+ agentConfig,
182
+ discordConfig,
183
+ botToken: "valid-token",
184
+ fleetManager,
185
+ sessionManager,
186
+ });
187
+ expect(connector.agentName).toBe("test-agent");
188
+ });
189
+ });
190
+ // =============================================================================
191
+ // getState Tests
192
+ // =============================================================================
193
+ describe("getState", () => {
194
+ it("returns initial disconnected state", () => {
195
+ const connector = new DiscordConnector({
196
+ agentConfig,
197
+ discordConfig,
198
+ botToken: "valid-token",
199
+ fleetManager,
200
+ sessionManager,
201
+ logger: mockLogger,
202
+ });
203
+ const state = connector.getState();
204
+ expect(state.status).toBe("disconnected");
205
+ expect(state.connectedAt).toBeNull();
206
+ expect(state.disconnectedAt).toBeNull();
207
+ expect(state.reconnectAttempts).toBe(0);
208
+ expect(state.lastError).toBeNull();
209
+ expect(state.botUser).toBeNull();
210
+ });
211
+ });
212
+ // =============================================================================
213
+ // connect Tests
214
+ // =============================================================================
215
+ describe("connect", () => {
216
+ it("connects successfully and updates state", async () => {
217
+ const connector = new DiscordConnector({
218
+ agentConfig,
219
+ discordConfig,
220
+ botToken: "valid-token",
221
+ fleetManager,
222
+ sessionManager,
223
+ logger: mockLogger,
224
+ });
225
+ const connectPromise = connector.connect();
226
+ // Wait for client to be created
227
+ await vi.waitFor(() => {
228
+ expect(connector.client).not.toBeNull();
229
+ });
230
+ // Get the actual mock client and simulate ready event
231
+ const client = connector.client;
232
+ client.login.mockResolvedValue("token");
233
+ // Emit ready event
234
+ client.emit("ready", { user: client.user });
235
+ await connectPromise;
236
+ expect(connector.isConnected()).toBe(true);
237
+ const state = connector.getState();
238
+ expect(state.status).toBe("connected");
239
+ expect(state.connectedAt).not.toBeNull();
240
+ expect(state.botUser).toEqual({
241
+ id: "123456789",
242
+ username: "TestBot",
243
+ discriminator: "0001",
244
+ });
245
+ });
246
+ it("emits ready event on successful connection", async () => {
247
+ const connector = new DiscordConnector({
248
+ agentConfig,
249
+ discordConfig,
250
+ botToken: "valid-token",
251
+ fleetManager,
252
+ sessionManager,
253
+ logger: mockLogger,
254
+ });
255
+ const readyHandler = vi.fn();
256
+ connector.on("ready", readyHandler);
257
+ const connectPromise = connector.connect();
258
+ await vi.waitFor(() => {
259
+ expect(connector.client).not.toBeNull();
260
+ });
261
+ const client = connector.client;
262
+ client.login.mockResolvedValue("token");
263
+ client.emit("ready", { user: client.user });
264
+ await connectPromise;
265
+ // Wait for the async ready handler to complete
266
+ await vi.waitFor(() => {
267
+ expect(readyHandler).toHaveBeenCalled();
268
+ });
269
+ expect(readyHandler).toHaveBeenCalledWith({
270
+ agentName: "test-agent",
271
+ botUser: {
272
+ id: "123456789",
273
+ username: "TestBot",
274
+ discriminator: "0001",
275
+ },
276
+ });
277
+ });
278
+ it("throws AlreadyConnectedError when connecting while connected", async () => {
279
+ const connector = new DiscordConnector({
280
+ agentConfig,
281
+ discordConfig,
282
+ botToken: "valid-token",
283
+ fleetManager,
284
+ sessionManager,
285
+ logger: mockLogger,
286
+ });
287
+ // First connect
288
+ const connectPromise = connector.connect();
289
+ await vi.waitFor(() => {
290
+ expect(connector.client).not.toBeNull();
291
+ });
292
+ const client = connector.client;
293
+ client.login.mockResolvedValue("token");
294
+ client.emit("ready", { user: client.user });
295
+ await connectPromise;
296
+ // Try to connect again
297
+ await expect(connector.connect()).rejects.toThrow(AlreadyConnectedError);
298
+ });
299
+ it("throws DiscordConnectionError on login failure", async () => {
300
+ // Set up login to fail BEFORE calling connect
301
+ setMockLoginBehavior("failure", "Invalid token");
302
+ const connector = new DiscordConnector({
303
+ agentConfig,
304
+ discordConfig,
305
+ botToken: "invalid-token",
306
+ fleetManager,
307
+ sessionManager,
308
+ logger: mockLogger,
309
+ });
310
+ await expect(connector.connect()).rejects.toThrow(DiscordConnectionError);
311
+ const state = connector.getState();
312
+ expect(state.status).toBe("error");
313
+ expect(state.lastError).toBe("Invalid token");
314
+ });
315
+ it("cleans up client on connection failure", async () => {
316
+ // Set up login to fail BEFORE calling connect
317
+ setMockLoginBehavior("failure", "Invalid token");
318
+ const connector = new DiscordConnector({
319
+ agentConfig,
320
+ discordConfig,
321
+ botToken: "invalid-token",
322
+ fleetManager,
323
+ sessionManager,
324
+ logger: mockLogger,
325
+ });
326
+ await expect(connector.connect()).rejects.toThrow(DiscordConnectionError);
327
+ expect(connector.client).toBeNull();
328
+ });
329
+ });
330
+ // =============================================================================
331
+ // disconnect Tests
332
+ // =============================================================================
333
+ describe("disconnect", () => {
334
+ it("disconnects successfully", async () => {
335
+ const connector = new DiscordConnector({
336
+ agentConfig,
337
+ discordConfig,
338
+ botToken: "valid-token",
339
+ fleetManager,
340
+ sessionManager,
341
+ logger: mockLogger,
342
+ });
343
+ // Connect first
344
+ const connectPromise = connector.connect();
345
+ await vi.waitFor(() => {
346
+ expect(connector.client).not.toBeNull();
347
+ });
348
+ const client = connector.client;
349
+ client.login.mockResolvedValue("token");
350
+ client.emit("ready", { user: client.user });
351
+ await connectPromise;
352
+ // Now disconnect
353
+ await connector.disconnect();
354
+ expect(connector.isConnected()).toBe(false);
355
+ const state = connector.getState();
356
+ expect(state.status).toBe("disconnected");
357
+ expect(state.disconnectedAt).not.toBeNull();
358
+ expect(state.botUser).toBeNull();
359
+ });
360
+ it("handles disconnect when already disconnected", async () => {
361
+ const connector = new DiscordConnector({
362
+ agentConfig,
363
+ discordConfig,
364
+ botToken: "valid-token",
365
+ fleetManager,
366
+ sessionManager,
367
+ logger: mockLogger,
368
+ });
369
+ // Should not throw
370
+ await connector.disconnect();
371
+ expect(connector.isConnected()).toBe(false);
372
+ });
373
+ it("calls client.destroy() on disconnect", async () => {
374
+ const connector = new DiscordConnector({
375
+ agentConfig,
376
+ discordConfig,
377
+ botToken: "valid-token",
378
+ fleetManager,
379
+ sessionManager,
380
+ logger: mockLogger,
381
+ });
382
+ const connectPromise = connector.connect();
383
+ await vi.waitFor(() => {
384
+ expect(connector.client).not.toBeNull();
385
+ });
386
+ const client = connector.client;
387
+ client.login.mockResolvedValue("token");
388
+ client.emit("ready", { user: client.user });
389
+ await connectPromise;
390
+ await connector.disconnect();
391
+ expect(client.destroy).toHaveBeenCalled();
392
+ });
393
+ it("handles destroy error gracefully", async () => {
394
+ const connector = new DiscordConnector({
395
+ agentConfig,
396
+ discordConfig,
397
+ botToken: "valid-token",
398
+ fleetManager,
399
+ sessionManager,
400
+ logger: mockLogger,
401
+ });
402
+ const connectPromise = connector.connect();
403
+ await vi.waitFor(() => {
404
+ expect(connector.client).not.toBeNull();
405
+ });
406
+ const client = connector.client;
407
+ client.login.mockResolvedValue("token");
408
+ client.emit("ready", { user: client.user });
409
+ await connectPromise;
410
+ // Make destroy throw
411
+ client.destroy.mockImplementation(() => {
412
+ throw new Error("Destroy failed");
413
+ });
414
+ // Should not throw
415
+ await connector.disconnect();
416
+ expect(connector.isConnected()).toBe(false);
417
+ expect(mockLogger.error).toHaveBeenCalled();
418
+ });
419
+ });
420
+ // =============================================================================
421
+ // Connection Event Tests
422
+ // =============================================================================
423
+ describe("connection events", () => {
424
+ it("emits disconnect event on shard disconnect", async () => {
425
+ const connector = new DiscordConnector({
426
+ agentConfig,
427
+ discordConfig,
428
+ botToken: "valid-token",
429
+ fleetManager,
430
+ sessionManager,
431
+ logger: mockLogger,
432
+ });
433
+ const disconnectHandler = vi.fn();
434
+ connector.on("disconnect", disconnectHandler);
435
+ const connectPromise = connector.connect();
436
+ await vi.waitFor(() => {
437
+ expect(connector.client).not.toBeNull();
438
+ });
439
+ const client = connector.client;
440
+ client.login.mockResolvedValue("token");
441
+ client.emit("ready", { user: client.user });
442
+ await connectPromise;
443
+ // Simulate disconnect
444
+ client.emit("shardDisconnect", { code: 1001 });
445
+ expect(disconnectHandler).toHaveBeenCalledWith({
446
+ agentName: "test-agent",
447
+ code: 1001,
448
+ reason: "Shard disconnected",
449
+ });
450
+ });
451
+ it("emits reconnecting event on shard reconnecting", async () => {
452
+ const connector = new DiscordConnector({
453
+ agentConfig,
454
+ discordConfig,
455
+ botToken: "valid-token",
456
+ fleetManager,
457
+ sessionManager,
458
+ logger: mockLogger,
459
+ });
460
+ const reconnectingHandler = vi.fn();
461
+ connector.on("reconnecting", reconnectingHandler);
462
+ const connectPromise = connector.connect();
463
+ await vi.waitFor(() => {
464
+ expect(connector.client).not.toBeNull();
465
+ });
466
+ const client = connector.client;
467
+ client.login.mockResolvedValue("token");
468
+ client.emit("ready", { user: client.user });
469
+ await connectPromise;
470
+ // Simulate reconnecting
471
+ client.emit("shardReconnecting");
472
+ expect(reconnectingHandler).toHaveBeenCalledWith({
473
+ agentName: "test-agent",
474
+ attempt: 1,
475
+ });
476
+ const state = connector.getState();
477
+ expect(state.status).toBe("reconnecting");
478
+ expect(state.reconnectAttempts).toBe(1);
479
+ });
480
+ it("emits reconnected event on shard resume", async () => {
481
+ const connector = new DiscordConnector({
482
+ agentConfig,
483
+ discordConfig,
484
+ botToken: "valid-token",
485
+ fleetManager,
486
+ sessionManager,
487
+ logger: mockLogger,
488
+ });
489
+ const reconnectedHandler = vi.fn();
490
+ connector.on("reconnected", reconnectedHandler);
491
+ const connectPromise = connector.connect();
492
+ await vi.waitFor(() => {
493
+ expect(connector.client).not.toBeNull();
494
+ });
495
+ const client = connector.client;
496
+ client.login.mockResolvedValue("token");
497
+ client.emit("ready", { user: client.user });
498
+ await connectPromise;
499
+ // Simulate reconnect cycle
500
+ client.emit("shardReconnecting");
501
+ client.emit("shardResume");
502
+ expect(reconnectedHandler).toHaveBeenCalledWith({
503
+ agentName: "test-agent",
504
+ });
505
+ const state = connector.getState();
506
+ expect(state.status).toBe("connected");
507
+ });
508
+ it("emits error event on client error", async () => {
509
+ const connector = new DiscordConnector({
510
+ agentConfig,
511
+ discordConfig,
512
+ botToken: "valid-token",
513
+ fleetManager,
514
+ sessionManager,
515
+ logger: mockLogger,
516
+ });
517
+ const errorHandler = vi.fn();
518
+ connector.on("error", errorHandler);
519
+ const connectPromise = connector.connect();
520
+ await vi.waitFor(() => {
521
+ expect(connector.client).not.toBeNull();
522
+ });
523
+ const client = connector.client;
524
+ client.login.mockResolvedValue("token");
525
+ client.emit("ready", { user: client.user });
526
+ await connectPromise;
527
+ // Simulate error
528
+ const testError = new Error("Test error");
529
+ client.emit("error", testError);
530
+ expect(errorHandler).toHaveBeenCalledWith({
531
+ agentName: "test-agent",
532
+ error: testError,
533
+ });
534
+ const state = connector.getState();
535
+ expect(state.lastError).toBe("Test error");
536
+ });
537
+ it("tracks multiple reconnection attempts", async () => {
538
+ const connector = new DiscordConnector({
539
+ agentConfig,
540
+ discordConfig,
541
+ botToken: "valid-token",
542
+ fleetManager,
543
+ sessionManager,
544
+ logger: mockLogger,
545
+ });
546
+ const connectPromise = connector.connect();
547
+ await vi.waitFor(() => {
548
+ expect(connector.client).not.toBeNull();
549
+ });
550
+ const client = connector.client;
551
+ client.login.mockResolvedValue("token");
552
+ client.emit("ready", { user: client.user });
553
+ await connectPromise;
554
+ // Simulate multiple reconnection attempts
555
+ client.emit("shardReconnecting");
556
+ expect(connector.getState().reconnectAttempts).toBe(1);
557
+ client.emit("shardReconnecting");
558
+ expect(connector.getState().reconnectAttempts).toBe(2);
559
+ client.emit("shardReconnecting");
560
+ expect(connector.getState().reconnectAttempts).toBe(3);
561
+ });
562
+ });
563
+ // =============================================================================
564
+ // Presence Tests
565
+ // =============================================================================
566
+ describe("presence", () => {
567
+ it("sets presence when configured", async () => {
568
+ const configWithPresence = {
569
+ ...discordConfig,
570
+ presence: {
571
+ activity_type: "watching",
572
+ activity_message: "for support requests",
573
+ },
574
+ };
575
+ const connector = new DiscordConnector({
576
+ agentConfig,
577
+ discordConfig: configWithPresence,
578
+ botToken: "valid-token",
579
+ fleetManager,
580
+ sessionManager,
581
+ logger: mockLogger,
582
+ });
583
+ const connectPromise = connector.connect();
584
+ await vi.waitFor(() => {
585
+ expect(connector.client).not.toBeNull();
586
+ });
587
+ const client = connector.client;
588
+ client.login.mockResolvedValue("token");
589
+ client.emit("ready", { user: client.user });
590
+ await connectPromise;
591
+ expect(client.user.setActivity).toHaveBeenCalledWith("for support requests", {
592
+ type: 3, // watching
593
+ });
594
+ });
595
+ it("does not set presence when not configured", async () => {
596
+ const connector = new DiscordConnector({
597
+ agentConfig,
598
+ discordConfig, // No presence configured
599
+ botToken: "valid-token",
600
+ fleetManager,
601
+ sessionManager,
602
+ logger: mockLogger,
603
+ });
604
+ const connectPromise = connector.connect();
605
+ await vi.waitFor(() => {
606
+ expect(connector.client).not.toBeNull();
607
+ });
608
+ const client = connector.client;
609
+ client.login.mockResolvedValue("token");
610
+ client.emit("ready", { user: client.user });
611
+ await connectPromise;
612
+ expect(client.user.setActivity).not.toHaveBeenCalled();
613
+ });
614
+ });
615
+ // =============================================================================
616
+ // Rate Limit Tests
617
+ // =============================================================================
618
+ describe("rate limit handling", () => {
619
+ it("emits rateLimit event when rate limited", async () => {
620
+ const connector = new DiscordConnector({
621
+ agentConfig,
622
+ discordConfig,
623
+ botToken: "valid-token",
624
+ fleetManager,
625
+ sessionManager,
626
+ logger: mockLogger,
627
+ });
628
+ const rateLimitHandler = vi.fn();
629
+ connector.on("rateLimit", rateLimitHandler);
630
+ const connectPromise = connector.connect();
631
+ await vi.waitFor(() => {
632
+ expect(connector.client).not.toBeNull();
633
+ });
634
+ const client = connector.client;
635
+ client.login.mockResolvedValue("token");
636
+ client.emit("ready", { user: client.user });
637
+ await connectPromise;
638
+ // Simulate rate limit event
639
+ client.rest.emit("rateLimited", {
640
+ timeToReset: 5000,
641
+ limit: 50,
642
+ method: "POST",
643
+ hash: "abc123",
644
+ route: "/channels/123/messages",
645
+ global: false,
646
+ });
647
+ expect(rateLimitHandler).toHaveBeenCalledWith({
648
+ agentName: "test-agent",
649
+ timeToReset: 5000,
650
+ limit: 50,
651
+ method: "POST",
652
+ hash: "abc123",
653
+ route: "/channels/123/messages",
654
+ global: false,
655
+ });
656
+ });
657
+ it("logs rate limit at info level", async () => {
658
+ const connector = new DiscordConnector({
659
+ agentConfig,
660
+ discordConfig,
661
+ botToken: "valid-token",
662
+ fleetManager,
663
+ sessionManager,
664
+ logger: mockLogger,
665
+ });
666
+ const connectPromise = connector.connect();
667
+ await vi.waitFor(() => {
668
+ expect(connector.client).not.toBeNull();
669
+ });
670
+ const client = connector.client;
671
+ client.login.mockResolvedValue("token");
672
+ client.emit("ready", { user: client.user });
673
+ await connectPromise;
674
+ // Simulate rate limit event
675
+ client.rest.emit("rateLimited", {
676
+ timeToReset: 5000,
677
+ limit: 50,
678
+ method: "POST",
679
+ hash: "abc123",
680
+ route: "/channels/123/messages",
681
+ global: false,
682
+ });
683
+ expect(mockLogger.info).toHaveBeenCalledWith("Rate limited by Discord API", expect.objectContaining({
684
+ route: "/channels/123/messages",
685
+ method: "POST",
686
+ timeToReset: 5000,
687
+ limit: 50,
688
+ global: false,
689
+ hash: "abc123",
690
+ }));
691
+ });
692
+ it("tracks rate limit count in state", async () => {
693
+ const connector = new DiscordConnector({
694
+ agentConfig,
695
+ discordConfig,
696
+ botToken: "valid-token",
697
+ fleetManager,
698
+ sessionManager,
699
+ logger: mockLogger,
700
+ });
701
+ const connectPromise = connector.connect();
702
+ await vi.waitFor(() => {
703
+ expect(connector.client).not.toBeNull();
704
+ });
705
+ const client = connector.client;
706
+ client.login.mockResolvedValue("token");
707
+ client.emit("ready", { user: client.user });
708
+ await connectPromise;
709
+ // Initial state should have no rate limits
710
+ let state = connector.getState();
711
+ expect(state.rateLimits.totalCount).toBe(0);
712
+ expect(state.rateLimits.lastRateLimitAt).toBeNull();
713
+ expect(state.rateLimits.isRateLimited).toBe(false);
714
+ // Simulate first rate limit
715
+ client.rest.emit("rateLimited", {
716
+ timeToReset: 5000,
717
+ limit: 50,
718
+ method: "POST",
719
+ hash: "abc123",
720
+ route: "/channels/123/messages",
721
+ global: false,
722
+ });
723
+ state = connector.getState();
724
+ expect(state.rateLimits.totalCount).toBe(1);
725
+ expect(state.rateLimits.lastRateLimitAt).not.toBeNull();
726
+ expect(state.rateLimits.isRateLimited).toBe(true);
727
+ expect(state.rateLimits.currentResetTime).toBe(5000);
728
+ // Simulate second rate limit
729
+ client.rest.emit("rateLimited", {
730
+ timeToReset: 3000,
731
+ limit: 50,
732
+ method: "GET",
733
+ hash: "def456",
734
+ route: "/guilds/123",
735
+ global: false,
736
+ });
737
+ state = connector.getState();
738
+ expect(state.rateLimits.totalCount).toBe(2);
739
+ expect(state.rateLimits.currentResetTime).toBe(3000);
740
+ });
741
+ it("clears rate limit status after reset time", async () => {
742
+ vi.useFakeTimers();
743
+ const connector = new DiscordConnector({
744
+ agentConfig,
745
+ discordConfig,
746
+ botToken: "valid-token",
747
+ fleetManager,
748
+ sessionManager,
749
+ logger: mockLogger,
750
+ });
751
+ const connectPromise = connector.connect();
752
+ await vi.waitFor(() => {
753
+ expect(connector.client).not.toBeNull();
754
+ });
755
+ const client = connector.client;
756
+ client.login.mockResolvedValue("token");
757
+ client.emit("ready", { user: client.user });
758
+ await connectPromise;
759
+ // Simulate rate limit with 5 second reset
760
+ client.rest.emit("rateLimited", {
761
+ timeToReset: 5000,
762
+ limit: 50,
763
+ method: "POST",
764
+ hash: "abc123",
765
+ route: "/channels/123/messages",
766
+ global: false,
767
+ });
768
+ let state = connector.getState();
769
+ expect(state.rateLimits.isRateLimited).toBe(true);
770
+ // Advance time past the reset
771
+ vi.advanceTimersByTime(5001);
772
+ state = connector.getState();
773
+ expect(state.rateLimits.isRateLimited).toBe(false);
774
+ expect(state.rateLimits.currentResetTime).toBe(0);
775
+ // Total count should still be 1
776
+ expect(state.rateLimits.totalCount).toBe(1);
777
+ vi.useRealTimers();
778
+ });
779
+ it("handles global rate limits", async () => {
780
+ const connector = new DiscordConnector({
781
+ agentConfig,
782
+ discordConfig,
783
+ botToken: "valid-token",
784
+ fleetManager,
785
+ sessionManager,
786
+ logger: mockLogger,
787
+ });
788
+ const rateLimitHandler = vi.fn();
789
+ connector.on("rateLimit", rateLimitHandler);
790
+ const connectPromise = connector.connect();
791
+ await vi.waitFor(() => {
792
+ expect(connector.client).not.toBeNull();
793
+ });
794
+ const client = connector.client;
795
+ client.login.mockResolvedValue("token");
796
+ client.emit("ready", { user: client.user });
797
+ await connectPromise;
798
+ // Simulate global rate limit
799
+ client.rest.emit("rateLimited", {
800
+ timeToReset: 60000,
801
+ limit: 50,
802
+ method: "POST",
803
+ hash: "global",
804
+ route: "/channels/123/messages",
805
+ global: true,
806
+ });
807
+ expect(rateLimitHandler).toHaveBeenCalledWith(expect.objectContaining({
808
+ global: true,
809
+ }));
810
+ });
811
+ it("clears rate limit timer on disconnect", async () => {
812
+ vi.useFakeTimers();
813
+ const connector = new DiscordConnector({
814
+ agentConfig,
815
+ discordConfig,
816
+ botToken: "valid-token",
817
+ fleetManager,
818
+ sessionManager,
819
+ logger: mockLogger,
820
+ });
821
+ const connectPromise = connector.connect();
822
+ await vi.waitFor(() => {
823
+ expect(connector.client).not.toBeNull();
824
+ });
825
+ const client = connector.client;
826
+ client.login.mockResolvedValue("token");
827
+ client.emit("ready", { user: client.user });
828
+ await connectPromise;
829
+ // Simulate rate limit
830
+ client.rest.emit("rateLimited", {
831
+ timeToReset: 5000,
832
+ limit: 50,
833
+ method: "POST",
834
+ hash: "abc123",
835
+ route: "/channels/123/messages",
836
+ global: false,
837
+ });
838
+ let state = connector.getState();
839
+ expect(state.rateLimits.isRateLimited).toBe(true);
840
+ // Disconnect before timer expires
841
+ await connector.disconnect();
842
+ state = connector.getState();
843
+ expect(state.rateLimits.isRateLimited).toBe(false);
844
+ expect(state.rateLimits.currentResetTime).toBe(0);
845
+ vi.useRealTimers();
846
+ });
847
+ it("returns initial rate limit state before connection", () => {
848
+ const connector = new DiscordConnector({
849
+ agentConfig,
850
+ discordConfig,
851
+ botToken: "valid-token",
852
+ fleetManager,
853
+ sessionManager,
854
+ logger: mockLogger,
855
+ });
856
+ const state = connector.getState();
857
+ expect(state.rateLimits).toEqual({
858
+ totalCount: 0,
859
+ lastRateLimitAt: null,
860
+ isRateLimited: false,
861
+ currentResetTime: 0,
862
+ });
863
+ });
864
+ });
865
+ // =============================================================================
866
+ // Message Stats Tests
867
+ // =============================================================================
868
+ describe("message stats", () => {
869
+ it("returns initial message stats before connection", () => {
870
+ const connector = new DiscordConnector({
871
+ agentConfig,
872
+ discordConfig,
873
+ botToken: "valid-token",
874
+ fleetManager,
875
+ sessionManager,
876
+ logger: mockLogger,
877
+ });
878
+ const state = connector.getState();
879
+ expect(state.messageStats).toEqual({
880
+ received: 0,
881
+ sent: 0,
882
+ ignored: 0,
883
+ });
884
+ });
885
+ });
886
+ // =============================================================================
887
+ // isConnected Tests
888
+ // =============================================================================
889
+ describe("isConnected", () => {
890
+ it("returns false when disconnected", () => {
891
+ const connector = new DiscordConnector({
892
+ agentConfig,
893
+ discordConfig,
894
+ botToken: "valid-token",
895
+ fleetManager,
896
+ sessionManager,
897
+ logger: mockLogger,
898
+ });
899
+ expect(connector.isConnected()).toBe(false);
900
+ });
901
+ it("returns true when connected", async () => {
902
+ const connector = new DiscordConnector({
903
+ agentConfig,
904
+ discordConfig,
905
+ botToken: "valid-token",
906
+ fleetManager,
907
+ sessionManager,
908
+ logger: mockLogger,
909
+ });
910
+ const connectPromise = connector.connect();
911
+ await vi.waitFor(() => {
912
+ expect(connector.client).not.toBeNull();
913
+ });
914
+ const client = connector.client;
915
+ client.login.mockResolvedValue("token");
916
+ client.emit("ready", { user: client.user });
917
+ await connectPromise;
918
+ expect(connector.isConnected()).toBe(true);
919
+ });
920
+ it("returns false when reconnecting", async () => {
921
+ const connector = new DiscordConnector({
922
+ agentConfig,
923
+ discordConfig,
924
+ botToken: "valid-token",
925
+ fleetManager,
926
+ sessionManager,
927
+ logger: mockLogger,
928
+ });
929
+ const connectPromise = connector.connect();
930
+ await vi.waitFor(() => {
931
+ expect(connector.client).not.toBeNull();
932
+ });
933
+ const client = connector.client;
934
+ client.login.mockResolvedValue("token");
935
+ client.emit("ready", { user: client.user });
936
+ await connectPromise;
937
+ client.emit("shardReconnecting");
938
+ expect(connector.isConnected()).toBe(false);
939
+ });
940
+ });
941
+ // =============================================================================
942
+ // agentName Tests
943
+ // =============================================================================
944
+ describe("agentName", () => {
945
+ it("returns the agent name from config", () => {
946
+ const connector = new DiscordConnector({
947
+ agentConfig,
948
+ discordConfig,
949
+ botToken: "valid-token",
950
+ fleetManager,
951
+ sessionManager,
952
+ logger: mockLogger,
953
+ });
954
+ expect(connector.agentName).toBe("test-agent");
955
+ });
956
+ });
957
+ });
958
+ //# sourceMappingURL=discord-connector.test.js.map