brainy/tests/integration/count-synchronization.test.ts
David Snelling 0ca0e5c6cc test(8.0): close brains in afterEach (count-sync, get-relations teardown)
Both suites removed their test directory without closing the brain, leaving the
writer-lock heartbeat + flush timers running into teardown and the next test.
Adds brain.close() before the rm.

Remaining failures in these files are separate, deeper issues (tracked in
.strategy): get-relations has one pagination assertion; count-sync is off-by-one
because a fresh brain carries one system/VFS-root noun that counts in
totalNounCount while find() excludes it — a counts-semantics decision.
2026-06-16 13:18:14 -07:00

342 lines
12 KiB
TypeScript

/**
* Count Synchronization Integration Tests (v4.1.2)
*
* Tests for Bug #1 and Bug #2: Count synchronization failures during add() and relate()
* This validates that counts.json is properly updated when entities and relationships are created
*
* NO MOCKS - Real integration tests with actual storage
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { Brainy } from '../../src/brainy.js'
import { NounType, VerbType } from '../../src/types/graphTypes.js'
import { rebuildCounts } from '../../src/utils/rebuildCounts.js'
import * as fs from 'fs/promises'
import * as path from 'path'
describe('Count Synchronization (Bug Fix v4.1.2)', () => {
let brain: Brainy
const testPath = './test-brainy-counts-sync'
beforeEach(async () => {
// Clean up test directory
try {
await fs.rm(testPath, { recursive: true, force: true })
} catch {
// Ignore if doesn't exist
}
// Create fresh Brainy instance
brain = new Brainy({ requireSubtype: false,
storage: { type: 'filesystem', path: testPath }
})
await brain.init()
})
afterEach(async () => {
// Close the brain first so its writer-lock heartbeat + flush timers stop
// before the directory is removed — otherwise they race teardown and bleed
// state into the next test.
try {
await brain?.close()
} catch {
// Best-effort teardown.
}
// Clean up test directory
try {
await fs.rm(testPath, { recursive: true, force: true })
} catch {
// Ignore cleanup errors
}
})
describe('Noun Count Synchronization', () => {
it('should update counts.json when adding a single entity', async () => {
// Add entity
const id = await brain.add({
data: 'Test Entity',
type: NounType.Document
})
// Flush to disk
await brain.flush()
// Read counts.json
const countsPath = path.join(testPath, '_system', 'counts.json')
const countsRaw = await fs.readFile(countsPath, 'utf8')
const counts = JSON.parse(countsRaw)
// Verify counts
expect(counts.totalNounCount).toBe(1)
expect(counts.entityCounts[NounType.Document]).toBe(1)
})
it('should update counts.json when adding multiple entities of same type', async () => {
// Add 3 entities of same type
await brain.add({ data: 'Entity 1', type: NounType.Document })
await brain.add({ data: 'Entity 2', type: NounType.Document })
await brain.add({ data: 'Entity 3', type: NounType.Document })
// Flush to disk
await brain.flush()
// Read counts.json
const countsPath = path.join(testPath, '_system', 'counts.json')
const countsRaw = await fs.readFile(countsPath, 'utf8')
const counts = JSON.parse(countsRaw)
// Verify counts
expect(counts.totalNounCount).toBe(3)
expect(counts.entityCounts[NounType.Document]).toBe(3)
})
it('should update counts.json when adding multiple entities of different types', async () => {
// Add entities of different types
await brain.add({ data: 'Document 1', type: NounType.Document })
await brain.add({ data: 'Person 1', type: NounType.Person })
await brain.add({ data: 'Document 2', type: NounType.Document })
await brain.add({ data: 'Organization 1', type: NounType.Organization })
// Flush to disk
await brain.flush()
// Read counts.json
const countsPath = path.join(testPath, '_system', 'counts.json')
const countsRaw = await fs.readFile(countsPath, 'utf8')
const counts = JSON.parse(countsRaw)
// Verify counts
expect(counts.totalNounCount).toBe(4)
expect(counts.entityCounts[NounType.Document]).toBe(2)
expect(counts.entityCounts[NounType.Person]).toBe(1)
expect(counts.entityCounts[NounType.Organization]).toBe(1)
})
it('should match find() results with counts.json', async () => {
// Add entities
await brain.add({ data: 'Entity 1', type: NounType.Document })
await brain.add({ data: 'Entity 2', type: NounType.Person })
await brain.add({ data: 'Entity 3', type: NounType.Document })
// Flush
await brain.flush()
// Get all entities using find with high limit
const entities = await brain.find({ limit: 1000 })
// Read counts
const countsPath = path.join(testPath, '_system', 'counts.json')
const countsRaw = await fs.readFile(countsPath, 'utf8')
const counts = JSON.parse(countsRaw)
// CRITICAL: Counts must match actual data
expect(counts.totalNounCount).toBe(entities.length)
expect(counts.totalNounCount).toBe(3)
})
})
describe('Verb Count Synchronization', () => {
it('should update counts.json when creating a single relationship', async () => {
// Create entities
const id1 = await brain.add({ data: 'Person 1', type: NounType.Person })
const id2 = await brain.add({ data: 'Person 2', type: NounType.Person })
// Create relationship
await brain.relate({
from: id1,
to: id2,
type: VerbType.FriendOf
})
// Flush to disk
await brain.flush()
// Read counts.json
const countsPath = path.join(testPath, '_system', 'counts.json')
const countsRaw = await fs.readFile(countsPath, 'utf8')
const counts = JSON.parse(countsRaw)
// Verify verb counts
expect(counts.totalVerbCount).toBe(1)
expect(counts.verbCounts[VerbType.FriendOf]).toBe(1)
})
it('should update counts.json when creating multiple relationships of same type', async () => {
// Create entities
const id1 = await brain.add({ data: 'Person 1', type: NounType.Person })
const id2 = await brain.add({ data: 'Person 2', type: NounType.Person })
const id3 = await brain.add({ data: 'Person 3', type: NounType.Person })
// Create relationships
await brain.relate({ from: id1, to: id2, type: VerbType.FriendOf })
await brain.relate({ from: id2, to: id3, type: VerbType.FriendOf })
// Flush to disk
await brain.flush()
// Read counts.json
const countsPath = path.join(testPath, '_system', 'counts.json')
const countsRaw = await fs.readFile(countsPath, 'utf8')
const counts = JSON.parse(countsRaw)
// Verify verb counts
expect(counts.totalVerbCount).toBe(2)
expect(counts.verbCounts[VerbType.FriendOf]).toBe(2)
})
it('should update counts.json when creating multiple relationships of different types', async () => {
// Create entities
const personId = await brain.add({ data: 'John', type: NounType.Person })
const docId = await brain.add({ data: 'Resume', type: NounType.Document })
const orgId = await brain.add({ data: 'Company', type: NounType.Organization })
// Create relationships of different types
await brain.relate({ from: personId, to: orgId, type: VerbType.WorksWith })
await brain.relate({ from: personId, to: docId, type: VerbType.Creates })
// Flush to disk
await brain.flush()
// Read counts.json
const countsPath = path.join(testPath, '_system', 'counts.json')
const countsRaw = await fs.readFile(countsPath, 'utf8')
const counts = JSON.parse(countsRaw)
// Verify verb counts
expect(counts.totalVerbCount).toBe(2)
expect(counts.verbCounts[VerbType.WorksWith]).toBe(1)
expect(counts.verbCounts[VerbType.Creates]).toBe(1)
})
})
describe('Batch Operations', () => {
it('should update counts.json during addMany()', async () => {
// Add multiple entities via batch
await brain.addMany({
items: [
{ data: 'Entity 1', type: NounType.Document },
{ data: 'Entity 2', type: NounType.Person },
{ data: 'Entity 3', type: NounType.Document }
]
})
// Flush to disk
await brain.flush()
// Read counts.json
const countsPath = path.join(testPath, '_system', 'counts.json')
const countsRaw = await fs.readFile(countsPath, 'utf8')
const counts = JSON.parse(countsRaw)
// Verify counts
expect(counts.totalNounCount).toBe(3)
expect(counts.entityCounts[NounType.Document]).toBe(2)
expect(counts.entityCounts[NounType.Person]).toBe(1)
})
it('should update counts.json during relateMany()', async () => {
// Create entities
const id1 = await brain.add({ data: 'Person 1', type: NounType.Person })
const id2 = await brain.add({ data: 'Person 2', type: NounType.Person })
const id3 = await brain.add({ data: 'Person 3', type: NounType.Person })
// Create multiple relationships via batch
await brain.relateMany({
items: [
{ from: id1, to: id2, type: VerbType.FriendOf },
{ from: id2, to: id3, type: VerbType.FriendOf }
]
})
// Flush to disk
await brain.flush()
// Read counts.json
const countsPath = path.join(testPath, '_system', 'counts.json')
const countsRaw = await fs.readFile(countsPath, 'utf8')
const counts = JSON.parse(countsRaw)
// Verify verb counts
expect(counts.totalVerbCount).toBe(2)
expect(counts.verbCounts[VerbType.FriendOf]).toBe(2)
})
})
describe('rebuildCounts Utility', () => {
it('should rebuild counts from corrupted state', async () => {
// Add entities
await brain.add({ data: 'Entity 1', type: NounType.Document })
await brain.add({ data: 'Entity 2', type: NounType.Person })
await brain.add({ data: 'Entity 3', type: NounType.Document })
// Flush to disk
await brain.flush()
// Manually corrupt counts.json (simulate the bug)
const countsPath = path.join(testPath, '_system', 'counts.json')
const corruptedCounts = {
entityCounts: {},
verbCounts: {},
totalNounCount: 0,
totalVerbCount: 0,
lastUpdated: new Date().toISOString()
}
await fs.writeFile(countsPath, JSON.stringify(corruptedCounts, null, 2))
// Force storage to reload corrupted counts by updating in-memory counts
// This simulates the scenario where counts are out of sync
;(brain.storage as any).totalNounCount = 0
;(brain.storage as any).totalVerbCount = 0
;(brain.storage as any).entityCounts = new Map()
;(brain.storage as any).verbCounts = new Map()
// Rebuild counts
const result = await rebuildCounts(brain.storage)
// Verify rebuild results
expect(result.nounCount).toBe(3)
expect(result.entityCounts.get(NounType.Document)).toBe(2)
expect(result.entityCounts.get(NounType.Person)).toBe(1)
// Verify counts.json was updated
const countsRaw = await fs.readFile(countsPath, 'utf8')
const counts = JSON.parse(countsRaw)
expect(counts.totalNounCount).toBe(3)
expect(counts.entityCounts[NounType.Document]).toBe(2)
expect(counts.entityCounts[NounType.Person]).toBe(1)
})
})
describe('Import Operations', () => {
it('should update counts.json during import', async () => {
// Create simple test data as JSON
const testData = {
entities: [
{ id: '1', name: 'Entity 1', type: 'document' },
{ id: '2', name: 'Entity 2', type: 'person' },
{ id: '3', name: 'Entity 3', type: 'document' }
]
}
// Import data
const result = await brain.import(testData, {
format: 'json',
createEntities: true,
createRelationships: false
})
// Flush to disk
await brain.flush()
// Read counts.json
const countsPath = path.join(testPath, '_system', 'counts.json')
const countsRaw = await fs.readFile(countsPath, 'utf8')
const counts = JSON.parse(countsRaw)
// Verify counts are updated (import may create additional VFS entities)
// So we check that totalNounCount >= graphNodesCreated
expect(counts.totalNounCount).toBeGreaterThanOrEqual(result.stats.graphNodesCreated)
expect(counts.totalNounCount).toBeGreaterThan(0)
})
})
})