109 lines
4.2 KiB
TypeScript
109 lines
4.2 KiB
TypeScript
|
|
/**
|
||
|
|
* @module tests/integration/brain-relocation
|
||
|
|
* @description LC8 — RELOCATABLE BRAIN DIRECTORY. A brain's directory moved
|
||
|
|
* wholesale to a new path (rename/copy — backup-restore, disk migration,
|
||
|
|
* container re-mount) must open and serve IDENTICALLY: no absolute paths may
|
||
|
|
* hide in any persisted artifact. Pinned across every intelligence: point
|
||
|
|
* reads, metadata find, semantic find, graph traversal, aggregation — plus
|
||
|
|
* continued writes with monotonic generations and time-travel reads over
|
||
|
|
* pre-move history.
|
||
|
|
*/
|
||
|
|
import { describe, it, expect, afterEach } from 'vitest'
|
||
|
|
import { mkdtempSync, rmSync, renameSync } from 'node:fs'
|
||
|
|
import { tmpdir } from 'node:os'
|
||
|
|
import { join } from 'node:path'
|
||
|
|
import { Brainy } from '../../src/index.js'
|
||
|
|
import { NounType, VerbType } from '../../src/types/graphTypes.js'
|
||
|
|
|
||
|
|
const dirs: string[] = []
|
||
|
|
const brains: Brainy[] = []
|
||
|
|
|
||
|
|
afterEach(async () => {
|
||
|
|
for (const b of brains.splice(0)) await b.close().catch(() => {})
|
||
|
|
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true })
|
||
|
|
})
|
||
|
|
|
||
|
|
const AGG = {
|
||
|
|
name: 'by_kind',
|
||
|
|
source: { type: NounType.Document },
|
||
|
|
groupBy: ['kind'] as string[],
|
||
|
|
metrics: { count: { op: 'count' as const } }
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('LC8 — a moved brain directory opens and serves identically', () => {
|
||
|
|
it('rename the directory: all three intelligences serve, writes continue, history travels', async () => {
|
||
|
|
const home = mkdtempSync(join(tmpdir(), 'brainy-reloc-'))
|
||
|
|
dirs.push(home)
|
||
|
|
const oldPath = join(home, 'brain-old')
|
||
|
|
const newPath = join(home, 'brain-new')
|
||
|
|
|
||
|
|
// Season a brain: rows, a relation, an aggregate, then flush + close.
|
||
|
|
let brain = new Brainy({ storage: { type: 'filesystem', path: oldPath }, requireSubtype: false })
|
||
|
|
await brain.init()
|
||
|
|
brains.push(brain)
|
||
|
|
brain.defineAggregate(AGG)
|
||
|
|
const alpha = await brain.add({
|
||
|
|
data: 'alpha document about mountain geology',
|
||
|
|
type: NounType.Document,
|
||
|
|
metadata: { kind: 'report', n: 1 }
|
||
|
|
})
|
||
|
|
const beta = await brain.add({
|
||
|
|
data: 'beta document about coastal erosion',
|
||
|
|
type: NounType.Document,
|
||
|
|
metadata: { kind: 'report', n: 2 }
|
||
|
|
})
|
||
|
|
await brain.relate({ from: alpha, to: beta, verb: VerbType.RelatedTo })
|
||
|
|
await brain.queryAggregate(AGG.name) // settle backfill
|
||
|
|
const preMoveGen = brain.generation()
|
||
|
|
await brain.flush()
|
||
|
|
await brain.close()
|
||
|
|
brains.pop()
|
||
|
|
|
||
|
|
// The move: wholesale directory rename.
|
||
|
|
renameSync(oldPath, newPath)
|
||
|
|
|
||
|
|
// Reopen at the NEW path — everything serves.
|
||
|
|
brain = new Brainy({ storage: { type: 'filesystem', path: newPath }, requireSubtype: false })
|
||
|
|
await brain.init()
|
||
|
|
brains.push(brain)
|
||
|
|
brain.defineAggregate(AGG)
|
||
|
|
|
||
|
|
// Point read + metadata find.
|
||
|
|
expect((await brain.get(alpha))!.data).toContain('mountain geology')
|
||
|
|
const found = await brain.find({ where: { kind: 'report' }, limit: 10 })
|
||
|
|
expect(found.map((r) => r.id).sort()).toEqual([alpha, beta].sort())
|
||
|
|
|
||
|
|
// Semantic find.
|
||
|
|
const sem = await brain.find({ query: 'alpha document about mountain geology', limit: 3 })
|
||
|
|
expect(sem.map((r) => r.id)).toContain(alpha)
|
||
|
|
|
||
|
|
// Graph traversal.
|
||
|
|
const related = await brain.related(alpha)
|
||
|
|
expect(related.map((r) => r.to)).toContain(beta)
|
||
|
|
|
||
|
|
// Aggregation.
|
||
|
|
const agg = (await brain.queryAggregate(AGG.name)) as Array<{
|
||
|
|
groupKey: Record<string, unknown>
|
||
|
|
metrics: Record<string, unknown>
|
||
|
|
}>
|
||
|
|
const reportRow = agg.find((g) => g.groupKey['kind'] === 'report')
|
||
|
|
expect(Number(reportRow?.metrics.count)).toBe(2)
|
||
|
|
|
||
|
|
// Writes continue with monotonic generations.
|
||
|
|
const gamma = await brain.add({
|
||
|
|
data: 'gamma addendum after the move',
|
||
|
|
type: NounType.Document,
|
||
|
|
metadata: { kind: 'report', n: 3 }
|
||
|
|
})
|
||
|
|
expect(brain.generation()).toBeGreaterThan(preMoveGen)
|
||
|
|
expect((await brain.get(gamma))!.data).toContain('addendum')
|
||
|
|
|
||
|
|
// Time travel across the move boundary: the pre-move pin sees exactly
|
||
|
|
// the pre-move world (no gamma), served from relocated history.
|
||
|
|
const dbPast = await brain.asOf(preMoveGen)
|
||
|
|
expect(await dbPast.get(gamma)).toBeNull()
|
||
|
|
expect((await dbPast.get(alpha))!.data).toContain('mountain geology')
|
||
|
|
await dbPast.release()
|
||
|
|
}, 120000)
|
||
|
|
})
|