2025-09-11 16:23:32 -07:00
/ * *
* 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 ( ) } `
}
2025-09-15 11:06:16 -07:00
// Generate test vector (384-dimensional vector matching all-MiniLM-L6-v2)
export function generateTestVector ( dimension = 384 ) : Vector {
2025-09-11 16:23:32 -07:00
// 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.)
2025-09-11 16:23:32 -07:00
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 ,
2026-06-16 10:57:05 -07:00
type : 'relatedTo' as VerbType ,
2025-09-11 16:23:32 -07:00
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.
2025-09-11 16:23:32 -07:00
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 ,
2025-09-11 16:23:32 -07:00
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 = [
2026-06-16 10:57:05 -07:00
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 } ) ,
2025-09-11 16:23:32 -07:00
]
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