2025-09-25 14:50:52 -07:00
/ * *
* Test VFS Graph Relationships
*
* Verifies that VFS properly uses Brainy ' s graph relationships
* instead of metadata - based path queries
* /
import { describe , it , expect , beforeEach } from 'vitest'
import { VirtualFileSystem } from '../../src/vfs/VirtualFileSystem.js'
import { Brainy } from '../../src/brainy.js'
import { VerbType } from '../../src/types/graphTypes.js'
describe ( 'VFS Graph Relationships' , ( ) = > {
let vfs : VirtualFileSystem
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-09-25 14:50:52 -07:00
await brain . init ( )
vfs = new VirtualFileSystem ( brain )
await vfs . init ( )
} )
it ( 'should use proper graph relationships for directory structure' , async ( ) = > {
// Create a directory structure
await vfs . mkdir ( '/projects' )
await vfs . mkdir ( '/projects/brainy' )
await vfs . writeFile ( '/projects/brainy/README.md' , 'Test content' )
await vfs . writeFile ( '/projects/brainy/package.json' , '{}' )
// Get the entity IDs
const projectsId = await vfs . resolvePath ( '/projects' )
const brainyId = await vfs . resolvePath ( '/projects/brainy' )
const readmeId = await vfs . resolvePath ( '/projects/brainy/README.md' )
// Verify relationships are created properly
2026-06-11 14:51:00 -07:00
const projectRelations = await brain . related ( {
2025-09-25 14:50:52 -07:00
from : projectsId ,
type : VerbType . Contains
} )
expect ( projectRelations ) . toHaveLength ( 1 )
expect ( projectRelations [ 0 ] . to ) . toBe ( brainyId )
// Verify brainy directory contains its files
2026-06-11 14:51:00 -07:00
const brainyRelations = await brain . related ( {
2025-09-25 14:50:52 -07:00
from : brainyId ,
type : VerbType . Contains
} )
expect ( brainyRelations ) . toHaveLength ( 2 )
const childIds = brainyRelations . map ( r = > r . to )
expect ( childIds ) . toContain ( readmeId )
} )
it ( 'should traverse directory tree using relationships' , async ( ) = > {
// Create nested 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 file' )
// Read directory using relationships
const contents = await vfs . readdir ( '/a/b/c' )
expect ( contents ) . toContain ( 'file.txt' )
// Verify path resolution uses graph traversal
const fileId = await vfs . resolvePath ( '/a/b/c/file.txt' )
const fileEntity = await brain . get ( fileId )
expect ( fileEntity ? . metadata ? . name ) . toBe ( 'file.txt' )
} )
it ( 'should properly handle custom relationships between files' , async ( ) = > {
// Create two files
await vfs . writeFile ( '/doc1.md' , 'Document 1' )
await vfs . writeFile ( '/doc2.md' , 'Document 2' )
// Add a custom relationship
await vfs . addRelationship ( '/doc1.md' , '/doc2.md' , VerbType . References )
// Get relationships using proper graph API
const relationships = await vfs . getRelationships ( '/doc1.md' )
// Should find the reference relationship
const refRelation = relationships . find ( r = > r . type === VerbType . References )
expect ( refRelation ) . toBeDefined ( )
// Verify it's using actual graph relationships, not metadata
const doc1Id = await vfs . resolvePath ( '/doc1.md' )
const doc2Id = await vfs . resolvePath ( '/doc2.md' )
2026-06-11 14:51:00 -07:00
const directRelations = await brain . related ( {
2025-09-25 14:50:52 -07:00
from : doc1Id ,
type : VerbType . References
} )
expect ( directRelations ) . toHaveLength ( 1 )
expect ( directRelations [ 0 ] . to ) . toBe ( doc2Id )
} )
it ( 'should not fall back to metadata path queries' , async ( ) = > {
// Create a file
await vfs . writeFile ( '/test.txt' , 'test' )
// Get the entity
const entity = await vfs . getEntity ( '/test.txt' )
// Verify the entity has proper metadata
expect ( entity . metadata . path ) . toBe ( '/test.txt' )
expect ( entity . metadata . name ) . toBe ( 'test.txt' )
// But the parent relationship should be through graph, not metadata
const rootId = await vfs . resolvePath ( '/' )
const testId = entity . id
// Check that root contains test.txt via relationships
2026-06-11 14:51:00 -07:00
const rootRelations = await brain . related ( {
2025-09-25 14:50:52 -07:00
from : rootId ,
type : VerbType . Contains
} )
expect ( rootRelations . some ( r = > r . to === testId ) ) . toBe ( true )
} )
it ( 'should efficiently query children using relationships' , async ( ) = > {
// Create many files in a directory
await vfs . mkdir ( '/many' )
for ( let i = 0 ; i < 10 ; i ++ ) {
await vfs . writeFile ( ` /many/file ${ i } .txt ` , ` content ${ i } ` )
}
// Get directory contents
const files = await vfs . readdir ( '/many' )
expect ( files ) . toHaveLength ( 10 )
// Verify it's using relationships, not metadata queries
const manyId = await vfs . resolvePath ( '/many' )
2026-06-11 14:51:00 -07:00
const relations = await brain . related ( {
2025-09-25 14:50:52 -07:00
from : manyId ,
type : VerbType . Contains
} )
expect ( relations ) . toHaveLength ( 10 )
} )
it ( 'should handle moving files by updating relationships' , async ( ) = > {
// Create source structure
await vfs . mkdir ( '/source' )
await vfs . writeFile ( '/source/file.txt' , 'content' )
await vfs . mkdir ( '/dest' )
// Move file
await vfs . move ( '/source/file.txt' , '/dest/file.txt' )
// Verify relationships are updated
const sourceId = await vfs . resolvePath ( '/source' )
const destId = await vfs . resolvePath ( '/dest' )
const fileId = await vfs . resolvePath ( '/dest/file.txt' )
// Source should not contain file anymore
2026-06-11 14:51:00 -07:00
const sourceRelations = await brain . related ( {
2025-09-25 14:50:52 -07:00
from : sourceId ,
type : VerbType . Contains
} )
expect ( sourceRelations ) . toHaveLength ( 0 )
// Dest should contain file
2026-06-11 14:51:00 -07:00
const destRelations = await brain . related ( {
2025-09-25 14:50:52 -07:00
from : destId ,
type : VerbType . Contains
} )
expect ( destRelations ) . toHaveLength ( 1 )
expect ( destRelations [ 0 ] . to ) . toBe ( fileId )
} )
it ( 'should support complex graph queries' , async ( ) = > {
// Create interconnected structure
await vfs . mkdir ( '/docs' )
await vfs . writeFile ( '/docs/main.md' , 'Main doc' )
await vfs . writeFile ( '/docs/related1.md' , 'Related 1' )
await vfs . writeFile ( '/docs/related2.md' , 'Related 2' )
// Add cross-references
await vfs . addRelationship ( '/docs/main.md' , '/docs/related1.md' , VerbType . References )
await vfs . addRelationship ( '/docs/main.md' , '/docs/related2.md' , VerbType . References )
await vfs . addRelationship ( '/docs/related1.md' , '/docs/related2.md' , VerbType . References )
// Get all related documents
const related = await vfs . getRelated ( '/docs/main.md' )
// Should find parent (Contains) and references
const references = related . filter ( r = > r . direction === 'from' )
expect ( references . length ) . toBeGreaterThanOrEqual ( 2 )
} )
2025-09-26 15:12:04 -07:00
it ( 'should always create Contains relationship when writing files' , async ( ) = > {
// This test verifies the fix for the critical bug where writeFile()
// was not creating Contains relationships for updated files
// Test 1: New file should have Contains relationship
await vfs . writeFile ( '/test-new.txt' , 'Hello World' )
const rootId = await vfs . resolvePath ( '/' )
const fileEntityId = await vfs . resolvePath ( '/test-new.txt' )
// Check that Contains relationship exists
2026-06-11 14:51:00 -07:00
const relations = await brain . related ( {
2025-09-26 15:12:04 -07:00
from : rootId ,
to : fileEntityId ,
type : VerbType . Contains
} )
expect ( relations ) . toHaveLength ( 1 )
expect ( relations [ 0 ] . type ) . toBe ( VerbType . Contains )
// Verify readdir returns the file
const files = await vfs . readdir ( '/' )
expect ( files ) . toContain ( 'test-new.txt' )
// Test 2: Updated file should maintain Contains relationship
await vfs . writeFile ( '/test-new.txt' , 'Updated content' )
// Relationship should still exist after update
2026-06-11 14:51:00 -07:00
const relationsAfterUpdate = await brain . related ( {
2025-09-26 15:12:04 -07:00
from : rootId ,
to : fileEntityId ,
type : VerbType . Contains
} )
expect ( relationsAfterUpdate ) . toHaveLength ( 1 )
// readdir should still work
const filesAfterUpdate = await vfs . readdir ( '/' )
expect ( filesAfterUpdate ) . toContain ( 'test-new.txt' )
// Test 3: Multiple updates should not create duplicate relationships
await vfs . writeFile ( '/test-new.txt' , 'Another update' )
await vfs . writeFile ( '/test-new.txt' , 'Yet another update' )
2026-06-11 14:51:00 -07:00
const finalRelations = await brain . related ( {
2025-09-26 15:12:04 -07:00
from : rootId ,
to : fileEntityId ,
type : VerbType . Contains
} )
// Should still have exactly one Contains relationship
expect ( finalRelations ) . toHaveLength ( 1 )
} )
it ( 'should repair missing Contains relationships on file update' , async ( ) = > {
// This test simulates a scenario where a file exists but its Contains
// relationship is missing (could happen due to corruption or bugs)
// Create a file normally first
await vfs . writeFile ( '/orphan-test.txt' , 'Initial content' )
const rootId = await vfs . resolvePath ( '/' )
const fileEntityId = await vfs . resolvePath ( '/orphan-test.txt' )
// Manually delete the Contains relationship to simulate the bug
2026-06-11 14:51:00 -07:00
const initialRelations = await brain . related ( {
2025-09-26 15:12:04 -07:00
from : rootId ,
to : fileEntityId ,
type : VerbType . Contains
} )
// Delete the relationship (simulating the corruption/bug)
for ( const rel of initialRelations ) {
await brain . unrelate ( rel . id )
}
// Verify the relationship is gone
2026-06-11 14:51:00 -07:00
const brokenRelations = await brain . related ( {
2025-09-26 15:12:04 -07:00
from : rootId ,
to : fileEntityId ,
type : VerbType . Contains
} )
expect ( brokenRelations ) . toHaveLength ( 0 )
// readdir should fail to find the file (the bug symptom)
const brokenList = await vfs . readdir ( '/' )
expect ( brokenList ) . not . toContain ( 'orphan-test.txt' )
// Now update the file - this should repair the missing relationship
await vfs . writeFile ( '/orphan-test.txt' , 'Fixed content' )
// Verify the relationship is restored
2026-06-11 14:51:00 -07:00
const fixedRelations = await brain . related ( {
2025-09-26 15:12:04 -07:00
from : rootId ,
to : fileEntityId ,
type : VerbType . Contains
} )
expect ( fixedRelations ) . toHaveLength ( 1 )
expect ( fixedRelations [ 0 ] . type ) . toBe ( VerbType . Contains )
// readdir should now work again
const fixedList = await vfs . readdir ( '/' )
expect ( fixedList ) . toContain ( 'orphan-test.txt' )
} )
it ( 'should create Contains relationships for files in nested directories' , async ( ) = > {
// Test that the fix works for nested directory structures
await vfs . mkdir ( '/level1' )
await vfs . mkdir ( '/level1/level2' )
await vfs . mkdir ( '/level1/level2/level3' )
// Write a file deep in the structure
await vfs . writeFile ( '/level1/level2/level3/deep.txt' , 'Deep content' )
// Get entity IDs
const level3Id = await vfs . resolvePath ( '/level1/level2/level3' )
const fileEntityId = await vfs . resolvePath ( '/level1/level2/level3/deep.txt' )
// Verify Contains relationship exists
2026-06-11 14:51:00 -07:00
const relations = await brain . related ( {
2025-09-26 15:12:04 -07:00
from : level3Id ,
to : fileEntityId ,
type : VerbType . Contains
} )
expect ( relations ) . toHaveLength ( 1 )
// Verify readdir works at the deep level
const files = await vfs . readdir ( '/level1/level2/level3' )
expect ( files ) . toContain ( 'deep.txt' )
// Update the file and verify relationship persists
await vfs . writeFile ( '/level1/level2/level3/deep.txt' , 'Updated deep content' )
2026-06-11 14:51:00 -07:00
const relationsAfterUpdate = await brain . related ( {
2025-09-26 15:12:04 -07:00
from : level3Id ,
to : fileEntityId ,
type : VerbType . Contains
} )
expect ( relationsAfterUpdate ) . toHaveLength ( 1 )
// readdir should still work
const filesAfterUpdate = await vfs . readdir ( '/level1/level2/level3' )
expect ( filesAfterUpdate ) . toContain ( 'deep.txt' )
} )
2025-09-25 14:50:52 -07:00
} )