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

@ -540,6 +540,11 @@ function rejectForgedSystemKeys(metadata: Record<string, unknown> | undefined, s
export function validateAddParams(params: AddParams): void {
rejectForgedSystemKeys(params.metadata as Record<string, unknown> | undefined, 'add()')
// 'data' is ABSENT only when null/undefined — an empty string ('') is real
// content (a legitimate empty file's first write) and must not be treated
// as missing. Falsy-but-present values (0, false, '') all count as present;
// only the true "nothing was given" case is absent.
const hasData = params.data !== undefined && params.data !== null
// MT5 deferred embedding: an explicit vector has nothing to defer, and a
// deferral without data has nothing to embed — both are caller bugs that
// must refuse with the fix, never be silently reinterpreted.
@ -550,14 +555,14 @@ export function validateAddParams(params: AddParams): void {
`the vector is already computed; drop one of the two.`
)
}
if (!params.data) {
if (!hasData) {
throw new Error(
`add(): deferEmbedding requires 'data' (the content the background worker will embed).`
)
}
}
// Universal truth: must have data or vector
if (!params.data && !params.vector) {
if (!hasData && !params.vector) {
throw new Error(
`Invalid add() parameters: Missing required field 'data'\n` +
`\nReceived: ${JSON.stringify({
@ -597,6 +602,10 @@ export function validateAddParams(params: AddParams): void {
*/
export function validateUpdateParams(params: UpdateParams): void {
rejectForgedSystemKeys(params.metadata as Record<string, unknown> | undefined, 'update()')
// Same absent-vs-empty distinction as validateAddParams: '' is a real new
// value (e.g. truncating a file to empty content via overwrite), only
// null/undefined means "no new data was given".
const hasData = params.data !== undefined && params.data !== null
if ((params as UpdateParams & { deferEmbedding?: boolean }).deferEmbedding === true) {
if (params.vector) {
throw new Error(
@ -604,7 +613,7 @@ export function validateUpdateParams(params: UpdateParams): void {
`the vector is already computed; drop one of the two.`
)
}
if (!params.data) {
if (!hasData) {
throw new Error(
`update(): deferEmbedding requires new 'data' — without a data change there is nothing to re-embed.`
)
@ -614,10 +623,10 @@ export function validateUpdateParams(params: UpdateParams): void {
if (!params.id) {
throw new Error('id is required for update')
}
// Universal truth: must update something
if (
!params.data &&
!hasData &&
!params.metadata &&
!params.type &&
!params.vector &&