brainy/tests/vfs/tree-operations.test.ts
David Snelling 780fb6444b 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

337 lines
No EOL
12 KiB
TypeScript

/**
* 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 () => {
brain = new Brainy({ requireSubtype: false })
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)
})
})
})