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.
- package/CHANGELOG.md +188 -0
- package/dist/cjs/_shared/{controller.decorator-XKMlcUZz.cjs → controller.decorator-hp5xdJU4.cjs} +550 -189
- package/dist/cjs/_shared/{startup-roots-CgfPQrNl.cjs → event-sources-DzqTODXg.cjs} +274 -88
- package/dist/cjs/_shared/{providers-BZgJyfZS.cjs → providers-BBvbnmLP.cjs} +837 -295
- package/dist/cjs/_shared/{sharded-cooldown-store-CsJJyCZd.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
- package/dist/cjs/_shared/{translate-error-BUAR1WYG.cjs → translate-error-wsgfsUET.cjs} +1 -1
- package/dist/cjs/common/index.cjs +4 -104
- package/dist/cjs/core/index.cjs +435 -182
- package/dist/cjs/decorator/index.cjs +116 -121
- package/dist/cjs/testing/index.cjs +1923 -808
- 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 +32 -10
- package/dist/esm/common/sharded-cooldown-store.js +11 -9
- package/dist/esm/common/theme.js +3 -1
- package/dist/esm/common/translator.js +27 -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 +92 -66
- 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 +6 -6
- 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 +54 -32
- 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 +83 -54
- package/dist/esm/core/meocord.app.js +174 -89
- package/dist/esm/core/message-help.js +37 -15
- 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 +4 -2
- package/dist/esm/core/process-state.js +19 -0
- package/dist/esm/core/providers.js +39 -4
- 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/stage-scope.js +2 -1
- 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 +31 -30
- package/dist/esm/core/theme-validation.js +1 -1
- package/dist/esm/decorator/app.decorator.js +37 -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 +4 -4
- 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 +32 -25
- package/dist/esm/testing/cooldown-store-suite.js +3 -3
- package/dist/esm/testing/discord-defaults.js +449 -335
- package/dist/esm/testing/inspect-handler.js +91 -10
- package/dist/esm/testing/meocord-testing-module.js +217 -91
- package/dist/esm/testing/mock-fn.js +8 -2
- package/dist/esm/testing/mock-interaction.js +864 -124
- 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 +8 -4
- 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 +33 -5
- package/dist/types/common/index.d.cts +17 -10
- package/dist/types/common/index.d.ts +17 -10
- 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 +158 -61
- package/dist/types/testing/index.d.ts +158 -61
- 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';
|
|
@@ -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 {
|
|
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 {
|
|
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.
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
* the
|
|
274
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
369
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
458
|
-
*
|
|
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
|
|
484
|
-
for (const handler of
|
|
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 = `${
|
|
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 =
|
|
494
|
-
|
|
495
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 ?? [],
|
|
972
|
-
|
|
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)
|
|
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
|
-
//
|
|
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
|
});
|