Compare commits
12 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 842ad44b88 | |||
| 360feaccf8 | |||
| a2ea21b330 | |||
| aac853d3e8 | |||
| 1882532cb7 | |||
| 6eb5e4483d | |||
| ba10aaf52e | |||
| 656d9f6f92 | |||
| be307a1579 | |||
| de79d6b5a4 | |||
| d6e7453f1f | |||
| 4c344782a7 |
49 changed files with 361 additions and 84 deletions
|
|
@ -3,6 +3,13 @@
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
|
||||||
|
### [10.4.13](https://source.soulcraft.com/soulcraftlabs/open-brainy/compare/v10.4.12...v10.4.13) (2026-09-03)
|
||||||
|
|
||||||
|
- A shutdown that holds its listener until the exit decision, and a test suite that closes every brain it opens
|
||||||
|
- fix(shutdown): the engine's signal handler keeps its listener registered until the exit decision is made — closing the last live instance no longer deregisters the handler mid-run, so a second signal delivery during a clean shutdown can never kill the process after the work is done (a2ea21b3)
|
||||||
|
- fix(release): the release wall entry commits under an explicit git identity read from the developer's checkout; a host with no identity refuses by name instead of failing inside git (aac853d3)
|
||||||
|
- test(hygiene): every brain a test file creates is closed by that file — 40 files fixed, the leaks that let a stray cadence narrate into later files are gone; brains whose init() was expected to fail are closed too (6eb5e448)
|
||||||
|
|
||||||
### [10.4.12](https://source.soulcraft.com/soulcraftlabs/open-brainy/compare/v10.4.11...v10.4.12) (2026-09-03)
|
### [10.4.12](https://source.soulcraft.com/soulcraftlabs/open-brainy/compare/v10.4.11...v10.4.12) (2026-09-03)
|
||||||
|
|
||||||
- Mixed-kind fields index exactly, arrays to 256, a drained loop is not a shutdown, and finds project from the column store
|
- Mixed-kind fields index exactly, arrays to 256, a drained loop is not a shutdown, and finds project from the column store
|
||||||
|
|
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@soulcraftlabs/brainy",
|
"name": "@soulcraftlabs/brainy",
|
||||||
"version": "10.4.12",
|
"version": "10.4.13",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@soulcraftlabs/brainy",
|
"name": "@soulcraftlabs/brainy",
|
||||||
"version": "10.4.12",
|
"version": "10.4.13",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@msgpack/msgpack": "^3.1.2",
|
"@msgpack/msgpack": "^3.1.2",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@soulcraftlabs/brainy",
|
"name": "@soulcraftlabs/brainy",
|
||||||
"version": "10.4.12",
|
"version": "10.4.13",
|
||||||
"brainyContract": 1,
|
"brainyContract": 1,
|
||||||
"description": "Universal Knowledge Protocol™ - World's first Triple Intelligence database unifying vector, graph, and document search in one API. Stage 3 CANONICAL: 42 nouns × 127 verbs covering 96-97% of all human knowledge.",
|
"description": "Universal Knowledge Protocol™ - World's first Triple Intelligence database unifying vector, graph, and document search in one API. Stage 3 CANONICAL: 42 nouns × 127 verbs covering 96-97% of all human knowledge.",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|
|
||||||
|
|
@ -337,6 +337,36 @@ function git(args, cwd) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the git identity for the wall commit from the repository the rail
|
||||||
|
* is actually running in — the developer's own checkout (`process.cwd()`;
|
||||||
|
* `release.sh` invokes this script from the repo root with no `cd`), via
|
||||||
|
* git's normal config precedence (repo-local, then global, then system).
|
||||||
|
* Never guessed and never left to git's own "who are you?" prompt: a host
|
||||||
|
* with no configured identity anywhere (a bare CI box, say) must refuse
|
||||||
|
* loudly rather than have git manufacture a placeholder identity or hang.
|
||||||
|
* @returns {{name: string, email: string}}
|
||||||
|
*/
|
||||||
|
function resolveWallCommitIdentity() {
|
||||||
|
const repo = process.cwd()
|
||||||
|
let name = ''
|
||||||
|
let email = ''
|
||||||
|
try {
|
||||||
|
name = git(['config', 'user.name'], repo)
|
||||||
|
} catch {
|
||||||
|
name = ''
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
email = git(['config', 'user.email'], repo)
|
||||||
|
} catch {
|
||||||
|
email = ''
|
||||||
|
}
|
||||||
|
if (!name || !email) {
|
||||||
|
fail('no git identity for the wall commit — set user.name/user.email')
|
||||||
|
}
|
||||||
|
return { name, email }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure a clean, up-to-date local clone of the releases repo at
|
* Ensure a clean, up-to-date local clone of the releases repo at
|
||||||
* `cacheDir`, checked out on `main` — cloning fresh if `cacheDir` has no
|
* `cacheDir`, checked out on `main` — cloning fresh if `cacheDir` has no
|
||||||
|
|
@ -423,9 +453,14 @@ function publishEntry(entry, product, remote, cacheDir) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const identity = resolveWallCommitIdentity()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
git(['add', `${product}.json`], cacheDir)
|
git(['add', `${product}.json`], cacheDir)
|
||||||
git(['commit', '-m', `chore(wall): ${product} ${entry.version}`], cacheDir)
|
git(
|
||||||
|
['-c', `user.name=${identity.name}`, '-c', `user.email=${identity.email}`, 'commit', '-m', `chore(wall): ${product} ${entry.version}`],
|
||||||
|
cacheDir,
|
||||||
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
fail(`cannot commit the wall entry in "${cacheDir}" — ${/** @type {Error} */ (err).message}\n cure: inspect "${cacheDir}" by hand and re-run once its git state is clean`)
|
fail(`cannot commit the wall entry in "${cacheDir}" — ${/** @type {Error} */ (err).message}\n cure: inspect "${cacheDir}" by hand and re-run once its git state is clean`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
134
src/brainy.ts
134
src/brainy.ts
|
|
@ -544,6 +544,23 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
* and narrate forever. Reset by {@link deregisterShutdownHooksIfIdle}. */
|
* and narrate forever. Reset by {@link deregisterShutdownHooksIfIdle}. */
|
||||||
private static beforeExitNarrated = false
|
private static beforeExitNarrated = false
|
||||||
|
|
||||||
|
/** True for the entire duration of ONE `closeOnShutdown()` run (the
|
||||||
|
* signal-path handler in {@link registerShutdownHooks}) — from before it
|
||||||
|
* starts closing instances until after it has decided whether to exit.
|
||||||
|
* THE RACE THIS CLOSES: closing the LAST live instance calls
|
||||||
|
* `close()` → `deregisterShutdownHooksIfIdle()` synchronously, which
|
||||||
|
* removes `Brainy.sigtermListener` from `process` — while `closeOnShutdown`
|
||||||
|
* (that very listener's OWN still-running invocation) hasn't yet reached
|
||||||
|
* `exitIfSoleShutdownOwner()`'s `process.exit(0)`. In that window Node has
|
||||||
|
* NO registered SIGTERM listener, so a second/concurrent delivery of the
|
||||||
|
* same signal (a raced re-send, common on a loaded host) falls through to
|
||||||
|
* Node's default disposition and kills the process outright — the
|
||||||
|
* clean-shutdown work already finished, but the process never reports the
|
||||||
|
* 0 it earned. `deregisterShutdownHooksIfIdle()` checks this flag and
|
||||||
|
* defers; `closeOnShutdown()`'s `finally` re-runs the deregistration check
|
||||||
|
* once it is done, so the listener never actually leaks past its use. */
|
||||||
|
private static shutdownSignalHandlerActive = false
|
||||||
|
|
||||||
/** Poll cadence (ms) for the migration LOCK when a provider exposes no
|
/** Poll cadence (ms) for the migration LOCK when a provider exposes no
|
||||||
* event-driven `whenMigrationComplete()` signal. See {@link awaitMigrationLock}. */
|
* event-driven `whenMigrationComplete()` signal. See {@link awaitMigrationLock}. */
|
||||||
private static readonly MIGRATION_POLL_INTERVAL_MS = 250
|
private static readonly MIGRATION_POLL_INTERVAL_MS = 250
|
||||||
|
|
@ -2196,51 +2213,74 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
*/
|
*/
|
||||||
const closeOnShutdown = async () => {
|
const closeOnShutdown = async () => {
|
||||||
console.log('Shutdown signal received - flushing pending data...')
|
console.log('Shutdown signal received - flushing pending data...')
|
||||||
// DEFER ONE MACROTASK. A host application registers its own listener on
|
// HOLD THE LISTENER FOR THE WHOLE RUN. Closing the LAST live instance
|
||||||
// the same signal, and Node runs listeners in registration order — ours
|
// below calls close() → deregisterShutdownHooksIfIdle(), which removes
|
||||||
// is usually first, because the brain was opened before the host wired
|
// Brainy's own SIGTERM/SIGINT listeners from `process` — synchronously,
|
||||||
// its shutdown. Yielding once lets every other listener for this signal
|
// before THIS invocation has reached exitIfSoleShutdownOwner()'s
|
||||||
// run its synchronous prologue, so a host that calls close() gets to be
|
// process.exit(0). Left alone, that opens a window with no registered
|
||||||
// the owner. It is only a courtesy, never the safety: close()'s own
|
// listener for the signal at all, so a second/concurrent delivery of
|
||||||
// single-flight gate is what makes a lost race harmless.
|
// the same signal (a raced re-send — not rare on a loaded host) falls
|
||||||
await new Promise<void>((resolve) => setImmediate(resolve))
|
// through to Node's default disposition and kills the process outright
|
||||||
|
// AFTER the clean-shutdown work already finished, reporting a signal
|
||||||
|
// kill instead of the 0 the shutdown earned. Setting this flag makes
|
||||||
|
// deregisterShutdownHooksIfIdle() defer; the `finally` below re-checks
|
||||||
|
// it once this run is fully done — closeOnShutdown, not a nested
|
||||||
|
// close(), owns exactly when the listener actually comes off.
|
||||||
|
Brainy.shutdownSignalHandlerActive = true
|
||||||
|
try {
|
||||||
|
// DEFER ONE MACROTASK. A host application registers its own listener
|
||||||
|
// on the same signal, and Node runs listeners in registration order —
|
||||||
|
// ours is usually first, because the brain was opened before the
|
||||||
|
// host wired its shutdown. Yielding once lets every other listener
|
||||||
|
// for this signal run its synchronous prologue, so a host that calls
|
||||||
|
// close() gets to be the owner. It is only a courtesy, never the
|
||||||
|
// safety: close()'s own single-flight gate is what makes a lost race
|
||||||
|
// harmless.
|
||||||
|
await new Promise<void>((resolve) => setImmediate(resolve))
|
||||||
|
|
||||||
let closedCount = 0
|
let closedCount = 0
|
||||||
let deferredCount = 0
|
let deferredCount = 0
|
||||||
let failedCount = 0
|
let failedCount = 0
|
||||||
// Snapshot: close() splices Brainy.instances while we iterate.
|
// Snapshot: close() splices Brainy.instances while we iterate.
|
||||||
for (const instance of [...Brainy.instances]) {
|
for (const instance of [...Brainy.instances]) {
|
||||||
if (!instance.initialized) continue
|
if (!instance.initialized) continue
|
||||||
// SOMEONE ELSE OWNS THIS ONE. Not a flush, not a lock release, not a
|
// SOMEONE ELSE OWNS THIS ONE. Not a flush, not a lock release, not a
|
||||||
// component close — nothing. Touching a brain whose close is running
|
// component close — nothing. Touching a brain whose close is running
|
||||||
// is the whole defect this handler was rewritten for.
|
// is the whole defect this handler was rewritten for.
|
||||||
if (instance.closed || instance._closeInFlight !== null) {
|
if (instance.closed || instance._closeInFlight !== null) {
|
||||||
deferredCount++
|
deferredCount++
|
||||||
continue
|
continue
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Law 1: this try/catch is the isolation — the loop continues.
|
||||||
|
await instance.close()
|
||||||
|
closedCount++
|
||||||
|
} catch (error) {
|
||||||
|
failedCount++
|
||||||
|
console.error('Failed to close one Brainy instance on shutdown:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try {
|
if (closedCount > 0) {
|
||||||
// Law 1: this try/catch is the isolation — the loop continues.
|
console.log(`Flushed successfully (${closedCount} instance${closedCount > 1 ? 's' : ''})`)
|
||||||
await instance.close()
|
|
||||||
closedCount++
|
|
||||||
} catch (error) {
|
|
||||||
failedCount++
|
|
||||||
console.error('Failed to close one Brainy instance on shutdown:', error)
|
|
||||||
}
|
}
|
||||||
}
|
if (deferredCount > 0) {
|
||||||
if (closedCount > 0) {
|
console.log(
|
||||||
console.log(`Flushed successfully (${closedCount} instance${closedCount > 1 ? 's' : ''})`)
|
`${deferredCount} Brainy instance${deferredCount > 1 ? 's are' : ' is'} already ` +
|
||||||
}
|
`closing — left to the caller that owns that close.`
|
||||||
if (deferredCount > 0) {
|
)
|
||||||
console.log(
|
}
|
||||||
`${deferredCount} Brainy instance${deferredCount > 1 ? 's are' : ' is'} already ` +
|
if (failedCount > 0) {
|
||||||
`closing — left to the caller that owns that close.`
|
console.error(
|
||||||
)
|
`${failedCount} Brainy instance${failedCount > 1 ? 's' : ''} did not complete shutdown — ` +
|
||||||
}
|
`their writer locks were released, but their next open will run crash recovery.`
|
||||||
if (failedCount > 0) {
|
)
|
||||||
console.error(
|
}
|
||||||
`${failedCount} Brainy instance${failedCount > 1 ? 's' : ''} did not complete shutdown — ` +
|
} finally {
|
||||||
`their writer locks were released, but their next open will run crash recovery.`
|
// Release the hold and run the deferred check ourselves — the last
|
||||||
)
|
// close() above may have found the flag set and skipped its own
|
||||||
|
// deregistration, so nobody else will do this if we don't.
|
||||||
|
Brainy.shutdownSignalHandlerActive = false
|
||||||
|
Brainy.deregisterShutdownHooksIfIdle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2404,9 +2444,17 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
* script that closed every brain exits on its own — a library must never
|
* script that closed every brain exits on its own — a library must never
|
||||||
* keep its host process alive. Re-initializing later re-registers them
|
* keep its host process alive. Re-initializing later re-registers them
|
||||||
* (the `shutdownHooksRegisteredGlobally` flag resets here).
|
* (the `shutdownHooksRegisteredGlobally` flag resets here).
|
||||||
|
*
|
||||||
|
* Deferred (not skipped — {@link closeOnShutdown}'s `finally` always
|
||||||
|
* re-checks) while a signal-path shutdown is actively running: that
|
||||||
|
* handler's OWN still-in-flight invocation is `Brainy.sigtermListener`, and
|
||||||
|
* removing it out from under itself — which closing the LAST instance here
|
||||||
|
* would otherwise do, synchronously, mid-run — would leave `process` with
|
||||||
|
* no listener for the signal for the remainder of that run. See
|
||||||
|
* {@link shutdownSignalHandlerActive}'s doc for the exact race this closes.
|
||||||
*/
|
*/
|
||||||
private static deregisterShutdownHooksIfIdle(): void {
|
private static deregisterShutdownHooksIfIdle(): void {
|
||||||
if (Brainy.instances.length > 0 || !Brainy.shutdownHooksRegisteredGlobally) {
|
if (Brainy.instances.length > 0 || !Brainy.shutdownHooksRegisteredGlobally || Brainy.shutdownSignalHandlerActive) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (Brainy.sigtermListener) process.off('SIGTERM', Brainy.sigtermListener)
|
if (Brainy.sigtermListener) process.off('SIGTERM', Brainy.sigtermListener)
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,10 @@ describe('API Parameter Validation', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
it('should use "where" parameter for metadata filtering', async () => {
|
it('should use "where" parameter for metadata filtering', async () => {
|
||||||
const results = await brain.find({
|
const results = await brain.find({
|
||||||
where: { category: 'test-category' },
|
where: { category: 'test-category' },
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
* - Backward compatibility preserved
|
* - Backward compatibility preserved
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy } from '../../src/brainy.js'
|
import { Brainy } from '../../src/brainy.js'
|
||||||
import { NounType } from '../../src/types/graphTypes.js'
|
import { NounType } from '../../src/types/graphTypes.js'
|
||||||
|
|
||||||
|
|
@ -19,6 +19,10 @@ describe('Entity Confidence & Weight Exposure', () => {
|
||||||
await brain.init()
|
await brain.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
describe('Entity interface', () => {
|
describe('Entity interface', () => {
|
||||||
it('should expose confidence when adding entity with confidence', async () => {
|
it('should expose confidence when adding entity with confidence', async () => {
|
||||||
const id = await brain.add({
|
const id = await brain.add({
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
* index-served (a body field, or a bucketed timestamp), exactly the owing rows
|
* index-served (a body field, or a bucketed timestamp), exactly the owing rows
|
||||||
* are read and the rest are still served from the index.
|
* are read and the rest are still served from the index.
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, beforeAll, vi } from 'vitest'
|
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'
|
||||||
import { Brainy } from '../../src/brainy'
|
import { Brainy } from '../../src/brainy'
|
||||||
import { NounType } from '../../src/types/graphTypes'
|
import { NounType } from '../../src/types/graphTypes'
|
||||||
import { generateTestVector } from '../helpers/test-factory'
|
import { generateTestVector } from '../helpers/test-factory'
|
||||||
|
|
@ -59,6 +59,10 @@ describe('find/get({ fields }) — projection', () => {
|
||||||
await brain.flush()
|
await brain.flush()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
/** Count canonical record reads for one call. */
|
/** Count canonical record reads for one call. */
|
||||||
const countingReads = async <R>(body: () => Promise<R>): Promise<{ out: R; reads: number }> => {
|
const countingReads = async <R>(body: () => Promise<R>): Promise<{ out: R; reads: number }> => {
|
||||||
const spy = vi.spyOn(brain as any, 'batchGet')
|
const spy = vi.spyOn(brain as any, 'batchGet')
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
* never the legs. And the text leg is asked about the universe's ids only —
|
* never the legs. And the text leg is asked about the universe's ids only —
|
||||||
* what it marshals is bounded by the universe, not by the store.
|
* what it marshals is bounded by the universe, not by the store.
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, beforeAll, vi } from 'vitest'
|
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'
|
||||||
import { Brainy } from '../../src/brainy'
|
import { Brainy } from '../../src/brainy'
|
||||||
import { NounType, VerbType } from '../../src/types/graphTypes'
|
import { NounType, VerbType } from '../../src/types/graphTypes'
|
||||||
import { rankIndicesByScore, reorderByIndices } from '../../src/utils/resultRanking'
|
import { rankIndicesByScore, reorderByIndices } from '../../src/utils/resultRanking'
|
||||||
|
|
@ -287,6 +287,10 @@ describe('hybrid find: filter before hydrate — the answer is unchanged', () =>
|
||||||
expect(typeof (brain as any).metadataIndex.getIdSetForFilter).not.toBe('function')
|
expect(typeof (brain as any).metadataIndex.getIdSetForFilter).not.toBe('function')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
it('the fixture does not truncate the text leg — the universe covers every text match', async () => {
|
it('the fixture does not truncate the text leg — the universe covers every text match', async () => {
|
||||||
const index = (brain as any).metadataIndex
|
const index = (brain as any).metadataIndex
|
||||||
const textMatches = await index.getIdsForTextQuery(QUERY)
|
const textMatches = await index.getIdsForTextQuery(QUERY)
|
||||||
|
|
@ -553,6 +557,10 @@ describe('hybrid find: the text leg ranks inside the filter, not around it', ()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
it('the old order let the filter consume the whole text leg', async () => {
|
it('the old order let the filter consume the whole text leg', async () => {
|
||||||
const index = (brain as any).metadataIndex
|
const index = (brain as any).metadataIndex
|
||||||
const universe: string[] = await (brain as any).filterIdsBelted({ lane: 'alpha' })
|
const universe: string[] = await (brain as any).filterIdsBelted({ lane: 'alpha' })
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
* it). Now the anchor is fetched with its vector, and an anchor without one
|
* it). Now the anchor is fetched with its vector, and an anchor without one
|
||||||
* refuses by name instead of failing inside the index.
|
* refuses by name instead of failing inside the index.
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, beforeAll } from 'vitest'
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
||||||
import { Brainy } from '../../src/brainy'
|
import { Brainy } from '../../src/brainy'
|
||||||
import { NounType } from '../../src/types/graphTypes'
|
import { NounType } from '../../src/types/graphTypes'
|
||||||
import { v5 } from '../../src/universal/uuid'
|
import { v5 } from '../../src/universal/uuid'
|
||||||
|
|
@ -28,6 +28,10 @@ describe('find({ near }) uses the anchor vector', () => {
|
||||||
await brain.add({ id: 'far', data: 'far row', type: NounType.Thing, vector: generateTestVector() })
|
await brain.add({ id: 'far', data: 'far row', type: NounType.Thing, vector: generateTestVector() })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
it('returns the anchor\'s neighbours by its own vector', async () => {
|
it('returns the anchor\'s neighbours by its own vector', async () => {
|
||||||
const results = await brain.find({ near: { id: 'anchor' }, limit: 3 })
|
const results = await brain.find({ near: { id: 'anchor' }, limit: 3 })
|
||||||
expect(results.length).toBeGreaterThan(0)
|
expect(results.length).toBeGreaterThan(0)
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
* the covering is ASSERTED from the leg's own output rather than assumed. This
|
* the covering is ASSERTED from the leg's own output rather than assumed. This
|
||||||
* pin is about ordering, and it says nothing about recall.
|
* pin is about ordering, and it says nothing about recall.
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, beforeAll } from 'vitest'
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
||||||
import { Brainy } from '../../src/brainy'
|
import { Brainy } from '../../src/brainy'
|
||||||
import { NounType, VerbType } from '../../src/types/graphTypes'
|
import { NounType, VerbType } from '../../src/types/graphTypes'
|
||||||
import { resolveEntityId } from '../../src/utils/idNormalization'
|
import { resolveEntityId } from '../../src/utils/idNormalization'
|
||||||
|
|
@ -107,6 +107,10 @@ describe('find(): orderBy is the order on every path', () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
it('the fixture: the hybrid candidate set covers the whole filter universe', async () => {
|
it('the fixture: the hybrid candidate set covers the whole filter universe', async () => {
|
||||||
const universe: string[] = await (brain as any).filterIdsBelted({ lane: 'alpha' })
|
const universe: string[] = await (brain as any).filterIdsBelted({ lane: 'alpha' })
|
||||||
expect(universe).toHaveLength(ROWS)
|
expect(universe).toHaveLength(ROWS)
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
* against the adjacency before it is believed, so a not-serving graph refuses
|
* against the adjacency before it is believed, so a not-serving graph refuses
|
||||||
* loudly instead of answering `[]` as truth.
|
* loudly instead of answering `[]` as truth.
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, beforeAll, vi } from 'vitest'
|
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'
|
||||||
import { Brainy } from '../../src/brainy'
|
import { Brainy } from '../../src/brainy'
|
||||||
import { NounType, VerbType } from '../../src/types/graphTypes'
|
import { NounType, VerbType } from '../../src/types/graphTypes'
|
||||||
import { generateTestVector } from '../helpers/test-factory'
|
import { generateTestVector } from '../helpers/test-factory'
|
||||||
|
|
@ -56,6 +56,10 @@ describe('find(): the optional planner door', () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
/** Install a planner door for one call, then remove it. */
|
/** Install a planner door for one call, then remove it. */
|
||||||
const withDoor = async <T>(
|
const withDoor = async <T>(
|
||||||
door: (...a: any[]) => Promise<any>,
|
door: (...a: any[]) => Promise<any>,
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ describe('Unified Find() Integration Tests', () => {
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await cleanup.cleanup()
|
await cleanup.cleanup()
|
||||||
|
await brain.close()
|
||||||
brain = null as any
|
brain = null as any
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
* All entities carry explicit 384-dim vectors so no test invokes the embedder.
|
* All entities carry explicit 384-dim vectors so no test invokes the embedder.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect, afterEach } from 'vitest'
|
||||||
import { Brainy } from '../../src/brainy.js'
|
import { Brainy } from '../../src/brainy.js'
|
||||||
import { NounType, VerbType } from '../../src/types/graphTypes.js'
|
import { NounType, VerbType } from '../../src/types/graphTypes.js'
|
||||||
import { v5, v7, isUUID } from '../../src/universal/uuid.js'
|
import { v5, v7, isUUID } from '../../src/universal/uuid.js'
|
||||||
|
|
@ -37,8 +37,15 @@ async function makeBrain(): Promise<Brainy> {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('id normalization — transparent string-key round-trips', () => {
|
describe('id normalization — transparent string-key round-trips', () => {
|
||||||
|
const opened: Brainy[] = []
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
for (const b of opened.splice(0)) await b.close().catch(() => {})
|
||||||
|
})
|
||||||
|
|
||||||
it('1. add() returns v5(key); get(key) and get(returnedId) both resolve; _originalId preserved', async () => {
|
it('1. add() returns v5(key); get(key) and get(returnedId) both resolve; _originalId preserved', async () => {
|
||||||
const brain = await makeBrain()
|
const brain = await makeBrain()
|
||||||
|
opened.push(brain)
|
||||||
|
|
||||||
const returnedId = await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person })
|
const returnedId = await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person })
|
||||||
|
|
||||||
|
|
@ -60,6 +67,7 @@ describe('id normalization — transparent string-key round-trips', () => {
|
||||||
|
|
||||||
it('2. relate() by string keys; related(key) and related({from:key}) return the edge to v5(toKey)', async () => {
|
it('2. relate() by string keys; related(key) and related({from:key}) return the edge to v5(toKey)', async () => {
|
||||||
const brain = await makeBrain()
|
const brain = await makeBrain()
|
||||||
|
opened.push(brain)
|
||||||
|
|
||||||
await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person })
|
await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person })
|
||||||
await brain.add({ id: 'doc-1', vector: vec(2), type: NounType.Document })
|
await brain.add({ id: 'doc-1', vector: vec(2), type: NounType.Document })
|
||||||
|
|
@ -85,6 +93,7 @@ describe('id normalization — transparent string-key round-trips', () => {
|
||||||
|
|
||||||
it('3. update() by string key reflects on get(key)', async () => {
|
it('3. update() by string key reflects on get(key)', async () => {
|
||||||
const brain = await makeBrain()
|
const brain = await makeBrain()
|
||||||
|
opened.push(brain)
|
||||||
|
|
||||||
await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person, metadata: { role: 'admin' } })
|
await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person, metadata: { role: 'admin' } })
|
||||||
await brain.update({ id: 'user-1', metadata: { role: 'owner' } })
|
await brain.update({ id: 'user-1', metadata: { role: 'owner' } })
|
||||||
|
|
@ -98,6 +107,7 @@ describe('id normalization — transparent string-key round-trips', () => {
|
||||||
|
|
||||||
it('4. remove() by string key deletes; get(key) is null', async () => {
|
it('4. remove() by string key deletes; get(key) is null', async () => {
|
||||||
const brain = await makeBrain()
|
const brain = await makeBrain()
|
||||||
|
opened.push(brain)
|
||||||
|
|
||||||
await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person })
|
await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person })
|
||||||
expect(await brain.get('user-1')).not.toBeNull()
|
expect(await brain.get('user-1')).not.toBeNull()
|
||||||
|
|
@ -110,6 +120,7 @@ describe('id normalization — transparent string-key round-trips', () => {
|
||||||
|
|
||||||
it('5. find({ connected: { from: key } }) resolves the anchor key', async () => {
|
it('5. find({ connected: { from: key } }) resolves the anchor key', async () => {
|
||||||
const brain = await makeBrain()
|
const brain = await makeBrain()
|
||||||
|
opened.push(brain)
|
||||||
|
|
||||||
await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person })
|
await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person })
|
||||||
await brain.add({ id: 'doc-1', vector: vec(2), type: NounType.Document })
|
await brain.add({ id: 'doc-1', vector: vec(2), type: NounType.Document })
|
||||||
|
|
@ -122,6 +133,7 @@ describe('id normalization — transparent string-key round-trips', () => {
|
||||||
|
|
||||||
it('6. transact() add+relate by string keys round-trips with consistent canonical ids', async () => {
|
it('6. transact() add+relate by string keys round-trips with consistent canonical ids', async () => {
|
||||||
const brain = await makeBrain()
|
const brain = await makeBrain()
|
||||||
|
opened.push(brain)
|
||||||
|
|
||||||
// Seed user-1 so the relate op has a target to point at.
|
// Seed user-1 so the relate op has a target to point at.
|
||||||
await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person })
|
await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person })
|
||||||
|
|
@ -149,6 +161,7 @@ describe('id normalization — transparent string-key round-trips', () => {
|
||||||
|
|
||||||
it('7. addMany() + relateMany() with string ids round-trip', async () => {
|
it('7. addMany() + relateMany() with string ids round-trip', async () => {
|
||||||
const brain = await makeBrain()
|
const brain = await makeBrain()
|
||||||
|
opened.push(brain)
|
||||||
|
|
||||||
const added = await brain.addMany({
|
const added = await brain.addMany({
|
||||||
items: [
|
items: [
|
||||||
|
|
@ -175,6 +188,7 @@ describe('id normalization — transparent string-key round-trips', () => {
|
||||||
|
|
||||||
it('8. determinism: same key maps to same UUID — two adds upsert ONE entity, not two', async () => {
|
it('8. determinism: same key maps to same UUID — two adds upsert ONE entity, not two', async () => {
|
||||||
const brain = await makeBrain()
|
const brain = await makeBrain()
|
||||||
|
opened.push(brain)
|
||||||
|
|
||||||
const id1 = await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person, metadata: { n: 1 } })
|
const id1 = await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person, metadata: { n: 1 } })
|
||||||
const id2 = await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person, metadata: { n: 2 } })
|
const id2 = await brain.add({ id: 'user-1', vector: vec(1), type: NounType.Person, metadata: { n: 2 } })
|
||||||
|
|
@ -193,6 +207,7 @@ describe('id normalization — transparent string-key round-trips', () => {
|
||||||
|
|
||||||
it('9. valid-UUID passthrough: a real UUID is kept verbatim with NO _originalId', async () => {
|
it('9. valid-UUID passthrough: a real UUID is kept verbatim with NO _originalId', async () => {
|
||||||
const brain = await makeBrain()
|
const brain = await makeBrain()
|
||||||
|
opened.push(brain)
|
||||||
|
|
||||||
const realUuid = v7()
|
const realUuid = v7()
|
||||||
const returnedId = await brain.add({ id: realUuid, vector: vec(5), type: NounType.Thing })
|
const returnedId = await brain.add({ id: realUuid, vector: vec(5), type: NounType.Thing })
|
||||||
|
|
@ -207,6 +222,7 @@ describe('id normalization — transparent string-key round-trips', () => {
|
||||||
|
|
||||||
it('10. no-id add() mints a v7; newId() mints a v7', async () => {
|
it('10. no-id add() mints a v7; newId() mints a v7', async () => {
|
||||||
const brain = await makeBrain()
|
const brain = await makeBrain()
|
||||||
|
opened.push(brain)
|
||||||
|
|
||||||
const autoId = await brain.add({ vector: vec(6), type: NounType.Thing })
|
const autoId = await brain.add({ vector: vec(6), type: NounType.Thing })
|
||||||
expect(isUUID(autoId)).toBe(true)
|
expect(isUUID(autoId)).toBe(true)
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,11 @@ describe('Multi-process safety + read-only mode', () => {
|
||||||
|
|
||||||
const blocked = new Brainy({ requireSubtype: false, storage: { type: 'filesystem', path: dir } })
|
const blocked = new Brainy({ requireSubtype: false, storage: { type: 'filesystem', path: dir } })
|
||||||
await expect(blocked.init()).rejects.toThrow(/another writer holds/i)
|
await expect(blocked.init()).rejects.toThrow(/another writer holds/i)
|
||||||
// Don't track `blocked` for afterEach cleanup since init failed.
|
// A rejected init() still registered `blocked` in Brainy's global
|
||||||
|
// instance registry (the constructor does that unconditionally) — close()
|
||||||
|
// is safe to call even though init() never completed, and is what
|
||||||
|
// deregisters it (and, once idle, the process-level shutdown hooks).
|
||||||
|
await blocked.close().catch(() => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('takes over a STALE foreign lock (dead PID + old heartbeat) and claims atomically', async () => {
|
it('takes over a STALE foreign lock (dead PID + old heartbeat) and claims atomically', async () => {
|
||||||
|
|
@ -151,6 +155,7 @@ describe('Multi-process safety + read-only mode', () => {
|
||||||
const err: any = await blocked.init().catch((e) => e)
|
const err: any = await blocked.init().catch((e) => e)
|
||||||
expect(err.code).toBe('BRAINY_WRITER_LOCKED')
|
expect(err.code).toBe('BRAINY_WRITER_LOCKED')
|
||||||
expect(err.lockInfo?.pid).toBe(otherPid)
|
expect(err.lockInfo?.pid).toBe(otherPid)
|
||||||
|
await blocked.close().catch(() => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('release drains an in-flight heartbeat — no phantom lock re-created after unlink', async () => {
|
it('release drains an in-flight heartbeat — no phantom lock re-created after unlink', async () => {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ describe('related() with a verb-type array returns every requested type', () =>
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
await brain.close()
|
||||||
brain = null as any
|
brain = null as any
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,8 @@ describe('Relationship Intelligence', () => {
|
||||||
await brain.init()
|
await brain.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(async () => {
|
||||||
|
await brain.close()
|
||||||
if (fs.existsSync(testDir)) {
|
if (fs.existsSync(testDir)) {
|
||||||
fs.rmSync(testDir, { recursive: true })
|
fs.rmSync(testDir, { recursive: true })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
* - addMany({ ifAbsent: true }) applies the flag to every item
|
* - addMany({ ifAbsent: true }) applies the flag to every item
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy } from '../../src/brainy.js'
|
import { Brainy } from '../../src/brainy.js'
|
||||||
import { RevisionConflictError } from '../../src/transaction/RevisionConflictError.js'
|
import { RevisionConflictError } from '../../src/transaction/RevisionConflictError.js'
|
||||||
import { NounType } from '../../src/types/graphTypes.js'
|
import { NounType } from '../../src/types/graphTypes.js'
|
||||||
|
|
@ -22,6 +22,10 @@ describe('7.31.0 — _rev CAS + ifAbsent', () => {
|
||||||
await brain.init()
|
await brain.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
describe('_rev initialization + surface', () => {
|
describe('_rev initialization + surface', () => {
|
||||||
it('initializes _rev to 1 on add()', async () => {
|
it('initializes _rev to 1 on add()', async () => {
|
||||||
const id = await brain.add({ data: 'hello', type: NounType.Document })
|
const id = await brain.add({ data: 'hello', type: NounType.Document })
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ describe('repairContainment: batched pass 2', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
await brain.close()
|
||||||
brain = null as any
|
brain = null as any
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,10 @@ import * as XLSX from 'xlsx'
|
||||||
describe('VFS Debug', () => {
|
describe('VFS Debug', () => {
|
||||||
it('minimal VFS writeFile test', async () => {
|
it('minimal VFS writeFile test', async () => {
|
||||||
const brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
|
const brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
|
||||||
await brain.init()
|
try {
|
||||||
|
await brain.init()
|
||||||
|
|
||||||
console.log('✅ Brain initialized')
|
console.log('✅ Brain initialized')
|
||||||
|
|
||||||
// Get VFS and initialize
|
// Get VFS and initialize
|
||||||
const vfs = brain.vfs
|
const vfs = brain.vfs
|
||||||
|
|
@ -77,5 +78,8 @@ describe('VFS Debug', () => {
|
||||||
// THE REAL TEST: Can we query VFS?
|
// THE REAL TEST: Can we query VFS?
|
||||||
expect(children.length).toBeGreaterThan(0)
|
expect(children.length).toBeGreaterThan(0)
|
||||||
expect(rootContents.length).toBeGreaterThan(0)
|
expect(rootContents.length).toBeGreaterThan(0)
|
||||||
|
} finally {
|
||||||
|
await brain.close()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ describe('writer-lock fencing', () => {
|
||||||
// Old rule: heartbeat-age eviction → silent takeover → split brain.
|
// Old rule: heartbeat-age eviction → silent takeover → split brain.
|
||||||
// New rule: live PID = live writer; the second opener throws typed.
|
// New rule: live PID = live writer; the second opener throws typed.
|
||||||
const second = new Brainy({ storage: { type: 'filesystem', path: dir }, requireSubtype: false })
|
const second = new Brainy({ storage: { type: 'filesystem', path: dir }, requireSubtype: false })
|
||||||
|
brains.push(second)
|
||||||
await expect(second.init()).rejects.toMatchObject({ code: 'BRAINY_WRITER_LOCKED' })
|
await expect(second.init()).rejects.toMatchObject({ code: 'BRAINY_WRITER_LOCKED' })
|
||||||
}, 120000)
|
}, 120000)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
* - Note limitations and edge cases
|
* - Note limitations and edge cases
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy } from '../../src/brainy.js'
|
import { Brainy } from '../../src/brainy.js'
|
||||||
import { TypeAwareStorageAdapter } from '../../src/storage/adapters/typeAwareStorageAdapter.js'
|
import { TypeAwareStorageAdapter } from '../../src/storage/adapters/typeAwareStorageAdapter.js'
|
||||||
import { FileSystemStorage } from '../../src/storage/adapters/fileSystemStorage.js'
|
import { FileSystemStorage } from '../../src/storage/adapters/fileSystemStorage.js'
|
||||||
|
|
@ -67,6 +67,10 @@ describe('TypeAware Performance Benchmarks', () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await brainMemory.close()
|
||||||
|
})
|
||||||
|
|
||||||
it('should measure type-based query performance', async () => {
|
it('should measure type-based query performance', async () => {
|
||||||
// MEASURED: Query for one type (200 entities)
|
// MEASURED: Query for one type (200 entities)
|
||||||
const start = performance.now()
|
const start = performance.now()
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* No mocks, no fakes, real implementation
|
* No mocks, no fakes, real implementation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy } from '../../src/brainy.js'
|
import { Brainy } from '../../src/brainy.js'
|
||||||
import { NounType } from '../../src/types/graphTypes.js'
|
import { NounType } from '../../src/types/graphTypes.js'
|
||||||
|
|
||||||
|
|
@ -21,6 +21,10 @@ describe('Brainy 3.0 Core (Unit Tests)', () => {
|
||||||
await brain.init()
|
await brain.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
describe('CRUD Operations', () => {
|
describe('CRUD Operations', () => {
|
||||||
it('should create items with add', async () => {
|
it('should create items with add', async () => {
|
||||||
const id = await brain.add({
|
const id = await brain.add({
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,19 @@ import { prodLog } from '../../../src/utils/logger.js'
|
||||||
const UUID = (suffix: string): string => `00000000-0000-4000-8000-0000000000${suffix}`
|
const UUID = (suffix: string): string => `00000000-0000-4000-8000-0000000000${suffix}`
|
||||||
|
|
||||||
describe('Finding 10 — degraded derived-index state is surfaced on reads', () => {
|
describe('Finding 10 — degraded derived-index state is surfaced on reads', () => {
|
||||||
|
const opened: Brainy[] = []
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
|
process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
|
||||||
})
|
})
|
||||||
afterEach(() => vi.restoreAllMocks())
|
afterEach(async () => {
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
for (const b of opened.splice(0)) await b.close().catch(() => {})
|
||||||
|
})
|
||||||
|
|
||||||
it('checkHealth() reports adopt-forward degraded ids as unhealthy', async () => {
|
it('checkHealth() reports adopt-forward degraded ids as unhealthy', async () => {
|
||||||
const brain = new Brainy({ storage: { type: 'memory' }, dimensions: 384, requireSubtype: false })
|
const brain = new Brainy({ storage: { type: 'memory' }, dimensions: 384, requireSubtype: false })
|
||||||
|
opened.push(brain)
|
||||||
await brain.init()
|
await brain.init()
|
||||||
;(brain as any)._indexDegradedIds.add(UUID('de'))
|
;(brain as any)._indexDegradedIds.add(UUID('de'))
|
||||||
|
|
||||||
|
|
@ -37,6 +43,7 @@ describe('Finding 10 — degraded derived-index state is surfaced on reads', ()
|
||||||
it('find()/get() warn loudly while degraded, ONCE, then repairIndex() clears it', async () => {
|
it('find()/get() warn loudly while degraded, ONCE, then repairIndex() clears it', async () => {
|
||||||
const warn = vi.spyOn(prodLog, 'warn').mockImplementation(() => {})
|
const warn = vi.spyOn(prodLog, 'warn').mockImplementation(() => {})
|
||||||
const brain = new Brainy({ storage: { type: 'memory' }, dimensions: 384, requireSubtype: false })
|
const brain = new Brainy({ storage: { type: 'memory' }, dimensions: 384, requireSubtype: false })
|
||||||
|
opened.push(brain)
|
||||||
await brain.init()
|
await brain.init()
|
||||||
await brain.add({ id: UUID('a1'), data: 'x', type: NounType.Document })
|
await brain.add({ id: UUID('a1'), data: 'x', type: NounType.Document })
|
||||||
;(brain as any)._indexRebuildFailed = new Error('rebuild boom')
|
;(brain as any)._indexRebuildFailed = new Error('rebuild boom')
|
||||||
|
|
@ -59,6 +66,7 @@ describe('Finding 10 — degraded derived-index state is surfaced on reads', ()
|
||||||
|
|
||||||
it('persistSingleOp records receipt.degraded (widened return type, not dropped)', async () => {
|
it('persistSingleOp records receipt.degraded (widened return type, not dropped)', async () => {
|
||||||
const brain = new Brainy({ storage: { type: 'memory' }, dimensions: 384, requireSubtype: false })
|
const brain = new Brainy({ storage: { type: 'memory' }, dimensions: 384, requireSubtype: false })
|
||||||
|
opened.push(brain)
|
||||||
await brain.init()
|
await brain.init()
|
||||||
// Simulate a degraded receipt by wrapping the generation store's commitSingleOp.
|
// Simulate a degraded receipt by wrapping the generation store's commitSingleOp.
|
||||||
const gs: any = (brain as any).generationStore
|
const gs: any = (brain as any).generationStore
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
* soft-delete semantic: `field !== value` MUST include entities that have no
|
* soft-delete semantic: `field !== value` MUST include entities that have no
|
||||||
* such field at all.
|
* such field at all.
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy } from '../../../src/brainy'
|
import { Brainy } from '../../../src/brainy'
|
||||||
import { NounType } from '../../../src/types/graphTypes'
|
import { NounType } from '../../../src/types/graphTypes'
|
||||||
|
|
||||||
|
|
@ -26,6 +26,10 @@ describe('find() complement operators (ne / exists:false / missing:true)', () =>
|
||||||
ids.noField2 = await brain.add({ data: 'n2', type: NounType.Thing, metadata: { other: 2 } })
|
ids.noField2 = await brain.add({ data: 'n2', type: NounType.Thing, metadata: { other: 2 } })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
it('ne returns everything except the matching value — INCLUDING entities without the field', async () => {
|
it('ne returns everything except the matching value — INCLUDING entities without the field', async () => {
|
||||||
const rows = await brain.find({ where: { status: { ne: 'active' } }, limit: 100 })
|
const rows = await brain.find({ where: { status: { ne: 'active' } }, limit: 100 })
|
||||||
const got = new Set(rows.map((r) => r.id))
|
const got = new Set(rows.map((r) => r.id))
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
* returns an id whose record matches NEITHER the type nor the where filter) and
|
* returns an id whose record matches NEITHER the type nor the where filter) and
|
||||||
* assert the phantom is dropped while the genuine matches survive.
|
* assert the phantom is dropped while the genuine matches survive.
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy } from '../../../src/brainy'
|
import { Brainy } from '../../../src/brainy'
|
||||||
import { NounType } from '../../../src/types/graphTypes'
|
import { NounType } from '../../../src/types/graphTypes'
|
||||||
|
|
||||||
|
|
@ -48,6 +48,10 @@ describe('find() index-integrity guard (phantom row class)', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
it('healthy index: the discriminant query returns only the staff Person', async () => {
|
it('healthy index: the discriminant query returns only the staff Person', async () => {
|
||||||
const rows = await brain.find({ type: NounType.Person, where: { entityType: 'staff' }, limit: 100 })
|
const rows = await brain.find({ type: NounType.Person, where: { entityType: 'staff' }, limit: 100 })
|
||||||
expect(rows.map((r) => r.id)).toEqual([staffId])
|
expect(rows.map((r) => r.id)).toEqual([staffId])
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy } from '../../../src/brainy'
|
import { Brainy } from '../../../src/brainy'
|
||||||
import { createAddParams } from '../../helpers/test-factory'
|
import { createAddParams } from '../../helpers/test-factory'
|
||||||
import { NounType } from '../../../src/types/graphTypes'
|
import { NounType } from '../../../src/types/graphTypes'
|
||||||
|
|
@ -13,6 +13,10 @@ describe('Brainy.find()', () => {
|
||||||
await brain.init()
|
await brain.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
describe('success paths', () => {
|
describe('success paths', () => {
|
||||||
it('should find entities by text query', async () => {
|
it('should find entities by text query', async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
* exercised by cor's combined matrix); they inject probe/spy hooks onto the live JS
|
* exercised by cor's combined matrix); they inject probe/spy hooks onto the live JS
|
||||||
* metadata index, which has neither method by default.
|
* metadata index, which has neither method by default.
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy } from '../../../src/brainy'
|
import { Brainy } from '../../../src/brainy'
|
||||||
import { NounType } from '../../../src/types/graphTypes'
|
import { NounType } from '../../../src/types/graphTypes'
|
||||||
|
|
||||||
|
|
@ -34,6 +34,10 @@ describe('metadata-provider contract wiring (getIdsForFilter opts)', () => {
|
||||||
mi = (brain as any).metadataIndex
|
mi = (brain as any).metadataIndex
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
it('RETIRED: a read never calls probeConsistency() / self-heals via detectAndRepairCorruption — that is the read-triggered dark rebuild the health-gate law forbids', async () => {
|
it('RETIRED: a read never calls probeConsistency() / self-heals via detectAndRepairCorruption — that is the read-triggered dark rebuild the health-gate law forbids', async () => {
|
||||||
let probes = 0
|
let probes = 0
|
||||||
let repairs = 0
|
let repairs = 0
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
* gate that hung getStats / readdir / readFile behind an unrelated family's
|
* gate that hung getStats / readdir / readFile behind an unrelated family's
|
||||||
* migration until the wait timed out.
|
* migration until the wait timed out.
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy } from '../../../src/brainy.js'
|
import { Brainy } from '../../../src/brainy.js'
|
||||||
import { MigrationInProgressError } from '../../../src/errors/brainyError.js'
|
import { MigrationInProgressError } from '../../../src/errors/brainyError.js'
|
||||||
|
|
||||||
|
|
@ -38,12 +38,19 @@ const jam = (provider: unknown) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('migration LOCK is family-scoped', () => {
|
describe('migration LOCK is family-scoped', () => {
|
||||||
|
const opened: Brainy[] = []
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
|
process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
for (const b of opened.splice(0)) await b.close().catch(() => {})
|
||||||
|
})
|
||||||
|
|
||||||
it('a stuck VECTOR migration does not block canonical or graph/metadata reads', async () => {
|
it('a stuck VECTOR migration does not block canonical or graph/metadata reads', async () => {
|
||||||
const brain = await seed()
|
const brain = await seed()
|
||||||
|
opened.push(brain)
|
||||||
const childId = (
|
const childId = (
|
||||||
(await brain.vfs.readdir('/notes', { withFileTypes: true })) as Array<{ entityId: string }>
|
(await brain.vfs.readdir('/notes', { withFileTypes: true })) as Array<{ entityId: string }>
|
||||||
)[0].entityId
|
)[0].entityId
|
||||||
|
|
@ -60,6 +67,7 @@ describe('migration LOCK is family-scoped', () => {
|
||||||
|
|
||||||
it('a stuck VECTOR migration STILL blocks a read that needs the vector family', async () => {
|
it('a stuck VECTOR migration STILL blocks a read that needs the vector family', async () => {
|
||||||
const brain = await seed()
|
const brain = await seed()
|
||||||
|
opened.push(brain)
|
||||||
jam((brain as any).index)
|
jam((brain as any).index)
|
||||||
|
|
||||||
// A semantic query consults the vector index — it must wait, and (bounded by
|
// A semantic query consults the vector index — it must wait, and (bounded by
|
||||||
|
|
@ -70,6 +78,7 @@ describe('migration LOCK is family-scoped', () => {
|
||||||
|
|
||||||
it('a stuck GRAPH migration blocks traversal but not vector/canonical reads', async () => {
|
it('a stuck GRAPH migration blocks traversal but not vector/canonical reads', async () => {
|
||||||
const brain = await seed()
|
const brain = await seed()
|
||||||
|
opened.push(brain)
|
||||||
const childId = (
|
const childId = (
|
||||||
(await brain.vfs.readdir('/notes', { withFileTypes: true })) as Array<{ entityId: string }>
|
(await brain.vfs.readdir('/notes', { withFileTypes: true })) as Array<{ entityId: string }>
|
||||||
)[0].entityId
|
)[0].entityId
|
||||||
|
|
@ -87,6 +96,7 @@ describe('migration LOCK is family-scoped', () => {
|
||||||
|
|
||||||
it('with no migration in flight, every read serves (the fast path is a no-op)', async () => {
|
it('with no migration in flight, every read serves (the fast path is a no-op)', async () => {
|
||||||
const brain = await seed()
|
const brain = await seed()
|
||||||
|
opened.push(brain)
|
||||||
await expect(brain.getStats()).resolves.toBeDefined()
|
await expect(brain.getStats()).resolves.toBeDefined()
|
||||||
await expect(brain.find({ query: 'doc' })).resolves.toBeDefined()
|
await expect(brain.find({ query: 'doc' })).resolves.toBeDefined()
|
||||||
await expect(brain.vfs.readdir('/notes')).resolves.toHaveLength(1)
|
await expect(brain.vfs.readdir('/notes')).resolves.toHaveLength(1)
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ describe('Duplicate Check Optimization', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
// Cleanup is automatic with memory storage
|
await brain.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should detect duplicate relationships using GraphAdjacencyIndex', async () => {
|
it('should detect duplicate relationships using GraphAdjacencyIndex', async () => {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
* _indexRebuildFailed / _indexDegradedIds degraded states (mirroring
|
* _indexRebuildFailed / _indexDegradedIds degraded states (mirroring
|
||||||
* validateIndexConsistency / checkHealth).
|
* validateIndexConsistency / checkHealth).
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy, NounType } from '../../src/index.js'
|
import { Brainy, NounType } from '../../src/index.js'
|
||||||
|
|
||||||
describe('getIndexStatus honest readiness (Finding 9)', () => {
|
describe('getIndexStatus honest readiness (Finding 9)', () => {
|
||||||
|
|
@ -20,6 +20,10 @@ describe('getIndexStatus honest readiness (Finding 9)', () => {
|
||||||
await brain.flush()
|
await brain.flush()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
it('a not-ready provider makes populated honest (false) and exposes ready:false', async () => {
|
it('a not-ready provider makes populated honest (false) and exposes ready:false', async () => {
|
||||||
brain.index.isReady = () => false // count present, serving structure NOT loaded
|
brain.index.isReady = () => false // count present, serving structure NOT loaded
|
||||||
const status = await brain.getIndexStatus()
|
const status = await brain.getIndexStatus()
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
* scan; and a one-shot probe self-heals a no-isReady provider whose adjacency
|
* scan; and a one-shot probe self-heals a no-isReady provider whose adjacency
|
||||||
* did not cold-load.
|
* did not cold-load.
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy, NounType, VerbType } from '../../../src/index.js'
|
import { Brainy, NounType, VerbType } from '../../../src/index.js'
|
||||||
|
|
||||||
describe('graph fast-path honest readiness (Finding 2)', () => {
|
describe('graph fast-path honest readiness (Finding 2)', () => {
|
||||||
|
|
@ -33,6 +33,10 @@ describe('graph fast-path honest readiness (Finding 2)', () => {
|
||||||
await storage.getVerbsBySource(a)
|
await storage.getVerbsBySource(a)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
it('not-ready provider → shard scan returns the REAL edges, not a silent []', async () => {
|
it('not-ready provider → shard scan returns the REAL edges, not a silent []', async () => {
|
||||||
const gi = storage.graphIndex
|
const gi = storage.graphIndex
|
||||||
// Simulate a cold native provider: count/manifest loaded (isInitialized) but
|
// Simulate a cold native provider: count/manifest loaded (isInitialized) but
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
* The 8.0 JS index cold-loads correctly, so we simulate the cold native failure
|
* The 8.0 JS index cold-loads correctly, so we simulate the cold native failure
|
||||||
* mode by intercepting the provider's getIdsForFilter/rebuild.
|
* mode by intercepting the provider's getIdsForFilter/rebuild.
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy, NounType, MetadataIndexNotReadyError } from '../../src/index.js'
|
import { Brainy, NounType, MetadataIndexNotReadyError } from '../../src/index.js'
|
||||||
|
|
||||||
const V = () => Array.from({ length: 384 }, (_, i) => Math.sin(i * 0.1) + 0.001)
|
const V = () => Array.from({ length: 384 }, (_, i) => Math.sin(i * 0.1) + 0.001)
|
||||||
|
|
@ -31,6 +31,10 @@ describe('Metadata cold-read guard (#venue silent-[])', () => {
|
||||||
await brain.flush()
|
await brain.flush()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
it('warm brain: filtered find is correct and the guard does not rebuild', async () => {
|
it('warm brain: filtered find is correct and the guard does not rebuild', async () => {
|
||||||
const mi = brain.metadataIndex
|
const mi = brain.metadataIndex
|
||||||
let rebuilds = 0
|
let rebuilds = 0
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
* the production feature-detection reads it.
|
* the production feature-detection reads it.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy, NounType, MigrationInProgressError } from '../../src/index.js'
|
import { Brainy, NounType, MigrationInProgressError } from '../../src/index.js'
|
||||||
import { GraphAdjacencyIndex } from '../../src/graph/graphAdjacencyIndex.js'
|
import { GraphAdjacencyIndex } from '../../src/graph/graphAdjacencyIndex.js'
|
||||||
|
|
||||||
|
|
@ -39,6 +39,12 @@ describe('Migration LOCK (#18) — coordinated 7.x→8.0 auto-upgrade', () => {
|
||||||
await brain.init()
|
await brain.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
// The "close() is not gated" test already closes `brain` itself as its
|
||||||
|
// own assertion — closing an already-closed brain is a safe no-op here.
|
||||||
|
await brain.close().catch(() => {})
|
||||||
|
})
|
||||||
|
|
||||||
it('does not gate operations when no provider is migrating (fast path)', async () => {
|
it('does not gate operations when no provider is migrating (fast path)', async () => {
|
||||||
const id = await brain.add({ data: 'hello', type: NounType.Concept })
|
const id = await brain.add({ data: 'hello', type: NounType.Concept })
|
||||||
expect(id).toBeTruthy()
|
expect(id).toBeTruthy()
|
||||||
|
|
@ -130,6 +136,9 @@ describe('Migration LOCK (#18) — coordinated 7.x→8.0 auto-upgrade', () => {
|
||||||
expect(e).toBeInstanceOf(MigrationInProgressError)
|
expect(e).toBeInstanceOf(MigrationInProgressError)
|
||||||
expect(e.retryable).toBe(true)
|
expect(e.retryable).toBe(true)
|
||||||
expect(typeof e.elapsedMs).toBe('number')
|
expect(typeof e.elapsedMs).toBe('number')
|
||||||
|
} finally {
|
||||||
|
// close() is proven not-gated by the test below — safe even mid-migration.
|
||||||
|
await shortBrain.close()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,11 @@ describe('EmbeddingSignal', () => {
|
||||||
signal = new EmbeddingSignal(brain)
|
signal = new EmbeddingSignal(brain)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(async () => {
|
||||||
signal.clearCache()
|
signal.clearCache()
|
||||||
signal.clearHistory()
|
signal.clearHistory()
|
||||||
signal.resetStats()
|
signal.resetStats()
|
||||||
|
await brain.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('initialization', () => {
|
describe('initialization', () => {
|
||||||
|
|
|
||||||
|
|
@ -89,12 +89,14 @@ describe('Guarded plugin auto-detection (plugins: undefined)', () => {
|
||||||
})
|
})
|
||||||
const brain: any = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true })
|
const brain: any = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true })
|
||||||
await expect(brain.init()).rejects.toThrow(/installed but failed to load/)
|
await expect(brain.init()).rejects.toThrow(/installed but failed to load/)
|
||||||
|
await brain.close().catch(() => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('installed but not a valid plugin (missing activate) → init() throws', async () => {
|
it('installed but not a valid plugin (missing activate) → init() throws', async () => {
|
||||||
stubImport(async () => ({ default: { name: '@soulcraft/cor' } })) // no activate()
|
stubImport(async () => ({ default: { name: '@soulcraft/cor' } })) // no activate()
|
||||||
const brain: any = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true })
|
const brain: any = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true })
|
||||||
await expect(brain.init()).rejects.toThrow(/not a valid Brainy plugin/)
|
await expect(brain.init()).rejects.toThrow(/not a valid Brainy plugin/)
|
||||||
|
await brain.close().catch(() => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('installed but activation fails → init() throws (activateAll posture applies)', async () => {
|
it('installed but activation fails → init() throws (activateAll posture applies)', async () => {
|
||||||
|
|
@ -108,6 +110,7 @@ describe('Guarded plugin auto-detection (plugins: undefined)', () => {
|
||||||
}))
|
}))
|
||||||
const brain: any = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true })
|
const brain: any = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true })
|
||||||
await expect(brain.init()).rejects.toThrow(/failed to activate/)
|
await expect(brain.init()).rejects.toThrow(/failed to activate/)
|
||||||
|
await brain.close().catch(() => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('plugins: [] and plugins: false → no probe at all (explicit opt-out)', async () => {
|
it('plugins: [] and plugins: false → no probe at all (explicit opt-out)', async () => {
|
||||||
|
|
@ -132,5 +135,6 @@ describe('Guarded plugin auto-detection (plugins: undefined)', () => {
|
||||||
silent: true
|
silent: true
|
||||||
})
|
})
|
||||||
await expect(brain.init()).rejects.toThrow(/listed in config\.plugins but could not be loaded/)
|
await expect(brain.init()).rejects.toThrow(/listed in config\.plugins but could not be loaded/)
|
||||||
|
await brain.close().catch(() => {})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -143,5 +143,6 @@ describe('version coupling at init() — no silent fallback', () => {
|
||||||
plugins: ['@soulcraft/this-package-does-not-exist-xyz']
|
plugins: ['@soulcraft/this-package-does-not-exist-xyz']
|
||||||
})
|
})
|
||||||
await expect(brain.init()).rejects.toThrow(/could not be loaded|config\.plugins/)
|
await expect(brain.init()).rejects.toThrow(/could not be loaded|config\.plugins/)
|
||||||
|
await brain.close().catch(() => {})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -298,9 +298,10 @@ describe('Brainy plugin integration', () => {
|
||||||
// must surface as a failed init(), NOT a silent degrade to the default
|
// must surface as a failed init(), NOT a silent degrade to the default
|
||||||
// engine (the version-coupling guard; see plugin-version-coupling.test.ts).
|
// engine (the version-coupling guard; see plugin-version-coupling.test.ts).
|
||||||
await expect(brain.init()).rejects.toThrow(/failed to activate|native module not found/)
|
await expect(brain.init()).rejects.toThrow(/failed to activate|native module not found/)
|
||||||
|
await brain.close().catch(() => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should use() return this for chaining', () => {
|
it('should use() return this for chaining', async () => {
|
||||||
const plugin: BrainyPlugin = {
|
const plugin: BrainyPlugin = {
|
||||||
name: 'chain-test',
|
name: 'chain-test',
|
||||||
activate: async () => true
|
activate: async () => true
|
||||||
|
|
@ -309,5 +310,8 @@ describe('Brainy plugin integration', () => {
|
||||||
const brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
|
const brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
|
||||||
const result = brain.use(plugin)
|
const result = brain.use(plugin)
|
||||||
expect(result).toBe(brain)
|
expect(result).toBe(brain)
|
||||||
|
// Never init()'d — the constructor still registered it in Brainy's global
|
||||||
|
// instance registry, so it still needs a close() to deregister.
|
||||||
|
await brain.close().catch(() => {})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,14 @@ let cacheDir: string
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
dir = mkdtempSync(join(tmpdir(), 'wall-entry-test-'))
|
dir = mkdtempSync(join(tmpdir(), 'wall-entry-test-'))
|
||||||
|
// wall-entry.mjs is run with this dir as its cwd, standing in for the real
|
||||||
|
// developer checkout it reads its commit identity from (process.cwd()) —
|
||||||
|
// give it a repo-local identity the same way seedRemote gives one to the
|
||||||
|
// seed clone, so the suite is deterministic on a host with no global git
|
||||||
|
// config (a bare CI box) as much as one with a developer's own.
|
||||||
|
execFileSync('git', ['init', '-q', dir])
|
||||||
|
git(['config', 'user.name', 'Wall Entry Test'], dir)
|
||||||
|
git(['config', 'user.email', 'wall-entry-test@example.com'], dir)
|
||||||
remoteDir = initBareRemote()
|
remoteDir = initBareRemote()
|
||||||
cacheDir = join(mkdtempSync(join(tmpdir(), 'wall-cache-')), 'soulcraft-releases')
|
cacheDir = join(mkdtempSync(join(tmpdir(), 'wall-cache-')), 'soulcraft-releases')
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
* hydration (zero per-entity reads when unfiltered). Both must preserve the exact
|
* hydration (zero per-entity reads when unfiltered). Both must preserve the exact
|
||||||
* pagination contract: same order, cursor continuation, filters, totalCount.
|
* pagination contract: same order, cursor continuation, filters, totalCount.
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||||
import { Brainy, NounType } from '../../../src/index.js'
|
import { Brainy, NounType } from '../../../src/index.js'
|
||||||
|
|
||||||
describe('paginated enumeration — parallel hydration + id-only (cortex heal-cost)', () => {
|
describe('paginated enumeration — parallel hydration + id-only (cortex heal-cost)', () => {
|
||||||
|
|
@ -30,6 +30,10 @@ describe('paginated enumeration — parallel hydration + id-only (cortex heal-co
|
||||||
storage = brain.storage
|
storage = brain.storage
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
/** Page the whole dataset through a small limit via cursor and collect ordered ids. */
|
/** Page the whole dataset through a small limit via cursor and collect ordered ids. */
|
||||||
const pageAll = async (fn: (opts: any) => Promise<any>, key: 'items' | 'ids') => {
|
const pageAll = async (fn: (opts: any) => Promise<any>, key: 'items' | 'ids') => {
|
||||||
const out: string[] = []
|
const out: string[] = []
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
* Tests to verify that brain.find({ type: NounType.X }) correctly filters entities
|
* Tests to verify that brain.find({ type: NounType.X }) correctly filters entities
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy, NounType } from '../../src/index.js'
|
import { Brainy, NounType } from '../../src/index.js'
|
||||||
|
|
||||||
describe('Type Filtering (A Consumer Team Issue)', () => {
|
describe('Type Filtering (A Consumer Team Issue)', () => {
|
||||||
|
|
@ -17,6 +17,10 @@ describe('Type Filtering (A Consumer Team Issue)', () => {
|
||||||
await brain.init()
|
await brain.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
it('should filter entities by NounType.Person', async () => {
|
it('should filter entities by NounType.Person', async () => {
|
||||||
// Add 3 people
|
// Add 3 people
|
||||||
await brain.add({ data: 'John Smith', type: NounType.Person, metadata: { name: 'John' } })
|
await brain.add({ data: 'John Smith', type: NounType.Person, metadata: { name: 'John' } })
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,10 @@ describe('the indexable-array bound', () => {
|
||||||
await brain.init()
|
await brain.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
describe('BELOW the bound: the array indexes, every element of it', () => {
|
describe('BELOW the bound: the array indexes, every element of it', () => {
|
||||||
it('the eleven-element array that used to vanish is searchable', async () => {
|
it('the eleven-element array that used to vanish is searchable', async () => {
|
||||||
// ELEVEN — one over the old silent limit, the whole shape of the defect.
|
// ELEVEN — one over the old silent limit, the whole shape of the defect.
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
* column store adopts the field. It is named in `getIdsFromChunksForRange`'s
|
* column store adopts the field. It is named in `getIdsFromChunksForRange`'s
|
||||||
* doc comment rather than papered over.
|
* doc comment rather than papered over.
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy } from '../../../src/brainy'
|
import { Brainy } from '../../../src/brainy'
|
||||||
import { NounType } from '../../../src/types/graphTypes'
|
import { NounType } from '../../../src/types/graphTypes'
|
||||||
import { SparseIndex, ChunkManager } from '../../../src/utils/metadataIndexChunking'
|
import { SparseIndex, ChunkManager } from '../../../src/utils/metadataIndexChunking'
|
||||||
|
|
@ -122,6 +122,10 @@ describe('legacy sparse index: range queries order values, or refuse', () => {
|
||||||
expect(index.columnStore.hasField(FIELD)).toBe(false)
|
expect(index.columnStore.hasField(FIELD)).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
describe('(a) a long BOUND against ordinary short values', () => {
|
describe('(a) a long BOUND against ordinary short values', () => {
|
||||||
// 'apple' < 'mango' < 'zebra', and every bound below is compared against
|
// 'apple' < 'mango' < 'zebra', and every bound below is compared against
|
||||||
// these three raw keys.
|
// these three raw keys.
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
* validateInvariants(), and repairIndex() maps a failing invariant with heal:'rebuild'
|
* validateInvariants(), and repairIndex() maps a failing invariant with heal:'rebuild'
|
||||||
* to that provider's rebuild(). "healthy-while-broken must be impossible."
|
* to that provider's rebuild(). "healthy-while-broken must be impossible."
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy, NounType } from '../../src/index.js'
|
import { Brainy, NounType } from '../../src/index.js'
|
||||||
import type { ProviderInvariantReport } from '../../src/index.js'
|
import type { ProviderInvariantReport } from '../../src/index.js'
|
||||||
|
|
||||||
|
|
@ -48,6 +48,10 @@ describe('validateIndexConsistency delegates to provider validateInvariants() (P
|
||||||
await brain.flush()
|
await brain.flush()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
it('a broken provider report makes the store unhealthy and names the failing invariant', async () => {
|
it('a broken provider report makes the store unhealthy and names the failing invariant', async () => {
|
||||||
brain.index.validateInvariants = async () => brokenReport('vector')
|
brain.index.validateInvariants = async () => brokenReport('vector')
|
||||||
const v = await brain.validateIndexConsistency()
|
const v = await brain.validateIndexConsistency()
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
* signal (from either strategy) THROWS VectorIndexNotReadyError immediately,
|
* signal (from either strategy) THROWS VectorIndexNotReadyError immediately,
|
||||||
* with no rebuild attempt in between — never a silent empty result.
|
* with no rebuild attempt in between — never a silent empty result.
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy, NounType, VectorIndexNotReadyError } from '../../src/index.js'
|
import { Brainy, NounType, VectorIndexNotReadyError } from '../../src/index.js'
|
||||||
|
|
||||||
const V = (): number[] => Array.from({ length: 384 }, (_, i) => Math.sin(i * 0.1) + 0.001)
|
const V = (): number[] => Array.from({ length: 384 }, (_, i) => Math.sin(i * 0.1) + 0.001)
|
||||||
|
|
@ -28,6 +28,10 @@ describe('Vector cold-read guard (verifyVectorLive) — silent-[] on cold semant
|
||||||
await brain.flush()
|
await brain.flush()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
it('warm brain: semantic find is correct and the guard does not rebuild', async () => {
|
it('warm brain: semantic find is correct and the guard does not rebuild', async () => {
|
||||||
const vi = brain.index
|
const vi = brain.index
|
||||||
let rebuilds = 0
|
let rebuilds = 0
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
* Tests to verify VFS import behavior and identify if VFS creates only wrappers or also graph entities
|
* Tests to verify VFS import behavior and identify if VFS creates only wrappers or also graph entities
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy, NounType } from '../../src/index.js'
|
import { Brainy, NounType } from '../../src/index.js'
|
||||||
|
|
||||||
describe('VFS Multi-instance Diagnostic', () => {
|
describe('VFS Multi-instance Diagnostic', () => {
|
||||||
|
|
@ -17,6 +17,10 @@ describe('VFS Multi-instance Diagnostic', () => {
|
||||||
await brain.init()
|
await brain.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
it('should verify VFS creates document wrappers AND allows entity filtering', async () => {
|
it('should verify VFS creates document wrappers AND allows entity filtering', async () => {
|
||||||
console.log('\n🔬 VFS Multi-instance Diagnostic Test\n')
|
console.log('\n🔬 VFS Multi-instance Diagnostic Test\n')
|
||||||
console.log('='.repeat(70))
|
console.log('='.repeat(70))
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
* Ensures tree methods prevent recursion and work correctly
|
* Ensures tree methods prevent recursion and work correctly
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy } from '../../src/brainy.js'
|
import { Brainy } from '../../src/brainy.js'
|
||||||
import { VirtualFileSystem } from '../../src/vfs/VirtualFileSystem.js'
|
import { VirtualFileSystem } from '../../src/vfs/VirtualFileSystem.js'
|
||||||
import { VFSTreeUtils } from '../../src/vfs/TreeUtils.js'
|
import { VFSTreeUtils } from '../../src/vfs/TreeUtils.js'
|
||||||
|
|
@ -24,6 +24,10 @@ describe('VFS Tree Operations', () => {
|
||||||
await vfs.init()
|
await vfs.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
describe('Critical: No Self-Inclusion Bug', () => {
|
describe('Critical: No Self-Inclusion Bug', () => {
|
||||||
it('should NEVER return a directory as its own child', async () => {
|
it('should NEVER return a directory as its own child', async () => {
|
||||||
// Create test structure
|
// Create test structure
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
* - Issue #2: File read decompression error
|
* - Issue #2: File read decompression error
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy } from '../../src/brainy.js'
|
import { Brainy } from '../../src/brainy.js'
|
||||||
import { VirtualFileSystem } from '../../src/vfs/VirtualFileSystem.js'
|
import { VirtualFileSystem } from '../../src/vfs/VirtualFileSystem.js'
|
||||||
|
|
||||||
|
|
@ -25,6 +25,10 @@ describe('VFS Bug Fixes', () => {
|
||||||
await vfs.init()
|
await vfs.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
describe('Issue #1: Duplicate Directory Nodes', () => {
|
describe('Issue #1: Duplicate Directory Nodes', () => {
|
||||||
it('should not create duplicate directory entries when writing multiple files to same directory', async () => {
|
it('should not create duplicate directory entries when writing multiple files to same directory', async () => {
|
||||||
// Write multiple files to the same directory (reproduce the bug scenario)
|
// Write multiple files to the same directory (reproduce the bug scenario)
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
* other operations in parallel batches.
|
* other operations in parallel batches.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
import { Brainy } from '../../src/brainy.js'
|
import { Brainy } from '../../src/brainy.js'
|
||||||
import { VirtualFileSystem } from '../../src/vfs/VirtualFileSystem.js'
|
import { VirtualFileSystem } from '../../src/vfs/VirtualFileSystem.js'
|
||||||
|
|
||||||
|
|
@ -30,6 +30,10 @@ describe('VFS bulkWrite Race Condition Fix', () => {
|
||||||
await vfs.init()
|
await vfs.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await brain.close()
|
||||||
|
})
|
||||||
|
|
||||||
describe('operation ordering', () => {
|
describe('operation ordering', () => {
|
||||||
it('should create directories before files when mixed in same batch', async () => {
|
it('should create directories before files when mixed in same batch', async () => {
|
||||||
// This is the exact scenario that triggered the race condition:
|
// This is the exact scenario that triggered the race condition:
|
||||||
|
|
|
||||||
Reference in a new issue