2025-09-26 10:17:59 -07:00
/ * *
* VFS Tree Operations Tests
* Ensures tree methods prevent recursion and work correctly
* /
import { describe , it , expect , beforeEach } from 'vitest'
import { Brainy } from '../../src/brainy.js'
import { VirtualFileSystem } from '../../src/vfs/VirtualFileSystem.js'
import { VFSTreeUtils } from '../../src/vfs/TreeUtils.js'
describe ( 'VFS Tree Operations' , ( ) = > {
let brain : Brainy
let vfs : VirtualFileSystem
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-09-26 10:17:59 -07:00
await brain . init ( {
storage : {
type : 'memory' // Use in-memory storage for tests
}
} )
vfs = new VirtualFileSystem ( brain )
await vfs . init ( )
} )
describe ( 'Critical: No Self-Inclusion Bug' , ( ) = > {
it ( 'should NEVER return a directory as its own child' , async ( ) = > {
// Create test structure
await vfs . mkdir ( '/test-dir' )
await vfs . writeFile ( '/test-dir/file1.txt' , 'content1' )
await vfs . writeFile ( '/test-dir/file2.txt' , 'content2' )
await vfs . mkdir ( '/test-dir/subdir' )
// Test getDirectChildren - should NOT include /test-dir itself
const children = await vfs . getDirectChildren ( '/test-dir' )
// Critical assertion - directory should NOT be in its own children
const selfIncluded = children . some ( child = > child . metadata . path === '/test-dir' )
expect ( selfIncluded ) . toBe ( false )
// Should have exactly 3 children
expect ( children . length ) . toBe ( 3 )
expect ( children . map ( c = > c . metadata . name ) . sort ( ) ) . toEqual ( [ 'file1.txt' , 'file2.txt' , 'subdir' ] )
} )
it ( 'should handle root directory correctly' , async ( ) = > {
await vfs . mkdir ( '/dir1' )
await vfs . mkdir ( '/dir2' )
const rootChildren = await vfs . getDirectChildren ( '/' )
// Root should not be in its own children
const rootInChildren = rootChildren . some ( child = > child . metadata . path === '/' )
expect ( rootInChildren ) . toBe ( false )
expect ( rootChildren . length ) . toBe ( 2 )
} )
it ( 'should prevent recursion in tree structure' , async ( ) = > {
// Create deeper structure
await vfs . mkdir ( '/a' )
await vfs . mkdir ( '/a/b' )
await vfs . mkdir ( '/a/b/c' )
await vfs . writeFile ( '/a/b/c/file.txt' , 'deep' )
const tree = await vfs . getTreeStructure ( '/a' )
// Validate no cycles
const validation = VFSTreeUtils . validateTree ( tree )
expect ( validation . valid ) . toBe ( true )
expect ( validation . errors ) . toHaveLength ( 0 )
// Check tree structure
expect ( tree . path ) . toBe ( '/a' )
expect ( tree . children ) . toBeDefined ( )
expect ( tree . children ! . length ) . toBe ( 1 ) // Only 'b'
expect ( tree . children ! [ 0 ] . name ) . toBe ( 'b' )
expect ( tree . children ! [ 0 ] . children ! . length ) . toBe ( 1 ) // Only 'c'
expect ( tree . children ! [ 0 ] . children ! [ 0 ] . name ) . toBe ( 'c' )
} )
} )
describe ( 'getDirectChildren' , ( ) = > {
it ( 'should return only immediate children' , async ( ) = > {
await vfs . mkdir ( '/parent' )
await vfs . mkdir ( '/parent/child1' )
await vfs . mkdir ( '/parent/child2' )
await vfs . mkdir ( '/parent/child1/grandchild' )
await vfs . writeFile ( '/parent/file.txt' , 'test' )
const children = await vfs . getDirectChildren ( '/parent' )
expect ( children . length ) . toBe ( 3 ) // child1, child2, file.txt
expect ( children . map ( c = > c . metadata . name ) . sort ( ) ) . toEqual ( [ 'child1' , 'child2' , 'file.txt' ] )
// Should NOT include grandchild
const hasGrandchild = children . some ( c = > c . metadata . name === 'grandchild' )
expect ( hasGrandchild ) . toBe ( false )
} )
it ( 'should throw error for non-directory' , async ( ) = > {
await vfs . writeFile ( '/file.txt' , 'content' )
await expect ( vfs . getDirectChildren ( '/file.txt' ) ) . rejects . toThrow ( 'Not a directory' )
} )
} )
describe ( 'getTreeStructure' , ( ) = > {
it ( 'should build correct tree with depth limit' , async ( ) = > {
// Create multi-level structure
await vfs . mkdir ( '/root' )
await vfs . mkdir ( '/root/level1' )
await vfs . mkdir ( '/root/level1/level2' )
await vfs . mkdir ( '/root/level1/level2/level3' )
await vfs . writeFile ( '/root/level1/level2/level3/deep.txt' , 'very deep' )
const tree = await vfs . getTreeStructure ( '/root' , { maxDepth : 2 } )
expect ( tree . children ) . toBeDefined ( )
expect ( tree . children ! [ 0 ] . name ) . toBe ( 'level1' )
expect ( tree . children ! [ 0 ] . children ! [ 0 ] . name ) . toBe ( 'level2' )
// Level 3 should be cut off due to maxDepth
expect ( tree . children ! [ 0 ] . children ! [ 0 ] . children ) . toBeUndefined ( )
} )
it ( 'should sort tree nodes correctly' , async ( ) = > {
await vfs . mkdir ( '/sorted' )
await vfs . writeFile ( '/sorted/zebra.txt' , 'z' )
await vfs . writeFile ( '/sorted/apple.txt' , 'a' )
await vfs . mkdir ( '/sorted/banana' )
await vfs . mkdir ( '/sorted/cherry' )
const tree = await vfs . getTreeStructure ( '/sorted' , { sort : 'name' } )
// Directories should come first, then files, both sorted by name
const names = tree . children ! . map ( c = > c . name )
expect ( names ) . toEqual ( [ 'banana' , 'cherry' , 'apple.txt' , 'zebra.txt' ] )
} )
it ( 'should filter hidden files' , async ( ) = > {
await vfs . mkdir ( '/hidden-test' )
await vfs . writeFile ( '/hidden-test/.hidden' , 'secret' )
await vfs . writeFile ( '/hidden-test/visible.txt' , 'public' )
await vfs . mkdir ( '/hidden-test/.secret-dir' )
const tree = await vfs . getTreeStructure ( '/hidden-test' , { includeHidden : false } )
expect ( tree . children ! . length ) . toBe ( 1 )
expect ( tree . children ! [ 0 ] . name ) . toBe ( 'visible.txt' )
} )
} )
describe ( 'getDescendants' , ( ) = > {
it ( 'should return all descendants flat' , async ( ) = > {
await vfs . mkdir ( '/desc' )
await vfs . mkdir ( '/desc/a' )
await vfs . mkdir ( '/desc/a/b' )
await vfs . writeFile ( '/desc/a/b/file.txt' , 'deep' )
await vfs . writeFile ( '/desc/file1.txt' , 'top' )
const descendants = await vfs . getDescendants ( '/desc' )
expect ( descendants . length ) . toBe ( 4 ) // a, a/b, a/b/file.txt, file1.txt
// Should NOT include /desc itself by default
const hasSelf = descendants . some ( d = > d . metadata . path === '/desc' )
expect ( hasSelf ) . toBe ( false )
} )
it ( 'should include ancestor when requested' , async ( ) = > {
await vfs . mkdir ( '/ancestor' )
await vfs . mkdir ( '/ancestor/child' )
const withAncestor = await vfs . getDescendants ( '/ancestor' , { includeAncestor : true } )
const withoutAncestor = await vfs . getDescendants ( '/ancestor' , { includeAncestor : false } )
expect ( withAncestor . length ) . toBe ( 2 ) // ancestor + child
expect ( withoutAncestor . length ) . toBe ( 1 ) // only child
} )
it ( 'should filter by type' , async ( ) = > {
await vfs . mkdir ( '/typed' )
await vfs . mkdir ( '/typed/dir1' )
await vfs . mkdir ( '/typed/dir2' )
await vfs . writeFile ( '/typed/file1.txt' , 'f1' )
await vfs . writeFile ( '/typed/file2.txt' , 'f2' )
const dirsOnly = await vfs . getDescendants ( '/typed' , { type : 'directory' } )
const filesOnly = await vfs . getDescendants ( '/typed' , { type : 'file' } )
expect ( dirsOnly . length ) . toBe ( 2 )
expect ( filesOnly . length ) . toBe ( 2 )
expect ( dirsOnly . every ( d = > d . metadata . vfsType === 'directory' ) ) . toBe ( true )
expect ( filesOnly . every ( f = > f . metadata . vfsType === 'file' ) ) . toBe ( true )
} )
} )
describe ( 'inspect' , ( ) = > {
it ( 'should return comprehensive information' , async ( ) = > {
await vfs . mkdir ( '/inspect-test' )
await vfs . mkdir ( '/inspect-test/child1' )
await vfs . writeFile ( '/inspect-test/file.txt' , 'content' )
const result = await vfs . inspect ( '/inspect-test/child1' )
expect ( result . node . metadata . name ) . toBe ( 'child1' )
expect ( result . node . metadata . path ) . toBe ( '/inspect-test/child1' )
expect ( result . children ) . toEqual ( [ ] ) // Empty directory
expect ( result . parent ) . toBeDefined ( )
expect ( result . parent ! . metadata . path ) . toBe ( '/inspect-test' )
expect ( result . stats ) . toBeDefined ( )
expect ( result . stats . isDirectory ( ) ) . toBe ( true )
} )
it ( 'should handle root directory specially' , async ( ) = > {
await vfs . mkdir ( '/root-child' )
const result = await vfs . inspect ( '/' )
expect ( result . node . metadata . path ) . toBe ( '/' )
expect ( result . parent ) . toBeNull ( ) // Root has no parent
expect ( result . children . length ) . toBeGreaterThan ( 0 )
expect ( result . stats . isDirectory ( ) ) . toBe ( true )
} )
} )
describe ( 'VFSTreeUtils' , ( ) = > {
it ( 'should validate tree structure correctly' , async ( ) = > {
// Create a valid tree
await vfs . mkdir ( '/valid' )
await vfs . mkdir ( '/valid/sub1' )
await vfs . mkdir ( '/valid/sub2' )
const tree = await vfs . getTreeStructure ( '/valid' )
const validation = VFSTreeUtils . validateTree ( tree )
expect ( validation . valid ) . toBe ( true )
expect ( validation . errors ) . toHaveLength ( 0 )
} )
it ( 'should detect cycles in tree' , ( ) = > {
// Manually create an invalid tree with cycle
const childNode : any = {
name : 'child' ,
path : '/root/child' ,
type : 'directory' as const ,
children : [ ]
}
const invalidTree = {
name : 'root' ,
path : '/root' ,
type : 'directory' as const ,
children : [ childNode ]
}
// Create a cycle by adding the child back to itself
childNode . children . push ( childNode ) // Child contains itself = cycle
const validation = VFSTreeUtils . validateTree ( invalidTree )
expect ( validation . valid ) . toBe ( false )
expect ( validation . errors . length ) . toBeGreaterThan ( 0 )
// The error could be either "Cycle detected" or "Directory contains itself"
const hasExpectedError = validation . errors . some ( e = >
e . includes ( 'Cycle detected' ) || e . includes ( 'Directory contains itself' )
)
expect ( hasExpectedError ) . toBe ( true )
} )
it ( 'should detect self-inclusion' , ( ) = > {
const badTree = {
name : 'dir' ,
path : '/dir' ,
type : 'directory' as const ,
children : [ {
name : 'dir' ,
path : '/dir' , // Same as parent!
type : 'directory' as const
} ]
}
const validation = VFSTreeUtils . validateTree ( badTree )
expect ( validation . valid ) . toBe ( false )
expect ( validation . errors ) . toContainEqual ( 'Directory contains itself: /dir' )
} )
it ( 'should calculate tree statistics' , async ( ) = > {
await vfs . mkdir ( '/stats' )
await vfs . mkdir ( '/stats/dir1' )
await vfs . mkdir ( '/stats/dir2' )
await vfs . writeFile ( '/stats/file1.txt' , 'a' . repeat ( 100 ) )
await vfs . writeFile ( '/stats/dir1/file2.txt' , 'b' . repeat ( 200 ) )
const tree = await vfs . getTreeStructure ( '/stats' )
const stats = VFSTreeUtils . getTreeStats ( tree )
expect ( stats . totalNodes ) . toBe ( 5 ) // /stats (root), dir1, dir2, file1, file2
expect ( stats . directories ) . toBe ( 3 ) // /stats, dir1, dir2
expect ( stats . files ) . toBe ( 2 )
expect ( stats . maxDepth ) . toBe ( 2 )
expect ( stats . totalSize ) . toBe ( 300 )
} )
} )
describe ( 'Performance with large trees' , ( ) = > {
it ( 'should handle large directory structures efficiently' , async ( ) = > {
// Create a reasonably large structure
const dirs = 10
const filesPerDir = 5
await vfs . mkdir ( '/perf-test' )
for ( let i = 0 ; i < dirs ; i ++ ) {
await vfs . mkdir ( ` /perf-test/dir ${ i } ` )
for ( let j = 0 ; j < filesPerDir ; j ++ ) {
await vfs . writeFile ( ` /perf-test/dir ${ i } /file ${ j } .txt ` , ` content- ${ i } - ${ j } ` )
}
}
const startTime = Date . now ( )
const tree = await vfs . getTreeStructure ( '/perf-test' )
const elapsed = Date . now ( ) - startTime
// Should complete reasonably fast (under 1 second for this size)
expect ( elapsed ) . toBeLessThan ( 1000 )
// Validate structure
expect ( tree . children ! . length ) . toBe ( dirs )
expect ( tree . children ! [ 0 ] . children ! . length ) . toBe ( filesPerDir )
// No cycles
const validation = VFSTreeUtils . validateTree ( tree )
expect ( validation . valid ) . toBe ( true )
} )
} )
} )