brainy/tests/helpers/test-factory.ts

492 lines
13 KiB
TypeScript
Raw Normal View History

/**
* Test Factory - Comprehensive test data generation and utilities
* Provides consistent, realistic test data for all Brainy tests
*/
import { v4 as uuidv4 } from '../../src/universal/uuid'
import type { NounType, VerbType } from '../../src/types/graphTypes'
import type {
Entity,
Relation,
Result,
AddParams,
UpdateParams,
RelateParams,
FindParams,
BrainyConfig,
} from '../../src/types/brainy.types'
import type { Vector } from '../../src/coreTypes'
/**
* Test Data Generators
*/
// Generate a unique test ID
export function generateTestId(prefix = 'test'): string {
return `${prefix}_${uuidv4().slice(0, 8)}_${Date.now()}`
}
// Generate test vector (384-dimensional vector matching all-MiniLM-L6-v2)
export function generateTestVector(dimension = 384): Vector {
// Generate a deterministic but varied embedding
const vector = new Array(dimension)
for (let i = 0; i < dimension; i++) {
// Create varied but deterministic values
vector[i] = Math.sin(i * 0.1) * 0.5 + Math.cos(i * 0.05) * 0.3
}
// Normalize to unit vector
const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0))
return vector.map(val => val / magnitude)
}
feat(8.0): reserved-field enforcement — reservedFieldPolicy defaults to throw An untyped (JS) caller that smuggles a Brainy-reserved field (confidence, weight, subtype, visibility, service, createdBy, noun/verb, data, createdAt, updatedAt, _rev) inside a write-path metadata bag previously got a silent remap-or-drop — a class of bug where confidence-evolution writes no-oped for weeks before being caught on read-back. 8.0 closes this with no silent failures. - New BrainyConfig.reservedFieldPolicy: 'throw' | 'warn' | 'remap' (default 'throw'). 'throw' rejects the write naming every offending key + its correct write path; 'warn' remaps with a one-shot per-key warning; 'remap' is the legacy silent path. - Central enforceReservedPolicy gate wired into all four remap methods (add, update, relate, updateRelation) so live calls AND their transact()/with() mirrors honor it. Single-source reservedWritePath guidance shared by throw+warn. - 'warn' now warns for EVERY reserved key (closes the gap where only system-managed fields warned). Dead warnDropped* helpers removed. - Import pipeline migrated to route reserved values (confidence/weight/subtype) through dedicated params and strip reserved keys from extractor/customMetadata bags via the canonical split*MetadataRecord helpers — imports no longer trip the default throw. - Tests: new reservedFieldPolicy matrix (throw/warn/remap across every write path + transact); remap-correctness suite reframed as opt-in 'remap'; shared test-factory no longer emits reserved keys in custom metadata.
2026-06-20 15:37:21 -07:00
// Generate realistic CUSTOM metadata.
//
// NOTE: this deliberately contains only custom (consumer-owned) fields. Reserved
// Brainy fields (`createdAt`, `updatedAt`, `confidence`, `weight`, `subtype`,
// `visibility`, `service`, `createdBy`, `data`, `_rev`) must NOT live in the
// metadata bag — Brainy 8.0's default `reservedFieldPolicy: 'throw'` rejects any
// reserved key passed through `metadata`. (Reserved values belong in their
// dedicated top-level params, set by the add/relate factories below.)
export function generateTestMetadata(overrides: any = {}): any {
return {
name: `Test Item ${generateTestId()}`,
description: 'Test description',
tags: ['test', 'automated'],
version: 1,
source: 'test-factory',
...overrides,
}
}
// Generate a test entity
export function createTestEntity(overrides: Partial<Entity> = {}): Entity {
const id = overrides.id || generateTestId('entity')
const now = Date.now()
return {
id,
type: 'person' as NounType,
vector: generateTestVector(),
metadata: generateTestMetadata(),
service: 'test',
createdAt: now,
updatedAt: now,
...overrides,
}
}
// Generate multiple test entities
export function createTestEntities(count: number, baseOverrides: Partial<Entity> = {}): Entity[] {
return Array.from({ length: count }, (_, i) =>
createTestEntity({
...baseOverrides,
metadata: {
...generateTestMetadata(),
name: `Test Entity ${i + 1}`,
index: i,
},
})
)
}
// Generate a test relation
export function createTestRelation(overrides: Partial<Relation> = {}): Relation {
const id = overrides.id || generateTestId('relation')
const now = Date.now()
return {
id,
from: overrides.from || generateTestId('from'),
to: overrides.to || generateTestId('to'),
type: (overrides.type || 'RelatedTo') as VerbType,
weight: 1.0,
metadata: generateTestMetadata(),
service: 'test',
createdAt: now,
updatedAt: now,
...overrides,
}
}
// Generate multiple test relations
export function createTestRelations(count: number, baseOverrides: Partial<Relation> = {}): Relation[] {
return Array.from({ length: count }, (_, i) =>
createTestRelation({
...baseOverrides,
metadata: {
...generateTestMetadata(),
index: i,
},
})
)
}
// Generate a test result (entity with score)
export function createTestResult(
score: number = 0.95,
entityOverrides: Partial<Entity> = {}
): Result {
return {
id: entityOverrides.id || generateTestId('result'),
score,
entity: createTestEntity(entityOverrides),
}
}
// Generate add parameters
export function createAddParams(overrides: Partial<AddParams> = {}): AddParams {
return {
data: 'Test content for embedding',
type: 'thing' as NounType,
metadata: generateTestMetadata(),
service: 'test',
...overrides,
}
}
// Generate update parameters
export function createUpdateParams(id: string, overrides: Partial<UpdateParams> = {}): UpdateParams {
return {
id,
metadata: { updated: true },
merge: true,
...overrides,
}
}
// Generate relate parameters
export function createRelateParams(from: string, to: string, overrides: Partial<RelateParams> = {}): RelateParams {
return {
from,
to,
type: 'relatedTo' as VerbType,
weight: 1.0,
metadata: generateTestMetadata(),
service: 'test',
...overrides,
}
}
// Generate find parameters
export function createFindParams(overrides: Partial<FindParams> = {}): FindParams {
return {
query: 'test query',
limit: 10,
offset: 0,
explain: false,
includeRelations: false,
service: 'test',
...overrides,
}
}
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
// Generate test configuration. Brainy 8.0 defaults `requireSubtype: true`;
// tests opt out unless they specifically exercise subtype enforcement.
export function createTestConfig(overrides: Partial<BrainyConfig> = {}): BrainyConfig {
return {
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
requireSubtype: false,
storage: { type: 'memory' },
model: { type: 'fast' },
index: {
m: 16,
efConstruction: 200,
efSearch: 50,
},
cache: { maxSize: 1000, ttl: 3600 },
...overrides,
}
}
/**
* Test Data Sets - Pre-built realistic scenarios
*/
// Create a social network graph
export function createSocialNetworkTestData() {
const alice = createTestEntity({
id: 'alice',
type: 'person' as NounType,
metadata: { name: 'Alice', age: 30 }
})
const bob = createTestEntity({
id: 'bob',
type: 'person' as NounType,
metadata: { name: 'Bob', age: 25 }
})
const charlie = createTestEntity({
id: 'charlie',
type: 'person' as NounType,
metadata: { name: 'Charlie', age: 35 }
})
const diana = createTestEntity({
id: 'diana',
type: 'person' as NounType,
metadata: { name: 'Diana', age: 28 }
})
const entities = [alice, bob, charlie, diana]
const relations = [
createTestRelation({ from: 'alice', to: 'bob', type: 'friendOf' as VerbType }),
createTestRelation({ from: 'alice', to: 'charlie', type: 'friendOf' as VerbType }),
createTestRelation({ from: 'bob', to: 'charlie', type: 'worksWith' as VerbType }),
createTestRelation({ from: 'charlie', to: 'diana', type: 'mentors' as VerbType }),
createTestRelation({ from: 'alice', to: 'diana', type: 'likes' as VerbType }),
]
return { entities, relations }
}
// Create a knowledge graph
export function createKnowledgeGraphTestData() {
const entities = [
createTestEntity({
id: 'earth',
type: 'location' as NounType,
metadata: { name: 'Earth', type: 'planet' }
}),
createTestEntity({
id: 'sun',
type: 'thing' as NounType,
metadata: { name: 'Sun', type: 'star' }
}),
createTestEntity({
id: 'moon',
type: 'thing' as NounType,
metadata: { name: 'Moon', type: 'satellite' }
}),
createTestEntity({
id: 'mars',
type: 'location' as NounType,
metadata: { name: 'Mars', type: 'planet' }
}),
]
const relations = [
createTestRelation({ from: 'earth', to: 'sun', type: 'dependsOn' as VerbType }),
createTestRelation({ from: 'moon', to: 'earth', type: 'dependsOn' as VerbType }),
createTestRelation({ from: 'mars', to: 'sun', type: 'dependsOn' as VerbType }),
createTestRelation({ from: 'earth', to: 'moon', type: 'contains' as VerbType }),
]
return { entities, relations }
}
// Create large dataset for performance testing
export function createLargeTestDataset(entityCount = 1000, relationCount = 5000) {
const entities = createTestEntities(entityCount)
const relations: Relation[] = []
const verbTypes = ['relatedTo', 'friendOf', 'worksWith', 'creates', 'owns'] as VerbType[]
// Create random relations between entities
for (let i = 0; i < relationCount; i++) {
const fromIdx = Math.floor(Math.random() * entityCount)
const toIdx = Math.floor(Math.random() * entityCount)
if (fromIdx !== toIdx) {
relations.push(
createTestRelation({
from: entities[fromIdx].id,
to: entities[toIdx].id,
type: verbTypes[Math.floor(Math.random() * verbTypes.length)],
})
)
}
}
return { entities, relations }
}
/**
* Test Assertion Helpers
*/
export function assertEntity(entity: any): asserts entity is Entity {
if (!entity || typeof entity !== 'object') {
throw new Error('Entity must be an object')
}
if (!entity.id || typeof entity.id !== 'string') {
throw new Error('Entity must have a string id')
}
if (!entity.type || typeof entity.type !== 'string') {
throw new Error('Entity must have a string type')
}
if (!entity.vector || !Array.isArray(entity.vector)) {
throw new Error('Entity must have an array vector')
}
if (typeof entity.createdAt !== 'number') {
throw new Error('Entity must have a numeric createdAt timestamp')
}
}
export function assertRelation(relation: any): asserts relation is Relation {
if (!relation || typeof relation !== 'object') {
throw new Error('Relation must be an object')
}
if (!relation.id || typeof relation.id !== 'string') {
throw new Error('Relation must have a string id')
}
if (!relation.from || typeof relation.from !== 'string') {
throw new Error('Relation must have a string from')
}
if (!relation.to || typeof relation.to !== 'string') {
throw new Error('Relation must have a string to')
}
if (!relation.type || typeof relation.type !== 'string') {
throw new Error('Relation must have a string type')
}
if (typeof relation.createdAt !== 'number') {
throw new Error('Relation must have a numeric createdAt timestamp')
}
}
export function assertResult(result: any): asserts result is Result {
if (!result || typeof result !== 'object') {
throw new Error('Result must be an object')
}
if (!result.id || typeof result.id !== 'string') {
throw new Error('Result must have a string id')
}
if (typeof result.score !== 'number') {
throw new Error('Result must have a numeric score')
}
assertEntity(result.entity)
}
/**
* Test Cleanup Helpers
*/
export class TestCleanup {
private cleanupFunctions: Array<() => Promise<void>> = []
register(fn: () => Promise<void>) {
this.cleanupFunctions.push(fn)
}
async cleanup() {
for (const fn of this.cleanupFunctions.reverse()) {
try {
await fn()
} catch (error) {
console.error('Cleanup error:', error)
}
}
this.cleanupFunctions = []
}
}
/**
* Test Timing Helpers
*/
export async function measureExecutionTime<T>(
fn: () => Promise<T>,
label?: string
): Promise<{ result: T; duration: number }> {
const start = performance.now()
const result = await fn()
const duration = performance.now() - start
if (label) {
console.log(`${label}: ${duration.toFixed(2)}ms`)
}
return { result, duration }
}
/**
* Test Delay Helper
*/
export function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
/**
* Mock Storage Helper
*/
export function createMockStorage() {
const store = new Map<string, any>()
return {
get: async (key: string) => store.get(key),
set: async (key: string, value: any) => { store.set(key, value); return true },
delete: async (key: string) => store.delete(key),
has: async (key: string) => store.has(key),
clear: async () => store.clear(),
size: () => store.size,
entries: () => Array.from(store.entries()),
}
}
/**
* Test Error Generators
*/
export function createTestError(message = 'Test error', code = 'TEST_ERROR') {
const error = new Error(message) as any
error.code = code
return error
}
export function createNetworkError() {
return createTestError('Network request failed', 'NETWORK_ERROR')
}
export function createTimeoutError() {
return createTestError('Operation timed out', 'TIMEOUT_ERROR')
}
export function createValidationError(field: string) {
return createTestError(`Validation failed for field: ${field}`, 'VALIDATION_ERROR')
}
// Export everything as a namespace for convenience
export const TestFactory = {
// IDs
generateTestId,
// Core data
generateTestVector,
generateTestMetadata,
createTestEntity,
createTestEntities,
createTestRelation,
createTestRelations,
createTestResult,
// Parameters
createAddParams,
createUpdateParams,
createRelateParams,
createFindParams,
// Config
createTestConfig,
// Datasets
createSocialNetworkTestData,
createKnowledgeGraphTestData,
createLargeTestDataset,
// Assertions
assertEntity,
assertRelation,
assertResult,
// Utilities
TestCleanup,
measureExecutionTime,
delay,
createMockStorage,
// Errors
createTestError,
createNetworkError,
createTimeoutError,
createValidationError,
}
export default TestFactory