331 lines
10 KiB
TypeScript
331 lines
10 KiB
TypeScript
/**
|
|
* Read-After-Write Consistency Integration Tests (v5.7.2)
|
|
*
|
|
* These tests verify the fix for the critical v5.7.x bug where:
|
|
* - brain.add() would create an entity
|
|
* - brain.relate() would immediately try to verify the entity exists
|
|
* - brain.get() would return null (entity not yet queryable)
|
|
* - Error thrown: "Source entity 19f3f6dd-2102-4b60-956c-bfc1d8213838 not found"
|
|
*
|
|
* The write-through cache in BaseStorage now guarantees that entities are
|
|
* immediately queryable after add() returns, fixing this race condition.
|
|
*
|
|
* Test Coverage:
|
|
* - brain.add() → brain.get() (basic read-after-write)
|
|
* - brain.add() → brain.relate() (the actual bug scenario)
|
|
* - Rapid entity creation + relationship creation (import workflow)
|
|
* - Update then read
|
|
* - Delete then verify deleted
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
import { Brainy } from '../../src/brainy.js'
|
|
import { NounType, VerbType } from '../../src/types/graphTypes.js'
|
|
import { tmpdir } from 'os'
|
|
import { join } from 'path'
|
|
import { rmSync } from 'fs'
|
|
|
|
describe('Read-After-Write Consistency (v5.7.2 Bug Fix)', () => {
|
|
let brain: Brainy
|
|
let testDir: string
|
|
|
|
beforeEach(async () => {
|
|
// Create unique test directory for each test
|
|
testDir = join(tmpdir(), `brainy-consistency-${Date.now()}-${Math.random().toString(36).substring(7)}`)
|
|
|
|
brain = new Brainy({ requireSubtype: false,
|
|
storage: {
|
|
type: 'filesystem',
|
|
config: {
|
|
baseDir: testDir,
|
|
enableCompression: false // Faster tests
|
|
}
|
|
},
|
|
dimensions: 384
|
|
})
|
|
|
|
await brain.init()
|
|
})
|
|
|
|
afterEach(async () => {
|
|
// Cleanup
|
|
try {
|
|
await brain.close()
|
|
} catch (err) {
|
|
// Ignore close errors
|
|
}
|
|
|
|
try {
|
|
rmSync(testDir, { recursive: true, force: true })
|
|
} catch (err) {
|
|
// Ignore cleanup errors
|
|
}
|
|
})
|
|
|
|
it('should allow brain.add() followed by immediate brain.get()', async () => {
|
|
// This tests the core read-after-write guarantee
|
|
const entityId = await brain.add({
|
|
data: 'Test Entity for Immediate Read',
|
|
type: NounType.Thing
|
|
})
|
|
|
|
// Immediately query - should NOT return null
|
|
const entity = await brain.get(entityId)
|
|
|
|
expect(entity).not.toBeNull()
|
|
expect(entity?.id).toBe(entityId)
|
|
expect(entity?.data).toBe('Test Entity for Immediate Read')
|
|
expect(entity?.type).toBe(NounType.Thing)
|
|
})
|
|
|
|
it('should allow brain.add() followed by immediate brain.relate() - THE BUG FIX', async () => {
|
|
// This is the EXACT bug scenario from the v5.7.x report
|
|
// ImportCoordinator would:
|
|
// 1. Create an entity with brain.add()
|
|
// 2. Immediately create a provenance link with brain.relate()
|
|
// 3. brain.relate() calls brain.get() to verify entities exist
|
|
// 4. brain.get() would return null → "Source entity not found" error
|
|
|
|
const sourceId = await brain.add({
|
|
data: 'Source Document',
|
|
type: NounType.Document,
|
|
metadata: { filename: 'test.pdf' }
|
|
})
|
|
|
|
const targetId = await brain.add({
|
|
data: 'Extracted Entity',
|
|
type: NounType.Thing,
|
|
metadata: { extractedFrom: 'test.pdf' }
|
|
})
|
|
|
|
// This should NOT throw "Source entity not found" or "Target entity not found"
|
|
// The write-through cache ensures both entities are immediately queryable
|
|
const relationId = await brain.relate({
|
|
from: sourceId,
|
|
to: targetId,
|
|
type: VerbType.Contains,
|
|
metadata: {
|
|
relationshipType: 'provenance',
|
|
evidence: 'Extracted from test.pdf'
|
|
}
|
|
})
|
|
|
|
expect(relationId).toBeTruthy()
|
|
expect(typeof relationId).toBe('string')
|
|
|
|
// Verify relationship exists and is correct
|
|
const relations = await brain.related(sourceId)
|
|
expect(relations).toHaveLength(1)
|
|
expect(relations[0].to).toBe(targetId)
|
|
expect(relations[0].type).toBe(VerbType.Contains)
|
|
})
|
|
|
|
it('should handle rapid entity creation + relationship creation (import scenario)', async () => {
|
|
// Simulate the import workflow from the bug report:
|
|
// - Extract 372 entities from PDF
|
|
// - Create entities rapidly
|
|
// - Create provenance relationships immediately after
|
|
// - v5.7.x would fail with "Source entity not found"
|
|
|
|
const documentId = await brain.add({
|
|
data: 'Test PDF Document',
|
|
type: NounType.Document,
|
|
metadata: { filename: 'large-import.pdf', pages: 50 }
|
|
})
|
|
|
|
const entityIds: string[] = []
|
|
const startTime = Date.now()
|
|
|
|
// Create 100 entities rapidly (simulates extraction phase)
|
|
for (let i = 0; i < 100; i++) {
|
|
const entityId = await brain.add({
|
|
data: `Entity ${i} extracted from PDF`,
|
|
type: NounType.Thing,
|
|
metadata: {
|
|
entityIndex: i,
|
|
extractedFrom: 'large-import.pdf',
|
|
page: Math.floor(i / 2) + 1
|
|
}
|
|
})
|
|
entityIds.push(entityId)
|
|
}
|
|
|
|
console.log(`Created 100 entities in ${Date.now() - startTime}ms`)
|
|
|
|
// Create provenance relationships immediately after entity creation
|
|
// This is where v5.7.x would fail
|
|
const relStartTime = Date.now()
|
|
for (let i = 0; i < entityIds.length; i++) {
|
|
await brain.relate({
|
|
from: documentId,
|
|
to: entityIds[i],
|
|
type: VerbType.Contains,
|
|
metadata: {
|
|
relationshipType: 'provenance',
|
|
extractionOrder: i
|
|
}
|
|
})
|
|
}
|
|
|
|
console.log(`Created 100 relationships in ${Date.now() - relStartTime}ms`)
|
|
|
|
// Verify all relationships created successfully
|
|
const relations = await brain.related(documentId)
|
|
expect(relations.length).toBe(100)
|
|
|
|
// Verify each entity is still queryable
|
|
for (const id of entityIds) {
|
|
const entity = await brain.get(id)
|
|
expect(entity).not.toBeNull()
|
|
expect(entity?.id).toBe(id)
|
|
}
|
|
})
|
|
|
|
it('should support read-after-update', async () => {
|
|
// Create entity
|
|
const entityId = await brain.add({
|
|
data: 'Original Data',
|
|
type: NounType.Thing,
|
|
metadata: { version: 1 }
|
|
})
|
|
|
|
// Update immediately
|
|
await brain.update({
|
|
id: entityId,
|
|
metadata: { version: 2, updated: true }
|
|
})
|
|
|
|
// Read immediately - should see updated data
|
|
const entity = await brain.get(entityId)
|
|
expect(entity).not.toBeNull()
|
|
expect(entity?.metadata?.version).toBe(2)
|
|
expect(entity?.metadata?.updated).toBe(true)
|
|
})
|
|
|
|
it('should verify entity is deleted after brain.remove()', async () => {
|
|
// Create entity
|
|
const entityId = await brain.add({
|
|
data: 'Entity to be deleted',
|
|
type: NounType.Thing
|
|
})
|
|
|
|
// Verify it exists
|
|
const beforeDelete = await brain.get(entityId)
|
|
expect(beforeDelete).not.toBeNull()
|
|
|
|
// Delete
|
|
await brain.remove(entityId)
|
|
|
|
// Verify it's deleted (should return null)
|
|
const afterDelete = await brain.get(entityId)
|
|
expect(afterDelete).toBeNull()
|
|
})
|
|
|
|
it('should handle complex relationship graph creation', async () => {
|
|
// Create a complex graph:
|
|
// Document → contains → Concept1
|
|
// → Concept2
|
|
// → Concept3
|
|
// Concept1 → relatedTo → Concept2
|
|
// Concept2 → relatedTo → Concept3
|
|
// All created in rapid succession
|
|
|
|
const documentId = await brain.add({
|
|
data: 'Complex Document',
|
|
type: NounType.Document
|
|
})
|
|
|
|
const concept1Id = await brain.add({
|
|
data: 'Concept 1',
|
|
type: NounType.Concept
|
|
})
|
|
|
|
const concept2Id = await brain.add({
|
|
data: 'Concept 2',
|
|
type: NounType.Concept
|
|
})
|
|
|
|
const concept3Id = await brain.add({
|
|
data: 'Concept 3',
|
|
type: NounType.Concept
|
|
})
|
|
|
|
// Create containment relationships
|
|
await brain.relate({ from: documentId, to: concept1Id, type: VerbType.Contains })
|
|
await brain.relate({ from: documentId, to: concept2Id, type: VerbType.Contains })
|
|
await brain.relate({ from: documentId, to: concept3Id, type: VerbType.Contains })
|
|
|
|
// Create concept relationships
|
|
await brain.relate({ from: concept1Id, to: concept2Id, type: VerbType.RelatedTo })
|
|
await brain.relate({ from: concept2Id, to: concept3Id, type: VerbType.RelatedTo })
|
|
|
|
// Verify all relationships exist
|
|
const docRelations = await brain.related(documentId)
|
|
expect(docRelations.length).toBe(3)
|
|
|
|
const concept1Relations = await brain.related(concept1Id)
|
|
expect(concept1Relations.length).toBeGreaterThan(0)
|
|
|
|
const concept2Relations = await brain.related(concept2Id)
|
|
expect(concept2Relations.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should handle the exact v5.7.x bug scenario with UUID from report', async () => {
|
|
// The bug report mentioned entity ID: 19f3f6dd-2102-4b60-956c-bfc1d8213838
|
|
// Let's use our own UUIDs but replicate the exact sequence
|
|
|
|
// Step 1: PDF import creates document entity
|
|
const documentId = await brain.add({
|
|
data: 'TfT~Sapient Species.pdf',
|
|
type: NounType.Document,
|
|
metadata: {
|
|
filename: 'TfT~Sapient Species.pdf',
|
|
size: 2036.8 * 1024,
|
|
format: 'pdf'
|
|
}
|
|
})
|
|
|
|
// Step 2: Extract 372 entities (we'll do 10 for speed)
|
|
const extractedIds: string[] = []
|
|
for (let i = 0; i < 10; i++) {
|
|
const id = await brain.add({
|
|
data: `Species ${i}`,
|
|
type: NounType.Thing,
|
|
metadata: {
|
|
extractedFrom: 'TfT~Sapient Species.pdf',
|
|
entityIndex: i
|
|
}
|
|
})
|
|
extractedIds.push(id)
|
|
}
|
|
|
|
// Step 3: VFSStructureGenerator creates provenance links
|
|
// This is where "Source entity not found" would occur
|
|
for (const entityId of extractedIds) {
|
|
const relationId = await brain.relate({
|
|
from: documentId,
|
|
to: entityId,
|
|
type: VerbType.Contains,
|
|
metadata: {
|
|
relationshipType: 'provenance',
|
|
evidence: 'Extracted from PDF'
|
|
}
|
|
})
|
|
expect(relationId).toBeTruthy()
|
|
}
|
|
|
|
// Step 4: Verify all entities are queryable
|
|
const doc = await brain.get(documentId)
|
|
expect(doc).not.toBeNull()
|
|
|
|
for (const id of extractedIds) {
|
|
const entity = await brain.get(id)
|
|
expect(entity).not.toBeNull()
|
|
expect(entity?.id).toBe(id)
|
|
}
|
|
|
|
// Step 5: Verify relationships exist
|
|
const relations = await brain.related(documentId)
|
|
expect(relations.length).toBe(10)
|
|
})
|
|
})
|