brainy/tests/integration/batchImportWithRelations.test.ts

309 lines
9 KiB
TypeScript
Raw Normal View History

fix: resolve REAL v5.7.x race condition - type cache layer (v5.7.3) v5.7.2's write-through cache fixed the WRONG layer. The actual bug was in the type cache layer (nounTypeCache), not the storage file I/O layer. ROOT CAUSE ANALYSIS: During batch imports (brain.addMany()), the race condition occurs at the TYPE CACHE LAYER, not the storage layer: 1. brain.addMany() creates entities in parallel 2. nounTypeCache.set(id, type) populates cache [SYNC] 3. File writes happen async 4. Promise.allSettled() returns when promises settle 5. brain.relateMany() IMMEDIATELY calls brain.get() 6. brain.get() → getNounMetadata() checks nounTypeCache 7. On CACHE MISS → falls back to searching ALL 42 types 8. Write-through cache already cleared (v5.7.2 lifetime: microseconds) 9. File system read returns NULL 10. Error: "Source entity not found" THE THREE-LAYER FIX: 1. EXPLICIT FLUSH in ImportCoordinator (line 1054) - Added: await brain.flush() after brain.addMany() - Guarantees all writes flushed before brain.relateMany() - Fixes the immediate race condition 2. TYPE CACHE WARMING in brainy.ts (lines 1859-1877) - After addMany() completes, ensure nounTypeCache populated - Prevents cache misses that trigger expensive 42-type fallback - Eliminates root cause of race condition 3. EXTENDED WRITE-THROUGH CACHE LIFETIME in baseStorage.ts - Cache now persists until explicit flush() call - Provides safety net for queries between batch write and flush - Changed from: write start → write complete (~1ms) - Changed to: write start → flush() call (batch operation lifetime) IMPACT: - Fixes "Source entity not found" in v5.7.0/v5.7.1/v5.7.2 - 100% success rate on 372-entity PDF imports - All 22 tests passing (15 existing + 7 new) - Zero performance regression (flush is explicit, not automatic) TEST COVERAGE: - 7 new integration tests for batch import scenarios - Updated 1 unit test to reflect extended cache lifetime - All tests verify exact bug scenario from production report FILES MODIFIED: - src/import/ImportCoordinator.ts: Added flush after addMany - src/brainy.ts: Added type cache warming + flush cache clear - src/storage/baseStorage.ts: Extended write-through cache lifetime - tests/integration/batchImportWithRelations.test.ts: NEW (7 tests) - tests/unit/storage/writeThroughCache.test.ts: Updated 1 test WHY v5.7.2 FAILED: The write-through cache in v5.7.2 operates at the storage FILE I/O layer, but the bug occurs at the TYPE CACHE layer which sits above storage. When nounTypeCache has a miss, it triggers a 42-type search fallback, which happens AFTER the write-through cache is already cleared. v5.7.3 fixes the ACTUAL root cause: type cache synchronization.
2025-11-12 12:13:35 -08:00
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
feat(8.0)!: flip requireSubtype default to true (BRAINY-8.0-SUBTYPE-CONTRACT § C-1) Brainy 8.0 makes subtype required by default on every public write path (`add`, `addMany`, `update`, `relate`, `relateMany`, `updateRelation`, import). Per the locked C-1 contract, every entity and relation gets a non-empty subtype string by the time the storage layer sees it. OPT-OUT REMAINS FULLY SUPPORTED The runtime flag is still consumer-controlled. Three opt-out paths cover migration / legacy fixtures / typed escape: - `new Brainy({ requireSubtype: false })` — last-resort: turn off the contract entirely. Recommended only for migration windows or test fixtures that legitimately can't supply a subtype. - `new Brainy({ requireSubtype: { except: [NounType.Thing, ...] } })` — per-type allowlist: strict everywhere except the listed types. - `brain.requireSubtype(type, options)` — per-type registration with optional vocabulary. Composes with the brain-wide flag. Default is now `true`. Opt-out is explicit and documented; nothing silently degrades. TEST SWEEP Bulk-applied `requireSubtype: false` to every `new Brainy({...})` call site across 120 test files. Three sed patterns covered the shapes: - `new Brainy({` → `new Brainy({ requireSubtype: false,` - `new Brainy<T>({` → `new Brainy<T>({ requireSubtype: false,` - `new Brainy()` → `new Brainy({ requireSubtype: false })` tests/helpers/test-factory.ts → createTestConfig() defaults `requireSubtype: false` so test files using the helper inherit the opt-out without per-site edits. The test sites that DO exercise subtype semantics (the subtype-and-facets suite, the strict-mode-self-test suite, the verb- subtype-and-enforcement suite, etc.) already pass real subtypes — they were the 7.30.x acceptance tests for this contract. Those tests continue to pass unchanged. CHANGES src/brainy.ts - normalizeConfig() — `requireSubtype` default `false` → `true`. Comment refreshed to document the three opt-out paths. tests/* (120 files) - Bulk-edited brain construction sites. No functional test changes; the opt-out preserves the test author's original intent. tests/helpers/test-factory.ts - createTestConfig() base config gains `requireSubtype: false`. NO-OP for consumers who were already passing subtype on every write. For consumers who weren't, the upgrade path is one of the three opt-out forms above. Migration recipe documented in 8.0 release notes (next commit). VERIFICATION - npx tsc --noEmit: clean - npm test: 1408 / 1409 (same pre-existing race-condition outstanding; no other regressions from the flip)
2026-06-09 14:58:25 -07:00
brain = new Brainy({ requireSubtype: false,
fix: resolve REAL v5.7.x race condition - type cache layer (v5.7.3) v5.7.2's write-through cache fixed the WRONG layer. The actual bug was in the type cache layer (nounTypeCache), not the storage file I/O layer. ROOT CAUSE ANALYSIS: During batch imports (brain.addMany()), the race condition occurs at the TYPE CACHE LAYER, not the storage layer: 1. brain.addMany() creates entities in parallel 2. nounTypeCache.set(id, type) populates cache [SYNC] 3. File writes happen async 4. Promise.allSettled() returns when promises settle 5. brain.relateMany() IMMEDIATELY calls brain.get() 6. brain.get() → getNounMetadata() checks nounTypeCache 7. On CACHE MISS → falls back to searching ALL 42 types 8. Write-through cache already cleared (v5.7.2 lifetime: microseconds) 9. File system read returns NULL 10. Error: "Source entity not found" THE THREE-LAYER FIX: 1. EXPLICIT FLUSH in ImportCoordinator (line 1054) - Added: await brain.flush() after brain.addMany() - Guarantees all writes flushed before brain.relateMany() - Fixes the immediate race condition 2. TYPE CACHE WARMING in brainy.ts (lines 1859-1877) - After addMany() completes, ensure nounTypeCache populated - Prevents cache misses that trigger expensive 42-type fallback - Eliminates root cause of race condition 3. EXTENDED WRITE-THROUGH CACHE LIFETIME in baseStorage.ts - Cache now persists until explicit flush() call - Provides safety net for queries between batch write and flush - Changed from: write start → write complete (~1ms) - Changed to: write start → flush() call (batch operation lifetime) IMPACT: - Fixes "Source entity not found" in v5.7.0/v5.7.1/v5.7.2 - 100% success rate on 372-entity PDF imports - All 22 tests passing (15 existing + 7 new) - Zero performance regression (flush is explicit, not automatic) TEST COVERAGE: - 7 new integration tests for batch import scenarios - Updated 1 unit test to reflect extended cache lifetime - All tests verify exact bug scenario from production report FILES MODIFIED: - src/import/ImportCoordinator.ts: Added flush after addMany - src/brainy.ts: Added type cache warming + flush cache clear - src/storage/baseStorage.ts: Extended write-through cache lifetime - tests/integration/batchImportWithRelations.test.ts: NEW (7 tests) - tests/unit/storage/writeThroughCache.test.ts: Updated 1 test WHY v5.7.2 FAILED: The write-through cache in v5.7.2 operates at the storage FILE I/O layer, but the bug occurs at the TYPE CACHE layer which sits above storage. When nounTypeCache has a miss, it triggers a 42-type search fallback, which happens AFTER the write-through cache is already cleared. v5.7.3 fixes the ACTUAL root cause: type cache synchronization.
2025-11-12 12:13:35 -08:00
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,
feat(8.0): reserved-field enforcement — reservedFieldPolicy defaults to throw An untyped (JS) caller that smuggles a Brainy-reserved field (confidence, weight, subtype, visibility, service, createdBy, noun/verb, data, createdAt, updatedAt, _rev) inside a write-path metadata bag previously got a silent remap-or-drop — a class of bug where confidence-evolution writes no-oped for weeks before being caught on read-back. 8.0 closes this with no silent failures. - New BrainyConfig.reservedFieldPolicy: 'throw' | 'warn' | 'remap' (default 'throw'). 'throw' rejects the write naming every offending key + its correct write path; 'warn' remaps with a one-shot per-key warning; 'remap' is the legacy silent path. - Central enforceReservedPolicy gate wired into all four remap methods (add, update, relate, updateRelation) so live calls AND their transact()/with() mirrors honor it. Single-source reservedWritePath guidance shared by throw+warn. - 'warn' now warns for EVERY reserved key (closes the gap where only system-managed fields warned). Dead warnDropped* helpers removed. - Import pipeline migrated to route reserved values (confidence/weight/subtype) through dedicated params and strip reserved keys from extractor/customMetadata bags via the canonical split*MetadataRecord helpers — imports no longer trip the default throw. - Tests: new reservedFieldPolicy matrix (throw/warn/remap across every write path + transact); remap-correctness suite reframed as opt-in 'remap'; shared test-factory no longer emits reserved keys in custom metadata.
2026-06-20 15:37:21 -07:00
// `confidence` is a reserved field — pass it as the dedicated param, not
// inside the metadata bag (8.0 reservedFieldPolicy defaults to 'throw').
confidence: 0.9,
fix: resolve REAL v5.7.x race condition - type cache layer (v5.7.3) v5.7.2's write-through cache fixed the WRONG layer. The actual bug was in the type cache layer (nounTypeCache), not the storage file I/O layer. ROOT CAUSE ANALYSIS: During batch imports (brain.addMany()), the race condition occurs at the TYPE CACHE LAYER, not the storage layer: 1. brain.addMany() creates entities in parallel 2. nounTypeCache.set(id, type) populates cache [SYNC] 3. File writes happen async 4. Promise.allSettled() returns when promises settle 5. brain.relateMany() IMMEDIATELY calls brain.get() 6. brain.get() → getNounMetadata() checks nounTypeCache 7. On CACHE MISS → falls back to searching ALL 42 types 8. Write-through cache already cleared (v5.7.2 lifetime: microseconds) 9. File system read returns NULL 10. Error: "Source entity not found" THE THREE-LAYER FIX: 1. EXPLICIT FLUSH in ImportCoordinator (line 1054) - Added: await brain.flush() after brain.addMany() - Guarantees all writes flushed before brain.relateMany() - Fixes the immediate race condition 2. TYPE CACHE WARMING in brainy.ts (lines 1859-1877) - After addMany() completes, ensure nounTypeCache populated - Prevents cache misses that trigger expensive 42-type fallback - Eliminates root cause of race condition 3. EXTENDED WRITE-THROUGH CACHE LIFETIME in baseStorage.ts - Cache now persists until explicit flush() call - Provides safety net for queries between batch write and flush - Changed from: write start → write complete (~1ms) - Changed to: write start → flush() call (batch operation lifetime) IMPACT: - Fixes "Source entity not found" in v5.7.0/v5.7.1/v5.7.2 - 100% success rate on 372-entity PDF imports - All 22 tests passing (15 existing + 7 new) - Zero performance regression (flush is explicit, not automatic) TEST COVERAGE: - 7 new integration tests for batch import scenarios - Updated 1 unit test to reflect extended cache lifetime - All tests verify exact bug scenario from production report FILES MODIFIED: - src/import/ImportCoordinator.ts: Added flush after addMany - src/brainy.ts: Added type cache warming + flush cache clear - src/storage/baseStorage.ts: Extended write-through cache lifetime - tests/integration/batchImportWithRelations.test.ts: NEW (7 tests) - tests/unit/storage/writeThroughCache.test.ts: Updated 1 test WHY v5.7.2 FAILED: The write-through cache in v5.7.2 operates at the storage FILE I/O layer, but the bug occurs at the TYPE CACHE layer which sits above storage. When nounTypeCache has a miss, it triggers a 42-type search fallback, which happens AFTER the write-through cache is already cleared. v5.7.3 fixes the ACTUAL root cause: type cache synchronization.
2025-11-12 12:13:35 -08:00
metadata: {
extractedFrom: 'TfT~Sapient Species.pdf',
feat(8.0): reserved-field enforcement — reservedFieldPolicy defaults to throw An untyped (JS) caller that smuggles a Brainy-reserved field (confidence, weight, subtype, visibility, service, createdBy, noun/verb, data, createdAt, updatedAt, _rev) inside a write-path metadata bag previously got a silent remap-or-drop — a class of bug where confidence-evolution writes no-oped for weeks before being caught on read-back. 8.0 closes this with no silent failures. - New BrainyConfig.reservedFieldPolicy: 'throw' | 'warn' | 'remap' (default 'throw'). 'throw' rejects the write naming every offending key + its correct write path; 'warn' remaps with a one-shot per-key warning; 'remap' is the legacy silent path. - Central enforceReservedPolicy gate wired into all four remap methods (add, update, relate, updateRelation) so live calls AND their transact()/with() mirrors honor it. Single-source reservedWritePath guidance shared by throw+warn. - 'warn' now warns for EVERY reserved key (closes the gap where only system-managed fields warned). Dead warnDropped* helpers removed. - Import pipeline migrated to route reserved values (confidence/weight/subtype) through dedicated params and strip reserved keys from extractor/customMetadata bags via the canonical split*MetadataRecord helpers — imports no longer trip the default throw. - Tests: new reservedFieldPolicy matrix (throw/warn/remap across every write path + transact); remap-correctness suite reframed as opt-in 'remap'; shared test-factory no longer emits reserved keys in custom metadata.
2026-06-20 15:37:21 -07:00
entityNumber: i
fix: resolve REAL v5.7.x race condition - type cache layer (v5.7.3) v5.7.2's write-through cache fixed the WRONG layer. The actual bug was in the type cache layer (nounTypeCache), not the storage file I/O layer. ROOT CAUSE ANALYSIS: During batch imports (brain.addMany()), the race condition occurs at the TYPE CACHE LAYER, not the storage layer: 1. brain.addMany() creates entities in parallel 2. nounTypeCache.set(id, type) populates cache [SYNC] 3. File writes happen async 4. Promise.allSettled() returns when promises settle 5. brain.relateMany() IMMEDIATELY calls brain.get() 6. brain.get() → getNounMetadata() checks nounTypeCache 7. On CACHE MISS → falls back to searching ALL 42 types 8. Write-through cache already cleared (v5.7.2 lifetime: microseconds) 9. File system read returns NULL 10. Error: "Source entity not found" THE THREE-LAYER FIX: 1. EXPLICIT FLUSH in ImportCoordinator (line 1054) - Added: await brain.flush() after brain.addMany() - Guarantees all writes flushed before brain.relateMany() - Fixes the immediate race condition 2. TYPE CACHE WARMING in brainy.ts (lines 1859-1877) - After addMany() completes, ensure nounTypeCache populated - Prevents cache misses that trigger expensive 42-type fallback - Eliminates root cause of race condition 3. EXTENDED WRITE-THROUGH CACHE LIFETIME in baseStorage.ts - Cache now persists until explicit flush() call - Provides safety net for queries between batch write and flush - Changed from: write start → write complete (~1ms) - Changed to: write start → flush() call (batch operation lifetime) IMPACT: - Fixes "Source entity not found" in v5.7.0/v5.7.1/v5.7.2 - 100% success rate on 372-entity PDF imports - All 22 tests passing (15 existing + 7 new) - Zero performance regression (flush is explicit, not automatic) TEST COVERAGE: - 7 new integration tests for batch import scenarios - Updated 1 unit test to reflect extended cache lifetime - All tests verify exact bug scenario from production report FILES MODIFIED: - src/import/ImportCoordinator.ts: Added flush after addMany - src/brainy.ts: Added type cache warming + flush cache clear - src/storage/baseStorage.ts: Extended write-through cache lifetime - tests/integration/batchImportWithRelations.test.ts: NEW (7 tests) - tests/unit/storage/writeThroughCache.test.ts: Updated 1 test WHY v5.7.2 FAILED: The write-through cache in v5.7.2 operates at the storage FILE I/O layer, but the bug occurs at the TYPE CACHE layer which sits above storage. When nounTypeCache has a miss, it triggers a 42-type search fallback, which happens AFTER the write-through cache is already cleared. v5.7.3 fixes the ACTUAL root cause: type cache synchronization.
2025-11-12 12:13:35 -08:00
}
}))
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.related({ from: documentId, limit: 500 })
fix: resolve REAL v5.7.x race condition - type cache layer (v5.7.3) v5.7.2's write-through cache fixed the WRONG layer. The actual bug was in the type cache layer (nounTypeCache), not the storage file I/O layer. ROOT CAUSE ANALYSIS: During batch imports (brain.addMany()), the race condition occurs at the TYPE CACHE LAYER, not the storage layer: 1. brain.addMany() creates entities in parallel 2. nounTypeCache.set(id, type) populates cache [SYNC] 3. File writes happen async 4. Promise.allSettled() returns when promises settle 5. brain.relateMany() IMMEDIATELY calls brain.get() 6. brain.get() → getNounMetadata() checks nounTypeCache 7. On CACHE MISS → falls back to searching ALL 42 types 8. Write-through cache already cleared (v5.7.2 lifetime: microseconds) 9. File system read returns NULL 10. Error: "Source entity not found" THE THREE-LAYER FIX: 1. EXPLICIT FLUSH in ImportCoordinator (line 1054) - Added: await brain.flush() after brain.addMany() - Guarantees all writes flushed before brain.relateMany() - Fixes the immediate race condition 2. TYPE CACHE WARMING in brainy.ts (lines 1859-1877) - After addMany() completes, ensure nounTypeCache populated - Prevents cache misses that trigger expensive 42-type fallback - Eliminates root cause of race condition 3. EXTENDED WRITE-THROUGH CACHE LIFETIME in baseStorage.ts - Cache now persists until explicit flush() call - Provides safety net for queries between batch write and flush - Changed from: write start → write complete (~1ms) - Changed to: write start → flush() call (batch operation lifetime) IMPACT: - Fixes "Source entity not found" in v5.7.0/v5.7.1/v5.7.2 - 100% success rate on 372-entity PDF imports - All 22 tests passing (15 existing + 7 new) - Zero performance regression (flush is explicit, not automatic) TEST COVERAGE: - 7 new integration tests for batch import scenarios - Updated 1 unit test to reflect extended cache lifetime - All tests verify exact bug scenario from production report FILES MODIFIED: - src/import/ImportCoordinator.ts: Added flush after addMany - src/brainy.ts: Added type cache warming + flush cache clear - src/storage/baseStorage.ts: Extended write-through cache lifetime - tests/integration/batchImportWithRelations.test.ts: NEW (7 tests) - tests/unit/storage/writeThroughCache.test.ts: Updated 1 test WHY v5.7.2 FAILED: The write-through cache in v5.7.2 operates at the storage FILE I/O layer, but the bug occurs at the TYPE CACHE layer which sits above storage. When nounTypeCache has a miss, it triggers a 42-type search fallback, which happens AFTER the write-through cache is already cleared. v5.7.3 fixes the ACTUAL root cause: type cache synchronization.
2025-11-12 12:13:35 -08:00
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.related(addResult.successful[0])
fix: resolve REAL v5.7.x race condition - type cache layer (v5.7.3) v5.7.2's write-through cache fixed the WRONG layer. The actual bug was in the type cache layer (nounTypeCache), not the storage file I/O layer. ROOT CAUSE ANALYSIS: During batch imports (brain.addMany()), the race condition occurs at the TYPE CACHE LAYER, not the storage layer: 1. brain.addMany() creates entities in parallel 2. nounTypeCache.set(id, type) populates cache [SYNC] 3. File writes happen async 4. Promise.allSettled() returns when promises settle 5. brain.relateMany() IMMEDIATELY calls brain.get() 6. brain.get() → getNounMetadata() checks nounTypeCache 7. On CACHE MISS → falls back to searching ALL 42 types 8. Write-through cache already cleared (v5.7.2 lifetime: microseconds) 9. File system read returns NULL 10. Error: "Source entity not found" THE THREE-LAYER FIX: 1. EXPLICIT FLUSH in ImportCoordinator (line 1054) - Added: await brain.flush() after brain.addMany() - Guarantees all writes flushed before brain.relateMany() - Fixes the immediate race condition 2. TYPE CACHE WARMING in brainy.ts (lines 1859-1877) - After addMany() completes, ensure nounTypeCache populated - Prevents cache misses that trigger expensive 42-type fallback - Eliminates root cause of race condition 3. EXTENDED WRITE-THROUGH CACHE LIFETIME in baseStorage.ts - Cache now persists until explicit flush() call - Provides safety net for queries between batch write and flush - Changed from: write start → write complete (~1ms) - Changed to: write start → flush() call (batch operation lifetime) IMPACT: - Fixes "Source entity not found" in v5.7.0/v5.7.1/v5.7.2 - 100% success rate on 372-entity PDF imports - All 22 tests passing (15 existing + 7 new) - Zero performance regression (flush is explicit, not automatic) TEST COVERAGE: - 7 new integration tests for batch import scenarios - Updated 1 unit test to reflect extended cache lifetime - All tests verify exact bug scenario from production report FILES MODIFIED: - src/import/ImportCoordinator.ts: Added flush after addMany - src/brainy.ts: Added type cache warming + flush cache clear - src/storage/baseStorage.ts: Extended write-through cache lifetime - tests/integration/batchImportWithRelations.test.ts: NEW (7 tests) - tests/unit/storage/writeThroughCache.test.ts: Updated 1 test WHY v5.7.2 FAILED: The write-through cache in v5.7.2 operates at the storage FILE I/O layer, but the bug occurs at the TYPE CACHE layer which sits above storage. When nounTypeCache has a miss, it triggers a 42-type search fallback, which happens AFTER the write-through cache is already cleared. v5.7.3 fixes the ACTUAL root cause: type cache synchronization.
2025-11-12 12:13:35 -08:00
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.related(rootId)
fix: resolve REAL v5.7.x race condition - type cache layer (v5.7.3) v5.7.2's write-through cache fixed the WRONG layer. The actual bug was in the type cache layer (nounTypeCache), not the storage file I/O layer. ROOT CAUSE ANALYSIS: During batch imports (brain.addMany()), the race condition occurs at the TYPE CACHE LAYER, not the storage layer: 1. brain.addMany() creates entities in parallel 2. nounTypeCache.set(id, type) populates cache [SYNC] 3. File writes happen async 4. Promise.allSettled() returns when promises settle 5. brain.relateMany() IMMEDIATELY calls brain.get() 6. brain.get() → getNounMetadata() checks nounTypeCache 7. On CACHE MISS → falls back to searching ALL 42 types 8. Write-through cache already cleared (v5.7.2 lifetime: microseconds) 9. File system read returns NULL 10. Error: "Source entity not found" THE THREE-LAYER FIX: 1. EXPLICIT FLUSH in ImportCoordinator (line 1054) - Added: await brain.flush() after brain.addMany() - Guarantees all writes flushed before brain.relateMany() - Fixes the immediate race condition 2. TYPE CACHE WARMING in brainy.ts (lines 1859-1877) - After addMany() completes, ensure nounTypeCache populated - Prevents cache misses that trigger expensive 42-type fallback - Eliminates root cause of race condition 3. EXTENDED WRITE-THROUGH CACHE LIFETIME in baseStorage.ts - Cache now persists until explicit flush() call - Provides safety net for queries between batch write and flush - Changed from: write start → write complete (~1ms) - Changed to: write start → flush() call (batch operation lifetime) IMPACT: - Fixes "Source entity not found" in v5.7.0/v5.7.1/v5.7.2 - 100% success rate on 372-entity PDF imports - All 22 tests passing (15 existing + 7 new) - Zero performance regression (flush is explicit, not automatic) TEST COVERAGE: - 7 new integration tests for batch import scenarios - Updated 1 unit test to reflect extended cache lifetime - All tests verify exact bug scenario from production report FILES MODIFIED: - src/import/ImportCoordinator.ts: Added flush after addMany - src/brainy.ts: Added type cache warming + flush cache clear - src/storage/baseStorage.ts: Extended write-through cache lifetime - tests/integration/batchImportWithRelations.test.ts: NEW (7 tests) - tests/unit/storage/writeThroughCache.test.ts: Updated 1 test WHY v5.7.2 FAILED: The write-through cache in v5.7.2 operates at the storage FILE I/O layer, but the bug occurs at the TYPE CACHE layer which sits above storage. When nounTypeCache has a miss, it triggers a 42-type search fallback, which happens AFTER the write-through cache is already cleared. v5.7.3 fixes the ACTUAL root cause: type cache synchronization.
2025-11-12 12:13:35 -08:00
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()
}
})
})