fix(add): empty string is real data, not a missing field

validateAddParams() treated '' as falsy and rejected it with "Missing
required field 'data'" — so a legitimate empty file's first write always
failed. Only null/undefined data (with no vector either) is genuinely
absent; '' is real content. Fixed the check, plus the identical bug in
validateUpdateParams() (truncating a file to empty via overwrite hit the
same falsy check) and in update()/transact()'s update planner, where a
plain `Boolean(params.data)`/truthy check on the resolved vector would have
silently skipped both the deferred-embed marker and the eager re-embed for
an emptied value — a stale vector with no path to ever correct itself.

Verified end-to-end: vfs.writeFile('/empty.txt', '') now succeeds,
readFile() returns '', the file lists, and stat() reports size 0; the
existing "should reject empty string as data" tests (unit + integration)
asserted the old buggy behavior and are updated to assert the fixed
contract instead.
This commit is contained in:
David Snelling 2026-08-25 10:10:19 -07:00
parent fc516da6eb
commit 258e9042af
6 changed files with 128 additions and 20 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

@ -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

@ -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'