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.
- package/bin/nova.mjs +23 -0
- package/dist/chunk-53M2BTB5.mjs +26 -0
- package/dist/chunk-LMORGVJR.mjs +124 -0
- package/dist/cli/index.d.mts +6 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.js +871 -0
- package/dist/cli/index.mjs +716 -0
- package/dist/client-DbKa6yJo.d.mts +4244 -0
- package/dist/client-DbKa6yJo.d.ts +4244 -0
- package/dist/devtools/index.d.mts +94 -0
- package/dist/devtools/index.d.ts +94 -0
- package/dist/devtools/index.js +160 -0
- package/dist/devtools/index.mjs +7 -0
- package/dist/index.d.mts +6802 -2834
- package/dist/index.d.ts +6802 -2834
- package/dist/index.js +11200 -1342
- package/dist/index.mjs +11109 -1342
- package/dist/testing/index.d.mts +689 -0
- package/dist/testing/index.d.ts +689 -0
- package/dist/testing/index.js +849 -0
- package/dist/testing/index.mjs +820 -0
- package/package.json +17 -3
|
@@ -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
|
+
});
|