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