meocord 4.2.0 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +264 -0
  2. package/THIRD_PARTY_NOTICES.md +14 -13
  3. package/dist/cjs/_shared/{controller.decorator-CRn9kWob.cjs → controller.decorator-XKMlcUZz.cjs} +376 -100
  4. package/dist/cjs/_shared/{providers-ChvMqnLd.cjs → providers-BZgJyfZS.cjs} +145 -36
  5. package/dist/cjs/_shared/{sharded-cooldown-store-BdAUTdYs.cjs → sharded-cooldown-store-CsJJyCZd.cjs} +10 -7
  6. package/dist/cjs/_shared/{startup-roots-CQo8mSP8.cjs → startup-roots-CgfPQrNl.cjs} +219 -63
  7. package/dist/cjs/_shared/{translate-error-DhDVbT5M.cjs → translate-error-BUAR1WYG.cjs} +10 -5
  8. package/dist/cjs/common/index.cjs +3 -3
  9. package/dist/cjs/core/index.cjs +185 -168
  10. package/dist/cjs/decorator/index.cjs +21 -6
  11. package/dist/cjs/testing/index.cjs +1594 -286
  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 +84 -10
  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 +37 -6
  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/fallback.js +20 -11
  37. package/dist/esm/core/guard-runner.js +16 -3
  38. package/dist/esm/core/handler-input.js +4 -3
  39. package/dist/esm/core/lifecycle-hooks.js +134 -47
  40. package/dist/esm/core/meocord-factory.js +55 -32
  41. package/dist/esm/core/meocord.app.js +116 -75
  42. package/dist/esm/core/providers.js +7 -2
  43. package/dist/esm/core/running-calls.js +43 -0
  44. package/dist/esm/core/shard-context.js +5 -6
  45. package/dist/esm/core/shard-manager.js +16 -10
  46. package/dist/esm/core/startup-roots.js +21 -1
  47. package/dist/esm/core/theme-resolvers.js +4 -1
  48. package/dist/esm/core/theme-scope.js +3 -1
  49. package/dist/esm/core/theme-validation.js +17 -10
  50. package/dist/esm/decorator/controller.decorator.js +56 -20
  51. package/dist/esm/decorator/cooldown.decorator.js +17 -0
  52. package/dist/esm/decorator/guard.decorator.js +3 -2
  53. package/dist/esm/package.json.js +1 -1
  54. package/dist/esm/testing/catalog.js +26 -10
  55. package/dist/esm/testing/cooldown-store-suite.js +67 -5
  56. package/dist/esm/testing/discord-defaults.js +205 -30
  57. package/dist/esm/testing/meocord-testing-module.js +134 -28
  58. package/dist/esm/testing/mock-fn.js +107 -26
  59. package/dist/esm/testing/mock-interaction.js +1026 -172
  60. package/dist/esm/testing/snowflake.js +9 -2
  61. package/dist/esm/testing/strict-mocks.js +37 -17
  62. package/dist/esm/util/common.util.js +47 -19
  63. package/dist/esm/util/dev-runner.util.js +8 -8
  64. package/dist/esm/util/generator-cli.util.js +4 -5
  65. package/dist/esm/util/interaction.util.js +2 -2
  66. package/dist/esm/util/meocord-config-loader.util.js +10 -2
  67. package/dist/esm/util/meocord-config-validation.util.js +3 -5
  68. package/dist/esm/util/meocord-source-config.util.js +65 -20
  69. package/dist/esm/util/method-wrapper.util.js +16 -0
  70. package/dist/esm/util/platform.util.js +10 -10
  71. package/dist/esm/util/shard-process.util.js +4 -1
  72. package/dist/esm/util/sharding-mode.util.js +5 -7
  73. package/dist/esm/util/shared-state.util.js +21 -0
  74. package/dist/esm/util/tsconfig-paths.util.js +29 -0
  75. package/dist/esm/util/tsconfig.util.js +10 -10
  76. package/dist/esm/util/value.util.js +12 -1
  77. package/dist/types/common/index.d.cts +6 -6
  78. package/dist/types/common/index.d.ts +6 -6
  79. package/dist/types/core/index.d.cts +3 -2
  80. package/dist/types/core/index.d.ts +3 -2
  81. package/dist/types/decorator/index.d.cts +1 -0
  82. package/dist/types/decorator/index.d.ts +1 -0
  83. package/dist/types/{index-CdXsFgjE.d.ts → index-DKJclIi-.d.ts} +45 -21
  84. package/dist/types/{index-nn5p7OoX.d.cts → index-Dlf0ZZtS.d.cts} +45 -21
  85. package/dist/types/interface/index.d.cts +1 -1
  86. package/dist/types/interface/index.d.ts +1 -1
  87. package/dist/types/{route-BZgFoNeB.d.cts → route-Bh1U13yy.d.cts} +3 -2
  88. package/dist/types/{route-BZgFoNeB.d.ts → route-Bh1U13yy.d.ts} +3 -2
  89. package/dist/types/testing/index.d.cts +175 -53
  90. package/dist/types/testing/index.d.ts +175 -53
  91. package/dist/types/{theme-resolvers-UKr3-6Ys.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
  92. package/dist/types/{theme-resolvers-BcOJVzR9.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
  93. package/dist/types/{translator-DHMkkCpg.d.cts → translator-Bg7hwxDF.d.cts} +21 -8
  94. package/dist/types/{translator-BViWKtLJ.d.ts → translator-CeP4bCzp.d.ts} +21 -8
  95. package/package.json +2 -2
@@ -1,14 +1,15 @@
1
1
  import 'reflect-metadata';
2
- import { createMockFn } from './mock-fn.js';
3
- import { DataManager, RoleManager, Role, GuildChannel, GuildMember, User, BaseGuild, MessageMentions, ThreadChannel, Message, BaseGuildVoiceChannel, VoiceChannel, BaseChannel, MessageReaction, InteractionType, ComponentType, ApplicationCommandType, GuildMemberManager, GuildChannelManager, UserManager, ChannelManager, GuildManager, Guild, GuildBanManager, GuildBan, ThreadMemberManager, ThreadMember, GuildMessageManager, DMMessageManager, GuildTextThreadManager, GuildForumThreadManager, PermissionOverwriteManager, PermissionOverwrites, CommandInteractionOptionResolver, ApplicationCommandOptionType, Client, ApplicationCommandManager, ClientUser, CDN, AuthorizingIntegrationOwners, BaseInteraction, UserContextMenuCommandInteraction, Locale, StringSelectMenuInteraction, UserSelectMenuInteraction, RoleSelectMenuInteraction, ChannelSelectMenuInteraction, MentionableSelectMenuInteraction, Collection, MessageFlagsBitField, PermissionsBitField, Base, SnowflakeUtil, DMChannel, GuildMemberRoleManager, TextChannel, Component, Embed, Attachment, BaseManager, ApplicationCommand, NewsChannel, ForumChannel, MediaChannel, DiscordjsErrorCodes, DiscordjsTypeError } from 'discord.js';
2
+ import { createMockFn, usesRunnerMockFn } from './mock-fn.js';
3
+ import { DataManager, RoleManager, Role, GuildChannel, GuildMember, User, BaseGuild, MessageMentions, ThreadChannel, Message, BaseGuildVoiceChannel, VoiceChannel, BaseChannel, MessageReaction, InteractionType, ComponentType, ApplicationCommandType, GuildMemberManager, GuildChannelManager, UserManager, ChannelManager, GuildManager, Guild, GuildBanManager, GuildBan, ThreadMemberManager, ThreadMember, GuildMessageManager, DMMessageManager, GuildTextThreadManager, GuildForumThreadManager, PermissionOverwriteManager, PermissionOverwrites, ReactionManager, ReactionUserManager, PresenceManager, Presence, CommandInteractionOptionResolver, ApplicationCommandOptionType, Client, ApplicationCommandManager, ClientUser, CDN, DiscordjsErrorCodes, AuthorizingIntegrationOwners, BaseInteraction, UserContextMenuCommandInteraction, MessageContextMenuCommandInteraction, Attachment, Locale, StringSelectMenuInteraction, UserSelectMenuInteraction, RoleSelectMenuInteraction, ChannelSelectMenuInteraction, MentionableSelectMenuInteraction, Collection, PermissionsBitField, Base, SnowflakeUtil, DiscordjsError, DMChannel, GuildMemberRoleManager, MessageFlagsBitField, BaseManager, TextChannel, Component, Embed, ActionRow, ApplicationCommand, NewsChannel, ForumChannel, MediaChannel, DiscordjsTypeError, MessageFlags } from 'discord.js';
4
4
  import { createDiscordError } from './response.js';
5
5
  import { stampCall } from '../common/response/call-order.js';
6
- import { discordDefault, REAL_GETTER } from './discord-defaults.js';
6
+ import { discordDefault, REAL_GETTER, placeholderValue } from './discord-defaults.js';
7
7
  import { Logger } from '../common/logger.js';
8
8
  import { warnOnce, warnPlaceholder } from '../common/deprecation.js';
9
9
  import { embedsAsDiscordStores, asDiscordStores } from './discord-shape.js';
10
10
  import { nextSnowflake, MOCK_BOT_ID } from './snowflake.js';
11
- import { noteMockMade, strictMocks } from './strict-mocks.js';
11
+ import { strictMocks, noteMockMade } from './strict-mocks.js';
12
+ import { sharedName } from '../util/shared-state.util.js';
12
13
 
13
14
  // ---------------------------------------------------------------------------
14
15
  // stubDeep — Proxy that answers each property it lacks: a mock fn for a method, a default, or a nested stub
@@ -20,9 +21,15 @@ const SKIP = new Set([
20
21
  'toJSON',
21
22
  'then'
22
23
  ]);
24
+ // Each mock's own instance, which the proxy's traps receive as their target
25
+ const mockTargets = new WeakMap();
26
+ // Warnings a read answered by the stub logs first, by the mock's instance and the property
27
+ const stubReadWarnings = new WeakMap();
23
28
  function stubDeep(instance, externalStubs, onSet) {
24
29
  noteMockMade();
25
30
  const stubs = externalStubs ?? new Map();
31
+ // Placeholders read so far, by the value strict mocks give: a read of a kept one still warns, once
32
+ const placeholders = new Map();
26
33
  const proxy = new Proxy(instance, {
27
34
  get (target, prop) {
28
35
  // Always pass through symbols
@@ -36,12 +43,17 @@ function stubDeep(instance, externalStubs, onSet) {
36
43
  if (Object.prototype.hasOwnProperty.call(target, key)) {
37
44
  return Reflect.get(target, prop, target);
38
45
  }
46
+ stubReadWarnings.get(target)?.get(key)?.();
39
47
  // Skip passthrough props — return prototype value as-is
40
48
  if (SKIP.has(key)) {
41
49
  return Reflect.get(target, prop, target);
42
50
  }
43
51
  // Return cached stub
44
- if (stubs.has(key)) return stubs.get(key);
52
+ if (stubs.has(key)) {
53
+ const kept = placeholders.get(key);
54
+ if (kept !== undefined) warnPlaceholderRead(target, key, kept);
55
+ return stubs.get(key);
56
+ }
45
57
  // A value discord.js always gives, in its shape, rather than a stub: kept once read, or its getter run live
46
58
  const known = discordDefault(target, key);
47
59
  if (known === REAL_GETTER) {
@@ -55,6 +67,12 @@ function stubDeep(instance, externalStubs, onSet) {
55
67
  stubs.set(key, value);
56
68
  return value;
57
69
  }
70
+ // A value strict mocks read as discord.js gives it, which is a placeholder otherwise
71
+ const strictValue = strictMocks() ? undefined : placeholderValue(target, key);
72
+ if (strictValue !== undefined) {
73
+ placeholders.set(key, strictValue);
74
+ warnPlaceholderRead(target, key, strictValue);
75
+ }
58
76
  // Walk the prototype chain to check if it's a function
59
77
  let proto = Object.getPrototypeOf(target);
60
78
  let protoValue;
@@ -65,7 +83,7 @@ function stubDeep(instance, externalStubs, onSet) {
65
83
  if (desc.get && isPlaceholder(proto, key)) {
66
84
  // Strict mocks run discord.js's getter, read live; otherwise the placeholder, with a warning once
67
85
  if (strictMocks()) return Reflect.get(target, prop, proxy);
68
- warnBooleanPlaceholder(target, key);
86
+ warnPlaceholderRead(target, key, 'false');
69
87
  }
70
88
  break;
71
89
  }
@@ -73,7 +91,7 @@ function stubDeep(instance, externalStubs, onSet) {
73
91
  }
74
92
  // A client the mock's structures share: its guild's, or one of its own
75
93
  if (key === 'client' && target instanceof Base) return clientOf(target);
76
- const stub = typeof protoValue === 'function' ? isRealMethod(target, key) ? createMockFn((...args)=>protoValue.apply(proxy, args)) : methodStub(target, key, protoValue, ()=>proxy) : stubDeep({});
94
+ const stub = typeof protoValue === 'function' ? isRealMethod(target, key) ? createMockFn((...args)=>quietly(target, key, ()=>protoValue.apply(proxy, args))) : methodStub(target, key, protoValue, ()=>proxy) : stubDeep({});
77
95
  stubs.set(key, stub);
78
96
  return stub;
79
97
  },
@@ -82,7 +100,10 @@ function stubDeep(instance, externalStubs, onSet) {
82
100
  // and a plain write against one of those throws. Defining an own
83
101
  // data property shadows the accessor, which is what test setup means.
84
102
  set (target, prop, value) {
85
- Object.defineProperty(target, prop, {
103
+ // An own accessor with a setter, such as a context menu's target, takes the value itself
104
+ const setter = Object.getOwnPropertyDescriptor(target, prop)?.set;
105
+ if (setter) setter.call(target, value);
106
+ else Object.defineProperty(target, prop, {
86
107
  value,
87
108
  writable: true,
88
109
  enumerable: true,
@@ -92,6 +113,7 @@ function stubDeep(instance, externalStubs, onSet) {
92
113
  return true;
93
114
  }
94
115
  });
116
+ mockTargets.set(proxy, instance);
95
117
  return proxy;
96
118
  }
97
119
  // discord.js methods that read only what the mock holds, so they run for real: looking items up in a manager's cache,
@@ -224,7 +246,8 @@ const PLACEHOLDER_BOOLEANS = new Map([
224
246
  [
225
247
  BaseGuildVoiceChannel.prototype,
226
248
  new Set([
227
- 'joinable'
249
+ 'joinable',
250
+ 'full'
228
251
  ])
229
252
  ],
230
253
  [
@@ -259,11 +282,42 @@ const PLACEHOLDER_BOOLEANS = new Map([
259
282
  return false;
260
283
  }
261
284
  const mockLogger = new Logger('Mocks');
262
- function warnBooleanPlaceholder(target, key) {
285
+ // Mock reactions, and their messages, that MeoCord's dispatcher reads `partial` on
286
+ const dispatchedReactions = new WeakSet();
287
+ let quiet = 0;
288
+ /**
289
+ * Runs a real method, keeping quiet about the placeholders `mentions.has()` reads: a placeholder `repliedUser` matches
290
+ * no user, so its answer is discord.js's either way.
291
+ */ function quietly(target, key, run) {
292
+ if (!(target instanceof MessageMentions && key === 'has')) return run();
293
+ quiet++;
294
+ try {
295
+ return run();
296
+ } finally{
297
+ quiet--;
298
+ }
299
+ }
300
+ /**
301
+ * Marks a mock reaction and its message as read by MeoCord's dispatcher, so a placeholder `partial` it reads warns
302
+ * that MeoCord read it, and fetched for it.
303
+ */ function noteReactionDispatch(reaction) {
304
+ const target = mockTargets.get(reaction);
305
+ if (!target) return;
306
+ dispatchedReactions.add(target);
307
+ const message = mockTargets.get(reaction.message ?? {});
308
+ if (message) dispatchedReactions.add(message);
309
+ }
310
+ function warnPlaceholderRead(target, key, strictValue) {
311
+ if (quiet > 0) return;
263
312
  const name = Object.getPrototypeOf(target).constructor.name;
313
+ if (key === 'partial' && dispatchedReactions.has(target)) {
314
+ const fetched = target instanceof MessageReaction ? 'the reaction' : "the reaction's message";
315
+ 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.');
316
+ return;
317
+ }
264
318
  // The class name in camel case, a leading acronym included: dmChannel, guildMember
265
319
  const variable = name.replace(/^[A-Z]+(?=[A-Z][a-z])|^[A-Z]/, (start)=>start.toLowerCase());
266
- 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.`);
320
+ 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.`);
267
321
  }
268
322
  // The client of a structure made without one: the one its guild has, else one of its own, made once
269
323
  const ownClients = new WeakMap();
@@ -371,25 +425,37 @@ const MANAGER_ITEMS = [
371
425
  ],
372
426
  [
373
427
  RoleManager,
374
- (id)=>createMockInteraction(Role, id ? {
375
- id
376
- } : {})
428
+ (id, manager)=>createMockInteraction(Role, {
429
+ ...id ? {
430
+ id
431
+ } : {},
432
+ ...guildOf(manager) ? {
433
+ guild: guildOf(manager)
434
+ } : {}
435
+ })
377
436
  ],
378
437
  [
379
438
  GuildBanManager,
380
439
  ()=>createMockInteraction(GuildBan)
381
440
  ],
441
+ // A message fetched from a channel is in it, and another user's
382
442
  [
383
443
  GuildMessageManager,
384
- (id)=>createMockMessage(id ? {
385
- id
386
- } : {})
444
+ (id, manager)=>createMockMessage({
445
+ ...id ? {
446
+ id
447
+ } : {},
448
+ ...channelOfManager(manager)
449
+ })
387
450
  ],
388
451
  [
389
452
  DMMessageManager,
390
- (id)=>createMockMessage(id ? {
391
- id
392
- } : {})
453
+ (id, manager)=>createMockMessage({
454
+ ...id ? {
455
+ id
456
+ } : {},
457
+ ...channelOfManager(manager)
458
+ })
393
459
  ],
394
460
  [
395
461
  GuildTextThreadManager,
@@ -407,6 +473,12 @@ const MANAGER_ITEMS = [
407
473
  ThreadMemberManager,
408
474
  ()=>createMockInteraction(ThreadMember)
409
475
  ],
476
+ [
477
+ ReactionUserManager,
478
+ (id)=>createMockUser(id ? {
479
+ id
480
+ } : {})
481
+ ],
410
482
  [
411
483
  ApplicationCommandManager,
412
484
  (id)=>createMockInteraction(ApplicationCommand, id ? {
@@ -434,6 +506,12 @@ const MANAGER_ITEMS = [
434
506
  const returnsPromise = (key, method)=>method.constructor.name === 'AsyncFunction' || PROMISE_METHODS.has(key) || /^set[A-Z]/.test(key) && !SYNC_SETTERS.has(key);
435
507
  // The ids a fetch asks for: an id, a discord.js object, or options naming one or several, such as `{ user: id }` or
436
508
  // `{ user: [ids] }`; undefined for a fetch of a list
509
+ /** The channel a message manager belongs to, as `createMockMessage` takes it. */ const channelOfManager = (manager)=>{
510
+ const channel = manager.channel;
511
+ return channel instanceof BaseChannel ? {
512
+ channel: channel
513
+ } : {};
514
+ };
437
515
  function fetchedIds(args) {
438
516
  const idOf = (value)=>typeof value === 'string' ? value : value instanceof Base ? value.id : undefined;
439
517
  const [first] = args;
@@ -469,6 +547,10 @@ function fetchedIds(args) {
469
547
  if (key === 'send' && target instanceof User) {
470
548
  return createMockFn(async (...args)=>(await receiver().createDM()).send(...args));
471
549
  }
550
+ // A channel's send resolves to the message it sent there
551
+ if (key === 'send' && target instanceof BaseChannel) {
552
+ return createMockFn(async (options)=>sentMessage(sent(options, false), receiver()));
553
+ }
472
554
  if (MESSAGE_METHODS.has(key)) return createMockFn(async ()=>createMockMessage());
473
555
  if (target instanceof BaseManager) {
474
556
  const item = MANAGER_ITEMS.find(([Manager])=>Manager.prototype.isPrototypeOf(target))?.[1];
@@ -485,7 +567,24 @@ function fetchedIds(args) {
485
567
  return ids === undefined ? new Collection() : one(ids);
486
568
  });
487
569
  }
488
- if (item && (key === 'create' || key === 'edit')) return createMockFn(async ()=>item(undefined, receiver()));
570
+ if (item && key === 'edit') return createMockFn(async ()=>item(undefined, receiver()));
571
+ // A ban resolves to the member, user or id it was given, as discord.js's does, and caches nothing
572
+ if (key === 'create' && target instanceof GuildBanManager) {
573
+ return createMockFn(async (user)=>{
574
+ if (user instanceof GuildMember) return user;
575
+ if (user instanceof User) return receiver().guild.members.resolve(user) ?? user;
576
+ return typeof user === 'string' ? user : user?.id;
577
+ });
578
+ }
579
+ // Every other manager here caches what its create() makes, as discord.js's does; check a manager's create when
580
+ // adding it to MANAGER_ITEMS
581
+ if (item && key === 'create') {
582
+ return createMockFn(async ()=>{
583
+ const made = item(undefined, receiver());
584
+ if (typeof made.id === 'string') cacheOf(receiver())?.set(made.id, made);
585
+ return made;
586
+ });
587
+ }
489
588
  } else if (target instanceof Base && (SELF_METHODS.has(key) || /^set[A-Z]/.test(key))) {
490
589
  return createMockFn(async ()=>receiver());
491
590
  }
@@ -645,14 +744,46 @@ const isSnowflake = (id)=>typeof id === 'string' && /^\d{1,20}$/.test(id);
645
744
  });
646
745
  }
647
746
  }
648
- /** Where a mock interaction keeps every answer it got, through respond() or discord.js directly, in order. */ const RESPONSE_LOG = Symbol('response log');
747
+ /**
748
+ * Gives a user, server or channel mock its creation time. Under strict mocks, a generated id gives the time the mock
749
+ * 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.
750
+ * An id the test gives decides it in both modes, and a value the test sets replaces it.
751
+ */ function defineMadeTime(mock, generatedId) {
752
+ if (strictMocks()) return defineCreatedTime(mock, generatedId);
753
+ const own = (key)=>Object.prototype.hasOwnProperty.call(mock, key);
754
+ const name = Object.getPrototypeOf(mock).constructor.name;
755
+ if (!own('createdTimestamp')) {
756
+ Object.defineProperty(mock, 'createdTimestamp', {
757
+ get () {
758
+ if (this.id === generatedId) {
759
+ 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.');
760
+ }
761
+ return isSnowflake(this.id) ? SnowflakeUtil.timestampFrom(this.id) : null;
762
+ },
763
+ configurable: true
764
+ });
765
+ }
766
+ if (!own('createdAt')) {
767
+ Object.defineProperty(mock, 'createdAt', {
768
+ get () {
769
+ return this.createdTimestamp === null ? null : new Date(this.createdTimestamp);
770
+ },
771
+ configurable: true
772
+ });
773
+ }
774
+ }
775
+ /**
776
+ * Where a mock interaction keeps every answer it got, through respond() or discord.js directly, in order, under a key
777
+ * both builds of this meocord version read.
778
+ */ const RESPONSE_LOG = Symbol.for(sharedName('responseLog'));
649
779
  /**
650
780
  * A mock of an answer method, `impl` its behaviour, that records each call in `log` as it is made, with what it sent
651
781
  * and, once it settles, what it rejected with. The record wraps every behaviour the mock is given, its own and any a
652
- * test sets, so the mock stays the plain mock function a runner's matchers read, bun's included. `withResponse` is how
653
- * a call asks discord.js for the message back, not part of what it sends, so it is left out.
654
- */ function recordedMock(method, impl, log) {
655
- const record = (behaviour)=>function(...args) {
782
+ * test sets, so the mock stays the plain mock function a runner's matchers read, bun's included, and `gate` keeps the
783
+ * reply state whichever behaviour runs. `withResponse` is how a call asks discord.js for the message back, not part of
784
+ * what it sends, so it is left out.
785
+ */ function recordedMock(method, impl, log, gate) {
786
+ const record = (behaviour, byTest = true)=>function(...args) {
656
787
  const [payload] = args;
657
788
  const sent = payload && typeof payload === 'object' && 'withResponse' in payload ? Object.fromEntries(Object.entries(payload).filter(([key])=>key !== 'withResponse')) : payload;
658
789
  const call = {
@@ -661,22 +792,29 @@ const isSnowflake = (id)=>typeof id === 'string' && /^\d{1,20}$/.test(id);
661
792
  };
662
793
  stampCall(call);
663
794
  log.push(call);
795
+ let undo;
796
+ try {
797
+ // Before the behaviour, as discord.js changes the state at once: a second answer made without awaiting is refused
798
+ if (gate) undo = gate(args, byTest);
799
+ } catch (error) {
800
+ call.error = error;
801
+ return Promise.reject(error);
802
+ }
803
+ const failed = (error)=>{
804
+ undo?.();
805
+ call.error = error;
806
+ throw error;
807
+ };
664
808
  try {
665
809
  const result = Reflect.apply(behaviour, this, args);
666
810
  // Recorded and passed on, so a rejection nobody awaits is unhandled in the test, as it is on a bot
667
- if (result instanceof Promise) {
668
- return result.then(undefined, (error)=>{
669
- call.error = error;
670
- throw error;
671
- });
672
- }
811
+ if (result instanceof Promise) return result.then(undefined, failed);
673
812
  return result;
674
813
  } catch (error) {
675
- call.error = error;
676
- throw error;
814
+ return failed(error);
677
815
  }
678
816
  };
679
- const mock = createMockFn(record(impl));
817
+ const mock = createMockFn(record(impl, false));
680
818
  const always = mock.mockImplementation.bind(mock);
681
819
  const once = mock.mockImplementationOnce.bind(mock);
682
820
  // Each way a test sets a behaviour goes through the record, a runner's own extras included where it has them
@@ -709,16 +847,26 @@ const isSnowflake = (id)=>typeof id === 'string' && /^\d{1,20}$/.test(id);
709
847
  return mock;
710
848
  }
711
849
  /** The interaction a mock options resolver was given to, for the server a user option's member is in. */ const optionOwners = new WeakMap();
850
+ /** 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) {
851
+ const error = new DiscordjsError(code);
852
+ if (message !== undefined) error.message = message;
853
+ return error;
854
+ }
712
855
  /** An error as discord.js's options resolver throws it: a `TypeError` with discord.js's code and message. */ const optionError = (code, ...args)=>new DiscordjsTypeError(code, ...args);
713
856
  // In discord.js's error codes, though missing from its typings
714
857
  const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
715
858
  /** Records the interaction a mock options resolver belongs to. */ const ownOptions = (interaction, options)=>{
716
859
  if (typeof options === 'object' && options !== null) optionOwners.set(options, interaction);
717
860
  };
718
- /** 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(User.prototype), {
719
- id,
861
+ /** A mock user that is a person, with an id of its own unless given one. */ const mockUser = (id)=>{
862
+ const generatedId = id === undefined ? nextSnowflake() : undefined;
863
+ const user = Object.assign(Object.create(User.prototype), {
864
+ id: id ?? generatedId,
720
865
  bot: false
721
- }));
866
+ });
867
+ defineMadeTime(user, generatedId);
868
+ return stubDeep(user);
869
+ };
722
870
  function createMockInteraction(Class, props) {
723
871
  const instance = Object.create(Class.prototype);
724
872
  const stubs = new Map();
@@ -738,14 +886,36 @@ function createMockInteraction(Class, props) {
738
886
  // mock lacks: a guildId is a guild, a guild object with it a cached one, neither a DM. A member
739
887
  // not given is the user's on an interaction, and the auto-stub elsewhere, so only one set to null or undefined fails.
740
888
  const own = (key)=>Object.prototype.hasOwnProperty.call(instance, key) ? instance[key] : undefined;
889
+ let strictPlace;
741
890
  const hasMember = ()=>!Object.prototype.hasOwnProperty.call(instance, 'member') || Boolean(instance.member);
742
891
  const guildChecks = {
743
892
  inGuild: ()=>Boolean(own('guildId') && hasMember()),
744
893
  inCachedGuild: ()=>Boolean(own('guildId') && own('guild') && hasMember()),
745
894
  inRawGuild: ()=>Boolean(own('guildId') && !own('guild') && hasMember())
746
895
  };
896
+ // What each answers where strict mocks place the mock: the place's guildId and guild for those the test left out, and
897
+ // a member wherever there is a guildId, unless the test gave one
898
+ const given = new Set(Object.keys(props ?? {}));
899
+ const strictChecks = (place)=>{
900
+ const pick = (key, placed)=>given.has(key) || placed === undefined ? own(key) : placed;
901
+ const guildId = pick('guildId', place.guildId);
902
+ const guild = place.raw ? null : pick('guild', place.guild);
903
+ const member = given.has('member') ? Boolean(own('member')) : Boolean(guildId);
904
+ return {
905
+ inGuild: Boolean(guildId && member),
906
+ inCachedGuild: Boolean(guildId && guild && member),
907
+ inRawGuild: Boolean(guildId && !guild && member)
908
+ };
909
+ };
910
+ // Each reads where the mock is as its own, and in default mode warns once, by its own name, where strict mocks answer
911
+ // otherwise or refuse the mock
747
912
  for (const [name, check] of Object.entries(guildChecks)){
748
- if (findPrototypeMethod(instance, name) !== null) stubs.set(name, createMockFn(check));
913
+ if (findPrototypeMethod(instance, name) === null) continue;
914
+ stubs.set(name, createMockFn(()=>{
915
+ const answer = readPlaceQuietly(check);
916
+ if (strictPlace && !strictMocks() && (strictPlace.refused || strictChecks(strictPlace)[name] !== answer)) placeWarners.get(instance)?.warn(`${name}()`);
917
+ return answer;
918
+ }));
749
919
  }
750
920
  // Set up reply state machine for repliable interactions
751
921
  const isRepliableMethod = findPrototypeMethod(instance, 'isRepliable');
@@ -754,83 +924,204 @@ function createMockInteraction(Class, props) {
754
924
  instance.replied = false;
755
925
  instance.deferred = false;
756
926
  instance.ephemeral = false;
757
- const alreadyReplied = ()=>new Error('The reply to this interaction has already been sent or deferred.');
758
- const notYetReplied = (method)=>new Error(`Cannot call ${method}() before replying or deferring.`);
927
+ // discord.js's errors, with their codes; a call before any answer keeps the message the mock has always given
928
+ const alreadyReplied = ()=>discordjsError(DiscordjsErrorCodes.InteractionAlreadyReplied);
929
+ const notYetReplied = (method)=>discordjsError(DiscordjsErrorCodes.InteractionNotReplied, `Cannot call ${method}() before replying or deferring.`);
759
930
  // Only `flags` is read: discord.js deprecates the `ephemeral: true` reply option, and honouring it here would let
760
- // a test pass against a deprecated call.
931
+ // a test pass against a deprecated call. Read as discord.js reads them, so a flag it refuses throws here too.
761
932
  const hasEphemeralFlag = (options)=>{
762
- if (!options) return false;
763
- const { flags } = options;
764
- if (typeof flags === 'number') return (flags & 64) !== 0;
765
- if (typeof flags === 'bigint') return (flags & 64n) !== 0n;
766
- return false;
933
+ const flags = options?.flags;
934
+ if (typeof options !== 'object' || flags === null || flags === undefined) return false;
935
+ return new MessageFlagsBitField(flags).has(MessageFlags.Ephemeral);
936
+ };
937
+ const built = (held)=>typeof held === 'function' ? held() : held;
938
+ let original;
939
+ let originalGone = false;
940
+ const followUps = new Map();
941
+ // Whether the last answer ran a behaviour a test set, after which a second answer runs with a warning by default
942
+ let answeredByTest = false;
943
+ // A command answered with a modal has no original response, and Discord refuses one asked for
944
+ let modalAnswered = false;
945
+ const isComponent = instance.type === InteractionType.MessageComponent;
946
+ // The original response of a component, or of a modal submitted from one, is the message it's on
947
+ const componentMessage = ()=>heldFrom(instance.message);
948
+ const snapshot = ()=>{
949
+ const saved = {
950
+ replied: instance.replied,
951
+ deferred: instance.deferred,
952
+ ephemeral: instance.ephemeral,
953
+ original,
954
+ originalGone,
955
+ followUps: new Map(followUps),
956
+ answeredByTest,
957
+ modalAnswered
958
+ };
959
+ return ()=>{
960
+ Object.assign(instance, {
961
+ replied: saved.replied,
962
+ deferred: saved.deferred,
963
+ ephemeral: saved.ephemeral
964
+ });
965
+ ({ original, originalGone, answeredByTest, modalAnswered } = saved);
966
+ followUps.clear();
967
+ for (const [id, held] of saved.followUps)followUps.set(id, held);
968
+ };
767
969
  };
970
+ /**
971
+ * Refuses an answer as discord.js does. Where the refusal turns on a behaviour a test set, this answer's or the
972
+ * one before it, which ran without one, default mode warns and lets it run; strict mocks refuse it.
973
+ */ const refuse = (method, error, byTest)=>{
974
+ if (!(byTest || answeredByTest) || strictMocks()) throw error;
975
+ 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.');
976
+ };
977
+ const answered = ()=>instance.deferred || instance.replied;
978
+ /**
979
+ * What an answer sends, built now, as discord.js builds it when sending. One it refuses to build is refused under
980
+ * strict mocks; default mode warns and holds it to build when first read.
981
+ */ const hold = (method, build)=>{
982
+ try {
983
+ return build();
984
+ } catch (error) {
985
+ if (strictMocks()) throw error;
986
+ 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.');
987
+ return build;
988
+ }
989
+ };
990
+ /** A first answer: refused once the interaction has one, else it replies or defers. */ const first = (method, kind, then)=>(args, byTest)=>{
991
+ if (answered()) refuse(method, alreadyReplied(), byTest);
992
+ // Read first: flags discord.js refuses throw before anything changes
993
+ const ephemeral = method === 'reply' || method === 'deferReply' ? hasEphemeralFlag(args[0]) : undefined;
994
+ const undo = snapshot();
995
+ instance[kind] = true;
996
+ if (ephemeral) instance.ephemeral = true;
997
+ answeredByTest = byTest;
998
+ try {
999
+ then(args);
1000
+ } catch (error) {
1001
+ undo();
1002
+ throw error;
1003
+ }
1004
+ return undo;
1005
+ };
1006
+ /** A later answer: refused before the interaction has one. */ const later = (method, then)=>(args, byTest)=>{
1007
+ if (!answered()) refuse(method, notYetReplied(method), byTest);
1008
+ const undo = snapshot();
1009
+ try {
1010
+ then(args);
1011
+ } catch (error) {
1012
+ undo();
1013
+ throw error;
1014
+ }
1015
+ return undo;
1016
+ };
1017
+ /** The id an edit, fetch or delete names, `undefined` for the original response. */ const targetId = (message)=>{
1018
+ if (message === undefined || message === '@original') return undefined;
1019
+ return typeof message === 'string' ? message : message.id ?? undefined;
1020
+ };
1021
+ /** The message an edit, fetch or delete reaches, or why Discord refuses it: it is gone or never was. */ const target = (id)=>{
1022
+ if (id !== undefined) {
1023
+ const held = followUps.get(id);
1024
+ if (held) followUps.set(id, built(held));
1025
+ return held && built(held);
1026
+ }
1027
+ if (originalGone || modalAnswered) return undefined;
1028
+ // Answered without one of the mock's calls, as after a 40060 or a test setting `replied`: the original is there
1029
+ if (!original && answered()) original = componentMessage();
1030
+ if (original) original = built(original);
1031
+ // An answer's message gets its id when first read, as it would be built then without strict mocks
1032
+ if (original?.id === '') original = {
1033
+ ...original,
1034
+ id: nextSnowflake()
1035
+ };
1036
+ return original;
1037
+ };
1038
+ const unknownMessage = ()=>createDiscordError(10008, 'Unknown Message');
1039
+ // Where its answers are sent, read as the mock's own, so a placement warning stays the test's
1040
+ const answeredIn = ()=>readPlaceQuietly(()=>[
1041
+ instance.channel,
1042
+ instance.client
1043
+ ]);
1044
+ // What each answer's message made for itself, by its id, so every read of it has the same author and place
1045
+ const madePlaces = new Map();
1046
+ const madeFor = (id)=>madePlaces.get(id) ?? madePlaces.set(id, {}).get(id);
1047
+ // Each call's message, read by its behaviour right after the gate made it
1048
+ let made;
1049
+ let followUpMade;
768
1050
  // Every answer the interaction gets, through respond() or discord.js directly, in order, for getResponse
769
1051
  const log = [];
770
1052
  Object.defineProperty(instance, RESPONSE_LOG, {
771
1053
  value: log
772
1054
  });
773
- const answer = (method, impl)=>stubs.set(method, recordedMock(method, impl, log));
774
- answer('reply', async (...args)=>{
775
- if (instance.deferred || instance.replied) throw alreadyReplied();
776
- instance.replied = true;
777
- if (hasEphemeralFlag(args[0])) instance.ephemeral = true;
778
- });
779
- answer('deferReply', async (...args)=>{
780
- if (instance.deferred || instance.replied) throw alreadyReplied();
781
- instance.deferred = true;
782
- if (hasEphemeralFlag(args[0])) instance.ephemeral = true;
783
- });
784
- answer('followUp', async ()=>{
785
- if (!instance.deferred && !instance.replied) throw notYetReplied('followUp');
786
- instance.replied = true;
787
- return createMockMessage();
788
- });
789
- // The interaction's own message as Discord holds it: the message a component is on, or the original response.
790
- // An edit replaces it in the shape Discord stores, with ids and resolved media, stamped with when it was edited,
791
- // and fetchReply() reads that back, so code comparing the two sees what it would against Discord.
792
- let held;
793
- const current = ()=>held ?? (held = heldFrom(instance.message));
794
- // A command answered with a modal has no original response, and Discord refuses one asked for
795
- let modalAnswered = false;
796
- const unknownMessage = ()=>createDiscordError(10008, 'Unknown Message');
797
- answer('editReply', async (options)=>{
798
- if (!instance.deferred && !instance.replied) throw notYetReplied('editReply');
799
- if (modalAnswered) throw unknownMessage();
1055
+ const answer = (method, impl, gate)=>stubs.set(method, recordedMock(method, impl, log, gate));
1056
+ answer('reply', async ()=>undefined, first('reply', 'replied', ([options])=>original = hold('reply', ()=>sent(options, false))));
1057
+ answer('deferReply', async ()=>undefined, first('deferReply', 'deferred', ()=>original = sent(undefined)));
1058
+ answer('followUp', // The follow-up as sent, which an edit, fetch or delete reaches by its id
1059
+ async ()=>{
1060
+ const held = followUps.get(followUpMade);
1061
+ // One held to build when read resolves to its id alone, as building it now would refuse it
1062
+ return typeof held === 'function' ? createMockMessage({
1063
+ id: followUpMade
1064
+ }) : messageFrom(held, madeFor(held.id), ...answeredIn());
1065
+ }, later('followUp', ([options])=>{
800
1066
  instance.replied = true;
801
- held = edited(current(), options);
802
- // The response has the message as Discord answers the edit, before its uploaded files have loaded again
803
- return messageFrom({
1067
+ // Held as it is when sent, as Discord stores it, so a later change to what was passed isn't seen; its id taken
1068
+ // once built, so one that fails to build under strict mocks takes none
1069
+ const held = hold('followUp', ()=>sent(options, false));
1070
+ const id = nextSnowflake();
1071
+ made = undefined;
1072
+ followUps.set(id, typeof held === 'function' ? ()=>({
1073
+ ...held(),
1074
+ id
1075
+ }) : {
804
1076
  ...held,
805
- components: held.returned ?? held.components
1077
+ id
806
1078
  });
807
- });
808
- stubs.set('fetchReply', createMockFn(async ()=>{
809
- if (modalAnswered) throw unknownMessage();
810
- return messageFrom(current());
1079
+ followUpMade = id;
1080
+ }));
1081
+ // An edit replaces the message in the shape Discord stores, with ids and resolved media, stamped with when it was
1082
+ // edited, and fetchReply() reads that back, so code comparing the two sees what it would against Discord
1083
+ answer('editReply', async ()=>{
1084
+ if (!made) throw unknownMessage();
1085
+ // The response has the message as Discord answers the edit, before its uploaded files have loaded again
1086
+ return messageFrom({
1087
+ ...made,
1088
+ components: made.returned ?? made.components
1089
+ }, madeFor(made.id), ...answeredIn());
1090
+ }, later('editReply', ([options])=>{
1091
+ const id = targetId(options?.message);
1092
+ const held = target(id);
1093
+ made = held && edited(held, options);
1094
+ if (!made) return;
1095
+ if (id === undefined) {
1096
+ original = made;
1097
+ instance.replied = true;
1098
+ } else followUps.set(id, made);
1099
+ }));
1100
+ stubs.set('fetchReply', createMockFn(async (message)=>{
1101
+ const held = target(targetId(message));
1102
+ if (!held) throw unknownMessage();
1103
+ return messageFrom(held, madeFor(held.id), ...answeredIn());
811
1104
  }));
812
1105
  answer('deleteReply', async ()=>{
813
- if (!instance.deferred && !instance.replied) throw notYetReplied('deleteReply');
814
- if (modalAnswered) throw unknownMessage();
815
- });
1106
+ if (!made) throw unknownMessage();
1107
+ }, later('deleteReply', ([message])=>{
1108
+ const id = targetId(message);
1109
+ made = target(id);
1110
+ if (!made) return;
1111
+ if (id === undefined) originalGone = true;
1112
+ else followUps.delete(id);
1113
+ }));
816
1114
  // showModal — the first response of a command or a component, like reply()
817
- if (instance.type === InteractionType.ApplicationCommand || instance.type === InteractionType.MessageComponent) {
818
- answer('showModal', async ()=>{
819
- if (instance.deferred || instance.replied) throw alreadyReplied();
820
- instance.replied = true;
1115
+ if (instance.type === InteractionType.ApplicationCommand || isComponent) {
1116
+ answer('showModal', async ()=>undefined, first('showModal', 'replied', ()=>{
821
1117
  modalAnswered = instance.type === InteractionType.ApplicationCommand;
822
- });
1118
+ if (isComponent) original = componentMessage();
1119
+ }));
823
1120
  }
824
1121
  // deferUpdate / update — components, and modals submitted from a message's component
825
- if (instance.type === InteractionType.MessageComponent || instance.type === InteractionType.ModalSubmit) {
826
- answer('update', async ()=>{
827
- if (instance.deferred || instance.replied) throw alreadyReplied();
828
- instance.replied = true;
829
- });
830
- answer('deferUpdate', async ()=>{
831
- if (instance.deferred || instance.replied) throw alreadyReplied();
832
- instance.deferred = true;
833
- });
1122
+ if (isComponent || instance.type === InteractionType.ModalSubmit) {
1123
+ answer('update', async ()=>undefined, first('update', 'replied', ([options])=>original = hold('update', ()=>edited(heldFrom(instance.message, false), options))));
1124
+ answer('deferUpdate', async ()=>undefined, first('deferUpdate', 'deferred', ()=>original = componentMessage()));
834
1125
  }
835
1126
  }
836
1127
  // Autocomplete is not repliable, but it has a response of its own: Discord accepts
@@ -840,7 +1131,7 @@ function createMockInteraction(Class, props) {
840
1131
  if (instance.type === InteractionType.ApplicationCommandAutocomplete) {
841
1132
  instance.responded = false;
842
1133
  stubs.set('respond', createMockFn(async (choices)=>{
843
- if (instance.responded) throw new Error('The reply to this interaction has already been sent or deferred.');
1134
+ if (instance.responded) throw discordjsError(DiscordjsErrorCodes.InteractionAlreadyReplied);
844
1135
  // Discord refuses a list longer than its limit, and the menu shows nothing
845
1136
  if (Array.isArray(choices) && choices.length > MAX_AUTOCOMPLETE_CHOICES) {
846
1137
  throw createDiscordError(50035, `Invalid Form Body\ndata.choices[BASE_TYPE_MAX_LENGTH]: Must be ${MAX_AUTOCOMPLETE_CHOICES} or fewer in length.`);
@@ -869,6 +1160,34 @@ function createMockInteraction(Class, props) {
869
1160
  const unset = (key)=>!Object.prototype.hasOwnProperty.call(instance, key);
870
1161
  const generatedId = unset('id') ? instance.id = nextSnowflake() : undefined;
871
1162
  defineCreatedTime(instance, generatedId);
1163
+ // Strict mocks place it where its message, guild or member is, and a guildId alone in a server the bot isn't in
1164
+ // Read as the mock's own, so a message's placement warning stays the test's
1165
+ strictPlace = readPlaceQuietly(()=>strictPlaceOf(own));
1166
+ if (strictPlace && strictMocks()) {
1167
+ if (strictPlace.refused) throw new Error(strictPlace.refused);
1168
+ const set = (key, value)=>Object.defineProperty(instance, key, {
1169
+ value,
1170
+ writable: true,
1171
+ enumerable: true,
1172
+ configurable: true
1173
+ });
1174
+ if (strictPlace.channel && unset('channel')) set('channel', strictPlace.channel);
1175
+ if (strictPlace.guildId !== undefined && unset('guildId')) set('guildId', strictPlace.guildId);
1176
+ if (strictPlace.guild !== undefined && unset('guild')) set('guild', strictPlace.guild);
1177
+ if (strictPlace.raw) {
1178
+ const user = own('user');
1179
+ const rawUser = user && {
1180
+ id: user.id,
1181
+ username: user.username,
1182
+ global_name: user.globalName ?? null,
1183
+ discriminator: user.discriminator,
1184
+ avatar: user.avatar ?? null
1185
+ };
1186
+ set('member', createMockRawMember(rawUser ? {
1187
+ user: rawUser
1188
+ } : {}));
1189
+ }
1190
+ }
872
1191
  // 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
873
1192
  const raw = own('member');
874
1193
  // Given with a guild, a raw-shaped member is kept as the cached server's member it was before raw members
@@ -951,12 +1270,13 @@ function createMockInteraction(Class, props) {
951
1270
  configurable: true
952
1271
  });
953
1272
  }
954
- // The channel it came from, as the gateway caches it for an interaction: one of its server's, or the user's DM
1273
+ // The channel it came from, as the gateway caches it for an interaction: its server's cached one, looked up on each
1274
+ // read so a channel the test caches later is the one read, or the user's DM
955
1275
  let channel;
956
1276
  if (unset('channel')) {
957
1277
  Object.defineProperty(instance, 'channel', {
958
1278
  get: ()=>{
959
- if (channel === undefined) {
1279
+ if (channel === undefined || own('guildId') && own('guild') instanceof Guild) {
960
1280
  channel = channelFor(own('guild'), own('guildId'), own('channelId'), own('user'));
961
1281
  cacheChannel(channel, own('guild'), own('client'));
962
1282
  }
@@ -1011,7 +1331,73 @@ function createMockInteraction(Class, props) {
1011
1331
  live('targetMember', ()=>own('guildId') ? memberFor(own('guild'), targetUser(), true, own('guildId')) : null);
1012
1332
  }
1013
1333
  }
1334
+ // A message context menu's target: the message given, else one made for its targetId. The message is the one
1335
+ // source: targetId reads its id, and setting either one moves the other with it
1336
+ if (MessageContextMenuCommandInteraction.prototype.isPrototypeOf(instance)) {
1337
+ let given = own('targetMessage');
1338
+ let explicitId = own('targetId');
1339
+ if (given && given.id === undefined) given.id = nextSnowflake();
1340
+ if (given && explicitId !== undefined && explicitId !== given.id) {
1341
+ 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.`;
1342
+ if (strictMocks()) throw new Error(conflict);
1343
+ warnOnce(mockLogger, `${conflict} The mock reads targetMessage's id; under useStrictMocks(), as in the next major version (5.0), it refuses the pair.`);
1344
+ explicitId = undefined;
1345
+ }
1346
+ const generated = given || explicitId !== undefined ? undefined : nextSnowflake();
1347
+ let made;
1348
+ const accessor = (key, get, set)=>Object.defineProperty(instance, key, {
1349
+ get,
1350
+ set,
1351
+ enumerable: true,
1352
+ configurable: true
1353
+ });
1354
+ const targetId = ()=>explicitId ?? given?.id ?? generated;
1355
+ accessor('targetId', targetId, (value)=>{
1356
+ explicitId = value;
1357
+ given = undefined;
1358
+ });
1359
+ // In the channel the menu was used in, and its server, as the message a menu is used on is: its channel is the
1360
+ // menu's, read live, so making the message makes and caches no channel. Read as the mock's own, so a placement
1361
+ // warning stays the test's
1362
+ const makeTarget = ()=>readPlaceQuietly(()=>{
1363
+ const [guild, guildId] = [
1364
+ own('guild'),
1365
+ own('guildId') ?? null
1366
+ ];
1367
+ const message = messageMock({
1368
+ id: targetId(),
1369
+ guild: guildId && guild instanceof Guild ? guild : null,
1370
+ client: instance.client
1371
+ }, undefined, {}, {
1372
+ channelId: own('channelId'),
1373
+ guildId,
1374
+ channel: ()=>readPlaceQuietly(()=>instance.channel)
1375
+ });
1376
+ // Where strict mocks place the menu elsewhere, its target's place warns as the menu's does, by the target's name
1377
+ const warner = placeWarners.get(instance);
1378
+ const target = mockTargets.get(message) ?? message;
1379
+ for (const key of warner?.keys ?? [])warnOnRead(target, key, ()=>readingPlace > 0 ? void placeReadsSeen?.add(key) : warner.warn(`targetMessage.${key}`));
1380
+ return message;
1381
+ });
1382
+ accessor('targetMessage', ()=>given ?? (made?.id === targetId() ? made : made = makeTarget()), (value)=>{
1383
+ given = value;
1384
+ if (given && given.id === undefined) given.id = nextSnowflake();
1385
+ explicitId = undefined;
1386
+ });
1387
+ }
1388
+ }
1389
+ // A user has an id of its own, as Discord sends one, and was created when the mock was made under strict mocks
1390
+ if (User.prototype.isPrototypeOf(instance) && !Object.prototype.hasOwnProperty.call(instance, 'id')) {
1391
+ const generatedId = instance.id = nextSnowflake();
1392
+ defineMadeTime(instance, generatedId);
1393
+ }
1394
+ // A role too, created when the mock was made in both modes
1395
+ if (Role.prototype.isPrototypeOf(instance) && !Object.prototype.hasOwnProperty.call(instance, 'id')) {
1396
+ const generatedId = instance.id = nextSnowflake();
1397
+ defineCreatedTime(instance, generatedId);
1014
1398
  }
1399
+ // An attachment too, from the same count as every mock's
1400
+ if (Attachment.prototype.isPrototypeOf(instance) && !Object.prototype.hasOwnProperty.call(instance, 'id')) instance.id = nextSnowflake();
1015
1401
  // Discord sends the user's locale with every interaction, and the server's preferred one with one made in a server
1016
1402
  if (BaseInteraction.prototype.isPrototypeOf(instance)) {
1017
1403
  if (own('locale') === undefined) instance.locale = Locale.EnglishUS;
@@ -1035,6 +1421,32 @@ function createMockInteraction(Class, props) {
1035
1421
  }
1036
1422
  for (const kind of choices)if (!Object.prototype.hasOwnProperty.call(instance, kind)) instance[kind] = new Collection();
1037
1423
  }
1424
+ // In default mode, where strict mocks place it elsewhere, reading where it is warns
1425
+ if (strictPlace && !strictMocks()) warnOnPlaceReads(instance, strictPlace, new Set(Object.keys(props ?? {})));
1426
+ // A message whose place the mock made moves to where the test placed the interaction under strict mocks, and is then
1427
+ // one the test placed; in default mode it stays, and reading its place warns
1428
+ const message = own('message');
1429
+ const placed = [
1430
+ 'channel',
1431
+ 'guild',
1432
+ 'guildId'
1433
+ ].some((key)=>Object.keys(props ?? {}).includes(key));
1434
+ if (BaseInteraction.prototype.isPrototypeOf(instance) && placed && typeof message === 'object' && message !== null && placeWasMade(message)) {
1435
+ if (strictMocks()) {
1436
+ const guild = own('guild');
1437
+ const channel = instance.channel;
1438
+ Object.assign(message, {
1439
+ guild: guild ?? null,
1440
+ guildId: own('guildId') ?? null,
1441
+ ...channel && {
1442
+ channel,
1443
+ channelId: channel.id
1444
+ }
1445
+ });
1446
+ if (guild) memberFor(guild, message.author, true);
1447
+ placesMade.delete(mockTargets.get(message) ?? message);
1448
+ } else warnOnMadePlaceReads(mockTargets.get(message) ?? message);
1449
+ }
1038
1450
  // Options assigned after creation, directly or through Object.assign, belong to it as those given do
1039
1451
  return stubDeep(instance, stubs, (prop, value)=>{
1040
1452
  if (prop === 'options') ownOptions(instance, value);
@@ -1046,8 +1458,15 @@ function createMockInteraction(Class, props) {
1046
1458
  /**
1047
1459
  * A mock function that answers property access with another one, so `cache.store.flush()` works
1048
1460
  * on a double whose shape is an interface with nothing at runtime to read it from.
1049
- */ function stubCallable() {
1461
+ */ function stubCallable(member) {
1050
1462
  const fn = createMockFn();
1463
+ // jest's mock reads `_protoImpl` from itself whenever it's called, so the prototype below must not answer it
1464
+ if (usesRunnerMockFn()) Object.defineProperty(fn, '_protoImpl', {
1465
+ value: undefined,
1466
+ writable: true,
1467
+ enumerable: false,
1468
+ configurable: true
1469
+ });
1051
1470
  // Any property beyond the mock's own API is a nested mock, made on first read and kept on the function, out of its
1052
1471
  // keys. Its prototype makes them rather than a Proxy around it, so it stays the plain mock function matchers read
1053
1472
  const inherited = Object.getPrototypeOf(fn);
@@ -1056,7 +1475,9 @@ function createMockInteraction(Class, props) {
1056
1475
  if (typeof prop === 'symbol' || prop in proto) return Reflect.get(proto, prop, receiver);
1057
1476
  // Never thenable — otherwise awaiting a mock hangs on itself
1058
1477
  if (prop === 'then') return undefined;
1059
- const nested = stubCallable();
1478
+ // jest's matchers take a function whose `calls` has `all` and `count` for a jasmine spy, read before `mock.calls`
1479
+ if (member === 'calls' && (prop === 'all' || prop === 'count')) return undefined;
1480
+ const nested = stubCallable(prop);
1060
1481
  Object.defineProperty(receiver, prop, {
1061
1482
  value: nested,
1062
1483
  writable: true,
@@ -1121,6 +1542,7 @@ function createMockInteraction(Class, props) {
1121
1542
  if (key === 'then') return undefined;
1122
1543
  // An explicitly supplied prop wins over the auto-stub.
1123
1544
  if (Object.prototype.hasOwnProperty.call(instance, key)) return Reflect.get(instance, prop, instance);
1545
+ // The double itself is no function a matcher reads, so a `calls` member here keeps its `all` and `count`
1124
1546
  if (!stubs.has(key)) stubs.set(key, stubCallable());
1125
1547
  return stubs.get(key);
1126
1548
  },
@@ -1160,11 +1582,12 @@ function createMockInteraction(Class, props) {
1160
1582
  * @group Testing
1161
1583
  * @category Mocks
1162
1584
  * @see {@link createMockInteraction}
1163
- */ const createMockUser = (props = {})=>createMockInteraction(User, {
1164
- id: nextSnowflake(),
1585
+ */ function createMockUser(props = {}) {
1586
+ return createMockInteraction(User, {
1165
1587
  bot: false,
1166
1588
  ...props
1167
1589
  });
1590
+ }
1168
1591
  /**
1169
1592
  * Creates a mock {@link Client}, with `users`, `channels`, `guilds` and `application.commands` ready to stub.
1170
1593
  *
@@ -1278,6 +1701,18 @@ const HOLDS = [
1278
1701
  [
1279
1702
  PermissionOverwriteManager.prototype,
1280
1703
  PermissionOverwrites
1704
+ ],
1705
+ [
1706
+ ReactionManager.prototype,
1707
+ MessageReaction
1708
+ ],
1709
+ [
1710
+ ReactionUserManager.prototype,
1711
+ User
1712
+ ],
1713
+ [
1714
+ PresenceManager.prototype,
1715
+ Presence
1281
1716
  ]
1282
1717
  ];
1283
1718
  /**
@@ -1293,6 +1728,11 @@ const HOLDS = [
1293
1728
  value: cache,
1294
1729
  writable: true
1295
1730
  });
1731
+ // The same collection where discord.js's own methods read it past a subclass's `cache`, as `super.cache` does
1732
+ Object.defineProperty(manager, '_cache', {
1733
+ value: cache,
1734
+ writable: true
1735
+ });
1296
1736
  const holds = HOLDS.find(([Manager])=>Manager === prototype || Object.prototype.isPrototypeOf.call(Manager, prototype))?.[1];
1297
1737
  if (holds) Object.defineProperty(manager, 'holds', {
1298
1738
  value: holds
@@ -1300,14 +1740,25 @@ const HOLDS = [
1300
1740
  let own;
1301
1741
  Object.defineProperty(manager, 'client', {
1302
1742
  get: ()=>{
1303
- const owner = manager.guild ?? manager.channel ?? manager.thread;
1743
+ const owner = manager.guild ?? manager.channel ?? manager.thread ?? manager.message;
1304
1744
  return owner instanceof Base ? owner.client : own ?? (own = createMockClient());
1305
1745
  },
1306
- set: (value)=>own = value,
1746
+ // A client the test sets is the one read from then on, as on any other mock
1747
+ set: (value)=>Object.defineProperty(manager, 'client', {
1748
+ value,
1749
+ writable: true,
1750
+ enumerable: true,
1751
+ configurable: true
1752
+ }),
1307
1753
  configurable: true
1308
1754
  });
1309
1755
  return stubDeep(manager);
1310
1756
  }
1757
+ /** An empty manager of `prototype` that `owner` names its own, such as `{ message }` for a message's reactions. */ function ownedManager(prototype, owner) {
1758
+ const manager = managerWith(prototype, undefined);
1759
+ Object.assign(manager, owner);
1760
+ return manager;
1761
+ }
1311
1762
  /**
1312
1763
  * Creates a mock {@link Guild}, with the `members`, `channels`, `roles` and `bans` managers ready to stub.
1313
1764
  *
@@ -1344,8 +1795,9 @@ const HOLDS = [
1344
1795
  * @see {@link createMockMessage}
1345
1796
  */ function createMockGuild(overrides = {}) {
1346
1797
  const instance = Object.create(Guild.prototype);
1347
- instance.id = nextSnowflake();
1348
- if (overrides.id !== undefined) instance.id = overrides.id;
1798
+ const generatedId = overrides.id === undefined ? nextSnowflake() : undefined;
1799
+ instance.id = overrides.id ?? generatedId;
1800
+ defineMadeTime(instance, generatedId);
1349
1801
  if (overrides.name !== undefined) instance.name = overrides.name;
1350
1802
  if (overrides.preferredLocale !== undefined) instance.preferredLocale = overrides.preferredLocale;
1351
1803
  instance.members = managerWith(GuildMemberManager.prototype, overrides.members);
@@ -1390,7 +1842,8 @@ const HOLDS = [
1390
1842
  * @see {@link createMockGuild}
1391
1843
  */ function createMockChannel(Class, props = {}) {
1392
1844
  const instance = Object.create(Class.prototype);
1393
- instance.id = nextSnowflake();
1845
+ const generatedId = props.id === undefined ? nextSnowflake() : undefined;
1846
+ instance.id = generatedId;
1394
1847
  const is = (Base)=>Base.prototype.isPrototypeOf(Class.prototype) || Class === Base;
1395
1848
  // Text and announcement channels: messages, and threads made in the channel
1396
1849
  if (is(TextChannel) || is(NewsChannel)) {
@@ -1418,6 +1871,7 @@ const HOLDS = [
1418
1871
  // A server's channel has its overwrites, none until a test puts some in the cache, which its permissions read
1419
1872
  if (is(GuildChannel) && !is(ThreadChannel)) instance.permissionOverwrites = managerWith(PermissionOverwriteManager.prototype, undefined);
1420
1873
  Object.assign(instance, props);
1874
+ defineMadeTime(instance, generatedId);
1421
1875
  const stubs = new Map();
1422
1876
  const channel = stubDeep(instance, stubs);
1423
1877
  // Each manager knows its channel, as discord.js's do, and a thread's members their thread
@@ -1543,16 +1997,24 @@ const CHANNEL_TYPE_GUARDS = [
1543
1997
  return stubDeep(manager);
1544
1998
  }
1545
1999
  /** The permissions Discord gives a server's @everyone role in a server made today. */ const NEW_SERVER_EVERYONE_PERMISSIONS = 2248473465835073n;
2000
+ // The @everyone roles default mode made with no permissions, where strict mocks give a new server's
2001
+ const madeEveryones = new WeakSet();
2002
+ /** 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');
1546
2003
  // A server's @everyone role, which has the server's id and ranks lowest, at position 0; with strict mocks, the
1547
2004
  // permissions Discord gives it in a new server
1548
- const everyoneRole = (guildId)=>createMockInteraction(Role, {
2005
+ function everyoneRole(guildId) {
2006
+ const strict = strictMocks();
2007
+ const role = createMockInteraction(Role, {
1549
2008
  id: guildId,
1550
2009
  name: '@everyone',
1551
2010
  position: 0,
1552
- ...strictMocks() ? {
2011
+ ...strict ? {
1553
2012
  permissions: new PermissionsBitField(NEW_SERVER_EVERYONE_PERMISSIONS).freeze()
1554
2013
  } : {}
1555
2014
  });
2015
+ if (!strict) madeEveryones.add(role);
2016
+ return role;
2017
+ }
1556
2018
  /**
1557
2019
  * A guild's role manager with `roles` in its cache, and the guild's @everyone role: the one given with the guild's id,
1558
2020
  * or a role of its own at position 0, as every server has one.
@@ -1652,8 +2114,9 @@ const everyoneRole = (guildId)=>createMockInteraction(Role, {
1652
2114
  * highest, by position, then the lower id. `permissions` are computed as discord.js computes them: every permission for
1653
2115
  * the server's owner, and otherwise its roles' permissions combined, @everyone's included, so a member with no roles has
1654
2116
  * @everyone's alone. The member is put in its guild's member cache, so an interaction or a message from its user in that
1655
- * server has it as its `member`, and `memberPermissions` are its permissions. A DM sent to the member goes through its
1656
- * user's `send()` and the user's one DM channel.
2117
+ * server has it as its `member`, and `memberPermissions` are its permissions, with the interaction's channel's
2118
+ * overwrites applied under `useStrictMocks()`. A DM sent to the member goes through its user's `send()` and the user's
2119
+ * one DM channel.
1657
2120
  *
1658
2121
  * @param overrides - The member's user, server, roles and nickname; see {@link MockMemberOverrides}.
1659
2122
  * @returns The mock member.
@@ -1700,6 +2163,218 @@ const everyoneRole = (guildId)=>createMockInteraction(Role, {
1700
2163
  return guild ? cached(cacheOf(guild.channels), channelId, make) : make();
1701
2164
  }
1702
2165
  /** A server's id as a refusal names it, or a DM. */ const placeName = (guildId)=>guildId === null ? 'a DM' : `server ${guildId}`;
2166
+ /**
2167
+ * Where strict mocks place an interaction: in its message's channel and server, in a guild's or a member's server, and
2168
+ * with a guildId alone in a server the bot isn't in. Undefined where a channel given, or nothing, places it.
2169
+ */ // Mock messages whose channel and server the mock made, as neither was given
2170
+ const placesMade = new WeakSet();
2171
+ // Messages whose place reads already warn, so a message reused for several interactions is wrapped once
2172
+ const madePlaceWarned = new WeakSet();
2173
+ /**
2174
+ * Has a message whose place the mock made warn once per key when its place is read, in default mode, where strict mocks
2175
+ * move it to the interaction it was given to. A read made while reading another of these, or by the mock, stays quiet.
2176
+ */ function warnOnMadePlaceReads(message) {
2177
+ if (madePlaceWarned.has(message)) return;
2178
+ madePlaceWarned.add(message);
2179
+ for (const key of [
2180
+ 'guild',
2181
+ 'guildId',
2182
+ 'channel',
2183
+ 'channelId',
2184
+ 'member'
2185
+ ]){
2186
+ const descriptor = Object.getOwnPropertyDescriptor(message, key);
2187
+ if (!descriptor) continue;
2188
+ let value = descriptor.value;
2189
+ Object.defineProperty(message, key, {
2190
+ get: ()=>{
2191
+ if (readingPlace === 0) {
2192
+ 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.');
2193
+ }
2194
+ readingPlace++;
2195
+ try {
2196
+ return descriptor.get ? descriptor.get.call(message) : value;
2197
+ } finally{
2198
+ readingPlace--;
2199
+ }
2200
+ },
2201
+ set: (next)=>{
2202
+ if (descriptor.set) descriptor.set.call(message, next);
2203
+ else value = next;
2204
+ },
2205
+ enumerable: true,
2206
+ configurable: true
2207
+ });
2208
+ }
2209
+ }
2210
+ // The DM channel each message the bot sent in a DM in default mode was sent in, where strict mocks place it
2211
+ const sentInDm = new WeakMap();
2212
+ /** 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);
2213
+ function strictPlaceOf(own) {
2214
+ const message = own('message');
2215
+ const channel = own('channel');
2216
+ const guild = own('guild');
2217
+ const guildId = own('guildId');
2218
+ const member = own('member');
2219
+ // A message whose place the mock made has none a test meant, so where the test placed the interaction wins
2220
+ const placed = channel !== undefined || guild !== undefined || guildId !== undefined;
2221
+ if (typeof message === 'object' && message !== null && typeof message.channelId === 'string' && !(placed && placeWasMade(message))) {
2222
+ // One the bot sent in a DM is in that DM, as strict mocks place it, though default mode reads a server for it
2223
+ const dm = sentInDm.get(mockTargets.get(message) ?? message);
2224
+ const channelId = dm ? dm.id : message.channelId;
2225
+ const at = dm ? null : message.guildId ?? null;
2226
+ 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;
2227
+ return {
2228
+ given: 'message',
2229
+ channel: dm ?? message.channel,
2230
+ guildId: at,
2231
+ guild: dm ? null : message.guild ?? null,
2232
+ refused
2233
+ };
2234
+ }
2235
+ if (guild instanceof Guild && guildId === undefined) return {
2236
+ given: 'guild',
2237
+ guildId: guild.id
2238
+ };
2239
+ if (member instanceof GuildMember && member.guild instanceof Guild && guild === undefined && guildId === undefined) {
2240
+ return {
2241
+ given: 'member',
2242
+ guild: member.guild,
2243
+ guildId: member.guild.id
2244
+ };
2245
+ }
2246
+ if (typeof guildId === 'string' && guild === undefined && channel === undefined && !(member instanceof GuildMember) && !isRawMember(member)) {
2247
+ return {
2248
+ given: 'guildId',
2249
+ raw: true
2250
+ };
2251
+ }
2252
+ return undefined;
2253
+ }
2254
+ // Set while one of the reads below runs, so what it reads in turn doesn't warn again
2255
+ let readingPlace = 0;
2256
+ /** Runs `read` as the mock's own read of where a mock is, which warns about no placement. */ function readPlaceQuietly(read) {
2257
+ readingPlace++;
2258
+ try {
2259
+ return read();
2260
+ } finally{
2261
+ readingPlace--;
2262
+ }
2263
+ }
2264
+ // 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
2265
+ const placeWarners = new WeakMap();
2266
+ // 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
2267
+ let placeReadsSeen;
2268
+ /**
2269
+ * Runs `read`, a value of `mock`'s own that reads where the mock is, quietly; in default mode it warns once, as `key`,
2270
+ * where one of those reads would warn, since strict mocks place the mock otherwise, unless `differs` says the value is
2271
+ * the same in that place.
2272
+ */ function readPlaceAs(mock, key, read, differs = ()=>true) {
2273
+ const outer = placeReadsSeen;
2274
+ const seen = placeReadsSeen = new Set();
2275
+ try {
2276
+ return readPlaceQuietly(read);
2277
+ } finally{
2278
+ placeReadsSeen = outer;
2279
+ const warner = placeWarners.get(mockTargets.get(mock) ?? mock);
2280
+ if (seen.size > 0 && warner && differs(warner.place)) warner.warn(key);
2281
+ }
2282
+ }
2283
+ /** What reading `key`, where strict mocks place an interaction otherwise, warns: what it reads now, and in 5.0. */ function placeWarning(name, key, place) {
2284
+ if (place.refused) {
2285
+ 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}`;
2286
+ }
2287
+ const why = {
2288
+ message: [
2289
+ 'a place other than its message\'s',
2290
+ "the mock is in its message's channel and server",
2291
+ "Give it the message's channel, or call useStrictMocks() to place it there now."
2292
+ ],
2293
+ guild: [
2294
+ "a DM's value, though the mock was given a guild",
2295
+ 'the mock is in that guild',
2296
+ 'Give its guildId or one of its channels too, or call useStrictMocks() to place it there now.'
2297
+ ],
2298
+ member: [
2299
+ "a DM's value, though the mock was given a member of a server",
2300
+ "the mock is in the member's server",
2301
+ "Give the member's guild too, or call useStrictMocks() to place it there now."
2302
+ ],
2303
+ guildId: [
2304
+ 'a server the bot is in, though the mock was given only a guildId',
2305
+ "a guildId alone is a server the bot isn't in: guild and channel are null, and the member is a raw one",
2306
+ 'Give a guild too, or call useStrictMocks() to read it so now.'
2307
+ ]
2308
+ }[place.given];
2309
+ return `${name}.${key} reads ${why[0]} here; in the next major version (5.0) ${why[1]}. ${why[2]}`;
2310
+ }
2311
+ /** Has a read of `instance`'s `key` call `warn` first, and what that read reads in turn stay quiet. */ function warnOnRead(instance, key, warn) {
2312
+ const descriptor = Object.getOwnPropertyDescriptor(instance, key);
2313
+ // One the mock answers with a stub stays unset, as the mock's own checks read it, and warns as the stub is read
2314
+ if (!descriptor) {
2315
+ if (!stubReadWarnings.has(instance)) stubReadWarnings.set(instance, new Map());
2316
+ stubReadWarnings.get(instance).set(key, warn);
2317
+ return;
2318
+ }
2319
+ const read = ()=>descriptor.get ? descriptor.get.call(instance) : descriptor.value;
2320
+ Object.defineProperty(instance, key, {
2321
+ get: ()=>{
2322
+ warn();
2323
+ readingPlace++;
2324
+ try {
2325
+ return read();
2326
+ } finally{
2327
+ readingPlace--;
2328
+ }
2329
+ },
2330
+ set: (next)=>Object.defineProperty(instance, key, {
2331
+ value: next,
2332
+ writable: true,
2333
+ enumerable: true,
2334
+ configurable: true
2335
+ }),
2336
+ enumerable: true,
2337
+ configurable: true
2338
+ });
2339
+ }
2340
+ /**
2341
+ * Has each of an interaction's `guildId`, `guild`, `channelId`, `channel` and `member` that strict mocks read otherwise
2342
+ * warn once when read, unless the test gave it.
2343
+ */ function warnOnPlaceReads(instance, place, given) {
2344
+ const value = (key)=>{
2345
+ const descriptor = Object.getOwnPropertyDescriptor(instance, key);
2346
+ return descriptor?.get ? undefined : descriptor?.value;
2347
+ };
2348
+ const keys = place.refused ? [
2349
+ 'guildId',
2350
+ 'guild',
2351
+ 'channelId',
2352
+ 'channel'
2353
+ ] : place.raw ? [
2354
+ 'guild',
2355
+ 'channel',
2356
+ 'member'
2357
+ ] : place.guildId !== value('guildId') ? [
2358
+ 'guildId',
2359
+ 'guild',
2360
+ 'channelId',
2361
+ 'channel',
2362
+ 'member'
2363
+ ] : place.channel && place.channel.id !== value('channelId') ? [
2364
+ 'channelId',
2365
+ 'channel'
2366
+ ] : [];
2367
+ const name = Object.getPrototypeOf(instance).constructor.name;
2368
+ placeWarners.set(instance, {
2369
+ place,
2370
+ keys,
2371
+ warn: (read)=>warnOnce(mockLogger, placeWarning(name, read, place))
2372
+ });
2373
+ for (const key of keys.filter((each)=>!given.has(each))){
2374
+ // A read the mock makes stays quiet, noted for a read of the mock's own to warn by its name
2375
+ warnOnRead(instance, key, ()=>readingPlace > 0 ? void placeReadsSeen?.add(key) : warnOnce(mockLogger, placeWarning(name, key, place)));
2376
+ }
2377
+ }
1703
2378
  /**
1704
2379
  * 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
1705
2380
  * DM channel, else the server the channel names. A server channel that names none is put in `fallback`'s, and named
@@ -1744,9 +2419,11 @@ const everyoneRole = (guildId)=>createMockInteraction(Role, {
1744
2419
  cached(cacheOf(manager), id, ()=>channel);
1745
2420
  }
1746
2421
  }
1747
- /** The guild a mock message carries: a guild with the same stubbed managers as createMockGuild. */ function createMockGuildForMessage(id = nextSnowflake()) {
2422
+ /** The guild a mock message carries: a guild with the same stubbed managers as createMockGuild. */ function createMockGuildForMessage(id) {
1748
2423
  const guild = Object.create(Guild.prototype);
1749
- guild.id = id;
2424
+ const generatedId = id === undefined ? nextSnowflake() : undefined;
2425
+ guild.id = id ?? generatedId;
2426
+ defineMadeTime(guild, generatedId);
1750
2427
  guild.members = managerWith(GuildMemberManager.prototype, undefined);
1751
2428
  guild.channels = managerWith(GuildChannelManager.prototype, undefined);
1752
2429
  guild.roles = guildRoleManager(guild.id, undefined);
@@ -1755,6 +2432,23 @@ const everyoneRole = (guildId)=>createMockInteraction(Role, {
1755
2432
  settleGuild(guild, proxy);
1756
2433
  return proxy;
1757
2434
  }
2435
+ // Each settled guild's bot member as `members.me` reads it, without caching one made for the read
2436
+ const botMembers = new WeakMap();
2437
+ /** 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) {
2438
+ return botMembers.get(guild)?.() ?? guild.members.me;
2439
+ }
2440
+ /**
2441
+ * The channel an interaction's `channel` reads, for the mock's own reads: `null` in a DM, else one set on it or its
2442
+ * server's cached one, without making or caching one. Undefined where `channel` would make one, which has no overwrites.
2443
+ */ function heldChannelOf(interaction, guild) {
2444
+ const target = mockTargets.get(interaction) ?? interaction;
2445
+ return readPlaceQuietly(()=>{
2446
+ const set = Object.getOwnPropertyDescriptor(target, 'channel');
2447
+ if (set && 'value' in set) return set.value;
2448
+ const { guildId, channelId } = target;
2449
+ return guildId ? cacheOf(guild.channels)?.get(channelId) : null;
2450
+ });
2451
+ }
1758
2452
  /** 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) {
1759
2453
  for (const key of [
1760
2454
  'members',
@@ -1776,11 +2470,13 @@ const everyoneRole = (guildId)=>createMockInteraction(Role, {
1776
2470
  configurable: true
1777
2471
  });
1778
2472
  const members = instance.members;
2473
+ const botMember = (keep)=>{
2474
+ const { user } = guild.client;
2475
+ return keep ? cached(cacheOf(members), user.id, ()=>memberIn(guild, user.id, user)) : cacheOf(members)?.get(user.id) ?? memberIn(guild, user.id, user);
2476
+ };
2477
+ botMembers.set(guild, ()=>botMember(false));
1779
2478
  Object.defineProperty(members, 'me', {
1780
- get: ()=>{
1781
- const { user } = guild.client;
1782
- return cached(cacheOf(members), user.id, ()=>memberIn(guild, user.id, user));
1783
- },
2479
+ get: ()=>botMember(true),
1784
2480
  configurable: true
1785
2481
  });
1786
2482
  for (const key of [
@@ -1854,8 +2550,11 @@ const everyoneRole = (guildId)=>createMockInteraction(Role, {
1854
2550
  };
1855
2551
  // Own values, so the prototype's getters, which read the raw mention data, are not reached
1856
2552
  const instance = Object.create(MessageMentions.prototype);
2553
+ const parsedUsers = new Collection(mentioned.users);
1857
2554
  for (const [key, value] of Object.entries({
1858
2555
  ...mentioned,
2556
+ parsedUsers,
2557
+ crosspostedChannels: new Collection(),
1859
2558
  everyone: false,
1860
2559
  client,
1861
2560
  guild: guild ?? null
@@ -1868,11 +2567,14 @@ const everyoneRole = (guildId)=>createMockInteraction(Role, {
1868
2567
  return stubDeep(instance);
1869
2568
  }
1870
2569
  /** 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;
1871
- /** The message a reply method starts from: the one a component is on, as it was, or an empty original response. */ function heldFrom(message) {
2570
+ /**
2571
+ * The message a reply method starts from: the one a component is on, as it was, or an empty original response. Without
2572
+ * `allocate`, one with no id of its own gets `''`, for its holder to give it one when first read.
2573
+ */ function heldFrom(message, allocate = true) {
1872
2574
  // Read only what the message really has: a mock's unset fields are stubs
1873
2575
  const from = message ?? {};
1874
2576
  return {
1875
- id: typeof from.id === 'string' ? from.id : nextSnowflake(),
2577
+ id: typeof from.id === 'string' ? from.id : allocate ? nextSnowflake() : '',
1876
2578
  content: typeof from.content === 'string' ? from.content : undefined,
1877
2579
  components: Array.isArray(from.components) ? from.components.map(jsonOf) : [],
1878
2580
  embeds: Array.isArray(from.embeds) ? from.embeds.map(jsonOf) : [],
@@ -1880,6 +2582,15 @@ const everyoneRole = (guildId)=>createMockInteraction(Role, {
1880
2582
  editedTimestamp: typeof from.editedTimestamp === 'number' ? from.editedTimestamp : null
1881
2583
  };
1882
2584
  }
2585
+ /** A message as an answer sends it: a new one, with what the answer gave, never edited. */ const sent = (options, allocate = true)=>{
2586
+ // Its id taken only once it builds, so a payload that fails to build takes none
2587
+ const held = edited(heldFrom(undefined, false), options);
2588
+ return {
2589
+ ...held,
2590
+ id: allocate ? nextSnowflake() : '',
2591
+ editedTimestamp: null
2592
+ };
2593
+ };
1883
2594
  /** A held message after an edit: what the edit sets replaced in Discord's shape, and a later edit stamp. */ function edited(held, options) {
1884
2595
  const edit = typeof options === 'string' ? {
1885
2596
  content: options
@@ -1908,29 +2619,28 @@ const everyoneRole = (guildId)=>createMockInteraction(Role, {
1908
2619
  editedTimestamp: Math.max(Date.now(), (held.editedTimestamp ?? 0) + 1)
1909
2620
  };
1910
2621
  }
1911
- /** A mock message showing a held message. */ function messageFrom(held) {
1912
- return createMockMessage({
2622
+ /** 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) {
2623
+ return sentMessage(held, channel, client, {
1913
2624
  id: held.id,
1914
- ...held.content !== undefined && {
1915
- content: held.content
1916
- },
1917
- components: held.components,
1918
- embeds: held.embeds,
1919
- ...held.flags !== undefined && {
1920
- flags: held.flags
1921
- },
1922
2625
  editedTimestamp: held.editedTimestamp
1923
- });
2626
+ }, made);
1924
2627
  }
1925
2628
  /**
1926
- * A component or embed as a message holds it: a discord.js instance as it is; anything else as
1927
- * its API JSON at the time of the call, behind `toJSON()`.
1928
- */ function asHeld(value) {
2629
+ * A component or embed as a message holds it: a discord.js instance as it is, anything else as the discord.js class
2630
+ * built from its API JSON at the time of the call, as discord.js builds it, an unknown type as a plain `Component`.
2631
+ */ function asHeld(value, kind) {
1929
2632
  if (value instanceof Component || value instanceof Embed) return value;
1930
- const json = structuredClone(typeof value.toJSON === 'function' ? value.toJSON() : value);
1931
- return {
1932
- toJSON: ()=>json
1933
- };
2633
+ // A copy, since discord.js's classes keep what they are given, one level deep
2634
+ const json = structuredClone(jsonOf(value));
2635
+ if (kind === 'embed') return new Embed(json);
2636
+ // An action row builds each child with discord.js's own createComponent, which it doesn't export
2637
+ const row = new ActionRow({
2638
+ type: ComponentType.ActionRow,
2639
+ components: [
2640
+ json
2641
+ ]
2642
+ });
2643
+ return row.components[0];
1934
2644
  }
1935
2645
  /**
1936
2646
  * Creates a mock {@link Message} that tracks whether it has been deleted.
@@ -1939,12 +2649,14 @@ const everyoneRole = (guildId)=>createMockInteraction(Role, {
1939
2649
  * `@MessageHandler` does not skip it.
1940
2650
  *
1941
2651
  * @remarks
1942
- * `delete()`, `edit()`, `reply()`, `react()`, `pin()` and `unpin()` throw once the message is deleted, and `edit()`
1943
- * and `reply()` resolve to a new mock message. Without overrides the message is empty, in a server, from a new
1944
- * person who is its `member`; with `guild: null` it is a direct message. Give `author` to send several messages as
1945
- * one user, such as to reach a per-user cooldown. An author given, and the users, roles and channels the content
1946
- * mentions, are cached as the gateway delivers them. It is typed as discord.js emits a message, never in a group DM,
1947
- * so it fits `messageCreate` and the other message events.
2652
+ * `delete()`, `edit()`, `reply()`, `react()`, `pin()` and `unpin()` throw once the message is deleted. `edit()` changes
2653
+ * this message and resolves to it, and `reply()` resolves to the reply, sent in this message's channel and holding what
2654
+ * was sent, as a channel's `send()` does. Under `useStrictMocks()` a message the bot sends is its `client.user`'s, and
2655
+ * 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
2656
+ * `member`; with `guild: null` it is a direct message. Give `author` to send several messages as one user, such as to
2657
+ * reach a per-user cooldown. An author given, and the users, roles and channels the content mentions, are cached as the
2658
+ * gateway delivers them. It is typed as discord.js emits a message, never in a group DM, so it fits `messageCreate` and
2659
+ * the other message events.
1948
2660
  *
1949
2661
  * @param overrides - The message's content, components and the rest; see {@link MockMessageOverrides}.
1950
2662
  *
@@ -1967,11 +2679,18 @@ const everyoneRole = (guildId)=>createMockInteraction(Role, {
1967
2679
  * @see {@link MockMessageOverrides}
1968
2680
  * @see {@link createMockGuild}
1969
2681
  */ function createMockMessage(overrides = {}) {
2682
+ return messageMock(overrides);
2683
+ }
2684
+ function messageMock(overrides = {}, sentBy, made = {}, at) {
2685
+ var _made, _made1, _made2;
1970
2686
  const instance = Object.create(Message.prototype);
1971
2687
  const stubs = new Map();
2688
+ const unplaced = overrides.channel === undefined && overrides.guild === undefined;
2689
+ // A message the bot sent is placed where it was sent, even where default mode reads a server of its own for a DM
2690
+ if (unplaced && !sentBy) placesMade.add(instance);
1972
2691
  instance.deleted = false;
1973
2692
  // Constructor-assigned — set as prototype-based stubs; a user rather than a bot, as dispatch handles only those
1974
- instance.author = overrides.author ?? mockUser();
2693
+ instance.author = overrides.author ?? (_made = made).author ?? (_made.author = mockUser());
1975
2694
  // Getters on the prototype, which the proxy would answer with a stub object: own properties shadow them
1976
2695
  // A channel given says where the message was sent, unless its guild is given too
1977
2696
  const place = overrides.channel ? placeOf(overrides.channel, overrides.guild === undefined ? undefined : overrides.guild?.id ?? null, ()=>{
@@ -1981,22 +2700,37 @@ const everyoneRole = (guildId)=>createMockInteraction(Role, {
1981
2700
  guild
1982
2701
  };
1983
2702
  }) : undefined;
1984
- const guild = overrides.guild !== undefined ? overrides.guild : place?.guildId !== undefined ? place.guildId === null ? null : place.guild ?? createMockGuildForMessage(place.guildId) : createMockGuildForMessage();
1985
- const channel = overrides.channel ?? channelFor(guild, guild?.id ?? null, nextSnowflake(), instance.author);
1986
- Object.defineProperty(instance, 'channel', {
2703
+ 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();
2704
+ const madeChannel = ()=>channelFor(guild, guild?.id ?? null, nextSnowflake(), instance.author);
2705
+ const channel = at ? undefined : overrides.channel ?? (unplaced ? (_made2 = made).channel ?? (_made2.channel = madeChannel()) : madeChannel());
2706
+ if (channel) Object.defineProperty(instance, 'channel', {
1987
2707
  value: channel,
1988
- writable: true
2708
+ writable: true,
2709
+ configurable: true
1989
2710
  });
2711
+ else {
2712
+ Object.defineProperty(instance, 'channel', {
2713
+ get: at.channel,
2714
+ set: (value)=>Object.defineProperty(instance, 'channel', {
2715
+ value,
2716
+ writable: true,
2717
+ configurable: true
2718
+ }),
2719
+ configurable: true
2720
+ });
2721
+ }
1990
2722
  Object.defineProperty(instance, 'guild', {
1991
2723
  value: guild,
1992
- writable: true
2724
+ writable: true,
2725
+ configurable: true
1993
2726
  });
1994
2727
  // The author as a member of the message's server; a direct message has none. A given author's member is the one
1995
2728
  // the server caches, as the gateway resolves it, so every message from that author has the same member
1996
- // The author's member, cached as the gateway caches it with the message; `member` reads the cache, as discord.js does
2729
+ // The author's member, cached as the gateway caches it with the message; `member` reads the cache, as discord.js
2730
+ // does, without calling the manager's resolve a test may stub
1997
2731
  if (guild) memberFor(guild, instance.author, true);
1998
2732
  Object.defineProperty(instance, 'member', {
1999
- get: ()=>guild ? guild.members.resolve(instance.author) : null,
2733
+ get: ()=>instance.guild ? cacheOf(instance.guild.members)?.get(instance.author.id) ?? null : null,
2000
2734
  set: (value)=>Object.defineProperty(instance, 'member', {
2001
2735
  value,
2002
2736
  writable: true,
@@ -2007,8 +2741,8 @@ const everyoneRole = (guildId)=>createMockInteraction(Role, {
2007
2741
  configurable: true
2008
2742
  });
2009
2743
  // In a server's text channel, the ids matching the objects; with no guild, a DM
2010
- instance.channelId = channel.id;
2011
- instance.guildId = guild?.id ?? null;
2744
+ instance.channelId = channel?.id ?? at.channelId;
2745
+ instance.guildId = at ? at.guildId : guild?.id ?? null;
2012
2746
  // The thread its channel caches under its id, as discord.js reads it; until 5.0, a placeholder thread otherwise
2013
2747
  let placeholderThread;
2014
2748
  Object.defineProperty(instance, 'thread', {
@@ -2038,7 +2772,7 @@ const everyoneRole = (guildId)=>createMockInteraction(Role, {
2038
2772
  configurable: true
2039
2773
  });
2040
2774
  if (guild instanceof Guild) cached(cacheOf(client.guilds), guild.id, ()=>guild);
2041
- cacheChannel(channel, guild, client);
2775
+ if (channel) cacheChannel(channel, guild, client);
2042
2776
  const userCache = cacheOf(client.users);
2043
2777
  for (const user of [
2044
2778
  ...overrides.users ?? [],
@@ -2048,8 +2782,8 @@ const everyoneRole = (guildId)=>createMockInteraction(Role, {
2048
2782
  instance.mentions = mentionsOf(overrides.content, client, guild);
2049
2783
  // Data a message always has, real rather than stubbed, so code reading it sees an empty message
2050
2784
  instance.flags = new MessageFlagsBitField(overrides.flags);
2051
- instance.components = (overrides.components ?? []).map(asHeld);
2052
- instance.embeds = (overrides.embeds ?? []).map(asHeld);
2785
+ instance.components = (overrides.components ?? []).map((component)=>asHeld(component, 'component'));
2786
+ instance.embeds = (overrides.embeds ?? []).map((embed)=>asHeld(embed, 'embed'));
2053
2787
  instance.attachments = new Collection();
2054
2788
  instance.editedTimestamp = overrides.editedTimestamp ?? null;
2055
2789
  const generatedId = overrides.id === undefined ? nextSnowflake() : undefined;
@@ -2062,14 +2796,27 @@ const everyoneRole = (guildId)=>createMockInteraction(Role, {
2062
2796
  if (instance.deleted) throw alreadyDeleted();
2063
2797
  instance.deleted = true;
2064
2798
  }));
2065
- stubs.set('edit', createMockFn(async ()=>{
2799
+ // An edit changes this message, as Discord edits it in place, and resolves to it
2800
+ stubs.set('edit', createMockFn(async (options)=>{
2066
2801
  if (instance.deleted) throw alreadyDeleted();
2067
- return createMockMessage();
2802
+ const held = edited(heldFrom(undefined, false), options);
2803
+ const edit = typeof options === 'string' ? {
2804
+ content: options
2805
+ } : options ?? {};
2806
+ if (edit.content !== undefined) instance.content = held.content;
2807
+ if (edit.components !== undefined) instance.components = held.components.map((component)=>asHeld(component, 'component'));
2808
+ if (edit.embeds !== undefined) instance.embeds = held.embeds.map((embed)=>asHeld(embed, 'embed'));
2809
+ if (edit.flags !== undefined) instance.flags = new MessageFlagsBitField(held.flags);
2810
+ instance.editedTimestamp = Math.max(Date.now(), (instance.editedTimestamp ?? 0) + 1);
2811
+ return message;
2068
2812
  }));
2069
- stubs.set('reply', createMockFn(async ()=>{
2813
+ stubs.set('reply', createMockFn(async (options)=>{
2070
2814
  if (instance.deleted) throw alreadyDeleted();
2071
- return createMockMessage();
2815
+ return sentMessage(sent(options, false), instance.channel, instance.client);
2072
2816
  }));
2817
+ // Crossposting publishes this message, and forwarding sends one in the channel it is forwarded to
2818
+ stubs.set('crosspost', createMockFn(async ()=>message));
2819
+ stubs.set('forward', createMockFn(async (channel)=>sentMessage(sent(undefined, false), channel, instance.client)));
2073
2820
  stubs.set('react', createMockFn(async ()=>{
2074
2821
  if (instance.deleted) throw alreadyDeleted();
2075
2822
  }));
@@ -2079,7 +2826,85 @@ const everyoneRole = (guildId)=>createMockInteraction(Role, {
2079
2826
  stubs.set('unpin', createMockFn(async ()=>{
2080
2827
  if (instance.deleted) throw alreadyDeleted();
2081
2828
  }));
2082
- return stubDeep(instance, stubs);
2829
+ if (sentBy) warnOnSentReads(instance, stubs, sentBy);
2830
+ const message = stubDeep(instance, stubs);
2831
+ return message;
2832
+ }
2833
+ /**
2834
+ * Has a message the bot sent warn once where strict mocks read it otherwise: its author, the bot's `client.user` under
2835
+ * strict mocks, and, for one sent in a DM, the server it reads as being in.
2836
+ */ function warnOnSentReads(instance, stubs, sentBy) {
2837
+ // A read made while reading another of these, as `member` reads `author`, warns only for the outer one
2838
+ let reading = 0;
2839
+ const read = (key, warning)=>{
2840
+ const descriptor = Object.getOwnPropertyDescriptor(instance, key);
2841
+ Object.defineProperty(instance, key, {
2842
+ get: ()=>{
2843
+ if (reading === 0) warnOnce(mockLogger, warning);
2844
+ reading++;
2845
+ try {
2846
+ return descriptor?.get ? descriptor.get.call(instance) : descriptor?.value;
2847
+ } finally{
2848
+ reading--;
2849
+ }
2850
+ },
2851
+ set: (next)=>Object.defineProperty(instance, key, {
2852
+ value: next,
2853
+ writable: true,
2854
+ enumerable: true,
2855
+ configurable: true
2856
+ }),
2857
+ enumerable: true,
2858
+ configurable: true
2859
+ });
2860
+ };
2861
+ if (sentBy.bot) {
2862
+ 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.");
2863
+ }
2864
+ if (!sentBy.dm) return;
2865
+ 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.";
2866
+ const inGuild = Boolean(instance.guildId);
2867
+ for (const key of [
2868
+ 'guild',
2869
+ 'guildId',
2870
+ 'member'
2871
+ ])read(key, inDm(key));
2872
+ stubs.set('inGuild', createMockFn(()=>(warnOnce(mockLogger, inDm('inGuild()')), inGuild)));
2873
+ }
2874
+ /**
2875
+ * A message the bot sent, as Discord returns it: in the channel it was sent in and that channel's server, holding what
2876
+ * 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
2877
+ * a DM send in a server of its own and keeps another author, with a warning when read.
2878
+ */ function sentMessage(held, channel, client, fields = {}, made) {
2879
+ const by = client ?? channel?.client;
2880
+ const inChannel = channel instanceof BaseChannel;
2881
+ const dm = inChannel && channel.isDMBased();
2882
+ const strict = strictMocks();
2883
+ const message = messageMock({
2884
+ ...held.content !== undefined && {
2885
+ content: held.content
2886
+ },
2887
+ components: held.components,
2888
+ embeds: held.embeds,
2889
+ ...held.flags !== undefined && {
2890
+ flags: held.flags
2891
+ },
2892
+ ...inChannel && (!dm || strict) && {
2893
+ channel: channel
2894
+ },
2895
+ ...by && {
2896
+ client: by
2897
+ },
2898
+ ...strict && by?.user && {
2899
+ author: by.user
2900
+ },
2901
+ ...fields
2902
+ }, strict ? undefined : {
2903
+ bot: Boolean(by?.user),
2904
+ dm
2905
+ }, made);
2906
+ if (dm && !strict) sentInDm.set(mockTargets.get(message) ?? message, channel);
2907
+ return message;
2083
2908
  }
2084
2909
  /** The option type Discord would have sent for a given JavaScript value. */ function optionTypeOf(value) {
2085
2910
  if (typeof value === 'boolean') return ApplicationCommandOptionType.Boolean;
@@ -2168,11 +2993,32 @@ const everyoneRole = (guildId)=>createMockInteraction(Role, {
2168
2993
  else if (isEntity) option.user = value;
2169
2994
  return option;
2170
2995
  }
2996
+ /**
2997
+ * The option an autocomplete's user is typing, as Discord sends it: its value a string, `''` before anything is typed.
2998
+ * A number given for it is its digits under strict mocks; otherwise it stays a number, with a warning when read.
2999
+ */ function focusedOptionData(name, value) {
3000
+ const option = toOptionData(name, value ?? '');
3001
+ option.focused = true;
3002
+ if (typeof value === 'number') {
3003
+ if (strictMocks()) option.value = String(value);
3004
+ else {
3005
+ Object.defineProperty(option, 'value', {
3006
+ get: ()=>{
3007
+ 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.`);
3008
+ return value;
3009
+ },
3010
+ enumerable: true
3011
+ });
3012
+ }
3013
+ }
3014
+ return option;
3015
+ }
2171
3016
  /**
2172
3017
  * Nests the supplied options under the subcommand path they were invoked through,
2173
3018
  * matching the shape Discord sends rather than a flat list.
2174
- */ function buildOptionData(subcommandGroup, subcommand, values, memberOf) {
2175
- const leaves = Object.entries(values).map(([name, value])=>toOptionData(name, value, memberOf));
3019
+ */ function buildOptionData(subcommandGroup, subcommand, values, memberOf, focused) {
3020
+ const leaves = Object.entries(values).map(([name, value])=>name === focused ? focusedOptionData(name, value) : toOptionData(name, value, memberOf));
3021
+ if (focused !== null && !Object.hasOwn(values, focused)) leaves.push(focusedOptionData(focused, null));
2176
3022
  if (subcommand === null) return leaves;
2177
3023
  const sub = {
2178
3024
  name: subcommand,
@@ -2212,6 +3058,10 @@ const everyoneRole = (guildId)=>createMockInteraction(Role, {
2212
3058
  * A whole number is an Integer option and a fraction a Number one, so
2213
3059
  * `getInteger()` reads only a whole number, and `getNumber()` reads either.
2214
3060
  *
3061
+ * The `focused` option reads as Discord sends it, through `getFocused()`, `getFocused(true)` and `data` alike: a
3062
+ * string, `''` when no value is given, and marked `focused` in `data`. A number given for it is its digits under
3063
+ * `useStrictMocks()`; otherwise it stays the number, with a warning when read.
3064
+ *
2215
3065
  * @typeParam Cached - `any` by default, to match `createMockInteraction(ChatInputCommandInteraction)`; pass it when the
2216
3066
  * interaction under test is cache-pinned.
2217
3067
  *
@@ -2332,17 +3182,21 @@ const everyoneRole = (guildId)=>createMockInteraction(Role, {
2332
3182
  ], ()=>false, required));
2333
3183
  base.getFocused = createMockFn((getFull)=>{
2334
3184
  if (focused === null) throw optionError(DiscordjsErrorCodes.AutocompleteInteractionOptionNoFocusedOption);
2335
- const option = toOptionData(focused, values[focused] ?? null, memberOf);
3185
+ // The one options.data holds, so the two agree
3186
+ const option = base._hoistedOptions.find((each)=>each.focused);
2336
3187
  return getFull === true ? {
2337
- ...option,
2338
- focused: true
3188
+ ...option
2339
3189
  } : option.value;
2340
3190
  });
2341
3191
  // `data` is what the framework reads to build a handler's params, so it is materialised here rather than
2342
3192
  // auto-stubbed, or every params assertion would see an empty record.
2343
- base.data = buildOptionData(subcommandGroup, subcommand, values, memberOf);
3193
+ base.data = buildOptionData(subcommandGroup, subcommand, values, memberOf, focused);
3194
+ // The fields discord.js's constructor sets from it, which its toString() reads
3195
+ base._group = subcommandGroup;
3196
+ base._subcommand = subcommand;
3197
+ base._hoistedOptions = subcommand === null ? base.data : (subcommandGroup === null ? base.data[0] : base.data[0].options[0]).options;
2344
3198
  const resolver = stubDeep(base);
2345
3199
  return resolver;
2346
3200
  }
2347
3201
 
2348
- export { RESPONSE_LOG, createChatInputOptions, createMock, createMockChannel, createMockClient, createMockGuild, createMockInteraction, createMockMember, createMockMessage, createMockRawMember, createMockUser, isRawMember, memberRoles };
3202
+ export { NEW_SERVER_EVERYONE_PERMISSIONS, RESPONSE_LOG, botMemberOf, createChatInputOptions, createMock, createMockChannel, createMockClient, createMockGuild, createMockInteraction, createMockMember, createMockMessage, createMockRawMember, createMockUser, heldChannelOf, isRawMember, isUnsetEveryone, memberRoles, noteReactionDispatch, ownedManager, readPlaceAs };