2025-09-11 16:23:32 -07:00
/ * *
* Streaming Pipeline Tests
* Tests for the new streaming data pipeline system
* /
import { describe , it , expect , beforeEach , afterEach } from 'vitest'
import { Pipeline , createPipeline } from '../src/streaming/pipeline.js'
import { Brainy } from '../src/brainy.js'
import { NounType } from '../src/types/graphTypes.js'
describe ( 'Streaming Pipeline' , ( ) = > {
let brain : Brainy
beforeEach ( async ( ) = > {
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
brain = new Brainy ( { requireSubtype : false ,
2025-09-11 16:23:32 -07:00
storage : { type : 'memory' } ,
warmup : false
} )
await brain . init ( )
} )
afterEach ( async ( ) = > {
await brain . close ( )
} )
describe ( 'Core Operations' , ( ) = > {
it ( 'should process data through pipeline' , async ( ) = > {
const results : number [ ] = [ ]
await new Pipeline ( )
. source ( async function * ( ) {
for ( let i = 1 ; i <= 5 ; i ++ ) {
yield i
}
} )
. map ( x = > x * 2 )
. filter ( x = > x > 4 )
. sink ( x = > { results . push ( x ) } )
. run ( )
expect ( results ) . toEqual ( [ 6 , 8 , 10 ] )
} )
it ( 'should handle async transformations' , async ( ) = > {
const results : string [ ] = [ ]
await new Pipeline ( )
. source ( async function * ( ) {
yield 'hello'
yield 'world'
} )
. map ( async ( text ) = > {
await new Promise ( resolve = > setTimeout ( resolve , 10 ) )
return text . toUpperCase ( )
} )
. sink ( x = > { results . push ( x ) } )
. run ( )
expect ( results ) . toEqual ( [ 'HELLO' , 'WORLD' ] )
} )
it ( 'should batch items' , async ( ) = > {
const batches : number [ ] [ ] = [ ]
await new Pipeline ( )
. source ( async function * ( ) {
for ( let i = 1 ; i <= 10 ; i ++ ) {
yield i
}
} )
. batch ( 3 )
. sink ( batch = > { batches . push ( batch ) } )
. run ( )
expect ( batches ) . toEqual ( [
[ 1 , 2 , 3 ] ,
[ 4 , 5 , 6 ] ,
[ 7 , 8 , 9 ] ,
[ 10 ]
] )
} )
it ( 'should collect all results' , async ( ) = > {
const pipeline = new Pipeline ( )
. source ( async function * ( ) {
for ( let i = 1 ; i <= 5 ; i ++ ) {
yield i
}
} )
. map ( x = > x * x )
const results = await pipeline . collect ( )
expect ( results ) . toEqual ( [ 1 , 4 , 9 , 16 , 25 ] )
} )
} )
describe ( 'Window Operations' , ( ) = > {
it ( 'should support tumbling windows' , async ( ) = > {
const windows : number [ ] [ ] = [ ]
await new Pipeline ( )
. source ( async function * ( ) {
for ( let i = 1 ; i <= 10 ; i ++ ) {
yield i
}
} )
. window ( 3 , 'tumbling' )
. sink ( window = > { windows . push ( window ) } )
. run ( )
expect ( windows ) . toEqual ( [
[ 1 , 2 , 3 ] ,
[ 4 , 5 , 6 ] ,
[ 7 , 8 , 9 ] ,
[ 10 ]
] )
} )
it ( 'should support sliding windows' , async ( ) = > {
const windows : number [ ] [ ] = [ ]
await new Pipeline ( )
. source ( async function * ( ) {
for ( let i = 1 ; i <= 5 ; i ++ ) {
yield i
}
} )
. window ( 3 , 'sliding' )
. sink ( window = > { windows . push ( [ . . . window ] ) } )
. run ( )
expect ( windows ) . toEqual ( [
[ 1 , 2 , 3 ] ,
[ 2 , 3 , 4 ] ,
[ 3 , 4 , 5 ]
] )
} )
} )
describe ( 'Reduce Operations' , ( ) = > {
it ( 'should reduce values' , async ( ) = > {
let result = 0
await new Pipeline ( )
. source ( async function * ( ) {
for ( let i = 1 ; i <= 5 ; i ++ ) {
yield i
}
} )
. reduce ( ( acc , val ) = > acc + val , 0 )
. sink ( sum = > { result = sum } )
. run ( )
expect ( result ) . toBe ( 15 )
} )
it ( 'should work with complex reducers' , async ( ) = > {
interface Stats {
count : number
sum : number
max : number
}
let stats : Stats = { count : 0 , sum : 0 , max : 0 }
await new Pipeline < number > ( )
. source ( async function * ( ) {
for ( let i = 1 ; i <= 10 ; i ++ ) {
yield i
}
} )
. reduce < Stats > ( ( acc , val ) = > ( {
count : acc.count + 1 ,
sum : acc.sum + val ,
max : Math.max ( acc . max , val )
} ) , { count : 0 , sum : 0 , max : 0 } )
. sink ( s = > { stats = s } )
. run ( )
expect ( stats ) . toEqual ( {
count : 10 ,
sum : 55 ,
max : 10
} )
} )
} )
describe ( 'Brainy Integration' , ( ) = > {
it ( 'should sink data to Brainy' , async ( ) = > {
const pipeline = new Pipeline ( brain )
. source ( async function * ( ) {
yield { content : 'Document 1' }
yield { content : 'Document 2' }
yield { content : 'Document 3' }
} )
. toBrainy ( {
type : NounType . Document ,
metadata : { source : 'pipeline' } ,
batchSize : 2
} )
await pipeline . run ( )
// Verify data was added
const results = await brain . find ( {
where : { 'metadata.source' : 'pipeline' }
} )
expect ( results . length ) . toBe ( 3 )
} )
it ( 'should process and transform before storing' , async ( ) = > {
const pipeline = new Pipeline ( brain )
. source ( async function * ( ) {
yield 'hello world'
yield 'goodbye world'
} )
. map ( text = > ( {
content : text ,
processed : text.toUpperCase ( )
} ) )
. toBrainy ( {
type : NounType . Document ,
metadata : { pipeline : true }
} )
await pipeline . run ( )
const results = await brain . find ( {
where : { 'metadata.pipeline' : true }
} )
expect ( results . length ) . toBe ( 2 )
} )
} )
describe ( 'Error Handling' , ( ) = > {
it ( 'should handle errors with handler' , async ( ) = > {
const errors : Error [ ] = [ ]
await new Pipeline ( )
. source ( async function * ( ) {
yield 1
yield 2
yield 3
} )
. map ( x = > {
if ( x === 2 ) throw new Error ( 'Test error' )
return x
} )
. sink ( ( ) = > { } )
. run ( {
errorHandler : ( error ) = > errors . push ( error )
} )
expect ( errors . length ) . toBe ( 1 )
expect ( errors [ 0 ] . message ) . toBe ( 'Test error' )
} )
it ( 'should stop on abort signal' , async ( ) = > {
let count = 0
const pipeline = new Pipeline ( )
. source ( async function * ( ) {
for ( let i = 1 ; i <= 100 ; i ++ ) {
yield i
}
} )
. sink ( ( ) = > { count ++ } )
// Start pipeline
const runPromise = pipeline . run ( )
// Stop after a short delay
setTimeout ( ( ) = > pipeline . stop ( ) , 50 )
await runPromise
// Should have processed some but not all items
expect ( count ) . toBeGreaterThan ( 0 )
expect ( count ) . toBeLessThan ( 100 )
} )
} )
describe ( 'Performance Features' , ( ) = > {
it ( 'should throttle sink operations' , async ( ) = > {
const times : number [ ] = [ ]
const startTime = Date . now ( )
await new Pipeline ( )
. source ( async function * ( ) {
for ( let i = 1 ; i <= 3 ; i ++ ) {
yield i
}
} )
. throttledSink (
( ) = > { times . push ( Date . now ( ) - startTime ) } ,
10 // 10 ops/sec max
)
. run ( )
// Check that operations were throttled
expect ( times . length ) . toBe ( 3 )
expect ( times [ 2 ] - times [ 0 ] ) . toBeGreaterThanOrEqual ( 200 ) // At least 200ms for 3 items at 10/sec
} )
it ( 'should run monitoring' , async ( ) = > {
const logs : string [ ] = [ ]
const originalLog = console . log
console . log = ( msg : string ) = > logs . push ( msg )
try {
await new Pipeline ( )
. source ( async function * ( ) {
for ( let i = 1 ; i <= 10 ; i ++ ) {
yield i
}
} )
. sink ( ( ) = > { } )
. run ( { monitoring : true } )
// Should have logged completion metrics
expect ( logs . some ( log = > log . includes ( 'Pipeline completed' ) ) ) . toBe ( true )
expect ( logs . some ( log = > log . includes ( 'Throughput' ) ) ) . toBe ( true )
} finally {
console . log = originalLog
}
} )
} )
} )