brainy/tests/unit/vfs-multi-instance-diagnostic.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

171 lines
7.2 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* VFS Multi-instance Diagnostic Test
*
* Tests to verify VFS import behavior and identify if VFS creates only wrappers or also graph entities
*/
import { describe, it, expect, beforeEach } from 'vitest'
import { Brainy, NounType } from '../../src/index.js'
describe('VFS Multi-instance Diagnostic', () => {
let brain: Brainy
beforeEach(async () => {
brain = new Brainy({ requireSubtype: false,
storage: { type: 'memory' }
})
await brain.init()
})
it('should verify VFS creates document wrappers AND allows entity filtering', async () => {
console.log('\n🔬 VFS Multi-instance Diagnostic Test\n')
console.log('='.repeat(70))
// Step 1: Add entities directly (control group)
console.log('\n1⃣ Adding entities directly (without VFS)...\n')
await brain.add({ data: 'Person 1', type: NounType.Person, metadata: { name: 'Person 1' } })
await brain.add({ data: 'Person 2', type: NounType.Person, metadata: { name: 'Person 2' } })
await brain.add({ data: 'Location 1', type: NounType.Location, metadata: { name: 'Location 1' } })
const beforeVfs = await brain.find({ limit: 100 })
console.log(` Total entities: ${beforeVfs.length}`)
const peopleBefore = await brain.find({ type: NounType.Person, limit: 100 })
console.log(` Person filter: ${peopleBefore.length} (expected: 2)`)
expect(peopleBefore.length).toBe(2)
console.log(' ✅ Type filtering works on direct entities\n')
// Step 2: Use VFS to create files
console.log('2⃣ Creating VFS files...\n')
const vfs = brain.vfs
await vfs.init()
await vfs.mkdir('/test', { recursive: true })
// Create a VFS file with entity data
const personData = {
id: 'ent_person_test',
name: 'John Smith',
type: 'person',
metadata: { source: 'test' }
}
await vfs.writeFile('/test/john.json', Buffer.from(JSON.stringify(personData, null, 2)))
console.log(' Created VFS file: /test/john.json')
// Step 3: Check what entities exist now
console.log('\n3⃣ Analyzing entities after VFS...\n')
const afterVfs = await brain.find({ limit: 100 })
console.log(` Total entities: ${afterVfs.length}`)
// Count by type
const typeCounts: Record<string, number> = {}
for (const result of afterVfs) {
const type = result.type || 'unknown'
typeCounts[type] = (typeCounts[type] || 0) + 1
}
console.log('\n Entity type breakdown:')
for (const [type, count] of Object.entries(typeCounts)) {
console.log(` - ${type}: ${count}`)
}
// Count VFS wrappers vs regular entities
const vfsWrappers = afterVfs.filter(e => e.metadata?.vfsType === 'file')
const regularEntities = afterVfs.filter(e => !e.metadata?.vfsType)
console.log(`\n VFS wrappers: ${vfsWrappers.length}`)
console.log(` Regular entities: ${regularEntities.length}`)
// Step 4: Test type filtering after VFS
console.log('\n4⃣ Testing type filtering after VFS...\n')
const peopleAfter = await brain.find({ type: NounType.Person, limit: 100 })
console.log(` Person filter: ${peopleAfter.length} (expected: 2 - same as before)`)
const documents = await brain.find({ type: NounType.Document, limit: 100 })
console.log(` Document filter: ${documents.length} (expected: ${vfsWrappers.length})`)
// Step 5: Analyze VFS wrapper structure
console.log('\n5⃣ Analyzing VFS wrapper structure...\n')
const wrapper = vfsWrappers[0]
if (wrapper) {
console.log(' VFS Wrapper Entity:')
console.log(` - ID: ${wrapper.id}`)
console.log(` - Type: ${wrapper.type}`)
console.log(` - VFS Type: ${wrapper.metadata?.vfsType}`)
console.log(` - Path: ${wrapper.metadata?.path}`)
console.log(` - Has rawData: ${!!wrapper.metadata?.rawData}`)
if (wrapper.metadata?.rawData) {
const decoded = Buffer.from(wrapper.metadata.rawData, 'base64').toString()
const embedded = JSON.parse(decoded)
console.log(`\n Embedded Entity Data:`)
console.log(` - Name: ${embedded.name}`)
console.log(` - Type: ${embedded.type}`)
console.log(`\n 🔍 KEY FINDING:`)
console.log(` Wrapper type: "${wrapper.type}"`)
console.log(` Embedded type: "${embedded.type}"`)
console.log(` Filtering by type="${embedded.type}" searches wrapper type, not embedded!`)
}
}
// Step 6: Diagnosis
console.log('\n' + '='.repeat(70))
console.log('📋 DIAGNOSIS\n')
if (peopleAfter.length === peopleBefore.length) {
console.log('✅ VFS does NOT create duplicate graph entities')
console.log('✅ VFS only creates document wrappers')
console.log('✅ Type filtering works on original entities, ignores VFS wrappers')
console.log('\nThis means:')
console.log(' - VFS files are type="document" wrappers')
console.log(' - Original entities keep their types')
console.log(' - filter({ type: "person" }) returns original entities only')
} else {
console.log('❌ Unexpected behavior - VFS may have created additional entities')
}
console.log('\n' + '='.repeat(70) + '\n')
// Assertions
expect(peopleAfter.length).toBe(2) // Should still be 2, VFS doesn't create person entities
expect(documents.length).toBeGreaterThan(0) // VFS creates document wrappers
expect(vfsWrappers.length).toBeGreaterThan(0) // Should have VFS wrappers
})
it('should verify import creates BOTH VFS wrappers AND graph entities', async () => {
// This test would require creating a test Excel file and running import
// For now, we'll document the expected behavior based on code analysis
console.log('\n📚 Expected Import Behavior (from code analysis):\n')
console.log('When you run brain.import("file.xlsx", { vfsPath: "/imports" }):')
console.log('\n1. ImportCoordinator.execute() calls:')
console.log(' a) vfsGenerator.generate() - creates VFS file wrappers')
console.log(' - Each entity → JSON file in VFS')
console.log(' - Wrapper entity with type="document"')
console.log(' - Entity data stored in metadata.rawData (base64)')
console.log('')
console.log(' b) createGraphEntities() - creates graph entities')
console.log(' - Each entity → graph entity with proper type')
console.log(' - type="person", "location", "concept", etc.')
console.log(' - metadata.vfsPath points to VFS file')
console.log('')
console.log('2. Result: Database contains BOTH:')
console.log(' - VFS wrappers (type="document", vfsType="file")')
console.log(' - Graph entities (type="person", etc., vfsPath set)')
console.log('')
console.log('3. Type filtering:')
console.log(' - filter({ type: "person" }) → returns graph entities')
console.log(' - filter({ type: "document" }) → returns VFS wrappers')
console.log('')
console.log('If a consumer gets 0 results, likely causes:')
console.log(' ❌ Only VFS wrappers created (createEntities: false)')
console.log(' ❌ Import not completing before query')
console.log(' ❌ Querying different Brainy instance')
console.log('')
})
})