brainy/tests/integration/readdir-no-duplicate-replace.test.ts
David Snelling 366f9a91f5 fix: full-removal canonical deletes + family-scoped migration gate
Two production-reported spine fixes:

- Canonical delete is a FULL removal: remove()/removeMany() deleted the
  metadata leg but never the canonical vectors.json leg or the <id>/
  directory, leaving ghost rows that inflated enumerated counts forever,
  read as damage scars, and caused duplicate readdir entries for
  re-created VFS paths (the stale-unpost path). The delete operation now
  routes through storage.deleteNoun — both legs + the entity container —
  with a full two-leg before-image rollback; deleteVerb symmetric; the
  blind 'no metadata file' catch that masked real faults is gone. The
  generation log still holds the delete's before-image, so asOf() history
  is unchanged. repairIndex() gains a conservative, loud orphan-prune
  sweep (containers with no metadata content leg) + count recompute for
  stores damaged by earlier versions.

- Family-scoped migration gate: every read blocked on the whole-brain
  migration lock even when the migrating index was irrelevant. The gate
  now scopes to the index families a read actually consults — canonical
  reads never wait; find() waits only on its query shape's families;
  graph traversals wait only on the graph family. Writes keep the
  conservative whole-brain wait. A read that needs the migrating family
  still blocks (bounded) with the retryable MigrationInProgressError.
2026-07-14 10:11:53 -07:00

56 lines
2.4 KiB
TypeScript

/**
* @module tests/integration/readdir-no-duplicate-replace
* @description A re-created path must NOT show up twice in readdir. The reported
* defect (memory-vm) was readdir serving DUPLICATE entries for re-created paths,
* rooted in a delete that left a ghost (partial removal) whose next delete read
* null metadata and skipped unposting the stale Contains edge. With full-removal
* deletes there is no ghost, so the edge is always unposted and the path appears
* exactly once after any number of delete→recreate cycles.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { Brainy } from '../../src/index.js'
describe('readdir shows a re-created path exactly once (no duplicate on replace)', () => {
let dir: string
let brain: any
beforeEach(async () => {
process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'brainy-readdir-'))
brain = new Brainy({ requireSubtype: false, storage: { type: 'filesystem', path: dir }, silent: true, dimensions: 384 })
await brain.init()
})
afterEach(async () => {
await brain.close?.().catch(() => {})
fs.rmSync(dir, { recursive: true, force: true })
})
it('delete then re-create the same file path — readdir lists it once', async () => {
await brain.vfs.mkdir('/d', { recursive: true })
await brain.vfs.writeFile('/d/x.txt', 'v1')
expect(await brain.vfs.readdir('/d')).toEqual(['x.txt'])
await brain.vfs.unlink('/d/x.txt')
expect(await brain.vfs.readdir('/d')).toEqual([])
await brain.vfs.writeFile('/d/x.txt', 'v2')
const listing = (await brain.vfs.readdir('/d')) as string[]
expect(listing).toEqual(['x.txt']) // exactly once — no ghost duplicate
expect(listing.filter((n) => n === 'x.txt')).toHaveLength(1)
})
it('survives several delete→recreate cycles without accumulating duplicates', async () => {
await brain.vfs.mkdir('/c', { recursive: true })
for (let i = 0; i < 4; i++) {
await brain.vfs.writeFile('/c/f.txt', `gen ${i}`)
await brain.vfs.unlink('/c/f.txt')
}
await brain.vfs.writeFile('/c/f.txt', 'final')
const listing = (await brain.vfs.readdir('/c')) as string[]
expect(listing).toEqual(['f.txt'])
expect(await brain.vfs.readFile('/c/f.txt')).toBeDefined()
})
})