meocord 4.2.1 → 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 (137) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/dist/cjs/_shared/{controller.decorator-XKMlcUZz.cjs → controller.decorator-hp5xdJU4.cjs} +550 -189
  3. package/dist/cjs/_shared/{startup-roots-CgfPQrNl.cjs → event-sources-DzqTODXg.cjs} +274 -88
  4. package/dist/cjs/_shared/{providers-BZgJyfZS.cjs → providers-BBvbnmLP.cjs} +837 -295
  5. package/dist/cjs/_shared/{sharded-cooldown-store-CsJJyCZd.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
  6. package/dist/cjs/_shared/{translate-error-BUAR1WYG.cjs → translate-error-wsgfsUET.cjs} +1 -1
  7. package/dist/cjs/common/index.cjs +4 -104
  8. package/dist/cjs/core/index.cjs +435 -182
  9. package/dist/cjs/decorator/index.cjs +116 -121
  10. package/dist/cjs/testing/index.cjs +1923 -808
  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 +32 -10
  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 +27 -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 +92 -66
  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 +6 -6
  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 +54 -32
  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 +83 -54
  60. package/dist/esm/core/meocord.app.js +174 -89
  61. package/dist/esm/core/message-help.js +37 -15
  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 +4 -2
  66. package/dist/esm/core/process-state.js +19 -0
  67. package/dist/esm/core/providers.js +39 -4
  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/stage-scope.js +2 -1
  73. package/dist/esm/core/startup-roots.js +28 -8
  74. package/dist/esm/core/theme-resolvers.js +26 -11
  75. package/dist/esm/core/theme-runtime.js +23 -13
  76. package/dist/esm/core/theme-scope.js +31 -30
  77. package/dist/esm/core/theme-validation.js +1 -1
  78. package/dist/esm/decorator/app.decorator.js +37 -1
  79. package/dist/esm/decorator/command-builder.decorator.js +14 -0
  80. package/dist/esm/decorator/controller-class.decorator.js +1 -1
  81. package/dist/esm/decorator/controller.decorator.js +48 -19
  82. package/dist/esm/decorator/defer.decorator.js +2 -2
  83. package/dist/esm/decorator/event.decorator.js +4 -5
  84. package/dist/esm/decorator/filter.decorator.js +4 -4
  85. package/dist/esm/decorator/guard.decorator.js +4 -61
  86. package/dist/esm/decorator/inject.decorator.js +4 -1
  87. package/dist/esm/decorator/interceptor.decorator.js +2 -2
  88. package/dist/esm/decorator/theme.decorator.js +4 -3
  89. package/dist/esm/decorator/validation.decorator.js +5 -5
  90. package/dist/esm/package.json.js +1 -1
  91. package/dist/esm/testing/catalog.js +32 -25
  92. package/dist/esm/testing/cooldown-store-suite.js +3 -3
  93. package/dist/esm/testing/discord-defaults.js +449 -335
  94. package/dist/esm/testing/inspect-handler.js +91 -10
  95. package/dist/esm/testing/meocord-testing-module.js +217 -91
  96. package/dist/esm/testing/mock-fn.js +8 -2
  97. package/dist/esm/testing/mock-interaction.js +864 -124
  98. package/dist/esm/testing/response-log.js +8 -0
  99. package/dist/esm/testing/response.js +7 -5
  100. package/dist/esm/testing/routing.js +48 -6
  101. package/dist/esm/testing/strict-mocks.js +8 -4
  102. package/dist/esm/testing/theme.js +3 -2
  103. package/dist/esm/util/common.util.js +30 -9
  104. package/dist/esm/util/generator-cli.util.js +6 -4
  105. package/dist/esm/util/inherited-env.util.js +72 -13
  106. package/dist/esm/util/json.util.js +10 -3
  107. package/dist/esm/util/meocord-cli.util.js +3 -2
  108. package/dist/esm/util/meocord-config-loader.util.js +14 -1
  109. package/dist/esm/util/meocord-config-validation.util.js +13 -3
  110. package/dist/esm/util/meocord-source-config.util.js +13 -3
  111. package/dist/esm/util/platform.util.js +3 -2
  112. package/dist/esm/util/refusal.util.js +29 -11
  113. package/dist/esm/util/registration-mode.util.js +14 -1
  114. package/dist/esm/util/timer-limit.util.js +4 -1
  115. package/dist/esm/util/tsconfig-paths.util.js +38 -7
  116. package/dist/esm/util/tsconfig.util.js +16 -58
  117. package/dist/esm/util/user-text.util.js +8 -4
  118. package/dist/esm/util/value.util.js +33 -5
  119. package/dist/types/common/index.d.cts +17 -10
  120. package/dist/types/common/index.d.ts +17 -10
  121. package/dist/types/core/index.d.cts +21 -11
  122. package/dist/types/core/index.d.ts +21 -11
  123. package/dist/types/decorator/index.d.cts +63 -63
  124. package/dist/types/decorator/index.d.ts +63 -63
  125. package/dist/types/{index-DKJclIi-.d.ts → index-Brkd6QGL.d.ts} +81 -35
  126. package/dist/types/{index-Dlf0ZZtS.d.cts → index-CqXImwF_.d.cts} +81 -35
  127. package/dist/types/interface/index.d.cts +1 -1
  128. package/dist/types/interface/index.d.ts +1 -1
  129. package/dist/types/{route-Bh1U13yy.d.ts → route-IQrK-MN2.d.cts} +11 -3
  130. package/dist/types/{route-Bh1U13yy.d.cts → route-bI-okq7g.d.ts} +11 -3
  131. package/dist/types/testing/index.d.cts +158 -61
  132. package/dist/types/testing/index.d.ts +158 -61
  133. package/dist/types/{theme-resolvers-CyEz9pnv.d.cts → theme-resolvers-AtgeaYqF.d.cts} +1 -1
  134. package/dist/types/{theme-resolvers-DA204O-q.d.ts → theme-resolvers-BgqysV8d.d.ts} +1 -1
  135. package/dist/types/{translator-Bg7hwxDF.d.cts → translator-D8aydlCE.d.ts} +12 -11
  136. package/dist/types/{translator-CeP4bCzp.d.ts → translator-iJhDlgwL.d.cts} +12 -11
  137. 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';
@@ -34,12 +35,14 @@ import { Dispatcher } from '../core/dispatcher.js';
34
35
  import { createFallback } from '../core/fallback.js';
35
36
  import { Logger } from '../common/logger.js';
36
37
  import { isExplainedError, markExplained } from '../common/explained-error.js';
37
- import { declaredErrorsOf, collectStartupErrors, startupError, stopOnStartupErrors, isRefusal, logStartupErrors } from '../util/refusal.util.js';
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 { resolveProviders, tokenName, bindsOwnToken, providerMap, 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';
40
43
  import { getEventHandlers } from '../decorator/event.decorator.js';
41
- import { getReactionHandlers } from '../decorator/controller.decorator.js';
42
44
  import { strictMocks } from './strict-mocks.js';
45
+ import { notAClass } from './routing.js';
43
46
  import { warnOnce } from '../common/deprecation.js';
44
47
  import { META } from '../util/metadata-keys.js';
45
48
  import { isUserOutcome } from '../common/user-outcome.js';
@@ -108,7 +111,13 @@ import { isUserOutcome } from '../common/user-outcome.js';
108
111
  * ```
109
112
  */ async init(options = {}) {
110
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));
111
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);
112
121
  }));
113
122
  await this.resolving;
114
123
  if (options.ready) {
@@ -196,10 +205,9 @@ import { isUserOutcome } from '../common/user-outcome.js';
196
205
  error
197
206
  });
198
207
  // No warning for a hook whose dependency failed: the test sees the dependency's own error
199
- // What close() shuts down is what was constructed, so the entries the runner records are not kept
200
208
  await runReadyHooks(this.container, this.lifecycle, client, {
201
209
  primary
202
- }, [], {
210
+ }, {
203
211
  resolveFailed: failed,
204
212
  hookFailed: failed
205
213
  }, {
@@ -266,16 +274,24 @@ import { isUserOutcome } from '../common/user-outcome.js';
266
274
  * command's or an autocomplete's options, or the handler's customId params with a modal's fields or
267
275
  * a select menu's choices.
268
276
  * An interaction's customId or command name must be one dispatch routes to the handler, ranking
269
- * 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
270
- * params its pattern captures from the content, after the prefix of the module's `app`, with typed
271
- * params resolved as dispatch resolves them, from the message's guild caches first; a message
272
- * without content gets `{}`. A word that is not a value of its type, and a prefixed message that names
273
- * the command but leaves out a param, go through the handler's filters as a `MessageUsageError`, as
274
- * 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.
275
290
  * @returns Whether the handler ran, and the error a filter handled, if any. Rejects with an error no
276
291
  * filter handles, or with the error a filter throws: the built-in fallback, which answers such
277
292
  * errors in the bot, does not run here. Rejects before running anything with an interaction or a
278
- * 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
279
295
  * declares, with a first that is neither an interaction nor a message, such as a reaction without its
280
296
  * `ReactionEvent`, it rejects under `useStrictMocks()`, and otherwise warns once and runs the handler.
281
297
  *
@@ -307,11 +323,25 @@ import { isUserOutcome } from '../common/user-outcome.js';
307
323
  const instance = this.container.get(controller);
308
324
  if (typeof instance[methodName] !== 'function') throw new Error(`${controller.name}.${methodName} is not a method.`);
309
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
+ }
310
339
  const interaction = first instanceof BaseInteraction ? first : undefined;
311
340
  // The handler dispatch gives the customId to, among every handler of the module
312
341
  const component = interaction && hasCustomId(interaction) ? componentRouteFor(this.componentRoutes(), controller, methodName, interaction) : undefined;
313
342
  const mismatch = component && 'mismatch' in component ? component.mismatch : interaction && commandMismatch(this.controllers, controller, methodName, interaction);
314
343
  if (mismatch) throw new Error(mismatch);
344
+ if (interaction) this.checkAutocompleteRoute(controller, methodName, interaction);
315
345
  let hooks = {};
316
346
  let callArgs = args.length === 1 && interaction ? [
317
347
  interaction,
@@ -330,10 +360,7 @@ import { isUserOutcome } from '../common/user-outcome.js';
330
360
  }
331
361
  }
332
362
  this.checkArgumentCount(controller, methodName, instance[methodName], first, callArgs.length);
333
- const presenter = appPresenterOf(this.container);
334
- // A message command's errors are drawn by the app's presenter too, as an interaction's are
335
- const client = first instanceof BaseInteraction || first instanceof Message ? first.client : undefined;
336
- if (presenter && client) setPresenter(client, presenter);
363
+ if (event) this.warnEventRun(controller, methodName, event, first, callArgs.length !== args.length);
337
364
  this.registerClient(first);
338
365
  const { ran, error } = await this.track(runHandler(this.container, instance, methodName, callArgs, {
339
366
  awaitObservers: true,
@@ -347,6 +374,36 @@ import { isUserOutcome } from '../common/user-outcome.js';
347
374
  };
348
375
  }
349
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
+ /**
350
407
  * Refuses under `useStrictMocks()`, or warns once otherwise, a call that gives a handler fewer arguments than it
351
408
  * declares when `invoke` builds none for it, as for a reaction given without its `ReactionEvent`.
352
409
  */ checkArgumentCount(controller, methodName, method, first, given) {
@@ -365,14 +422,35 @@ import { isUserOutcome } from '../common/user-outcome.js';
365
422
  return call;
366
423
  }
367
424
  /**
368
- * Makes this module the app of the input's client, as the bot is of its own, so `respond()` outside any call,
369
- * 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.
370
429
  */ registerClient(input) {
371
430
  const client = input?.client;
372
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
+ }
373
437
  registerClientTheme(client, this.container);
374
438
  registerClientTranslator(client, this.container.isBound(Translator) ? this.container.get(Translator) : undefined);
375
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
+ }
376
454
  /** The module's component routes, ranked as dispatch ranks them; throws, as the bot would, for two of one shape. */ componentRoutes() {
377
455
  return this.builtComponentRoutes ?? (this.builtComponentRoutes = buildComponentRoutes(this.controllers));
378
456
  }
@@ -380,6 +458,26 @@ import { isUserOutcome } from '../common/user-outcome.js';
380
458
  if (this.dispatcher) return this.dispatcher;
381
459
  const logger = new Logger('TestingModule');
382
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
+ };
383
481
  this.dispatcher = new Dispatcher({
384
482
  container: this.container,
385
483
  controllerClasses: this.controllers,
@@ -387,12 +485,7 @@ import { isUserOutcome } from '../common/user-outcome.js';
387
485
  logger,
388
486
  // A test's fake clock can pass 5 s inside a handler, so the slow-handler warning waits to be asked for
389
487
  warnSlowMessageHandlers: false,
390
- // Strict: an answer MeoCord fails to build rejects the dispatch, where a bot logs it and carries on
391
- fallback: createFallback(logger, ()=>this.messageOptions, {
392
- strict: true,
393
- cooldownNotice: (refusal)=>claimCooldownNotice(this.container, refusal),
394
- storeDownNotice: (who)=>claimStoreDownNotice(this.container, who)
395
- }),
488
+ fallback,
396
489
  // A mock's client is the one bot every mock client is, so a mention of it starts a command
397
490
  botUserId: (event)=>{
398
491
  const id = event.client?.user?.id;
@@ -410,6 +503,8 @@ import { isUserOutcome } from '../common/user-outcome.js';
410
503
  const dispatcher = this.dispatcherOf();
411
504
  const handlers = [];
412
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();
413
508
  const record = {
414
509
  settled: (controller, method, { ran, error })=>handlers.push({
415
510
  controller,
@@ -419,7 +514,8 @@ import { isUserOutcome } from '../common/user-outcome.js';
419
514
  error
420
515
  }
421
516
  }),
422
- unhandled: (error)=>unhandled.push(error)
517
+ unhandled: (error)=>unhandled.push(error),
518
+ unanswered: (error, failure)=>unanswered.set(error, failure)
423
519
  };
424
520
  if (options) {
425
521
  noteReactionDispatch(input);
@@ -428,18 +524,19 @@ import { isUserOutcome } from '../common/user-outcome.js';
428
524
  action: options.action ?? ReactionHandlerAction.ADD
429
525
  }, record));
430
526
  } else if (input instanceof BaseInteraction) {
431
- const presenter = appPresenterOf(this.container);
432
- if (presenter) setPresenter(input.client, presenter);
433
527
  await this.track(dispatcher.interaction(input, record));
434
528
  } else if (input instanceof Message) {
435
- const presenter = appPresenterOf(this.container);
436
- if (presenter) setPresenter(input.client, presenter);
437
529
  await this.track(dispatcher.message(input, record));
438
530
  } else {
439
531
  throw new TypeError('dispatch takes an interaction, a message, or a reaction with { user }.');
440
532
  }
441
- // What the fallback answers as the user's own outcome, such as a usage reply or a refusal, is an outcome to assert on
442
- 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
+ ]);
443
540
  if (failures.length === 1) throw failures[0];
444
541
  if (failures.length > 1) throw new AggregateError(failures, `${failures.length} errors were left to the fallback.`);
445
542
  const error = handlers.find((handler)=>handler.error !== undefined)?.error ?? unhandled[0];
@@ -454,8 +551,9 @@ import { isUserOutcome } from '../common/user-outcome.js';
454
551
  /**
455
552
  * Emits a client event to the module's `@On` and `@Once` handlers, through the pipeline the app runs
456
553
  * them in up to its event fallback: the global guards of the module's `app`, then each handler's own.
457
- * Handlers on the module's controllers, class providers and their dependencies all receive it. A
458
- * `@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.
459
557
  *
460
558
  * The bot's event fallback doesn't run here. On a bot, a guard's `GuardDeniedError` is logged and the
461
559
  * event skipped, and a handler's `UserError` is logged and, when the event carries a message, answered
@@ -479,22 +577,21 @@ import { isUserOutcome } from '../common/user-outcome.js';
479
577
  */ async emit(event, ...args) {
480
578
  this.warnIfClosed();
481
579
  await this.init();
580
+ this.registerEventClients(args);
482
581
  const calls = [];
483
- for (const cls of this.eventClasses){
484
- for (const handler of getEventHandlers(cls.prototype)){
582
+ for (const [index, source] of (this.sources ?? []).entries()){
583
+ for (const handler of source.handlers){
485
584
  if (handler.event !== event) continue;
486
585
  if (handler.once) {
487
- const key = `${cls.name}.${handler.method}:${event}`;
586
+ const key = `${index}.${handler.method}:${event}`;
488
587
  if (this.firedOnce.has(key)) continue;
489
588
  this.firedOnce.add(key);
490
589
  }
491
590
  // Resolved inside the call, so a class that cannot be resolved fails as its handler would
492
591
  const run = async ()=>{
493
- const instance = this.container.get(cls);
494
- const { ran } = await runHandler(this.container, instance, handler.method, args, {
495
- type: 'event',
496
- awaitObservers: true
497
- });
592
+ const instance = source.resolve();
593
+ if (!instance) return false;
594
+ const { ran } = await this.runEventHandler(instance, handler.method, args);
498
595
  return ran;
499
596
  };
500
597
  calls.push(this.track(run()));
@@ -510,7 +607,7 @@ import { isUserOutcome } from '../common/user-outcome.js';
510
607
  ran: results.filter((result)=>result.status === 'fulfilled' && result.value).length
511
608
  };
512
609
  }
513
- 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){
514
611
  this.container = container;
515
612
  this.controllers = controllers;
516
613
  this.eventClasses = eventClasses;
@@ -522,10 +619,13 @@ import { isUserOutcome } from '../common/user-outcome.js';
522
619
  this.appWarnUnanswered = appWarnUnanswered;
523
620
  this.services = services;
524
621
  this.shutdownTimeout = shutdownTimeout;
622
+ this.app = app;
525
623
  /** The `onReady` hooks' pass, which `close()` waits for, less one whose `onReady` called `close()`. */ this.readyPass = new ReadyPass();
526
624
  this.warnedClosed = false;
527
625
  /** The `@Once` handlers that have already handled their event, as a client forgets its once listeners. */ this.firedOnce = new Set();
528
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();
529
629
  /** The calls under way, each removed once it settles: what `close()` lets finish before the cooldown store stops. */ this.calls = new Set();
530
630
  }
531
631
  }
@@ -535,6 +635,10 @@ import { isUserOutcome } from '../common/user-outcome.js';
535
635
  throw new AggregateError(failures.map(({ error })=>error), `${failures.length} ${hook} hooks threw: ${failures.map(({ name })=>name).join(', ')}.`);
536
636
  }
537
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
+ }
538
642
  /** The app's `messages` options, when the testing module is given an app. */ function messagesOf(app) {
539
643
  return app && Reflect.getMetadata(META.appOptions, app)?.messages;
540
644
  }
@@ -569,7 +673,9 @@ import { isUserOutcome } from '../common/user-outcome.js';
569
673
  /**
570
674
  * Replaces a provider with a test double.
571
675
  *
572
- * 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.
573
679
  *
574
680
  * @param token - The provider to replace.
575
681
  * @example
@@ -753,7 +859,7 @@ import { isUserOutcome } from '../common/user-outcome.js';
753
859
  // Bound first, as in the app, so a class that injects it gets this instance
754
860
  const appClasses = [];
755
861
  const translator = ()=>container.isBound(Translator) ? container.get(Translator) : undefined;
756
- 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 ?? []));
757
863
  // A testing module runs as one process, so a cross-shard call runs once, here
758
864
  container.bind(ShardContext).toConstantValue(new ShardContext(undefined, shardCallHandler(container, ()=>[
759
865
  ...appClasses,
@@ -766,6 +872,17 @@ import { isUserOutcome } from '../common/user-outcome.js';
766
872
  // Checked as the app checks its own: the app's providers in their order, then the test's, which replace them
767
873
  // by token, then the overrides, which win
768
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
+ }
769
886
  for (const [token, provider] of providerMap(this.options.providers ?? [], "the testing module's providers"))providers.set(token, provider);
770
887
  for (const [token, override] of this.overrides)providers.set(token, override);
771
888
  // A stage override stands in for its class wherever the class is bound. A stage also provided takes the stub in
@@ -803,7 +920,17 @@ import { isUserOutcome } from '../common/user-outcome.js';
803
920
  }
804
921
  // Each pass of checks ends before the next step that is no check, so a module with one error stops where it always did
805
922
  stopOnStartupErrors();
923
+ // What the walk below takes as classes, reported with what the walk finds; an entry refused here is left out of it
806
924
  const services = this.wiring?.services ?? [];
925
+ if (this.options.app) assertProvidedServices(this.options.app.name, services, providers, "@MeoCord({ providers }) or the testing module's providers");
926
+ for (const controller of this.options.controllers ?? []){
927
+ if (!isConstructor(controller)) startupError(new Error(`the testing module's controllers takes classes, not ${describeValue(controller)}.`));
928
+ }
929
+ const observers = [
930
+ ...this.options.app ? appObservers(this.options.app) : [],
931
+ ...this.options.observers ?? []
932
+ ];
933
+ assertObservers("the testing module's observers", observers);
807
934
  const appOptions = this.options.app ? Reflect.getMetadata(META.appOptions, this.options.app) : undefined;
808
935
  // The app's own store, as the bot binds it, whether the module is given the app or built from it; none when the
809
936
  // test provides the CooldownStore, so nothing builds the app's or asks for what it injects
@@ -811,20 +938,16 @@ import { isUserOutcome } from '../common/user-outcome.js';
811
938
  // The themeFor class this module runs, the app's or overrideThemeFor's, bound on its own as the app binds it
812
939
  const themeResolver = themeResolverClass(stages?.themeFor?.resolvers);
813
940
  // Every class the module runs, as the app finds them, except stage classes an override stands in for
814
- const i18n = this.options.app && Reflect.getMetadata(META.appOptions, this.options.app)?.i18n;
815
941
  const appPresenterClass = this.options.app && Reflect.getMetadata(META.appOptions, this.options.app)?.presenter;
816
942
  const startup = startupClasses({
817
- controllers: this.options.controllers ?? [],
943
+ controllers: (this.options.controllers ?? []).filter(isConstructor),
818
944
  services,
819
945
  providers,
820
946
  stages,
821
947
  presenter: appPresenterClass,
822
948
  cooldownStore: store,
823
949
  themeFor: stages?.themeFor?.resolvers,
824
- observers: [
825
- ...this.options.app ? appObservers(this.options.app) : [],
826
- ...this.options.observers ?? []
827
- ],
950
+ observers,
828
951
  stubs: [
829
952
  ...this.guardOverrides.keys(),
830
953
  ...this.interceptorOverrides.keys(),
@@ -868,8 +991,7 @@ import { isUserOutcome } from '../common/user-outcome.js';
868
991
  container.bind(cls).toSelf().inSingletonScope();
869
992
  // By constructor type or @inject token, as the app binds them
870
993
  for (const dep of injectedTokens(cls)){
871
- if (dep === Translator && !container.isBound(Translator)) startupError(missingTranslatorError(cls));
872
- else if (isAppClassToken(dep)) bindClass(dep);
994
+ if (isAppClassToken(dep)) bindClass(dep);
873
995
  }
874
996
  };
875
997
  // The app's own store, resolved like a service, unless the test provides the CooldownStore itself; bound first, as the
@@ -883,8 +1005,12 @@ import { isUserOutcome } from '../common/user-outcome.js';
883
1005
  }
884
1006
  bindsOwnToken(container, CooldownStore, store);
885
1007
  }
886
- // Before the controllers, so a class token that is provided is not also bound as itself
887
- 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
+ }
888
1014
  for (const ctrl of this.options.controllers ?? [])bindClass(ctrl);
889
1015
  for (const service of services)bindClass(service);
890
1016
  if (themeResolver) bindClass(themeResolver);
@@ -914,13 +1040,6 @@ import { isUserOutcome } from '../common/user-outcome.js';
914
1040
  // As the app does when it is created, so a test sees the refusal the bot would give
915
1041
  assertDistinctCommands(this.options.controllers ?? []);
916
1042
  if (this.options.app) prepareAppPresenter(container, this.options.app);
917
- const observers = [
918
- ...this.options.app ? appObservers(this.options.app) : [],
919
- ...this.options.observers ?? []
920
- ];
921
- assertObservers("the testing module's observers", observers);
922
- // Only @Observer classes are bound as observers
923
- stopOnStartupErrors();
924
1043
  bindObservers(container, observers);
925
1044
  // The store calls ask: the test's own when it provides CooldownStore, else the app's
926
1045
  const boundStore = store ?? (providers.has(CooldownStore) ? CooldownStore : undefined);
@@ -949,15 +1068,18 @@ import { isUserOutcome } from '../common/user-outcome.js';
949
1068
  cooldownStore: true
950
1069
  }
951
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
+ }
952
1079
  // Every check has run, and touched only the container: from here the module is made, once none found an error
953
1080
  stopOnStartupErrors();
954
1081
  // Every class the module runs, so @UseGuard resolves guards on a direct call to any of them
955
- for (const cls of [
956
- ...this.options.controllers ?? [],
957
- ...startup.classes
958
- ])Reflect.defineMetadata(META.container, container, cls);
959
- // And each instance with the module that made it, as another module made from the same classes stamps them again
960
- recordInstanceContainers(container, [
1082
+ prepareDirectCalls(container, [
961
1083
  ...this.options.controllers ?? [],
962
1084
  ...startup.classes
963
1085
  ]);
@@ -968,20 +1090,12 @@ import { isUserOutcome } from '../common/user-outcome.js';
968
1090
  });
969
1091
  warnInheritedRoutes(this.options.controllers ?? []);
970
1092
  warnOverlappingPatterns(this.options.controllers ?? []);
971
- warnHandlersOffControllers(this.options.controllers ?? [], appClasses);
972
- if (this.options.app) bindAppPresenter(container, this.options.app);
973
- // Recorded as the container makes each one, so close() shuts down exactly what exists
974
- const constructed = new Set();
975
- for (const { token } of lifecycle){
976
- container.onActivation(token, (_context, instance)=>{
977
- constructed.add(token);
978
- return instance;
979
- });
980
- }
1093
+ warnHandlersOffControllers(this.options.controllers ?? [], startup.classes);
1094
+ for (const warning of perCallEventWarnings(startup, providers, appPresenterClass))new Logger('TestingModule').warn(warning);
981
1095
  const warnUnanswered = this.options.app && Reflect.getMetadata(META.appOptions, this.options.app)?.warnUnanswered;
982
1096
  return new TestingModule(container, [
983
1097
  ...this.options.controllers ?? []
984
- ], 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);
985
1099
  });
986
1100
  } catch (error) {
987
1101
  if (isRefusal(error) && !isExplainedError(error)) {
@@ -1060,7 +1174,8 @@ import { isUserOutcome } from '../common/user-outcome.js';
1060
1174
  * @param app - The class `@MeoCord` decorates.
1061
1175
  * @param options - Providers that replace the app's by token, extra controllers, and observers.
1062
1176
  * @returns The builder, whose `override*` methods still apply, for `compile()`.
1063
- * @throws TypeError when `app` is not a `@MeoCord` class.
1177
+ * @throws TypeError when `app` is not a `@MeoCord` class. After {@link reportAllStartupErrors}, an app whose
1178
+ * `@MeoCord` was refused throws the error it was refused for instead.
1064
1179
  *
1065
1180
  * @example
1066
1181
  * ```ts
@@ -1100,8 +1215,19 @@ import { isUserOutcome } from '../common/user-outcome.js';
1100
1215
  * expect(getResponse(interaction).calls[0].payload).toMatchObject({ content: '1 notes' })
1101
1216
  * ```
1102
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
+ }
1103
1223
  const appOptions = Reflect.getMetadata(META.appOptions, app);
1104
- if (!appOptions) throw new TypeError(`${app?.name ?? String(app)} is not a @MeoCord app: fromApp takes the class @MeoCord decorates.`);
1224
+ if (!appOptions) {
1225
+ // A refused @MeoCord stores no options, so its own errors, kept with startupErrors: 'all', say why
1226
+ collectStartupErrors(()=>declaredErrorsOf([
1227
+ app
1228
+ ]).forEach(startupError));
1229
+ throw new TypeError(`${app?.name ?? String(app)} is not a @MeoCord app: fromApp takes the class @MeoCord decorates.`);
1230
+ }
1105
1231
  const controllers = [
1106
1232
  ...new Set([
1107
1233
  ...appOptions.controllers,
@@ -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
  });