Merge branch 'worktree-agent-ad3aff0dffd17a6eb'
This commit is contained in:
commit
f14da34b27
12 changed files with 726 additions and 80 deletions
|
|
@ -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 () => {
|
||||
|
|
|
|||
199
tests/unit/brainy/open-path.test.ts
Normal file
199
tests/unit/brainy/open-path.test.ts
Normal 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()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
99
tests/unit/vfs-readdir-recursive.test.ts
Normal file
99
tests/unit/vfs-readdir-recursive.test.ts
Normal 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'])
|
||||
})
|
||||
})
|
||||
Reference in a new issue