meocord 4.2.2 → 4.2.3

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 (136) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/dist/cjs/_shared/{controller.decorator-U2NBWcoU.cjs → controller.decorator-hp5xdJU4.cjs} +512 -183
  3. package/dist/cjs/_shared/{startup-roots-lA5xkBFq.cjs → event-sources-DzqTODXg.cjs} +265 -80
  4. package/dist/cjs/_shared/{providers-B4MOFBrW.cjs → providers-BBvbnmLP.cjs} +780 -281
  5. package/dist/cjs/_shared/{sharded-cooldown-store-HTUb3Ii_.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
  6. package/dist/cjs/_shared/{translate-error-CbBHUu5P.cjs → translate-error-wsgfsUET.cjs} +1 -1
  7. package/dist/cjs/common/index.cjs +4 -104
  8. package/dist/cjs/core/index.cjs +432 -181
  9. package/dist/cjs/decorator/index.cjs +85 -120
  10. package/dist/cjs/testing/index.cjs +1664 -784
  11. package/dist/cli.json +1 -1
  12. package/dist/esm/bin/app-template/package.json.template +9 -9
  13. package/dist/esm/bin/app-template/src/main.ts.template +3 -2
  14. package/dist/esm/bin/generator.js +6 -4
  15. package/dist/esm/bin/helper/filter-generator.helper.js +6 -4
  16. package/dist/esm/bin/helper/guard-generator.helper.js +6 -4
  17. package/dist/esm/bin/helper/interceptor-generator.helper.js +6 -4
  18. package/dist/esm/bin/helper/observer-generator.helper.js +6 -4
  19. package/dist/esm/bin/helper/pipe-generator.helper.js +6 -4
  20. package/dist/esm/bin/helper/service-generator.helper.js +6 -4
  21. package/dist/esm/bin/meocord.js +106 -48
  22. package/dist/esm/build/native-addons.js +75 -19
  23. package/dist/esm/build/rsbuild-config.js +11 -3
  24. package/dist/esm/build/typed-externals.js +15 -2
  25. package/dist/esm/common/cooldown-store.js +38 -14
  26. package/dist/esm/common/deprecation.js +4 -1
  27. package/dist/esm/common/execution-context.js +3 -1
  28. package/dist/esm/common/explained-error.js +7 -3
  29. package/dist/esm/common/log-level.js +13 -9
  30. package/dist/esm/common/logger.js +4 -2
  31. package/dist/esm/common/meocord-text.js +11 -6
  32. package/dist/esm/common/redis-cooldown-store.js +3 -7
  33. package/dist/esm/common/response/flags.js +17 -1
  34. package/dist/esm/common/response/install-context.js +27 -16
  35. package/dist/esm/common/response/presenter.js +7 -3
  36. package/dist/esm/common/response/response-state.js +27 -5
  37. package/dist/esm/common/sharded-cooldown-store.js +11 -9
  38. package/dist/esm/common/theme.js +3 -1
  39. package/dist/esm/common/translator.js +17 -8
  40. package/dist/esm/core/autocomplete-routes.js +26 -0
  41. package/dist/esm/core/command-conflicts.js +15 -7
  42. package/dist/esm/core/command-registration.js +15 -1
  43. package/dist/esm/core/cooldown-runner.js +7 -9
  44. package/dist/esm/core/defer.js +2 -5
  45. package/dist/esm/core/dispatcher.js +83 -58
  46. package/dist/esm/core/event-requirements.js +67 -19
  47. package/dist/esm/core/event-sources.js +100 -0
  48. package/dist/esm/core/fallback.js +3 -1
  49. package/dist/esm/core/filter-runner.js +4 -4
  50. package/dist/esm/core/guard-runner.js +173 -52
  51. package/dist/esm/core/handler-input.js +24 -3
  52. package/dist/esm/core/handler-pipeline.js +44 -28
  53. package/dist/esm/core/handler-registry.js +51 -21
  54. package/dist/esm/core/input-runner.js +7 -8
  55. package/dist/esm/core/interceptor-runner.js +4 -5
  56. package/dist/esm/core/lifecycle-hooks.js +3 -8
  57. package/dist/esm/core/lifecycle-order.js +1 -15
  58. package/dist/esm/core/meocord-classes.js +14 -1
  59. package/dist/esm/core/meocord-factory.js +80 -53
  60. package/dist/esm/core/meocord.app.js +174 -89
  61. package/dist/esm/core/message-help.js +5 -9
  62. package/dist/esm/core/message-params.js +126 -46
  63. package/dist/esm/core/message-routes.js +225 -45
  64. package/dist/esm/core/message-words.js +30 -2
  65. package/dist/esm/core/observer-runner.js +2 -1
  66. package/dist/esm/core/process-state.js +19 -0
  67. package/dist/esm/core/providers.js +28 -3
  68. package/dist/esm/core/scalar-types.js +57 -16
  69. package/dist/esm/core/shard-context.js +11 -10
  70. package/dist/esm/core/shard-exit.js +3 -3
  71. package/dist/esm/core/shard-manager.js +16 -6
  72. package/dist/esm/core/startup-roots.js +28 -8
  73. package/dist/esm/core/theme-resolvers.js +26 -11
  74. package/dist/esm/core/theme-runtime.js +23 -13
  75. package/dist/esm/core/theme-scope.js +27 -29
  76. package/dist/esm/core/theme-validation.js +1 -1
  77. package/dist/esm/decorator/app.decorator.js +6 -1
  78. package/dist/esm/decorator/command-builder.decorator.js +14 -0
  79. package/dist/esm/decorator/controller-class.decorator.js +1 -1
  80. package/dist/esm/decorator/controller.decorator.js +48 -19
  81. package/dist/esm/decorator/defer.decorator.js +2 -2
  82. package/dist/esm/decorator/event.decorator.js +4 -5
  83. package/dist/esm/decorator/filter.decorator.js +2 -2
  84. package/dist/esm/decorator/guard.decorator.js +4 -61
  85. package/dist/esm/decorator/inject.decorator.js +4 -1
  86. package/dist/esm/decorator/interceptor.decorator.js +2 -2
  87. package/dist/esm/decorator/theme.decorator.js +4 -3
  88. package/dist/esm/decorator/validation.decorator.js +5 -5
  89. package/dist/esm/package.json.js +1 -1
  90. package/dist/esm/testing/catalog.js +29 -13
  91. package/dist/esm/testing/cooldown-store-suite.js +3 -3
  92. package/dist/esm/testing/discord-defaults.js +449 -355
  93. package/dist/esm/testing/inspect-handler.js +91 -10
  94. package/dist/esm/testing/meocord-testing-module.js +195 -77
  95. package/dist/esm/testing/mock-fn.js +8 -2
  96. package/dist/esm/testing/mock-interaction.js +639 -113
  97. package/dist/esm/testing/response-log.js +8 -0
  98. package/dist/esm/testing/response.js +7 -5
  99. package/dist/esm/testing/routing.js +48 -6
  100. package/dist/esm/testing/strict-mocks.js +1 -0
  101. package/dist/esm/testing/theme.js +3 -2
  102. package/dist/esm/util/common.util.js +30 -9
  103. package/dist/esm/util/generator-cli.util.js +6 -4
  104. package/dist/esm/util/inherited-env.util.js +72 -13
  105. package/dist/esm/util/json.util.js +10 -3
  106. package/dist/esm/util/meocord-cli.util.js +3 -2
  107. package/dist/esm/util/meocord-config-loader.util.js +14 -1
  108. package/dist/esm/util/meocord-config-validation.util.js +13 -3
  109. package/dist/esm/util/meocord-source-config.util.js +13 -3
  110. package/dist/esm/util/platform.util.js +3 -2
  111. package/dist/esm/util/refusal.util.js +29 -11
  112. package/dist/esm/util/registration-mode.util.js +14 -1
  113. package/dist/esm/util/timer-limit.util.js +4 -1
  114. package/dist/esm/util/tsconfig-paths.util.js +38 -7
  115. package/dist/esm/util/tsconfig.util.js +16 -58
  116. package/dist/esm/util/user-text.util.js +8 -4
  117. package/dist/esm/util/value.util.js +15 -4
  118. package/dist/types/common/index.d.cts +13 -9
  119. package/dist/types/common/index.d.ts +13 -9
  120. package/dist/types/core/index.d.cts +21 -11
  121. package/dist/types/core/index.d.ts +21 -11
  122. package/dist/types/decorator/index.d.cts +63 -63
  123. package/dist/types/decorator/index.d.ts +63 -63
  124. package/dist/types/{index-DKJclIi-.d.ts → index-Brkd6QGL.d.ts} +81 -35
  125. package/dist/types/{index-Dlf0ZZtS.d.cts → index-CqXImwF_.d.cts} +81 -35
  126. package/dist/types/interface/index.d.cts +1 -1
  127. package/dist/types/interface/index.d.ts +1 -1
  128. package/dist/types/{route-Bh1U13yy.d.ts → route-IQrK-MN2.d.cts} +11 -3
  129. package/dist/types/{route-Bh1U13yy.d.cts → route-bI-okq7g.d.ts} +11 -3
  130. package/dist/types/testing/index.d.cts +137 -54
  131. package/dist/types/testing/index.d.ts +137 -54
  132. package/dist/types/{theme-resolvers-CyEz9pnv.d.cts → theme-resolvers-AtgeaYqF.d.cts} +1 -1
  133. package/dist/types/{theme-resolvers-DA204O-q.d.ts → theme-resolvers-BgqysV8d.d.ts} +1 -1
  134. package/dist/types/{translator-Bg7hwxDF.d.cts → translator-D8aydlCE.d.ts} +12 -11
  135. package/dist/types/{translator-CeP4bCzp.d.ts → translator-iJhDlgwL.d.cts} +12 -11
  136. package/package.json +5 -2
@@ -1,15 +1,15 @@
1
1
  import 'reflect-metadata';
2
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, GuildEmojiManager, GuildEmoji, GuildStickerManager, Sticker, GuildScheduledEventManager, GuildScheduledEvent, AutoModerationRuleManager, AutoModerationRule, GuildInviteManager, Invite, 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';
3
+ import { DataManager, RoleManager, Role, GuildChannel, GuildMember, User, BaseGuild, MessageMentions, ThreadChannel, Message, BaseGuildVoiceChannel, VoiceChannel, BaseChannel, MessageReaction, ChannelType, MediaChannel, ForumChannel, DirectoryChannel, StageChannel, NewsChannel, CategoryChannel, TextChannel, InteractionType, ComponentType, ApplicationCommandType, GuildMemberManager, GuildMemberRoleManager, GuildChannelManager, UserManager, ChannelManager, GuildManager, Guild, GuildBanManager, GuildBan, ThreadMemberManager, ThreadMember, GuildMessageManager, DMMessageManager, GuildTextThreadManager, GuildForumThreadManager, PermissionOverwriteManager, PermissionOverwrites, ReactionManager, ReactionUserManager, PresenceManager, Presence, GuildEmojiManager, GuildEmoji, GuildStickerManager, Sticker, GuildScheduledEventManager, GuildScheduledEvent, AutoModerationRuleManager, AutoModerationRule, GuildInviteManager, Invite, ApplicationCommandOptionType, CommandInteractionOptionResolver, Client, ApplicationCommandManager, ClientUser, CDN, AuthorizingIntegrationOwners, BaseInteraction, UserContextMenuCommandInteraction, MessageContextMenuCommandInteraction, Attachment, Locale, StringSelectMenuInteraction, UserSelectMenuInteraction, RoleSelectMenuInteraction, ChannelSelectMenuInteraction, MentionableSelectMenuInteraction, Collection, PermissionsBitField, Base, SnowflakeUtil, DMChannel, MessageFlagsBitField, BaseManager, Component, Embed, ActionRow, GuildEmojiRoleManager, ApplicationCommand, DiscordjsErrorCodes, DiscordjsTypeError, MessageFlags, DiscordjsError } 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, placeholderValue } from './discord-defaults.js';
6
+ import { discordDefaults, REAL_GETTER, placeholderValue } from './discord-defaults.js';
7
7
  import { Logger } from '../common/logger.js';
