brainy/tests/unit/plugin-autodetect.test.ts
David Snelling 588267be7f feat: guarded plugin auto-detection — installing @soulcraft/cor is the opt-in
With plugins unset (the default), init() probes for the first-party
accelerator: not installed means plain brainy with zero noise; installed and
healthy means it loads and announces itself; installed but broken (import
failure, invalid shape, failed activation, version mismatch) makes init()
THROW. An installed accelerator never silently vanishes behind the JS engines
- the anti-drift posture of the explicit list, applied to detection.

plugins: []/false stays a true opt-out (no probe); an explicit list keeps its
required-and-loud semantics. The import runs through an importPluginPackage
seam (variable specifier - bundlers cannot static-resolve the optional
package; tests simulate all outcomes without it installed).

Also: when a plugin activates but registers zero native providers (e.g. a
licensing gate declining to engage), the provider summary now warns loudly
instead of leaving every query silently on the JS engines.

Supersedes 8.0.8's explicit-opt-in wording; README/PLUGINS/types now document
the guarded contract. 7 new tests (tests/unit/plugin-autodetect.test.ts).
2026-07-02 16:19:55 -07:00

136 lines
5.6 KiB
TypeScript

/**
* Guarded plugin auto-detection (8.0.9) — the "install it and it's on" contract.
*
* With `plugins` unset, brainy probes for the first-party accelerator
* (@soulcraft/cor). The probe applies the same loud posture as the explicit
* list to everything except "not installed":
* - not installed → plain brainy, silently (the free path)
* - installed + healthy → registered and activated
* - installed + broken → init() THROWS (import failure, invalid shape,
* or failed activation — an installed
* accelerator never silently vanishes)
* - plugins: [] / false → no probe at all (explicit opt-out)
* - explicit list → unchanged: required, loud on any failure
*
* The import is isolated behind the private `importPluginPackage` seam so the
* three outcomes are simulated without the package being present.
*/
import { describe, it, expect, afterEach } from 'vitest'
import { Brainy, NounType } from '../../src/index.js'
const proto = Brainy.prototype as any
const origImport = proto.importPluginPackage
afterEach(() => {
proto.importPluginPackage = origImport
})
/** Simulate the probe outcome per package name. */
function stubImport(fn: (pkg: string) => Promise<any>): string[] {
const probed: string[] = []
proto.importPluginPackage = async function (pkg: string) {
probed.push(pkg)
return fn(pkg)
}
return probed
}
function notInstalledError(pkg: string): Error {
// Node's exact shape for a missing package in a dynamic ESM import.
const e = new Error(`Cannot find package '${pkg}' imported from /app/index.js`)
;(e as any).code = 'ERR_MODULE_NOT_FOUND'
return e
}
/** A minimal valid plugin that registers one well-known provider. */
function validPlugin(name: string) {
return {
name,
activate: async (context: any) => {
context.registerProvider('distance', { euclidean: (a: number[], b: number[]) => 0 })
return true
}
}
}
describe('Guarded plugin auto-detection (plugins: undefined)', () => {
it('not installed → plain brainy, no plugins, init succeeds silently', async () => {
const probed = stubImport(async (pkg) => {
throw notInstalledError(pkg)
})
const brain: any = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true })
await brain.init()
expect(probed).toContain('@soulcraft/cor') // the probe ran…
expect(brain.pluginRegistry.hasActivePlugins()).toBe(false) // …and found nothing, quietly
const id = await brain.add({ data: 'works without any plugin', type: NounType.Concept })
expect(id).toBeTruthy()
await brain.close()
})
it('installed + healthy → auto-registered and activated', async () => {
stubImport(async () => ({ default: validPlugin('@soulcraft/cor') }))
const brain: any = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true })
await brain.init()
expect(brain.pluginRegistry.getActivePlugins()).toContain('@soulcraft/cor')
expect(brain.pluginRegistry.hasProvider('distance')).toBe(true)
await brain.close()
})
it('installed but import fails (broken install) → init() throws, never a silent JS fallback', async () => {
stubImport(async () => {
// A resolution failure INSIDE the package (names a sub-path, not the
// package) — present but broken, must be loud.
const e = new Error(
"Cannot find module '/app/node_modules/@soulcraft/cor/dist/native.js' imported from " +
'/app/node_modules/@soulcraft/cor/dist/index.js'
)
;(e as any).code = 'ERR_MODULE_NOT_FOUND'
throw e
})
const brain: any = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true })
await expect(brain.init()).rejects.toThrow(/installed but failed to load/)
})
it('installed but not a valid plugin (missing activate) → init() throws', async () => {
stubImport(async () => ({ default: { name: '@soulcraft/cor' } })) // no activate()
const brain: any = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true })
await expect(brain.init()).rejects.toThrow(/not a valid Brainy plugin/)
})
it('installed but activation fails → init() throws (activateAll posture applies)', async () => {
stubImport(async () => ({
default: {
name: '@soulcraft/cor',
activate: async () => {
throw new Error('native binary missing for platform')
}
}
}))
const brain: any = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true })
await expect(brain.init()).rejects.toThrow(/failed to activate/)
})
it('plugins: [] and plugins: false → no probe at all (explicit opt-out)', async () => {
const probed = stubImport(async () => ({ default: validPlugin('@soulcraft/cor') }))
for (const plugins of [[], false] as const) {
const brain: any = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, plugins: plugins as any, silent: true })
await brain.init()
expect(brain.pluginRegistry.hasActivePlugins()).toBe(false)
await brain.close()
}
expect(probed).toEqual([]) // the seam was never touched
})
it('explicit list is unchanged: a listed-but-missing plugin throws with the config-pointing message', async () => {
stubImport(async (pkg) => {
throw notInstalledError(pkg)
})
const brain: any = new Brainy({
requireSubtype: false,
storage: { type: 'memory' },
plugins: ['@soulcraft/cor'],
silent: true
})
await expect(brain.init()).rejects.toThrow(/listed in config\.plugins but could not be loaded/)
})
})