137 lines
5.6 KiB
TypeScript
137 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/)
|
||
|
|
})
|
||
|
|
})
|