8
8
  import { warnPlaceholder, warnOnce } from '../common/deprecation.js';
9
9
  import { embedsAsDiscordStores, asDiscordStores } from './discord-shape.js';
10
10
  import { nextSnowflake, MOCK_BOT_ID } from './snowflake.js';
11
11
  import { strictMocks, noteMockMade } from './strict-mocks.js';
12
- import { sharedName } from '../util/shared-state.util.js';
12
+ import { RESPONSE_LOG } from './response-log.js';
13
13
 
14
14
  // ---------------------------------------------------------------------------
15
15
  // stubDeep — Proxy that answers each property it lacks: a mock fn for a method, a default, or a nested stub
@@ -25,8 +25,12 @@ const SKIP = new Set([
25
25
  const mockTargets = new WeakMap();
26
26
  // Warnings a read answered by the stub logs first, by the mock's instance and the property
27
27
  const stubReadWarnings = new WeakMap();
28
+ // When each mock was made, which what Discord stamps when a structure arrives, such as a member's join, reads
29
+ const madeTimes = new WeakMap();
30
+ /** When `mock` was made: the time a value Discord stamps as it sends the structure reads. */ const madeAt = (mock)=>madeTimes.get(mock) ?? Date.now();
28
31
  function stubDeep(instance, externalStubs, onSet) {
29
32
  noteMockMade();
33
+ const made = Date.now();
30
34
  const stubs = externalStubs ?? new Map();
31
35
  // Placeholders read so far, by the value strict mocks give: a read of a kept one still warns, once
32
36
  const placeholders = new Map();
@@ -91,7 +95,7 @@ function stubDeep(instance, externalStubs, onSet) {
91
95
  }
92
96
  // A client the mock's structures share: its guild's, or one of its own
93
97
  if (key === 'client' && target instanceof Base) return clientOf(target);
94
- const stub = typeof protoValue === 'function' ? isRealMethod(target, key) ? createMockFn((...args)=>quietly(target, key, ()=>protoValue.apply(proxy, args))) : methodStub(target, key, protoValue, ()=>proxy) : stubDeep({});
98
+ const stub = typeof protoValue === 'function' ? isRealMethod(target, key) ? createMockFn((...args)=>lacksServer(target, key, proxy) ? undefined : quietly(target, key, ()=>protoValue.apply(proxy, args))) : methodStub(target, key, protoValue, ()=>proxy) : autoStub();
95
99
  stubs.set(key, stub);
96
100
  return stub;
97
101
  },
@@ -114,6 +118,7 @@ function stubDeep(instance, externalStubs, onSet) {
114
118
  }
115
119
  });
116
120
  mockTargets.set(proxy, instance);
121
+ madeTimes.set(proxy, made);
117
122
  return proxy;
118
123
  }
119
124
  // discord.js methods that read only what the mock holds, so they run for real: looking items up in a manager's cache,
@@ -187,6 +192,32 @@ const STRICT_REAL_METHODS = new Map([
187
192
  ])
188
193
  ]
189
194
  ]);
195
+ // A channel's permission methods, each of which reads the channel's server
196
+ const SERVER_PERMISSION_METHODS = new Set([
197
+ 'permissionsFor',
198
+ 'overwritesFor',
199
+ 'memberPermissions',
200
+ 'rolePermissions'
201
+ ]);
202
+ // The stubs a mock makes for what a test never gave it, as opposed to anything a test passes or sets
203
+ const autoStubs = new WeakSet();
204
+ function autoStub() {
205
+ const stub = stubDeep({});
206
+ autoStubs.add(stub);
207
+ return stub;
208
+ }
209
+ /**
210
+ * Whether a channel's permission method has no server to read: in default mode a channel made without one holds the
211
+ * mock's own stub for it, so the method returns `undefined`, as a thread's does, and says so once. A server the test
212
+ * gave, of whatever class, is read as it is. Strict mocks give the channel one.
213
+ */ function lacksServer(target, key, channel) {
214
+ if (strictMocks() || !(target instanceof GuildChannel) || !SERVER_PERMISSION_METHODS.has(key)) return false;
215
+ const guild = readPlaceQuietly(()=>channel.guild);
216
+ if (typeof guild !== 'object' || guild === null || !autoStubs.has(guild)) return false;
217
+ const name = Object.getPrototypeOf(target).constructor.name;
218
+ warnPlaceholder(mockLogger, `${name}.${key}`, 'the mock gives the channel a server and computes it', `Give the channel a guild, such as createMockChannel(${name}, { guild }), or call useStrictMocks() to have the mock compute it now.`);
219
+ return true;
220
+ }
190
221
  function isRealMethod(target, key) {
191
222
  for(let proto = Object.getPrototypeOf(target); proto !== null; proto = Object.getPrototypeOf(proto)){
192
223
  if (REAL_METHODS.get(proto)?.has(key) || strictMocks() && STRICT_REAL_METHODS.get(proto)?.has(key)) return true;
@@ -331,28 +362,68 @@ function clientOf(target) {
331
362
  // ---------------------------------------------------------------------------
332
363
  // methodStub — what an auto-stubbed discord.js method returns
333
364
  // ---------------------------------------------------------------------------
334
- // discord.js methods not declared `async` that still return a promise; `set*` setters are matched by name
365
+ // discord.js methods not declared `async` that still return a promise; `set*` setters are matched by name.
366
+ // mock-promise-methods.spec checks this list against discord.js's typings
335
367
  const PROMISE_METHODS = new Set([
368
+ 'awaitMessageComponent',
369
+ 'awaitMessages',
370
+ 'awaitReactions',
336
371
  'ban',
372
+ 'bulkBan',
337
373
  'clone',
374
+ 'create',
338
375
  'createDM',
339
376
  'createInvite',
377
+ 'createStageInstance',
378
+ 'createWebhook',
379
+ 'delete',
380
+ 'deleteDM',
340
381
  'disableCommunicationUntil',
382
+ 'disconnect',
341
383
  'edit',
342
384
  'fetch',
385
+ 'fetchAuthor',
343
386
  'fetchFlags',
344
387
  'fetchInvites',
345
388
  'fetchMe',
346
389
  'fetchReply',
390
+ 'fetchSubscribers',
391
+ 'fetchUser',
392
+ 'fetchVoters',
347
393
  'fetchWebhooks',
348
394
  'fetchWidget',
349
395
  'forward',
396
+ 'kick',
350
397
  'pin',
398
+ 'rawFetchGuildActiveThreads',
351
399
  'removeAttachments',
352
400
  'suppressEmbeds',
353
401
  'timeout',
402
+ 'unban',
354
403
  'unpin'
355
404
  ]);
405
+ // Methods that return a promise on one class only, as their name is synchronous on another: a role manager's set, and
406
+ // a scheduled event's setStatus, where a client user's setStatus applies at once
407
+ const PROMISE_METHODS_OF = [
408
+ [
409
+ GuildEmojiRoleManager,
410
+ new Set([
411
+ 'set'
412
+ ])
413
+ ],
414
+ [
415
+ GuildMemberRoleManager,
416
+ new Set([
417
+ 'set'
418
+ ])
419
+ ],
420
+ [
421
+ GuildScheduledEvent,
422
+ new Set([
423
+ 'setStatus'
424
+ ])
425
+ ]
426
+ ];
356
427
  // The client user's presence setters, which apply at once and return the presence
357
428
  const SYNC_SETTERS = new Set([
358
429
  'setActivity',
@@ -377,27 +448,129 @@ const SELF_METHODS = new Set([
377
448
  'disableCommunicationUntil',
378
449
  'edit',
379
450
  'fetch',
451
+ 'kick',
380
452
  'pin',
381
453
  'timeout',
382
454
  'unpin'
383
455
  ]);
456
+ // Collectors that resolve to what they collected: nothing, for a mock no one answers
457
+ const COLLECTING_METHODS = new Set([
458
+ 'awaitMessages',
459
+ 'awaitReactions'
460
+ ]);
384
461
  // The DM channel of each user, made once, as Discord keeps one per user
385
462
  const dmChannels = new WeakMap();
386
- function dmChannelOf(user) {
463
+ /**
464
+ * A user's one DM channel: its recipient is the user, and it is the user's `dmChannel`. Under strict mocks its client
465
+ * is the user's; default mode keeps a client of its own, and a read of it warns once where it isn't the user's.
466
+ */ function dmChannelOf(user) {
387
467
  let channel = dmChannels.get(user);
388
468
  if (!channel) {
389
- channel = createMockChannel(DMChannel);
390
- channel.recipientId = user.id;
469
+ const made = createMockChannel(DMChannel);
470
+ // Getters in discord.js, so set through the mock, which defines them
471
+ made.recipientId = user.id;
472
+ if (user instanceof User) {
473
+ // Not enumerated, so a DM channel's own keys, and a user's, stay those a test may compare
474
+ const place = (target, key, value)=>Object.defineProperty(target, key, {
475
+ value,
476
+ writable: true,
477
+ configurable: true
478
+ });
479
+ place(made, 'recipient', user);
480
+ place(user, 'dmChannel', made);
481
+ if (user.client instanceof Client) {
482
+ if (strictMocks()) place(made, 'client', user.client);
483
+ else warnOnClientRead(made, user.client);
484
+ }
485
+ }
486
+ channel = made;
391
487
  dmChannels.set(user, channel);
392
488
  }
393
489
  return channel;
394
490
  }
491
+ /**
492
+ * Has a read of a default-mode DM channel's `client`, its own where strict mocks give the user's, warn once, so a test
493
+ * that reads it hears of the difference and the mock's own reads, as a send through the channel makes, stay quiet.
494
+ */ function warnOnClientRead(channel, userClient) {
495
+ if (readPlaceQuietly(()=>channel.client) === userClient) return;
496
+ // The read is answered by the channel's stub, which warns by the mock's own instance
497
+ warnOnRead(mockTargets.get(channel) ?? channel, 'client', ()=>{
498
+ if (readingPlace > 0) return;
499
+ warnOnce(mockLogger, "A mock user's DM channel here has a client of its own, not the user's; under useStrictMocks() it shares the user's " + 'client.');
500
+ });
501
+ }
502
+ // What default mode's user manager DMs do apart from discord.js's, warned of once each
503
+ const USER_MANAGER_DM_WARNINGS = {
504
+ createDM: 'UserManager.createDM() here gives every user the same DM channel; under useStrictMocks() each user has their own, ' + 'as Discord keeps one per user.',
505
+ send: "UserManager.send() here resolves to a message that doesn't hold what was sent; under useStrictMocks() it sends " + "through the user's DM channel, as discord.js does."
506
+ };
507
+ /** A user made for a manager, on the manager's client, as discord.js makes one. */ function userOf(manager, id) {
508
+ const user = createMockUser(id ? {
509
+ id
510
+ } : {});
511
+ const client = manager.client;
512
+ if (client instanceof Client) Object.defineProperty(user, 'client', {
513
+ value: client,
514
+ writable: true,
515
+ configurable: true
516
+ });
517
+ return user;
518
+ }
395
519
  /** The guild a manager belongs to, when it is a mock guild's. */ const guildOf = (manager)=>{
396
520
  const guild = manager.guild;
397
521
  return guild instanceof Guild ? guild : undefined;
398
522
  };
399
- // The item each manager fetches, creates and edits: with the id asked for, but for a ban or a thread member, and a
400
- // member or channel in the manager's guild
523
+ // The class discord.js makes for each type of channel a server's channel manager creates; a text channel by default
524
+ const CHANNEL_CLASSES = {
525
+ [ChannelType.GuildText]: TextChannel,
526
+ [ChannelType.GuildVoice]: VoiceChannel,
527
+ [ChannelType.GuildCategory]: CategoryChannel,
528
+ [ChannelType.GuildAnnouncement]: NewsChannel,
529
+ [ChannelType.GuildStageVoice]: StageChannel,
530
+ [ChannelType.GuildDirectory]: DirectoryChannel,
531
+ [ChannelType.GuildForum]: ForumChannel,
532
+ [ChannelType.GuildMedia]: MediaChannel
533
+ };
534
+ /**
535
+ * The class of a channel a server's channel manager makes for `type`: under strict mocks the one discord.js makes,
536
+ * while default mode keeps a text channel and warns once for another type.
537
+ */ function createdChannelClass(type) {
538
+ const Class = CHANNEL_CLASSES[type ?? ChannelType.GuildText] ?? TextChannel;
539
+ if (Class === TextChannel || strictMocks()) return Class;
540
+ warnOnce(mockLogger, 'GuildChannelManager.create() here makes a text channel whatever type its options name; under useStrictMocks() it ' + 'makes the channel discord.js makes for that type.');
541
+ return TextChannel;
542
+ }
543
+ /** A thread a channel's thread manager makes: in that channel, as its parent, and in its server. */ function threadIn(id, manager) {
544
+ const channel = manager.channel;
545
+ if (!(channel instanceof GuildChannel)) return createMockChannel(ThreadChannel, id ? {
546
+ id
547
+ } : {});
548
+ const guild = channel.guild;
549
+ const thread = createMockChannel(ThreadChannel, id ? {
550
+ id
551
+ } : {});
552
+ // Not enumerated, so a mock thread's own keys stay those of one made alone, as a test may compare
553
+ const place = (key, value)=>Object.defineProperty(thread, key, {
554
+ value,
555
+ writable: true,
556
+ configurable: true
557
+ });
558
+ place('parentId', channel.id);
559
+ if (!(guild instanceof Guild)) return thread;
560
+ place('guild', guild);
561
+ place('guildId', guild.id);
562
+ // The parent is found through its server's channels, as discord.js finds it, by the thread's parentId. Strict mocks
563
+ // cache the channel there; default mode leaves the server's cache as it was and reads the channel itself
564
+ if (strictMocks()) cached(cacheOf(guild.channels), channel.id, ()=>channel);
565
+ Object.defineProperty(thread, 'parent', {
566
+ get: ()=>{
567
+ const parentId = thread.parentId ?? '';
568
+ return cacheOf(guild.channels)?.get(parentId) ?? (parentId === channel.id ? channel : null);
569
+ },
570
+ configurable: true
571
+ });
572
+ return thread;
573
+ }
401
574
  const MANAGER_ITEMS = [
402
575
  [
403
576
  UserManager,
@@ -439,9 +612,10 @@ const MANAGER_ITEMS = [
439
612
  ()=>createMockInteraction(GuildBan)
440
613
  ],
441
614
  // A message fetched from a channel is in it, and another user's
615
+ // A message made in the channel is the one fetched by its id, as Discord has it
442
616
  [
443
617
  GuildMessageManager,
444
- (id, manager)=>createMockMessage({
618
+ (id, manager)=>madeIn(manager, id) ?? createMockMessage({
445
619
  ...id ? {
446
620
  id
447
621
  } : {},
@@ -450,7 +624,7 @@ const MANAGER_ITEMS = [
450
624
  ],
451
625
  [
452
626
  DMMessageManager,
453
- (id, manager)=>createMockMessage({
627
+ (id, manager)=>madeIn(manager, id) ?? createMockMessage({
454
628
  ...id ? {
455
629
  id
456
630
  } : {},
@@ -459,15 +633,11 @@ const MANAGER_ITEMS = [
459
633
  ],
460
634
  [
461
635
  GuildTextThreadManager,
462
- (id)=>createMockChannel(ThreadChannel, id ? {
463
- id
464
- } : {})
636
+ threadIn
465
637
  ],
466
638
  [
467
639
  GuildForumThreadManager,
468
- (id)=>createMockChannel(ThreadChannel, id ? {
469
- id
470
- } : {})
640
+ threadIn
471
641
  ],
472
642
  [
473
643
  ThreadMemberManager,
@@ -493,7 +663,7 @@ const MANAGER_ITEMS = [
493
663
  ],
494
664
  [
495
665
  GuildChannelManager,
496
- (id, manager)=>createMockChannel(TextChannel, {
666
+ (id, manager, options)=>createMockChannel(createdChannelClass(options?.type), {
497
667
  ...id ? {
498
668
  id
499
669
  } : {},
@@ -544,7 +714,22 @@ const MANAGER_ITEMS = [
544
714
  } : {}
545
715
  });
546
716
  }
547
- const returnsPromise = (key, method)=>method.constructor.name === 'AsyncFunction' || PROMISE_METHODS.has(key) || /^set[A-Z]/.test(key) && !SYNC_SETTERS.has(key);
717
+ /**
718
+ * Names what a manager's create() or edit() made the `name` its options give, as discord.js does, under strict mocks;
719
+ * default mode keeps the name it has, with a warning once.
720
+ */ function withName(manager, method, item, options) {
721
+ const name = options?.name;
722
+ const current = item.name;
723
+ if (typeof name !== 'string' || current === name) return item;
724
+ if (strictMocks()) {
725
+ item.name = name;
726
+ return item;
727
+ }
728
+ const Manager = Object.getPrototypeOf(manager).constructor.name;
729
+ warnOnce(mockLogger, `${Manager}.${method}() here leaves the item with the mock's name, not the name its options give, as discord.js names it; ` + 'in the next major version (5.0) the mock names it so. Call useStrictMocks() to have it named now.');
730
+ return item;
731
+ }
732
+ /** Whether a discord.js method on `target` returns a promise: an async one, or one the lists above name. */ const returnsPromise = (target, key, method)=>method.constructor.name === 'AsyncFunction' || PROMISE_METHODS.has(key) || PROMISE_METHODS_OF.some(([Class, keys])=>keys.has(key) && Class.prototype.isPrototypeOf(target)) || /^set[A-Z]/.test(key) && !SYNC_SETTERS.has(key);
548
733
  // The ids a fetch asks for: an id, a discord.js object, or options naming one or several, such as `{ user: id }` or
549
734
  // `{ user: [ids] }`; undefined for a fetch of a list
550
735
  /** The channel a message manager belongs to, as `createMockMessage` takes it. */ const channelOfManager = (manager)=>{
@@ -572,18 +757,58 @@ function fetchedIds(args) {
572
757
  }
573
758
  return undefined;
574
759
  }
760
+ // How many messages discord.js's MessageManager.fetch() asks Discord for when its options give no limit
761
+ const DEFAULT_FETCH_LIMIT = 50;
762
+ /** Which of two messages is newer, by snowflake id, else by when the mock was made: negative when `a` is. */ function newerFirst(a, b) {
763
+ if (isSnowflake(a.id) && isSnowflake(b.id) && a.id !== b.id) return BigInt(b.id) > BigInt(a.id) ? 1 : -1;
764
+ return b.createdTimestamp - a.createdTimestamp;
765
+ }
766
+ /**
767
+ * A channel's messages as `messages.fetch(options)` lists them under strict mocks: those its cache holds, newest first,
768
+ * before, after or around the message `options` names, at most `limit` of them.
769
+ */ function recentMessages(manager, options) {
770
+ const { limit = DEFAULT_FETCH_LIMIT, before, after, around } = options ?? {};
771
+ const all = [
772
+ ...cacheOf(manager)?.values() ?? []
773
+ ];
774
+ const sorted = all.sort(newerFirst);
775
+ const at = (id)=>sorted.findIndex((message)=>message.id === id);
776
+ let window = sorted;
777
+ if (before !== undefined) window = at(before) === -1 ? [] : sorted.slice(at(before) + 1);
778
+ else if (after !== undefined) window = at(after) === -1 ? [] : sorted.slice(0, at(after)).slice(-limit);
779
+ else if (around !== undefined) window = at(around) === -1 ? [] : sorted.slice(Math.max(0, at(around) - Math.floor(limit / 2)));
780
+ return new Collection(window.slice(0, limit).map((message)=>[
781
+ message.id,
782
+ message
783
+ ]));
784
+ }
575
785
  /**
576
786
  * The mock function for a method found on a discord.js prototype. One that returns a promise in discord.js resolves:
577
- * `createDM` to the user's DM channel, `send` and its kin to a message, a manager's `fetch`, `create` and `edit` to its
578
- * item (a collection of each for a fetch of several, an empty one for a list fetch), a structure's own `edit`, `fetch`,
579
- * `delete`, `ban`, `pin`, `unpin`, `timeout`, `disableCommunicationUntil` and setters to the structure, and the rest to
580
- * `undefined`. Any other method returns `undefined`.
787
+ * `createDM` to the user's DM channel, `send` and its kin to a message, `awaitMessages` and `awaitReactions` to an
788
+ * empty collection, a manager's `fetch`, `create` and `edit` to its item (a collection of each for a fetch of several,
789
+ * an empty one for a list fetch), a structure's own `edit`, `fetch`, `delete`, `ban`, `kick`, `pin`, `unpin`, `timeout`,
790
+ * `disableCommunicationUntil` and setters to the structure, and the rest to `undefined`. Any other method returns
791
+ * `undefined`.
581
792
  */ function methodStub(target, key, method, receiver) {
582
- if (!returnsPromise(key, method)) return createMockFn();
793
+ if (!returnsPromise(target, key, method)) return createMockFn();
583
794
  // A member's DMs go through its user, and a user's through its one DM channel, as discord.js sends them
584
795
  if (target instanceof GuildMember && (key === 'createDM' || key === 'send')) {
585
796
  return createMockFn(async (...args)=>receiver().user[key](...args));
586
797
  }
798
+ // Under strict mocks a user manager's DMs go to the user named, its cached one or one made and cached, as discord.js
799
+ // resolves it; default mode keeps one DM channel for the manager and a send that resolves to an empty message
800
+ if (target instanceof UserManager && (key === 'createDM' || key === 'send')) {
801
+ return createMockFn(async (user, ...args)=>{
802
+ if (!strictMocks()) {
803
+ warnOnce(mockLogger, USER_MANAGER_DM_WARNINGS[key]);
804
+ return key === 'createDM' ? dmChannelOf(receiver()) : createMockMessage();
805
+ }
806
+ const named = user instanceof GuildMember ? user.user : user;
807
+ const id = typeof named === 'string' ? named : named?.id;
808
+ const resolved = named instanceof User ? named : cached(cacheOf(receiver()), id ?? nextSnowflake(), ()=>userOf(receiver(), id));
809
+ return key === 'createDM' ? resolved.createDM() : resolved.send(...args);
810
+ });
811
+ }
587
812
  if (key === 'createDM') return createMockFn(async ()=>dmChannelOf(receiver()));
588
813
  if (key === 'send' && target instanceof User) {
589
814
  return createMockFn(async (...args)=>(await receiver().createDM()).send(...args));
@@ -593,6 +818,7 @@ function fetchedIds(args) {
593
818
  return createMockFn(async (options)=>sentMessage(sent(options, false), receiver()));
594
819
  }
595
820
  if (MESSAGE_METHODS.has(key)) return createMockFn(async ()=>createMockMessage());
821
+ if (COLLECTING_METHODS.has(key)) return createMockFn(async ()=>new Collection());
596
822
  if (target instanceof BaseManager) {
597
823
  const item = MANAGER_ITEMS.find(([Manager])=>Manager.prototype.isPrototypeOf(target))?.[1];
598
824
  // A fetch of one item finds it in the cache first, as discord.js does, and caches one it makes; a fetch of
@@ -605,10 +831,27 @@ function fetchedIds(args) {
605
831
  id,
606
832
  one(id)
607
833
  ]));
608
- return ids === undefined ? new Collection() : one(ids);
834
+ if (ids !== undefined) return one(ids);
835
+ // Under strict mocks a channel's fetch() lists the messages it holds, as discord.js lists its recent ones;
836
+ // default mode resolves to none, and warns once for a channel that strict mocks would list messages of
837
+ const messages = target instanceof GuildMessageManager || target instanceof DMMessageManager;
838
+ if (messages && strictMocks()) return recentMessages(receiver(), args[0]);
839
+ if (messages) warnUnlisted(receiver());
840
+ return new Collection();
841
+ });
842
+ }
843
+ // Under strict mocks an edit reaches the item named, the cached one or one made and cached under its id, as
844
+ // discord.js patches it; default mode resolves to a new item and warns once
845
+ if (item && key === 'edit') {
846
+ return createMockFn(async (named, options)=>{
847
+ const id = typeof named === 'string' ? named : named?.id;
848
+ if (id !== undefined && !strictMocks()) {
849
+ warnOnce(mockLogger, `${Object.getPrototypeOf(receiver()).constructor.name}.edit() here ` + 'resolves to a new item with an id of its own, not the item named; under useStrictMocks() it resolves to ' + 'the item with that id, the cached one or one made under it.');
850
+ }
851
+ const edited = id === undefined || !strictMocks() ? item(undefined, receiver()) : cached(cacheOf(receiver()), id, ()=>item(id, receiver()));
852
+ return withName(receiver(), 'edit', edited, options);
609
853
  });
610
854
  }
611
- if (item && key === 'edit') return createMockFn(async ()=>item(undefined, receiver()));
612
855
  // A ban resolves to the member, user or id it was given, as discord.js's does, and caches nothing
613
856
  if (key === 'create' && target instanceof GuildBanManager) {
614
857
  return createMockFn(async (user)=>{
@@ -632,8 +875,8 @@ function fetchedIds(args) {
632
875
  // Every other manager here caches what its create() makes, as discord.js's does; check a manager's create when
633
876
  // adding it to MANAGER_ITEMS
634
877
  if (item && key === 'create') {
635
- return createMockFn(async ()=>{
636
- const made = item(undefined, receiver());
878
+ return createMockFn(async (options)=>{
879
+ const made = withName(receiver(), 'create', item(undefined, receiver(), options), options);
637
880
  if (typeof made.id === 'string') cacheOf(receiver())?.set(made.id, made);
638
881
  return made;
639
882
  });
@@ -825,10 +1068,6 @@ const isSnowflake = (id)=>typeof id === 'string' && /^\d{1,20}$/.test(id);
825
1068
  });
826
1069
  }
827
1070
  }
828
- /**
829
- * Where a mock interaction keeps every answer it got, through respond() or discord.js directly, in order, under a key
830
- * both builds of this meocord version read.
831
- */ const RESPONSE_LOG = Symbol.for(sharedName('responseLog'));
832
1071
  /**
833
1072
  * A mock of an answer method, `impl` its behaviour, that records each call in `log` as it is made, with what it sent
834
1073
  * and, once it settles, what it rejected with. The record wraps every behaviour the mock is given, its own and any a
@@ -917,11 +1156,33 @@ const isSnowflake = (id)=>typeof id === 'string' && /^\d{1,20}$/.test(id);
917
1156
  if (message !== undefined) error.message = message;
918
1157
  return error;
919
1158
  }
1159
+ // Why default mode runs an answer discord.js refuses, as the warning words it
1160
+ const SET_BY_TEST = 'a test set this answer or the one before it';
1161
+ /**
1162
+ * Refuses an answer as discord.js does. Where `lenient` says why the mock ran it so far, such as a behaviour a test
1163
+ * set, default mode warns and lets it run; strict mocks refuse it.
1164
+ */ function refuseAnswer(method, error, lenient) {
1165
+ if (lenient === undefined || strictMocks()) throw error;
1166
+ warnOnce(mockLogger, `${method}() here is refused by discord.js with "${error.message}", but the mock runs it, as ${lenient}. In the ` + 'next major version (5.0) the mock refuses it; call useStrictMocks() to have it refused now.');
1167
+ }
920
1168
  /** 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);
921
1169
  // In discord.js's error codes, though missing from its typings
922
1170
  const INVALID_CHANNEL_TYPE = 'CommandInteractionOptionInvalidChannelType';
923
- /** Records the interaction a mock options resolver belongs to. */ const ownOptions = (interaction, options)=>{
924
- if (typeof options === 'object' && options !== null) optionOwners.set(options, interaction);
1171
+ /** The names of a mock options resolver's entity options, for one given to an autocomplete. */ const optionEntities = new WeakMap();
1172
+ /** The resolvers whose entity options a warning has named. */ const warnedEntities = new WeakSet();
1173
+ /** Whether a mock options resolver belongs to an autocomplete, whose options Discord sends unresolved. */ const ownedByAutocomplete = (options)=>optionOwners.get(options)?.type === InteractionType.ApplicationCommandAutocomplete;
1174
+ /**
1175
+ * Records the interaction a mock options resolver belongs to. Given to an autocomplete, an entity option reads as its
1176
+ * id under strict mocks, as Discord sends it; otherwise it keeps the object given, with a warning once per resolver.
1177
+ */ const ownOptions = (interaction, options)=>{
1178
+ if (typeof options !== 'object' || options === null) return;
1179
+ optionOwners.set(options, interaction);
1180
+ const entities = optionEntities.get(options);
1181
+ if (!entities?.length || strictMocks() || warnedEntities.has(options) || !ownedByAutocomplete(options)) return;
1182
+ warnedEntities.add(options);
1183
+ const names = entities.map((name)=>`"${name}"`).join(', ');
1184
+ const many = entities.length > 1;
1185
+ mockLogger.warn(`The autocomplete option${many ? 's' : ''} ${names} ${many ? 'hold resolved objects' : 'holds a resolved object'} here, where Discord ` + `sends an autocomplete only ${many ? 'their ids' : 'its id'}, so a bot's handler gets the id and getUser() and the like return null. ` + `Call useStrictMocks(), which reads ${many ? 'them as their ids' : 'it as its id'}, as Discord sends ${many ? 'them' : 'it'}. A bare id string reads as ` + 'a string option.');
925
1186
  };
926
1187
  /** A mock user that is a person, with an id of its own unless given one. */ const mockUser = (id)=>{
927
1188
  const generatedId = id === undefined ? nextSnowflake() : undefined;
@@ -1029,11 +1290,15 @@ function createMockInteraction(Class, props) {
1029
1290
  const followUps = new Map();
1030
1291
  // Whether the last answer ran a behaviour a test set, after which a second answer runs with a warning by default
1031
1292
  let answeredByTest = false;
1032
- // A command answered with a modal has no original response, and Discord refuses one asked for
1033
- let modalAnswered = false;
1293
+ // Whether default mode ran a launchActivity() or sendPremiumRequired() before any answer, which it doesn't count as one
1294
+ let uncounted = false;
1295
+ // A command answered without a message, as with a modal, has no original response, and Discord refuses one asked for
1296
+ let noOriginal = false;
1034
1297
  const isComponent = instance.type === InteractionType.MessageComponent;
1035
1298
  // The original response of a component, or of a modal submitted from one, is the message it's on
1036
1299
  const componentMessage = ()=>heldFrom(instance.message);
1300
+ // Read when answered, after a test's props are applied: a modal submitted from a command has no message
1301
+ const onMessage = ()=>isComponent || instance.type === InteractionType.ModalSubmit && Boolean(instance.message);
1037
1302
  const snapshot = ()=>{
1038
1303
  const saved = {
1039
1304
  replied: instance.replied,
@@ -1045,7 +1310,7 @@ function createMockInteraction(Class, props) {
1045
1310
  originalGone,
1046
1311
  followUps: new Map(followUps),
1047
1312
  answeredByTest,
1048
- modalAnswered
1313
+ noOriginal
1049
1314
  };
1050
1315
  return ()=>{
1051
1316
  Object.assign(instance, {
@@ -1053,19 +1318,29 @@ function createMockInteraction(Class, props) {
1053
1318
  deferred: saved.deferred
1054
1319
  });
1055
1320
  decideEphemeral(saved.ephemeral, saved.ephemeralDecided);
1056
- ({ original, originalGone, answeredByTest, answeredByMock, modalAnswered } = saved);
1321
+ ({ original, originalGone, answeredByTest, answeredByMock, noOriginal } = saved);
1057
1322
  followUps.clear();
1058
1323
  for (const [id, held] of saved.followUps)followUps.set(id, held);
1059
1324
  };
1060
1325
  };
1061
- /**
1062
- * Refuses an answer as discord.js does. Where the refusal turns on a behaviour a test set, this answer's or the
1063
- * one before it, which ran without one, default mode warns and lets it run; strict mocks refuse it.
1064
- */ const refuse = (method, error, byTest)=>{
1065
- if (!(byTest || answeredByTest) || strictMocks()) throw error;
1066
- 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.');
1067
- };
1326
+ // Refused as discord.js does, leniently in default mode where this answer or the one before it ran a test's behaviour
1327
+ const refuse = (method, error, byTest)=>refuseAnswer(method, error, byTest || answeredByTest ? SET_BY_TEST : undefined);
1068
1328
  const answered = ()=>instance.deferred || instance.replied;
1329
+ // Default mode's launchActivity() and sendPremiumRequired(), which leave the interaction as it was, as before
1330
+ const countedOnlyWhenStrict = (method)=>{
1331
+ if (answered()) {
1332
+ warnOnce(mockLogger, `${method}() here runs though the interaction was answered; under useStrictMocks() this is refused with ` + 'InteractionAlreadyReplied, as discord.js refuses it.');
1333
+ return;
1334
+ }
1335
+ uncounted = true;
1336
+ warnOnce(mockLogger, `${method}() here leaves the interaction unanswered, and getResponse() doesn't report it; under useStrictMocks() this ` + "is the interaction's first answer, as in discord.js: it marks the interaction replied, and an answer before or " + 'after it is refused.');
1337
+ };
1338
+ // What Discord answers here isn't known, so the refusal names the cause rather than a code
1339
+ const refuseModalUpdate = ()=>{
1340
+ const reason = 'update() needs the message a component opened this modal from, and this modal came from a command. Use ' + 'reply() or deferReply() instead.';
1341
+ if (strictMocks()) throw new Error(reason);
1342
+ warnOnce(mockLogger, `${reason} The mock runs it; in the next major version (5.0) it refuses it. Call useStrictMocks() to have it refused now.`);
1343
+ };
1069
1344
  /**
1070
1345
  * What an answer sends, built now, as discord.js builds it when sending. One it refuses to build is refused under
1071
1346
  * strict mocks; default mode warns and holds it to build when first read.
@@ -1143,20 +1418,26 @@ function createMockInteraction(Class, props) {
1143
1418
  settle: ()=>void unsettled.splice(unsettled.indexOf(method), 1)
1144
1419
  };
1145
1420
  };
1146
- /** A later answer: refused before the interaction has one. */ const later = (method, then)=>(args, byTest)=>{
1147
- if (!answered()) refuse(method, notYetReplied(method), byTest);
1421
+ /**
1422
+ * A later answer: refused before the interaction has one. One that `replies` marks the interaction replied, under
1423
+ * strict mocks once it lands, as discord.js does once the request resolves; default mode marks it at once.
1424
+ */ const later = (method, then, replies = false)=>(args, byTest)=>{
1425
+ // A follow-up after an answer default mode doesn't count is sent, as it is once strict mocks count that answer
1426
+ if (!answered() && !(method === 'followUp' && uncounted)) refuse(method, notYetReplied(method), byTest);
1148
1427
  if (unsettled.length > 0) {
1149
1428
  warnInFlight(`${unsettled[0]}() had not resolved when ${method}() was called; on a bot this throws InteractionNotReplied. ` + `Await ${unsettled[0]}() first, or call useStrictMocks() to have the mock refuse it now.`);
1150
1429
  }
1151
1430
  const undo = snapshot();
1152
1431
  try {
1153
1432
  then(args);
1433
+ if (replies && !strict) instance.replied = true;
1154
1434
  } catch (error) {
1155
1435
  undo();
1156
1436
  throw error;
1157
1437
  }
1158
1438
  return {
1159
- undo: undoneInTurn(undo)
1439
+ undo: undoneInTurn(undo),
1440
+ land: replies && strict ? ()=>void (instance.replied = true) : undefined
1160
1441
  };
1161
1442
  };
1162
1443
  /** The id an edit, fetch or delete names, `undefined` for the original response. */ const targetId = (message)=>{
@@ -1169,7 +1450,7 @@ function createMockInteraction(Class, props) {
1169
1450
  if (held) followUps.set(id, built(held));
1170
1451
  return held && built(held);
1171
1452
  }
1172
- if (originalGone || modalAnswered) return undefined;
1453
+ if (originalGone || noOriginal) return undefined;
1173
1454
  // Answered without one of the mock's calls, as after a 40060 or a test setting `replied`: the original is there
1174
1455
  if (!original && answered()) original = componentMessage();
1175
1456
  if (original) original = built(original);
@@ -1208,7 +1489,6 @@ function createMockInteraction(Class, props) {
1208
1489
  id: followUpMade
1209
1490
  }) : messageFrom(held, madeFor(held.id), ...answeredIn());
1210
1491
  }, later('followUp', ([options])=>{
1211
- instance.replied = true;
1212
1492
  // Held as it is when sent, as Discord stores it, so a later change to what was passed isn't seen; its id taken
1213
1493
  // once built, so one that fails to build under strict mocks takes none
1214
1494
  const held = hold('followUp', ()=>sent(options, false));
@@ -1222,7 +1502,7 @@ function createMockInteraction(Class, props) {
1222
1502
  id
1223
1503
  });
1224
1504
  followUpMade = id;
1225
- }));
1505
+ }, true));
1226
1506
  // An edit replaces the message in the shape Discord stores, with ids and resolved media, stamped with when it was
1227
1507
  // edited, and fetchReply() reads that back, so code comparing the two sees what it would against Discord
1228
1508
  answer('editReply', async ()=>{
@@ -1237,11 +1517,9 @@ function createMockInteraction(Class, props) {
1237
1517
  const held = target(id);
1238
1518
  made = held && edited(held, options);
1239
1519
  if (!made) return;
1240
- if (id === undefined) {
1241
- original = made;
1242
- instance.replied = true;
1243
- } else followUps.set(id, made);
1244
- }));
1520
+ if (id === undefined) original = made;
1521
+ else followUps.set(id, made);
1522
+ }, true));
1245
1523
  stubs.set('fetchReply', createMockFn(async (message)=>{
1246
1524
  const held = target(targetId(message));
1247
1525
  if (!held) throw unknownMessage();
@@ -1256,16 +1534,31 @@ function createMockInteraction(Class, props) {
1256
1534
  if (id === undefined) originalGone = true;
1257
1535
  else followUps.delete(id);
1258
1536
  }));
1537
+ // A first answer that sends no message: the original response stays the message the interaction is on, if any
1538
+ const sendsNoMessage = ()=>{
1539
+ if (onMessage()) original = componentMessage();
1540
+ else noOriginal = true;
1541
+ };
1259
1542
  // showModal — the first response of a command or a component, like reply()
1260
1543
  if (instance.type === InteractionType.ApplicationCommand || isComponent) {
1261
- answer('showModal', async ()=>undefined, first('showModal', 'replied', ()=>{
1262
- modalAnswered = instance.type === InteractionType.ApplicationCommand;
1263
- if (isComponent) original = componentMessage();
1264
- }));
1544
+ answer('showModal', async ()=>undefined, first('showModal', 'replied', sendsNoMessage));
1545
+ }
1546
+ // An activity launched, or the upgrade button sent, is a first answer under strict mocks, as reply() is, on every
1547
+ // repliable interaction. Default mode leaves the interaction unanswered and unreported, and warns once where they differ
1548
+ for (const method of [
1549
+ 'launchActivity',
1550
+ 'sendPremiumRequired'
1551
+ ]){
1552
+ if (strict) answer(method, async ()=>undefined, first(method, 'replied', sendsNoMessage));
1553
+ else stubs.set(method, recordedMock(method, async ()=>undefined, [], ()=>(countedOnlyWhenStrict(method), {})));
1265
1554
  }
1266
1555
  // deferUpdate / update — components, and modals submitted from a message's component
1267
1556
  if (isComponent || instance.type === InteractionType.ModalSubmit) {
1268
- answer('update', async ()=>undefined, first('update', 'replied', ([options])=>original = hold('update', ()=>edited(heldFrom(instance.message, false), options))));
1557
+ answer('update', async ()=>undefined, first('update', 'replied', ([options])=>{
1558
+ // Discord updates only a message a component is on; a modal from a command has none
1559
+ if (!onMessage()) refuseModalUpdate();
1560
+ original = hold('update', ()=>edited(heldFrom(instance.message, false), options));
1561
+ }));
1269
1562
  answer('deferUpdate', async ()=>undefined, first('deferUpdate', 'deferred', ()=>original = componentMessage()));
1270
1563
  }
1271
1564
  }
@@ -1275,11 +1568,25 @@ function createMockInteraction(Class, props) {
1275
1568
  // it before answering would decide the window was already closed.
1276
1569
  if (instance.type === InteractionType.ApplicationCommandAutocomplete) {
1277
1570
  instance.responded = false;
1278
- // Under strict mocks responded is set once respond() lands, as discord.js sets it once the request resolves, and a
1279
- // second one sent while the first is in flight is refused by Discord with 40060; default mode sets it at once
1571
+ const strict = strictMocks();
1572
+ const log = [];
1573
+ // getResponse reports each respond() under strict mocks; default mode reports none, as before
1574
+ if (strict) Object.defineProperty(instance, RESPONSE_LOG, {
1575
+ value: log
1576
+ });
1577
+ // Responds whose change stands, and what `responded` read before the first of them, for a failed one to undo
1578
+ let standing = 0;
1579
+ let before = false;
1280
1580
  let respondsInFlight = 0;
1281
1581
  let respondWarned = false;
1282
- stubs.set('respond', createMockFn(async (choices)=>{
1582
+ // Under strict mocks responded is set once respond() lands, as discord.js sets it once the request resolves, and a
1583
+ // second one sent while the first is in flight is refused by Discord with 40060; default mode sets it at once
1584
+ const gate = (_args, byTest)=>{
1585
+ // Default mode leaves a behaviour a test set to itself, as before: responded stays as it was
1586
+ if (!strict && byTest) {
1587
+ warnOnce(mockLogger, 'respond() here runs a behaviour a test set and leaves responded as it was; under useStrictMocks() this marks the ' + 'autocomplete responded once it resolves, as discord.js does, and a second respond() is refused.');
1588
+ return {};
1589
+ }
1283
1590
  if (instance.responded) {
1284
1591
  if (respondsInFlight > 0 && !respondWarned) {
1285
1592
  respondWarned = true;
@@ -1287,21 +1594,32 @@ function createMockInteraction(Class, props) {
1287
1594
  }
1288
1595
  throw discordjsError(DiscordjsErrorCodes.InteractionAlreadyReplied);
1289
1596
  }
1597
+ respondsInFlight++;
1598
+ const settle = ()=>void respondsInFlight--;
1599
+ if (strict) {
1600
+ return {
1601
+ settle,
1602
+ land: ()=>{
1603
+ if (instance.responded) throw createDiscordError(40060, 'Interaction has already been acknowledged.');
1604
+ instance.responded = true;
1605
+ }
1606
+ };
1607
+ }
1608
+ if (standing++ === 0) before = instance.responded;
1609
+ instance.responded = true;
1610
+ return {
1611
+ settle,
1612
+ undo: ()=>{
1613
+ if (--standing === 0) instance.responded = before;
1614
+ }
1615
+ };
1616
+ };
1617
+ stubs.set('respond', recordedMock('respond', async (choices)=>{
1290
1618
  // Discord refuses a list longer than its limit, and the menu shows nothing
1291
1619
  if (Array.isArray(choices) && choices.length > MAX_AUTOCOMPLETE_CHOICES) {
1292
1620
  throw createDiscordError(50035, `Invalid Form Body\ndata.choices[BASE_TYPE_MAX_LENGTH]: Must be ${MAX_AUTOCOMPLETE_CHOICES} or fewer in length.`);
1293
1621
  }
1294
- respondsInFlight++;
1295
- if (!strictMocks()) instance.responded = true;
1296
- try {
1297
- await undefined;
1298
- } finally{
1299
- respondsInFlight--;
1300
- }
1301
- if (!strictMocks()) return;
1302
- if (instance.responded) throw createDiscordError(40060, 'Interaction has already been acknowledged.');
1303
- instance.responded = true;
1304
- }));
1622
+ }, log, gate));
1305
1623
  }
1306
1624
  // Applied last so an explicit prop wins over the type fields and the reply
1307
1625
  // state machine. defineProperty rather than assignment for the same reason the
@@ -1434,6 +1752,9 @@ function createMockInteraction(Class, props) {
1434
1752
  configurable: true
1435
1753
  });
1436
1754
  }
1755
+ // Default mode reads a guildId alone as a placeholder server with that id, as its warning says, answered in place of
1756
+ // the mock's own stub, so where the interaction is still reads as given. Strict mocks read null, as discord.js does
1757
+ if (strictPlace?.given === 'guildId' && !strictMocks() && unset('guild')) stubs.set('guild', guildPlaceholder(own('guildId')));
1437
1758
  // The channel it came from, as the gateway caches it for an interaction: its server's cached one, looked up on each
1438
1759
  // read so a channel the test caches later is the one read, or the user's DM
1439
1760
  let channel;
@@ -1475,6 +1796,11 @@ function createMockInteraction(Class, props) {
1475
1796
  // and in a server that user's member; each read live, so a targetId set later picks its user, and assignable
1476
1797
  if (UserContextMenuCommandInteraction.prototype.isPrototypeOf(instance)) {
1477
1798
  if (unset('targetId')) instance.targetId = own('targetUser')?.id ?? nextSnowflake();
1799
+ // Under strict mocks a target user given is the client's cached one, as discord.js resolves it, so its member is
1800
+ // that user's; default mode keeps the member's own user, and a read of targetMember warns once
1801
+ const givenTarget = own('targetUser');
1802
+ const users = cacheOf(instance.client.users);
1803
+ if (strictMocks() && typeof givenTarget === 'object' && givenTarget !== null) users?.set(givenTarget.id, givenTarget);
1478
1804
  const targetUser = ()=>{
1479
1805
  const id = own('targetId');
1480
1806
  return cached(cacheOf(instance.client.users), id, ()=>mockUser(id));
@@ -1492,7 +1818,15 @@ function createMockInteraction(Class, props) {
1492
1818
  });
1493
1819
  if (unset('targetUser')) live('targetUser', targetUser);
1494
1820
  if (unset('targetMember')) {
1495
- live('targetMember', ()=>own('guildId') ? memberFor(own('guild'), targetUser(), true, own('guildId')) : null);
1821
+ live('targetMember', ()=>{
1822
+ if (!own('guildId')) return null;
1823
+ const member = memberFor(own('guild'), targetUser(), true, own('guildId'));
1824
+ // Only the mock reads this for the test, so the warning comes at the test's read
1825
+ if (givenTarget && member.user !== givenTarget) {
1826
+ warnOnce(mockLogger, "UserContextMenuCommandInteraction.targetMember here is the member of another user with the targetUser's " + "id; under useStrictMocks() the targetUser given is cached on the client, so targetMember is that user's member.");
1827
+ }
1828
+ return member;
1829
+ });
1496
1830
  }
1497
1831
  }
1498
1832
  // A message context menu's target: the message given, else one made for its targetId. The message is the one
@@ -1822,6 +2156,10 @@ const HOLDS = [
1822
2156
  RoleManager.prototype,
1823
2157
  Role
1824
2158
  ],
2159
+ [
2160
+ GuildMemberRoleManager.prototype,
2161
+ Role
2162
+ ],
1825
2163
  [
1826
2164
  GuildChannelManager.prototype,
1827
2165
  GuildChannel
@@ -1899,16 +2237,24 @@ const HOLDS = [
1899
2237
  Invite
1900
2238
  ]
1901
2239
  ];
2240
+ /** What a manager of `prototype` holds, as discord.js's resolve() checks an item against. */ const holdsOf = (prototype)=>HOLDS.find(([Manager])=>Manager === prototype || Object.prototype.isPrototypeOf.call(Manager, prototype))?.[1];
1902
2241
  /**
1903
2242
  * A manager whose `cache` is a real collection of `items`, by id, empty without them, and whose methods are stubs but
1904
- * for `resolve` and `resolveId`, which read the cache. Its `client` is the one its guild or channel has.
1905
- */ function managerWith(prototype, items) {
2243
+ * for `resolve` and `resolveId`, which read the cache. Its `client` is the one its guild or channel has. `onCacheRead`,
2244
+ * when given, runs on each read of `cache`, before it is returned.
2245
+ */ function managerWith(prototype, items, onCacheRead) {
1906
2246
  const manager = Object.create(prototype);
1907
2247
  const cache = new Collection((items ?? []).map((item)=>[
1908
2248
  String(item.id ?? item.user?.id),
1909
2249
  item
1910
2250
  ]));
1911
- Object.defineProperty(manager, 'cache', {
2251
+ if (onCacheRead) {
2252
+ let current = cache;
2253
+ Object.defineProperty(manager, 'cache', {
2254
+ get: ()=>(onCacheRead(), current),
2255
+ set: (value)=>void (current = value)
2256
+ });
2257
+ } else Object.defineProperty(manager, 'cache', {
1912
2258
  value: cache,
1913
2259
  writable: true
1914
2260
  });
@@ -1917,7 +2263,7 @@ const HOLDS = [
1917
2263
  value: cache,
1918
2264
  writable: true
1919
2265
  });
1920
- const holds = HOLDS.find(([Manager])=>Manager === prototype || Object.prototype.isPrototypeOf.call(Manager, prototype))?.[1];
2266
+ const holds = holdsOf(prototype);
1921
2267
  if (holds) Object.defineProperty(manager, 'holds', {
1922
2268
  value: holds
1923
2269
  });
@@ -1938,6 +2284,51 @@ const HOLDS = [
1938
2284
  });
1939
2285
  return stubDeep(manager);
1940
2286
  }
2287
+ // Set while the mock reads a manager's cache itself
2288
+ let readingOwnCache = 0;
2289
+ // The messages made in each channel, by its messages manager, for a fetch to find and strict mocks to cache
2290
+ const messagesMade = new WeakMap();
2291
+ const madeIn = (manager, id)=>id === undefined ? undefined : messagesMade.get(manager)?.get(id);
2292
+ // The message managers whose cache read has warned
2293
+ const uncachedWarned = new WeakSet();
2294
+ /**
2295
+ * A channel's empty messages manager. Default mode caches none of the messages made in the channel, where strict mocks
2296
+ * and discord.js cache each, so a test's first read of the cache warns once while one of them is missing from it.
2297
+ */ function messageManager(prototype) {
2298
+ const manager = managerWith(prototype, undefined, ()=>{
2299
+ const made = messagesMade.get(manager);
2300
+ if (readingOwnCache > 0 || !made || strictMocks() || uncachedWarned.has(manager)) return;
2301
+ // Read as the mock's own, so this check doesn't call itself
2302
+ const cache = cacheOf(manager);
2303
+ if (![
2304
+ ...made.values()
2305
+ ].some((each)=>!cache?.has(each.id))) return;
2306
+ uncachedWarned.add(manager);
2307
+ mockLogger.warn(`${prototype.constructor.name}.cache here lacks messages made in its channel, where ` + 'discord.js caches each message as it arrives and each one the bot sends; in the next major version (5.0) the mock caches ' + 'them. Fetch the message, or call useStrictMocks() to have them cached now.');
2308
+ });
2309
+ return manager;
2310
+ }
2311
+ // The message managers whose fetch() with no id has warned
2312
+ const unlistedWarned = new WeakSet();
2313
+ /**
2314
+ * Warns once for a channel whose messages.fetch() with no id resolves to none in default mode, though strict mocks
2315
+ * would list messages there: those its cache holds, read as the mock's own, and those made or sent in it, which they
2316
+ * cache.
2317
+ */ function warnUnlisted(manager) {
2318
+ if (unlistedWarned.has(manager)) return;
2319
+ if (!cacheOf(manager)?.size && !messagesMade.get(manager)?.size) return;
2320
+ unlistedWarned.add(manager);
2321
+ mockLogger.warn(`${Object.getPrototypeOf(manager).constructor.name}.fetch() here resolves to no ` + "messages, though messages were made, sent or cached in its channel, where discord.js lists a channel's recent " + 'messages; under useStrictMocks() fetch() lists them.');
2322
+ }
2323
+ /** Notes a message made in a channel, and caches it there under strict mocks, as discord.js caches what arrives. */ function noteMadeIn(channel, message) {
2324
+ if (!(channel instanceof BaseChannel)) return;
2325
+ const manager = Reflect.getOwnPropertyDescriptor(channel, 'messages')?.value;
2326
+ const cache = manager && cacheOf(manager);
2327
+ if (!manager || !cache) return;
2328
+ if (!messagesMade.has(manager)) messagesMade.set(manager, new Map());
2329
+ messagesMade.get(manager).set(message.id, message);
2330
+ if (strictMocks()) cache.set(message.id, message);
2331
+ }
1941
2332
  /** An empty manager of `prototype` that `owner` names its own, such as `{ message }` for a message's reactions. */ function ownedManager(prototype, owner) {
1942
2333
  const manager = managerWith(prototype, undefined);
1943
2334
  Object.assign(manager, owner);
@@ -1957,14 +2348,16 @@ const HOLDS = [
1957
2348
  * A manager's `fetch(id)` resolves to its cached item with that id, as discord.js looks there first, or to a new one it
1958
2349
  * caches under that id: a member, channel, emoji, sticker, scheduled event or AutoMod rule in this guild, a role with
1959
2350
  * that id, a ban, or an invite with that code. `members.fetch({ user: ids })` resolves to a collection of each of those
1960
- * members, found or made the same way; `create()` and `edit()` resolve to a mock of its item, which `create()` caches
1961
- * as discord.js does, but for an invite, made for the channel given and cached nowhere; a list fetch resolves to an
1962
- * empty collection. Members, roles and channels given are put in
1963
- * their managers' caches, where dispatch looks first when it resolves a message's typed params; a member
1964
- * {@link createMockMember} made without a server is in this one. Its `roles.everyone` is the role given with the
1965
- * guild's id, or else an @everyone role of its own at position 0 with no permissions, in `roles.cache` as Discord has
1966
- * it. The guild is named `'Guild'` and its `preferredLocale` is `'en-US'` unless given, so `t.forGuild(guild)`
1967
- * translates as for a new English server.
2351
+ * members, found or made the same way; `create()` resolves to a mock of its item, a channel of the type its options
2352
+ * name under `useStrictMocks()`, which it caches as discord.js does, but for an invite, made for the channel given and
2353
+ * cached nowhere; `edit()` resolves under `useStrictMocks()` to the item with the id given, found or made the same way,
2354
+ * and under `useStrictMocks()` either takes the `name` its options give. A list fetch resolves to an empty collection.
2355
+ * Members, roles and channels given are put in their managers' caches, where dispatch looks first when it resolves a
2356
+ * message's typed params; a member {@link createMockMember} made without a server is in this one. Its `roles.everyone`
2357
+ * is the role given with the guild's id, or else an @everyone role of its own at position 0, in `roles.cache` as
2358
+ * Discord has it. That role has no permissions in default mode; under `useStrictMocks()` it has the ones Discord
2359
+ * gives @everyone in a new server. The guild is named `'Guild'` and its `preferredLocale` is `'en-US'` unless given,
2360
+ * so `t.forGuild(guild)` translates as for a new English server.
1968
2361
  *
1969
2362
  * The mock makes a member for any id it's asked for, so a test of an id that isn't a member rejects the fetch, as
1970
2363
  * `guild.members.fetch.mockRejectedValue(createDiscordError(10007))`, or resolves a collection without that member.
@@ -2034,7 +2427,7 @@ const HOLDS = [
2034
2427
  const is = (Base)=>Base.prototype.isPrototypeOf(Class.prototype) || Class === Base;
2035
2428
  // Text and announcement channels: messages, and threads made in the channel
2036
2429
  if (is(TextChannel) || is(NewsChannel)) {
2037
- instance.messages = managerWith(GuildMessageManager.prototype, undefined);
2430
+ instance.messages = messageManager(GuildMessageManager.prototype);
2038
2431
  instance.threads = managerWith(GuildTextThreadManager.prototype, undefined);
2039
2432
  }
2040
2433
  // Forum and media channels hold posts, each a thread started with its first message
@@ -2044,15 +2437,15 @@ const HOLDS = [
2044
2437
  }
2045
2438
  // Voice and stage channels carry a text chat of their own, and a bitrate, which discord.js tells them apart by
2046
2439
  if (is(BaseGuildVoiceChannel)) {
2047
- instance.messages = managerWith(GuildMessageManager.prototype, undefined);
2440
+ instance.messages = messageManager(GuildMessageManager.prototype);
2048
2441
  instance.bitrate = 64000;
2049
2442
  instance.userLimit = 0;
2050
2443
  }
2051
2444
  if (is(DMChannel)) {
2052
- instance.messages = managerWith(DMMessageManager.prototype, undefined);
2445
+ instance.messages = messageManager(DMMessageManager.prototype);
2053
2446
  }
2054
2447
  if (is(ThreadChannel)) {
2055
- instance.messages = managerWith(GuildMessageManager.prototype, undefined);
2448
+ instance.messages = messageManager(GuildMessageManager.prototype);
2056
2449
  instance.members = managerWith(ThreadMemberManager.prototype, undefined);
2057
2450
  }
2058
2451
  // A server's channel has its overwrites, none until a test puts some in the cache, which its permissions read
@@ -2157,6 +2550,7 @@ const CHANNEL_TYPE_GUARDS = [
2157
2550
  id: entry
2158
2551
  }) : entry);
2159
2552
  const manager = Object.create(GuildMemberRoleManager.prototype);
2553
+ // Its member's server and client, as discord.js's constructor sets them, and what it holds, so resolve() reads it
2160
2554
  Object.defineProperties(manager, {
2161
2555
  cache: {
2162
2556
  get: cache
@@ -2164,6 +2558,15 @@ const CHANNEL_TYPE_GUARDS = [
2164
2558
  member: {
2165
2559
  value: member
2166
2560
  },
2561
+ guild: {
2562
+ get: ()=>member.guild
2563
+ },
2564
+ client: {
2565
+ get: ()=>member.client
2566
+ },
2567
+ holds: {
2568
+ value: holdsOf(GuildMemberRoleManager.prototype)
2569
+ },
2167
2570
  highest: {
2168
2571
  get: highest
2169
2572
  }
@@ -2488,7 +2891,7 @@ let placeReadsSeen;
2488
2891
  "Give the member's guild too, or call useStrictMocks() to place it there now."
2489
2892
  ],
2490
2893
  guildId: [
2491
- 'a server the bot is in, though the mock was given only a guildId',
2894
+ 'a placeholder server with that id, though the mock was given only a guildId',
2492
2895
  "a guildId alone is a server the bot isn't in: guild and channel are null, and the member is a raw one",
2493
2896
  'Give a guild too, or call useStrictMocks() to read it so now.'
2494
2897
  ]
@@ -2717,9 +3120,38 @@ const GUILD_MANAGERS = [
2717
3120
  };
2718
3121
  }
2719
3122
  /** A cache's collection, or undefined for a stub a test put in place of the manager. */ const cacheOf = (manager)=>{
2720
- const cache = manager?.cache;
2721
- return cache instanceof Collection ? cache : undefined;
3123
+ // The mock's own read, which a message cache's warning leaves to the test's reads
3124
+ readingOwnCache++;
3125
+ try {
3126
+ const cache = manager?.cache;
3127
+ return cache instanceof Collection ? cache : undefined;
3128
+ } finally{
3129
+ readingOwnCache--;
3130
+ }
2722
3131
  };
3132
+ // The managers of a server whose caches a placeholder one holds empty
3133
+ const PLACEHOLDER_GUILD_MANAGERS = [
3134
+ 'members',
3135
+ 'roles',
3136
+ 'channels',
3137
+ 'emojis',
3138
+ 'stickers',
3139
+ 'bans',
3140
+ 'invites',
3141
+ 'scheduledEvents',
3142
+ 'autoModerationRules'
3143
+ ];
3144
+ /** A server default mode reads for a guildId alone: no `Guild`, so it answers no server checks, but with that id and empty caches. */ function guildPlaceholder(id) {
3145
+ return stubDeep({
3146
+ id,
3147
+ ...Object.fromEntries(PLACEHOLDER_GUILD_MANAGERS.map((key)=>[
3148
+ key,
3149
+ stubDeep({
3150
+ cache: new Collection()
3151
+ })
3152
+ ]))
3153
+ });
3154
+ }
2723
3155
  /** The cached item under `id`, put there by `make` when the cache lacks it. */ function cached(cache, id, make) {
2724
3156
  const existing = cache?.get(id);
2725
3157
  if (existing !== undefined) return existing;
@@ -2747,19 +3179,22 @@ const GUILD_MANAGERS = [
2747
3179
  id,
2748
3180
  cached(guildCaches.members, id, ()=>memberIn(guild, id, cached(userCache, id, ()=>mockUser(id))))
2749
3181
  ])) : null,
3182
+ // Full mocks, as the gateway delivers them: a role of the server, and a channel with its managers and overwrites
2750
3183
  roles: new Collection(guildCaches ? roles.map((id)=>[
2751
3184
  id,
2752
- cached(guildCaches.roles, id, ()=>stubDeep(Object.assign(Object.create(Role.prototype), {
3185
+ cached(guildCaches.roles, id, ()=>createMockInteraction(Role, {
2753
3186
  id,
2754
3187
  guild
2755
- })))
3188
+ }))
2756
3189
  ]) : []),
2757
3190
  channels: new Collection(channels.map((id)=>[
2758
3191
  id,
2759
- cached(guildCaches ? guildCaches.channels : cacheOf(client.channels), id, ()=>stubDeep(Object.assign(Object.create(TextChannel.prototype), {
3192
+ cached(guildCaches ? guildCaches.channels : cacheOf(client.channels), id, ()=>createMockChannel(TextChannel, guild ? {
2760
3193
  id,
2761
3194
  guild
2762
- })))
3195
+ } : {
3196
+ id
3197
+ }))
2763
3198
  ]))
2764
3199
  };
2765
3200
  // Own values, so the prototype's getters, which read the raw mention data, are not reached
@@ -3042,6 +3477,7 @@ function messageMock(overrides = {}, sentBy, made = {}, at) {
3042
3477
  }));
3043
3478
  if (sentBy) warnOnSentReads(instance, stubs, sentBy);
3044
3479
  const message = stubDeep(instance, stubs);
3480
+ if (channel) noteMadeIn(channel, message);
3045
3481
  return message;
3046
3482
  }
3047
3483
  /**
@@ -3090,7 +3526,8 @@ function messageMock(overrides = {}, sentBy, made = {}, at) {
3090
3526
  * 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
3091
3527
  * a DM send in a server of its own and keeps another author, with a warning when read.
3092
3528
  */ function sentMessage(held, channel, client, fields = {}, made) {
3093
- const by = client ?? channel?.client;
3529
+ // The mock's own read of where the message was sent
3530
+ const by = client ?? readPlaceQuietly(()=>channel?.client);
3094
3531
  const inChannel = channel instanceof BaseChannel;
3095
3532
  const dm = inChannel && channel.isDMBased();
3096
3533
  const strict = strictMocks();
@@ -3131,6 +3568,11 @@ function messageMock(overrides = {}, sentBy, made = {}, at) {
3131
3568
  if (typeof value === 'object' && value !== null) return ApplicationCommandOptionType.Mentionable;
3132
3569
  return ApplicationCommandOptionType.String;
3133
3570
  }
3571
+ /** The option types a user types into, which an autocomplete's focused option is one of. */ const TYPED = [
3572
+ ApplicationCommandOptionType.String,
3573
+ ApplicationCommandOptionType.Integer,
3574
+ ApplicationCommandOptionType.Number
3575
+ ];
3134
3576
  /** The discord.js classes an entity option's value is an instance of, each read by its own getters. */ const ENTITY_CLASSES = [
3135
3577
  User,
3136
3578
  GuildMember,
@@ -3183,7 +3625,7 @@ function messageMock(overrides = {}, sentBy, made = {}, at) {
3183
3625
  * An entity option arrives as a snowflake in `value` *and* as the resolved object on
3184
3626
  * its own field, and code that reads only one of the two is exactly what this lets a
3185
3627
  * test catch — so both are set. A user option has its user, and in a server its member too.
3186
- */ function toOptionData(name, value, memberOf) {
3628
+ */ function toOptionData(name, value, memberOf, unresolved) {
3187
3629
  const type = optionTypeOf(value);
3188
3630
  const isEntity = typeof value === 'object' && value !== null;
3189
3631
  const option = {
@@ -3196,7 +3638,8 @@ function messageMock(overrides = {}, sentBy, made = {}, at) {
3196
3638
  // Read once the options are given to an interaction, which says whether it was made in a server
3197
3639
  if (memberOf) Object.defineProperty(option, 'member', {
3198
3640
  get: ()=>memberOf(name, value) ?? undefined,
3199
- enumerable: true
3641
+ enumerable: true,
3642
+ configurable: true
3200
3643
  });
3201
3644
  } else if (value instanceof GuildMember) {
3202
3645
  option.user = value.user;
@@ -3205,6 +3648,25 @@ function messageMock(overrides = {}, sentBy, made = {}, at) {
3205
3648
  else if (value instanceof BaseChannel) option.channel = value;
3206
3649
  else if (value instanceof Attachment) option.attachment = value;
3207
3650
  else if (isEntity) option.user = value;
3651
+ // An autocomplete's entity option is its id alone, as Discord sends it, when the options belong to one
3652
+ if (unresolved) {
3653
+ for (const field of [
3654
+ 'user',
3655
+ 'member',
3656
+ 'role',
3657
+ 'channel',
3658
+ 'attachment'
3659
+ ]){
3660
+ const descriptor = Object.getOwnPropertyDescriptor(option, field);
3661
+ if (!descriptor) continue;
3662
+ const read = descriptor.get ?? (()=>descriptor.value);
3663
+ Object.defineProperty(option, field, {
3664
+ get: ()=>unresolved() ? undefined : read(),
3665
+ enumerable: true,
3666
+ configurable: true
3667
+ });
3668
+ }
3669
+ }
3208
3670
  return option;
3209
3671
  }
3210
3672
  /**
@@ -3230,8 +3692,8 @@ function messageMock(overrides = {}, sentBy, made = {}, at) {
3230
3692
  /**
3231
3693
  * Nests the supplied options under the subcommand path they were invoked through,
3232
3694
  * matching the shape Discord sends rather than a flat list.
3233
- */ function buildOptionData(subcommandGroup, subcommand, values, memberOf, focused) {
3234
- const leaves = Object.entries(values).map(([name, value])=>name === focused ? focusedOptionData(name, value) : toOptionData(name, value, memberOf));
3695
+ */ function buildOptionData(subcommandGroup, subcommand, values, memberOf, focused, unresolved) {
3696
+ const leaves = Object.entries(values).map(([name, value])=>name === focused ? focusedOptionData(name, value) : toOptionData(name, value, memberOf, unresolved));
3235
3697
  if (focused !== null && !Object.hasOwn(values, focused)) leaves.push(focusedOptionData(focused, null));
3236
3698
  if (subcommand === null) return leaves;
3237
3699
  const sub = {
@@ -3266,7 +3728,8 @@ function messageMock(overrides = {}, sentBy, made = {}, at) {
3266
3728
  * entity option carries its id in `value` and the object itself, as the gateway sends it: a user option its `user`,
3267
3729
  * and in a server its `member` too. A user option's
3268
3730
  * `getMember()` is the user's member in the server of the interaction the options are given to, and `null` in a DM;
3269
- * a member given resolves `getUser()` to its user. A getter throws discord.js's type error for an option of another
3731
+ * a member given resolves `getUser()` to its user. Under `useStrictMocks()` a user option's `getMentionable()` is its
3732
+ * member too where there is one, as discord.js reads it; default mode returns the user, with a warning once. A getter throws discord.js's type error for an option of another
3270
3733
  * type, as discord.js does, required or not. Only a user or member read as a role, or a role read as a user or member,
3271
3734
  * is `null`, or that error when required, as the option may be a mentionable one; a plain `{ id }` reads as any entity.
3272
3735
  * A whole number is an Integer option and a fraction a Number one, so
@@ -3274,7 +3737,13 @@ function messageMock(overrides = {}, sentBy, made = {}, at) {
3274
3737
  *
3275
3738
  * The `focused` option reads as Discord sends it, through `getFocused()`, `getFocused(true)` and `data` alike: a
3276
3739
  * string, `''` when no value is given, and marked `focused` in `data`. A number given for it is its digits under
3277
- * `useStrictMocks()`; otherwise it stays the number, with a warning when read.
3740
+ * `useStrictMocks()`; otherwise it stays the number, with a warning when read. Under `useStrictMocks()` `get()` and the
3741
+ * typed getters read it by name from that same option, as discord.js does; one given no value takes the string,
3742
+ * integer and number getters alike. Default mode reads what was given, `null` for no value, with a warning once.
3743
+ *
3744
+ * Given to an `AutocompleteInteraction`, a user, member, role, channel, mentionable or attachment option reads as
3745
+ * Discord sends an autocomplete's options under `useStrictMocks()`: its id alone, in `data` and the handler's params,
3746
+ * with the entity getters returning `null`. Otherwise it keeps the object given, with a warning once.
3278
3747
  *
3279
3748
  * @typeParam Cached - `any` by default, to match `createMockInteraction(ChatInputCommandInteraction)`; pass it when the
3280
3749
  * interaction under test is cache-pinned.
@@ -3296,6 +3765,10 @@ function messageMock(overrides = {}, sentBy, made = {}, at) {
3296
3765
  * @see {@link ChatInputOptions}
3297
3766
  */ function createChatInputOptions(opts = {}) {
3298
3767
  const { subcommandGroup = null, subcommand = null, focused = null, ...values } = opts;
3768
+ // The option the user is typing, as Discord sent it among the others, which every getter reads in discord.js
3769
+ const focusedOption = ()=>base._hoistedOptions.find((each)=>each.focused);
3770
+ // Default mode reads the focused option from what the test gave, and says once what discord.js reads instead
3771
+ const warnFocused = (name)=>warnOnce(mockLogger, `The focused option "${name}" is read here from what the test gave, where discord.js reads the option Discord sent: ` + "marked focused, its value a string ('' before anything is typed). In the next major version (5.0) the mock reads " + 'it so; call useStrictMocks() to have it read so now.');
3299
3772
  function resolveOrThrow(name, value, required) {
3300
3773
  {
3301
3774
  if (required === true) throw optionError(DiscordjsErrorCodes.CommandInteractionOptionNotFound, name);
@@ -3312,14 +3785,36 @@ function messageMock(overrides = {}, sentBy, made = {}, at) {
3312
3785
  // As in discord.js, a subcommand is required unless told otherwise, and a group is not
3313
3786
  base.getSubcommandGroup = createMockFn((required = false)=>resolveSubEntry(subcommandGroup, DiscordjsErrorCodes.CommandInteractionOptionNoSubcommandGroup, required));
3314
3787
  base.getSubcommand = createMockFn((required = true)=>resolveSubEntry(subcommand, DiscordjsErrorCodes.CommandInteractionOptionNoSubcommand, required));
3788
+ // Under strict mocks, options given to an autocomplete hold their entities as ids alone, as Discord sends them
3789
+ const unresolved = ()=>strictMocks() && ownedByAutocomplete(resolver);
3315
3790
  // As discord.js reads an option by its type: another type throws its type error, required or not. A value whose
3316
3791
  // option may be of the getter's type, as a user may be a mentionable option's, reads as null if the getter doesn't
3317
3792
  // take it, and throws when required
3318
3793
  const readOption = (name, expected, takes, required)=>{
3794
+ if (name === focused && strictMocks()) {
3795
+ // discord.js reads the focused option as Discord sent it: a string, its own type kept. One given no value may be
3796
+ // any type the user can type into, as the mock doesn't know which it was declared as
3797
+ const option = focusedOption();
3798
+ const types = values[name] === undefined || values[name] === null ? TYPED : [
3799
+ option.type
3800
+ ];
3801
+ if (!types.some((type)=>expected.includes(type))) {
3802
+ throw optionError(DiscordjsErrorCodes.CommandInteractionOptionType, name, option.type, expected.join(', '));
3803
+ }
3804
+ return option.value;
3805
+ }
3319
3806
  const value = values[name];
3807
+ if (name === focused && (value === undefined || value === null)) warnFocused(name);
3808
+ // Read through the option Discord sent, whose number warns where discord.js reads a string
3809
+ if (name === focused && typeof value === 'number') void focusedOption().value;
3320
3810
  if (value === undefined || value === null) return resolveOrThrow(name, null, required);
3321
3811
  const typeError = ()=>optionError(DiscordjsErrorCodes.CommandInteractionOptionType, name, optionTypeOf(value), expected.join(', '));
3322
3812
  if (!possibleTypesOf(value).some((type)=>expected.includes(type))) throw typeError();
3813
+ // An entity in an autocomplete's options is its id alone, so a getter finds nothing, as in discord.js
3814
+ if (typeof value === 'object' && unresolved()) {
3815
+ if (required === true) throw optionError(DiscordjsErrorCodes.CommandInteractionOptionEmpty, name, optionTypeOf(value));
3816
+ return null;
3817
+ }
3323
3818
  if (takes(value)) return value;
3324
3819
  if (required === true) throw typeError();
3325
3820
  return null;
@@ -3380,15 +3875,25 @@ function messageMock(overrides = {}, sentBy, made = {}, at) {
3380
3875
  ], plainOr(User, GuildMember));
3381
3876
  return value instanceof User ? memberOf(name, value) : value;
3382
3877
  });
3383
- base.getMentionable = createMockFn((name, required)=>readOption(name, [
3878
+ // A user's member in a server, as discord.js reads `member ?? user`; default mode keeps the user, with a warning
3879
+ base.getMentionable = createMockFn((name, required)=>{
3880
+ const value = readOption(name, [
3384
3881
  MENTIONABLE
3385
- ], plainOr(User, GuildMember, Role), required));
3882
+ ], plainOr(User, GuildMember, Role), required);
3883
+ if (!(value instanceof User)) return value;
3884
+ const member = memberOf(name, value);
3885
+ if (member === null || strictMocks()) return member ?? value;
3886
+ warnOnce(mockLogger, `getMentionable('${name}') returns the user here, where discord.js returns the user's member in a server; in the next ` + 'major version (5.0) the mock returns the member. Call useStrictMocks() to have it returned now.');
3887
+ return value;
3888
+ });
3386
3889
  base.getAttachment = createMockFn((name, required)=>readOption(name, [
3387
3890
  ATTACHMENT
3388
3891
  ], plainOr(Attachment), required));
3389
3892
  base.get = createMockFn((name, required)=>{
3893
+ if (name === focused && strictMocks()) return focusedOption();
3894
+ if (name === focused) warnFocused(name);
3390
3895
  const value = values[name];
3391
- return value === undefined || value === null ? resolveOrThrow(name, null, required) : toOptionData(name, value, memberOf);
3896
+ return value === undefined || value === null ? resolveOrThrow(name, null, required) : toOptionData(name, value, memberOf, unresolved);
3392
3897
  });
3393
3898
  // A message option is a message context menu's own, never a slash command's
3394
3899
  base.getMessage = createMockFn((name, required)=>readOption(name, [
@@ -3397,20 +3902,41 @@ function messageMock(overrides = {}, sentBy, made = {}, at) {
3397
3902
  base.getFocused = createMockFn((getFull)=>{
3398
3903
  if (focused === null) throw optionError(DiscordjsErrorCodes.AutocompleteInteractionOptionNoFocusedOption);
3399
3904
  // The one options.data holds, so the two agree
3400
- const option = base._hoistedOptions.find((each)=>each.focused);
3905
+ const option = focusedOption();
3401
3906
  return getFull === true ? {
3402
3907
  ...option
3403
3908
  } : option.value;
3404
3909
  });
3405
3910
  // `data` is what the framework reads to build a handler's params, so it is materialised here rather than
3406
3911
  // auto-stubbed, or every params assertion would see an empty record.
3407
- base.data = buildOptionData(subcommandGroup, subcommand, values, memberOf, focused);
3912
+ base.data = buildOptionData(subcommandGroup, subcommand, values, memberOf, focused, unresolved);
3408
3913
  // The fields discord.js's constructor sets from it, which its toString() reads
3409
3914
  base._group = subcommandGroup;
3410
3915
  base._subcommand = subcommand;
3411
3916
  base._hoistedOptions = subcommand === null ? base.data : (subcommandGroup === null ? base.data[0] : base.data[0].options[0]).options;
3412
3917
  const resolver = stubDeep(base);
3918
+ const entities = Object.entries(values).filter(([name, value])=>name !== focused && typeof value === 'object' && value !== null);
3919
+ optionEntities.set(resolver, entities.map(([name])=>name));
3413
3920
  return resolver;
3414
3921
  }
3922
+ // What a mock reads for a property the test gave none, made with this module's factories and helpers. Built last, as it
3923
+ // reads them, and only read once a mock is made
3924
+ const discordDefault = discordDefaults({
3925
+ botMemberOf,
3926
+ createChatInputOptions,
3927
+ createMockChannel,
3928
+ createMockGuild,
3929
+ createMockMessage,
3930
+ createMockUser,
3931
+ heldChannelOf,
3932
+ isRawMember,
3933
+ isUnsetEveryone,
3934
+ madeAt,
3935
+ memberRoles,
3936
+ NEW_SERVER_EVERYONE_PERMISSIONS,
3937
+ ownedManager,
3938
+ readPlaceAs,
3939
+ readPlaceQuietly
3940
+ });
3415
3941
 
3416
- 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 };
3942
+ export { NEW_SERVER_EVERYONE_PERMISSIONS, botMemberOf, createChatInputOptions, createMock, createMockChannel, createMockClient, createMockGuild, createMockInteraction, createMockMember, createMockMessage, createMockRawMember, createMockUser, heldChannelOf, isRawMember, isUnsetEveryone, madeAt, memberRoles, noteReactionDispatch, ownedManager, readPlaceAs, readPlaceQuietly, returnsPromise };