Merge branch 'worktree-agent-ad3aff0dffd17a6eb'
Some checks failed
CI / Node 22 (push) Successful in 12m19s
CI / Node 24 (push) Has been cancelled
CI / Integration + conformance (Node 22) (push) Has been cancelled
CI / Bun (latest) (push) Has been cancelled

This commit is contained in:
David Snelling 2026-08-25 11:47:25 -07:00
commit f14da34b27
12 changed files with 726 additions and 80 deletions

View file

@ -337,12 +337,18 @@ describe('Brainy 3.0 Core (Integration Tests - Real AI)', () => {
describe('Error Handling and Edge Cases', () => {
it('should handle invalid inputs gracefully', async () => {
// Empty data is rejected with a clear validation error (8.0 requires a
// non-empty `data` or a `vector` — empty string carries no signal to embed).
// Empty string is REAL content (e.g. an empty file's first write), not
// a missing field — only null/undefined data (with no vector either)
// is rejected. See src/utils/paramValidation.ts validateAddParams().
await expect(brain.add({
data: '',
type: 'document'
})).rejects.toThrow(/data/)
})).resolves.toBeDefined()
// Missing BOTH data and vector is still the real "nothing to embed" error.
await expect(brain.add({
type: 'document'
} as any)).rejects.toThrow(/data/)
// Test with very long text — valid input, resolves to an id.
const longText = 'Lorem ipsum '.repeat(10000)

View file

@ -335,15 +335,24 @@ describe('Brainy.add()', () => {
})
describe('edge cases', () => {
it('should reject empty string as data', async () => {
// Arrange
it('should accept an empty string as real (empty) data', async () => {
// Arrange — '' is legitimate content (e.g. an empty file's first
// write), not a missing field. Only null/undefined data (with no
// vector either) is "missing" — see the separate
// 'data and vector are both missing' test above.
const params = createAddParams({
data: '',
type: 'thing'
})
// Act & Assert - Empty string is not valid data
await expect(brain.add(params)).rejects.toThrow('Invalid add() parameters: Missing required field \'data\'')
// Act
const id = await brain.add(params)
// Assert — stored and readable back as empty, not rejected
expect(id).toBeDefined()
const entity = await brain.get(id)
expect(entity).not.toBeNull()
expect(entity!.data).toBe('')
})
it('should handle very long text content', async () => {

View file

@ -0,0 +1,199 @@
/**
* OPEN-PATH tests: init() must never gate on the embedding model, the VFS
* root bootstrap must never touch the embedding engine, and a slow open
* must narrate its phases.
*
* Background: a production restart storm measured 90,017ms for a single
* brain init vs 1,117ms quiet an ~80x contention multiplier traced to
* every writer's init() eagerly awaiting the process-global WASM embedding
* engine before the VFS root even existed. See src/brainy.ts performInit()
* and src/vfs/VirtualFileSystem.ts doInitializeRoot().
*/
import { describe, it, expect, vi } from 'vitest'
import { Brainy } from '../../../src/brainy'
import { MemoryStorage } from '../../../src/storage/adapters/memoryStorage'
import { embeddingManager } from '../../../src/embeddings/EmbeddingManager'
import { createTestConfig } from '../../helpers/test-factory'
/**
* The four signals `isDeterministicEmbedMode()` checks (see
* src/embeddings/deterministicEmbedMode.ts). The global unit-test setup
* (tests/setup-unit.ts) sets some of these for the whole file/run in some
* vitest configurations; other configurations leave them unset and run the
* real WASM engine instead. The background-warm tests below need the
* "not unit-test mode" branch of performInit() to actually execute, so they
* save/clear/restore all four explicitly deterministic regardless of
* which config invoked this file, never relying on ambient state.
*/
function withRealEmbedderBranch<T>(fn: () => Promise<T>): Promise<T> {
const savedEnvDeterministic = process.env.BRAINY_DETERMINISTIC_EMBEDDINGS
const savedEnvUnitTest = process.env.BRAINY_UNIT_TEST
const g = globalThis as Record<string, unknown>
const savedGlobalDeterministic = g.__BRAINY_DETERMINISTIC_EMBED__
const savedGlobalUnitTest = g.__BRAINY_UNIT_TEST__
delete process.env.BRAINY_DETERMINISTIC_EMBEDDINGS
delete process.env.BRAINY_UNIT_TEST
delete g.__BRAINY_DETERMINISTIC_EMBED__
delete g.__BRAINY_UNIT_TEST__
const restore = () => {
if (savedEnvDeterministic !== undefined) process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = savedEnvDeterministic
if (savedEnvUnitTest !== undefined) process.env.BRAINY_UNIT_TEST = savedEnvUnitTest
if (savedGlobalDeterministic !== undefined) g.__BRAINY_DETERMINISTIC_EMBED__ = savedGlobalDeterministic
if (savedGlobalUnitTest !== undefined) g.__BRAINY_UNIT_TEST__ = savedGlobalUnitTest
}
return fn().finally(restore)
}
/**
* A MemoryStorage whose init() takes an artificially long time a
* controllable fake seam (not a wall-clock race) that reliably pushes
* performInit()'s "storage-init" phase (and therefore the total open time)
* past the 2000ms narration threshold, without touching the filesystem or
* relying on real contention.
*/
class SlowMemoryStorage extends MemoryStorage {
override async init(): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, 2200))
await super.init()
}
}
describe('OPEN-PATH: init() never gates on the embedding model', () => {
it('bootstrapping a fresh store never calls the embedding engine (VFS root add is engine-untouched)', async () => {
const embedSpy = vi.spyOn(embeddingManager, 'embed')
const brain = new Brainy(createTestConfig())
try {
await brain.init()
// The VFS root's add() must never have reached the embedding engine —
// it carries an explicit placeholder vector instead (see
// VirtualFileSystem.doInitializeRoot()).
expect(embedSpy).not.toHaveBeenCalled()
// Sanity: the VFS is genuinely usable afterwards.
const files = await brain.vfs.readdir('/')
expect(files).toEqual([])
} finally {
await brain.close()
embedSpy.mockRestore()
}
})
it('starts the embedding-engine warm in the BACKGROUND — init() resolves before the warm does', async () => {
await withRealEmbedderBranch(async () => {
const events: string[] = []
let releaseWarm!: () => void
const warmGate = new Promise<void>((resolve) => {
releaseWarm = resolve
})
const initSpy = vi.spyOn(embeddingManager, 'init').mockImplementation(async () => {
events.push('warm-start')
await warmGate
events.push('warm-resolve')
})
const brain = new Brainy(createTestConfig())
try {
await brain.init()
events.push('init-resolved')
// init() started the warm but returned WITHOUT waiting for it.
expect(initSpy).toHaveBeenCalledTimes(1)
expect(events).toEqual(['warm-start', 'init-resolved'])
// Now let the fake warm finish and confirm it lands strictly after.
releaseWarm()
await new Promise((resolve) => setTimeout(resolve, 0))
expect(events).toEqual(['warm-start', 'init-resolved', 'warm-resolve'])
} finally {
await brain.close()
initSpy.mockRestore()
}
})
})
it('narrates a background warm FAILURE loudly instead of losing it silently', async () => {
await withRealEmbedderBranch(async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const initSpy = vi
.spyOn(embeddingManager, 'init')
.mockRejectedValue(new Error('simulated cold-compile failure'))
const brain = new Brainy(createTestConfig())
try {
// init() itself must still resolve — a failed background warm is
// never fatal to open().
await expect(brain.init()).resolves.toBeUndefined()
// Give the background .catch() a microtask/macrotask to run.
await new Promise((resolve) => setTimeout(resolve, 0))
const failureLine = warnSpy.mock.calls
.map((args) => args.map(String).join(' '))
.find((line) => line.includes('background embedding-engine warm FAILED'))
expect(failureLine).toBeDefined()
expect(failureLine).toContain('simulated cold-compile failure')
} finally {
await brain.close()
initSpy.mockRestore()
warnSpy.mockRestore()
}
})
})
it('eagerEmbeddings: false starts no warm at all', async () => {
await withRealEmbedderBranch(async () => {
const initSpy = vi.spyOn(embeddingManager, 'init')
const brain = new Brainy({ ...createTestConfig(), eagerEmbeddings: false })
try {
await brain.init()
expect(initSpy).not.toHaveBeenCalled()
} finally {
await brain.close()
initSpy.mockRestore()
}
})
})
it('narrates a slow open with a per-phase ms breakdown once total time exceeds 2000ms', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const brain = new Brainy({ ...createTestConfig(), storage: new SlowMemoryStorage() })
try {
await brain.init()
const slowOpenLine = warnSpy.mock.calls
.map((args) => args.map(String).join(' '))
.find((line) => line.includes('[Brainy] slow open:'))
expect(slowOpenLine).toBeDefined()
expect(slowOpenLine).toContain('storage-init=')
expect(slowOpenLine).toContain('generation-store-open-fold=')
expect(slowOpenLine).toContain('index-init-gate=')
expect(slowOpenLine).toContain('vfs-bootstrap=')
expect(slowOpenLine).toContain('embedding-warm-started=')
} finally {
await brain.close()
warnSpy.mockRestore()
}
}, 20000)
it('stays silent about phase timing when open is fast (under 2000ms)', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const brain = new Brainy(createTestConfig())
try {
await brain.init()
const slowOpenLine = warnSpy.mock.calls
.map((args) => args.map(String).join(' '))
.find((line) => line.includes('[Brainy] slow open:'))
expect(slowOpenLine).toBeUndefined()
} finally {
await brain.close()
warnSpy.mockRestore()
}
})
})

View file

@ -149,7 +149,33 @@ describe('Zero-Config Parameter Validation', () => {
type: NounType.Document
} as AddParams)).toThrow('Invalid add() parameters: Missing required field \'data\'')
})
it('should accept an empty string as real data — only null/undefined is "missing"', () => {
// A legitimate empty file's first write: '' is content, not absence.
expect(() => validateAddParams({
data: '',
type: NounType.Document
})).not.toThrow()
// null/undefined (with no vector) is still the genuine missing-field case.
expect(() => validateAddParams({
data: null as any,
type: NounType.Document
})).toThrow('Invalid add() parameters: Missing required field \'data\'')
expect(() => validateAddParams({
data: undefined,
type: NounType.Document
})).toThrow('Invalid add() parameters: Missing required field \'data\'')
})
it('deferEmbedding accepts empty-string data (real content, not absence)', () => {
expect(() => validateAddParams({
data: '',
type: NounType.Document,
deferEmbedding: true
} as AddParams)).not.toThrow()
})
it('should validate NounType', () => {
expect(() => validateAddParams({
data: 'test',
@ -190,7 +216,22 @@ describe('Zero-Config Parameter Validation', () => {
id: 'test-id'
})).toThrow('must specify at least one field to update')
})
it('empty-string data counts as a real field to update (truncating content)', () => {
expect(() => validateUpdateParams({
id: 'test-id',
data: ''
})).not.toThrow()
})
it('deferEmbedding accepts empty-string data on update', () => {
expect(() => validateUpdateParams({
id: 'test-id',
data: '',
deferEmbedding: true
} as UpdateParams)).not.toThrow()
})
it('should validate NounType if changing', () => {
expect(() => validateUpdateParams({
id: 'test-id',

View file

@ -0,0 +1,99 @@
/**
* vfs.readdir()'s `recursive` option: typed since 7.30 but never read, so it
* silently behaved exactly like `recursive: false`. This pins the real,
* documented contract: a recursive listing returns every descendant (files
* AND directories, all depths) as paths RELATIVE TO THE QUERIED DIRECTORY
* the same convention Node's `fs.readdir(dir, { recursive: true })` uses
* for both the plain string-array form and the `withFileTypes` VFSDirent
* form (whose `name` carries that same relative path when recursive).
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { Brainy } from '../../src/brainy.js'
import type { VFSDirent } from '../../src/vfs/types.js'
describe('vfs.readdir() recursive option', () => {
let brain: Brainy
beforeEach(async () => {
brain = new Brainy({
requireSubtype: false,
storage: { type: 'memory' },
silent: true
})
await brain.init()
// Build:
// /a/b.txt
// /a/sub/c.txt
// /a/sub/deeper/d.txt
// /a/sub2/ (empty directory)
await brain.vfs.writeFile('/a/b.txt', 'B')
await brain.vfs.writeFile('/a/sub/c.txt', 'C')
await brain.vfs.writeFile('/a/sub/deeper/d.txt', 'D')
await brain.vfs.mkdir('/a/sub2', { recursive: true })
})
afterEach(async () => {
await brain.close()
})
it('non-recursive (default) still returns only direct children, by basename', async () => {
const entries = await brain.vfs.readdir('/a') as string[]
expect([...entries].sort()).toEqual(['b.txt', 'sub', 'sub2'])
})
it('recursive: true returns every descendant as a path relative to the queried directory', async () => {
const entries = await brain.vfs.readdir('/a', { recursive: true }) as string[]
expect([...entries].sort()).toEqual([
'b.txt',
'sub',
'sub/c.txt',
'sub/deeper',
'sub/deeper/d.txt',
'sub2'
])
})
it('recursive: true at the root has no leading slash on relative entries', async () => {
const entries = await brain.vfs.readdir('/', { recursive: true }) as string[]
expect(entries).toContain('a')
expect(entries).toContain('a/b.txt')
expect(entries).toContain('a/sub/deeper/d.txt')
for (const entry of entries) {
expect(entry.startsWith('/')).toBe(false)
}
})
it('recursive + withFileTypes: VFSDirent.name is the relative path, .path stays absolute', async () => {
const entries = await brain.vfs.readdir('/a', {
recursive: true,
withFileTypes: true
}) as VFSDirent[]
const byName = new Map(entries.map((e) => [e.name, e]))
const nested = byName.get('sub/deeper/d.txt')
expect(nested).toBeDefined()
expect(nested!.path).toBe('/a/sub/deeper/d.txt')
expect(nested!.type).toBe('file')
const nestedDir = byName.get('sub/deeper')
expect(nestedDir).toBeDefined()
expect(nestedDir!.path).toBe('/a/sub/deeper')
expect(nestedDir!.type).toBe('directory')
// Non-recursive VFSDirent behavior is unchanged: name is the basename.
const direct = await brain.vfs.readdir('/a', { withFileTypes: true }) as VFSDirent[]
const directEntry = direct.find((e) => e.path === '/a/b.txt')
expect(directEntry?.name).toBe('b.txt')
})
it('recursive + filter composes: only files survive a type filter', async () => {
const entries = await brain.vfs.readdir('/a', {
recursive: true,
filter: { type: 'file' }
}) as string[]
expect([...entries].sort()).toEqual(['b.txt', 'sub/c.txt', 'sub/deeper/d.txt'])
})
})

View file

@ -53,6 +53,35 @@ describe('VirtualFileSystem - Production Tests', () => {
expect(exists).toBe(true)
})
it('should write and read an empty (0-byte) file end-to-end', async () => {
// Pin: validateAddParams() used to treat '' as a missing 'data' field
// (falsy check), so a legitimate empty file's FIRST write threw
// "Missing required field 'data'". '' is real content, not an absent
// field — only null/undefined is absent.
const path = '/empty.txt'
await vfs.writeFile(path, '')
const result = await vfs.readFile(path)
expect(result.toString()).toBe('')
const exists = await vfs.exists(path)
expect(exists).toBe(true)
const stats = await vfs.stat(path)
expect(stats.size).toBe(0)
expect(stats.isFile()).toBe(true)
// The file lists like any other.
const entries = await vfs.readdir('/') as string[]
expect(entries).toContain('empty.txt')
// Overwriting it back to empty (truncate) must also succeed.
await vfs.writeFile(path, 'not empty anymore')
await vfs.writeFile(path, '')
expect((await vfs.readFile(path)).toString()).toBe('')
})
it('should handle binary files', async () => {
const binaryData = Buffer.from([0x00, 0x01, 0x02, 0xFF])
const path = '/binary.dat'