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.
- package/CHANGELOG.md +130 -0
- package/dist/cjs/_shared/{controller.decorator-U2NBWcoU.cjs → controller.decorator-hp5xdJU4.cjs} +512 -183
- package/dist/cjs/_shared/{startup-roots-lA5xkBFq.cjs → event-sources-DzqTODXg.cjs} +265 -80
- package/dist/cjs/_shared/{providers-B4MOFBrW.cjs → providers-BBvbnmLP.cjs} +780 -281
- package/dist/cjs/_shared/{sharded-cooldown-store-HTUb3Ii_.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
- package/dist/cjs/_shared/{translate-error-CbBHUu5P.cjs → translate-error-wsgfsUET.cjs} +1 -1
- package/dist/cjs/common/index.cjs +4 -104
- package/dist/cjs/core/index.cjs +432 -181
- package/dist/cjs/decorator/index.cjs +85 -120
- package/dist/cjs/testing/index.cjs +1664 -784
- package/dist/cli.json +1 -1
- package/dist/esm/bin/app-template/package.json.template +9 -9
- package/dist/esm/bin/app-template/src/main.ts.template +3 -2
- package/dist/esm/bin/generator.js +6 -4
- package/dist/esm/bin/helper/filter-generator.helper.js +6 -4
- package/dist/esm/bin/helper/guard-generator.helper.js +6 -4
- package/dist/esm/bin/helper/interceptor-generator.helper.js +6 -4
- package/dist/esm/bin/helper/observer-generator.helper.js +6 -4
- package/dist/esm/bin/helper/pipe-generator.helper.js +6 -4
- package/dist/esm/bin/helper/service-generator.helper.js +6 -4
- package/dist/esm/bin/meocord.js +106 -48
- package/dist/esm/build/native-addons.js +75 -19
- package/dist/esm/build/rsbuild-config.js +11 -3
- package/dist/esm/build/typed-externals.js +15 -2
- package/dist/esm/common/cooldown-store.js +38 -14
- package/dist/esm/common/deprecation.js +4 -1
- package/dist/esm/common/execution-context.js +3 -1
- package/dist/esm/common/explained-error.js +7 -3
- package/dist/esm/common/log-level.js +13 -9
- package/dist/esm/common/logger.js +4 -2
- package/dist/esm/common/meocord-text.js +11 -6
- package/dist/esm/common/redis-cooldown-store.js +3 -7
- package/dist/esm/common/response/flags.js +17 -1
- package/dist/esm/common/response/install-context.js +27 -16
- package/dist/esm/common/response/presenter.js +7 -3
- package/dist/esm/common/response/response-state.js +27 -5
- package/dist/esm/common/sharded-cooldown-store.js +11 -9
- package/dist/esm/common/theme.js +3 -1
- package/dist/esm/common/translator.js +17 -8
- package/dist/esm/core/autocomplete-routes.js +26 -0
- package/dist/esm/core/command-conflicts.js +15 -7
- package/dist/esm/core/command-registration.js +15 -1
- package/dist/esm/core/cooldown-runner.js +7 -9
- package/dist/esm/core/defer.js +2 -5
- package/dist/esm/core/dispatcher.js +83 -58
- package/dist/esm/core/event-requirements.js +67 -19
- package/dist/esm/core/event-sources.js +100 -0
- package/dist/esm/core/fallback.js +3 -1
- package/dist/esm/core/filter-runner.js +4 -4
- package/dist/esm/core/guard-runner.js +173 -52
- package/dist/esm/core/handler-input.js +24 -3
- package/dist/esm/core/handler-pipeline.js +44 -28
- package/dist/esm/core/handler-registry.js +51 -21
- package/dist/esm/core/input-runner.js +7 -8
- package/dist/esm/core/interceptor-runner.js +4 -5
- package/dist/esm/core/lifecycle-hooks.js +3 -8
- package/dist/esm/core/lifecycle-order.js +1 -15
- package/dist/esm/core/meocord-classes.js +14 -1
- package/dist/esm/core/meocord-factory.js +80 -53
- package/dist/esm/core/meocord.app.js +174 -89
- package/dist/esm/core/message-help.js +5 -9
- package/dist/esm/core/message-params.js +126 -46
- package/dist/esm/core/message-routes.js +225 -45
- package/dist/esm/core/message-words.js +30 -2
- package/dist/esm/core/observer-runner.js +2 -1
- package/dist/esm/core/process-state.js +19 -0
- package/dist/esm/core/providers.js +28 -3
- package/dist/esm/core/scalar-types.js +57 -16
- package/dist/esm/core/shard-context.js +11 -10
- package/dist/esm/core/shard-exit.js +3 -3
- package/dist/esm/core/shard-manager.js +16 -6
- package/dist/esm/core/startup-roots.js +28 -8
- package/dist/esm/core/theme-resolvers.js +26 -11
- package/dist/esm/core/theme-runtime.js +23 -13
- package/dist/esm/core/theme-scope.js +27 -29
- package/dist/esm/core/theme-validation.js +1 -1
- package/dist/esm/decorator/app.decorator.js +6 -1
- package/dist/esm/decorator/command-builder.decorator.js +14 -0
- package/dist/esm/decorator/controller-class.decorator.js +1 -1
- package/dist/esm/decorator/controller.decorator.js +48 -19
- package/dist/esm/decorator/defer.decorator.js +2 -2
- package/dist/esm/decorator/event.decorator.js +4 -5
- package/dist/esm/decorator/filter.decorator.js +2 -2
- package/dist/esm/decorator/guard.decorator.js +4 -61
- package/dist/esm/decorator/inject.decorator.js +4 -1
- package/dist/esm/decorator/interceptor.decorator.js +2 -2
- package/dist/esm/decorator/theme.decorator.js +4 -3
- package/dist/esm/decorator/validation.decorator.js +5 -5
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +29 -13
- package/dist/esm/testing/cooldown-store-suite.js +3 -3
- package/dist/esm/testing/discord-defaults.js +449 -355
- package/dist/esm/testing/inspect-handler.js +91 -10
- package/dist/esm/testing/meocord-testing-module.js +195 -77
- package/dist/esm/testing/mock-fn.js +8 -2
- package/dist/esm/testing/mock-interaction.js +639 -113
- package/dist/esm/testing/response-log.js +8 -0
- package/dist/esm/testing/response.js +7 -5
- package/dist/esm/testing/routing.js +48 -6
- package/dist/esm/testing/strict-mocks.js +1 -0
- package/dist/esm/testing/theme.js +3 -2
- package/dist/esm/util/common.util.js +30 -9
- package/dist/esm/util/generator-cli.util.js +6 -4
- package/dist/esm/util/inherited-env.util.js +72 -13
- package/dist/esm/util/json.util.js +10 -3
- package/dist/esm/util/meocord-cli.util.js +3 -2
- package/dist/esm/util/meocord-config-loader.util.js +14 -1
- package/dist/esm/util/meocord-config-validation.util.js +13 -3
- package/dist/esm/util/meocord-source-config.util.js +13 -3
- package/dist/esm/util/platform.util.js +3 -2
- package/dist/esm/util/refusal.util.js +29 -11
- package/dist/esm/util/registration-mode.util.js +14 -1
- package/dist/esm/util/timer-limit.util.js +4 -1
- package/dist/esm/util/tsconfig-paths.util.js +38 -7
- package/dist/esm/util/tsconfig.util.js +16 -58
- package/dist/esm/util/user-text.util.js +8 -4
- package/dist/esm/util/value.util.js +15 -4
- package/dist/types/common/index.d.cts +13 -9
- package/dist/types/common/index.d.ts +13 -9
- package/dist/types/core/index.d.cts +21 -11
- package/dist/types/core/index.d.ts +21 -11
- package/dist/types/decorator/index.d.cts +63 -63
- package/dist/types/decorator/index.d.ts +63 -63
- package/dist/types/{index-DKJclIi-.d.ts → index-Brkd6QGL.d.ts} +81 -35
- package/dist/types/{index-Dlf0ZZtS.d.cts → index-CqXImwF_.d.cts} +81 -35
- package/dist/types/interface/index.d.cts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/{route-Bh1U13yy.d.ts → route-IQrK-MN2.d.cts} +11 -3
- package/dist/types/{route-Bh1U13yy.d.cts → route-bI-okq7g.d.ts} +11 -3
- package/dist/types/testing/index.d.cts +137 -54
- package/dist/types/testing/index.d.ts +137 -54
- package/dist/types/{theme-resolvers-CyEz9pnv.d.cts → theme-resolvers-AtgeaYqF.d.cts} +1 -1
- package/dist/types/{theme-resolvers-DA204O-q.d.ts → theme-resolvers-BgqysV8d.d.ts} +1 -1
- package/dist/types/{translator-Bg7hwxDF.d.cts → translator-D8aydlCE.d.ts} +12 -11
- package/dist/types/{translator-CeP4bCzp.d.ts → translator-iJhDlgwL.d.cts} +12 -11
- 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
|
|
9
|
-
import { injectedTokens, singletonContextError,
|
|
10
|
-
import {
|
|
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 {
|
|
26
|
-
import {
|
|
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.
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
* the
|
|
275
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
370
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
459
|
-
*
|
|
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
|
|
485
|
-
for (const handler of
|
|
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 = `${
|
|
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 =
|
|
495
|
-
|
|
496
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 ?? [],
|
|
973
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
270
|
+
Object.assign(result, {
|
|
265
271
|
type,
|
|
266
272
|
value
|
|
267
273
|
});
|