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.
56 lines
2.4 KiB
TypeScript
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()
|
|
})
|
|
})
|