novaapp-sdk 1.4.0 → 1.4.2

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.
@@ -0,0 +1,849 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/testing/index.ts
21
+ var testing_exports = {};
22
+ __export(testing_exports, {
23
+ EventSimulator: () => EventSimulator,
24
+ HarnessAssertionError: () => HarnessAssertionError,
25
+ MockClient: () => MockClient,
26
+ TestHarness: () => TestHarness,
27
+ asMockClient: () => asMockClient
28
+ });
29
+ module.exports = __toCommonJS(testing_exports);
30
+
31
+ // src/testing/MockClient.ts
32
+ var import_node_events = require("events");
33
+ var import_node_crypto = require("crypto");
34
+ function mockMsg(override = {}) {
35
+ return {
36
+ id: `mock-msg-${(0, import_node_crypto.randomUUID)()}`,
37
+ content: "",
38
+ channelId: "ch-mock",
39
+ author: { id: "bot-user-id", username: "testbot", displayName: "Test Bot", avatar: null, isBot: true },
40
+ attachments: [],
41
+ reactions: [],
42
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
43
+ ...override
44
+ };
45
+ }
46
+ function makeApiProxy(namespace, calls, overrides = {}) {
47
+ return new Proxy({}, {
48
+ get(_target, prop) {
49
+ if (prop in overrides) return overrides[prop];
50
+ return (...args) => {
51
+ const result = { id: `mock-${(0, import_node_crypto.randomUUID)()}`, ok: true };
52
+ calls.push({ api: namespace, method: prop, args, result, timestamp: /* @__PURE__ */ new Date() });
53
+ return Promise.resolve(result);
54
+ };
55
+ }
56
+ });
57
+ }
58
+ var MockClient = class extends import_node_events.EventEmitter {
59
+ constructor() {
60
+ super(...arguments);
61
+ /** All recorded API calls, in chronological order. */
62
+ this.calls = [];
63
+ /** Simulated bot application info. Pre-set to a default test value. */
64
+ this.botUser = {
65
+ id: "bot-app-id",
66
+ name: "TestBot",
67
+ username: "testbot",
68
+ displayName: "Test Bot",
69
+ description: null,
70
+ avatar: null,
71
+ category: null,
72
+ isPublic: false,
73
+ isVerified: false,
74
+ callbackUrl: null,
75
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
76
+ botUser: {
77
+ id: "bot-user-id",
78
+ username: "testbot",
79
+ displayName: "Test Bot",
80
+ avatar: null,
81
+ isBot: true
82
+ },
83
+ scopes: ["messages.read", "messages.write"]
84
+ };
85
+ // ── API namespaces ─────────────────────────────────────────────────────────
86
+ /** Mocked `client.messages` — same method names as the real MessagesAPI. */
87
+ this.messages = makeApiProxy("messages", this.calls, {
88
+ send: (channelId, options) => {
89
+ const opts = options;
90
+ const result = mockMsg({ channelId, ...opts });
91
+ this.calls.push({ api: "messages", method: "send", args: [channelId, options], result, timestamp: /* @__PURE__ */ new Date() });
92
+ return Promise.resolve(result);
93
+ },
94
+ fetch: (channelId, _opts) => {
95
+ const result = [];
96
+ this.calls.push({ api: "messages", method: "fetch", args: [channelId, _opts], result, timestamp: /* @__PURE__ */ new Date() });
97
+ return Promise.resolve(result);
98
+ }
99
+ });
100
+ /** Mocked `client.interactions` — records `respond()` calls. */
101
+ this.interactions = makeApiProxy("interactions", this.calls);
102
+ /** Mocked `client.members` */
103
+ this.members = makeApiProxy("members", this.calls);
104
+ /** Mocked `client.channels` */
105
+ this.channels = makeApiProxy("channels", this.calls);
106
+ /** Mocked `client.roles` */
107
+ this.roles = makeApiProxy("roles", this.calls);
108
+ /** Mocked `client.servers` */
109
+ this.servers = makeApiProxy("servers", this.calls);
110
+ /** Mocked `client.invites` */
111
+ this.invites = makeApiProxy("invites", this.calls);
112
+ // Everything else (webhooks, reactions, permissions, etc.)
113
+ this.webhooks = makeApiProxy("webhooks", this.calls);
114
+ this.reactions = makeApiProxy("reactions", this.calls);
115
+ this.permissions = makeApiProxy("permissions", this.calls);
116
+ this.auditlog = makeApiProxy("auditlog", this.calls);
117
+ this.forum = makeApiProxy("forum", this.calls);
118
+ this.events = makeApiProxy("events", this.calls);
119
+ this.categories = makeApiProxy("categories", this.calls);
120
+ this.automod = makeApiProxy("automod", this.calls);
121
+ this.users = makeApiProxy("users", this.calls);
122
+ // ── Client methods ─────────────────────────────────────────────────────────
123
+ /**
124
+ * Recorded WS sends (via `client.wsSend`).
125
+ * Inspected by `harness.assert.wsSent()`.
126
+ */
127
+ this.wsSends = [];
128
+ }
129
+ wsSend(channelId, content) {
130
+ this.wsSends.push({ channelId, content });
131
+ this.calls.push({
132
+ api: "ws",
133
+ method: "wsSend",
134
+ args: [channelId, content],
135
+ result: void 0,
136
+ timestamp: /* @__PURE__ */ new Date()
137
+ });
138
+ }
139
+ wsTypingStart(channelId) {
140
+ this.calls.push({ api: "ws", method: "wsTypingStart", args: [channelId], result: void 0, timestamp: /* @__PURE__ */ new Date() });
141
+ }
142
+ wsTypingStop(channelId) {
143
+ this.calls.push({ api: "ws", method: "wsTypingStop", args: [channelId], result: void 0, timestamp: /* @__PURE__ */ new Date() });
144
+ }
145
+ setStatus(_status) {
146
+ }
147
+ /** Simulated `client.connect()` — no-op in tests. */
148
+ connect() {
149
+ this.emit("ready", this.botUser);
150
+ return Promise.resolve(this);
151
+ }
152
+ /** Simulated `client.disconnect()` — emits `disconnect`. */
153
+ disconnect() {
154
+ this.emit("disconnect", "manual");
155
+ }
156
+ // ── Call inspection helpers ────────────────────────────────────────────────
157
+ /**
158
+ * Find all recorded calls to a given API namespace + method.
159
+ *
160
+ * @example
161
+ * ```ts
162
+ * const sends = client.findCalls('messages', 'send')
163
+ * // sends[0].args → [channelId, options]
164
+ * ```
165
+ */
166
+ findCalls(api, method) {
167
+ return this.calls.filter((c) => c.api === api && c.method === method);
168
+ }
169
+ /**
170
+ * Find all `messages.send` calls targeting a specific channel.
171
+ */
172
+ sentTo(channelId) {
173
+ return this.findCalls("messages", "send").filter((c) => c.args[0] === channelId);
174
+ }
175
+ /**
176
+ * Find all `interactions.respond` calls for a specific interaction ID.
177
+ */
178
+ repliedTo(interactionId) {
179
+ return this.findCalls("interactions", "respond").filter((c) => c.args[0] === interactionId);
180
+ }
181
+ /** Reset all recorded calls and WS sends. */
182
+ resetCalls() {
183
+ this.calls.length = 0;
184
+ this.wsSends.length = 0;
185
+ }
186
+ on(event, listener) {
187
+ return super.on(event, listener);
188
+ }
189
+ once(event, listener) {
190
+ return super.once(event, listener);
191
+ }
192
+ off(event, listener) {
193
+ return super.off(event, listener);
194
+ }
195
+ emit(event, ...args) {
196
+ return super.emit(event, ...args);
197
+ }
198
+ };
199
+ function asMockClient(client) {
200
+ return client;
201
+ }
202
+
203
+ // src/testing/EventSimulator.ts
204
+ var import_node_crypto2 = require("crypto");
205
+
206
+ // src/testing/types.ts
207
+ function buildMockMessage(input = {}) {
208
+ return {
209
+ id: input.id ?? `msg-${Date.now()}`,
210
+ content: input.content ?? "",
211
+ channelId: input.channelId ?? "ch-default",
212
+ author: {
213
+ id: input.authorId ?? "user-default",
214
+ username: input.authorName ?? "user",
215
+ displayName: input.authorName ?? "User",
216
+ avatar: null,
217
+ isBot: false
218
+ },
219
+ attachments: [],
220
+ reactions: [],
221
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
222
+ ...input.extra
223
+ };
224
+ }
225
+ function buildMockInteraction(input = {}) {
226
+ return {
227
+ id: `int-${Date.now()}`,
228
+ type: input.type ?? "SLASH_COMMAND",
229
+ commandName: input.commandName ?? null,
230
+ customId: input.customId ?? null,
231
+ data: input.data ?? null,
232
+ values: input.values,
233
+ modalData: input.modalData,
234
+ userId: input.userId ?? "user-default",
235
+ channelId: input.channelId ?? "ch-default",
236
+ serverId: input.serverId ?? "srv-default",
237
+ triggerMsgId: null,
238
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
239
+ ...input.extra
240
+ };
241
+ }
242
+ function buildMockMember(input = {}) {
243
+ return {
244
+ role: input.role ?? "MEMBER",
245
+ joinedAt: (/* @__PURE__ */ new Date()).toISOString(),
246
+ user: {
247
+ id: input.userId ?? "user-default",
248
+ username: "user",
249
+ displayName: "User",
250
+ avatar: null,
251
+ status: "ONLINE",
252
+ isBot: false
253
+ },
254
+ ...input.extra
255
+ };
256
+ }
257
+
258
+ // src/testing/EventSimulator.ts
259
+ var EventSimulator = class {
260
+ constructor(client) {
261
+ this.client = client;
262
+ }
263
+ // ── Helpers ────────────────────────────────────────────────────────────────
264
+ /** Await all already-queued microtasks (useful after async event handlers). */
265
+ flush() {
266
+ return new Promise((resolve) => setImmediate(resolve));
267
+ }
268
+ // ── Message events ─────────────────────────────────────────────────────────
269
+ /**
270
+ * Emit a `message` event (bot received a message in a text channel).
271
+ *
272
+ * @example
273
+ * ```ts
274
+ * sim.message({ content: '!ping', channelId: 'ch-1' })
275
+ * ```
276
+ */
277
+ message(input = {}) {
278
+ const msg = buildMockMessage(input);
279
+ this.client.emit("message", msg);
280
+ return msg;
281
+ }
282
+ /**
283
+ * Emit a `messageEdit` event.
284
+ */
285
+ messageEdit(input = {}) {
286
+ const msg = buildMockMessage({ ...input, content: input.newContent ?? input.content });
287
+ this.client.emit("messageEdit", msg);
288
+ return msg;
289
+ }
290
+ /**
291
+ * Emit a `messageDelete` event.
292
+ */
293
+ messageDelete(messageId, channelId) {
294
+ const payload = { messageId: messageId ?? `msg-${(0, import_node_crypto2.randomUUID)()}`, channelId: channelId ?? "ch-default" };
295
+ this.client.emit("messageDelete", payload);
296
+ return payload;
297
+ }
298
+ // ── Interaction events ─────────────────────────────────────────────────────
299
+ /**
300
+ * Emit an `interaction` event for any interaction type.
301
+ *
302
+ * @example
303
+ * ```ts
304
+ * sim.interaction({ type: 'PREFIX_COMMAND', commandName: 'help' })
305
+ * ```
306
+ */
307
+ interaction(input = {}) {
308
+ const interaction = buildMockInteraction(input);
309
+ this.client.emit("interaction", interaction);
310
+ return interaction;
311
+ }
312
+ /**
313
+ * Fire a `SLASH_COMMAND` interaction.
314
+ *
315
+ * @example
316
+ * ```ts
317
+ * sim.slashCommand({ name: 'ping' })
318
+ * sim.slashCommand({ name: 'ban', options: { user: 'user-123', reason: 'spam' } })
319
+ * ```
320
+ */
321
+ slashCommand(input) {
322
+ return this.interaction({
323
+ type: "SLASH_COMMAND",
324
+ commandName: input.name,
325
+ userId: input.userId,
326
+ channelId: input.channelId,
327
+ serverId: input.serverId,
328
+ data: input.options
329
+ });
330
+ }
331
+ /**
332
+ * Fire a `BUTTON_CLICK` interaction.
333
+ *
334
+ * @example
335
+ * ```ts
336
+ * sim.buttonClick({ customId: 'confirm_delete' })
337
+ * ```
338
+ */
339
+ buttonClick(input) {
340
+ return this.interaction({
341
+ type: "BUTTON_CLICK",
342
+ customId: input.customId,
343
+ userId: input.userId,
344
+ channelId: input.channelId,
345
+ serverId: input.serverId,
346
+ extra: { triggerMsgId: input.triggerMsgId ?? null }
347
+ });
348
+ }
349
+ /**
350
+ * Fire a `SELECT_MENU` interaction.
351
+ *
352
+ * @example
353
+ * ```ts
354
+ * sim.selectMenu({ customId: 'role_picker', values: ['mod', 'admin'] })
355
+ * ```
356
+ */
357
+ selectMenu(input) {
358
+ return this.interaction({
359
+ type: "SELECT_MENU",
360
+ customId: input.customId,
361
+ values: input.values,
362
+ userId: input.userId,
363
+ channelId: input.channelId,
364
+ serverId: input.serverId
365
+ });
366
+ }
367
+ /**
368
+ * Fire a `MODAL_SUBMIT` interaction.
369
+ *
370
+ * @example
371
+ * ```ts
372
+ * sim.modalSubmit({ customId: 'report_modal', modalData: { reason: 'spam' } })
373
+ * ```
374
+ */
375
+ modalSubmit(input) {
376
+ return this.interaction({
377
+ type: "MODAL_SUBMIT",
378
+ customId: input.customId,
379
+ modalData: input.modalData,
380
+ userId: input.userId,
381
+ channelId: input.channelId,
382
+ serverId: input.serverId
383
+ });
384
+ }
385
+ /**
386
+ * Fire a `PREFIX_COMMAND` interaction.
387
+ *
388
+ * @example
389
+ * ```ts
390
+ * sim.prefixCommand({ name: 'kick', userId: 'user-1' })
391
+ * ```
392
+ */
393
+ prefixCommand(input) {
394
+ return this.interaction({
395
+ type: "PREFIX_COMMAND",
396
+ commandName: input.name,
397
+ userId: input.userId,
398
+ channelId: input.channelId,
399
+ serverId: input.serverId,
400
+ data: input.options
401
+ });
402
+ }
403
+ // ── Member events ──────────────────────────────────────────────────────────
404
+ /**
405
+ * Emit a `memberJoin` event.
406
+ *
407
+ * @example
408
+ * ```ts
409
+ * sim.memberJoin({ userId: 'user-123', serverId: 'srv-1' })
410
+ * ```
411
+ */
412
+ memberJoin(input = {}) {
413
+ const member = buildMockMember(input);
414
+ this.client.emit("memberJoin", member, input.serverId ?? "srv-default");
415
+ return member;
416
+ }
417
+ /**
418
+ * Emit a `memberLeave` event.
419
+ */
420
+ memberLeave(input = {}) {
421
+ const member = buildMockMember(input);
422
+ this.client.emit("memberLeave", member, input.serverId ?? "srv-default");
423
+ return member;
424
+ }
425
+ /**
426
+ * Emit a `memberUpdate` event.
427
+ */
428
+ memberUpdate(serverId, userId) {
429
+ this.client.emit("memberUpdate", {
430
+ userId: userId ?? "user-default",
431
+ serverId: serverId ?? "srv-default"
432
+ });
433
+ }
434
+ // ── Voice events ───────────────────────────────────────────────────────────
435
+ /**
436
+ * Emit a `voiceJoin` event.
437
+ *
438
+ * @example
439
+ * ```ts
440
+ * sim.voiceJoin({ userId: 'user-1', channelId: 'vc-1', serverId: 'srv-1' })
441
+ * ```
442
+ */
443
+ voiceJoin(input = {}) {
444
+ const payload = {
445
+ userId: input.userId ?? "user-default",
446
+ channelId: input.channelId ?? "vc-default",
447
+ serverId: input.serverId ?? "srv-default"
448
+ };
449
+ this.client.emit("voiceJoin", payload);
450
+ return payload;
451
+ }
452
+ /**
453
+ * Emit a `voiceLeave` event.
454
+ */
455
+ voiceLeave(input = {}) {
456
+ const payload = {
457
+ userId: input.userId ?? "user-default",
458
+ channelId: input.channelId ?? "vc-default",
459
+ serverId: input.serverId ?? "srv-default"
460
+ };
461
+ this.client.emit("voiceLeave", payload);
462
+ return payload;
463
+ }
464
+ // ── Reaction events ────────────────────────────────────────────────────────
465
+ /**
466
+ * Emit a `reactionAdd` event.
467
+ *
468
+ * @example
469
+ * ```ts
470
+ * sim.reactionAdd({ messageId: 'msg-1', emoji: '👍' })
471
+ * ```
472
+ */
473
+ reactionAdd(input) {
474
+ this.client.emit("reactionAdd", {
475
+ messageId: input.messageId,
476
+ emoji: input.emoji,
477
+ userId: input.userId ?? "user-default",
478
+ channelId: input.channelId ?? "ch-default"
479
+ });
480
+ }
481
+ /**
482
+ * Emit a `reactionRemove` event.
483
+ */
484
+ reactionRemove(input) {
485
+ this.client.emit("reactionRemove", {
486
+ messageId: input.messageId,
487
+ emoji: input.emoji,
488
+ userId: input.userId ?? "user-default",
489
+ channelId: input.channelId ?? "ch-default"
490
+ });
491
+ }
492
+ // ── Channel / role / server events ─────────────────────────────────────────
493
+ /**
494
+ * Emit a `channelCreate` event.
495
+ */
496
+ channelCreate(partial) {
497
+ const ch = {
498
+ id: `ch-${(0, import_node_crypto2.randomUUID)()}`,
499
+ name: "test-channel",
500
+ type: "TEXT",
501
+ topic: null,
502
+ position: 0,
503
+ slowMode: 0,
504
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
505
+ ...partial
506
+ };
507
+ this.client.emit("channelCreate", ch);
508
+ return ch;
509
+ }
510
+ /**
511
+ * Emit a `channelDelete` event.
512
+ */
513
+ channelDelete(id, serverId) {
514
+ this.client.emit("channelDelete", { id, serverId });
515
+ }
516
+ /**
517
+ * Emit a `roleCreate` event.
518
+ */
519
+ roleCreate(partial) {
520
+ const role = {
521
+ id: `role-${(0, import_node_crypto2.randomUUID)()}`,
522
+ name: "test-role",
523
+ color: null,
524
+ position: 0,
525
+ hoist: false,
526
+ permissions: {},
527
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
528
+ ...partial
529
+ };
530
+ this.client.emit("roleCreate", role);
531
+ return role;
532
+ }
533
+ /**
534
+ * Emit a `roleDelete` event.
535
+ */
536
+ roleDelete(id, serverId) {
537
+ this.client.emit("roleDelete", { id, serverId });
538
+ }
539
+ // ── Moderation events ──────────────────────────────────────────────────────
540
+ /**
541
+ * Emit a `memberBanned` event.
542
+ */
543
+ memberBanned(userId, serverId, reason) {
544
+ this.client.emit("memberBanned", {
545
+ userId,
546
+ serverId,
547
+ moderatorId: this.client.botUser?.botUser?.id ?? null,
548
+ reason: reason ?? null
549
+ });
550
+ }
551
+ /**
552
+ * Emit a `memberUnbanned` event.
553
+ */
554
+ memberUnbanned(userId, serverId) {
555
+ this.client.emit("memberUnbanned", { userId, serverId });
556
+ }
557
+ // ── Bot lifecycle events ───────────────────────────────────────────────────
558
+ /**
559
+ * Emit the `ready` event (simulates successful bot connection).
560
+ */
561
+ ready() {
562
+ this.client.emit("ready", this.client.botUser);
563
+ }
564
+ /**
565
+ * Emit the `disconnect` event.
566
+ */
567
+ disconnect(reason = "test") {
568
+ this.client.emit("disconnect", reason);
569
+ }
570
+ };
571
+
572
+ // src/testing/TestHarness.ts
573
+ var HarnessAssertions = class {
574
+ constructor(client) {
575
+ this.client = client;
576
+ }
577
+ // ── Send assertions ──────────────────────────────────────────────────────
578
+ /**
579
+ * Assert that `messages.send` was called **at least once** (or exactly `times`
580
+ * times), optionally matching specific call properties.
581
+ */
582
+ sent(options = {}) {
583
+ const raw = this.client.findCalls("messages", "send");
584
+ const { content, channelId, embedTitle, embedField, times } = options;
585
+ const matches = raw.filter((call) => {
586
+ const [cid, opts] = call.args;
587
+ if (channelId && cid !== channelId) return false;
588
+ if (content) {
589
+ const bodyContent = opts?.content ?? "";
590
+ if (!bodyContent.includes(content)) return false;
591
+ }
592
+ if (embedTitle) {
593
+ const embeds = opts?.embeds ?? [];
594
+ if (!embeds.some((e) => e.title?.includes(embedTitle))) return false;
595
+ }
596
+ if (embedField) {
597
+ const embeds = opts?.embeds ?? [];
598
+ const hasField = embeds.some(
599
+ (e) => e.fields?.some(
600
+ (f) => f.name?.includes(embedField) || f.value?.includes(embedField)
601
+ )
602
+ );
603
+ if (!hasField) return false;
604
+ }
605
+ return true;
606
+ });
607
+ if (times !== void 0) {
608
+ if (matches.length !== times) {
609
+ throw new HarnessAssertionError(
610
+ `Expected messages.send to be called ${times} time(s) matching options, but got ${matches.length}.` + formatCallDiff(raw, options)
611
+ );
612
+ }
613
+ } else if (matches.length === 0) {
614
+ throw new HarnessAssertionError(
615
+ `Expected messages.send to be called${describeOptions(options)}, but no matching calls were found.` + formatCallDiff(raw, options)
616
+ );
617
+ }
618
+ }
619
+ /**
620
+ * Assert that `interactions.respond` was called, optionally matching content.
621
+ */
622
+ replied(options = {}) {
623
+ const raw = this.client.findCalls("interactions", "respond");
624
+ const { content, embedTitle, times } = options;
625
+ const matches = raw.filter((call) => {
626
+ const [, opts] = call.args;
627
+ if (content) {
628
+ const bodyContent = opts?.content ?? "";
629
+ if (!bodyContent.includes(content)) return false;
630
+ }
631
+ if (embedTitle) {
632
+ const embeds = opts?.embeds ?? [];
633
+ if (!embeds.some((e) => e.title?.includes(embedTitle))) return false;
634
+ }
635
+ return true;
636
+ });
637
+ if (times !== void 0) {
638
+ if (matches.length !== times) {
639
+ throw new HarnessAssertionError(
640
+ `Expected interactions.respond to be called ${times} time(s), but got ${matches.length}.`
641
+ );
642
+ }
643
+ } else if (matches.length === 0) {
644
+ throw new HarnessAssertionError(
645
+ `Expected interactions.respond to be called${describeOptions(options)}, but no matching calls were found.`
646
+ );
647
+ }
648
+ }
649
+ /**
650
+ * Assert that `client.wsSend` was called (optionally matching channel/content).
651
+ */
652
+ wsSent(channelId, content) {
653
+ const matches = this.client.wsSends.filter((send) => {
654
+ if (channelId && send.channelId !== channelId) return false;
655
+ if (content && !send.content.includes(content)) return false;
656
+ return true;
657
+ });
658
+ if (matches.length === 0) {
659
+ throw new HarnessAssertionError(
660
+ `Expected wsSend to be called${channelId ? ` to channel "${channelId}"` : ""}${content ? ` with content "${content}"` : ""}, but no matching sends found.`
661
+ );
662
+ }
663
+ }
664
+ /**
665
+ * Assert that a specific API method was called exactly `expected` times.
666
+ *
667
+ * @example
668
+ * ```ts
669
+ * harness.assert.callCount('members', 'kick', 1)
670
+ * ```
671
+ */
672
+ callCount(api, method, expected) {
673
+ const actual = this.client.findCalls(api, method).length;
674
+ if (actual !== expected) {
675
+ throw new HarnessAssertionError(
676
+ `Expected ${api}.${method} to be called ${expected} time(s), but was called ${actual} time(s).`
677
+ );
678
+ }
679
+ }
680
+ /**
681
+ * Assert that no API calls have been recorded.
682
+ */
683
+ noCalls() {
684
+ if (this.client.calls.length > 0) {
685
+ const summary = this.client.calls.map((c) => ` ${c.api}.${c.method}`).join("\n");
686
+ throw new HarnessAssertionError(
687
+ `Expected no API calls, but found ${this.client.calls.length}:
688
+ ${summary}`
689
+ );
690
+ }
691
+ }
692
+ /**
693
+ * Assert that no messages were sent (neither via `messages.send` nor `wsSend`).
694
+ */
695
+ noMessagesSent() {
696
+ const sends = this.client.findCalls("messages", "send");
697
+ const wsSends = this.client.wsSends;
698
+ if (sends.length > 0 || wsSends.length > 0) {
699
+ throw new HarnessAssertionError(
700
+ `Expected no messages to be sent, but found ${sends.length} message.send call(s) and ${wsSends.length} wsSend call(s).`
701
+ );
702
+ }
703
+ }
704
+ /**
705
+ * Assert that a specific API method was called at least once.
706
+ *
707
+ * @example
708
+ * ```ts
709
+ * harness.assert.called('members', 'kick')
710
+ * ```
711
+ */
712
+ called(api, method) {
713
+ const found = this.client.findCalls(api, method);
714
+ if (found.length === 0) {
715
+ throw new HarnessAssertionError(`Expected ${api}.${method} to be called, but it was not.`);
716
+ }
717
+ }
718
+ /**
719
+ * Assert that a specific API method was **never** called.
720
+ *
721
+ * @example
722
+ * ```ts
723
+ * harness.assert.notCalled('members', 'ban')
724
+ * ```
725
+ */
726
+ notCalled(api, method) {
727
+ const found = this.client.findCalls(api, method);
728
+ if (found.length > 0) {
729
+ throw new HarnessAssertionError(
730
+ `Expected ${api}.${method} not to be called, but it was called ${found.length} time(s).`
731
+ );
732
+ }
733
+ }
734
+ };
735
+ var TestHarness = class {
736
+ constructor() {
737
+ this.client = new MockClient();
738
+ this.simulate = new EventSimulator(this.client);
739
+ this.assert = new HarnessAssertions(this.client);
740
+ }
741
+ // ── Setup ──────────────────────────────────────────────────────────────────
742
+ /**
743
+ * Run a setup function against the mock client (e.g. register commands /
744
+ * event handlers). Supports both sync and async setups.
745
+ *
746
+ * Chainable — call `use` multiple times to compose setup:
747
+ *
748
+ * @example
749
+ * ```ts
750
+ * harness
751
+ * .use(registerCommands)
752
+ * .use(registerEventHandlers)
753
+ * ```
754
+ */
755
+ use(fn) {
756
+ const result = fn(this.client);
757
+ if (result instanceof Promise) {
758
+ result.catch((err) => {
759
+ throw err;
760
+ });
761
+ }
762
+ return this;
763
+ }
764
+ // ── State management ───────────────────────────────────────────────────────
765
+ /**
766
+ * Clear all recorded API calls, wsSend history, and event listeners.
767
+ * Returns `this` so you can chain: `harness.reset().use(...)`.
768
+ */
769
+ reset() {
770
+ this.client.resetCalls();
771
+ this.client.removeAllListeners();
772
+ return this;
773
+ }
774
+ /**
775
+ * Awaits all queued microtasks and setImmediate callbacks so that any async
776
+ * event handlers registered with `use()` have fully run.
777
+ *
778
+ * @example
779
+ * ```ts
780
+ * harness.simulate.slashCommand({ name: 'ban', options: { user: 'u-1' } })
781
+ * await harness.flush()
782
+ * harness.assert.called('members', 'ban')
783
+ * ```
784
+ */
785
+ flush() {
786
+ return new Promise((resolve) => setImmediate(resolve));
787
+ }
788
+ // ── Direct call access ─────────────────────────────────────────────────────
789
+ /**
790
+ * All recorded API calls (shorthand for `harness.client.calls`).
791
+ */
792
+ get calls() {
793
+ return this.client.calls;
794
+ }
795
+ /**
796
+ * All `messages.send` calls targeting `channelId` (or all if omitted).
797
+ */
798
+ getSent(channelId) {
799
+ const raw = this.client.findCalls("messages", "send");
800
+ return raw.filter((c) => {
801
+ const [cid] = c.args;
802
+ return channelId === void 0 || cid === channelId;
803
+ }).map((c) => {
804
+ const [cid, ...rest] = c.args;
805
+ return { channelId: cid, args: rest };
806
+ });
807
+ }
808
+ /**
809
+ * All `interactions.respond` call payloads.
810
+ */
811
+ getReplies() {
812
+ return this.client.findCalls("interactions", "respond").map((c) => {
813
+ const [iid, ...rest] = c.args;
814
+ return { interactionId: iid, args: rest };
815
+ });
816
+ }
817
+ };
818
+ var HarnessAssertionError = class extends Error {
819
+ constructor(message) {
820
+ super(message);
821
+ this.name = "HarnessAssertionError";
822
+ }
823
+ };
824
+ function describeOptions(options) {
825
+ const parts = [];
826
+ if ("channelId" in options && options.channelId) parts.push(`to channel "${options.channelId}"`);
827
+ if (options.content) parts.push(`containing "${options.content}"`);
828
+ if (options.embedTitle) parts.push(`with embed title "${options.embedTitle}"`);
829
+ return parts.length ? ` ${parts.join(", ")}` : "";
830
+ }
831
+ function formatCallDiff(calls, options) {
832
+ if (calls.length === 0) return "\n (no messages.send calls recorded)";
833
+ const list = calls.map((c) => {
834
+ const [cid, opts] = c.args;
835
+ const content = opts?.content ?? "(none)";
836
+ return ` \u2192 channelId=${cid} content="${content.slice(0, 60)}"`;
837
+ }).join("\n");
838
+ return `
839
+ Recorded calls:
840
+ ${list}`;
841
+ }
842
+ // Annotate the CommonJS export names for ESM import in node:
843
+ 0 && (module.exports = {
844
+ EventSimulator,
845
+ HarnessAssertionError,
846
+ MockClient,
847
+ TestHarness,
848
+ asMockClient
849
+ });