2025-12-04 11:22:30 -08:00
/ * *
* VFS mkdir ( ) Bug Fix Test ( v6 . 2.9 )
*
* Tests for the bug where vfs . mkdir ( ) caused previously created directories
* to disappear from vfs . readdir ( '/' ) .
*
* Root cause : The verbCountsByType optimization skipped verb types with count 0 ,
* which could happen when statistics were stale after restart .
*
* Fix :
* 1 . Option A : Never skip a verb type that ' s explicitly requested in the filter
* 2 . Option B : Added fast path for sourceId + verbType combo ( common VFS pattern )
*
2026-06-11 14:51:00 -07:00
* Originally reported by a downstream application using the VFS .
2025-12-04 11:22:30 -08:00
* /
import { describe , it , expect , beforeEach , afterEach } from 'vitest'
import { Brainy , VerbType } from '../../../src/index.js'
describe ( 'VFS mkdir() Bug Fix (v6.2.9)' , ( ) = > {
let brain : Brainy
beforeEach ( async ( ) = > {
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-12-04 11:22:30 -08:00
storage : { type : 'memory' }
} )
await brain . init ( )
} )
afterEach ( async ( ) = > {
await brain . close ( )
} )
describe ( 'mkdir() should not corrupt VFS index' , ( ) = > {
it ( 'should show all directories after mkdir() is called' , async ( ) = > {
const vfs = brain . vfs
await vfs . init ( )
// Step 1: Create directory via writeFile (auto-creates parent directories)
await vfs . writeFile ( '/Personas/user.md' , 'content' )
console . log ( 'Created /Personas/user.md' )
// Verify /Personas is visible
const entries1 = await vfs . readdir ( '/' , { withFileTypes : true } )
const names1 = entries1 . map ( ( e : any ) = > e . name )
console . log ( 'After writeFile - root entries:' , names1 )
expect ( names1 ) . toContain ( 'Personas' )
// Step 2: Create another directory via mkdir
await vfs . mkdir ( '/apps/my-app' , { recursive : true } )
console . log ( 'Created /apps/my-app' )
// Step 3: Verify BOTH directories are visible (THIS IS THE BUG)
const entries2 = await vfs . readdir ( '/' , { withFileTypes : true } )
const names2 = entries2 . map ( ( e : any ) = > e . name )
console . log ( 'After mkdir - root entries:' , names2 )
// This assertion failed before the fix
expect ( names2 ) . toContain ( 'Personas' )
expect ( names2 ) . toContain ( 'apps' )
expect ( names2 . length ) . toBe ( 2 )
} )
it ( 'should handle multiple nested directories' , async ( ) = > {
const vfs = brain . vfs
await vfs . init ( )
// Create multiple directories via different methods
await vfs . writeFile ( '/docs/readme.md' , 'readme' )
await vfs . mkdir ( '/config' , { recursive : true } )
await vfs . writeFile ( '/data/users/user1.json' , '{}' )
await vfs . mkdir ( '/logs/archive' , { recursive : true } )
// Verify all root directories are visible
const entries = await vfs . readdir ( '/' , { withFileTypes : true } )
const names = entries . map ( ( e : any ) = > e . name )
console . log ( 'All root entries:' , names )
expect ( names ) . toContain ( 'docs' )
expect ( names ) . toContain ( 'config' )
expect ( names ) . toContain ( 'data' )
expect ( names ) . toContain ( 'logs' )
expect ( names . length ) . toBe ( 4 )
} )
} )
2026-06-11 14:51:00 -07:00
describe ( 'related should query Contains relationships correctly' , ( ) = > {
2025-12-04 11:22:30 -08:00
it ( 'should find Contains relationships via sourceId + verbType filter' , async ( ) = > {
// Create some entities and relationships
const parent = await brain . add ( {
data : 'Parent entity' ,
type : 'collection' ,
metadata : { name : 'parent' }
} )
const child1 = await brain . add ( {
data : 'Child 1' ,
type : 'document' ,
metadata : { name : 'child1' }
} )
const child2 = await brain . add ( {
data : 'Child 2' ,
type : 'document' ,
metadata : { name : 'child2' }
} )
// Create Contains relationships
await brain . relate ( { from : parent , to : child1 , type : VerbType . Contains } )
await brain . relate ( { from : parent , to : child2 , type : VerbType . Contains } )
// Query relationships (this is what getChildren() does)
2026-06-11 14:51:00 -07:00
const relations = await brain . related ( {
2025-12-04 11:22:30 -08:00
from : parent ,
type : VerbType . Contains
} )
console . log ( 'Relations found:' , relations . length )
expect ( relations . length ) . toBe ( 2 )
expect ( relations . map ( r = > r . to ) ) . toContain ( child1 )
expect ( relations . map ( r = > r . to ) ) . toContain ( child2 )
} )
it ( 'should return relationships even with mixed verb types' , async ( ) = > {
// Create entities
const entityA = await brain . add ( { data : 'Entity A' , type : 'concept' } )
const entityB = await brain . add ( { data : 'Entity B' , type : 'concept' } )
const entityC = await brain . add ( { data : 'Entity C' , type : 'concept' } )
// Create different types of relationships
await brain . relate ( { from : entityA , to : entityB , type : VerbType . Contains } )
await brain . relate ( { from : entityA , to : entityC , type : VerbType . RelatedTo } )
// Query only Contains relationships
2026-06-11 14:51:00 -07:00
const containsRelations = await brain . related ( {
2025-12-04 11:22:30 -08:00
from : entityA ,
type : VerbType . Contains
} )
expect ( containsRelations . length ) . toBe ( 1 )
expect ( containsRelations [ 0 ] . to ) . toBe ( entityB )
// Query only RelatedTo relationships
2026-06-11 14:51:00 -07:00
const relatedRelations = await brain . related ( {
2025-12-04 11:22:30 -08:00
from : entityA ,
type : VerbType . RelatedTo
} )
expect ( relatedRelations . length ) . toBe ( 1 )
expect ( relatedRelations [ 0 ] . to ) . toBe ( entityC )
} )
} )
describe ( 'Fast path for sourceId + verbType filter' , ( ) = > {
it ( 'should use fast path for VFS-style queries' , async ( ) = > {
// Create parent and children
const parent = await brain . add ( {
data : 'Parent' ,
type : 'collection' ,
metadata : { name : 'parent' }
} )
// Create many children to verify performance
const children : string [ ] = [ ]
for ( let i = 0 ; i < 10 ; i ++ ) {
const child = await brain . add ( {
data : ` Child ${ i } ` ,
type : 'document' ,
metadata : { name : ` child ${ i } ` }
} )
children . push ( child )
await brain . relate ( { from : parent , to : child , type : VerbType . Contains } )
}
// Query should use the new fast path
const startTime = performance . now ( )
2026-06-11 14:51:00 -07:00
const relations = await brain . related ( {
2025-12-04 11:22:30 -08:00
from : parent ,
type : VerbType . Contains
} )
const elapsed = performance . now ( ) - startTime
console . log ( ` Query returned ${ relations . length } relations in ${ elapsed . toFixed ( 2 ) } ms ` )
expect ( relations . length ) . toBe ( 10 )
// Verify all children are found
for ( const child of children ) {
expect ( relations . map ( r = > r . to ) ) . toContain ( child )
}
} )
} )
describe ( 'Delete and recreate folder (v6.2.9 cache invalidation fix)' , ( ) = > {
it ( 'should handle delete folder → recreate folder without corruption' , async ( ) = > {
const vfs = brain . vfs
await vfs . init ( )
// Step 1: Create a folder with content
await vfs . mkdir ( '/test-folder' , { recursive : true } )
await vfs . writeFile ( '/test-folder/file1.txt' , 'content1' )
console . log ( 'Created /test-folder with file1.txt' )
// Verify folder exists
const entries1 = await vfs . readdir ( '/' , { withFileTypes : true } )
expect ( entries1 . map ( ( e : any ) = > e . name ) ) . toContain ( 'test-folder' )
// Step 2: Delete the folder
await vfs . rmdir ( '/test-folder' , { recursive : true } )
console . log ( 'Deleted /test-folder' )
// Verify folder is gone
const entries2 = await vfs . readdir ( '/' , { withFileTypes : true } )
expect ( entries2 . map ( ( e : any ) = > e . name ) ) . not . toContain ( 'test-folder' )
// Step 3: Recreate the folder (THIS IS WHERE THE BUG WAS)
await vfs . mkdir ( '/test-folder' , { recursive : true } )
console . log ( 'Recreated /test-folder' )
// Step 4: Create new content
await vfs . writeFile ( '/test-folder/file2.txt' , 'content2' )
console . log ( 'Created /test-folder/file2.txt' )
// Step 5: Verify everything works
const entries3 = await vfs . readdir ( '/' , { withFileTypes : true } )
expect ( entries3 . map ( ( e : any ) = > e . name ) ) . toContain ( 'test-folder' )
const folderContents = await vfs . readdir ( '/test-folder' , { withFileTypes : true } )
expect ( folderContents . map ( ( e : any ) = > e . name ) ) . toContain ( 'file2.txt' )
console . log ( 'Folder recreate test passed!' )
} )
it ( 'should handle multiple delete/recreate cycles' , async ( ) = > {
const vfs = brain . vfs
await vfs . init ( )
for ( let i = 0 ; i < 3 ; i ++ ) {
// Create
await vfs . mkdir ( '/cycle-test' , { recursive : true } )
await vfs . writeFile ( ` /cycle-test/file ${ i } .txt ` , ` content ${ i } ` )
// Verify
const contents = await vfs . readdir ( '/cycle-test' , { withFileTypes : true } )
expect ( contents . length ) . toBe ( 1 )
expect ( contents [ 0 ] . name ) . toBe ( ` file ${ i } .txt ` )
// Delete
await vfs . rmdir ( '/cycle-test' , { recursive : true } )
console . log ( ` Cycle ${ i + 1 } completed ` )
}
} )
} )
} )