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
@@ -4,26 +4,27 @@ import { COOLDOWN_POLICY, cooldownPolicyFrom, claimStoreDownNotice, claimCooldow
4
4
  import { Message, BaseInteraction, Client } from 'discord.js';
5
5
  import { ReactionHandlerAction } from '../enum/controller.enum.js';
6
6
  import { CooldownStore, MemoryCooldownStore } from '../common/cooldown-store.js';
7
- import { ExecutionContext } from '../common/execution-context.js';
8
- import { Translator, missingTranslatorError } from '../common/translator.js';
9
- import { injectedTokens, singletonContextError, recordInstanceContainers } from '../core/guard-runner.js';
10
- import { appPresenterOf, runHandler, appStages, bindGlobalStages, prepareHandlerStages, prepareAppPresenter, prepareThemes, bindAppPresenter } from '../core/handler-pipeline.js';
11
- import { setPresenter } from '../common/response/presenter.js';
12
- import { componentRouteFor, commandMismatch, handlerInput } from '../core/handler-input.js';
7
+ import { ExecutionContext, inferContextType } from '../common/execution-context.js';
8
+ import { Translator } from '../common/translator.js';
9
+ import { injectedTokens, singletonContextError, prepareDirectCalls } from '../core/guard-runner.js';
10
+ import { bindAppPresenter, runHandler, appPresenterOf, appStages, bindGlobalStages, prepareHandlerStages, prepareAppPresenter, prepareThemes } from '../core/handler-pipeline.js';
11
+ import { setPresenter, clientPresenter } from '../common/response/presenter.js';
12
+ import { componentRouteFor, commandMismatch, handlerInput, autocompleteMismatch } from '../core/handler-input.js';
13
13
  import { buildComponentRoutes } from '../core/component-routes.js';
14
14
  import { hasCustomId } from '../util/interaction.util.js';
15
15
  import { ThemeCache, themeResolverClass } from '../core/theme-resolvers.js';
16
- import { releaseAmbientAppTheme, claimAmbientAppTheme, registerClientTheme } from '../core/theme-runtime.js';
16
+ import { prepareLateThemes, releaseAmbientAppTheme, claimAmbientAppTheme, registerClientTheme, appTheme } from '../core/theme-runtime.js';
17
17
  import { registerClientTranslator } from '../common/meocord-text.js';
18
- import { copyLayer, mergeTheme } from '../core/theme-scope.js';
18
+ import { defaultTheme, copyLayer, mergeTheme } from '../core/theme-scope.js';
19
19
  import { assertValidTheme, themeForProblem } from '../core/theme-validation.js';
20
20
  import { messageParamsFor, buildMessageRoutes } from '../core/message-routes.js';
21
21
  import { assertDistinctCommands, warnUnregisteredCommands, warnInheritedRoutes, warnOverlappingPatterns, warnHandlersOffControllers } from '../core/command-conflicts.js';
22
22
  import { messageCommandHooks } from '../core/message-params.js';
23
23
  import { appObservers, assertObservers, bindObservers } from '../core/observer-runner.js';
24
- import { HandlerRegistry } from '../core/handler-registry.js';
25
- import { adviseInPlaceOfInjecting, meocordClasses } from '../core/meocord-classes.js';
26
- import { startupClasses, assertStartupClasses, lateGuardCheck, stageDependencies } from '../core/startup-roots.js';
24
+ import { listEventSources, HandlerRegistry } from '../core/handler-registry.js';
25
+ import { eventSources } from '../core/event-sources.js';
26
+ import { adviseInPlaceOfInjecting, boundBeforeProviders, meocordClasses } from '../core/meocord-classes.js';
27
+ import { startupClasses, assertStartupClasses, lateGuardCheck, stageDependencies, perCallEventWarnings } from '../core/startup-roots.js';
27
28
  import { markStartupChecked, assertStartupChecked } from '../core/startup-checked.js';
28
29
  import { makeInjectable } from '../util/injectable.util.js';
29
30
  import { ShardContext, shardCallHandler } from '../core/shard-context.js';
@@ -36,11 +37,12 @@ import { Logger } from '../common/logger.js';
36
37
  import { isExplainedError, markExplained } from '../common/explained-error.js';
37
38
  import { collectStartupErrors, declaredErrorsOf, startupError, stopOnStartupErrors, isRefusal, logStartupErrors } from '../util/refusal.util.js';
38
39
  import { DEFAULT_SHUTDOWN_TIMEOUT_MS, shutdownTimeoutProblem } from '../util/shutdown-timeout.util.js';
39
- import { isConstructor, describeValue } from '../util/value.util.js';
40
- import { resolveProviders, tokenName, bindsOwnToken, providerMap, assertProvidedServices, bindProvider, resolutionOrder, isClassProvider, assertProvided, tokenDependencies } from '../core/providers.js';
40
+ import { isConstructor, describeValue, withArticle } from '../util/value.util.js';
41
+ import { resolveProviders, tokenName, bindsOwnToken, providerMap, appProviderRefusal, assertProvidedServices, bindProvider, resolutionOrder, isClassProvider, assertProvided, tokenDependencies } from '../core/providers.js';
42
+ import { getReactionHandlers, getHandlerRoutes } from '../decorator/controller.decorator.js';
41
43
  import { getEventHandlers } from '../decorator/event.decorator.js';
42
- import { getReactionHandlers } from '../decorator/controller.decorator.js';
43
44
  import { strictMocks } from './strict-mocks.js';
45
+ import { notAClass } from './routing.js';
44
46
  import { warnOnce } from '../common/deprecation.js';
45
47
  import { META } from '../util/metadata-keys.js';
46
48
  import { isUserOutcome } from '../common/user-outcome.js';
@@ -109,7 +111,13 @@ import { isUserOutcome } from '../common/user-outcome.js';
109
111
  * ```
110
112
  */ async init(options = {}) {
111
113
  this.resolving ?? (this.resolving = resolveProviders(this.container, this.providers, this.order).then(()=>{
114
+ // As the bot reads them, once every provided value is made and before the services that could list them
115
+ this.sources ?? (this.sources = eventSources(this.container, this.providers, this.lifecycle, this.eventClasses, (text)=>new Logger('TestingModule').warn(text)));
116
+ listEventSources(this.container.get(HandlerRegistry), this.sources);
117
+ prepareLateThemes(this.container, this.sources.map(({ cls })=>cls));
112
118
  for (const service of this.services)this.container.get(service);
119
+ // After the providers and listed services, as the bot makes it, so a provided value it injects is made
120
+ if (this.app) bindAppPresenter(this.container, this.app);
113
121
  }));
114
122
  await this.resolving;
115
123
  if (options.ready) {
@@ -197,10 +205,9 @@ import { isUserOutcome } from '../common/user-outcome.js';
197
205
  error
198
206
  });
199
207
  // No warning for a hook whose dependency failed: the test sees the dependency's own error
200
- // What close() shuts down is what was constructed, so the entries the runner records are not kept
201
208
  await runReadyHooks(this.container, this.lifecycle, client, {
202
209
  primary
203
- }, [], {
210
+ }, {
204
211
  resolveFailed: failed,
205
212
  hookFailed: failed
206
213
  }, {
@@ -267,16 +274,24 @@ import { isUserOutcome } from '../common/user-outcome.js';
267
274
  * command's or an autocomplete's options, or the handler's customId params with a modal's fields or
268
275
  * a select menu's choices.
269
276
  * An interaction's customId or command name must be one dispatch routes to the handler, ranking
270
- * every handler of the module as the bot does; a mock built without one is not checked. With a message alone, a patterned `@MessageHandler` gets the
271
- * params its pattern captures from the content, after the prefix of the module's `app`, with typed
272
- * params resolved as dispatch resolves them, from the message's guild caches first; a message
273
- * without content gets `{}`. A word that is not a value of its type, and a prefixed message that names
274
- * the command but leaves out a param, go through the handler's filters as a `MessageUsageError`, as
275
- * dispatch answers them.
277
+ * every handler of the module as the bot does; a mock built without one is not checked. An autocomplete is
278
+ * checked by its command path and its focused option: one dispatch gives to another `@Autocomplete` handler,
279
+ * or to none, is refused under `useStrictMocks()`, and otherwise warned about once and run.
280
+ * With a message alone, a patterned `@MessageHandler` gets the params its pattern captures from the content,
281
+ * after the prefix of the module's `app`, with typed params resolved as dispatch resolves them, from the
282
+ * message's guild caches first. A message whose `content` isn't a string, as a mock's without
283
+ * `useStrictMocks()`, gets `{}`. Under strict mocks a message has empty `content`, and a patterned handler
284
+ * rejects it, as dispatch runs no handler for a message with no text: give the mock the `content` the handler
285
+ * takes. A word that is not a value of its type, and a prefixed message that names the command but leaves out
286
+ * a param, go through the handler's filters as a `MessageUsageError`, as dispatch answers them.
287
+ * An `@On` or `@Once` handler no route reaches runs as {@link emit} runs it, as an event with the arguments given,
288
+ * under `useStrictMocks()`; without strict mocks it runs as its first argument's type, and `invoke` warns once
289
+ * where that differs.
276
290
  * @returns Whether the handler ran, and the error a filter handled, if any. Rejects with an error no
277
291
  * filter handles, or with the error a filter throws: the built-in fallback, which answers such
278
292
  * errors in the bot, does not run here. Rejects before running anything with an interaction or a
279
- * message the handler's route does not match, naming both. Given fewer arguments than the handler
293
+ * message the handler's route does not match, naming both, and under `useStrictMocks()` with such an
294
+ * autocomplete. Given fewer arguments than the handler
280
295
  * declares, with a first that is neither an interaction nor a message, such as a reaction without its
281
296
  * `ReactionEvent`, it rejects under `useStrictMocks()`, and otherwise warns once and runs the handler.
282
297
  *
@@ -308,11 +323,25 @@ import { isUserOutcome } from '../common/user-outcome.js';
308
323
  const instance = this.container.get(controller);
309
324
  if (typeof instance[methodName] !== 'function') throw new Error(`${controller.name}.${methodName} is not a method.`);
310
325
  const [first] = args;
326
+ const event = eventHandlerOf(controller, methodName);
327
+ // As emit and the bot run it; without strict mocks it runs as before, as a test that passes that way could fail
328
+ if (event && strictMocks()) {
329
+ this.checkArgumentCount(controller, methodName, instance[methodName], first, args.length);
330
+ this.registerEventClients(args);
331
+ const { ran, error } = await this.track(this.runEventHandler(instance, methodName, args));
332
+ return error === undefined ? {
333
+ ran
334
+ } : {
335
+ ran,
336
+ error
337
+ };
338
+ }
311
339
  const interaction = first instanceof BaseInteraction ? first : undefined;
312
340
  // The handler dispatch gives the customId to, among every handler of the module
313
341
  const component = interaction && hasCustomId(interaction) ? componentRouteFor(this.componentRoutes(), controller, methodName, interaction) : undefined;
314
342
  const mismatch = component && 'mismatch' in component ? component.mismatch : interaction && commandMismatch(this.controllers, controller, methodName, interaction);
315
343
  if (mismatch) throw new Error(mismatch);
344
+ if (interaction) this.checkAutocompleteRoute(controller, methodName, interaction);
316
345
  let hooks = {};
317
346
  let callArgs = args.length === 1 && interaction ? [
318
347
  interaction,
@@ -331,10 +360,7 @@ import { isUserOutcome } from '../common/user-outcome.js';
331
360
  }
332
361
  }
333
362
  this.checkArgumentCount(controller, methodName, instance[methodName], first, callArgs.length);
334
- const presenter = appPresenterOf(this.container);
335
- // A message command's errors are drawn by the app's presenter too, as an interaction's are
336
- const client = first instanceof BaseInteraction || first instanceof Message ? first.client : undefined;
337
- if (presenter && client) setPresenter(client, presenter);
363
+ if (event) this.warnEventRun(controller, methodName, event, first, callArgs.length !== args.length);
338
364
  this.registerClient(first);
339
365
  const { ran, error } = await this.track(runHandler(this.container, instance, methodName, callArgs, {
340
366
  awaitObservers: true,
@@ -348,6 +374,36 @@ import { isUserOutcome } from '../common/user-outcome.js';
348
374
  };
349
375
  }
350
376
  /**
377
+ * Warns once, without strict mocks, that `invoke` runs an event handler other than as the bot does: as the type its
378
+ * first argument gives, or with params it added. Only `invoke` warns, on the test's own call.
379
+ */ warnEventRun(controller, methodName, event, first, paramsAdded) {
380
+ const type = inferContextType(first);
381
+ // invoke adds params only for an interaction or a message, whose type is never 'event'
382
+ if (type === 'event') return;
383
+ const key = `${controller.name}.${methodName}`;
384
+ if (this.warnedEventRuns.has(key)) return;
385
+ this.warnedEventRuns.add(key);
386
+ const call = `${withArticle(type)} call${paramsAdded ? ' with its params added' : ''}`;
387
+ new Logger('TestingModule').warn(`invoke runs ${key}, an @${event.once ? 'Once' : 'On'}('${event.event}') handler, as ${call}; the bot runs it as ` + 'an event with its arguments as given, and useStrictMocks() runs it so.');
388
+ }
389
+ /** Runs an `@On` or `@Once` handler as the bot and `emit` do: as an event, with its arguments as given. */ runEventHandler(instance, method, args) {
390
+ return runHandler(this.container, instance, method, args, {
391
+ type: 'event',
392
+ awaitObservers: true
393
+ });
394
+ }
395
+ /**
396
+ * Refuses under strict mocks, and otherwise warns once, an autocomplete dispatch gives to another handler or to none.
397
+ * Without strict mocks the handler still runs, since a test that passes that way would fail.
398
+ */ checkAutocompleteRoute(controller, methodName, interaction) {
399
+ const mismatch = autocompleteMismatch(this.controllers, controller, methodName, interaction);
400
+ if (!mismatch) return;
401
+ if (strictMocks()) throw new Error(mismatch);
402
+ if (this.warnedAutocompleteRoutes.has(mismatch)) return;
403
+ this.warnedAutocompleteRoutes.add(mismatch);
404
+ new Logger('TestingModule').warn(`${mismatch} Under useStrictMocks(), invoke rejects it.`);
405
+ }
406
+ /**
351
407
  * Refuses under `useStrictMocks()`, or warns once otherwise, a call that gives a handler fewer arguments than it
352
408
  * declares when `invoke` builds none for it, as for a reaction given without its `ReactionEvent`.
353
409
  */ checkArgumentCount(controller, methodName, method, first, given) {
@@ -366,14 +422,35 @@ import { isUserOutcome } from '../common/user-outcome.js';
366
422
  return call;
367
423
  }
368
424
  /**
369
- * Makes this module the app of the input's client, as the bot is of its own, so `respond()` outside any call,
370
- * as in a collector's callback on that client, answers in this module's theme.
425
+ * Makes this module the app of the input's client, as the bot is of its own: its answers are drawn by this
426
+ * module's presenter, in its theme and language, `respond()` outside any call included, as in a collector's callback
427
+ * on that client. A module without a presenter clears the client's under strict mocks, so the default draws; default
428
+ * mode leaves another module's in place and warns once.
371
429
  */ registerClient(input) {
372
430
  const client = input?.client;
373
431
  if (!client || typeof client !== 'object') return;
432
+ const presenter = appPresenterOf(this.container);
433
+ if (presenter || strictMocks()) setPresenter(client, presenter);
434
+ else if (clientPresenter(client)) {
435
+ warnOnce(new Logger('TestingModule'), "A module without a presenter here answers with the presenter another module set on the same client; under " + "useStrictMocks() this module's own presenter, or the default, is used.");
436
+ }
374
437
  registerClientTheme(client, this.container);
375
438
  registerClientTranslator(client, this.container.isBound(Translator) ? this.container.get(Translator) : undefined);
376
439
  }
440
+ /**
441
+ * Makes this module the app of each event argument's client, as the bot is of its own, under strict mocks; default
442
+ * mode leaves the clients as they are and warns once where this module would draw answers otherwise.
443
+ */ registerEventClients(args) {
444
+ const withClient = args.filter((arg)=>typeof arg?.client === 'object');
445
+ if (withClient.length === 0) return;
446
+ if (strictMocks()) {
447
+ for (const arg of withClient)this.registerClient(arg);
448
+ return;
449
+ }
450
+ const ownTheme = appTheme(this.container) !== defaultTheme();
451
+ if (!appPresenterOf(this.container) && !this.container.isBound(Translator) && !ownTheme) return;
452
+ warnOnce(new Logger('TestingModule'), "emit() here leaves the client's presenter, theme and translator as they were, where the bot draws the answers to " + "an event with its app's; in the next major version (5.0) emit sets the module's on the client. Call " + 'useStrictMocks() to have it set them now.');
453
+ }
377
454
  /** The module's component routes, ranked as dispatch ranks them; throws, as the bot would, for two of one shape. */ componentRoutes() {
378
455
  return this.builtComponentRoutes ?? (this.builtComponentRoutes = buildComponentRoutes(this.controllers));
379
456
  }
@@ -381,6 +458,26 @@ import { isUserOutcome } from '../common/user-outcome.js';
381
458
  if (this.dispatcher) return this.dispatcher;
382
459
  const logger = new Logger('TestingModule');
383
460
  const warnUnanswered = this.appWarnUnanswered ?? process.env.NODE_ENV === 'development';
461
+ const notices = {
462
+ cooldownNotice: (refusal)=>claimCooldownNotice(this.container, refusal),
463
+ storeDownNotice: (who)=>claimStoreDownNotice(this.container, who)
464
+ };
465
+ // Strict mocks log an answer MeoCord fails to build and end the call in that fault, as a bot does; default mode
466
+ // rethrows it from the fallback, so observers hear the call's own outcome, and an interaction's again as an error
467
+ const answer = createFallback(logger, ()=>this.messageOptions, notices);
468
+ const rethrowing = createFallback(logger, ()=>this.messageOptions, {
469
+ ...notices,
470
+ strict: true
471
+ });
472
+ const fallback = async (error, context)=>{
473
+ if (strictMocks()) return answer(error, context);
474
+ try {
475
+ return await rethrowing(error, context);
476
+ } catch (failure) {
477
+ warnOnce(logger, "dispatch() here reports a call whose answer MeoCord couldn't build with the call's own outcome, and an " + "interaction's a second time as an error; under useStrictMocks() the call is reported once, as 'error'.");
478
+ throw failure;
479
+ }
480
+ };
384
481
  this.dispatcher = new Dispatcher({
385
482
  container: this.container,
386
483
  controllerClasses: this.controllers,
@@ -388,12 +485,7 @@ import { isUserOutcome } from '../common/user-outcome.js';
388
485
  logger,
389
486
  // A test's fake clock can pass 5 s inside a handler, so the slow-handler warning waits to be asked for
390
487
  warnSlowMessageHandlers: false,
391
- // Strict: an answer MeoCord fails to build rejects the dispatch, where a bot logs it and carries on
392
- fallback: createFallback(logger, ()=>this.messageOptions, {
393
- strict: true,
394
- cooldownNotice: (refusal)=>claimCooldownNotice(this.container, refusal),
395
- storeDownNotice: (who)=>claimStoreDownNotice(this.container, who)
396
- }),
488
+ fallback,
397
489
  // A mock's client is the one bot every mock client is, so a mention of it starts a command
398
490
  botUserId: (event)=>{
399
491
  const id = event.client?.user?.id;
@@ -411,6 +503,8 @@ import { isUserOutcome } from '../common/user-outcome.js';
411
503
  const dispatcher = this.dispatcherOf();
412
504
  const handlers = [];
413
505
  const unhandled = [];
506
+ // The fault each error's answer failed with, which the call ends in in that error's place
507
+ const unanswered = new Map();
414
508
  const record = {
415
509
  settled: (controller, method, { ran, error })=>handlers.push({
416
510
  controller,
@@ -420,7 +514,8 @@ import { isUserOutcome } from '../common/user-outcome.js';
420
514
  error
421
515
  }
422
516
  }),
423
- unhandled: (error)=>unhandled.push(error)
517
+ unhandled: (error)=>unhandled.push(error),
518
+ unanswered: (error, failure)=>unanswered.set(error, failure)
424
519
  };
425
520
  if (options) {
426
521
  noteReactionDispatch(input);
@@ -429,18 +524,19 @@ import { isUserOutcome } from '../common/user-outcome.js';
429
524
  action: options.action ?? ReactionHandlerAction.ADD
430
525
  }, record));
431
526
  } else if (input instanceof BaseInteraction) {
432
- const presenter = appPresenterOf(this.container);
433
- if (presenter) setPresenter(input.client, presenter);
434
527
  await this.track(dispatcher.interaction(input, record));
435
528
  } else if (input instanceof Message) {
436
- const presenter = appPresenterOf(this.container);
437
- if (presenter) setPresenter(input.client, presenter);
438
529
  await this.track(dispatcher.message(input, record));
439
530
  } else {
440
531
  throw new TypeError('dispatch takes an interaction, a message, or a reaction with { user }.');
441
532
  }
442
- // What the fallback answers as the user's own outcome, such as a usage reply or a refusal, is an outcome to assert on
443
- const failures = unhandled.filter((error)=>!isUserOutcome(error, options ? undefined : input));
533
+ // What the fallback answers as the user's own outcome, such as a usage reply or a refusal, is an outcome to assert on,
534
+ // unless its answer failed to build, which is the call's fault, as on a bot
535
+ const failures = unhandled.flatMap((error)=>unanswered.has(error) ? [
536
+ unanswered.get(error)
537
+ ] : isUserOutcome(error, options ? undefined : input) ? [] : [
538
+ error
539
+ ]);
444
540
  if (failures.length === 1) throw failures[0];
445
541
  if (failures.length > 1) throw new AggregateError(failures, `${failures.length} errors were left to the fallback.`);
446
542
  const error = handlers.find((handler)=>handler.error !== undefined)?.error ?? unhandled[0];
@@ -455,8 +551,9 @@ import { isUserOutcome } from '../common/user-outcome.js';
455
551
  /**
456
552
  * Emits a client event to the module's `@On` and `@Once` handlers, through the pipeline the app runs
457
553
  * them in up to its event fallback: the global guards of the module's `app`, then each handler's own.
458
- * Handlers on the module's controllers, class providers and their dependencies all receive it. A
459
- * `@Once` handler handles only the first event, as it would on a client.
554
+ * As on the bot, every instance the module runs lifecycle hooks on, the cooldown store aside, receives
555
+ * it: the controllers and services, what they and the stages inject, and each provided class or value.
556
+ * A `@Once` handler handles only the first event, as it would on a client.
460
557
  *
461
558
  * The bot's event fallback doesn't run here. On a bot, a guard's `GuardDeniedError` is logged and the
462
559
  * event skipped, and a handler's `UserError` is logged and, when the event carries a message, answered
@@ -480,22 +577,21 @@ import { isUserOutcome } from '../common/user-outcome.js';
480
577
  */ async emit(event, ...args) {
481
578
  this.warnIfClosed();
482
579
  await this.init();
580
+ this.registerEventClients(args);
483
581
  const calls = [];
484
- for (const cls of this.eventClasses){
485
- for (const handler of getEventHandlers(cls.prototype)){
582
+ for (const [index, source] of (this.sources ?? []).entries()){
583
+ for (const handler of source.handlers){
486
584
  if (handler.event !== event) continue;
487
585
  if (handler.once) {
488
- const key = `${cls.name}.${handler.method}:${event}`;
586
+ const key = `${index}.${handler.method}:${event}`;
489
587
  if (this.firedOnce.has(key)) continue;
490
588
  this.firedOnce.add(key);
491
589
  }
492
590
  // Resolved inside the call, so a class that cannot be resolved fails as its handler would
493
591
  const run = async ()=>{
494
- const instance = this.container.get(cls);
495
- const { ran } = await runHandler(this.container, instance, handler.method, args, {
496
- type: 'event',
497
- awaitObservers: true
498
- });
592
+ const instance = source.resolve();
593
+ if (!instance) return false;
594
+ const { ran } = await this.runEventHandler(instance, handler.method, args);
499
595
  return ran;
500
596
  };
501
597
  calls.push(this.track(run()));
@@ -511,7 +607,7 @@ import { isUserOutcome } from '../common/user-outcome.js';
511
607
  ran: results.filter((result)=>result.status === 'fulfilled' && result.value).length
512
608
  };
513
609
  }
514
- constructor(container, controllers = [], eventClasses = [], providers = new Map(), order = [], messageOptions = {}, lifecycle = [], /** The lifecycle units the container has constructed, which `close()` shuts down. */ constructed = new Set(), /** The `app`'s `warnUnanswered`, which dispatch follows as the bot does. */ appWarnUnanswered, /** The app's listed services, made at `init()` as the bot makes them before it logs in. */ services = [], /** How long `close()` waits for the `onShutdown` hooks, as the bot's `shutdownTimeout` does. */ shutdownTimeout = DEFAULT_SHUTDOWN_TIMEOUT_MS){
610
+ constructor(container, controllers = [], eventClasses = [], providers = new Map(), order = [], messageOptions = {}, lifecycle = [], /** The lifecycle units the container has constructed, which `close()` shuts down. */ constructed = new Set(), /** The `app`'s `warnUnanswered`, which dispatch follows as the bot does. */ appWarnUnanswered, /** The app's listed services, made at `init()` as the bot makes them before it logs in. */ services = [], /** How long `close()` waits for the `onShutdown` hooks, as the bot's `shutdownTimeout` does. */ shutdownTimeout = DEFAULT_SHUTDOWN_TIMEOUT_MS, /** The `app`, whose presenter `init()` makes after the providers, as the bot does. */ app){
515
611
  this.container = container;
516
612
  this.controllers = controllers;
517
613
  this.eventClasses = eventClasses;
@@ -523,10 +619,13 @@ import { isUserOutcome } from '../common/user-outcome.js';
523
619
  this.appWarnUnanswered = appWarnUnanswered;
524
620
  this.services = services;
525
621
  this.shutdownTimeout = shutdownTimeout;
622
+ this.app = app;
526
623
  /** The `onReady` hooks' pass, which `close()` waits for, less one whose `onReady` called `close()`. */ this.readyPass = new ReadyPass();
527
624
  this.warnedClosed = false;
528
625
  /** The `@Once` handlers that have already handled their event, as a client forgets its once listeners. */ this.firedOnce = new Set();
529
626
  /** The handlers already warned about for being given fewer arguments than they take, so each is named once. */ this.warnedArgumentCounts = new Set();
627
+ /** The `@On` and `@Once` handlers already warned about for running other than as an event, so each is named once. */ this.warnedEventRuns = new Set();
628
+ /** The autocomplete mismatches already warned about without strict mocks, so each handler and route warns once. */ this.warnedAutocompleteRoutes = new Set();
530
629
  /** The calls under way, each removed once it settles: what `close()` lets finish before the cooldown store stops. */ this.calls = new Set();
531
630
  }
532
631
  }
@@ -536,6 +635,10 @@ import { isUserOutcome } from '../common/user-outcome.js';
536
635
  throw new AggregateError(failures.map(({ error })=>error), `${failures.length} ${hook} hooks threw: ${failures.map(({ name })=>name).join(', ')}.`);
537
636
  }
538
637
  }
638
+ /** A method's `@On` or `@Once` handler, when no route also reaches it: a route's input decides how that one runs. */ function eventHandlerOf(controller, methodName) {
639
+ if (getHandlerRoutes(controller.prototype).some((route)=>route.method === methodName)) return undefined;
640
+ return getEventHandlers(controller.prototype).find((handler)=>handler.method === methodName);
641
+ }
539
642
  /** The app's `messages` options, when the testing module is given an app. */ function messagesOf(app) {
540
643
  return app && Reflect.getMetadata(META.appOptions, app)?.messages;
541
644
  }
@@ -570,7 +673,9 @@ import { isUserOutcome } from '../common/user-outcome.js';
570
673
  /**
571
674
  * Replaces a provider with a test double.
572
675
  *
573
- * The double needs only the members the test uses; misspelled member names are still rejected.
676
+ * The double needs only the members the test uses; misspelled member names are still rejected. A token MeoCord binds
677
+ * itself, such as `ShardContext` or `HandlerRegistry`, takes the double in place of MeoCord's own, so a test can stub
678
+ * cross-shard calls.
574
679
  *
575
680
  * @param token - The provider to replace.
576
681
  * @example
@@ -754,7 +859,7 @@ import { isUserOutcome } from '../common/user-outcome.js';
754
859
  // Bound first, as in the app, so a class that injects it gets this instance
755
860
  const appClasses = [];
756
861
  const translator = ()=>container.isBound(Translator) ? container.get(Translator) : undefined;
757
- container.bind(HandlerRegistry).toConstantValue(new HandlerRegistry(appClasses, messagesOf(this.options.app), translator));
862
+ container.bind(HandlerRegistry).toConstantValue(new HandlerRegistry(appClasses, messagesOf(this.options.app), translator, this.options.controllers ?? []));
758
863
  // A testing module runs as one process, so a cross-shard call runs once, here
759
864
  container.bind(ShardContext).toConstantValue(new ShardContext(undefined, shardCallHandler(container, ()=>[
760
865
  ...appClasses,
@@ -767,6 +872,17 @@ import { isUserOutcome } from '../common/user-outcome.js';
767
872
  // Checked as the app checks its own: the app's providers in their order, then the test's, which replace them
768
873
  // by token, then the overrides, which win
769
874
  const providers = providerMap(this.wiring?.providers ?? [], '@MeoCord({ providers })');
875
+ // The app's own providers checked as the bot checks them. A token the module binds itself above fails at its
876
+ // first call, so it is refused; another the bot binds before its providers is refused under strict mocks, and
877
+ // runs here in default mode with a warning
878
+ const i18n = this.options.app && Reflect.getMetadata(META.appOptions, this.options.app)?.i18n;
879
+ const botBinds = boundBeforeProviders(i18n !== undefined);
880
+ for (const token of providers.keys()){
881
+ const refusal = appProviderRefusal(`${this.options.app?.name}`, token, (own)=>botBinds.includes(own), new Logger('TestingModule'));
882
+ if (!refusal) continue;
883
+ if (container.isBound(token) || strictMocks()) startupError(refusal);
884
+ else warnOnce(new Logger('TestingModule'), `${refusal.message} The testing module runs it; under useStrictMocks() compile() refuses it, as the bot does.`);
885
+ }
770
886
  for (const [token, provider] of providerMap(this.options.providers ?? [], "the testing module's providers"))providers.set(token, provider);
771
887
  for (const [token, override] of this.overrides)providers.set(token, override);
772
888
  // A stage override stands in for its class wherever the class is bound. A stage also provided takes the stub in
@@ -822,7 +938,6 @@ import { isUserOutcome } from '../common/user-outcome.js';
822
938
  // The themeFor class this module runs, the app's or overrideThemeFor's, bound on its own as the app binds it
823
939
  const themeResolver = themeResolverClass(stages?.themeFor?.resolvers);
824
940
  // Every class the module runs, as the app finds them, except stage classes an override stands in for
825
- const i18n = this.options.app && Reflect.getMetadata(META.appOptions, this.options.app)?.i18n;
826
941
  const appPresenterClass = this.options.app && Reflect.getMetadata(META.appOptions, this.options.app)?.presenter;
827
942
  const startup = startupClasses({
828
943
  controllers: (this.options.controllers ?? []).filter(isConstructor),
@@ -876,8 +991,7 @@ import { isUserOutcome } from '../common/user-outcome.js';
876
991
  container.bind(cls).toSelf().inSingletonScope();
877
992
  // By constructor type or @inject token, as the app binds them
878
993
  for (const dep of injectedTokens(cls)){
879
- if (dep === Translator && !container.isBound(Translator)) startupError(missingTranslatorError(cls));
880
- else if (isAppClassToken(dep)) bindClass(dep);
994
+ if (isAppClassToken(dep)) bindClass(dep);
881
995
  }
882
996
  };
883
997
  // The app's own store, resolved like a service, unless the test provides the CooldownStore itself; bound first, as the
@@ -891,8 +1005,12 @@ import { isUserOutcome } from '../common/user-outcome.js';
891
1005
  }
892
1006
  bindsOwnToken(container, CooldownStore, store);
893
1007
  }
894
- // Before the controllers, so a class token that is provided is not also bound as itself
895
- for (const provider of providers.values())bindProvider(container, provider, bindClass);
1008
+ // Before the controllers, so a class token that is provided is not also bound as itself. A test's provider of a
1009
+ // token the module bound above, as HandlerRegistry or ShardContext, stands in for that binding
1010
+ for (const provider of providers.values()){
1011
+ if (container.isBound(provider.provide)) container.unbind(provider.provide);
1012
+ bindProvider(container, provider, bindClass);
1013
+ }
896
1014
  for (const ctrl of this.options.controllers ?? [])bindClass(ctrl);
897
1015
  for (const service of services)bindClass(service);
898
1016
  if (themeResolver) bindClass(themeResolver);
@@ -950,15 +1068,18 @@ import { isUserOutcome } from '../common/user-outcome.js';
950
1068
  cooldownStore: true
951
1069
  }
952
1070
  }));
1071
+ // Recorded as the container makes each one, before anything can be made, so close() shuts down exactly what exists
1072
+ const constructed = new Set();
1073
+ for (const { token } of lifecycle){
1074
+ container.onActivation(token, (_context, instance)=>{
1075
+ constructed.add(token);
1076
+ return instance;
1077
+ });
1078
+ }
953
1079
  // Every check has run, and touched only the container: from here the module is made, once none found an error
954
1080
  stopOnStartupErrors();
955
1081
  // Every class the module runs, so @UseGuard resolves guards on a direct call to any of them
956
- for (const cls of [
957
- ...this.options.controllers ?? [],
958
- ...startup.classes
959
- ])Reflect.defineMetadata(META.container, container, cls);
960
- // And each instance with the module that made it, as another module made from the same classes stamps them again
961
- recordInstanceContainers(container, [
1082
+ prepareDirectCalls(container, [
962
1083
  ...this.options.controllers ?? [],
963
1084
  ...startup.classes
964
1085
  ]);
@@ -969,20 +1090,12 @@ import { isUserOutcome } from '../common/user-outcome.js';
969
1090
  });
970
1091
  warnInheritedRoutes(this.options.controllers ?? []);
971
1092
  warnOverlappingPatterns(this.options.controllers ?? []);
972
- warnHandlersOffControllers(this.options.controllers ?? [], appClasses);
973
- if (this.options.app) bindAppPresenter(container, this.options.app);
974
- // Recorded as the container makes each one, so close() shuts down exactly what exists
975
- const constructed = new Set();
976
- for (const { token } of lifecycle){
977
- container.onActivation(token, (_context, instance)=>{
978
- constructed.add(token);
979
- return instance;
980
- });
981
- }
1093
+ warnHandlersOffControllers(this.options.controllers ?? [], startup.classes);
1094
+ for (const warning of perCallEventWarnings(startup, providers, appPresenterClass))new Logger('TestingModule').warn(warning);
982
1095
  const warnUnanswered = this.options.app && Reflect.getMetadata(META.appOptions, this.options.app)?.warnUnanswered;
983
1096
  return new TestingModule(container, [
984
1097
  ...this.options.controllers ?? []
985
- ], appClasses, providers, order, messages, lifecycle, constructed, warnUnanswered, services, this.options.shutdownTimeout ?? DEFAULT_SHUTDOWN_TIMEOUT_MS);
1098
+ ], appClasses, providers, order, messages, lifecycle, constructed, warnUnanswered, services, this.options.shutdownTimeout ?? DEFAULT_SHUTDOWN_TIMEOUT_MS, this.options.app);
986
1099
  });
987
1100
  } catch (error) {
988
1101
  if (isRefusal(error) && !isExplainedError(error)) {
@@ -1102,6 +1215,11 @@ import { isUserOutcome } from '../common/user-outcome.js';
1102
1215
  * expect(getResponse(interaction).calls[0].payload).toMatchObject({ content: '1 notes' })
1103
1216
  * ```
1104
1217
  */ static fromApp(app, options = {}) {
1218
+ // An app that isn't a class at all, as a circular import gives, has no metadata to read
1219
+ if (typeof app !== 'function') {
1220
+ const { name, hint } = notAClass(app);
1221
+ throw new TypeError(`${name} is not a @MeoCord app: fromApp takes the class @MeoCord decorates.${hint}`);
1222
+ }
1105
1223
  const appOptions = Reflect.getMetadata(META.appOptions, app);
1106
1224
  if (!appOptions) {
1107
1225
  // A refused @MeoCord stores no options, so its own errors, kept with startupErrors: 'all', say why
@@ -213,7 +213,13 @@ const collected = new FinalizationRegistry((ref)=>mocks.created.delete(ref));
213
213
  instances.push(this);
214
214
  contexts.push(this);
215
215
  invocationCallOrder.push(++mocks.invocationOrder);
216
- // Updated in place as the call settles, so a promise's outcome shows once it resolves or rejects
216
+ // Recorded as the call starts, so each call's results sit at its own index however calls nest, and filled in as it
217
+ // ends: a promise's outcome shows in settledResults once it resolves or rejects
218
+ const result = {
219
+ type: 'incomplete',
220
+ value: undefined
221
+ };
222
+ results.push(result);
217
223
  const settled = {
218
224
  type: 'incomplete',
219
225
  value: undefined
@@ -261,7 +267,7 @@ const collected = new FinalizationRegistry((ref)=>mocks.created.delete(ref));
261
267
  });
262
268
  throw err;
263
269
  } finally{
264
- results.push({
270
+ Object.assign(result, {
265
271
  type,
266
272
  value
267
273
  });