overrideCapability(token: CapabilityToken,service: object): IPlugin
Creates a plugin that REPLACES an already-provided capability with a test double, leaving the rest of the application's composition intact.
It is ordered after the provider and before ordinary consumers, so it reaches
a consumer that resolves the capability during its own register() as well as
one that resolves it per request.
This is the replacement mechanism AI_GUIDELINES §3.4 describes — "a
replacement plugin registers the same capability token with
override: true" — with the three constraints that make it work applied for
you:
- It declares no
provides. A second plugin declaring a token the real one already declares is refused by the kernel before any of them runs (Capability 'x' is provided by both 'a' and 'b'), which is whycreateMockPlugincannot be used to override. - It registers with
{ override: true }, without which the kernel refuses a second registration of a live token. - It is ordered after the provider and before ordinary consumers, which
is what lets it reach a consumer that resolves the capability during its
OWN
register()—NotificationPlugindoes exactly that. Ordering comes from anoptionalDependenciesedge on the token (the resolver visits a dependency first, whatever its priority band) plus an early priority, so the depth-first sort reaches this plugin, and through the edge its provider, before aPLUGIN_PRIORITY.NORMALconsumer.
What it still cannot do: undo the provider's eager side effects. The real
plugin's register() has run by the time this one replaces its service, so a
database adapter's connect() or a broker's dial has already happened. Only
removing the plugin prevents that — createTestApp({ app, without }), or
app.unregister(name) directly.
It requires the provider to declare the token in provides. That is what
the ordering edge hangs on, and it is how a plugin is depended upon at all. A
plugin that registers a capability without declaring it cannot be ordered
against, so the override registers first and the provider's own plain
registration then fails the application's startup with Capability '<token>' is already registered. Declare provides, or use the removal form below.
To remove the provider instead of replacing it — which also prevents its eager side effects — supply the double as a provider ahead of its consumers:
await createTestApp({ app: createApp(), without: ['mail-plugin'], overrides: [createMockPlugin({ name: 'mail-plugin', provides: CAPABILITIES.MAIL, service: fakeMailer, priority: PLUGIN_PRIORITY.HIGH, // ahead of the consumer that captures it })], });
Example 1
Example 1
import { createTestApp, overrideCapability } from '@setu-ts/testing'; import { CAPABILITIES } from '@setu-ts/common'; import { createApp } from '../setu.config.ts'; const app = await createTestApp({ app: createApp(), overrides: [overrideCapability(CAPABILITIES.MAIL, { send: () => Promise.resolve() })], });
A plugin to append to createTestApp's overrides, or to pass to
app.register() on an un-started application
Multi-provider capabilities cannot be overridden, and are refused. A token
registered with { multi: true } — the kernel's health-indicator,
metric-registration, openapi-schema, decorator-handler and
cli-command, and any an application registers itself — has no single
provider to replace: getAll returns the single and multi registrations
concatenated, so an override would add a provider while every real one kept
running. Detection is generic rather than a list of known tokens, so an
application's own multi capability is refused too. Exclude the plugin that
registers the provider instead.
During start() — from an onInit hook, not from
register() — if nothing provides token, or if token is a multi-provider
capability. Both checks run there because this plugin registers EARLY: at its
own register() a multi-provider capability has not accumulated its providers
yet, and a token it does not shadow may still be registered by a later plugin.
A silent no-op would leave the real service serving while the test reported
success.