Plugins
Sounding keeps core small and lets heavier capabilities live in installable plugins.
That means a normal Sails app can keep using the core testing runtime, while features such as stress testing, mutation testing, or future specialized reporters can bring their own dependencies only when an app asks for them.
Install-only setup
Install a Sounding plugin as a dev dependency:
npm install -D sounding-plugin-stressThen use the API it adds:
npx sounding stress /api/health --duration=10 --concurrency=25There is no plugins array and no config/sounding.js registration step. The dev dependency is the registration: Sounding discovers plugin packages from the app's package.json.
config/sounding.js is still available for core Sounding runtime overrides, such as datastore, auth, browser projects, or diagnostics. It is not where users register plugins.
Discovery
Sounding discovers package names that match either of these patterns:
sounding-plugin-*@sounding/plugin-*
For local monorepo development, Sounding also discovers plugins under plugins/* when each plugin folder has its own package.json with a matching package name.
For example:
my-sails-app/
package.json
plugins/
stress/
package.json
index.jsIf plugins/stress/package.json is named sounding-plugin-stress, Sounding can load it while you work locally.
What plugins can add
A plugin can add three public surfaces:
- CLI commands, such as
sounding stress - focused test methods, such as
test.stress(...) - trial context helpers, such as
{ stress }
The plugin returns those surfaces from its factory:
module.exports = function stressPlugin(api) {
return {
name: 'stress',
commands: {
stress(argv, context) {
return runStressCommand(argv, {
...context,
api
})
}
},
testMethods: {
stress: {
mode: 'stress',
options: {
transport: 'http'
}
}
},
trial({ sails, config, world, appPath, events }) {
return {
stress: createStressClient({
api,
sails,
getConfig: () => config,
world,
appPath,
events
})
}
}
}
}That shape is intentionally close to Sails' hook idea: install a package, let it extend the runtime, and keep the app-facing API calm.
Commands
Commands are registered under commands.
commands: {
example(argv, context) {
context.stdout.write('Hello from Sounding\n')
return {
status: 0
}
}
}If a plugin registers example, users can run:
npx sounding exampleCommand handlers receive:
argv, the command arguments after the command namecontext.appPath, the Sails app pathcontext.stdoutcontext.stderrcontext.api, the Sounding plugin API
Commands should return an object with a numeric status when they want to control the process exit code.
Test methods
Plugins can add focused test methods when a feature deserves its own lane.
The stress plugin adds test.stress(...):
test.stress(
'billing summary stays fast',
{ world: 'subscribed-creator' },
async ({ stress, expect }) => {
const result = await stress
.get('/api/billing/summary')
.as('owner')
.concurrently(20)
.for(10)
.seconds()
expect(result.requests.failed().count()).toBe(0)
}
)test.stress() is still a Sounding trial. The plugin only sets helpful defaults for that lane, such as transport: 'http'.
Trial helpers
Plugins can add keys to the trial context through trial().
trial({ sails, config, world, events }) {
return {
example: createExampleClient({
sails,
config,
world,
events
})
}
}The returned object is merged into the trial context:
test('uses a plugin helper', async ({ example }) => {
await example.run()
})Keep trial helpers small and product-facing. If a helper needs a heavy engine, put that engine dependency in the plugin package instead of Sounding core.
Plugin API
The plugin factory receives a small Sails-aware API:
module.exports = function myPlugin(api) {
// ...
}Available helpers include:
| API | Purpose |
|---|---|
api.appPath | The resolved app path Sounding is running against. |
api.events | Shared plugin event bus. |
api.createAppManager() | Load or lift the Sails app for command-style plugins. |
api.createSessionCookie(sails, session) | Create a real Sails session cookie for HTTP runs. |
api.createSoundingError(details) | Create a structured Sounding error. |
api.resolveAuthConfig(input) | Resolve the app's auth convention. |
api.resolveWorldActor(input) | Resolve an actor alias from the active world. |
api.resolveActorHeaders(actor) | Read actor-provided request headers. |
api.resolveActorSession(actor, auth) | Build session data from an actor and auth convention. |
api.resolveBaseUrl(input) | Resolve a base URL from app config. |
api.resolveUrl(input) | Resolve a request target to an absolute URL. |
api.createRequestActorUnresolvedError(input) | Build the same actor resolution error used by request clients. |
api.loadDependencyFromApp(input) | Load a dependency from the app with a friendly install error. |
That API lets plugins feel native without duplicating Sounding internals.
Events
Sounding exposes an EventEmitter as a lifecycle and observability channel.
Core emits:
plugin:loadedtrial:plugin:beforetrial:plugin:after
Plugins can emit their own events:
events.emit('stress:start', options)
events.emit('stress:done', result)Events are good for:
- progress output
- custom reporters
- logging
- optional plugin-to-plugin integrations
- future streaming dashboards
Events should not be the main registration mechanism for public APIs. Commands, test methods, and trial helpers should stay explicit so users can understand what a plugin adds by reading its returned object.
Missing plugins
When a known plugin command is missing, Sounding should explain the setup instead of forcing that feature into core.
For example, running sounding stress without the stress plugin points users to:
npm install -D sounding-plugin-stressThat is the whole point of the plugin split: the API can feel first-party, while the dependency stays optional.
First plugin
The first official plugin is Stress testing.