brainy/tests/integration/phase3TypeFirstQuery.integration.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

229 lines
6.6 KiB
TypeScript

/**
* Phase 3 Integration Tests - Type-First Query Optimization
*
* End-to-end tests verifying Phase 3 works with the complete Brainy system
* Target: 8 tests covering real-world scenarios
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { Brainy } from '../../src/brainy.js'
import { NounType } from '../../src/types/graphTypes.js'
import { TypeAwareHNSWIndex } from '../../src/hnsw/typeAwareHNSWIndex.js'
describe('Phase 3: Type-First Query Optimization - Integration', () => {
let brainy: Brainy<any>
beforeEach(async () => {
// Initialize with memory storage and TypeAwareHNSWIndex
brainy = new Brainy({ requireSubtype: false,
name: 'phase3-test',
dimension: 384,
storage: {
type: 'memory' // Use memory for fast tests
},
index: {
M: 16,
efConstruction: 200,
efSearch: 50
},
debug: false // Disable debug logging for tests
})
await brainy.initialize()
})
afterEach(async () => {
// Clean up
await brainy.close()
})
// ========== Basic Type Inference Tests (3 tests) ==========
describe('Basic Type Inference', () => {
it('should automatically infer Person type from "engineer" query', async () => {
// Add test data
await brainy.add({
type: NounType.Person,
data: { name: 'Alice', role: 'engineer' }
})
await brainy.add({
type: NounType.Document,
data: { title: 'Engineering Guide' }
})
// Query with natural language
const results = await brainy.find('Find engineers')
// Should find Person, not Document
expect(results.length).toBeGreaterThan(0)
// Verify TypeAwareHNSWIndex is being used
expect((brainy as any).index).toBeInstanceOf(TypeAwareHNSWIndex)
})
it('should handle queries with explicit type override', async () => {
await brainy.add({
type: NounType.Person,
data: { name: 'Bob', role: 'developer' }
})
await brainy.add({
type: NounType.Document,
data: { title: 'Development Process' }
})
// Query with explicit type should override inference
const results = await brainy.find({
query: 'development',
type: NounType.Document // Explicit override
})
// Should only find documents
expect(results.length).toBeGreaterThan(0)
expect(results.every(r => r.entity.noun === NounType.Document)).toBe(true)
})
it('should handle multi-type queries efficiently', async () => {
// Add diverse data
await brainy.add({
type: NounType.Person,
data: { name: 'Charlie', company: 'TechCorp' }
})
await brainy.add({
type: NounType.Organization,
data: { name: 'TechCorp', industry: 'Software' }
})
await brainy.add({
type: NounType.Document,
data: { title: 'TechCorp Overview' }
})
// Query that should infer multiple types
const results = await brainy.find('people at TechCorp')
expect(results.length).toBeGreaterThan(0)
// Should find both Person and Organization
const types = results.map(r => r.entity.noun)
expect(types).toContain(NounType.Person)
})
})
// ========== Performance Tests (2 tests) ==========
describe('Performance Impact', () => {
it('should reduce query latency for type-specific queries', async () => {
// Add 100 diverse entities
for (let i = 0; i < 50; i++) {
await brainy.add({
type: NounType.Person,
data: { name: `Person ${i}`, role: 'engineer' }
})
}
for (let i = 0; i < 50; i++) {
await brainy.add({
type: NounType.Document,
data: { title: `Document ${i}` }
})
}
// Measure query with type inference
const start = Date.now()
const results = await brainy.find('Find engineers')
const elapsed = Date.now() - start
expect(results.length).toBeGreaterThan(0)
expect(elapsed).toBeLessThan(1000) // Should be fast even with 100 entities
// Verify results are correct type
const personResults = results.filter(r => r.entity.noun === NounType.Person)
expect(personResults.length).toBeGreaterThan(0)
}, 10000)
it('should handle high-volume queries without degradation', async () => {
// Add test data
for (let i = 0; i < 20; i++) {
await brainy.add({
type: NounType.Person,
data: { name: `Person ${i}` }
})
}
// Run 10 queries in sequence
const latencies: number[] = []
for (let i = 0; i < 10; i++) {
const start = Date.now()
await brainy.find('Find people')
const elapsed = Date.now() - start
latencies.push(elapsed)
}
// Verify consistent performance (no degradation)
const avgLatency = latencies.reduce((a, b) => a + b, 0) / latencies.length
const maxLatency = Math.max(...latencies)
expect(maxLatency).toBeLessThan(avgLatency * 2) // Max should not be > 2x avg
}, 15000)
})
// ========== Edge Cases (2 tests) ==========
describe('Edge Cases', () => {
it('should handle queries with no matching types gracefully', async () => {
await brainy.add({
type: NounType.Person,
data: { name: 'Dave' }
})
// Query that won't infer any specific type
const results = await brainy.find('random stuff xyz')
// Should still work (fallback to all-types)
expect(Array.isArray(results)).toBe(true)
})
it('should handle empty query gracefully', async () => {
await brainy.add({
type: NounType.Person,
data: { name: 'Eve' }
})
// Empty query should return results
const results = await brainy.find({ limit: 5 })
expect(Array.isArray(results)).toBe(true)
expect(results.length).toBeGreaterThan(0)
})
})
// ========== Backward Compatibility (1 test) ==========
describe('Backward Compatibility', () => {
it('should work with all existing query patterns', async () => {
await brainy.add({
type: NounType.Person,
data: { name: 'Frank', age: 30 }
})
// Test various query patterns
const results1 = await brainy.find({ query: 'Frank' })
expect(results1.length).toBeGreaterThan(0)
const results2 = await brainy.find({ type: NounType.Person })
expect(results2.length).toBeGreaterThan(0)
const results3 = await brainy.find({
where: { age: 30 }
})
expect(results3.length).toBeGreaterThan(0)
const results4 = await brainy.find('Find Frank')
expect(results4.length).toBeGreaterThan(0)
})
})
})