2025-11-12 09:32:52 -08:00
/ * *
* 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 ) } ` )
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 ,
2025-11-12 09:32:52 -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 {
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 . getRelations ( 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 . getRelations ( 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.delete()' , 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 . delete ( 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 . getRelations ( documentId )
expect ( docRelations . length ) . toBe ( 3 )
const concept1Relations = await brain . getRelations ( concept1Id )
expect ( concept1Relations . length ) . toBeGreaterThan ( 0 )
const concept2Relations = await brain . getRelations ( 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 . getRelations ( documentId )
expect ( relations . length ) . toBe ( 10 )
} )
} )