307 lines
8.9 KiB
TypeScript
307 lines
8.9 KiB
TypeScript
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||
|
|
import { Brainy } from '../../src/brainy.js'
|
||
|
|
import { NounType, VerbType } from '../../src/types/graphTypes.js'
|
||
|
|
import * as fs from 'node:fs'
|
||
|
|
import * as path from 'node:path'
|
||
|
|
|
||
|
|
describe('Batch Import with Immediate Relations (v5.7.3 Fix)', () => {
|
||
|
|
let brain: Brainy
|
||
|
|
let testDir: string
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
// Create test directory
|
||
|
|
testDir = path.join(process.cwd(), 'test-batch-relations-' + Date.now())
|
||
|
|
fs.mkdirSync(testDir, { recursive: true })
|
||
|
|
|
||
|
|
// Initialize brain
|
||
|
|
brain = new Brainy({
|
||
|
|
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 {
|
||
|
|
fs.rmSync(testDir, { recursive: true, force: true })
|
||
|
|
} catch (err) {
|
||
|
|
// Ignore cleanup errors
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should create 372 entities and immediately query them for relationships (exact bug scenario)', async () => {
|
||
|
|
// Simulate the exact PDF import scenario from bug report
|
||
|
|
// 1. Create document entity (source)
|
||
|
|
const documentId = await brain.add({
|
||
|
|
data: 'TfT~Sapient Species.pdf',
|
||
|
|
type: NounType.Document,
|
||
|
|
metadata: {
|
||
|
|
filename: 'TfT~Sapient Species.pdf',
|
||
|
|
format: 'pdf'
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
expect(documentId).toBeTruthy()
|
||
|
|
|
||
|
|
// 2. Batch create 372 extracted entities (simulating entity extraction)
|
||
|
|
const entityParams = Array(372).fill(null).map((_, i) => ({
|
||
|
|
data: `Extracted Entity ${i}`,
|
||
|
|
type: NounType.Thing,
|
||
|
|
metadata: {
|
||
|
|
extractedFrom: 'TfT~Sapient Species.pdf',
|
||
|
|
entityNumber: i,
|
||
|
|
confidence: 0.9
|
||
|
|
}
|
||
|
|
}))
|
||
|
|
|
||
|
|
const addResult = await brain.addMany({
|
||
|
|
items: entityParams,
|
||
|
|
continueOnError: true
|
||
|
|
})
|
||
|
|
|
||
|
|
expect(addResult.successful.length).toBe(372)
|
||
|
|
expect(addResult.failed.length).toBe(0)
|
||
|
|
|
||
|
|
// 3. IMMEDIATELY create provenance links (this is where v5.7.0/v5.7.1/v5.7.2 failed)
|
||
|
|
// brain.relateMany() → brain.relate() → brain.get() must succeed
|
||
|
|
const provenanceParams = addResult.successful.map(entityId => ({
|
||
|
|
from: documentId,
|
||
|
|
to: entityId,
|
||
|
|
type: VerbType.Contains,
|
||
|
|
metadata: {
|
||
|
|
relationshipType: 'provenance',
|
||
|
|
evidence: 'Extracted from PDF'
|
||
|
|
}
|
||
|
|
}))
|
||
|
|
|
||
|
|
// THIS SHOULD NOT THROW "Source entity not found" error
|
||
|
|
const relationIds = await brain.relateMany({
|
||
|
|
items: provenanceParams,
|
||
|
|
continueOnError: true
|
||
|
|
})
|
||
|
|
|
||
|
|
expect(relationIds.length).toBe(372)
|
||
|
|
|
||
|
|
// 4. Verify all relationships exist (use higher limit to get all 372)
|
||
|
|
const relationships = await brain.getRelations({ from: documentId, limit: 500 })
|
||
|
|
expect(relationships.length).toBe(372)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should handle batch create + immediate single relate (simplified scenario)', async () => {
|
||
|
|
// Create 100 entities in batch
|
||
|
|
const entities = Array(100).fill(null).map((_, i) => ({
|
||
|
|
data: `Entity ${i}`,
|
||
|
|
type: NounType.Person,
|
||
|
|
metadata: { index: i }
|
||
|
|
}))
|
||
|
|
|
||
|
|
const addResult = await brain.addMany({
|
||
|
|
items: entities,
|
||
|
|
continueOnError: true
|
||
|
|
})
|
||
|
|
|
||
|
|
expect(addResult.successful.length).toBe(100)
|
||
|
|
|
||
|
|
// IMMEDIATELY try to relate first two entities (common pattern)
|
||
|
|
const relationId = await brain.relate({
|
||
|
|
from: addResult.successful[0],
|
||
|
|
to: addResult.successful[1],
|
||
|
|
type: VerbType.FriendOf
|
||
|
|
})
|
||
|
|
|
||
|
|
expect(relationId).toBeTruthy()
|
||
|
|
|
||
|
|
// Verify relationship exists
|
||
|
|
const relations = await brain.getRelations(addResult.successful[0])
|
||
|
|
expect(relations.length).toBe(1)
|
||
|
|
expect(relations[0].to).toBe(addResult.successful[1])
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should handle concurrent batch operations with cross-batch relationships', async () => {
|
||
|
|
// Create three batches in parallel
|
||
|
|
const batch1 = Array(50).fill(null).map((_, i) => ({
|
||
|
|
data: `Batch1-Entity${i}`,
|
||
|
|
type: NounType.Document
|
||
|
|
}))
|
||
|
|
|
||
|
|
const batch2 = Array(50).fill(null).map((_, i) => ({
|
||
|
|
data: `Batch2-Entity${i}`,
|
||
|
|
type: NounType.Organization
|
||
|
|
}))
|
||
|
|
|
||
|
|
const batch3 = Array(50).fill(null).map((_, i) => ({
|
||
|
|
data: `Batch3-Entity${i}`,
|
||
|
|
type: NounType.Location
|
||
|
|
}))
|
||
|
|
|
||
|
|
// Create all batches concurrently
|
||
|
|
const [result1, result2, result3] = await Promise.all([
|
||
|
|
brain.addMany({ items: batch1, continueOnError: true }),
|
||
|
|
brain.addMany({ items: batch2, continueOnError: true }),
|
||
|
|
brain.addMany({ items: batch3, continueOnError: true })
|
||
|
|
])
|
||
|
|
|
||
|
|
expect(result1.successful.length).toBe(50)
|
||
|
|
expect(result2.successful.length).toBe(50)
|
||
|
|
expect(result3.successful.length).toBe(50)
|
||
|
|
|
||
|
|
// Immediately create cross-batch relationships
|
||
|
|
const crossBatchRelations = []
|
||
|
|
for (let i = 0; i < 10; i++) {
|
||
|
|
crossBatchRelations.push({
|
||
|
|
from: result1.successful[i],
|
||
|
|
to: result2.successful[i],
|
||
|
|
type: VerbType.RelatedTo
|
||
|
|
})
|
||
|
|
crossBatchRelations.push({
|
||
|
|
from: result2.successful[i],
|
||
|
|
to: result3.successful[i],
|
||
|
|
type: VerbType.RelatedTo
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// THIS SHOULD NOT FAIL with "Source entity not found"
|
||
|
|
const relationIds = await brain.relateMany({
|
||
|
|
items: crossBatchRelations,
|
||
|
|
continueOnError: true
|
||
|
|
})
|
||
|
|
|
||
|
|
expect(relationIds.length).toBe(20)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should ensure type cache is populated after addMany', async () => {
|
||
|
|
// Create entities
|
||
|
|
const entities = Array(100).fill(null).map((_, i) => ({
|
||
|
|
data: `Test${i}`,
|
||
|
|
type: NounType.Concept
|
||
|
|
}))
|
||
|
|
|
||
|
|
const addResult = await brain.addMany({
|
||
|
|
items: entities,
|
||
|
|
continueOnError: true
|
||
|
|
})
|
||
|
|
|
||
|
|
// Immediately query all entities (tests type cache population)
|
||
|
|
for (const id of addResult.successful) {
|
||
|
|
const entity = await brain.get(id)
|
||
|
|
expect(entity).not.toBeNull()
|
||
|
|
expect(entity?.type).toBe(NounType.Concept)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should handle flush after batch operations', async () => {
|
||
|
|
// Create entities
|
||
|
|
const entities = Array(50).fill(null).map((_, i) => ({
|
||
|
|
data: `FlushTest${i}`,
|
||
|
|
type: NounType.Thing
|
||
|
|
}))
|
||
|
|
|
||
|
|
const addResult = await brain.addMany({
|
||
|
|
items: entities,
|
||
|
|
continueOnError: true
|
||
|
|
})
|
||
|
|
|
||
|
|
// Explicit flush (this is what ImportCoordinator does)
|
||
|
|
await brain.flush()
|
||
|
|
|
||
|
|
// After flush, write-through cache should be cleared but entities still queryable
|
||
|
|
for (const id of addResult.successful) {
|
||
|
|
const entity = await brain.get(id)
|
||
|
|
expect(entity).not.toBeNull()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should handle VFS-style structure generation after batch import', async () => {
|
||
|
|
// Simulate VFS structure generation workflow:
|
||
|
|
// 1. Create root collection
|
||
|
|
const rootId = await brain.add({
|
||
|
|
data: 'Import Collection',
|
||
|
|
type: NounType.Collection,
|
||
|
|
metadata: { isRoot: true }
|
||
|
|
})
|
||
|
|
|
||
|
|
// 2. Batch create child entities
|
||
|
|
const children = Array(100).fill(null).map((_, i) => ({
|
||
|
|
data: `Child${i}`,
|
||
|
|
type: NounType.Thing,
|
||
|
|
metadata: { parentCollection: rootId }
|
||
|
|
}))
|
||
|
|
|
||
|
|
const addResult = await brain.addMany({
|
||
|
|
items: children,
|
||
|
|
continueOnError: true
|
||
|
|
})
|
||
|
|
|
||
|
|
// 3. IMMEDIATELY create hierarchical relationships (VFS tree structure)
|
||
|
|
const hierarchyRelations = addResult.successful.map(childId => ({
|
||
|
|
from: rootId,
|
||
|
|
to: childId,
|
||
|
|
type: VerbType.Contains,
|
||
|
|
metadata: { hierarchyType: 'vfs-structure' }
|
||
|
|
}))
|
||
|
|
|
||
|
|
// THIS IS WHERE v5.7.0/v5.7.1/v5.7.2 FAILED
|
||
|
|
const relationIds = await brain.relateMany({
|
||
|
|
items: hierarchyRelations,
|
||
|
|
continueOnError: true
|
||
|
|
})
|
||
|
|
|
||
|
|
expect(relationIds.length).toBe(100)
|
||
|
|
|
||
|
|
// 4. Verify structure
|
||
|
|
const childrenRelations = await brain.getRelations(rootId)
|
||
|
|
expect(childrenRelations.length).toBe(100)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('should handle the exact error case from bug report: brain.relate after brain.addMany', async () => {
|
||
|
|
// This test replicates the EXACT code path that failed in v5.7.2
|
||
|
|
// ImportCoordinator: addMany() → relateMany() → relate() → get() → "Source entity not found"
|
||
|
|
|
||
|
|
// 1. Create source entity
|
||
|
|
const sourceId = await brain.add({
|
||
|
|
data: 'Source Document',
|
||
|
|
type: NounType.Document
|
||
|
|
})
|
||
|
|
|
||
|
|
// 2. Batch create targets
|
||
|
|
const targets = Array(10).fill(null).map((_, i) => ({
|
||
|
|
data: `Target${i}`,
|
||
|
|
type: NounType.Thing
|
||
|
|
}))
|
||
|
|
|
||
|
|
const addResult = await brain.addMany({
|
||
|
|
items: targets,
|
||
|
|
continueOnError: true
|
||
|
|
})
|
||
|
|
|
||
|
|
expect(addResult.successful.length).toBe(10)
|
||
|
|
|
||
|
|
// 3. IMMEDIATELY call brain.relate (not relateMany, to test the exact path)
|
||
|
|
for (const targetId of addResult.successful) {
|
||
|
|
// This is the exact call that fails in v5.7.2:
|
||
|
|
// brain.relate() → brain.get(from) → "Source entity not found"
|
||
|
|
const relationId = await brain.relate({
|
||
|
|
from: sourceId,
|
||
|
|
to: targetId,
|
||
|
|
type: VerbType.Contains
|
||
|
|
})
|
||
|
|
|
||
|
|
expect(relationId).toBeTruthy()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
})
|