meocord 4.2.0 → 4.2.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.
Files changed (103) hide show
  1. package/CHANGELOG.md +322 -0
  2. package/THIRD_PARTY_NOTICES.md +14 -13
  3. package/dist/cjs/_shared/{controller.decorator-CRn9kWob.cjs → controller.decorator-U2NBWcoU.cjs} +416 -108
  4. package/dist/cjs/_shared/{providers-ChvMqnLd.cjs → providers-B4MOFBrW.cjs} +202 -50
  5. package/dist/cjs/_shared/{sharded-cooldown-store-BdAUTdYs.cjs → sharded-cooldown-store-HTUb3Ii_.cjs} +10 -7
  6. package/dist/cjs/_shared/{startup-roots-CQo8mSP8.cjs → startup-roots-lA5xkBFq.cjs} +228 -71
  7. package/dist/cjs/_shared/{translate-error-DhDVbT5M.cjs → translate-error-CbBHUu5P.cjs} +10 -5
  8. package/dist/cjs/common/index.cjs +3 -3
  9. package/dist/cjs/core/index.cjs +188 -169
  10. package/dist/cjs/decorator/index.cjs +52 -7
  11. package/dist/cjs/testing/index.cjs +1854 -311
  12. package/dist/cli.json +1 -1
  13. package/dist/esm/bin/generator.js +4 -5
  14. package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
  15. package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
  16. package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
  17. package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
  18. package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
  19. package/dist/esm/bin/helper/service-generator.helper.js +4 -5
  20. package/dist/esm/bin/meocord.js +84 -25
  21. package/dist/esm/build/load-config.pre-entry.js +13 -3
  22. package/dist/esm/build/rsbuild-config.js +16 -12
  23. package/dist/esm/build/typed-externals.js +36 -0
  24. package/dist/esm/common/cooldown-store.js +10 -2
  25. package/dist/esm/common/errors.js +25 -0
  26. package/dist/esm/common/meocord-text.js +11 -8
  27. package/dist/esm/common/response/call-order.js +9 -4
  28. package/dist/esm/common/response/presenter.js +33 -6
  29. package/dist/esm/common/response/response-state.js +89 -15
  30. package/dist/esm/common/route.js +11 -4
  31. package/dist/esm/common/sharded-cooldown-store.js +1 -1
  32. package/dist/esm/common/translator.js +52 -11
  33. package/dist/esm/core/command-registration.js +93 -24
  34. package/dist/esm/core/command-routes.js +2 -2
  35. package/dist/esm/core/cooldown-runner.js +29 -8
  36. package/dist/esm/core/dispatcher.js +9 -8
  37. package/dist/esm/core/fallback.js +20 -11
  38. package/dist/esm/core/filter-runner.js +2 -2
  39. package/dist/esm/core/guard-runner.js +16 -3
  40. package/dist/esm/core/handler-input.js +4 -3
  41. package/dist/esm/core/handler-pipeline.js +10 -4
  42. package/dist/esm/core/lifecycle-hooks.js +134 -47
  43. package/dist/esm/core/meocord-factory.js +59 -34
  44. package/dist/esm/core/meocord.app.js +116 -75
  45. package/dist/esm/core/message-help.js +32 -6
  46. package/dist/esm/core/observer-runner.js +2 -1
  47. package/dist/esm/core/providers.js +19 -4
  48. package/dist/esm/core/running-calls.js +43 -0
  49. package/dist/esm/core/shard-context.js +5 -6
  50. package/dist/esm/core/shard-manager.js +16 -10
  51. package/dist/esm/core/stage-scope.js +2 -1
  52. package/dist/esm/core/startup-roots.js +21 -1
  53. package/dist/esm/core/theme-resolvers.js +4 -1
  54. package/dist/esm/core/theme-scope.js +7 -2
  55. package/dist/esm/core/theme-validation.js +17 -10
  56. package/dist/esm/decorator/app.decorator.js +31 -0
  57. package/dist/esm/decorator/controller.decorator.js +56 -20
  58. package/dist/esm/decorator/cooldown.decorator.js +17 -0
  59. package/dist/esm/decorator/filter.decorator.js +2 -2
  60. package/dist/esm/decorator/guard.decorator.js +3 -2
  61. package/dist/esm/package.json.js +1 -1
  62. package/dist/esm/testing/catalog.js +20 -13
  63. package/dist/esm/testing/cooldown-store-suite.js +67 -5
  64. package/dist/esm/testing/discord-defaults.js +225 -30
  65. package/dist/esm/testing/meocord-testing-module.js +158 -44
  66. package/dist/esm/testing/mock-fn.js +107 -26
  67. package/dist/esm/testing/mock-interaction.js +1250 -182
  68. package/dist/esm/testing/snowflake.js +9 -2
  69. package/dist/esm/testing/strict-mocks.js +40 -17
  70. package/dist/esm/util/common.util.js +47 -19
  71. package/dist/esm/util/dev-runner.util.js +8 -8
  72. package/dist/esm/util/generator-cli.util.js +4 -5
  73. package/dist/esm/util/interaction.util.js +2 -2
  74. package/dist/esm/util/meocord-config-loader.util.js +10 -2
  75. package/dist/esm/util/meocord-config-validation.util.js +3 -5
  76. package/dist/esm/util/meocord-source-config.util.js +65 -20
  77. package/dist/esm/util/method-wrapper.util.js +16 -0
  78. package/dist/esm/util/platform.util.js +10 -10
  79. package/dist/esm/util/shard-process.util.js +4 -1
  80. package/dist/esm/util/sharding-mode.util.js +5 -7
  81. package/dist/esm/util/shared-state.util.js +21 -0
  82. package/dist/esm/util/tsconfig-paths.util.js +29 -0
  83. package/dist/esm/util/tsconfig.util.js +10 -10
  84. package/dist/esm/util/value.util.js +30 -2
  85. package/dist/types/common/index.d.cts +10 -7
  86. package/dist/types/common/index.d.ts +10 -7
  87. package/dist/types/core/index.d.cts +3 -2
  88. package/dist/types/core/index.d.ts +3 -2
  89. package/dist/types/decorator/index.d.cts +1 -0
  90. package/dist/types/decorator/index.d.ts +1 -0
  91. package/dist/types/{index-CdXsFgjE.d.ts → index-DKJclIi-.d.ts} +45 -21
  92. package/dist/types/{index-nn5p7OoX.d.cts → index-Dlf0ZZtS.d.cts} +45 -21
  93. package/dist/types/interface/index.d.cts +1 -1
  94. package/dist/types/interface/index.d.ts +1 -1
  95. package/dist/types/{route-BZgFoNeB.d.cts → route-Bh1U13yy.d.cts} +3 -2
  96. package/dist/types/{route-BZgFoNeB.d.ts → route-Bh1U13yy.d.ts} +3 -2
  97. package/dist/types/testing/index.d.cts +198 -62
  98. package/dist/types/testing/index.d.ts +198 -62
  99. package/dist/types/{theme-resolvers-UKr3-6Ys.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
  100. package/dist/types/{theme-resolvers-BcOJVzR9.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
  101. package/dist/types/{translator-DHMkkCpg.d.cts → translator-Bg7hwxDF.d.cts} +21 -8
  102. package/dist/types/{translator-BViWKtLJ.d.ts → translator-CeP4bCzp.d.ts} +21 -8
  103. package/package.json +2 -2
@@ -2,11 +2,11 @@
2
2
 
3
3
  require('reflect-metadata');
4
4
  var inversify = require('inversify');
5
- var providers = require('../_shared/providers-ChvMqnLd.cjs');
5
+ var providers = require('../_shared/providers-B4MOFBrW.cjs');
6
6
  var discord_js = require('discord.js');
7
7
  var enum_index = require('../enum/index.cjs');
8
- var controller_decorator = require('../_shared/controller.decorator-CRn9kWob.cjs');
9
- var startupRoots = require('../_shared/startup-roots-CQo8mSP8.cjs');
8
+ var controller_decorator = require('../_shared/controller.decorator-U2NBWcoU.cjs');
9
+ var startupRoots = require('../_shared/startup-roots-lA5xkBFq.cjs');
10
10
  var node_crypto = require('node:crypto');
11
11
  require('node:async_hooks');
12
12
  require('crypto');
@@ -21,12 +21,10 @@ require('dayjs/plugin/utc.js');
21
21
  require('dayjs/plugin/timezone.js');
22
22
  require('module');
23
23
  require('chalk');
24
- require('../_shared/translate-error-DhDVbT5M.cjs');
24
+ require('../_shared/translate-error-CbBHUu5P.cjs');
25
25
 
26
26
  // Framework-agnostic mock function, so `meocord/testing` depends on neither jest nor vitest.
27
27
  // Both detect it through `_isMockFunction` and read `fn.mock.calls` in their assertions.
28
- // Each call signature of `T`, up to four, as a union: `Parameters` and `ReturnType` read only an overloaded function's
29
- // last signature, such as the list form of a manager's `fetch`
30
28
  /**
31
29
  * Tells whether a value is a mock function: one from `meocord/testing`, `jest.fn()` or `vi.fn()`.
32
30
  *
@@ -49,13 +47,20 @@ require('../_shared/translate-error-DhDVbT5M.cjs');
49
47
  */ function isMockFunction(fn) {
50
48
  return typeof fn === 'function' && '_isMockFunction' in fn && fn._isMockFunction === true;
51
49
  }
52
- // Every mock createMockFn made, held weakly so the mocks of a finished test can still be collected
53
- const created = new Set();
54
- const collected = new FinalizationRegistry((ref)=>created.delete(ref));
55
- // The runner's factory useMockFn registered, and for each of its mocks how to give back the implementation it was
56
- // created with, through the runner's own setter, which a mock's behaviour may wrap
57
- let runnerFactory;
58
- const restoreImpl = new WeakMap();
50
+ /**
51
+ * What `useMockFn` set and the mocks made, kept for both builds of this meocord version, so a setup file in one module
52
+ * format sets them for specs in the other:
53
+ * - `invocationOrder`: when each call of meocord's own mocks came, across all of them, as Vitest numbers its own;
54
+ * - `created`: every mock createMockFn made, held weakly so the mocks of a finished test can still be collected;
55
+ * - `runnerFactory` and `restoreImpl`: the runner's factory useMockFn registered, and for each of its mocks how to
56
+ * give back the implementation it was created with, through the runner's own setter, which a mock's behaviour may wrap.
57
+ */ const mocks$1 = controller_decorator.shared('mocks', ()=>({
58
+ invocationOrder: 0,
59
+ created: new Set(),
60
+ runnerFactory: undefined,
61
+ restoreImpl: new WeakMap()
62
+ }));
63
+ const collected = new FinalizationRegistry((ref)=>mocks$1.created.delete(ref));
59
64
  /**
60
65
  * Makes every mock `meocord/testing` creates with the test runner's own mock function, such as Vitest's `vi.fn`.
61
66
  *
@@ -86,26 +91,36 @@ const restoreImpl = new WeakMap();
86
91
  * @category Mocks
87
92
  * @see {@link createMockFn}
88
93
  */ function useMockFn(factory) {
89
- if (factory === runnerFactory) return;
94
+ if (factory === mocks$1.runnerFactory) return;
90
95
  if ([
91
- ...created
96
+ ...mocks$1.created
92
97
  ].some((ref)=>ref.deref() !== undefined)) {
93
98
  throw new Error("useMockFn() goes before any mock is made: a mock made before it keeps meocord's own function, and the two kinds " + "would mix. Call it once, in the test runner's setup file.");
94
99
  }
95
- const probe = factory();
100
+ const refusal = (cause)=>new Error("useMockFn() takes a function that makes mocks with jest's and Vitest's API, such as vi.fn, jest.fn or bun's " + "mock. node:test's mock.fn records calls in its own shape, so under node:test leave meocord's own mock function " + 'in place.', cause === undefined ? undefined : {
101
+ cause
102
+ });
103
+ let probe;
104
+ try {
105
+ probe = factory();
106
+ } catch (error) {
107
+ // node:test's mock.fn, given on its own, throws for want of its tracker
108
+ throw refusal(error);
109
+ }
96
110
  const api = [
97
111
  'mockImplementation',
98
112
  'mockClear',
99
113
  'mockReset'
100
114
  ];
101
115
  if (!isMockFunction(probe) || !Array.isArray(probe.mock?.calls) || api.some((name)=>typeof probe[name] !== 'function')) {
102
- throw new Error("useMockFn() takes a function that makes mocks with jest's and Vitest's API, such as vi.fn, jest.fn or bun's " + "mock. node:test's mock.fn records calls in its own shape, so under node:test leave meocord's own mock function " + 'in place.');
116
+ throw refusal();
103
117
  }
104
- runnerFactory = factory;
118
+ mocks$1.runnerFactory = factory;
105
119
  }
120
+ /** Whether mocks are made with a runner's function, which {@link useMockFn} registered. */ const usesRunnerMockFn = ()=>mocks$1.runnerFactory !== undefined;
106
121
  /** Tracks a mock for clearAllMocks and resetAllMocks. */ function track(mockFn) {
107
122
  const ref = new WeakRef(mockFn);
108
- created.add(ref);
123
+ mocks$1.created.add(ref);
109
124
  collected.register(mockFn, ref);
110
125
  return mockFn;
111
126
  }
@@ -128,7 +143,7 @@ const restoreImpl = new WeakMap();
128
143
  * @category Mocks
129
144
  * @see {@link resetAllMocks}
130
145
  */ function clearAllMocks() {
131
- for (const ref of created)ref.deref()?.mockClear();
146
+ for (const ref of mocks$1.created)ref.deref()?.mockClear();
132
147
  }
133
148
  /**
134
149
  * Clears every mock from `meocord/testing`, and puts each back to the implementation it was created with.
@@ -151,12 +166,12 @@ const restoreImpl = new WeakMap();
151
166
  * @category Mocks
152
167
  * @see {@link clearAllMocks}
153
168
  */ function resetAllMocks() {
154
- for (const ref of created){
169
+ for (const ref of mocks$1.created){
155
170
  const mockFn = ref.deref();
156
171
  if (!mockFn) continue;
157
172
  mockFn.mockReset();
158
173
  // A runner whose reset drops the implementation the mock was created with, as jest's and bun's do, gets it back
159
- restoreImpl.get(mockFn)?.();
174
+ mocks$1.restoreImpl.get(mockFn)?.();
160
175
  }
161
176
  }
162
177
  /**
@@ -170,6 +185,14 @@ const restoreImpl = new WeakMap();
170
185
  * `mockReturnValue`, `mockResolvedValue`, `mockRejectedValue` and `mockImplementation` share one implementation, so
171
186
  * the last one set wins. The `*Once` variants queue, and run first, in the order they were set.
172
187
  *
188
+ * It records what Vitest's `toHaveResolved` and `toHaveBeenCalledBefore` read as well. Its call order is counted
189
+ * across meocord's own mocks, apart from the runner's, so compare the order of two of meocord's mocks, or of two of the
190
+ * runner's, not one of each.
191
+ *
192
+ * A promise the implementation returns is handed back as one that settles the same way, not as the same object: test
193
+ * it with `toEqual`, `toHaveReturnedWith` or by awaiting it, not with `toBe`. A property set on the given promise, such
194
+ * as a `cancel` method, isn't carried over.
195
+ *
173
196
  * @param impl - The implementation to run until another is set.
174
197
  *
175
198
  * @example
@@ -187,11 +210,12 @@ const restoreImpl = new WeakMap();
187
210
  * @category Mocks
188
211
  * @see {@link MockInstance}
189
212
  */ function createMockFn(impl) {
213
+ const { runnerFactory } = mocks$1;
190
214
  if (runnerFactory) {
191
215
  const runnerMock = runnerFactory(impl);
192
216
  if (impl) {
193
217
  const setImpl = runnerMock.mockImplementation.bind(runnerMock);
194
- restoreImpl.set(runnerMock, ()=>setImpl(impl));
218
+ mocks$1.restoreImpl.set(runnerMock, ()=>setImpl(impl));
195
219
  }
196
220
  return track(runnerMock);
197
221
  }
@@ -204,9 +228,20 @@ const restoreImpl = new WeakMap();
204
228
  const calls = [];
205
229
  const results = [];
206
230
  const instances = [];
231
+ const settledResults = [];
232
+ const invocationCallOrder = [];
233
+ const contexts = [];
207
234
  const mockFn = function(...args) {
208
235
  calls.push(args);
209
236
  instances.push(this);
237
+ contexts.push(this);
238
+ invocationCallOrder.push(++mocks$1.invocationOrder);
239
+ // Updated in place as the call settles, so a promise's outcome shows once it resolves or rejects
240
+ const settled = {
241
+ type: 'incomplete',
242
+ value: undefined
243
+ };
244
+ settledResults.push(settled);
210
245
  let type = 'return';
211
246
  let value;
212
247
  try {
@@ -217,10 +252,36 @@ const restoreImpl = new WeakMap();
217
252
  } else {
218
253
  value = undefined;
219
254
  }
255
+ if (value instanceof Promise) {
256
+ // Returned in its place, so a rejection nobody awaits stays unhandled in the test, as it is on a bot: a handler
257
+ // on the implementation's own promise would mark it handled
258
+ value = value.then((resolved)=>{
259
+ Object.assign(settled, {
260
+ type: 'fulfilled',
261
+ value: resolved
262
+ });
263
+ return resolved;
264
+ }, (rejected)=>{
265
+ Object.assign(settled, {
266
+ type: 'rejected',
267
+ value: rejected
268
+ });
269
+ throw rejected;
270
+ });
271
+ } else {
272
+ Object.assign(settled, {
273
+ type: 'fulfilled',
274
+ value
275
+ });
276
+ }
220
277
  return value;
221
278
  } catch (err) {
222
279
  type = 'throw';
223
280
  value = err;
281
+ Object.assign(settled, {
282
+ type: 'rejected',
283
+ value: err
284
+ });
224
285
  throw err;
225
286
  } finally{
226
287
  results.push({
@@ -248,6 +309,18 @@ const restoreImpl = new WeakMap();
248
309
  return calls.length > 0 ? calls[calls.length - 1] : undefined;
249
310
  }
250
311
  };
312
+ // Out of the mock's keys, so a test comparing `mock` as a whole sees the shape it always had
313
+ Object.defineProperties(mock, {
314
+ settledResults: {
315
+ get: ()=>settledResults
316
+ },
317
+ invocationCallOrder: {
318
+ get: ()=>invocationCallOrder
319
+ },
320
+ contexts: {
321
+ get: ()=>contexts
322
+ }
323
+ });
251
324
  Object.defineProperty(mockFn, 'mock', {
252
325
  value: mock,
253
326
  enumerable: false
@@ -284,16 +357,22 @@ const restoreImpl = new WeakMap();
284
357
  onceQueue.push(fn);
285
358
  return mockFn;
286
359
  };
360
+ const forgetCalls = ()=>{
361
+ for (const record of [
362
+ calls,
363
+ results,
364
+ instances,
365
+ settledResults,
366
+ invocationCallOrder,
367
+ contexts
368
+ ])record.length = 0;
369
+ };
287
370
  mockFn.mockClear = ()=>{
288
- calls.length = 0;
289
- results.length = 0;
290
- instances.length = 0;
371
+ forgetCalls();
291
372
  return mockFn;
292
373
  };
293
374
  mockFn.mockReset = ()=>{
294
- calls.length = 0;
295
- results.length = 0;
296
- instances.length = 0;
375
+ forgetCalls();
297
376
  onceQueue = [];
298
377
  currentImpl = impl;
299
378
  return mockFn;
@@ -407,29 +486,62 @@ const VISIBLE = new Set([
407
486
  }, code, 400, 'POST', 'https://discord.com/api/v10/interactions', {});
408
487
  }
409
488
 
410
- // Whether useStrictMocks() is on, and whether a mock was made before it could be
411
- let strict = false;
412
- let mockMade = false;
413
- /** Whether mocks compute discord.js's state, as {@link useStrictMocks} turns on. */ const strictMocks = ()=>strict;
489
+ /**
490
+ * The id of the bot every mock client is logged in as, so a test can mention it. Fixed, and below the
491
+ * ids other mocks take, so it is the same on every run and in any test order, and no mock shares it.
492
+ */ const MOCK_BOT_ID = '1300000000000000000';
493
+ /**
494
+ * The last id a mock was given, for both builds of this meocord version; ids count up from a real snowflake, so each
495
+ * mock's is its own.
496
+ */ const ids = controller_decorator.shared('mockIds', ()=>({
497
+ last: 1400000000000000000n
498
+ }));
499
+ /** A snowflake-shaped id nothing else a mock holds in this run has, for a mock or the media it resolves. */ const nextSnowflake = ()=>String(++ids.last);
500
+
501
+ // Whether useStrictMocks() is on, and whether a mock was made before it could be, for both builds of this version
502
+ const settings = controller_decorator.shared('strictMocks', ()=>({
503
+ strict: false,
504
+ mockMade: false
505
+ }));
506
+ /** Whether mocks compute discord.js's state, as {@link useStrictMocks} turns on. */ const strictMocks = ()=>settings.strict;
414
507
  /** Notes that a mock was made, which useStrictMocks() must come before. */ function noteMockMade() {
415
- mockMade = true;
508
+ settings.mockMade = true;
416
509
  }
417
510
  /**
418
511
  * Has every mock from `meocord/testing` compute the values discord.js computes, where it reads a placeholder otherwise.
419
512
  *
420
513
  * Call it once in the test runner's setup file. A message's `editable`, `deletable`, `pinnable`, `crosspostable`,
421
514
  * `bulkDeletable`, `hasThread` and `partial`, a member's `manageable`, `kickable`, `bannable` and `moderatable`, a
422
- * role's `editable`, a channel's and a thread's `viewable`, `manageable`, `deletable`, `joinable` and the rest, and
423
- * `partial` on users, channels and reactions then run discord.js's own getters against the mock, and `message.thread`
424
- * is `null` when the channel caches no thread under the message's id. No placeholder warning is logged.
515
+ * role's `editable`, a channel's and a thread's `viewable`, `manageable`, `deletable`, `joinable` and the rest, a voice
516
+ * channel's `full`, and `partial` on users, channels and reactions then run discord.js's own getters against the mock,
517
+ * and `message.thread` is `null` when the channel caches no thread under the message's id. So do a message's
518
+ * `editedAt`, a member's `presence`, a server's `verified` and `systemChannel` and a channel's `parent`, each `null` or
519
+ * `false` until the test gives what it is computed from. A reaction's `me` is `false`, a message's
520
+ * `mentions.repliedUser` and a modal's `message` are `null`, an interaction's `ephemeral` is `null` until `reply()` or
521
+ * `deferReply()` decides it, and a reaction has a whole message of its own, so MeoCord's dispatcher fetches neither. No
522
+ * placeholder warning is logged.
425
523
  *
426
524
  * @remarks
427
- * Those getters read what a server holds, so strict mocks also have it as Discord sends it: the bot's member is in
428
- * the server's member cache from the start, @everyone has the permissions Discord gives it in a new server, and a
429
- * channel or thread made without a server has one of its own, a thread with a text channel as its parent. A message
430
- * has empty `content`, a thread is neither archived nor locked, and a reaction's `count` is 1. Give the bot's member
431
- * a role to let it do more, such as kick a member its role ranks above. A value the test sets on a mock wins over the
432
- * computed one. The next major version (5.0) computes these values without the call.
525
+ * Those getters read what a server holds, so strict mocks also have it as Discord sends it: the bot's member is in the
526
+ * server's member cache from the start, @everyone has the permissions Discord gives it in a new server, and a channel
527
+ * or thread made without a server has one of its own, a thread with a text channel as its parent. A message has empty
528
+ * `content`, a thread is neither archived nor locked, and a reaction's `count` is 1. A user, server or channel made
529
+ * with a generated id was created when the mock was made, as a message is. An interaction's `memberPermissions` apply
530
+ * its channel's overwrites, a thread's parent's for a thread, to its member's permissions. An interaction is in its
531
+ * message's channel and server, a guild's or a member's server, or, given a `guildId` alone, a server the bot isn't in.
532
+ * A message the bot sends is its `client.user`'s, and one it sends in a DM is in that DM. Give the bot's member a role
533
+ * to let it do more, such as kick a member its role ranks above. A value the test sets on a mock wins over the computed
534
+ * one. An interaction's answer that discord.js would refuse, such as a second `reply()` after one a test set with
535
+ * `mockResolvedValue`, is refused too, where without it the mock runs it and warns. An answer marks the interaction
536
+ * `replied`, `deferred` or `responded` once it resolves, as discord.js does, so code that doesn't await one fails as it
537
+ * would on a bot, within one run of the test's code. The next major version (5.0) computes these values, and refuses
538
+ * those answers, without the call.
539
+ *
540
+ * `invoke` also refuses a call that gives a handler fewer arguments than it declares where it builds none, such as a
541
+ * reaction handler given its reaction without its `ReactionEvent`; without strict mocks, it warns once and calls it.
542
+ *
543
+ * `compile()` also refuses an `overrideGuard`, `overrideInterceptor` or `overrideFilter` stub without the stage's
544
+ * method, `canActivate`, `intercept` or `catch`; without strict mocks, it warns and builds the module.
433
545
  *
434
546
  * @throws Error when a mock already exists, which was made without it.
435
547
  *
@@ -444,16 +556,69 @@ let mockMade = false;
444
556
  * @see {@link useMockFn}
445
557
  * @see {@link https://meocord.dev/docs/4.2/mocks | Mocks}
446
558
  */ function useStrictMocks() {
447
- if (strict) return;
448
- if (mockMade) {
559
+ if (settings.strict) return;
560
+ if (settings.mockMade) {
449
561
  throw new Error('useStrictMocks() goes before any mock is made: a mock made before it reads placeholders, and the two kinds would ' + "mix. Call it once, in the test runner's setup file.");
450
562
  }
451
- strict = true;
563
+ settings.strict = true;
452
564
  }
453
565
 
566
+ const mockLogger$1 = new controller_decorator.Logger('Mocks');
567
+ /**
568
+ * A member's permissions where an interaction was made, as Discord computes them: under strict mocks, every permission
569
+ * for an Administrator or the owner, else the channel's overwrites on top of `member.permissions`, so a test that sets
570
+ * those keeps the base; otherwise `member.permissions`, with a warning once where strict mocks would read otherwise.
571
+ */ function inChannel(interaction, member) {
572
+ const base = member.permissions;
573
+ // Only a server the mock caches has the overwrites to apply
574
+ const { guild } = interaction;
575
+ if (!(guild instanceof discord_js.Guild)) return base;
576
+ const computed = base.has(discord_js.PermissionFlagsBits.Administrator) || member.id === guild.ownerId ? new discord_js.PermissionsBitField(discord_js.PermissionsBitField.All).freeze() : withOverwrites(base, overwritingChannel(interaction, guild), member);
577
+ if (strictMocks()) return computed;
578
+ if (!computed.equals(base)) {
579
+ const lost = new discord_js.PermissionsBitField(base).remove(computed).toArray();
580
+ const gained = new discord_js.PermissionsBitField(computed).remove(base).toArray();
581
+ const change = lost.length > 0 ? [
582
+ `without ${lost.join(', ')}`
583
+ ] : [];
584
+ if (gained.length > 0) change.push(`with ${gained.join(', ')}`);
585
+ controller_decorator.warnOnce(mockLogger$1, `${interaction.constructor.name}.memberPermissions reads the member's own permissions here, without the ` + "channel's overwrites or every permission an Administrator or the owner has; in the next major version (5.0) " + `it reads them as Discord computes them in the channel, ${change.join(' and ')}. Set memberPermissions on the ` + 'mock, or call useStrictMocks() to compute them now.');
586
+ }
587
+ // Unless the test set them, a member's permissions come from its roles, @everyone's included
588
+ if (!Object.prototype.hasOwnProperty.call(member, 'permissions')) warnOnUnsetEveryone(interaction, guild, 'memberPermissions', base);
589
+ return base;
590
+ }
591
+ /**
592
+ * Warns once, as `key`, where `permissions` read a server's @everyone role default mode made with none, and lack what
593
+ * strict mocks give @everyone in a new server.
594
+ */ function warnOnUnsetEveryone(interaction, guild, key, permissions) {
595
+ const everyone = guild.roles.everyone;
596
+ if (!everyone || !isUnsetEveryone(everyone) || permissions.has(NEW_SERVER_EVERYONE_PERMISSIONS)) return;
597
+ controller_decorator.warnOnce(mockLogger$1, `${interaction.constructor.name}.${key} reads the server's @everyone role here with no permissions; in the next major ` + 'version (5.0) it has the permissions Discord gives @everyone in a new server, as strict mocks read it. Set ' + 'guild.roles.everyone.permissions, or call useStrictMocks() to read it so now.');
598
+ }
599
+ /**
600
+ * The channel whose overwrites apply where an interaction was made, read without making or caching one: a thread's
601
+ * parent, or undefined where the channel, or a thread's parent, is one the mock would make, which has none.
602
+ */ function overwritingChannel(interaction, guild) {
603
+ const channel = heldChannelOf(interaction, guild);
604
+ // A thread's parent by the id the test gave it or one already read, so strict mocks' parent isn't made for this read
605
+ const parentId = (thread)=>Object.prototype.hasOwnProperty.call(thread, 'parentId') ? thread.parentId : madeParentIds.get(thread);
606
+ const at = channel instanceof discord_js.ThreadChannel ? guild.channels.cache.get(parentId(channel) ?? '') : channel;
607
+ return at instanceof discord_js.GuildChannel ? at : undefined;
608
+ }
609
+ // The parent id strict mocks made for each thread made without one, once read
610
+ const madeParentIds = new WeakMap();
611
+ // Private in discord.js's typings, though they are what its own permissionsFor computes with; called on the prototype
612
+ // so a test's stub on the channel stays for its caller
613
+ const channelPermissions = discord_js.GuildChannel.prototype;
614
+ /** `base` with `channel`'s overwrites for `member` applied, in discord.js's order: @everyone's, the roles', the member's. */ function withOverwrites(base, channel, member) {
615
+ if (!channel) return base;
616
+ // Given the member and its roles as verified, as discord.js's memberPermissions does, so it resolves nothing again
617
+ const overwrites = channelPermissions.overwritesFor.call(channel, member, true, member.roles.cache);
618
+ return new discord_js.PermissionsBitField(base).remove(overwrites.everyone?.deny ?? 0n).add(overwrites.everyone?.allow ?? 0n).remove(overwrites.roles.length > 0 ? overwrites.roles.map((role)=>role.deny) : 0n).add(overwrites.roles.length > 0 ? overwrites.roles.map((role)=>role.allow) : 0n).remove(overwrites.member?.deny ?? 0n).add(overwrites.member?.allow ?? 0n).freeze();
619
+ }
454
620
  /** A permission set with none, frozen as discord.js freezes the ones it gives. */ const noPermissions = ()=>new discord_js.PermissionsBitField().freeze();
455
621
  /** A property discord.js computes from the others: its own getter runs, reading the mock's values. */ const REAL_GETTER = Symbol('real getter');
456
- const snowflake = ()=>discord_js.SnowflakeUtil.generate().toString();
457
622
  /** A class and its defaults, typed together so a default that stops fitting its property fails to compile. */ const defaultsOf = (Class, defaults)=>[
458
623
  Class.prototype,
459
624
  defaults
@@ -466,7 +631,7 @@ const snowflake = ()=>discord_js.SnowflakeUtil.generate().toString();
466
631
  defaultsOf(discord_js.Guild, {
467
632
  name: ()=>'Guild',
468
633
  preferredLocale: ()=>discord_js.Locale.EnglishUS,
469
- ownerId: snowflake,
634
+ ownerId: nextSnowflake,
470
635
  memberCount: ()=>1,
471
636
  available: ()=>true,
472
637
  large: ()=>false,
@@ -479,6 +644,9 @@ const snowflake = ()=>discord_js.SnowflakeUtil.generate().toString();
479
644
  shardId: ()=>0,
480
645
  nsfwLevel: ()=>discord_js.GuildNSFWLevel.Default,
481
646
  verificationLevel: ()=>discord_js.GuildVerificationLevel.None,
647
+ presences: (guild)=>ownedManager(discord_js.PresenceManager.prototype, {
648
+ guild
649
+ }),
482
650
  premiumTier: ()=>discord_js.GuildPremiumTier.None,
483
651
  createdTimestamp: REAL_GETTER,
484
652
  createdAt: REAL_GETTER
@@ -493,6 +661,7 @@ const snowflake = ()=>discord_js.SnowflakeUtil.generate().toString();
493
661
  discriminator: ()=>'0',
494
662
  avatar: ()=>null,
495
663
  system: ()=>false,
664
+ flags: ()=>new discord_js.UserFlagsBitField(),
496
665
  tag: REAL_GETTER,
497
666
  displayName: REAL_GETTER,
498
667
  defaultAvatarURL: REAL_GETTER,
@@ -513,12 +682,32 @@ const snowflake = ()=>discord_js.SnowflakeUtil.generate().toString();
513
682
  premiumSinceTimestamp: ()=>null,
514
683
  communicationDisabledUntilTimestamp: ()=>null,
515
684
  pending: ()=>false,
685
+ flags: ()=>new discord_js.GuildMemberFlagsBitField().freeze(),
516
686
  displayName: REAL_GETTER,
517
687
  roles: (member)=>memberRoles(member, []),
518
688
  permissions: REAL_GETTER
519
689
  }),
690
+ // A server's emoji, sticker, scheduled event and AutoMod rule are named, and what discord.js formats from them
691
+ // computes: an emoji's mention and identifier, an event's and an invite's link
692
+ defaultsOf(discord_js.GuildEmoji, {
693
+ name: ()=>'emoji',
694
+ animated: ()=>false,
695
+ identifier: REAL_GETTER
696
+ }),
697
+ defaultsOf(discord_js.Sticker, {
698
+ name: ()=>'sticker'
699
+ }),
700
+ defaultsOf(discord_js.GuildScheduledEvent, {
701
+ name: ()=>'event',
702
+ url: REAL_GETTER
703
+ }),
704
+ defaultsOf(discord_js.AutoModerationRule, {
705
+ name: ()=>'rule'
706
+ }),
707
+ defaultsOf(discord_js.Invite, {
708
+ url: REAL_GETTER
709
+ }),
520
710
  defaultsOf(discord_js.Role, {
521
- id: snowflake,
522
711
  name: ()=>'role',
523
712
  // Above @everyone, which a server's role manager makes at position 0
524
713
  position: ()=>1,
@@ -556,37 +745,71 @@ const snowflake = ()=>discord_js.SnowflakeUtil.generate().toString();
556
745
  poll: ()=>null,
557
746
  interactionMetadata: ()=>null,
558
747
  stickers: ()=>new discord_js.Collection(),
748
+ messageSnapshots: ()=>new discord_js.Collection(),
749
+ reactions: (message)=>ownedManager(discord_js.ReactionManager.prototype, {
750
+ message
751
+ }),
559
752
  url: REAL_GETTER
560
753
  }),
561
754
  // A command's name and a component's customId say which handler a call is for, so they stay the test's to give
755
+ defaultsOf(discord_js.ChatInputCommandInteraction, {
756
+ options: ()=>createChatInputOptions()
757
+ }),
562
758
  defaultsOf(discord_js.CommandInteraction, {
563
- commandId: snowflake,
759
+ commandId: nextSnowflake,
564
760
  commandGuildId: ()=>null
565
761
  }),
566
762
  defaultsOf(discord_js.AutocompleteInteraction, {
567
- commandId: snowflake,
568
- commandGuildId: ()=>null
763
+ commandId: nextSnowflake,
764
+ commandGuildId: ()=>null,
765
+ options: ()=>createChatInputOptions()
766
+ }),
767
+ defaultsOf(discord_js.MessageReaction, {
768
+ users: (reaction)=>ownedManager(discord_js.ReactionUserManager.prototype, {
769
+ reaction
770
+ })
569
771
  }),
570
772
  defaultsOf(discord_js.BaseInteraction, {
571
- applicationId: snowflake,
773
+ applicationId: nextSnowflake,
572
774
  token: ()=>'mock-interaction-token',
573
775
  version: ()=>1,
574
776
  context: ()=>null,
777
+ entitlements: ()=>new discord_js.Collection(),
575
778
  // What Discord sends with an interaction in a server, the member's permissions there; typed for a cached one
576
- memberPermissions: (interaction)=>{
577
- const { guildId } = interaction;
578
- const member = interaction.member;
579
- if (!guildId) return null;
580
- if (member instanceof discord_js.GuildMember) return member.permissions;
581
- // A raw member's permissions are the string Discord sends, read as discord.js reads them
582
- return isRawMember(member) && !interaction.guild ? new discord_js.PermissionsBitField(BigInt(member.permissions)).freeze() : null;
583
- },
584
- // The bot's permissions where the interaction was made: its member's in the channel, or none outside a server
585
- appPermissions: (interaction)=>{
586
- const { guild, channel } = interaction;
587
- const me = guild instanceof discord_js.Guild ? guild.members.me : null;
588
- return (me && channel instanceof discord_js.GuildChannel ? channel.permissionsFor(me) : null) ?? noPermissions();
589
- }
779
+ // Each reads where the interaction is as its own, and warns by its own name where strict mocks place it otherwise
780
+ memberPermissions: (interaction)=>readPlaceAs(interaction, 'memberPermissions', ()=>{
781
+ const { guildId } = interaction;
782
+ const member = interaction.member;
783
+ if (!guildId) return null;
784
+ if (member instanceof discord_js.GuildMember) return inChannel(interaction, member);
785
+ // A raw member's permissions are the string Discord sends, read as discord.js reads them
786
+ return isRawMember(member) && !interaction.guild ? new discord_js.PermissionsBitField(BigInt(member.permissions)).freeze() : null;
787
+ }),
788
+ // The bot's permissions where the interaction was made: its member's in the channel, or in a thread its parent, as
789
+ // discord.js computes them; none outside a server
790
+ appPermissions: (interaction)=>readPlaceAs(interaction, 'appPermissions', ()=>{
791
+ const { guild } = interaction;
792
+ const me = guild instanceof discord_js.Guild ? botMemberOf(guild) : null;
793
+ if (!(me instanceof discord_js.GuildMember) || heldChannelOf(interaction, guild) === null) return noPermissions();
794
+ // discord.js's own computation, given its own overwritesFor over a test's stub on the channel; a channel the
795
+ // mock would make has no overwrites
796
+ const at = overwritingChannel(interaction, guild) ?? {
797
+ guild,
798
+ permissionOverwrites: {
799
+ cache: new discord_js.Collection()
800
+ }
801
+ };
802
+ const channel = Object.create(at, {
803
+ overwritesFor: {
804
+ value: channelPermissions.overwritesFor
805
+ }
806
+ });
807
+ const permissions = channelPermissions.memberPermissions.call(channel, me, true);
808
+ // The bot's member has @everyone's too
809
+ warnOnUnsetEveryone(interaction, guild, 'appPermissions', permissions);
810
+ return permissions;
811
+ }, // A server the bot isn't in has no guild in either mode, so no permissions in both
812
+ (place)=>!place.raw)
590
813
  }),
591
814
  defaultsOf(discord_js.TextChannel, {
592
815
  type: ()=>discord_js.ChannelType.GuildText,
@@ -622,7 +845,7 @@ const snowflake = ()=>discord_js.SnowflakeUtil.generate().toString();
622
845
  }),
623
846
  defaultsOf(discord_js.GuildChannel, {
624
847
  name: ()=>'channel',
625
- guildId: (channel)=>typeof channel.guild?.id === 'string' ? channel.guild.id : snowflake(),
848
+ guildId: (channel)=>typeof channel.guild?.id === 'string' ? channel.guild.id : nextSnowflake(),
626
849
  position: ()=>0,
627
850
  rawPosition: ()=>0,
628
851
  parentId: ()=>null
@@ -646,11 +869,26 @@ const snowflake = ()=>discord_js.SnowflakeUtil.generate().toString();
646
869
  * thread made without one, a thread's parent text channel in it, and data that is otherwise a placeholder.
647
870
  */ const STRICT_DEFAULTS = new Map([
648
871
  defaultsOf(discord_js.Message, {
649
- content: ()=>''
872
+ content: ()=>'',
873
+ editedTimestamp: ()=>null,
874
+ editedAt: REAL_GETTER
875
+ }),
876
+ defaultsOf(discord_js.MessageMentions, {
877
+ repliedUser: ()=>null
650
878
  }),
651
879
  defaultsOf(discord_js.Guild, {
652
880
  rulesChannelId: ()=>null,
653
- publicUpdatesChannelId: ()=>null
881
+ publicUpdatesChannelId: ()=>null,
882
+ systemChannel: REAL_GETTER
883
+ }),
884
+ defaultsOf(discord_js.BaseGuild, {
885
+ verified: REAL_GETTER
886
+ }),
887
+ defaultsOf(discord_js.GuildMember, {
888
+ presence: REAL_GETTER
889
+ }),
890
+ defaultsOf(discord_js.ModalSubmitInteraction, {
891
+ message: ()=>null
654
892
  }),
655
893
  defaultsOf(discord_js.ThreadChannel, {
656
894
  guild: ()=>createMockGuild(),
@@ -660,25 +898,91 @@ const snowflake = ()=>discord_js.SnowflakeUtil.generate().toString();
660
898
  guild: thread.guild
661
899
  });
662
900
  thread.guild.channels.cache.set(parent.id, parent);
901
+ madeParentIds.set(thread, parent.id);
663
902
  return parent.id;
664
903
  },
665
904
  parent: REAL_GETTER,
666
- ownerId: snowflake,
905
+ ownerId: nextSnowflake,
667
906
  archived: ()=>false,
668
907
  locked: ()=>false
669
908
  }),
670
909
  defaultsOf(discord_js.GuildChannel, {
671
- guild: ()=>createMockGuild()
910
+ guild: ()=>createMockGuild(),
911
+ parent: REAL_GETTER
672
912
  }),
673
913
  // Full once its members reach a user limit, which a voice channel has none of unless given one
674
914
  defaultsOf(discord_js.BaseGuildVoiceChannel, {
675
915
  userLimit: ()=>0,
676
916
  full: REAL_GETTER
677
917
  }),
918
+ // A reaction's message, as discord.js always has one, is a whole one
678
919
  defaultsOf(discord_js.MessageReaction, {
679
- count: ()=>1
920
+ count: ()=>1,
921
+ me: ()=>false,
922
+ message: ()=>createMockMessage()
680
923
  })
681
924
  ]);
925
+ /**
926
+ * What a mock reads as a placeholder unless strict mocks read it as discord.js gives it, by the value a test sets for
927
+ * that, which the warning on reading one names.
928
+ */ const PLACEHOLDER_VALUES = new Map([
929
+ [
930
+ discord_js.MessageReaction.prototype,
931
+ {
932
+ me: 'false',
933
+ message: 'createMockMessage()'
934
+ }
935
+ ],
936
+ [
937
+ discord_js.MessageMentions.prototype,
938
+ {
939
+ repliedUser: 'null'
940
+ }
941
+ ],
942
+ [
943
+ discord_js.Message.prototype,
944
+ {
945
+ editedAt: 'null'
946
+ }
947
+ ],
948
+ [
949
+ discord_js.GuildMember.prototype,
950
+ {
951
+ presence: 'null'
952
+ }
953
+ ],
954
+ [
955
+ discord_js.BaseGuild.prototype,
956
+ {
957
+ verified: 'false'
958
+ }
959
+ ],
960
+ [
961
+ discord_js.Guild.prototype,
962
+ {
963
+ systemChannel: 'null'
964
+ }
965
+ ],
966
+ [
967
+ discord_js.GuildChannel.prototype,
968
+ {
969
+ parent: 'null'
970
+ }
971
+ ],
972
+ [
973
+ discord_js.ModalSubmitInteraction.prototype,
974
+ {
975
+ message: 'null'
976
+ }
977
+ ]
978
+ ]);
979
+ /** The value a test sets for `key` on a mock of `target`'s class, when a mock reads it as a placeholder. */ function placeholderValue(target, key) {
980
+ for(let proto = Object.getPrototypeOf(target); proto !== null; proto = Object.getPrototypeOf(proto)){
981
+ const value = PLACEHOLDER_VALUES.get(proto)?.[key];
982
+ if (value !== undefined) return value;
983
+ }
984
+ return undefined;
985
+ }
682
986
  /** The default a mock of `target`'s class has for `key`, read from the nearest class that has one, strict ones first. */ function discordDefault(target, key) {
683
987
  for (const defaults of strictMocks() ? [
684
988
  STRICT_DEFAULTS,
@@ -694,13 +998,6 @@ const snowflake = ()=>discord_js.SnowflakeUtil.generate().toString();
694
998
  return undefined;
695
999
  }
696
1000
 
697
- /**
698
- * The id of the bot every mock client is logged in as, so a test can mention it. Fixed, and below the
699
- * ids other mocks take, so it is the same on every run and in any test order, and no mock shares it.
700
- */ const MOCK_BOT_ID = '1300000000000000000';
701
- /** The last id a mock was given; ids count up from a real snowflake, so each mock's is its own. */ let lastSnowflake = 1400000000000000000n;
702
- /** A snowflake-shaped id nothing else a mock holds in this run has, for a mock or the media it resolves. */ const nextSnowflake = ()=>String(++lastSnowflake);
703
-
704
1001
  /** Where a mock resolves unfurled media to, as Discord's media proxy would. */ const PROXY = 'https://images-ext-1.discordapp.net/external/mock';
705
1002
  /** Unfurled media as Discord stores it: the url given, resolved to a proxied image of a fixed size. */ function resolvedMedia(media) {
706
1003
  return {
@@ -830,9 +1127,15 @@ const SKIP = new Set([
830
1127
  'toJSON',
831
1128
  'then'
832
1129
  ]);
1130
+ // Each mock's own instance, which the proxy's traps receive as their target
1131
+ const mockTargets = new WeakMap();
1132
+ // Warnings a read answered by the stub logs first, by the mock's instance and the property
1133
+ const stubReadWarnings = new WeakMap();
833
1134
  function stubDeep(instance, externalStubs, onSet) {
834
1135
  noteMockMade();
835
1136
  const stubs = externalStubs ?? new Map();
1137
+ // Placeholders read so far, by the value strict mocks give: a read of a kept one still warns, once
1138
+ const placeholders = new Map();
836
1139
  const proxy = new Proxy(instance, {
837
1140
  get (target, prop) {
838
1141
  // Always pass through symbols
@@ -846,12 +1149,17 @@ function stubDeep(instance, externalStubs, onSet) {
846
1149
  if (Object.prototype.hasOwnProperty.call(target, key)) {
847
1150
  return Reflect.get(target, prop, target);
848
1151
  }
1152
+ stubReadWarnings.get(target)?.get(key)?.();
849
1153
  // Skip passthrough props — return prototype value as-is
850
1154
  if (SKIP.has(key)) {
851
1155
  return Reflect.get(target, prop, target);
852
1156
  }
853
1157
  // Return cached stub
854
- if (stubs.has(key)) return stubs.get(key);
1158
+ if (stubs.has(key)) {
1159
+ const kept = placeholders.get(key);
1160
+ if (kept !== undefined) warnPlaceholderRead(target, key, kept);
1161
+ return stubs.get(key);
1162
+ }
855
1163
  // A value discord.js always gives, in its shape, rather than a stub: kept once read, or its getter run live
856
1164
  const known = discordDefault(target, key);
857
1165
  if (known === REAL_GETTER) {
@@ -865,6 +1173,12 @@ function stubDeep(instance, externalStubs, onSet) {
865
1173
  stubs.set(key, value);
866
1174
  return value;
867
1175
  }
1176
+ // A value strict mocks read as discord.js gives it, which is a placeholder otherwise
1177
+ const strictValue = strictMocks() ? undefined : placeholderValue(target, key);
1178
+ if (strictValue !== undefined) {
1179
+ placeholders.set(key, strictValue);
1180
+ warnPlaceholderRead(target, key, strictValue);
1181
+ }
868
1182
  // Walk the prototype chain to check if it's a function
869
1183
  let proto = Object.getPrototypeOf(target);
870
1184
  let protoValue;
@@ -875,7 +1189,7 @@ function stubDeep(instance, externalStubs, onSet) {
875
1189
  if (desc.get && isPlaceholder(proto, key)) {
876
1190
  // Strict mocks run discord.js's getter, read live; otherwise the placeholder, with a warning once
877
1191
  if (strictMocks()) return Reflect.get(target, prop, proxy);
878
- warnBooleanPlaceholder(target, key);
1192
+ warnPlaceholderRead(target, key, 'false');
879
1193
  }
880
1194
  break;
881
1195
  }
@@ -883,7 +1197,7 @@ function stubDeep(instance, externalStubs, onSet) {
883
1197
  }
884
1198
  // A client the mock's structures share: its guild's, or one of its own
885
1199
  if (key === 'client' && target instanceof discord_js.Base) return clientOf(target);
886
- const stub = typeof protoValue === 'function' ? isRealMethod(target, key) ? createMockFn((...args)=>protoValue.apply(proxy, args)) : methodStub(target, key, protoValue, ()=>proxy) : stubDeep({});
1200
+ const stub = typeof protoValue === 'function' ? isRealMethod(target, key) ? createMockFn((...args)=>quietly(target, key, ()=>protoValue.apply(proxy, args))) : methodStub(target, key, protoValue, ()=>proxy) : stubDeep({});
887
1201
  stubs.set(key, stub);
888
1202
  return stub;
889
1203
  },
@@ -892,7 +1206,10 @@ function stubDeep(instance, externalStubs, onSet) {
892
1206
  // and a plain write against one of those throws. Defining an own
893
1207
  // data property shadows the accessor, which is what test setup means.
894
1208
  set (target, prop, value) {
895
- Object.defineProperty(target, prop, {
1209
+ // An own accessor with a setter, such as a context menu's target, takes the value itself
1210
+ const setter = Object.getOwnPropertyDescriptor(target, prop)?.set;
1211
+ if (setter) setter.call(target, value);
1212
+ else Object.defineProperty(target, prop, {
896
1213
  value,
897
1214
  writable: true,
898
1215
  enumerable: true,
@@ -902,6 +1219,7 @@ function stubDeep(instance, externalStubs, onSet) {
902
1219
  return true;
903
1220
  }
904
1221
  });
1222
+ mockTargets.set(proxy, instance);
905
1223
  return proxy;
906
1224
  }
907
1225
  // discord.js methods that read only what the mock holds, so they run for real: looking items up in a manager's cache,
@@ -1034,7 +1352,8 @@ const PLACEHOLDER_BOOLEANS = new Map([
1034
1352
  [
1035
1353
  discord_js.BaseGuildVoiceChannel.prototype,
1036
1354
  new Set([
1037
- 'joinable'
1355
+ 'joinable',
1356
+ 'full'
1038
1357
  ])
1039
1358
  ],
1040
1359
  [
@@ -1069,11 +1388,42 @@ const PLACEHOLDER_BOOLEANS = new Map([
1069
1388
  return false;
1070
1389
  }
1071
1390
  const mockLogger = new controller_decorator.Logger('Mocks');
1072
- function warnBooleanPlaceholder(target, key) {
1391
+ // Mock reactions, and their messages, that MeoCord's dispatcher reads `partial` on
1392
+ const dispatchedReactions = new WeakSet();
1393
+ let quiet = 0;
1394
+ /**
1395
+ * Runs a real method, keeping quiet about the placeholders `mentions.has()` reads: a placeholder `repliedUser` matches
1396
+ * no user, so its answer is discord.js's either way.
1397
+ */ function quietly(target, key, run) {
1398
+ if (!(target instanceof discord_js.MessageMentions && key === 'has')) return run();
1399
+ quiet++;
1400
+ try {
1401
+ return run();
1402
+ } finally{
1403
+ quiet--;
1404
+ }
1405
+ }
1406
+ /**
1407
+ * Marks a mock reaction and its message as read by MeoCord's dispatcher, so a placeholder `partial` it reads warns
1408
+ * that MeoCord read it, and fetched for it.
1409
+ */ function noteReactionDispatch(reaction) {
1410
+ const target = mockTargets.get(reaction);
1411
+ if (!target) return;
1412
+ dispatchedReactions.add(target);
1413
+ const message = mockTargets.get(reaction.message ?? {});
1414
+ if (message) dispatchedReactions.add(message);
1415
+ }
1416
+ function warnPlaceholderRead(target, key, strictValue) {
1417
+ if (quiet > 0) return;
1073
1418
  const name = Object.getPrototypeOf(target).constructor.name;
1419
+ if (key === 'partial' && dispatchedReactions.has(target)) {
1420
+ const fetched = target instanceof discord_js.MessageReaction ? 'the reaction' : "the reaction's message";
1421
+ controller_decorator.warnPlaceholder(mockLogger, `${name}.${key}`, 'the mock computes it as discord.js does', `MeoCord's dispatcher read it while dispatching the reaction, and fetched ${fetched} since the placeholder is truthy. ` + 'Set reaction.partial = false and reaction.message.partial = false on the mock, or call useStrictMocks() to have the mock compute them now.');
1422
+ return;
1423
+ }
1074
1424
  // The class name in camel case, a leading acronym included: dmChannel, guildMember
1075
1425
  const variable = name.replace(/^[A-Z]+(?=[A-Z][a-z])|^[A-Z]/, (start)=>start.toLowerCase());
1076
- controller_decorator.warnPlaceholder(mockLogger, `${name}.${key}`, 'the mock computes it as discord.js does', `Set it on the mock, such as ${variable}.${key} = false, to test either way, or call useStrictMocks() to have the mock compute it now.`);
1426
+ controller_decorator.warnPlaceholder(mockLogger, `${name}.${key}`, 'the mock computes it as discord.js does', `Set it on the mock, such as ${variable}.${key} = ${strictValue}, to test either way, or call useStrictMocks() to have the mock compute it now.`);
1077
1427
  }
1078
1428
  // The client of a structure made without one: the one its guild has, else one of its own, made once
1079
1429
  const ownClients = new WeakMap();
@@ -1181,25 +1531,37 @@ const MANAGER_ITEMS = [
1181
1531
  ],
1182
1532
  [
1183
1533
  discord_js.RoleManager,
1184
- (id)=>createMockInteraction(discord_js.Role, id ? {
1185
- id
1186
- } : {})
1534
+ (id, manager)=>createMockInteraction(discord_js.Role, {
1535
+ ...id ? {
1536
+ id
1537
+ } : {},
1538
+ ...guildOf(manager) ? {
1539
+ guild: guildOf(manager)
1540
+ } : {}
1541
+ })
1187
1542
  ],
1188
1543
  [
1189
1544
  discord_js.GuildBanManager,
1190
1545
  ()=>createMockInteraction(discord_js.GuildBan)
1191
1546
  ],
1547
+ // A message fetched from a channel is in it, and another user's
1192
1548
  [
1193
1549
  discord_js.GuildMessageManager,
1194
- (id)=>createMockMessage(id ? {
1195
- id
1196
- } : {})
1550
+ (id, manager)=>createMockMessage({
1551
+ ...id ? {
1552
+ id
1553
+ } : {},
1554
+ ...channelOfManager(manager)
1555
+ })
1197
1556
  ],
1198
1557
  [
1199
1558
  discord_js.DMMessageManager,
1200
- (id)=>createMockMessage(id ? {
1201
- id
1202
- } : {})
1559
+ (id, manager)=>createMockMessage({
1560
+ ...id ? {
1561
+ id
1562
+ } : {},
1563
+ ...channelOfManager(manager)
1564
+ })
1203
1565
  ],
1204
1566
  [
1205
1567
  discord_js.GuildTextThreadManager,
@@ -1217,6 +1579,12 @@ const MANAGER_ITEMS = [
1217
1579
  discord_js.ThreadMemberManager,
1218
1580
  ()=>createMockInteraction(discord_js.ThreadMember)
1219
1581
  ],
1582
+ [
1583
+ discord_js.ReactionUserManager,
1584
+ (id)=>createMockUser(id ? {
1585
+ id
1586
+ } : {})
1587
+ ],
1220
1588
  [
1221
1589
  discord_js.ApplicationCommandManager,
1222
1590
  (id)=>createMockInteraction(discord_js.ApplicationCommand, id ? {
@@ -1239,11 +1607,58 @@ const MANAGER_ITEMS = [
1239
1607
  guild: guildOf(manager)
1240
1608
  } : {}
1241
1609
  })
1610
+ ],
1611
+ [
1612
+ discord_js.GuildEmojiManager,
1613
+ (id, manager)=>itemInGuild(discord_js.GuildEmoji, {
1614
+ id: id ?? nextSnowflake()
1615
+ }, manager)
1616
+ ],
1617
+ [
1618
+ discord_js.GuildStickerManager,
1619
+ (id, manager)=>itemInGuild(discord_js.Sticker, {
1620
+ id: id ?? nextSnowflake()
1621
+ }, manager)
1622
+ ],
1623
+ [
1624
+ discord_js.GuildScheduledEventManager,
1625
+ (id, manager)=>itemInGuild(discord_js.GuildScheduledEvent, {
1626
+ id: id ?? nextSnowflake()
1627
+ }, manager)
1628
+ ],
1629
+ [
1630
+ discord_js.AutoModerationRuleManager,
1631
+ (id, manager)=>itemInGuild(discord_js.AutoModerationRule, {
1632
+ id: id ?? nextSnowflake()
1633
+ }, manager)
1634
+ ],
1635
+ // An invite is keyed by its code, as discord.js caches and fetches one
1636
+ [
1637
+ discord_js.GuildInviteManager,
1638
+ (code, manager)=>itemInGuild(discord_js.Invite, {
1639
+ code: code ?? BigInt(nextSnowflake()).toString(36)
1640
+ }, manager)
1242
1641
  ]
1243
1642
  ];
1643
+ /** A mock of `Class` with `props`, in the manager's guild, by the guild and its id, when the manager has one. */ function itemInGuild(Class, props, manager) {
1644
+ const guild = guildOf(manager);
1645
+ return createMockInteraction(Class, {
1646
+ ...props,
1647
+ ...guild ? {
1648
+ guild,
1649
+ guildId: guild.id
1650
+ } : {}
1651
+ });
1652
+ }
1244
1653
  const returnsPromise = (key, method)=>method.constructor.name === 'AsyncFunction' || PROMISE_METHODS.has(key) || /^set[A-Z]/.test(key) && !SYNC_SETTERS.has(key);
1245
1654
  // The ids a fetch asks for: an id, a discord.js object, or options naming one or several, such as `{ user: id }` or
1246
1655
  // `{ user: [ids] }`; undefined for a fetch of a list
1656
+ /** The channel a message manager belongs to, as `createMockMessage` takes it. */ const channelOfManager = (manager)=>{
1657
+ const channel = manager.channel;
1658
+ return channel instanceof discord_js.BaseChannel ? {
1659
+ channel: channel
1660
+ } : {};
1661
+ };
1247
1662
  function fetchedIds(args) {
1248
1663
  const idOf = (value)=>typeof value === 'string' ? value : value instanceof discord_js.Base ? value.id : undefined;
1249
1664
  const [first] = args;
@@ -1279,6 +1694,10 @@ function fetchedIds(args) {
1279
1694
  if (key === 'send' && target instanceof discord_js.User) {
1280
1695
  return createMockFn(async (...args)=>(await receiver().createDM()).send(...args));
1281
1696
  }
1697
+ // A channel's send resolves to the message it sent there
1698
+ if (key === 'send' && target instanceof discord_js.BaseChannel) {
1699
+ return createMockFn(async (options)=>sentMessage(sent(options, false), receiver()));
1700
+ }
1282
1701
  if (MESSAGE_METHODS.has(key)) return createMockFn(async ()=>createMockMessage());
1283
1702
  if (target instanceof discord_js.BaseManager) {
1284
1703
  const item = MANAGER_ITEMS.find(([Manager])=>Manager.prototype.isPrototypeOf(target))?.[1];
@@ -1295,7 +1714,36 @@ function fetchedIds(args) {
1295
1714
  return ids === undefined ? new discord_js.Collection() : one(ids);
1296
1715
  });
1297
1716
  }
1298
- if (item && (key === 'create' || key === 'edit')) return createMockFn(async ()=>item(undefined, receiver()));
1717
+ if (item && key === 'edit') return createMockFn(async ()=>item(undefined, receiver()));
1718
+ // A ban resolves to the member, user or id it was given, as discord.js's does, and caches nothing
1719
+ if (key === 'create' && target instanceof discord_js.GuildBanManager) {
1720
+ return createMockFn(async (user)=>{
1721
+ if (user instanceof discord_js.GuildMember) return user;
1722
+ if (user instanceof discord_js.User) return receiver().guild.members.resolve(user) ?? user;
1723
+ return typeof user === 'string' ? user : user?.id;
1724
+ });
1725
+ }
1726
+ // An invite is made for the channel given, in its guild, and cached nowhere, as discord.js's create does
1727
+ if (item && key === 'create' && target instanceof discord_js.GuildInviteManager) {
1728
+ return createMockFn(async (channel)=>{
1729
+ const invite = item(undefined, receiver());
1730
+ const channelId = typeof channel === 'string' ? channel : channel?.id;
1731
+ const found = channel instanceof discord_js.BaseChannel ? channel : channelId ? cacheOf(guildOf(receiver())?.channels)?.get(channelId) ?? null : null;
1732
+ return Object.assign(invite, {
1733
+ channel: found,
1734
+ channelId: channelId ?? null
1735
+ });
1736
+ });
1737
+ }
1738
+ // Every other manager here caches what its create() makes, as discord.js's does; check a manager's create when
1739
+ // adding it to MANAGER_ITEMS
1740
+ if (item && key === 'create') {
1741
+ return createMockFn(async ()=>{
1742
+ const made = item(undefined, receiver());
1743
+ if (typeof made.id === 'string') cacheOf(receiver())?.set(made.id, made);
1744
+ return made;
1745
+ });
1746
+ }
1299
1747
  } else if (target instanceof discord_js.Base && (SELF_METHODS.has(key) || /^set[A-Z]/.test(key))) {
1300
1748
  return createMockFn(async ()=>receiver());
1301
1749
  }
@@ -1455,14 +1903,46 @@ const isSnowflake = (id)=>typeof id === 'string' && /^\d{1,20}$/.test(id);
1455
1903
  });
1456
1904
  }
1457
1905
  }
1458
- /** Where a mock interaction keeps every answer it got, through respond() or discord.js directly, in order. */ const RESPONSE_LOG = Symbol('response log');
1906
+ /**
1907
+ * Gives a user, server or channel mock its creation time. Under strict mocks, a generated id gives the time the mock
1908
+ * was made, as a message's. Otherwise it gives that id's own time, a fixed day in 2025, with a warning once when read.
1909
+ * An id the test gives decides it in both modes, and a value the test sets replaces it.
1910
+ */ function defineMadeTime(mock, generatedId) {
1911
+ if (strictMocks()) return defineCreatedTime(mock, generatedId);
1912
+ const own = (key)=>Object.prototype.hasOwnProperty.call(mock, key);
1913
+ const name = Object.getPrototypeOf(mock).constructor.name;
1914
+ if (!own('createdTimestamp')) {
1915
+ Object.defineProperty(mock, 'createdTimestamp', {
1916
+ get () {
1917
+ if (this.id === generatedId) {
1918
+ controller_decorator.warnOnce(mockLogger, `${name}.createdTimestamp reads the time of its generated id here, a fixed day in 2025; in the next major version (5.0) ` + 'it is the time the mock was made. Give the mock an id or a createdTimestamp, or call useStrictMocks() to have it read the time the mock was made now.');
1919
+ }
1920
+ return isSnowflake(this.id) ? discord_js.SnowflakeUtil.timestampFrom(this.id) : null;
1921
+ },
1922
+ configurable: true
1923
+ });
1924
+ }
1925
+ if (!own('createdAt')) {
1926
+ Object.defineProperty(mock, 'createdAt', {
1927
+ get () {
1928
+ return this.createdTimestamp === null ? null : new Date(this.createdTimestamp);
1929
+ },
1930
+ configurable: true
1931
+ });
1932
+ }
1933
+ }
1934
+ /**
1935
+ * Where a mock interaction keeps every answer it got, through respond() or discord.js directly, in order, under a key
1936
+ * both builds of this meocord version read.
1937
+ */ const RESPONSE_LOG = Symbol.for(controller_decorator.sharedName('responseLog'));
1459
1938
  /**
1460
1939
  * A mock of an answer method, `impl` its behaviour, that records each call in `log` as it is made, with what it sent
1461
1940
  * and, once it settles, what it rejected with. The record wraps every behaviour the mock is given, its own and any a
1462
- * test sets, so the mock stays the plain mock function a runner's matchers read, bun's included. `withResponse` is how
1463
- * a call asks discord.js for the message back, not part of what it sends, so it is left out.
1464
- */ function recordedMock(method, impl, log) {
1465
- const record = (behaviour)=>function(...args) {
1941
+ * test sets, so the mock stays the plain mock function a runner's matchers read, bun's included, and `gate` keeps the
1942
+ * reply state whichever behaviour runs. `withResponse` is how a call asks discord.js for the message back, not part of
1943
+ * what it sends, so it is left out.
1944
+ */ function recordedMock(method, impl, log, gate) {
1945
+ const record = (behaviour, byTest = true)=>function(...args) {
1466
1946
  const [payload] = args;
1467
1947
  const sent = payload && typeof payload === 'object' && 'withResponse' in payload ? Object.fromEntries(Object.entries(payload).filter(([key])=>key !== 'withResponse')) : payload;
1468
1948
  const call = {
@@ -1471,22 +1951,41 @@ const isSnowflake = (id)=>typeof id === 'string' && /^\d{1,20}$/.test(id);
1471
1951
  };
1472
1952
  controller_decorator.stampCall(call);
1473
1953
  log.push(call);
1954
+ let gated;
1474
1955
  try {
1475
- const result = Reflect.apply(behaviour, this, args);
1476
- // Recorded and passed on, so a rejection nobody awaits is unhandled in the test, as it is on a bot
1477
- if (result instanceof Promise) {
1478
- return result.then(undefined, (error)=>{
1479
- call.error = error;
1480
- throw error;
1481
- });
1482
- }
1483
- return result;
1956
+ // Before the behaviour, as discord.js checks the state before it sends an answer
1957
+ if (gate) gated = gate(args, byTest);
1484
1958
  } catch (error) {
1959
+ call.error = error;
1960
+ return Promise.reject(error);
1961
+ }
1962
+ const failed = (error)=>{
1963
+ gated?.settle?.();
1964
+ gated?.undo?.();
1485
1965
  call.error = error;
1486
1966
  throw error;
1967
+ };
1968
+ // Once the behaviour resolves, as discord.js changes the state once Discord has taken the answer
1969
+ const landed = (value)=>{
1970
+ gated?.settle?.();
1971
+ try {
1972
+ gated?.land?.();
1973
+ } catch (error) {
1974
+ call.error = error;
1975
+ throw error;
1976
+ }
1977
+ return value;
1978
+ };
1979
+ try {
1980
+ const result = Reflect.apply(behaviour, this, args);
1981
+ // Recorded and passed on, so a rejection nobody awaits is unhandled in the test, as it is on a bot
1982
+ if (result instanceof Promise) return result.then(landed, failed);
1983
+ return landed(result);
1984
+ } catch (error) {
1985
+ return failed(error);
1487
1986
  }
1488
1987
  };
1489
- const mock = createMockFn(record(impl));
1988
+ const mock = createMockFn(record(impl, false));
1490
1989
  const always = mock.mockImplementation.bind(mock);
1491
1990
  const once = mock.mockImplementationOnce.bind(mock);
1492
1991
  // Each way a test sets a behaviour goes through the record, a runner's own extras included where it has them
@@ -1519,16 +2018,26 @@ const isSnowflake = (id)=>typeof id === 'string' && /^\d{1,20}$/.test(id);
1519
2018
  return mock;
1520
2019
  }
1521
2020
  /** The interaction a mock options resolver was given to, for the server a user option's member is in. */ const optionOwners = new WeakMap();
2021
+ /** An error as discord.js throws it, with its code, and with `message` in place of discord.js's own text when given. */ function discordjsError(code, message) {
2022
+ const error = new discord_js.DiscordjsError(code);
2023
+ if (message !== undefined) error.message = message;
2024
+ return error;
2025
+ }
1522
2026
  /** An error as discord.js's options resolver throws it: a `TypeError` with discord.js's code and message. */ const optionError = (code, ...args)=>new discord_js.DiscordjsTypeError(code, ...args);
1523
2027
  // In discord.js's error codes, though missing from its typings
1524
2028
  const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
1525
2029
  /** Records the interaction a mock options resolver belongs to. */ const ownOptions = (interaction, options)=>{
1526
2030
  if (typeof options === 'object' && options !== null) optionOwners.set(options, interaction);
1527
2031
  };
1528
- /** A mock user that is a person, with an id of its own unless given one. */ const mockUser = (id = nextSnowflake())=>stubDeep(Object.assign(Object.create(discord_js.User.prototype), {
1529
- id,
2032
+ /** A mock user that is a person, with an id of its own unless given one. */ const mockUser = (id)=>{
2033
+ const generatedId = id === undefined ? nextSnowflake() : undefined;
2034
+ const user = Object.assign(Object.create(discord_js.User.prototype), {
2035
+ id: id ?? generatedId,
1530
2036
  bot: false
1531
- }));
2037
+ });
2038
+ defineMadeTime(user, generatedId);
2039
+ return stubDeep(user);
2040
+ };
1532
2041
  function createMockInteraction(Class, props) {
1533
2042
  const instance = Object.create(Class.prototype);
1534
2043
  const stubs = new Map();
@@ -1548,14 +2057,36 @@ function createMockInteraction(Class, props) {
1548
2057
  // mock lacks: a guildId is a guild, a guild object with it a cached one, neither a DM. A member
1549
2058
  // not given is the user's on an interaction, and the auto-stub elsewhere, so only one set to null or undefined fails.
1550
2059
  const own = (key)=>Object.prototype.hasOwnProperty.call(instance, key) ? instance[key] : undefined;
2060
+ let strictPlace;
1551
2061
  const hasMember = ()=>!Object.prototype.hasOwnProperty.call(instance, 'member') || Boolean(instance.member);
1552
2062
  const guildChecks = {
1553
2063
  inGuild: ()=>Boolean(own('guildId') && hasMember()),
1554
2064
  inCachedGuild: ()=>Boolean(own('guildId') && own('guild') && hasMember()),
1555
2065
  inRawGuild: ()=>Boolean(own('guildId') && !own('guild') && hasMember())
1556
2066
  };
2067
+ // What each answers where strict mocks place the mock: the place's guildId and guild for those the test left out, and
2068
+ // a member wherever there is a guildId, unless the test gave one
2069
+ const given = new Set(Object.keys(props ?? {}));
2070
+ const strictChecks = (place)=>{
2071
+ const pick = (key, placed)=>given.has(key) || placed === undefined ? own(key) : placed;
2072
+ const guildId = pick('guildId', place.guildId);
2073
+ const guild = place.raw ? null : pick('guild', place.guild);
2074
+ const member = given.has('member') ? Boolean(own('member')) : Boolean(guildId);
2075
+ return {
2076
+ inGuild: Boolean(guildId && member),
2077
+ inCachedGuild: Boolean(guildId && guild && member),
2078
+ inRawGuild: Boolean(guildId && !guild && member)
2079
+ };
2080
+ };
2081
+ // Each reads where the mock is as its own, and in default mode warns once, by its own name, where strict mocks answer
2082
+ // otherwise or refuse the mock
1557
2083
  for (const [name, check] of Object.entries(guildChecks)){
1558
- if (findPrototypeMethod(instance, name) !== null) stubs.set(name, createMockFn(check));
2084
+ if (findPrototypeMethod(instance, name) === null) continue;
2085
+ stubs.set(name, createMockFn(()=>{
2086
+ const answer = readPlaceQuietly(check);
2087
+ if (strictPlace && !strictMocks() && (strictPlace.refused || strictChecks(strictPlace)[name] !== answer)) placeWarners.get(instance)?.warn(`${name}()`);
2088
+ return answer;
2089
+ }));
1559
2090
  }
1560
2091
  // Set up reply state machine for repliable interactions
1561
2092
  const isRepliableMethod = findPrototypeMethod(instance, 'isRepliable');
@@ -1563,84 +2094,285 @@ function createMockInteraction(Class, props) {
1563
2094
  if (repliable) {
1564
2095
  instance.replied = false;
1565
2096
  instance.deferred = false;
1566
- instance.ephemeral = false;
1567
- const alreadyReplied = ()=>new Error('The reply to this interaction has already been sent or deferred.');
1568
- const notYetReplied = (method)=>new Error(`Cannot call ${method}() before replying or deferring.`);
2097
+ // Null until reply() or deferReply() decides it, as discord.js has it; default mode reads false until then, and a
2098
+ // test reading it there is told once. A value the test sets is read as set
2099
+ const strict = strictMocks();
2100
+ let ephemeral = strict ? null : false;
2101
+ let ephemeralDecided = false;
2102
+ // Whether one of the mock's own first answers ran; flags a test sets without one leave ephemeral as discord.js would
2103
+ let answeredByMock = false;
2104
+ const answeredOutside = ()=>Boolean(instance.replied || instance.deferred) && !answeredByMock;
2105
+ const decideEphemeral = (value, decided)=>{
2106
+ ephemeral = value;
2107
+ ephemeralDecided = decided;
2108
+ };
2109
+ Object.defineProperty(instance, 'ephemeral', {
2110
+ get: ()=>{
2111
+ // Quiet once a test answered the interaction itself: MeoCord then reads ephemeral only for truthiness, where false
2112
+ // and null act alike
2113
+ if (!ephemeralDecided && !strict && !answeredOutside()) {
2114
+ controller_decorator.warnPlaceholder(mockLogger, `${Object.getPrototypeOf(instance).constructor.name}.ephemeral`, 'it is null until reply() or deferReply() decides it, as discord.js has it', 'Reply or defer first, or call useStrictMocks() to read null now.');
2115
+ }
2116
+ return ephemeral;
2117
+ },
2118
+ set: (value)=>decideEphemeral(value, true),
2119
+ enumerable: true,
2120
+ configurable: true
2121
+ });
2122
+ // discord.js's errors, with their codes; a call before any answer keeps the message the mock has always given
2123
+ const alreadyReplied = ()=>discordjsError(discord_js.DiscordjsErrorCodes.InteractionAlreadyReplied);
2124
+ const notYetReplied = (method)=>discordjsError(discord_js.DiscordjsErrorCodes.InteractionNotReplied, `Cannot call ${method}() before replying or deferring.`);
1569
2125
  // Only `flags` is read: discord.js deprecates the `ephemeral: true` reply option, and honouring it here would let
1570
- // a test pass against a deprecated call.
2126
+ // a test pass against a deprecated call. Read as discord.js reads them, so a flag it refuses throws here too.
1571
2127
  const hasEphemeralFlag = (options)=>{
1572
- if (!options) return false;
1573
- const { flags } = options;
1574
- if (typeof flags === 'number') return (flags & 64) !== 0;
1575
- if (typeof flags === 'bigint') return (flags & 64n) !== 0n;
1576
- return false;
2128
+ const flags = options?.flags;
2129
+ if (typeof options !== 'object' || flags === null || flags === undefined) return false;
2130
+ return new discord_js.MessageFlagsBitField(flags).has(discord_js.MessageFlags.Ephemeral);
2131
+ };
2132
+ const built = (held)=>typeof held === 'function' ? held() : held;
2133
+ let original;
2134
+ let originalGone = false;
2135
+ const followUps = new Map();
2136
+ // Whether the last answer ran a behaviour a test set, after which a second answer runs with a warning by default
2137
+ let answeredByTest = false;
2138
+ // A command answered with a modal has no original response, and Discord refuses one asked for
2139
+ let modalAnswered = false;
2140
+ const isComponent = instance.type === discord_js.InteractionType.MessageComponent;
2141
+ // The original response of a component, or of a modal submitted from one, is the message it's on
2142
+ const componentMessage = ()=>heldFrom(instance.message);
2143
+ const snapshot = ()=>{
2144
+ const saved = {
2145
+ replied: instance.replied,
2146
+ deferred: instance.deferred,
2147
+ ephemeral,
2148
+ ephemeralDecided,
2149
+ answeredByMock,
2150
+ original,
2151
+ originalGone,
2152
+ followUps: new Map(followUps),
2153
+ answeredByTest,
2154
+ modalAnswered
2155
+ };
2156
+ return ()=>{
2157
+ Object.assign(instance, {
2158
+ replied: saved.replied,
2159
+ deferred: saved.deferred
2160
+ });
2161
+ decideEphemeral(saved.ephemeral, saved.ephemeralDecided);
2162
+ ({ original, originalGone, answeredByTest, answeredByMock, modalAnswered } = saved);
2163
+ followUps.clear();
2164
+ for (const [id, held] of saved.followUps)followUps.set(id, held);
2165
+ };
2166
+ };
2167
+ /**
2168
+ * Refuses an answer as discord.js does. Where the refusal turns on a behaviour a test set, this answer's or the
2169
+ * one before it, which ran without one, default mode warns and lets it run; strict mocks refuse it.
2170
+ */ const refuse = (method, error, byTest)=>{
2171
+ if (!(byTest || answeredByTest) || strictMocks()) throw error;
2172
+ controller_decorator.warnOnce(mockLogger, `${method}() here is refused by discord.js with "${error.message}", but the mock runs it, as a test set this ` + 'answer or the one before it. In the next major version (5.0) the mock refuses it; call useStrictMocks() to ' + 'have it refused now.');
2173
+ };
2174
+ const answered = ()=>instance.deferred || instance.replied;
2175
+ /**
2176
+ * What an answer sends, built now, as discord.js builds it when sending. One it refuses to build is refused under
2177
+ * strict mocks; default mode warns and holds it to build when first read.
2178
+ */ const hold = (method, build)=>{
2179
+ try {
2180
+ return build();
2181
+ } catch (error) {
2182
+ if (strictMocks()) throw error;
2183
+ controller_decorator.warnOnce(mockLogger, `${method}() here sends what discord.js refuses to build: "${error.message}". The mock sends it and ` + 'builds it when read; in the next major version (5.0) it rejects, as discord.js does. Call useStrictMocks() to ' + 'have it rejected now.');
2184
+ return build;
2185
+ }
2186
+ };
2187
+ // The changes answers make to the reply state, in order. One whose answer fails is undone once every change after
2188
+ // it is undone too, so a later answer's change stands while it does, as on a bot
2189
+ const changes = [];
2190
+ const undoneInTurn = (undo)=>{
2191
+ const change = {
2192
+ undo,
2193
+ failed: false
2194
+ };
2195
+ changes.push(change);
2196
+ return ()=>{
2197
+ change.failed = true;
2198
+ while(changes.at(-1)?.failed)changes.pop().undo();
2199
+ };
1577
2200
  };
2201
+ // The first answers called and not yet settled, oldest first, for default mode to warn where a bot would differ
2202
+ const unsettled = [];
2203
+ // Default mode warns once per interaction where code that doesn't await an answer would fail on a bot
2204
+ let inFlightWarned = false;
2205
+ const warnInFlight = (text)=>{
2206
+ if (inFlightWarned) return;
2207
+ inFlightWarned = true;
2208
+ mockLogger.warn(text);
2209
+ };
2210
+ /**
2211
+ * A first answer: refused once the interaction has one, else it replies or defers. Under strict mocks the change
2212
+ * waits for the answer to land, as discord.js makes it once the request resolves, so one sent while another is in
2213
+ * flight passes discord.js's check and Discord refuses it with 40060 if the other landed first. Default mode makes
2214
+ * the change at once.
2215
+ */ const first = (method, kind, then)=>(args, byTest)=>{
2216
+ if (answered()) {
2217
+ if (unsettled.length > 0) {
2218
+ warnInFlight(`${method}() was called while ${unsettled[0]}() had not resolved; on a bot discord.js sends it, and Discord ` + `refuses it with 40060 ("Interaction has already been acknowledged") once the first lands. Await ` + `${unsettled[0]}() before answering again, or call useStrictMocks() to have the mock answer as Discord does.`);
2219
+ }
2220
+ refuse(method, alreadyReplied(), byTest);
2221
+ }
2222
+ // Read first: flags discord.js refuses throw before anything changes; reply() and deferReply() decide
2223
+ // ephemeral, as discord.js's do, and update() and deferUpdate() leave it
2224
+ const flagged = method === 'reply' || method === 'deferReply' ? hasEphemeralFlag(args[0]) : undefined;
2225
+ const undo = snapshot();
2226
+ instance[kind] = true;
2227
+ if (flagged !== undefined) decideEphemeral(flagged, true);
2228
+ answeredByTest = byTest;
2229
+ answeredByMock = true;
2230
+ try {
2231
+ then(args);
2232
+ } catch (error) {
2233
+ undo();
2234
+ throw error;
2235
+ }
2236
+ if (strict) {
2237
+ const land = snapshot();
2238
+ undo();
2239
+ return {
2240
+ land: ()=>{
2241
+ if (answered()) throw createDiscordError(40060, 'Interaction has already been acknowledged.');
2242
+ land();
2243
+ }
2244
+ };
2245
+ }
2246
+ unsettled.push(method);
2247
+ return {
2248
+ undo: undoneInTurn(undo),
2249
+ settle: ()=>void unsettled.splice(unsettled.indexOf(method), 1)
2250
+ };
2251
+ };
2252
+ /** A later answer: refused before the interaction has one. */ const later = (method, then)=>(args, byTest)=>{
2253
+ if (!answered()) refuse(method, notYetReplied(method), byTest);
2254
+ if (unsettled.length > 0) {
2255
+ warnInFlight(`${unsettled[0]}() had not resolved when ${method}() was called; on a bot this throws InteractionNotReplied. ` + `Await ${unsettled[0]}() first, or call useStrictMocks() to have the mock refuse it now.`);
2256
+ }
2257
+ const undo = snapshot();
2258
+ try {
2259
+ then(args);
2260
+ } catch (error) {
2261
+ undo();
2262
+ throw error;
2263
+ }
2264
+ return {
2265
+ undo: undoneInTurn(undo)
2266
+ };
2267
+ };
2268
+ /** The id an edit, fetch or delete names, `undefined` for the original response. */ const targetId = (message)=>{
2269
+ if (message === undefined || message === '@original') return undefined;
2270
+ return typeof message === 'string' ? message : message.id ?? undefined;
2271
+ };
2272
+ /** The message an edit, fetch or delete reaches, or why Discord refuses it: it is gone or never was. */ const target = (id)=>{
2273
+ if (id !== undefined) {
2274
+ const held = followUps.get(id);
2275
+ if (held) followUps.set(id, built(held));
2276
+ return held && built(held);
2277
+ }
2278
+ if (originalGone || modalAnswered) return undefined;
2279
+ // Answered without one of the mock's calls, as after a 40060 or a test setting `replied`: the original is there
2280
+ if (!original && answered()) original = componentMessage();
2281
+ if (original) original = built(original);
2282
+ // An answer's message gets its id when first read, as it would be built then without strict mocks
2283
+ if (original?.id === '') original = {
2284
+ ...original,
2285
+ id: nextSnowflake()
2286
+ };
2287
+ return original;
2288
+ };
2289
+ const unknownMessage = ()=>createDiscordError(10008, 'Unknown Message');
2290
+ // Where its answers are sent, read as the mock's own, so a placement warning stays the test's
2291
+ const answeredIn = ()=>readPlaceQuietly(()=>[
2292
+ instance.channel,
2293
+ instance.client
2294
+ ]);
2295
+ // What each answer's message made for itself, by its id, so every read of it has the same author and place
2296
+ const madePlaces = new Map();
2297
+ const madeFor = (id)=>madePlaces.get(id) ?? madePlaces.set(id, {}).get(id);
2298
+ // Each call's message, read by its behaviour right after the gate made it
2299
+ let made;
2300
+ let followUpMade;
1578
2301
  // Every answer the interaction gets, through respond() or discord.js directly, in order, for getResponse
1579
2302
  const log = [];
1580
2303
  Object.defineProperty(instance, RESPONSE_LOG, {
1581
2304
  value: log
1582
2305
  });
1583
- const answer = (method, impl)=>stubs.set(method, recordedMock(method, impl, log));
1584
- answer('reply', async (...args)=>{
1585
- if (instance.deferred || instance.replied) throw alreadyReplied();
1586
- instance.replied = true;
1587
- if (hasEphemeralFlag(args[0])) instance.ephemeral = true;
1588
- });
1589
- answer('deferReply', async (...args)=>{
1590
- if (instance.deferred || instance.replied) throw alreadyReplied();
1591
- instance.deferred = true;
1592
- if (hasEphemeralFlag(args[0])) instance.ephemeral = true;
1593
- });
1594
- answer('followUp', async ()=>{
1595
- if (!instance.deferred && !instance.replied) throw notYetReplied('followUp');
1596
- instance.replied = true;
1597
- return createMockMessage();
1598
- });
1599
- // The interaction's own message as Discord holds it: the message a component is on, or the original response.
1600
- // An edit replaces it in the shape Discord stores, with ids and resolved media, stamped with when it was edited,
1601
- // and fetchReply() reads that back, so code comparing the two sees what it would against Discord.
1602
- let held;
1603
- const current = ()=>held ?? (held = heldFrom(instance.message));
1604
- // A command answered with a modal has no original response, and Discord refuses one asked for
1605
- let modalAnswered = false;
1606
- const unknownMessage = ()=>createDiscordError(10008, 'Unknown Message');
1607
- answer('editReply', async (options)=>{
1608
- if (!instance.deferred && !instance.replied) throw notYetReplied('editReply');
1609
- if (modalAnswered) throw unknownMessage();
2306
+ const answer = (method, impl, gate)=>stubs.set(method, recordedMock(method, impl, log, gate));
2307
+ answer('reply', async ()=>undefined, first('reply', 'replied', ([options])=>original = hold('reply', ()=>sent(options, false))));
2308
+ answer('deferReply', async ()=>undefined, first('deferReply', 'deferred', ()=>original = sent(undefined)));
2309
+ answer('followUp', // The follow-up as sent, which an edit, fetch or delete reaches by its id
2310
+ async ()=>{
2311
+ const held = followUps.get(followUpMade);
2312
+ // One held to build when read resolves to its id alone, as building it now would refuse it
2313
+ return typeof held === 'function' ? createMockMessage({
2314
+ id: followUpMade
2315
+ }) : messageFrom(held, madeFor(held.id), ...answeredIn());
2316
+ }, later('followUp', ([options])=>{
1610
2317
  instance.replied = true;
1611
- held = edited(current(), options);
1612
- // The response has the message as Discord answers the edit, before its uploaded files have loaded again
1613
- return messageFrom({
2318
+ // Held as it is when sent, as Discord stores it, so a later change to what was passed isn't seen; its id taken
2319
+ // once built, so one that fails to build under strict mocks takes none
2320
+ const held = hold('followUp', ()=>sent(options, false));
2321
+ const id = nextSnowflake();
2322
+ made = undefined;
2323
+ followUps.set(id, typeof held === 'function' ? ()=>({
2324
+ ...held(),
2325
+ id
2326
+ }) : {
1614
2327
  ...held,
1615
- components: held.returned ?? held.components
2328
+ id
1616
2329
  });
1617
- });
1618
- stubs.set('fetchReply', createMockFn(async ()=>{
1619
- if (modalAnswered) throw unknownMessage();
1620
- return messageFrom(current());
2330
+ followUpMade = id;
2331
+ }));
2332
+ // An edit replaces the message in the shape Discord stores, with ids and resolved media, stamped with when it was
2333
+ // edited, and fetchReply() reads that back, so code comparing the two sees what it would against Discord
2334
+ answer('editReply', async ()=>{
2335
+ if (!made) throw unknownMessage();
2336
+ // The response has the message as Discord answers the edit, before its uploaded files have loaded again
2337
+ return messageFrom({
2338
+ ...made,
2339
+ components: made.returned ?? made.components
2340
+ }, madeFor(made.id), ...answeredIn());
2341
+ }, later('editReply', ([options])=>{
2342
+ const id = targetId(options?.message);
2343
+ const held = target(id);
2344
+ made = held && edited(held, options);
2345
+ if (!made) return;
2346
+ if (id === undefined) {
2347
+ original = made;
2348
+ instance.replied = true;
2349
+ } else followUps.set(id, made);
2350
+ }));
2351
+ stubs.set('fetchReply', createMockFn(async (message)=>{
2352
+ const held = target(targetId(message));
2353
+ if (!held) throw unknownMessage();
2354
+ return messageFrom(held, madeFor(held.id), ...answeredIn());
1621
2355
  }));
1622
2356
  answer('deleteReply', async ()=>{
1623
- if (!instance.deferred && !instance.replied) throw notYetReplied('deleteReply');
1624
- if (modalAnswered) throw unknownMessage();
1625
- });
2357
+ if (!made) throw unknownMessage();
2358
+ }, later('deleteReply', ([message])=>{
2359
+ const id = targetId(message);
2360
+ made = target(id);
2361
+ if (!made) return;
2362
+ if (id === undefined) originalGone = true;
2363
+ else followUps.delete(id);
2364
+ }));
1626
2365
  // showModal — the first response of a command or a component, like reply()
1627
- if (instance.type === discord_js.InteractionType.ApplicationCommand || instance.type === discord_js.InteractionType.MessageComponent) {
1628
- answer('showModal', async ()=>{
1629
- if (instance.deferred || instance.replied) throw alreadyReplied();
1630
- instance.replied = true;
2366
+ if (instance.type === discord_js.InteractionType.ApplicationCommand || isComponent) {
2367
+ answer('showModal', async ()=>undefined, first('showModal', 'replied', ()=>{
1631
2368
  modalAnswered = instance.type === discord_js.InteractionType.ApplicationCommand;
1632
- });
2369
+ if (isComponent) original = componentMessage();
2370
+ }));
1633
2371
  }
1634
2372
  // deferUpdate / update — components, and modals submitted from a message's component
1635
- if (instance.type === discord_js.InteractionType.MessageComponent || instance.type === discord_js.InteractionType.ModalSubmit) {
1636
- answer('update', async ()=>{
1637
- if (instance.deferred || instance.replied) throw alreadyReplied();
1638
- instance.replied = true;
1639
- });
1640
- answer('deferUpdate', async ()=>{
1641
- if (instance.deferred || instance.replied) throw alreadyReplied();
1642
- instance.deferred = true;
1643
- });
2373
+ if (isComponent || instance.type === discord_js.InteractionType.ModalSubmit) {
2374
+ answer('update', async ()=>undefined, first('update', 'replied', ([options])=>original = hold('update', ()=>edited(heldFrom(instance.message, false), options))));
2375
+ answer('deferUpdate', async ()=>undefined, first('deferUpdate', 'deferred', ()=>original = componentMessage()));
1644
2376
  }
1645
2377
  }
1646
2378
  // Autocomplete is not repliable, but it has a response of its own: Discord accepts
@@ -1649,12 +2381,31 @@ function createMockInteraction(Class, props) {
1649
2381
  // it before answering would decide the window was already closed.
1650
2382
  if (instance.type === discord_js.InteractionType.ApplicationCommandAutocomplete) {
1651
2383
  instance.responded = false;
2384
+ // Under strict mocks responded is set once respond() lands, as discord.js sets it once the request resolves, and a
2385
+ // second one sent while the first is in flight is refused by Discord with 40060; default mode sets it at once
2386
+ let respondsInFlight = 0;
2387
+ let respondWarned = false;
1652
2388
  stubs.set('respond', createMockFn(async (choices)=>{
1653
- if (instance.responded) throw new Error('The reply to this interaction has already been sent or deferred.');
2389
+ if (instance.responded) {
2390
+ if (respondsInFlight > 0 && !respondWarned) {
2391
+ respondWarned = true;
2392
+ mockLogger.warn('respond() was called while respond() had not resolved; on a bot discord.js sends it, and Discord refuses ' + 'it with 40060 ("Interaction has already been acknowledged") once the first lands. Await respond() ' + 'before responding again, or call useStrictMocks() to have the mock respond as Discord does.');
2393
+ }
2394
+ throw discordjsError(discord_js.DiscordjsErrorCodes.InteractionAlreadyReplied);
2395
+ }
1654
2396
  // Discord refuses a list longer than its limit, and the menu shows nothing
1655
2397
  if (Array.isArray(choices) && choices.length > MAX_AUTOCOMPLETE_CHOICES) {
1656
2398
  throw createDiscordError(50035, `Invalid Form Body\ndata.choices[BASE_TYPE_MAX_LENGTH]: Must be ${MAX_AUTOCOMPLETE_CHOICES} or fewer in length.`);
1657
2399
  }
2400
+ respondsInFlight++;
2401
+ if (!strictMocks()) instance.responded = true;
2402
+ try {
2403
+ await undefined;
2404
+ } finally{
2405
+ respondsInFlight--;
2406
+ }
2407
+ if (!strictMocks()) return;
2408
+ if (instance.responded) throw createDiscordError(40060, 'Interaction has already been acknowledged.');
1658
2409
  instance.responded = true;
1659
2410
  }));
1660
2411
  }
@@ -1679,6 +2430,34 @@ function createMockInteraction(Class, props) {
1679
2430
  const unset = (key)=>!Object.prototype.hasOwnProperty.call(instance, key);
1680
2431
  const generatedId = unset('id') ? instance.id = nextSnowflake() : undefined;
1681
2432
  defineCreatedTime(instance, generatedId);
2433
+ // Strict mocks place it where its message, guild or member is, and a guildId alone in a server the bot isn't in
2434
+ // Read as the mock's own, so a message's placement warning stays the test's
2435
+ strictPlace = readPlaceQuietly(()=>strictPlaceOf(own));
2436
+ if (strictPlace && strictMocks()) {
2437
+ if (strictPlace.refused) throw new Error(strictPlace.refused);
2438
+ const set = (key, value)=>Object.defineProperty(instance, key, {
2439
+ value,
2440
+ writable: true,
2441
+ enumerable: true,
2442
+ configurable: true
2443
+ });
2444
+ if (strictPlace.channel && unset('channel')) set('channel', strictPlace.channel);
2445
+ if (strictPlace.guildId !== undefined && unset('guildId')) set('guildId', strictPlace.guildId);
2446
+ if (strictPlace.guild !== undefined && unset('guild')) set('guild', strictPlace.guild);
2447
+ if (strictPlace.raw) {
2448
+ const user = own('user');
2449
+ const rawUser = user && {
2450
+ id: user.id,
2451
+ username: user.username,
2452
+ global_name: user.globalName ?? null,
2453
+ discriminator: user.discriminator,
2454
+ avatar: user.avatar ?? null
2455
+ };
2456
+ set('member', createMockRawMember(rawUser ? {
2457
+ user: rawUser
2458
+ } : {}));
2459
+ }
2460
+ }
1682
2461
  // A raw member is from a server the bot isn't in: no cached guild, and its user the interaction's, as discord.js reads it
1683
2462
  const raw = own('member');
1684
2463
  // Given with a guild, a raw-shaped member is kept as the cached server's member it was before raw members
@@ -1761,12 +2540,13 @@ function createMockInteraction(Class, props) {
1761
2540
  configurable: true
1762
2541
  });
1763
2542
  }
1764
- // The channel it came from, as the gateway caches it for an interaction: one of its server's, or the user's DM
2543
+ // The channel it came from, as the gateway caches it for an interaction: its server's cached one, looked up on each
2544
+ // read so a channel the test caches later is the one read, or the user's DM
1765
2545
  let channel;
1766
2546
  if (unset('channel')) {
1767
2547
  Object.defineProperty(instance, 'channel', {
1768
2548
  get: ()=>{
1769
- if (channel === undefined) {
2549
+ if (channel === undefined || own('guildId') && own('guild') instanceof discord_js.Guild) {
1770
2550
  channel = channelFor(own('guild'), own('guildId'), own('channelId'), own('user'));
1771
2551
  cacheChannel(channel, own('guild'), own('client'));
1772
2552
  }
@@ -1821,7 +2601,73 @@ function createMockInteraction(Class, props) {
1821
2601
  live('targetMember', ()=>own('guildId') ? memberFor(own('guild'), targetUser(), true, own('guildId')) : null);
1822
2602
  }
1823
2603
  }
2604
+ // A message context menu's target: the message given, else one made for its targetId. The message is the one
2605
+ // source: targetId reads its id, and setting either one moves the other with it
2606
+ if (discord_js.MessageContextMenuCommandInteraction.prototype.isPrototypeOf(instance)) {
2607
+ let given = own('targetMessage');
2608
+ let explicitId = own('targetId');
2609
+ if (given && given.id === undefined) given.id = nextSnowflake();
2610
+ if (given && explicitId !== undefined && explicitId !== given.id) {
2611
+ const conflict = `The targetMessage given is message ${given.id}, but the mock's targetId is ${explicitId}: give the same id, or leave one of them out.`;
2612
+ if (strictMocks()) throw new Error(conflict);
2613
+ controller_decorator.warnOnce(mockLogger, `${conflict} The mock reads targetMessage's id; under useStrictMocks(), as in the next major version (5.0), it refuses the pair.`);
2614
+ explicitId = undefined;
2615
+ }
2616
+ const generated = given || explicitId !== undefined ? undefined : nextSnowflake();
2617
+ let made;
2618
+ const accessor = (key, get, set)=>Object.defineProperty(instance, key, {
2619
+ get,
2620
+ set,
2621
+ enumerable: true,
2622
+ configurable: true
2623
+ });
2624
+ const targetId = ()=>explicitId ?? given?.id ?? generated;
2625
+ accessor('targetId', targetId, (value)=>{
2626
+ explicitId = value;
2627
+ given = undefined;
2628
+ });
2629
+ // In the channel the menu was used in, and its server, as the message a menu is used on is: its channel is the
2630
+ // menu's, read live, so making the message makes and caches no channel. Read as the mock's own, so a placement
2631
+ // warning stays the test's
2632
+ const makeTarget = ()=>readPlaceQuietly(()=>{
2633
+ const [guild, guildId] = [
2634
+ own('guild'),
2635
+ own('guildId') ?? null
2636
+ ];
2637
+ const message = messageMock({
2638
+ id: targetId(),
2639
+ guild: guildId && guild instanceof discord_js.Guild ? guild : null,
2640
+ client: instance.client
2641
+ }, undefined, {}, {
2642
+ channelId: own('channelId'),
2643
+ guildId,
2644
+ channel: ()=>readPlaceQuietly(()=>instance.channel)
2645
+ });
2646
+ // Where strict mocks place the menu elsewhere, its target's place warns as the menu's does, by the target's name
2647
+ const warner = placeWarners.get(instance);
2648
+ const target = mockTargets.get(message) ?? message;
2649
+ for (const key of warner?.keys ?? [])warnOnRead(target, key, ()=>readingPlace > 0 ? void placeReadsSeen?.add(key) : warner.warn(`targetMessage.${key}`));
2650
+ return message;
2651
+ });
2652
+ accessor('targetMessage', ()=>given ?? (made?.id === targetId() ? made : made = makeTarget()), (value)=>{
2653
+ given = value;
2654
+ if (given && given.id === undefined) given.id = nextSnowflake();
2655
+ explicitId = undefined;
2656
+ });
2657
+ }
1824
2658
  }
2659
+ // A user has an id of its own, as Discord sends one, and was created when the mock was made under strict mocks
2660
+ if (discord_js.User.prototype.isPrototypeOf(instance) && !Object.prototype.hasOwnProperty.call(instance, 'id')) {
2661
+ const generatedId = instance.id = nextSnowflake();
2662
+ defineMadeTime(instance, generatedId);
2663
+ }
2664
+ // A role too, created when the mock was made in both modes
2665
+ if (discord_js.Role.prototype.isPrototypeOf(instance) && !Object.prototype.hasOwnProperty.call(instance, 'id')) {
2666
+ const generatedId = instance.id = nextSnowflake();
2667
+ defineCreatedTime(instance, generatedId);
2668
+ }
2669
+ // An attachment too, from the same count as every mock's
2670
+ if (discord_js.Attachment.prototype.isPrototypeOf(instance) && !Object.prototype.hasOwnProperty.call(instance, 'id')) instance.id = nextSnowflake();
1825
2671
  // Discord sends the user's locale with every interaction, and the server's preferred one with one made in a server
1826
2672
  if (discord_js.BaseInteraction.prototype.isPrototypeOf(instance)) {
1827
2673
  if (own('locale') === undefined) instance.locale = discord_js.Locale.EnglishUS;
@@ -1845,6 +2691,32 @@ function createMockInteraction(Class, props) {
1845
2691
  }
1846
2692
  for (const kind of choices)if (!Object.prototype.hasOwnProperty.call(instance, kind)) instance[kind] = new discord_js.Collection();
1847
2693
  }
2694
+ // In default mode, where strict mocks place it elsewhere, reading where it is warns
2695
+ if (strictPlace && !strictMocks()) warnOnPlaceReads(instance, strictPlace, new Set(Object.keys(props ?? {})));
2696
+ // A message whose place the mock made moves to where the test placed the interaction under strict mocks, and is then
2697
+ // one the test placed; in default mode it stays, and reading its place warns
2698
+ const message = own('message');
2699
+ const placed = [
2700
+ 'channel',
2701
+ 'guild',
2702
+ 'guildId'
2703
+ ].some((key)=>Object.keys(props ?? {}).includes(key));
2704
+ if (discord_js.BaseInteraction.prototype.isPrototypeOf(instance) && placed && typeof message === 'object' && message !== null && placeWasMade(message)) {
2705
+ if (strictMocks()) {
2706
+ const guild = own('guild');
2707
+ const channel = instance.channel;
2708
+ Object.assign(message, {
2709
+ guild: guild ?? null,
2710
+ guildId: own('guildId') ?? null,
2711
+ ...channel && {
2712
+ channel,
2713
+ channelId: channel.id
2714
+ }
2715
+ });
2716
+ if (guild) memberFor(guild, message.author, true);
2717
+ placesMade.delete(mockTargets.get(message) ?? message);
2718
+ } else warnOnMadePlaceReads(mockTargets.get(message) ?? message);
2719
+ }
1848
2720
  // Options assigned after creation, directly or through Object.assign, belong to it as those given do
1849
2721
  return stubDeep(instance, stubs, (prop, value)=>{
1850
2722
  if (prop === 'options') ownOptions(instance, value);
@@ -1856,8 +2728,15 @@ function createMockInteraction(Class, props) {
1856
2728
  /**
1857
2729
  * A mock function that answers property access with another one, so `cache.store.flush()` works
1858
2730
  * on a double whose shape is an interface with nothing at runtime to read it from.
1859
- */ function stubCallable() {
2731
+ */ function stubCallable(member) {
1860
2732
  const fn = createMockFn();
2733
+ // jest's mock reads `_protoImpl` from itself whenever it's called, so the prototype below must not answer it
2734
+ if (usesRunnerMockFn()) Object.defineProperty(fn, '_protoImpl', {
2735
+ value: undefined,
2736
+ writable: true,
2737
+ enumerable: false,
2738
+ configurable: true
2739
+ });
1861
2740
  // Any property beyond the mock's own API is a nested mock, made on first read and kept on the function, out of its
1862
2741
  // keys. Its prototype makes them rather than a Proxy around it, so it stays the plain mock function matchers read
1863
2742
  const inherited = Object.getPrototypeOf(fn);
@@ -1866,7 +2745,9 @@ function createMockInteraction(Class, props) {
1866
2745
  if (typeof prop === 'symbol' || prop in proto) return Reflect.get(proto, prop, receiver);
1867
2746
  // Never thenable — otherwise awaiting a mock hangs on itself
1868
2747
  if (prop === 'then') return undefined;
1869
- const nested = stubCallable();
2748
+ // jest's matchers take a function whose `calls` has `all` and `count` for a jasmine spy, read before `mock.calls`
2749
+ if (member === 'calls' && (prop === 'all' || prop === 'count')) return undefined;
2750
+ const nested = stubCallable(prop);
1870
2751
  Object.defineProperty(receiver, prop, {
1871
2752
  value: nested,
1872
2753
  writable: true,
@@ -1931,6 +2812,7 @@ function createMockInteraction(Class, props) {
1931
2812
  if (key === 'then') return undefined;
1932
2813
  // An explicitly supplied prop wins over the auto-stub.
1933
2814
  if (Object.prototype.hasOwnProperty.call(instance, key)) return Reflect.get(instance, prop, instance);
2815
+ // The double itself is no function a matcher reads, so a `calls` member here keeps its `all` and `count`
1934
2816
  if (!stubs.has(key)) stubs.set(key, stubCallable());
1935
2817
  return stubs.get(key);
1936
2818
  },
@@ -1970,11 +2852,12 @@ function createMockInteraction(Class, props) {
1970
2852
  * @group Testing
1971
2853
  * @category Mocks
1972
2854
  * @see {@link createMockInteraction}
1973
- */ const createMockUser = (props = {})=>createMockInteraction(discord_js.User, {
1974
- id: nextSnowflake(),
2855
+ */ function createMockUser(props = {}) {
2856
+ return createMockInteraction(discord_js.User, {
1975
2857
  bot: false,
1976
2858
  ...props
1977
2859
  });
2860
+ }
1978
2861
  /**
1979
2862
  * Creates a mock {@link Client}, with `users`, `channels`, `guilds` and `application.commands` ready to stub.
1980
2863
  *
@@ -2088,6 +2971,38 @@ const HOLDS = [
2088
2971
  [
2089
2972
  discord_js.PermissionOverwriteManager.prototype,
2090
2973
  discord_js.PermissionOverwrites
2974
+ ],
2975
+ [
2976
+ discord_js.ReactionManager.prototype,
2977
+ discord_js.MessageReaction
2978
+ ],
2979
+ [
2980
+ discord_js.ReactionUserManager.prototype,
2981
+ discord_js.User
2982
+ ],
2983
+ [
2984
+ discord_js.PresenceManager.prototype,
2985
+ discord_js.Presence
2986
+ ],
2987
+ [
2988
+ discord_js.GuildEmojiManager.prototype,
2989
+ discord_js.GuildEmoji
2990
+ ],
2991
+ [
2992
+ discord_js.GuildStickerManager.prototype,
2993
+ discord_js.Sticker
2994
+ ],
2995
+ [
2996
+ discord_js.GuildScheduledEventManager.prototype,
2997
+ discord_js.GuildScheduledEvent
2998
+ ],
2999
+ [
3000
+ discord_js.AutoModerationRuleManager.prototype,
3001
+ discord_js.AutoModerationRule
3002
+ ],
3003
+ [
3004
+ discord_js.GuildInviteManager.prototype,
3005
+ discord_js.Invite
2091
3006
  ]
2092
3007
  ];
2093
3008
  /**
@@ -2103,6 +3018,11 @@ const HOLDS = [
2103
3018
  value: cache,
2104
3019
  writable: true
2105
3020
  });
3021
+ // The same collection where discord.js's own methods read it past a subclass's `cache`, as `super.cache` does
3022
+ Object.defineProperty(manager, '_cache', {
3023
+ value: cache,
3024
+ writable: true
3025
+ });
2106
3026
  const holds = HOLDS.find(([Manager])=>Manager === prototype || Object.prototype.isPrototypeOf.call(Manager, prototype))?.[1];
2107
3027
  if (holds) Object.defineProperty(manager, 'holds', {
2108
3028
  value: holds
@@ -2110,16 +3030,28 @@ const HOLDS = [
2110
3030
  let own;
2111
3031
  Object.defineProperty(manager, 'client', {
2112
3032
  get: ()=>{
2113
- const owner = manager.guild ?? manager.channel ?? manager.thread;
3033
+ const owner = manager.guild ?? manager.channel ?? manager.thread ?? manager.message;
2114
3034
  return owner instanceof discord_js.Base ? owner.client : own ?? (own = createMockClient());
2115
3035
  },
2116
- set: (value)=>own = value,
3036
+ // A client the test sets is the one read from then on, as on any other mock
3037
+ set: (value)=>Object.defineProperty(manager, 'client', {
3038
+ value,
3039
+ writable: true,
3040
+ enumerable: true,
3041
+ configurable: true
3042
+ }),
2117
3043
  configurable: true
2118
3044
  });
2119
3045
  return stubDeep(manager);
2120
3046
  }
3047
+ /** An empty manager of `prototype` that `owner` names its own, such as `{ message }` for a message's reactions. */ function ownedManager(prototype, owner) {
3048
+ const manager = managerWith(prototype, undefined);
3049
+ Object.assign(manager, owner);
3050
+ return manager;
3051
+ }
2121
3052
  /**
2122
- * Creates a mock {@link Guild}, with the `members`, `channels`, `roles` and `bans` managers ready to stub.
3053
+ * Creates a mock {@link Guild}, with its `members`, `channels`, `roles`, `bans`, `emojis`, `stickers`, `scheduledEvents`,
3054
+ * `autoModerationRules` and `invites` managers ready to stub.
2123
3055
  *
2124
3056
  * Use it for the server a message or an interaction came from, with the members, roles and channels a handler looks
2125
3057
  * up in it.
@@ -2129,9 +3061,11 @@ const HOLDS = [
2129
3061
  *
2130
3062
  * @remarks
2131
3063
  * A manager's `fetch(id)` resolves to its cached item with that id, as discord.js looks there first, or to a new one it
2132
- * caches under that id: a member or channel in this guild, a role with that id, or a ban. `members.fetch({ user: ids })`
2133
- * resolves to a collection of each of those members, found or made the same way; `create()` and `edit()` resolve to a
2134
- * mock of its item, and a list fetch to an empty collection. Members, roles and channels given are put in
3064
+ * caches under that id: a member, channel, emoji, sticker, scheduled event or AutoMod rule in this guild, a role with
3065
+ * that id, a ban, or an invite with that code. `members.fetch({ user: ids })` resolves to a collection of each of those
3066
+ * members, found or made the same way; `create()` and `edit()` resolve to a mock of its item, which `create()` caches
3067
+ * as discord.js does, but for an invite, made for the channel given and cached nowhere; a list fetch resolves to an
3068
+ * empty collection. Members, roles and channels given are put in
2135
3069
  * their managers' caches, where dispatch looks first when it resolves a message's typed params; a member
2136
3070
  * {@link createMockMember} made without a server is in this one. Its `roles.everyone` is the role given with the
2137
3071
  * guild's id, or else an @everyone role of its own at position 0 with no permissions, in `roles.cache` as Discord has
@@ -2154,14 +3088,15 @@ const HOLDS = [
2154
3088
  * @see {@link createMockMessage}
2155
3089
  */ function createMockGuild(overrides = {}) {
2156
3090
  const instance = Object.create(discord_js.Guild.prototype);
2157
- instance.id = nextSnowflake();
2158
- if (overrides.id !== undefined) instance.id = overrides.id;
3091
+ const generatedId = overrides.id === undefined ? nextSnowflake() : undefined;
3092
+ instance.id = overrides.id ?? generatedId;
3093
+ defineMadeTime(instance, generatedId);
2159
3094
  if (overrides.name !== undefined) instance.name = overrides.name;
2160
3095
  if (overrides.preferredLocale !== undefined) instance.preferredLocale = overrides.preferredLocale;
2161
3096
  instance.members = managerWith(discord_js.GuildMemberManager.prototype, overrides.members);
2162
3097
  instance.channels = managerWith(discord_js.GuildChannelManager.prototype, overrides.channels);
2163
3098
  instance.roles = guildRoleManager(instance.id, overrides.roles);
2164
- instance.bans = managerWith(discord_js.GuildBanManager.prototype, undefined);
3099
+ for (const [key, Manager] of GUILD_MANAGERS)instance[key] = managerWith(Manager.prototype, undefined);
2165
3100
  const guild = stubDeep(instance);
2166
3101
  settleGuild(instance, guild);
2167
3102
  // A member made without a server of its own is in this one
@@ -2200,7 +3135,8 @@ const HOLDS = [
2200
3135
  * @see {@link createMockGuild}
2201
3136
  */ function createMockChannel(Class, props = {}) {
2202
3137
  const instance = Object.create(Class.prototype);
2203
- instance.id = nextSnowflake();
3138
+ const generatedId = props.id === undefined ? nextSnowflake() : undefined;
3139
+ instance.id = generatedId;
2204
3140
  const is = (Base)=>Base.prototype.isPrototypeOf(Class.prototype) || Class === Base;
2205
3141
  // Text and announcement channels: messages, and threads made in the channel
2206
3142
  if (is(discord_js.TextChannel) || is(discord_js.NewsChannel)) {
@@ -2228,6 +3164,7 @@ const HOLDS = [
2228
3164
  // A server's channel has its overwrites, none until a test puts some in the cache, which its permissions read
2229
3165
  if (is(discord_js.GuildChannel) && !is(discord_js.ThreadChannel)) instance.permissionOverwrites = managerWith(discord_js.PermissionOverwriteManager.prototype, undefined);
2230
3166
  Object.assign(instance, props);
3167
+ defineMadeTime(instance, generatedId);
2231
3168
  const stubs = new Map();
2232
3169
  const channel = stubDeep(instance, stubs);
2233
3170
  // Each manager knows its channel, as discord.js's do, and a thread's members their thread
@@ -2353,16 +3290,24 @@ const CHANNEL_TYPE_GUARDS = [
2353
3290
  return stubDeep(manager);
2354
3291
  }
2355
3292
  /** The permissions Discord gives a server's @everyone role in a server made today. */ const NEW_SERVER_EVERYONE_PERMISSIONS = 2248473465835073n;
3293
+ // The @everyone roles default mode made with no permissions, where strict mocks give a new server's
3294
+ const madeEveryones = new WeakSet();
3295
+ /** Whether `role` is an @everyone role default mode made, whose permissions the test hasn't set. */ const isUnsetEveryone = (role)=>madeEveryones.has(role) && !Object.prototype.hasOwnProperty.call(role, 'permissions');
2356
3296
  // A server's @everyone role, which has the server's id and ranks lowest, at position 0; with strict mocks, the
2357
3297
  // permissions Discord gives it in a new server
2358
- const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3298
+ function everyoneRole(guildId) {
3299
+ const strict = strictMocks();
3300
+ const role = createMockInteraction(discord_js.Role, {
2359
3301
  id: guildId,
2360
3302
  name: '@everyone',
2361
3303
  position: 0,
2362
- ...strictMocks() ? {
3304
+ ...strict ? {
2363
3305
  permissions: new discord_js.PermissionsBitField(NEW_SERVER_EVERYONE_PERMISSIONS).freeze()
2364
3306
  } : {}
2365
3307
  });
3308
+ if (!strict) madeEveryones.add(role);
3309
+ return role;
3310
+ }
2366
3311
  /**
2367
3312
  * A guild's role manager with `roles` in its cache, and the guild's @everyone role: the one given with the guild's id,
2368
3313
  * or a role of its own at position 0, as every server has one.
@@ -2462,8 +3407,9 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
2462
3407
  * highest, by position, then the lower id. `permissions` are computed as discord.js computes them: every permission for
2463
3408
  * the server's owner, and otherwise its roles' permissions combined, @everyone's included, so a member with no roles has
2464
3409
  * @everyone's alone. The member is put in its guild's member cache, so an interaction or a message from its user in that
2465
- * server has it as its `member`, and `memberPermissions` are its permissions. A DM sent to the member goes through its
2466
- * user's `send()` and the user's one DM channel.
3410
+ * server has it as its `member`, and `memberPermissions` are its permissions, with the interaction's channel's
3411
+ * overwrites applied under `useStrictMocks()`. A DM sent to the member goes through its user's `send()` and the user's
3412
+ * one DM channel.
2467
3413
  *
2468
3414
  * @param overrides - The member's user, server, roles and nickname; see {@link MockMemberOverrides}.
2469
3415
  * @returns The mock member.
@@ -2510,6 +3456,218 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
2510
3456
  return guild ? cached(cacheOf(guild.channels), channelId, make) : make();
2511
3457
  }
2512
3458
  /** A server's id as a refusal names it, or a DM. */ const placeName = (guildId)=>guildId === null ? 'a DM' : `server ${guildId}`;
3459
+ /**
3460
+ * Where strict mocks place an interaction: in its message's channel and server, in a guild's or a member's server, and
3461
+ * with a guildId alone in a server the bot isn't in. Undefined where a channel given, or nothing, places it.
3462
+ */ // Mock messages whose channel and server the mock made, as neither was given
3463
+ const placesMade = new WeakSet();
3464
+ // Messages whose place reads already warn, so a message reused for several interactions is wrapped once
3465
+ const madePlaceWarned = new WeakSet();
3466
+ /**
3467
+ * Has a message whose place the mock made warn once per key when its place is read, in default mode, where strict mocks
3468
+ * move it to the interaction it was given to. A read made while reading another of these, or by the mock, stays quiet.
3469
+ */ function warnOnMadePlaceReads(message) {
3470
+ if (madePlaceWarned.has(message)) return;
3471
+ madePlaceWarned.add(message);
3472
+ for (const key of [
3473
+ 'guild',
3474
+ 'guildId',
3475
+ 'channel',
3476
+ 'channelId',
3477
+ 'member'
3478
+ ]){
3479
+ const descriptor = Object.getOwnPropertyDescriptor(message, key);
3480
+ if (!descriptor) continue;
3481
+ let value = descriptor.value;
3482
+ Object.defineProperty(message, key, {
3483
+ get: ()=>{
3484
+ if (readingPlace === 0) {
3485
+ controller_decorator.warnOnce(mockLogger, `Message.${key} reads the place createMockMessage() made for this message here, though the interaction it was ` + "given to is placed elsewhere; in the next major version (5.0) the message moves to the interaction's place. " + 'Give the message a channel or guild, or call useStrictMocks() to move it now.');
3486
+ }
3487
+ readingPlace++;
3488
+ try {
3489
+ return descriptor.get ? descriptor.get.call(message) : value;
3490
+ } finally{
3491
+ readingPlace--;
3492
+ }
3493
+ },
3494
+ set: (next)=>{
3495
+ if (descriptor.set) descriptor.set.call(message, next);
3496
+ else value = next;
3497
+ },
3498
+ enumerable: true,
3499
+ configurable: true
3500
+ });
3501
+ }
3502
+ }
3503
+ // The DM channel each message the bot sent in a DM in default mode was sent in, where strict mocks place it
3504
+ const sentInDm = new WeakMap();
3505
+ /** Whether a message is one whose channel and server the mock made, which a test placing an interaction overrides. */ const placeWasMade = (message)=>placesMade.has(mockTargets.get(message) ?? message);
3506
+ function strictPlaceOf(own) {
3507
+ const message = own('message');
3508
+ const channel = own('channel');
3509
+ const guild = own('guild');
3510
+ const guildId = own('guildId');
3511
+ const member = own('member');
3512
+ // A message whose place the mock made has none a test meant, so where the test placed the interaction wins
3513
+ const placed = channel !== undefined || guild !== undefined || guildId !== undefined;
3514
+ if (typeof message === 'object' && message !== null && typeof message.channelId === 'string' && !(placed && placeWasMade(message))) {
3515
+ // One the bot sent in a DM is in that DM, as strict mocks place it, though default mode reads a server for it
3516
+ const dm = sentInDm.get(mockTargets.get(message) ?? message);
3517
+ const channelId = dm ? dm.id : message.channelId;
3518
+ const at = dm ? null : message.guildId ?? null;
3519
+ const refused = channel && channel.id !== channelId ? `The message given is in channel ${channelId}, but the mock's channel is ${channel.id}: give the message's channel, or leave one of them out.` : guild !== undefined && (guild?.id ?? null) !== at ? `The message given is in ${placeName(at)}, but the mock's guild is ${placeName(guild?.id ?? null)}: give the message's guild, or leave one of them out.` : guildId !== undefined && guildId !== at ? `The message given is in ${placeName(at)}, but the mock's guildId is ${placeName(guildId)}: give the message's guildId, or leave one of them out.` : undefined;
3520
+ return {
3521
+ given: 'message',
3522
+ channel: dm ?? message.channel,
3523
+ guildId: at,
3524
+ guild: dm ? null : message.guild ?? null,
3525
+ refused
3526
+ };
3527
+ }
3528
+ if (guild instanceof discord_js.Guild && guildId === undefined) return {
3529
+ given: 'guild',
3530
+ guildId: guild.id
3531
+ };
3532
+ if (member instanceof discord_js.GuildMember && member.guild instanceof discord_js.Guild && guild === undefined && guildId === undefined) {
3533
+ return {
3534
+ given: 'member',
3535
+ guild: member.guild,
3536
+ guildId: member.guild.id
3537
+ };
3538
+ }
3539
+ if (typeof guildId === 'string' && guild === undefined && channel === undefined && !(member instanceof discord_js.GuildMember) && !isRawMember(member)) {
3540
+ return {
3541
+ given: 'guildId',
3542
+ raw: true
3543
+ };
3544
+ }
3545
+ return undefined;
3546
+ }
3547
+ // Set while one of the reads below runs, so what it reads in turn doesn't warn again
3548
+ let readingPlace = 0;
3549
+ /** Runs `read` as the mock's own read of where a mock is, which warns about no placement. */ function readPlaceQuietly(read) {
3550
+ readingPlace++;
3551
+ try {
3552
+ return read();
3553
+ } finally{
3554
+ readingPlace--;
3555
+ }
3556
+ }
3557
+ // Each interaction's strict place and its warning, by the read it names, for the mock's own reads that warn as the test's
3558
+ const placeWarners = new WeakMap();
3559
+ // The place keys a read of the mock's own read while it runs, so it warns by its own name where one would warn
3560
+ let placeReadsSeen;
3561
+ /**
3562
+ * Runs `read`, a value of `mock`'s own that reads where the mock is, quietly; in default mode it warns once, as `key`,
3563
+ * where one of those reads would warn, since strict mocks place the mock otherwise, unless `differs` says the value is
3564
+ * the same in that place.
3565
+ */ function readPlaceAs(mock, key, read, differs = ()=>true) {
3566
+ const outer = placeReadsSeen;
3567
+ const seen = placeReadsSeen = new Set();
3568
+ try {
3569
+ return readPlaceQuietly(read);
3570
+ } finally{
3571
+ placeReadsSeen = outer;
3572
+ const warner = placeWarners.get(mockTargets.get(mock) ?? mock);
3573
+ if (seen.size > 0 && warner && differs(warner.place)) warner.warn(key);
3574
+ }
3575
+ }
3576
+ /** What reading `key`, where strict mocks place an interaction otherwise, warns: what it reads now, and in 5.0. */ function placeWarning(name, key, place) {
3577
+ if (place.refused) {
3578
+ return `${name}.${key} reads where the mock's channel, guild or guildId put it, not where its message is; under useStrictMocks(), as in the next major version (5.0), the mock is refused: ${place.refused}`;
3579
+ }
3580
+ const why = {
3581
+ message: [
3582
+ 'a place other than its message\'s',
3583
+ "the mock is in its message's channel and server",
3584
+ "Give it the message's channel, or call useStrictMocks() to place it there now."
3585
+ ],
3586
+ guild: [
3587
+ "a DM's value, though the mock was given a guild",
3588
+ 'the mock is in that guild',
3589
+ 'Give its guildId or one of its channels too, or call useStrictMocks() to place it there now.'
3590
+ ],
3591
+ member: [
3592
+ "a DM's value, though the mock was given a member of a server",
3593
+ "the mock is in the member's server",
3594
+ "Give the member's guild too, or call useStrictMocks() to place it there now."
3595
+ ],
3596
+ guildId: [
3597
+ 'a server the bot is in, though the mock was given only a guildId',
3598
+ "a guildId alone is a server the bot isn't in: guild and channel are null, and the member is a raw one",
3599
+ 'Give a guild too, or call useStrictMocks() to read it so now.'
3600
+ ]
3601
+ }[place.given];
3602
+ return `${name}.${key} reads ${why[0]} here; in the next major version (5.0) ${why[1]}. ${why[2]}`;
3603
+ }
3604
+ /** Has a read of `instance`'s `key` call `warn` first, and what that read reads in turn stay quiet. */ function warnOnRead(instance, key, warn) {
3605
+ const descriptor = Object.getOwnPropertyDescriptor(instance, key);
3606
+ // One the mock answers with a stub stays unset, as the mock's own checks read it, and warns as the stub is read
3607
+ if (!descriptor) {
3608
+ if (!stubReadWarnings.has(instance)) stubReadWarnings.set(instance, new Map());
3609
+ stubReadWarnings.get(instance).set(key, warn);
3610
+ return;
3611
+ }
3612
+ const read = ()=>descriptor.get ? descriptor.get.call(instance) : descriptor.value;
3613
+ Object.defineProperty(instance, key, {
3614
+ get: ()=>{
3615
+ warn();
3616
+ readingPlace++;
3617
+ try {
3618
+ return read();
3619
+ } finally{
3620
+ readingPlace--;
3621
+ }
3622
+ },
3623
+ set: (next)=>Object.defineProperty(instance, key, {
3624
+ value: next,
3625
+ writable: true,
3626
+ enumerable: true,
3627
+ configurable: true
3628
+ }),
3629
+ enumerable: true,
3630
+ configurable: true
3631
+ });
3632
+ }
3633
+ /**
3634
+ * Has each of an interaction's `guildId`, `guild`, `channelId`, `channel` and `member` that strict mocks read otherwise
3635
+ * warn once when read, unless the test gave it.
3636
+ */ function warnOnPlaceReads(instance, place, given) {
3637
+ const value = (key)=>{
3638
+ const descriptor = Object.getOwnPropertyDescriptor(instance, key);
3639
+ return descriptor?.get ? undefined : descriptor?.value;
3640
+ };
3641
+ const keys = place.refused ? [
3642
+ 'guildId',
3643
+ 'guild',
3644
+ 'channelId',
3645
+ 'channel'
3646
+ ] : place.raw ? [
3647
+ 'guild',
3648
+ 'channel',
3649
+ 'member'
3650
+ ] : place.guildId !== value('guildId') ? [
3651
+ 'guildId',
3652
+ 'guild',
3653
+ 'channelId',
3654
+ 'channel',
3655
+ 'member'
3656
+ ] : place.channel && place.channel.id !== value('channelId') ? [
3657
+ 'channelId',
3658
+ 'channel'
3659
+ ] : [];
3660
+ const name = Object.getPrototypeOf(instance).constructor.name;
3661
+ placeWarners.set(instance, {
3662
+ place,
3663
+ keys,
3664
+ warn: (read)=>controller_decorator.warnOnce(mockLogger, placeWarning(name, read, place))
3665
+ });
3666
+ for (const key of keys.filter((each)=>!given.has(each))){
3667
+ // A read the mock makes stays quiet, noted for a read of the mock's own to warn by its name
3668
+ warnOnRead(instance, key, ()=>readingPlace > 0 ? void placeReadsSeen?.add(key) : controller_decorator.warnOnce(mockLogger, placeWarning(name, key, place)));
3669
+ }
3670
+ }
2513
3671
  /**
2514
3672
  * Where a channel a test gives puts the mock made in it, read from the channel as discord.js reads it: no server for a
2515
3673
  * DM channel, else the server the channel names. A server channel that names none is put in `fallback`'s, and named
@@ -2554,23 +3712,69 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
2554
3712
  cached(cacheOf(manager), id, ()=>channel);
2555
3713
  }
2556
3714
  }
2557
- /** The guild a mock message carries: a guild with the same stubbed managers as createMockGuild. */ function createMockGuildForMessage(id = nextSnowflake()) {
3715
+ /** The guild a mock message carries: a guild with the same stubbed managers as createMockGuild. */ function createMockGuildForMessage(id) {
2558
3716
  const guild = Object.create(discord_js.Guild.prototype);
2559
- guild.id = id;
3717
+ const generatedId = id === undefined ? nextSnowflake() : undefined;
3718
+ guild.id = id ?? generatedId;
3719
+ defineMadeTime(guild, generatedId);
2560
3720
  guild.members = managerWith(discord_js.GuildMemberManager.prototype, undefined);
2561
3721
  guild.channels = managerWith(discord_js.GuildChannelManager.prototype, undefined);
2562
3722
  guild.roles = guildRoleManager(guild.id, undefined);
2563
- guild.bans = managerWith(discord_js.GuildBanManager.prototype, undefined);
3723
+ for (const [key, Manager] of GUILD_MANAGERS)guild[key] = managerWith(Manager.prototype, undefined);
2564
3724
  const proxy = stubDeep(guild);
2565
3725
  settleGuild(guild, proxy);
2566
3726
  return proxy;
2567
3727
  }
3728
+ // Each settled guild's bot member as `members.me` reads it, without caching one made for the read
3729
+ const botMembers = new WeakMap();
3730
+ /** The bot's member in `guild` for the mock's own reads: the one `members.me` reads, made without caching it there. */ function botMemberOf(guild) {
3731
+ return botMembers.get(guild)?.() ?? guild.members.me;
3732
+ }
3733
+ /**
3734
+ * The channel an interaction's `channel` reads, for the mock's own reads: `null` in a DM, else one set on it or its
3735
+ * server's cached one, without making or caching one. Undefined where `channel` would make one, which has no overwrites.
3736
+ */ function heldChannelOf(interaction, guild) {
3737
+ const target = mockTargets.get(interaction) ?? interaction;
3738
+ return readPlaceQuietly(()=>{
3739
+ const set = Object.getOwnPropertyDescriptor(target, 'channel');
3740
+ if (set && 'value' in set) return set.value;
3741
+ const { guildId, channelId } = target;
3742
+ return guildId ? cacheOf(guild.channels)?.get(channelId) : null;
3743
+ });
3744
+ }
3745
+ // A guild's managers beside members, channels and roles, each empty, of its discord.js class
3746
+ const GUILD_MANAGERS = [
3747
+ [
3748
+ 'bans',
3749
+ discord_js.GuildBanManager
3750
+ ],
3751
+ [
3752
+ 'emojis',
3753
+ discord_js.GuildEmojiManager
3754
+ ],
3755
+ [
3756
+ 'stickers',
3757
+ discord_js.GuildStickerManager
3758
+ ],
3759
+ [
3760
+ 'scheduledEvents',
3761
+ discord_js.GuildScheduledEventManager
3762
+ ],
3763
+ [
3764
+ 'autoModerationRules',
3765
+ discord_js.AutoModerationRuleManager
3766
+ ],
3767
+ [
3768
+ 'invites',
3769
+ discord_js.GuildInviteManager
3770
+ ]
3771
+ ];
2568
3772
  /** Gives a guild's managers the guild, as discord.js's do, so what they fetch or make is in it. */ function homeManagers(instance, guild) {
2569
3773
  for (const key of [
2570
3774
  'members',
2571
3775
  'channels',
2572
3776
  'roles',
2573
- 'bans'
3777
+ ...GUILD_MANAGERS.map(([key])=>key)
2574
3778
  ])instance[key].guild = guild;
2575
3779
  }
2576
3780
  /**
@@ -2586,11 +3790,13 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
2586
3790
  configurable: true
2587
3791
  });
2588
3792
  const members = instance.members;
3793
+ const botMember = (keep)=>{
3794
+ const { user } = guild.client;
3795
+ return keep ? cached(cacheOf(members), user.id, ()=>memberIn(guild, user.id, user)) : cacheOf(members)?.get(user.id) ?? memberIn(guild, user.id, user);
3796
+ };
3797
+ botMembers.set(guild, ()=>botMember(false));
2589
3798
  Object.defineProperty(members, 'me', {
2590
- get: ()=>{
2591
- const { user } = guild.client;
2592
- return cached(cacheOf(members), user.id, ()=>memberIn(guild, user.id, user));
2593
- },
3799
+ get: ()=>botMember(true),
2594
3800
  configurable: true
2595
3801
  });
2596
3802
  for (const key of [
@@ -2664,8 +3870,11 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
2664
3870
  };
2665
3871
  // Own values, so the prototype's getters, which read the raw mention data, are not reached
2666
3872
  const instance = Object.create(discord_js.MessageMentions.prototype);
3873
+ const parsedUsers = new discord_js.Collection(mentioned.users);
2667
3874
  for (const [key, value] of Object.entries({
2668
3875
  ...mentioned,
3876
+ parsedUsers,
3877
+ crosspostedChannels: new discord_js.Collection(),
2669
3878
  everyone: false,
2670
3879
  client,
2671
3880
  guild: guild ?? null
@@ -2678,11 +3887,14 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
2678
3887
  return stubDeep(instance);
2679
3888
  }
2680
3889
  /** A value as its API JSON: a builder or discord.js structure through `toJSON()`, anything else as it is. */ const jsonOf = (value)=>value && typeof value.toJSON === 'function' ? value.toJSON() : value;
2681
- /** The message a reply method starts from: the one a component is on, as it was, or an empty original response. */ function heldFrom(message) {
3890
+ /**
3891
+ * The message a reply method starts from: the one a component is on, as it was, or an empty original response. Without
3892
+ * `allocate`, one with no id of its own gets `''`, for its holder to give it one when first read.
3893
+ */ function heldFrom(message, allocate = true) {
2682
3894
  // Read only what the message really has: a mock's unset fields are stubs
2683
3895
  const from = message ?? {};
2684
3896
  return {
2685
- id: typeof from.id === 'string' ? from.id : nextSnowflake(),
3897
+ id: typeof from.id === 'string' ? from.id : allocate ? nextSnowflake() : '',
2686
3898
  content: typeof from.content === 'string' ? from.content : undefined,
2687
3899
  components: Array.isArray(from.components) ? from.components.map(jsonOf) : [],
2688
3900
  embeds: Array.isArray(from.embeds) ? from.embeds.map(jsonOf) : [],
@@ -2690,6 +3902,15 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
2690
3902
  editedTimestamp: typeof from.editedTimestamp === 'number' ? from.editedTimestamp : null
2691
3903
  };
2692
3904
  }
3905
+ /** A message as an answer sends it: a new one, with what the answer gave, never edited. */ const sent = (options, allocate = true)=>{
3906
+ // Its id taken only once it builds, so a payload that fails to build takes none
3907
+ const held = edited(heldFrom(undefined, false), options);
3908
+ return {
3909
+ ...held,
3910
+ id: allocate ? nextSnowflake() : '',
3911
+ editedTimestamp: null
3912
+ };
3913
+ };
2693
3914
  /** A held message after an edit: what the edit sets replaced in Discord's shape, and a later edit stamp. */ function edited(held, options) {
2694
3915
  const edit = typeof options === 'string' ? {
2695
3916
  content: options
@@ -2718,29 +3939,28 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
2718
3939
  editedTimestamp: Math.max(Date.now(), (held.editedTimestamp ?? 0) + 1)
2719
3940
  };
2720
3941
  }
2721
- /** A mock message showing a held message. */ function messageFrom(held) {
2722
- return createMockMessage({
3942
+ /** A mock message showing a held message, one the bot sent in `channel` by `client`, with what `made` keeps. */ function messageFrom(held, made, channel, client) {
3943
+ return sentMessage(held, channel, client, {
2723
3944
  id: held.id,
2724
- ...held.content !== undefined && {
2725
- content: held.content
2726
- },
2727
- components: held.components,
2728
- embeds: held.embeds,
2729
- ...held.flags !== undefined && {
2730
- flags: held.flags
2731
- },
2732
3945
  editedTimestamp: held.editedTimestamp
2733
- });
3946
+ }, made);
2734
3947
  }
2735
3948
  /**
2736
- * A component or embed as a message holds it: a discord.js instance as it is; anything else as
2737
- * its API JSON at the time of the call, behind `toJSON()`.
2738
- */ function asHeld(value) {
3949
+ * A component or embed as a message holds it: a discord.js instance as it is, anything else as the discord.js class
3950
+ * built from its API JSON at the time of the call, as discord.js builds it, an unknown type as a plain `Component`.
3951
+ */ function asHeld(value, kind) {
2739
3952
  if (value instanceof discord_js.Component || value instanceof discord_js.Embed) return value;
2740
- const json = structuredClone(typeof value.toJSON === 'function' ? value.toJSON() : value);
2741
- return {
2742
- toJSON: ()=>json
2743
- };
3953
+ // A copy, since discord.js's classes keep what they are given, one level deep
3954
+ const json = structuredClone(jsonOf(value));
3955
+ if (kind === 'embed') return new discord_js.Embed(json);
3956
+ // An action row builds each child with discord.js's own createComponent, which it doesn't export
3957
+ const row = new discord_js.ActionRow({
3958
+ type: discord_js.ComponentType.ActionRow,
3959
+ components: [
3960
+ json
3961
+ ]
3962
+ });
3963
+ return row.components[0];
2744
3964
  }
2745
3965
  /**
2746
3966
  * Creates a mock {@link Message} that tracks whether it has been deleted.
@@ -2749,12 +3969,14 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
2749
3969
  * `@MessageHandler` does not skip it.
2750
3970
  *
2751
3971
  * @remarks
2752
- * `delete()`, `edit()`, `reply()`, `react()`, `pin()` and `unpin()` throw once the message is deleted, and `edit()`
2753
- * and `reply()` resolve to a new mock message. Without overrides the message is empty, in a server, from a new
2754
- * person who is its `member`; with `guild: null` it is a direct message. Give `author` to send several messages as
2755
- * one user, such as to reach a per-user cooldown. An author given, and the users, roles and channels the content
2756
- * mentions, are cached as the gateway delivers them. It is typed as discord.js emits a message, never in a group DM,
2757
- * so it fits `messageCreate` and the other message events.
3972
+ * `delete()`, `edit()`, `reply()`, `react()`, `pin()` and `unpin()` throw once the message is deleted. `edit()` changes
3973
+ * this message and resolves to it, and `reply()` resolves to the reply, sent in this message's channel and holding what
3974
+ * was sent, as a channel's `send()` does. Under `useStrictMocks()` a message the bot sends is its `client.user`'s, and
3975
+ * one sent in a DM is in that DM. Without overrides the message is empty, in a server, from a new person who is its
3976
+ * `member`; with `guild: null` it is a direct message. Give `author` to send several messages as one user, such as to
3977
+ * reach a per-user cooldown. An author given, and the users, roles and channels the content mentions, are cached as the
3978
+ * gateway delivers them. It is typed as discord.js emits a message, never in a group DM, so it fits `messageCreate` and
3979
+ * the other message events.
2758
3980
  *
2759
3981
  * @param overrides - The message's content, components and the rest; see {@link MockMessageOverrides}.
2760
3982
  *
@@ -2777,11 +3999,18 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
2777
3999
  * @see {@link MockMessageOverrides}
2778
4000
  * @see {@link createMockGuild}
2779
4001
  */ function createMockMessage(overrides = {}) {
4002
+ return messageMock(overrides);
4003
+ }
4004
+ function messageMock(overrides = {}, sentBy, made = {}, at) {
4005
+ var _made, _made1, _made2;
2780
4006
  const instance = Object.create(discord_js.Message.prototype);
2781
4007
  const stubs = new Map();
4008
+ const unplaced = overrides.channel === undefined && overrides.guild === undefined;
4009
+ // A message the bot sent is placed where it was sent, even where default mode reads a server of its own for a DM
4010
+ if (unplaced && !sentBy) placesMade.add(instance);
2782
4011
  instance.deleted = false;
2783
4012
  // Constructor-assigned — set as prototype-based stubs; a user rather than a bot, as dispatch handles only those
2784
- instance.author = overrides.author ?? mockUser();
4013
+ instance.author = overrides.author ?? (_made = made).author ?? (_made.author = mockUser());
2785
4014
  // Getters on the prototype, which the proxy would answer with a stub object: own properties shadow them
2786
4015
  // A channel given says where the message was sent, unless its guild is given too
2787
4016
  const place = overrides.channel ? placeOf(overrides.channel, overrides.guild === undefined ? undefined : overrides.guild?.id ?? null, ()=>{
@@ -2791,22 +4020,37 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
2791
4020
  guild
2792
4021
  };
2793
4022
  }) : undefined;
2794
- const guild = overrides.guild !== undefined ? overrides.guild : place?.guildId !== undefined ? place.guildId === null ? null : place.guild ?? createMockGuildForMessage(place.guildId) : createMockGuildForMessage();
2795
- const channel = overrides.channel ?? channelFor(guild, guild?.id ?? null, nextSnowflake(), instance.author);
2796
- Object.defineProperty(instance, 'channel', {
4023
+ const guild = overrides.guild !== undefined ? overrides.guild : place?.guildId !== undefined ? place.guildId === null ? null : place.guild ?? createMockGuildForMessage(place.guildId) : unplaced ? (_made1 = made).guild ?? (_made1.guild = createMockGuildForMessage()) : createMockGuildForMessage();
4024
+ const madeChannel = ()=>channelFor(guild, guild?.id ?? null, nextSnowflake(), instance.author);
4025
+ const channel = at ? undefined : overrides.channel ?? (unplaced ? (_made2 = made).channel ?? (_made2.channel = madeChannel()) : madeChannel());
4026
+ if (channel) Object.defineProperty(instance, 'channel', {
2797
4027
  value: channel,
2798
- writable: true
4028
+ writable: true,
4029
+ configurable: true
2799
4030
  });
4031
+ else {
4032
+ Object.defineProperty(instance, 'channel', {
4033
+ get: at.channel,
4034
+ set: (value)=>Object.defineProperty(instance, 'channel', {
4035
+ value,
4036
+ writable: true,
4037
+ configurable: true
4038
+ }),
4039
+ configurable: true
4040
+ });
4041
+ }
2800
4042
  Object.defineProperty(instance, 'guild', {
2801
4043
  value: guild,
2802
- writable: true
4044
+ writable: true,
4045
+ configurable: true
2803
4046
  });
2804
4047
  // The author as a member of the message's server; a direct message has none. A given author's member is the one
2805
4048
  // the server caches, as the gateway resolves it, so every message from that author has the same member
2806
- // The author's member, cached as the gateway caches it with the message; `member` reads the cache, as discord.js does
4049
+ // The author's member, cached as the gateway caches it with the message; `member` reads the cache, as discord.js
4050
+ // does, without calling the manager's resolve a test may stub
2807
4051
  if (guild) memberFor(guild, instance.author, true);
2808
4052
  Object.defineProperty(instance, 'member', {
2809
- get: ()=>guild ? guild.members.resolve(instance.author) : null,
4053
+ get: ()=>instance.guild ? cacheOf(instance.guild.members)?.get(instance.author.id) ?? null : null,
2810
4054
  set: (value)=>Object.defineProperty(instance, 'member', {
2811
4055
  value,
2812
4056
  writable: true,
@@ -2817,8 +4061,8 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
2817
4061
  configurable: true
2818
4062
  });
2819
4063
  // In a server's text channel, the ids matching the objects; with no guild, a DM
2820
- instance.channelId = channel.id;
2821
- instance.guildId = guild?.id ?? null;
4064
+ instance.channelId = channel?.id ?? at.channelId;
4065
+ instance.guildId = at ? at.guildId : guild?.id ?? null;
2822
4066
  // The thread its channel caches under its id, as discord.js reads it; until 5.0, a placeholder thread otherwise
2823
4067
  let placeholderThread;
2824
4068
  Object.defineProperty(instance, 'thread', {
@@ -2848,7 +4092,7 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
2848
4092
  configurable: true
2849
4093
  });
2850
4094
  if (guild instanceof discord_js.Guild) cached(cacheOf(client.guilds), guild.id, ()=>guild);
2851
- cacheChannel(channel, guild, client);
4095
+ if (channel) cacheChannel(channel, guild, client);
2852
4096
  const userCache = cacheOf(client.users);
2853
4097
  for (const user of [
2854
4098
  ...overrides.users ?? [],
@@ -2858,8 +4102,8 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
2858
4102
  instance.mentions = mentionsOf(overrides.content, client, guild);
2859
4103
  // Data a message always has, real rather than stubbed, so code reading it sees an empty message
2860
4104
  instance.flags = new discord_js.MessageFlagsBitField(overrides.flags);
2861
- instance.components = (overrides.components ?? []).map(asHeld);
2862
- instance.embeds = (overrides.embeds ?? []).map(asHeld);
4105
+ instance.components = (overrides.components ?? []).map((component)=>asHeld(component, 'component'));
4106
+ instance.embeds = (overrides.embeds ?? []).map((embed)=>asHeld(embed, 'embed'));
2863
4107
  instance.attachments = new discord_js.Collection();
2864
4108
  instance.editedTimestamp = overrides.editedTimestamp ?? null;
2865
4109
  const generatedId = overrides.id === undefined ? nextSnowflake() : undefined;
@@ -2872,14 +4116,27 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
2872
4116
  if (instance.deleted) throw alreadyDeleted();
2873
4117
  instance.deleted = true;
2874
4118
  }));
2875
- stubs.set('edit', createMockFn(async ()=>{
4119
+ // An edit changes this message, as Discord edits it in place, and resolves to it
4120
+ stubs.set('edit', createMockFn(async (options)=>{
2876
4121
  if (instance.deleted) throw alreadyDeleted();
2877
- return createMockMessage();
4122
+ const held = edited(heldFrom(undefined, false), options);
4123
+ const edit = typeof options === 'string' ? {
4124
+ content: options
4125
+ } : options ?? {};
4126
+ if (edit.content !== undefined) instance.content = held.content;
4127
+ if (edit.components !== undefined) instance.components = held.components.map((component)=>asHeld(component, 'component'));
4128
+ if (edit.embeds !== undefined) instance.embeds = held.embeds.map((embed)=>asHeld(embed, 'embed'));
4129
+ if (edit.flags !== undefined) instance.flags = new discord_js.MessageFlagsBitField(held.flags);
4130
+ instance.editedTimestamp = Math.max(Date.now(), (instance.editedTimestamp ?? 0) + 1);
4131
+ return message;
2878
4132
  }));
2879
- stubs.set('reply', createMockFn(async ()=>{
4133
+ stubs.set('reply', createMockFn(async (options)=>{
2880
4134
  if (instance.deleted) throw alreadyDeleted();
2881
- return createMockMessage();
4135
+ return sentMessage(sent(options, false), instance.channel, instance.client);
2882
4136
  }));
4137
+ // Crossposting publishes this message, and forwarding sends one in the channel it is forwarded to
4138
+ stubs.set('crosspost', createMockFn(async ()=>message));
4139
+ stubs.set('forward', createMockFn(async (channel)=>sentMessage(sent(undefined, false), channel, instance.client)));
2883
4140
  stubs.set('react', createMockFn(async ()=>{
2884
4141
  if (instance.deleted) throw alreadyDeleted();
2885
4142
  }));
@@ -2889,7 +4146,85 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
2889
4146
  stubs.set('unpin', createMockFn(async ()=>{
2890
4147
  if (instance.deleted) throw alreadyDeleted();
2891
4148
  }));
2892
- return stubDeep(instance, stubs);
4149
+ if (sentBy) warnOnSentReads(instance, stubs, sentBy);
4150
+ const message = stubDeep(instance, stubs);
4151
+ return message;
4152
+ }
4153
+ /**
4154
+ * Has a message the bot sent warn once where strict mocks read it otherwise: its author, the bot's `client.user` under
4155
+ * strict mocks, and, for one sent in a DM, the server it reads as being in.
4156
+ */ function warnOnSentReads(instance, stubs, sentBy) {
4157
+ // A read made while reading another of these, as `member` reads `author`, warns only for the outer one
4158
+ let reading = 0;
4159
+ const read = (key, warning)=>{
4160
+ const descriptor = Object.getOwnPropertyDescriptor(instance, key);
4161
+ Object.defineProperty(instance, key, {
4162
+ get: ()=>{
4163
+ if (reading === 0) controller_decorator.warnOnce(mockLogger, warning);
4164
+ reading++;
4165
+ try {
4166
+ return descriptor?.get ? descriptor.get.call(instance) : descriptor?.value;
4167
+ } finally{
4168
+ reading--;
4169
+ }
4170
+ },
4171
+ set: (next)=>Object.defineProperty(instance, key, {
4172
+ value: next,
4173
+ writable: true,
4174
+ enumerable: true,
4175
+ configurable: true
4176
+ }),
4177
+ enumerable: true,
4178
+ configurable: true
4179
+ });
4180
+ };
4181
+ if (sentBy.bot) {
4182
+ read('author', "Message.author reads a user here, though the bot sent this message; in the next major version (5.0) it is the bot's " + "client.user, as Discord sends it. Call useStrictMocks() to read client.user now.");
4183
+ }
4184
+ if (!sentBy.dm) return;
4185
+ const inDm = (what)=>`Message.${what} reads a server here, though this message was sent in a DM; in the next major version (5.0) it reads ` + "as a DM's, with no server. Call useStrictMocks() to read it so now.";
4186
+ const inGuild = Boolean(instance.guildId);
4187
+ for (const key of [
4188
+ 'guild',
4189
+ 'guildId',
4190
+ 'member'
4191
+ ])read(key, inDm(key));
4192
+ stubs.set('inGuild', createMockFn(()=>(controller_decorator.warnOnce(mockLogger, inDm('inGuild()')), inGuild)));
4193
+ }
4194
+ /**
4195
+ * A message the bot sent, as Discord returns it: in the channel it was sent in and that channel's server, holding what
4196
+ * was sent. Under strict mocks it is the bot's `client.user`'s, and one sent in a DM is in that DM; default mode places
4197
+ * a DM send in a server of its own and keeps another author, with a warning when read.
4198
+ */ function sentMessage(held, channel, client, fields = {}, made) {
4199
+ const by = client ?? channel?.client;
4200
+ const inChannel = channel instanceof discord_js.BaseChannel;
4201
+ const dm = inChannel && channel.isDMBased();
4202
+ const strict = strictMocks();
4203
+ const message = messageMock({
4204
+ ...held.content !== undefined && {
4205
+ content: held.content
4206
+ },
4207
+ components: held.components,
4208
+ embeds: held.embeds,
4209
+ ...held.flags !== undefined && {
4210
+ flags: held.flags
4211
+ },
4212
+ ...inChannel && (!dm || strict) && {
4213
+ channel: channel
4214
+ },
4215
+ ...by && {
4216
+ client: by
4217
+ },
4218
+ ...strict && by?.user && {
4219
+ author: by.user
4220
+ },
4221
+ ...fields
4222
+ }, strict ? undefined : {
4223
+ bot: Boolean(by?.user),
4224
+ dm
4225
+ }, made);
4226
+ if (dm && !strict) sentInDm.set(mockTargets.get(message) ?? message, channel);
4227
+ return message;
2893
4228
  }
2894
4229
  /** The option type Discord would have sent for a given JavaScript value. */ function optionTypeOf(value) {
2895
4230
  if (typeof value === 'boolean') return discord_js.ApplicationCommandOptionType.Boolean;
@@ -2978,11 +4313,32 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
2978
4313
  else if (isEntity) option.user = value;
2979
4314
  return option;
2980
4315
  }
4316
+ /**
4317
+ * The option an autocomplete's user is typing, as Discord sends it: its value a string, `''` before anything is typed.
4318
+ * A number given for it is its digits under strict mocks; otherwise it stays a number, with a warning when read.
4319
+ */ function focusedOptionData(name, value) {
4320
+ const option = toOptionData(name, value ?? '');
4321
+ option.focused = true;
4322
+ if (typeof value === 'number') {
4323
+ if (strictMocks()) option.value = String(value);
4324
+ else {
4325
+ Object.defineProperty(option, 'value', {
4326
+ get: ()=>{
4327
+ controller_decorator.warnOnce(mockLogger, `The focused option "${name}" reads the number ${value} here, where Discord sends a string; in the next major version (5.0) ` + `the mock gives it as one. Give it as a string, such as '${value}', or call useStrictMocks() to have the mock give it now.`);
4328
+ return value;
4329
+ },
4330
+ enumerable: true
4331
+ });
4332
+ }
4333
+ }
4334
+ return option;
4335
+ }
2981
4336
  /**
2982
4337
  * Nests the supplied options under the subcommand path they were invoked through,
2983
4338
  * matching the shape Discord sends rather than a flat list.
2984
- */ function buildOptionData(subcommandGroup, subcommand, values, memberOf) {
2985
- const leaves = Object.entries(values).map(([name, value])=>toOptionData(name, value, memberOf));
4339
+ */ function buildOptionData(subcommandGroup, subcommand, values, memberOf, focused) {
4340
+ const leaves = Object.entries(values).map(([name, value])=>name === focused ? focusedOptionData(name, value) : toOptionData(name, value, memberOf));
4341
+ if (focused !== null && !Object.hasOwn(values, focused)) leaves.push(focusedOptionData(focused, null));
2986
4342
  if (subcommand === null) return leaves;
2987
4343
  const sub = {
2988
4344
  name: subcommand,
@@ -3022,6 +4378,10 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3022
4378
  * A whole number is an Integer option and a fraction a Number one, so
3023
4379
  * `getInteger()` reads only a whole number, and `getNumber()` reads either.
3024
4380
  *
4381
+ * The `focused` option reads as Discord sends it, through `getFocused()`, `getFocused(true)` and `data` alike: a
4382
+ * string, `''` when no value is given, and marked `focused` in `data`. A number given for it is its digits under
4383
+ * `useStrictMocks()`; otherwise it stays the number, with a warning when read.
4384
+ *
3025
4385
  * @typeParam Cached - `any` by default, to match `createMockInteraction(ChatInputCommandInteraction)`; pass it when the
3026
4386
  * interaction under test is cache-pinned.
3027
4387
  *
@@ -3142,15 +4502,19 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3142
4502
  ], ()=>false, required));
3143
4503
  base.getFocused = createMockFn((getFull)=>{
3144
4504
  if (focused === null) throw optionError(discord_js.DiscordjsErrorCodes.AutocompleteInteractionOptionNoFocusedOption);
3145
- const option = toOptionData(focused, values[focused] ?? null, memberOf);
4505
+ // The one options.data holds, so the two agree
4506
+ const option = base._hoistedOptions.find((each)=>each.focused);
3146
4507
  return getFull === true ? {
3147
- ...option,
3148
- focused: true
4508
+ ...option
3149
4509
  } : option.value;
3150
4510
  });
3151
4511
  // `data` is what the framework reads to build a handler's params, so it is materialised here rather than
3152
4512
  // auto-stubbed, or every params assertion would see an empty record.
3153
- base.data = buildOptionData(subcommandGroup, subcommand, values, memberOf);
4513
+ base.data = buildOptionData(subcommandGroup, subcommand, values, memberOf, focused);
4514
+ // The fields discord.js's constructor sets from it, which its toString() reads
4515
+ base._group = subcommandGroup;
4516
+ base._subcommand = subcommand;
4517
+ base._hoistedOptions = subcommand === null ? base.data : (subcommandGroup === null ? base.data[0] : base.data[0].options[0]).options;
3154
4518
  const resolver = stubDeep(base);
3155
4519
  return resolver;
3156
4520
  }
@@ -3224,7 +4588,7 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3224
4588
  await this.resolving;
3225
4589
  if (options.ready) {
3226
4590
  const { client = createMockClient(), primary = true } = options.ready === true ? {} : options.ready;
3227
- this.readying ?? (this.readying = this.runReady(client, primary));
4591
+ this.readying ?? (this.readying = this.readyPass.run(()=>this.runReady(client, primary)));
3228
4592
  await this.readying;
3229
4593
  }
3230
4594
  return this;
@@ -3239,12 +4603,21 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3239
4603
  * It first gives up the theme read outside calls, if `init({ ready: true })` made it this module's;
3240
4604
  * reads outside calls then return MeoCord's defaults until another module or app is ready.
3241
4605
  *
4606
+ * A stopped bot runs nothing more. Once `close()` is called, a module still runs what `dispatch`, `invoke` and `emit`
4607
+ * are given, against services shut down, or shutting down, and warns once that it is closed, so a `close()` in the
4608
+ * wrong place shows.
4609
+ *
3242
4610
  * The cooldown store and what it injects shut down last, in the same sequence as the bot's. When any of them has an
3243
4611
  * `onShutdown`, the calls `invoke`, `dispatch` and `emit` have under way finish first, then the store operations
3244
4612
  * they started, so the store's last writes still reach it. The module stops waiting for the whole sequence after
3245
4613
  * its `shutdownTimeout`, 10 seconds unless set, and logs that it did. Give a test whose fake store never answers, or
3246
4614
  * whose `onShutdown` never settles, a short `shutdownTimeout`.
3247
4615
  *
4616
+ * A `close()` while `init({ ready: true })` runs the `onReady` hooks waits for them first, within the same
4617
+ * `shutdownTimeout`. An `onReady` still running once only the hooks' share of it is left is named in a warning, and
4618
+ * its class is shut down all the same, as everything the module constructed is. One that calls `close()` itself isn't
4619
+ * waited for.
4620
+ *
3248
4621
  * @returns Once every hook has run. Rejects with the error of a hook that failed, or an
3249
4622
  * `AggregateError` naming each when several did.
3250
4623
  *
@@ -3259,14 +4632,18 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3259
4632
  * expect(module.get(POOL).ended).toBe(true)
3260
4633
  * ```
3261
4634
  */ async close() {
4635
+ // An onReady awaiting close() would otherwise hold up the close that waits for it
4636
+ this.readyPass.excuseCurrent();
3262
4637
  this.closing ?? (this.closing = (async ()=>{
3263
- // A close during init waits for the hooks it started, so it shuts down whatever they constructed
3264
- await this.readying?.catch(()=>undefined);
3265
- const entries = this.lifecycle.filter((unit)=>this.constructed.has(unit.token))// Already made, so this returns the instance; a provided value may be anything, null included
3266
- .map((unit)=>startupRoots.lifecycleEntry(unit, this.container.get(unit.token) ?? {}));
4638
+ // A close during init waits for the providers it started, and the sequence for its onReady hooks, so it shuts
4639
+ // down whatever they constructed
4640
+ await this.resolving?.catch(()=>undefined);
4641
+ const entries = ()=>this.lifecycle.filter((unit)=>this.constructed.has(unit.token))// Already made, so this returns the instance; a provided value may be anything, null included
4642
+ .map((unit)=>startupRoots.lifecycleEntry(unit, this.container.get(unit.token) ?? {}));
3267
4643
  const failures = [];
3268
4644
  await startupRoots.runShutdownSequence(this.container, entries, {
3269
4645
  drainCalls: ()=>startupRoots.callsSettled(this.calls),
4646
+ readyPass: this.readyPass,
3270
4647
  timeoutMs: this.shutdownTimeout,
3271
4648
  hookFailed: (name, error)=>failures.push({
3272
4649
  name,
@@ -3280,6 +4657,11 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3280
4657
  })());
3281
4658
  await this.closing;
3282
4659
  }
4660
+ /** Warns once, after `close()`, that a call runs against services already shut down. */ warnIfClosed() {
4661
+ if (this.closing === undefined || this.warnedClosed) return;
4662
+ this.warnedClosed = true;
4663
+ new controller_decorator.Logger('TestingModule').warn('This testing module is closed, and its services are shut down, or shutting down, so dispatch, invoke and emit run ' + 'against that state. Close it after the last call, in afterEach.');
4664
+ }
3283
4665
  async runReady(client, primary) {
3284
4666
  // As the bot claims it before it comes online, so onReady reads the app's theme
3285
4667
  providers.claimAmbientAppTheme(this.container);
@@ -3295,6 +4677,8 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3295
4677
  }, [], {
3296
4678
  resolveFailed: failed,
3297
4679
  hookFailed: failed
4680
+ }, {
4681
+ runOnReady: this.readyPass.runOnReady
3298
4682
  });
3299
4683
  throwFailures('onReady', failures);
3300
4684
  }
@@ -3366,7 +4750,9 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3366
4750
  * @returns Whether the handler ran, and the error a filter handled, if any. Rejects with an error no
3367
4751
  * filter handles, or with the error a filter throws: the built-in fallback, which answers such
3368
4752
  * errors in the bot, does not run here. Rejects before running anything with an interaction or a
3369
- * message the handler's route does not match, naming both.
4753
+ * message the handler's route does not match, naming both. Given fewer arguments than the handler
4754
+ * declares, with a first that is neither an interaction nor a message, such as a reaction without its
4755
+ * `ReactionEvent`, it rejects under `useStrictMocks()`, and otherwise warns once and runs the handler.
3370
4756
  *
3371
4757
  * @example
3372
4758
  * ```ts
@@ -3388,6 +4774,7 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3388
4774
  * expect(outcome.error).toBeInstanceOf(GuardDeniedError)
3389
4775
  * ```
3390
4776
  */ async invoke(controller, methodName, ...args) {
4777
+ this.warnIfClosed();
3391
4778
  await this.init();
3392
4779
  if (!this.controllers.includes(controller)) {
3393
4780
  throw new Error(`${controller.name} is not a controller of this testing module. Add it to \`controllers\`.`);
@@ -3417,6 +4804,7 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3417
4804
  hooks = providers.messageCommandHooks(route, params, first, start, given, this.messageOptions.types);
3418
4805
  }
3419
4806
  }
4807
+ this.checkArgumentCount(controller, methodName, instance[methodName], first, callArgs.length);
3420
4808
  const presenter = providers.appPresenterOf(this.container);
3421
4809
  // A message command's errors are drawn by the app's presenter too, as an interaction's are
3422
4810
  const client = first instanceof discord_js.BaseInteraction || first instanceof discord_js.Message ? first.client : undefined;
@@ -3433,6 +4821,19 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3433
4821
  error
3434
4822
  };
3435
4823
  }
4824
+ /**
4825
+ * Refuses under `useStrictMocks()`, or warns once otherwise, a call that gives a handler fewer arguments than it
4826
+ * declares when `invoke` builds none for it, as for a reaction given without its `ReactionEvent`.
4827
+ */ checkArgumentCount(controller, methodName, method, first, given) {
4828
+ if (first instanceof discord_js.BaseInteraction || first instanceof discord_js.Message || method.length <= given) return;
4829
+ const reaction = controller_decorator.getReactionHandlers(controller.prototype).some((handler)=>handler.method === methodName);
4830
+ const problem = `${controller.name}.${methodName} takes ${method.length} arguments, and invoke was given ${given}: ` + (reaction ? 'pass its ReactionEvent after the reaction, as { user, action }.' : 'pass each argument it takes.');
4831
+ if (strictMocks()) throw new Error(problem);
4832
+ const key = `${controller.name}.${methodName}`;
4833
+ if (this.warnedArgumentCounts.has(key)) return;
4834
+ this.warnedArgumentCounts.add(key);
4835
+ new controller_decorator.Logger('TestingModule').warn(problem);
4836
+ }
3436
4837
  /** Counts `call` as under way until it settles. */ track(call) {
3437
4838
  this.calls.add(call);
3438
4839
  void call.then(()=>this.calls.delete(call), ()=>this.calls.delete(call));
@@ -3478,6 +4879,7 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3478
4879
  return this.dispatcher;
3479
4880
  }
3480
4881
  async dispatch(input, options) {
4882
+ this.warnIfClosed();
3481
4883
  await this.init();
3482
4884
  this.registerClient(input);
3483
4885
  const dispatcher = this.dispatcherOf();
@@ -3495,6 +4897,7 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3495
4897
  unhandled: (error)=>unhandled.push(error)
3496
4898
  };
3497
4899
  if (options) {
4900
+ noteReactionDispatch(input);
3498
4901
  await this.track(dispatcher.reaction(input, {
3499
4902
  user: options.user,
3500
4903
  action: options.action ?? enum_index.ReactionHandlerAction.ADD
@@ -3524,15 +4927,20 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3524
4927
  };
3525
4928
  }
3526
4929
  /**
3527
- * Emits a client event to the module's `@On` and `@Once` handlers, through the same pipeline the app
3528
- * runs them in: the global guards of the module's `app`, then each handler's own. Handlers on the
3529
- * module's controllers, class providers and their dependencies all receive it. A `@Once` handler
3530
- * handles only the first event, as it would on a client.
4930
+ * Emits a client event to the module's `@On` and `@Once` handlers, through the pipeline the app runs
4931
+ * them in up to its event fallback: the global guards of the module's `app`, then each handler's own.
4932
+ * Handlers on the module's controllers, class providers and their dependencies all receive it. A
4933
+ * `@Once` handler handles only the first event, as it would on a client.
4934
+ *
4935
+ * The bot's event fallback doesn't run here. On a bot, a guard's `GuardDeniedError` is logged and the
4936
+ * event skipped, and a handler's `UserError` is logged and, when the event carries a message, answered
4937
+ * as a reply to it; here both reject `emit` as any error does, so a test checks them with `rejects`.
3531
4938
  *
3532
4939
  * @param event - The client event, such as `'guildMemberAdd'`.
3533
4940
  * @param args - The event's arguments, typed from discord.js's `ClientEvents`.
3534
- * @returns How many handlers ran. Rejects once every handler has settled if any threw: with that
3535
- * error when one handler failed, or an `AggregateError` of them when several did.
4941
+ * @returns How many handlers ran. Rejects once every handler has settled if any threw, a guard's
4942
+ * `GuardDeniedError` and a handler's `UserError` included: with that error when one handler failed,
4943
+ * or an `AggregateError` of them when several did.
3536
4944
  *
3537
4945
  * @example
3538
4946
  * ```ts
@@ -3544,6 +4952,7 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3544
4952
  * expect(ran).toBe(1)
3545
4953
  * ```
3546
4954
  */ async emit(event, ...args) {
4955
+ this.warnIfClosed();
3547
4956
  await this.init();
3548
4957
  const calls = [];
3549
4958
  for (const cls of this.eventClasses){
@@ -3588,7 +4997,10 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3588
4997
  this.appWarnUnanswered = appWarnUnanswered;
3589
4998
  this.services = services;
3590
4999
  this.shutdownTimeout = shutdownTimeout;
5000
+ /** The `onReady` hooks' pass, which `close()` waits for, less one whose `onReady` called `close()`. */ this.readyPass = new startupRoots.ReadyPass();
5001
+ this.warnedClosed = false;
3591
5002
  /** The `@Once` handlers that have already handled their event, as a client forgets its once listeners. */ this.firedOnce = new Set();
5003
+ /** The handlers already warned about for being given fewer arguments than they take, so each is named once. */ this.warnedArgumentCounts = new Set();
3592
5004
  /** The calls under way, each removed once it settles: what `close()` lets finish before the cooldown store stops. */ this.calls = new Set();
3593
5005
  }
3594
5006
  }
@@ -3653,6 +5065,10 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3653
5065
  /**
3654
5066
  * Replaces a guard with a stub wherever it applies, globally or on a controller or handler.
3655
5067
  *
5068
+ * It stands in for the guard wherever the guard is bound, a provider of the same token included, and wins over an
5069
+ * `overrideProvider` of it. The stub needs a `canActivate`, its own or its prototype's: `compile()` refuses one without
5070
+ * it under `useStrictMocks()`, and otherwise warns.
5071
+ *
3656
5072
  * @param guard - The guard class to replace.
3657
5073
  * @example
3658
5074
  * ```ts
@@ -3670,6 +5086,10 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3670
5086
  * Replaces an interceptor with a stub wherever it applies, globally or on a controller or handler.
3671
5087
  * The stub's `intercept` receives the context and `next`; call `next.handle()` to run the handler.
3672
5088
  *
5089
+ * It stands in for the interceptor wherever it is bound, a provider of the same token included, and wins over an
5090
+ * `overrideProvider` of it. A stub without `intercept` is refused by `compile()` under `useStrictMocks()`, and
5091
+ * otherwise warned about.
5092
+ *
3673
5093
  * @param interceptor - The interceptor class to replace.
3674
5094
  * @example
3675
5095
  * ```ts
@@ -3687,6 +5107,10 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3687
5107
  * Replaces an exception filter with a stub wherever it applies. The filter's `@Catch` still decides
3688
5108
  * which errors reach the stub.
3689
5109
  *
5110
+ * It stands in for the filter wherever it is bound, a provider of the same token included, and wins over an
5111
+ * `overrideProvider` of it. A stub without `catch` is refused by `compile()` under `useStrictMocks()`, and otherwise
5112
+ * warned about.
5113
+ *
3690
5114
  * @param filter - The filter class to replace.
3691
5115
  * @example
3692
5116
  * ```ts
@@ -3819,9 +5243,52 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3819
5243
  const providers$1 = providers.providerMap(this.wiring?.providers ?? [], '@MeoCord({ providers })');
3820
5244
  for (const [token, provider] of providers.providerMap(this.options.providers ?? [], "the testing module's providers"))providers$1.set(token, provider);
3821
5245
  for (const [token, override] of this.overrides)providers$1.set(token, override);
5246
+ // A stage override stands in for its class wherever the class is bound. A stage also provided takes the stub in
5247
+ // place of its provider, one binding per token, and runs lifecycle hooks as that provider would; any other is
5248
+ // bound on its own below, as the bot never makes a lifecycle unit of it. A stub that can't run is named here
5249
+ const stageStubs = new Map();
5250
+ for (const [method, overrides, kind] of [
5251
+ [
5252
+ 'canActivate',
5253
+ this.guardOverrides,
5254
+ 'overrideGuard'
5255
+ ],
5256
+ [
5257
+ 'intercept',
5258
+ this.interceptorOverrides,
5259
+ 'overrideInterceptor'
5260
+ ],
5261
+ [
5262
+ 'catch',
5263
+ this.filterOverrides,
5264
+ 'overrideFilter'
5265
+ ]
5266
+ ]){
5267
+ for (const [stage, stub] of overrides){
5268
+ if (providers$1.has(stage)) providers$1.set(stage, {
5269
+ provide: stage,
5270
+ useValue: stub
5271
+ });
5272
+ else stageStubs.set(stage, stub);
5273
+ if (typeof stub[method] === 'function') continue;
5274
+ const problem = `${kind}(${stage.name}).useValue(…) has no ${method} method.`;
5275
+ if (strictMocks()) controller_decorator.startupError(new Error(problem));
5276
+ else controller_decorator.warnOnce(new controller_decorator.Logger('TestingModule'), `${problem} A call that reaches ${stage.name} fails; useStrictMocks() refuses it here.`);
5277
+ }
5278
+ }
3822
5279
  // Each pass of checks ends before the next step that is no check, so a module with one error stops where it always did
3823
5280
  controller_decorator.stopOnStartupErrors();
5281
+ // What the walk below takes as classes, reported with what the walk finds; an entry refused here is left out of it
3824
5282
  const services = this.wiring?.services ?? [];
5283
+ if (this.options.app) providers.assertProvidedServices(this.options.app.name, services, providers$1, "@MeoCord({ providers }) or the testing module's providers");
5284
+ for (const controller of this.options.controllers ?? []){
5285
+ if (!controller_decorator.isConstructor(controller)) controller_decorator.startupError(new Error(`the testing module's controllers takes classes, not ${controller_decorator.describeValue(controller)}.`));
5286
+ }
5287
+ const observers = [
5288
+ ...this.options.app ? providers.appObservers(this.options.app) : [],
5289
+ ...this.options.observers ?? []
5290
+ ];
5291
+ providers.assertObservers("the testing module's observers", observers);
3825
5292
  const appOptions = this.options.app ? Reflect.getMetadata(controller_decorator.META.appOptions, this.options.app) : undefined;
3826
5293
  // The app's own store, as the bot binds it, whether the module is given the app or built from it; none when the
3827
5294
  // test provides the CooldownStore, so nothing builds the app's or asks for what it injects
@@ -3830,18 +5297,16 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3830
5297
  const themeResolver = controller_decorator.themeResolverClass(stages?.themeFor?.resolvers);
3831
5298
  // Every class the module runs, as the app finds them, except stage classes an override stands in for
3832
5299
  const i18n = this.options.app && Reflect.getMetadata(controller_decorator.META.appOptions, this.options.app)?.i18n;
5300
+ const appPresenterClass = this.options.app && Reflect.getMetadata(controller_decorator.META.appOptions, this.options.app)?.presenter;
3833
5301
  const startup = startupRoots.startupClasses({
3834
- controllers: this.options.controllers ?? [],
5302
+ controllers: (this.options.controllers ?? []).filter(controller_decorator.isConstructor),
3835
5303
  services,
3836
5304
  providers: providers$1,
3837
5305
  stages,
3838
- presenter: this.options.app && Reflect.getMetadata(controller_decorator.META.appOptions, this.options.app)?.presenter,
5306
+ presenter: appPresenterClass,
3839
5307
  cooldownStore: store,
3840
5308
  themeFor: stages?.themeFor?.resolvers,
3841
- observers: [
3842
- ...this.options.app ? providers.appObservers(this.options.app) : [],
3843
- ...this.options.observers ?? []
3844
- ],
5309
+ observers,
3845
5310
  stubs: [
3846
5311
  ...this.guardOverrides.keys(),
3847
5312
  ...this.interceptorOverrides.keys(),
@@ -3866,16 +5331,8 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3866
5331
  throw new Error(`${needClient.map((cls)=>cls.name).join(', ')} ${needClient.length === 1 ? 'injects' : 'inject'} the Discord Client, ` + 'which a testing module does not make: give one in its providers, such as ' + '{ provide: Client, useValue: createMockClient() }.');
3867
5332
  });
3868
5333
  }
3869
- // Bind guard overrides — prevents inversify from auto-wiring guard dependencies
3870
- for (const [guardClass, stub] of this.guardOverrides){
3871
- container.bind(guardClass).toConstantValue(stub);
3872
- }
3873
- for (const [filterClass, stub] of this.filterOverrides){
3874
- container.bind(filterClass).toConstantValue(stub);
3875
- }
3876
- for (const [interceptorClass, stub] of this.interceptorOverrides){
3877
- container.bind(interceptorClass).toConstantValue(stub);
3878
- }
5334
+ // A stage override nothing provides, bound before the classes so none of them binds the stage itself
5335
+ for (const [stage, stub] of stageStubs)container.bind(stage).toConstantValue(stub);
3879
5336
  // The app's cooldown policy, so a test of a failing store sees what the bot would do
3880
5337
  if (appOptions && !container.isBound(providers.COOLDOWN_POLICY)) container.bind(providers.COOLDOWN_POLICY).toConstantValue(providers.cooldownPolicyFrom(appOptions));
3881
5338
  // The app's translator, unless a provider stands in for it
@@ -3939,16 +5396,12 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3939
5396
  // As the app does when it is created, so a test sees the refusal the bot would give
3940
5397
  startupRoots.assertDistinctCommands(this.options.controllers ?? []);
3941
5398
  if (this.options.app) providers.prepareAppPresenter(container, this.options.app);
3942
- const observers = [
3943
- ...this.options.app ? providers.appObservers(this.options.app) : [],
3944
- ...this.options.observers ?? []
3945
- ];
3946
- providers.assertObservers("the testing module's observers", observers);
3947
- // Only @Observer classes are bound as observers
3948
- controller_decorator.stopOnStartupErrors();
3949
5399
  providers.bindObservers(container, observers);
3950
5400
  // The store calls ask: the test's own when it provides CooldownStore, else the app's
3951
5401
  const boundStore = store ?? (providers$1.has(controller_decorator.CooldownStore) ? controller_decorator.CooldownStore : undefined);
5402
+ // What the stages and presenter inject, as the bot makes it; a stage the test replaced contributes nothing
5403
+ const stageServices = startupRoots.stageDependencies(startup, providers$1, appPresenterClass);
5404
+ for (const service of stageServices)bindClass(service);
3952
5405
  // The order the app runs lifecycle hooks in: its cooldown store, then providers, controllers and observers, each
3953
5406
  // after what it injects
3954
5407
  const lifecycle = providers.resolutionOrder(container, providers$1, [
@@ -3957,6 +5410,7 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3957
5410
  ] : [],
3958
5411
  ...providers$1.keys(),
3959
5412
  ...services,
5413
+ ...stageServices,
3960
5414
  ...themeResolver ? [
3961
5415
  themeResolver
3962
5416
  ] : [],
@@ -3977,6 +5431,11 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
3977
5431
  ...this.options.controllers ?? [],
3978
5432
  ...startup.classes
3979
5433
  ])Reflect.defineMetadata(controller_decorator.META.container, container, cls);
5434
+ // And each instance with the module that made it, as another module made from the same classes stamps them again
5435
+ providers.recordInstanceContainers(container, [
5436
+ ...this.options.controllers ?? [],
5437
+ ...startup.classes
5438
+ ]);
3980
5439
  providers.prepareThemes(container, appClasses);
3981
5440
  // A handler with no builder is how a fixture is written, so only what is always a mistake is named
3982
5441
  startupRoots.warnUnregisteredCommands(this.options.controllers ?? [], {
@@ -4050,7 +5509,15 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
4050
5509
  * @param options - The controllers, providers, observers and app the module is built from.
4051
5510
  * @returns The builder, to override what the test replaces before `compile()`.
4052
5511
  */ static create(options) {
4053
- return new TestingModuleBuilder(options);
5512
+ // A controller listed twice is one controller, as the bot and fromApp take it
5513
+ return new TestingModuleBuilder({
5514
+ ...options,
5515
+ ...options.controllers && {
5516
+ controllers: [
5517
+ ...new Set(options.controllers)
5518
+ ]
5519
+ }
5520
+ });
4054
5521
  }
4055
5522
  /**
4056
5523
  * Starts building a testing module from a whole `@MeoCord` app, wired as the bot wires it.
@@ -4068,7 +5535,8 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
4068
5535
  * @param app - The class `@MeoCord` decorates.
4069
5536
  * @param options - Providers that replace the app's by token, extra controllers, and observers.
4070
5537
  * @returns The builder, whose `override*` methods still apply, for `compile()`.
4071
- * @throws TypeError when `app` is not a `@MeoCord` class.
5538
+ * @throws TypeError when `app` is not a `@MeoCord` class. After {@link reportAllStartupErrors}, an app whose
5539
+ * `@MeoCord` was refused throws the error it was refused for instead.
4072
5540
  *
4073
5541
  * @example
4074
5542
  * ```ts
@@ -4109,7 +5577,13 @@ const everyoneRole = (guildId)=>createMockInteraction(discord_js.Role, {
4109
5577
  * ```
4110
5578
  */ static fromApp(app, options = {}) {
4111
5579
  const appOptions = Reflect.getMetadata(controller_decorator.META.appOptions, app);
4112
- if (!appOptions) throw new TypeError(`${app?.name ?? String(app)} is not a @MeoCord app: fromApp takes the class @MeoCord decorates.`);
5580
+ if (!appOptions) {
5581
+ // A refused @MeoCord stores no options, so its own errors, kept with startupErrors: 'all', say why
5582
+ controller_decorator.collectStartupErrors(()=>controller_decorator.declaredErrorsOf([
5583
+ app
5584
+ ]).forEach(controller_decorator.startupError));
5585
+ throw new TypeError(`${app?.name ?? String(app)} is not a @MeoCord app: fromApp takes the class @MeoCord decorates.`);
5586
+ }
4113
5587
  const controllers = [
4114
5588
  ...new Set([
4115
5589
  ...appOptions.controllers,
@@ -4357,7 +5831,6 @@ providers.adviseInPlaceOfInjecting(TestingModule, 'make it with MeoCordTestingMo
4357
5831
  return new Fields(components);
4358
5832
  }
4359
5833
 
4360
- const isPlural = (value)=>typeof value === 'object' && value !== null && typeof value.other === 'string';
4361
5834
  /** Every message key of a catalog, and whether it is a plural. */ function leaves(catalog, prefix = '') {
4362
5835
  return Object.entries(catalog).flatMap(([name, value])=>{
4363
5836
  const key = `${prefix}${name}`;
@@ -4367,7 +5840,7 @@ const isPlural = (value)=>typeof value === 'object' && value !== null && typeof
4367
5840
  plural: false
4368
5841
  }
4369
5842
  ];
4370
- if (isPlural(value)) return [
5843
+ if (controller_decorator.isPluralShape(value)) return [
4371
5844
  {
4372
5845
  key,
4373
5846
  plural: true
@@ -4376,8 +5849,9 @@ const isPlural = (value)=>typeof value === 'object' && value !== null && typeof
4376
5849
  return leaves(value, `${key}.`);
4377
5850
  });
4378
5851
  }
4379
- function at(catalog, key) {
4380
- return key.split('.').reduce((current, part)=>current?.[part], catalog);
5852
+ /** A translation's text, which an empty string isn't: the translator reads `''` as missing, as tools export one untranslated. */ const isText = (value)=>typeof value === 'string' && value !== '';
5853
+ /** The value at a key's path, through own properties only, so `constructor` or `toString` is not read off a prototype. */ function at(catalog, key) {
5854
+ return key.split('.').reduce((current, part)=>typeof current === 'object' && current !== null && Object.hasOwn(current, part) ? current[part] : undefined, catalog);
4381
5855
  }
4382
5856
  const MEOCORD_KEYS = new Set(leaves(controller_decorator.MEOCORD_MESSAGES).map(({ key })=>key));
4383
5857
  const isMeoCordKey = (key)=>key.startsWith('meocord.');
@@ -4390,7 +5864,7 @@ const isMeoCordKey = (key)=>key.startsWith('meocord.');
4390
5864
  /** A message as a report shows it: the text, or a plural's `other` form. */ const shown = (message)=>typeof message === 'string' ? message : String(message.other);
4391
5865
  /** The `{params}` a translation uses that the message it translates doesn't take; a plural's `{count}` is taken. */ function strayParams(translation, original) {
4392
5866
  const taken = placeholders(original);
4393
- if (isPlural(original)) taken.add('count');
5867
+ if (controller_decorator.isPluralShape(original)) taken.add('count');
4394
5868
  return [
4395
5869
  ...placeholders(translation)
4396
5870
  ].filter((name)=>!taken.has(name));
@@ -4400,9 +5874,11 @@ const isMeoCordKey = (key)=>key.startsWith('meocord.');
4400
5874
  *
4401
5875
  * Use it in a test for a team that wants no fallback in production. It reports the messages a locale lacks, the
4402
5876
  * messages the default catalog doesn't have, the plural forms a language needs but a plural lacks, such as `few` for
4403
- * Russian, and each `{param}` a translation uses that the message it translates doesn't take. It reads the catalogs'
4404
- * own strings, so it checks the params of a catalog from a plain variable or a JSON file too, which the compiler
4405
- * can't.
5877
+ * Russian, a translation of another shape than the default's, a text for a plural or a plural for a text, and each
5878
+ * `{param}` a translation uses that the message it translates doesn't take. An empty translation, `''`, counts as
5879
+ * lacking, as the translator reads it as missing. It reads the catalogs' own strings, so it checks the params of a
5880
+ * catalog from a plain variable or a JSON file too, which the compiler can't. An object counts as a plural only when
5881
+ * its every key is a plural category, so a group with an `other` key is a group.
4406
5882
  *
4407
5883
  * @remarks
4408
5884
  * MeoCord's own texts, a catalog's `meocord` group, fall back to English by design, so only a key MeoCord lacks, and
@@ -4449,12 +5925,17 @@ const isMeoCordKey = (key)=>key.startsWith('meocord.');
4449
5925
  const categories = new Intl.PluralRules(locale).resolvedOptions().pluralCategories;
4450
5926
  for (const { key, plural } of reference){
4451
5927
  const message = at(catalog, key);
4452
- if (message === undefined) {
5928
+ if (message === undefined || message === '') {
4453
5929
  problems.push(`missing ${key}`);
4454
5930
  continue;
4455
5931
  }
4456
- if (plural && isPlural(message)) {
4457
- const missing = categories.filter((category)=>typeof message[category] !== 'string');
5932
+ // A translation of another shape than the default's can't be read as it: a text for a plural, or the reverse
5933
+ if (plural !== controller_decorator.isPluralShape(message) || !plural && typeof message !== 'string') {
5934
+ problems.push(`${key} should be a ${plural ? 'plural' : 'text'}, as the default is`);
5935
+ continue;
5936
+ }
5937
+ if (plural) {
5938
+ const missing = categories.filter((category)=>!isText(message[category]));
4458
5939
  if (missing.length > 0) problems.push(`${key} lacks ${missing.join(', ')}`);
4459
5940
  }
4460
5941
  const original = at(defaultCatalog, key);
@@ -4462,7 +5943,7 @@ const isMeoCordKey = (key)=>key.startsWith('meocord.');
4462
5943
  for (const name of stray)problems.push(`${key} takes no {${name}}; the default is "${shown(original)}"`);
4463
5944
  }
4464
5945
  if (options.meocord && !locale.startsWith('en-')) {
4465
- for (const key of MEOCORD_KEYS)if (at(catalog, key) === undefined) problems.push(`missing ${key}`);
5946
+ for (const key of MEOCORD_KEYS)if (at(catalog, key) === undefined || at(catalog, key) === '') problems.push(`missing ${key}`);
4466
5947
  }
4467
5948
  for (const { key } of leaves(catalog)){
4468
5949
  if (isMeoCordKey(key)) {
@@ -4490,8 +5971,9 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
4490
5971
  *
4491
5972
  * @remarks
4492
5973
  * The cases cover what a shared store most often gets wrong: a sliding window rather than fixed buckets,
4493
- * `retryAfterMs` from the oldest call still in the window, calls in the same millisecond kept apart, and concurrent
4494
- * calls at the limit where exactly one may pass. They use real time, with windows short enough that the suite takes
5974
+ * `retryAfterMs` until a use frees up, from the oldest of the newest `uses` calls in the window, calls in the same
5975
+ * millisecond kept apart, concurrent calls at the limit where exactly one may pass, a batch refusal that names the
5976
+ * longest wait, and a release that frees only the call it gave back. They use real time, with windows short enough that the suite takes
4495
5977
  * a few seconds and margins wide enough that a correct store passes when timers fire late, as on a busy CI runner,
4496
5978
  * and each case counts under keys of its own. Each window is a whole number of milliseconds, as
4497
5979
  * `@Cooldown` gives a store: it rounds its `seconds` to the millisecond.
@@ -4566,6 +6048,30 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
4566
6048
  expect(retryAfterMs).toBeLessThanOrEqual(limit.windowMs - (secondSent - firstDone) + CLOCK_SLACK_MS);
4567
6049
  expect(retryAfterMs).toBeGreaterThanOrEqual(limit.windowMs - (thirdDone - firstSent) - CLOCK_SLACK_MS);
4568
6050
  });
6051
+ // @Cooldown keeps the calls counted so far when a limit is lowered, so a key can hold more calls than `uses`
6052
+ test('counts retryAfterMs from the oldest of the newest `uses` calls when a lowered limit leaves more in the window', async (store)=>{
6053
+ const windowMs = 2000;
6054
+ for(let call = 0; call < 3; call += 1)await store.consume(key('lowered'), {
6055
+ uses: 5,
6056
+ windowMs
6057
+ });
6058
+ await sleep(1000);
6059
+ const newerSent = Date.now();
6060
+ for(let call = 0; call < 2; call += 1)await store.consume(key('lowered'), {
6061
+ uses: 5,
6062
+ windowMs
6063
+ });
6064
+ const { allowed, retryAfterMs } = await store.consume(key('lowered'), {
6065
+ uses: 2,
6066
+ windowMs
6067
+ });
6068
+ const refusedDone = Date.now();
6069
+ expect(allowed).toBe(false);
6070
+ // A use frees up when the older of the two newest calls leaves the window; counted from the oldest call, it would
6071
+ // be a second sooner
6072
+ expect(retryAfterMs).toBeGreaterThanOrEqual(windowMs - (refusedDone - newerSent) - CLOCK_SLACK_MS);
6073
+ expect(retryAfterMs).toBeLessThanOrEqual(windowMs + CLOCK_SLACK_MS);
6074
+ });
4569
6075
  test('counts each key on its own', async (store)=>{
4570
6076
  const limit = {
4571
6077
  uses: 1,
@@ -4627,6 +6133,41 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
4627
6133
  expect(refused.retryAfterMs).toBeGreaterThan(short.windowMs);
4628
6134
  expect(refused.retryAfterMs).toBeLessThanOrEqual(long.windowMs);
4629
6135
  });
6136
+ // A refusal names the wait until every cooldown of the batch allows the call; the default consumeMany stops at the first
6137
+ test('names the longest wait when several cooldowns refuse a batch, or with the default consumeMany, the first', async (store)=>{
6138
+ const short = {
6139
+ uses: 1,
6140
+ windowMs: 1000
6141
+ };
6142
+ const long = {
6143
+ uses: 1,
6144
+ windowMs: 2000
6145
+ };
6146
+ const batch = [
6147
+ {
6148
+ key: key('both-short'),
6149
+ limit: short
6150
+ },
6151
+ {
6152
+ key: key('both-long'),
6153
+ limit: long
6154
+ }
6155
+ ];
6156
+ await store.consume(batch[0].key, short);
6157
+ await store.consume(batch[1].key, long);
6158
+ const refused = await store.consumeMany(batch);
6159
+ expect(refused.allowed).toBe(false);
6160
+ const [blocked, limit] = overridesConsumeMany(store) ? [
6161
+ 1,
6162
+ long
6163
+ ] : [
6164
+ 0,
6165
+ short
6166
+ ];
6167
+ expect(refused.blocked).toBe(blocked);
6168
+ expect(refused.retryAfterMs).toBeGreaterThan(limit.windowMs - 1000);
6169
+ expect(refused.retryAfterMs).toBeLessThanOrEqual(limit.windowMs);
6170
+ });
4630
6171
  // The default consumeMany counts in order and stops at the first refusal, so the entries before it count
4631
6172
  test('records nothing when one cooldown of a batch refuses it, or with the default consumeMany, counts those before it', async (store)=>{
4632
6173
  const limit = {
@@ -4669,10 +6210,11 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
4669
6210
  }, ()=>store.consumeMany(batch)));
4670
6211
  expect(verdicts.filter((verdict)=>verdict.allowed).length).toBe(1);
4671
6212
  });
4672
- // A store without release keeps a counted call counted, as the default consumeMany does
4673
- test('releases a call it counted when the verdict gives release, or keeps it counted without one', async (store)=>{
6213
+ // A store without release keeps a counted call counted, as the default consumeMany does. Two uses tell a release of
6214
+ // the one call apart from one that frees the key's other calls too
6215
+ test('releases only the call it counted when the verdict gives release, or keeps it counted without one', async (store)=>{
4674
6216
  const limit = {
4675
- uses: 1,
6217
+ uses: 2,
4676
6218
  windowMs: 2000
4677
6219
  };
4678
6220
  const entries = [
@@ -4685,6 +6227,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
4685
6227
  limit
4686
6228
  }
4687
6229
  ];
6230
+ expect((await store.consumeMany(entries)).allowed).toBe(true);
4688
6231
  const counted = await store.consumeMany(entries);
4689
6232
  expect(counted.allowed).toBe(true);
4690
6233
  if (!counted.release) {