Two production correctness defects fixed together so consumers can land
one upgrade.
BR-FIND-WHERE-ZERO — find() returned [] and stats() reported 0 entities
for any workspace whose data was written after the 7.20.0 column-store
refactor. Root cause: getStats() and the getIds() fallback still read
from the deleted sparse-index path. Separately, BaseStorage.getNounType()
was hardcoded to return 'thing', poisoning type-statistics.json with
every noun attributed to that bucket.
- MetadataIndex.getStats() reads from ColumnStore + idMapper.
- MetadataIndex.getIdsFromChunks() throws BrainyError(FIELD_NOT_INDEXED)
when neither store has the field. getIdsForFilter() catches per
clause, logs once, returns [].
- BaseStorage.nounTypeByIdCache populated in saveNounMetadata_internal
and consumed in saveNoun_internal. flushCounts() now persists the
Uint32Array counters too, so readers see fresh per-type counts.
- Self-heal at init: loadTypeStatistics() auto-rebuilds when the
poisoned signature is detected. rebuildTypeCounts() is now public for
use from `brainy inspect repair`.
- Dead state removed: dirtyChunks, dirtySparseIndices,
flushDirtyMetadata().
BR-DEFENSIVE-INTERFACE — 7.21.0 called supportsMultiProcessLocking()
unconditionally, crashing on older Cortex storage adapters that predate
the method. New hasStorageMethod(name) helper gates every new-method
call site. Older adapter triggers a one-line warning at init pointing
at the recommended plugin version.
Tests:
- new tests/integration/find-where-zero.test.ts (7 cases)
- new tests/integration/cortex-compat.test.ts (5 cases)
- multi-process-safety.test.ts updated to enforce correct counts
- 1329/1329 unit + 24/24 new integration tests passing
106 lines
4 KiB
TypeScript
106 lines
4 KiB
TypeScript
/**
|
|
* Cortex compatibility test (BR-DEFENSIVE-INTERFACE regression).
|
|
*
|
|
* Brainy 7.21.0 added several optional storage-adapter methods
|
|
* (`supportsMultiProcessLocking`, `acquireWriterLock`, etc.). Older plugins
|
|
* (notably `@soulcraft/cortex@2.2.0`) bundle a pre-7.21 `BaseStorage` that
|
|
* doesn't have them. Calling them unconditionally crashed boot.
|
|
*
|
|
* 7.22.0 fix: every call site is gated by `hasStorageMethod(name)` so older
|
|
* adapters degrade with a single warning at init.
|
|
*
|
|
* This test constructs a deliberately-stripped storage adapter (no new
|
|
* methods) and proves Brainy boots, can add/find/close normally, and logs
|
|
* the expected one-line warning.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
import { Brainy } from '../../src/brainy.js'
|
|
import { MemoryStorage } from '../../src/storage/adapters/memoryStorage.js'
|
|
import { NounType } from '../../src/types/graphTypes.js'
|
|
|
|
/**
|
|
* Mock storage adapter modeling pre-7.21 Cortex: extends MemoryStorage
|
|
* (which has full storage semantics) but deletes the new methods so Brainy
|
|
* sees a "method missing" surface. Class name is masked so the warning
|
|
* message names it as `LegacyCortexLikeStorage`.
|
|
*/
|
|
class LegacyCortexLikeStorage extends MemoryStorage {
|
|
constructor() {
|
|
super()
|
|
// Strip the 7.21+ methods. `delete` on prototype methods is awkward
|
|
// across TS strictness levels; assigning `undefined` produces the same
|
|
// `typeof === 'function'` false that older adapters exhibit.
|
|
;(this as any).supportsMultiProcessLocking = undefined
|
|
;(this as any).acquireWriterLock = undefined
|
|
;(this as any).releaseWriterLock = undefined
|
|
;(this as any).readWriterLock = undefined
|
|
;(this as any).startFlushRequestWatcher = undefined
|
|
;(this as any).stopFlushRequestWatcher = undefined
|
|
;(this as any).requestFlushOverFilesystem = undefined
|
|
}
|
|
}
|
|
|
|
describe('Cortex compatibility (BR-DEFENSIVE-INTERFACE)', () => {
|
|
let brain: Brainy | null = null
|
|
let warnSpy: ReturnType<typeof vi.spyOn>
|
|
|
|
beforeEach(() => {
|
|
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
})
|
|
|
|
afterEach(async () => {
|
|
if (brain) {
|
|
try { await brain.close() } catch { /* may already be closed */ }
|
|
brain = null
|
|
}
|
|
warnSpy.mockRestore()
|
|
})
|
|
|
|
it('boots without throwing against an adapter missing the 7.21+ methods', async () => {
|
|
brain = new Brainy({ storage: new LegacyCortexLikeStorage() as any })
|
|
await expect(brain.init()).resolves.toBeUndefined()
|
|
})
|
|
|
|
it('logs a one-line warning naming the older adapter', async () => {
|
|
brain = new Brainy({ storage: new LegacyCortexLikeStorage() as any })
|
|
await brain.init()
|
|
|
|
const warnMessages = warnSpy.mock.calls
|
|
.map(call => String(call[0] ?? ''))
|
|
.join('\n')
|
|
expect(warnMessages).toMatch(/LegacyCortexLikeStorage|predates the 7\.21/i)
|
|
})
|
|
|
|
it('basic CRUD works normally (the missing methods are not on the hot path)', async () => {
|
|
brain = new Brainy({ storage: new LegacyCortexLikeStorage() as any })
|
|
await brain.init()
|
|
|
|
const id = await brain.add({ data: 'hello', type: NounType.Concept })
|
|
const fetched = await brain.get(id)
|
|
expect(fetched).toBeTruthy()
|
|
expect(fetched!.id).toBe(id)
|
|
|
|
await brain.flush() // No-op for memory-backed, but must not throw.
|
|
})
|
|
|
|
it('requestFlush() returns false when storage does not implement the RPC', async () => {
|
|
brain = new Brainy({ storage: new LegacyCortexLikeStorage() as any })
|
|
await brain.init()
|
|
|
|
// Same-process call would normally flush directly. Force the
|
|
// cross-process path by temporarily making this brain look read-only.
|
|
;(brain as any).operationalMode = { canWrite: false, validateOperation: () => {} }
|
|
const ok = await brain.requestFlush({ timeoutMs: 500 })
|
|
expect(ok).toBe(false)
|
|
})
|
|
|
|
it('stats() reports no writerLock for older adapters', async () => {
|
|
brain = new Brainy({ storage: new LegacyCortexLikeStorage() as any })
|
|
await brain.init()
|
|
|
|
const stats = await brain.stats()
|
|
expect(stats.mode).toBe('writer')
|
|
expect(stats.writerLock).toBeUndefined()
|
|
})
|
|
})
|