158 lines
6.7 KiB
TypeScript
158 lines
6.7 KiB
TypeScript
|
|
/**
|
||
|
|
* @module tests/integration/vfs-rename-containment
|
||
|
|
* @description A cross-directory rename must MOVE the containment edge, not
|
||
|
|
* accumulate one per parent. The pre-fix rename added the new parent's
|
||
|
|
* Contains edge but skipped removing the old one ("not critical") — leaving
|
||
|
|
* the entity a child of BOTH directories: readdir(oldDir) kept listing it,
|
||
|
|
* re-creating the old path showed the name twice (the duplicate-readdir /
|
||
|
|
* "cosmetic ghost" field report), and tree-walking consumers saw the file in
|
||
|
|
* two places. Also covers the repair sweep (repairIndex → vfs.repairContainment)
|
||
|
|
* that heals ghosts left by earlier versions, and move-to-root (whose edge
|
||
|
|
* used to be skipped entirely).
|
||
|
|
*/
|
||
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||
|
|
import { Brainy, VerbType } from '../../src/index.js'
|
||
|
|
|
||
|
|
describe('VFS rename moves the containment edge (no ghost in the old directory)', () => {
|
||
|
|
let brain: any
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
|
||
|
|
brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true, dimensions: 384 })
|
||
|
|
await brain.init()
|
||
|
|
await brain.vfs.mkdir('/a', { recursive: true })
|
||
|
|
await brain.vfs.mkdir('/b', { recursive: true })
|
||
|
|
})
|
||
|
|
afterEach(async () => {
|
||
|
|
await brain.close?.().catch(() => {})
|
||
|
|
})
|
||
|
|
|
||
|
|
it('cross-directory move: old dir stops listing it, new dir lists it exactly once', async () => {
|
||
|
|
await brain.vfs.writeFile('/a/x.txt', 'v1')
|
||
|
|
await brain.vfs.rename('/a/x.txt', '/b/x.txt')
|
||
|
|
|
||
|
|
expect(await brain.vfs.readdir('/a')).toEqual([])
|
||
|
|
expect(await brain.vfs.readdir('/b')).toEqual(['x.txt'])
|
||
|
|
expect(String(await brain.vfs.readFile('/b/x.txt'))).toBe('v1')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('re-creating the old path lists each name exactly once in each directory', async () => {
|
||
|
|
await brain.vfs.writeFile('/a/x.txt', 'moved away')
|
||
|
|
await brain.vfs.rename('/a/x.txt', '/b/x.txt')
|
||
|
|
await brain.vfs.writeFile('/a/x.txt', 'new file at old path')
|
||
|
|
|
||
|
|
const a = (await brain.vfs.readdir('/a')) as string[]
|
||
|
|
const b = (await brain.vfs.readdir('/b')) as string[]
|
||
|
|
expect(a).toEqual(['x.txt']) // exactly once — the pre-fix ghost made this list the moved entity too
|
||
|
|
expect(b).toEqual(['x.txt'])
|
||
|
|
expect(String(await brain.vfs.readFile('/a/x.txt'))).toBe('new file at old path')
|
||
|
|
expect(String(await brain.vfs.readFile('/b/x.txt'))).toBe('moved away')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('repeated moves never accumulate containment edges', async () => {
|
||
|
|
await brain.vfs.writeFile('/a/f.txt', 'wanderer')
|
||
|
|
for (let i = 0; i < 3; i++) {
|
||
|
|
await brain.vfs.rename('/a/f.txt', '/b/f.txt')
|
||
|
|
await brain.vfs.rename('/b/f.txt', '/a/f.txt')
|
||
|
|
}
|
||
|
|
expect(await brain.vfs.readdir('/a')).toEqual(['f.txt'])
|
||
|
|
expect(await brain.vfs.readdir('/b')).toEqual([])
|
||
|
|
|
||
|
|
// Exactly ONE containment edge exists on the entity.
|
||
|
|
const stat = await brain.vfs.stat('/a/f.txt')
|
||
|
|
const edges = await brain.related({ to: stat.entityId, type: VerbType.Contains })
|
||
|
|
expect(edges).toHaveLength(1)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('move to the root gets a containment edge (used to be skipped → orphan)', async () => {
|
||
|
|
await brain.vfs.writeFile('/a/up.txt', 'to the top')
|
||
|
|
await brain.vfs.rename('/a/up.txt', '/up.txt')
|
||
|
|
|
||
|
|
const rootListing = (await brain.vfs.readdir('/')) as string[]
|
||
|
|
expect(rootListing).toContain('up.txt')
|
||
|
|
expect(await brain.vfs.readdir('/a')).toEqual([])
|
||
|
|
expect(String(await brain.vfs.readFile('/up.txt'))).toBe('to the top')
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('repairIndex() heals pre-fix containment ghosts (vfs.repairContainment)', () => {
|
||
|
|
let brain: any
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
|
||
|
|
brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' }, silent: true, dimensions: 384 })
|
||
|
|
await brain.init()
|
||
|
|
await brain.vfs.mkdir('/a', { recursive: true })
|
||
|
|
await brain.vfs.mkdir('/b', { recursive: true })
|
||
|
|
})
|
||
|
|
afterEach(async () => {
|
||
|
|
await brain.close?.().catch(() => {})
|
||
|
|
})
|
||
|
|
|
||
|
|
/** Reproduce the PRE-FIX defect state: file lives at /b/g.txt but a stale
|
||
|
|
* vfs-contains edge from /a lingers (what old renames left behind). */
|
||
|
|
const synthesizeGhost = async () => {
|
||
|
|
await brain.vfs.writeFile('/b/g.txt', 'ghost target')
|
||
|
|
const fileId = (await brain.vfs.stat('/b/g.txt')).entityId
|
||
|
|
const aId = (await brain.vfs.stat('/a')).entityId
|
||
|
|
await brain.relate({
|
||
|
|
from: aId,
|
||
|
|
to: fileId,
|
||
|
|
type: VerbType.Contains,
|
||
|
|
subtype: 'vfs-contains',
|
||
|
|
metadata: { isVFS: true }
|
||
|
|
})
|
||
|
|
return { fileId, aId }
|
||
|
|
}
|
||
|
|
|
||
|
|
it('a stale old-parent edge is removed; listings become honest', async () => {
|
||
|
|
const { fileId } = await synthesizeGhost()
|
||
|
|
// The defect state is visible: /a lists a file whose path says /b.
|
||
|
|
expect((await brain.vfs.readdir('/a')) as string[]).toContain('g.txt')
|
||
|
|
|
||
|
|
await brain.repairIndex()
|
||
|
|
|
||
|
|
expect(await brain.vfs.readdir('/a')).toEqual([])
|
||
|
|
expect(await brain.vfs.readdir('/b')).toEqual(['g.txt'])
|
||
|
|
const edges = await brain.related({ to: fileId, type: VerbType.Contains })
|
||
|
|
expect(edges).toHaveLength(1)
|
||
|
|
})
|
||
|
|
|
||
|
|
it("a user's own knowledge Contains edge onto the file survives the repair", async () => {
|
||
|
|
const { fileId } = await synthesizeGhost()
|
||
|
|
// A knowledge-graph containment from a NON-directory entity (a collection
|
||
|
|
// curating the file) — same verb TYPE, not a vfs-contains edge. The repair
|
||
|
|
// must remove only VFS containment ghosts, never user knowledge edges.
|
||
|
|
// (Edges dedupe by (from,to,type), so the user edge needs its own source.)
|
||
|
|
const collectionId = await brain.add({
|
||
|
|
data: 'reading list',
|
||
|
|
type: 'collection',
|
||
|
|
metadata: { kind: 'curation' }
|
||
|
|
})
|
||
|
|
await brain.relate({ from: collectionId, to: fileId, type: VerbType.Contains, subtype: 'curates' })
|
||
|
|
|
||
|
|
await brain.repairIndex()
|
||
|
|
|
||
|
|
const edges = await brain.related({ to: fileId, type: VerbType.Contains })
|
||
|
|
const subtypes = edges.map((e: any) => e.subtype).sort()
|
||
|
|
// The stale vfs ghost edge is gone; the correct vfs edge + the user's
|
||
|
|
// curation edge remain.
|
||
|
|
expect(subtypes).toEqual(['curates', 'vfs-contains'])
|
||
|
|
expect(edges.find((e: any) => e.subtype === 'curates')?.from).toBe(collectionId)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('a missing expected edge is restored (entity unreachable from its own directory)', async () => {
|
||
|
|
await brain.vfs.writeFile('/b/lost.txt', 'find me')
|
||
|
|
const fileId = (await brain.vfs.stat('/b/lost.txt')).entityId
|
||
|
|
// Simulate total edge loss (an older damage shape).
|
||
|
|
for (const e of await brain.related({ to: fileId, type: VerbType.Contains })) {
|
||
|
|
await brain.unrelate(e.id)
|
||
|
|
}
|
||
|
|
expect((await brain.vfs.readdir('/b')) as string[]).not.toContain('lost.txt')
|
||
|
|
|
||
|
|
await brain.repairIndex()
|
||
|
|
|
||
|
|
expect((await brain.vfs.readdir('/b')) as string[]).toContain('lost.txt')
|
||
|
|
})
|
||
|
|
})
|