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).
This commit is contained in:
parent
b37359e097
commit
588267be7f
6 changed files with 265 additions and 26 deletions
136
tests/unit/plugin-autodetect.test.ts
Normal file
136
tests/unit/plugin-autodetect.test.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
/**
|
||||
* 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/)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue