fix(plugins): the silent-degrade doors close — a broken accelerator install can never read as absent
The auto-detection "not installed" heuristic accepted any resolution failure whose message merely CONTAINED the package name, unterminated — so a missing platform-binary sibling package (what a deploy replacing node_modules mid-restart leaves behind) read as "the accelerator is not installed", and brainy silently served the default WASM engines with zero journal lines. A production restart storm paid 90 seconds of throttled WASM compile behind exactly that hole. The name must now terminate where it ends (quote, whitespace, punctuation, end) — a sibling package, an inner file path, or a dependency failure is a broken install and init() throws, as the guard's own law always stated. Second door: activate() returning false (the documented graceful decline) warned on console.warn, which `silent: true` patches away — an invisible degrade. The decline now narrates via the always-on channel. Also exports CanonicalCounts from the public surface (the coverage-ledger denominator type consumers read through getCanonicalCounts()). Pinned in tests/unit/plugin-activation-loudness.test.ts (five error shapes; the decline warn under silent: true).
This commit is contained in:
parent
18f172e098
commit
b9ba50fbec
4 changed files with 92 additions and 6 deletions
|
|
@ -17622,8 +17622,15 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
private static isPackageNotInstalledError(error: unknown, pkg: string): boolean {
|
private static isPackageNotInstalledError(error: unknown, pkg: string): boolean {
|
||||||
const code = (error as { code?: string })?.code
|
const code = (error as { code?: string })?.code
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
const namesPackage =
|
// The package name must TERMINATE where it ends: an unanchored prefix match
|
||||||
message.includes(`'${pkg}'`) || message.includes(`"${pkg}"`) || message.includes(` ${pkg}`)
|
// read a missing platform-binary SIBLING package (e.g. "<pkg>-linux-x64-gnu",
|
||||||
|
// exactly what a deploy replacing node_modules mid-restart leaves behind) as
|
||||||
|
// "<pkg> is not installed" — and a present-but-broken accelerator silently
|
||||||
|
// degraded to the default JS engines. A production storm was hunted for a
|
||||||
|
// day because of that swallow. The name must be followed by a quote,
|
||||||
|
// whitespace, punctuation, or end-of-message — never a longer name's tail.
|
||||||
|
const escaped = pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
|
const namesPackage = new RegExp("(^|['\"\\s])" + escaped + "(?=$|['\"\\s.,)])").test(message)
|
||||||
const isResolutionFailure =
|
const isResolutionFailure =
|
||||||
code === 'ERR_MODULE_NOT_FOUND' ||
|
code === 'ERR_MODULE_NOT_FOUND' ||
|
||||||
code === 'MODULE_NOT_FOUND' ||
|
code === 'MODULE_NOT_FOUND' ||
|
||||||
|
|
|
||||||
|
|
@ -389,7 +389,10 @@ import type {
|
||||||
HNSWVerb,
|
HNSWVerb,
|
||||||
HNSWConfig,
|
HNSWConfig,
|
||||||
StorageAdapter,
|
StorageAdapter,
|
||||||
DerivedFamilyDeclaration
|
DerivedFamilyDeclaration,
|
||||||
|
// The canonical count ledger a storage adapter maintains (counted + ALL-visibility
|
||||||
|
// scalars per family, the coverage-ledger denominators) — see StorageAdapter.getCanonicalCounts.
|
||||||
|
CanonicalCounts
|
||||||
} from './coreTypes.js'
|
} from './coreTypes.js'
|
||||||
|
|
||||||
// Export vector index implementation (the JS HNSW path)
|
// Export vector index implementation (the JS HNSW path)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
* registered manually via `brain.use()` — there is no implicit detection.
|
* registered manually via `brain.use()` — there is no implicit detection.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { prodLog } from './utils/logger.js'
|
||||||
import type {
|
import type {
|
||||||
StorageAdapter,
|
StorageAdapter,
|
||||||
Vector,
|
Vector,
|
||||||
|
|
@ -1574,9 +1575,13 @@ export class PluginRegistry {
|
||||||
this.activated.add(name)
|
this.activated.add(name)
|
||||||
activated.push(name)
|
activated.push(name)
|
||||||
} else {
|
} else {
|
||||||
// Documented graceful decline (activate() → false). Surface it loudly so
|
// Documented graceful decline (activate() → false). Surface it on the
|
||||||
// a silent degrade to the default engine never goes unnoticed.
|
// ALWAYS-ON channel: `silent: true` patches console, and a declined
|
||||||
console.warn(
|
// accelerator warned into a patched console is a silent degrade to the
|
||||||
|
// default engines — the exact invisible-fallback class this registry
|
||||||
|
// exists to prevent (a production storm ran the WASM engine for 90s
|
||||||
|
// behind one suppressed warn).
|
||||||
|
prodLog.warn(
|
||||||
`[brainy] Plugin "${name}" declined activation (activate() returned false); ` +
|
`[brainy] Plugin "${name}" declined activation (activate() returned false); ` +
|
||||||
`the default engine is in use for its providers.`
|
`the default engine is in use for its providers.`
|
||||||
)
|
)
|
||||||
|
|
|
||||||
71
tests/unit/plugin-activation-loudness.test.ts
Normal file
71
tests/unit/plugin-activation-loudness.test.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
/**
|
||||||
|
* @module tests/unit/plugin-activation-loudness
|
||||||
|
* @description The plugin-activation swallow closes. Two laws:
|
||||||
|
* (1) THE NOT-INSTALLED FREE PASS IS EXACT — a resolution failure earns the
|
||||||
|
* silent skip ONLY when it names the probed package itself, terminated
|
||||||
|
* where the name ends. A missing platform-binary SIBLING package
|
||||||
|
* ("<pkg>-linux-x64-gnu" — what a deploy replacing node_modules
|
||||||
|
* mid-restart leaves), an inner file path, or a dependency failure is a
|
||||||
|
* BROKEN install and must fail loud. A production storm ran 90s of
|
||||||
|
* throttled WASM behind this exact prefix-match hole.
|
||||||
|
* (2) A GRACEFUL DECLINE IS NARRATED ON THE ALWAYS-ON CHANNEL — activate()
|
||||||
|
* returning false warns via prodLog, which `silent: true` cannot patch
|
||||||
|
* away; a declined accelerator is never an invisible degrade.
|
||||||
|
*/
|
||||||
|
import { describe, it, expect, vi, afterEach } from 'vitest'
|
||||||
|
import { Brainy } from '../../src/brainy.js'
|
||||||
|
import { prodLog } from '../../src/utils/logger.js'
|
||||||
|
|
||||||
|
const isNotInstalled = (error: unknown, pkg: string): boolean =>
|
||||||
|
(Brainy as unknown as {
|
||||||
|
isPackageNotInstalledError(e: unknown, p: string): boolean
|
||||||
|
}).isPackageNotInstalledError(error, pkg)
|
||||||
|
|
||||||
|
const resolutionError = (message: string): Error => {
|
||||||
|
const e = new Error(message) as Error & { code?: string }
|
||||||
|
e.code = 'ERR_MODULE_NOT_FOUND'
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('the not-installed free pass is exact', () => {
|
||||||
|
const PKG = '@soulcraft/cor'
|
||||||
|
|
||||||
|
it('the package itself, quoted or bare → not-installed (the one free path)', () => {
|
||||||
|
expect(isNotInstalled(resolutionError(`Cannot find package '${PKG}' imported from /app/x.js`), PKG)).toBe(true)
|
||||||
|
expect(isNotInstalled(resolutionError(`Cannot find module ${PKG}`), PKG)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('a missing platform-binary SIBLING package is a broken install, never not-installed', () => {
|
||||||
|
expect(isNotInstalled(resolutionError(`Cannot find package '${PKG}-linux-x64-gnu' imported from /app`), PKG)).toBe(false)
|
||||||
|
expect(isNotInstalled(resolutionError(`Failed to resolve ${PKG}-darwin-arm64`), PKG)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('an inner file path or a non-resolution error is never not-installed', () => {
|
||||||
|
expect(isNotInstalled(resolutionError(`Cannot find module '/app/node_modules/${PKG}/native/b.node'`), PKG)).toBe(false)
|
||||||
|
expect(isNotInstalled(new Error(`dlopen failed: wrong ELF class in ${PKG}`), PKG)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('a graceful decline is narrated on the always-on channel', () => {
|
||||||
|
afterEach(() => vi.restoreAllMocks())
|
||||||
|
|
||||||
|
it('activate() → false warns via prodLog even under silent: true', async () => {
|
||||||
|
process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
|
||||||
|
const warn = vi.spyOn(prodLog, 'warn')
|
||||||
|
const brain: any = new Brainy({
|
||||||
|
requireSubtype: false,
|
||||||
|
storage: { type: 'memory' },
|
||||||
|
silent: true,
|
||||||
|
dimensions: 384
|
||||||
|
})
|
||||||
|
brain.use({ name: 'declining-accelerator', activate: async () => false })
|
||||||
|
await brain.init()
|
||||||
|
try {
|
||||||
|
expect(
|
||||||
|
warn.mock.calls.some((c) => String(c[0]).includes('"declining-accelerator" declined activation'))
|
||||||
|
).toBe(true)
|
||||||
|
} finally {
|
||||||
|
await brain.close().catch(() => {})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue