2025-09-11 16:23:32 -07:00
import { describe , it , expect , beforeAll , afterAll } from 'vitest' ;
import { Brainy } from '../../src/brainy' ;
import { MinioTestServer } from '../helpers/minio-server' ;
import { NounType } from '../../src/types/graphTypes' ;
describe ( 'S3 Storage Integration' , ( ) = > {
let minioServer : MinioTestServer ;
let s3Config : any ;
beforeAll ( async ( ) = > {
// Start MinIO test server
minioServer = new MinioTestServer ( {
bucketName : 'test-brainy-bucket'
} ) ;
await minioServer . start ( ) ;
s3Config = minioServer . getConnectionConfig ( ) ;
} ) ;
afterAll ( async ( ) = > {
if ( minioServer ) {
await minioServer . stop ( ) ;
}
} ) ;
describe ( 'Basic S3 Operations' , ( ) = > {
let brain : Brainy ;
beforeAll ( 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 : 's3' ,
options : {
bucket : s3Config.bucket ,
region : s3Config.region ,
endpoint : s3Config.endpoint ,
accessKeyId : s3Config.accessKeyId ,
secretAccessKey : s3Config.secretAccessKey ,
forcePathStyle : true
}
}
} ) ;
await brain . init ( ) ;
} ) ;
afterAll ( async ( ) = > {
if ( brain ) {
await brain . close ( ) ;
}
} ) ;
it ( 'should store and retrieve items from S3' , async ( ) = > {
const id = await brain . add ( {
data : 'Test item for S3 storage' ,
type : NounType . Document ,
metadata : { source : 's3-test' }
} ) ;
expect ( id ) . toBeDefined ( ) ;
const retrieved = await brain . get ( id ) ;
expect ( retrieved ) . toBeDefined ( ) ;
// Content is stored in metadata, not data property
expect ( retrieved ? . metadata ? . content ) . toBe ( 'Test item for S3 storage' ) ;
expect ( retrieved ? . metadata ? . source ) . toBe ( 's3-test' ) ;
} ) ;
it ( 'should handle batch operations' , async ( ) = > {
const items = Array . from ( { length : 10 } , ( _ , i ) = > ( {
data : ` S3 batch item ${ i } ` ,
type : NounType . Document ,
metadata : { index : i }
} ) ) ;
const result = await brain . addMany ( { items } ) ;
expect ( result . successful ) . toHaveLength ( 10 ) ;
expect ( result . failed ) . toHaveLength ( 0 ) ;
// Get items individually since getBatch doesn't exist
for ( const id of result . successful ) {
const item = await brain . get ( id ) ;
expect ( item ) . toBeDefined ( ) ;
}
} ) ;
it ( 'should delete items from S3' , async ( ) = > {
const id = await brain . add ( {
data : 'Item to delete' ,
type : NounType . Document ,
metadata : { }
} ) ;
const deleted = await brain . delete ( id ) ;
expect ( deleted ) . toBe ( true ) ;
const retrieved = await brain . get ( id ) ;
expect ( retrieved ) . toBeNull ( ) ;
} ) ;
it ( 'should update items in S3' , async ( ) = > {
const id = await brain . add ( {
data : 'Original text' ,
type : NounType . Document ,
metadata : { version : 1 }
} ) ;
await brain . update ( {
id ,
data : 'Updated text' ,
metadata : { version : 2 }
} ) ;
const retrieved = await brain . get ( id ) ;
// Content is stored in metadata, not data property
expect ( retrieved ? . metadata ? . content ) . toBe ( 'Updated text' ) ;
expect ( retrieved ? . metadata ? . version ) . toBe ( 2 ) ;
} ) ;
it ( 'should handle search operations with S3 backend' , async ( ) = > {
// Add test data
const ids : string [ ] = [ ] ;
for ( let i = 0 ; i < 5 ; i ++ ) {
const id = await brain . add ( {
data : ` Search test item ${ i } ` ,
type : NounType . Document ,
metadata : { category : i % 2 === 0 ? 'even' : 'odd' }
} ) ;
ids . push ( id ) ;
}
// Search by query
const results = await brain . find ( {
query : 'Search test item' ,
limit : 10
} ) ;
expect ( results . length ) . toBeGreaterThan ( 0 ) ;
expect ( results [ 0 ] . entity . metadata ? . content ) . toContain ( 'Search test item' ) ;
// Search with metadata filter
const evenResults = await brain . find ( {
query : 'Search test item' ,
where : { category : 'even' } ,
limit : 10
} ) ;
expect ( evenResults . length ) . toBeGreaterThan ( 0 ) ;
evenResults . forEach ( result = > {
expect ( result . entity . metadata ? . category ) . toBe ( 'even' ) ;
} ) ;
// Similar search
const similarResults = await brain . similar ( {
to : ids [ 0 ] ,
limit : 5
} ) ;
expect ( similarResults . length ) . toBeGreaterThan ( 0 ) ;
expect ( similarResults [ 0 ] . id ) . toBe ( ids [ 0 ] ) ; // Most similar should be itself
} ) ;
} ) ;
describe ( 'S3 with Custom Prefix' , ( ) = > {
let prefixedBrain : Brainy ;
beforeAll ( 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
prefixedBrain = new Brainy ( { requireSubtype : false ,
2025-09-11 16:23:32 -07:00
storage : {
type : 's3' ,
options : {
bucket : s3Config.bucket ,
prefix : 'test-prefix/' ,
region : s3Config.region ,
endpoint : s3Config.endpoint ,
accessKeyId : s3Config.accessKeyId ,
secretAccessKey : s3Config.secretAccessKey ,
forcePathStyle : true
}
}
} ) ;
await prefixedBrain . init ( ) ;
} ) ;
afterAll ( async ( ) = > {
if ( prefixedBrain ) {
await prefixedBrain . close ( ) ;
}
} ) ;
it ( 'should store items with prefix in S3' , async ( ) = > {
const id = await prefixedBrain . add ( {
data : 'Prefixed item' ,
type : NounType . Document ,
metadata : { }
} ) ;
const retrieved = await prefixedBrain . get ( id ) ;
// Content is stored in metadata, not data property
expect ( retrieved ? . metadata ? . content ) . toBe ( 'Prefixed item' ) ;
} ) ;
} ) ;
describe ( 'S3 Error Handling' , ( ) = > {
it ( 'should handle invalid S3 credentials gracefully' , 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
const invalidBrain = new Brainy ( { requireSubtype : false ,
2025-09-11 16:23:32 -07:00
storage : {
type : 's3' ,
options : {
bucket : 'invalid-bucket' ,
region : 'us-east-1' ,
accessKeyId : 'invalid' ,
secretAccessKey : 'invalid'
}
}
} ) ;
try {
await invalidBrain . init ( ) ;
await invalidBrain . add ( {
data : 'Test' ,
type : NounType . Document
} ) ;
expect . fail ( 'Should have thrown an error' ) ;
} catch ( error ) {
expect ( error ) . toBeDefined ( ) ;
} finally {
await invalidBrain . close ( ) ;
}
} ) ;
it ( 'should handle network errors gracefully' , async ( ) = > {
// Simulate network error by stopping MinIO temporarily
await minioServer . stop ( ) ;
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
const brain = new Brainy ( { requireSubtype : false ,
2025-09-11 16:23:32 -07:00
storage : {
type : 's3' ,
options : s3Config
}
} ) ;
try {
await brain . init ( ) ;
await brain . add ( {
data : 'Test' ,
type : NounType . Document
} ) ;
expect . fail ( 'Should have thrown an error' ) ;
} catch ( error ) {
expect ( error ) . toBeDefined ( ) ;
} finally {
await brain . close ( ) ;
// Restart MinIO for other tests
await minioServer . start ( ) ;
}
} ) ;
} ) ;
describe ( 'S3 Performance' , ( ) = > {
let perfBrain : Brainy ;
beforeAll ( 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
perfBrain = new Brainy ( { requireSubtype : false ,
2025-09-11 16:23:32 -07:00
storage : {
type : 's3' ,
options : {
bucket : s3Config.bucket ,
region : s3Config.region ,
endpoint : s3Config.endpoint ,
accessKeyId : s3Config.accessKeyId ,
secretAccessKey : s3Config.secretAccessKey ,
forcePathStyle : true
}
}
} ) ;
await perfBrain . init ( ) ;
} ) ;
afterAll ( async ( ) = > {
if ( perfBrain ) {
await perfBrain . close ( ) ;
}
} ) ;
it ( 'should handle large batches efficiently' , async ( ) = > {
const startTime = Date . now ( ) ;
const items = Array . from ( { length : 100 } , ( _ , i ) = > ( {
data : ` Large batch item ${ i } ` ,
type : NounType . Document ,
metadata : { index : i }
} ) ) ;
const result = await perfBrain . addMany ( {
items ,
chunkSize : 25
} ) ;
const duration = Date . now ( ) - startTime ;
expect ( result . successful . length ) . toBe ( 100 ) ;
expect ( result . failed . length ) . toBe ( 0 ) ;
expect ( duration ) . toBeLessThan ( 30000 ) ; // Should complete within 30 seconds
} ) ;
it ( 'should support concurrent operations' , async ( ) = > {
const operations = Array . from ( { length : 20 } , ( _ , i ) = >
perfBrain . add ( {
data : ` Concurrent item ${ i } ` ,
type : NounType . Document ,
metadata : { index : i }
} )
) ;
const ids = await Promise . all ( operations ) ;
expect ( ids ) . toHaveLength ( 20 ) ;
expect ( new Set ( ids ) . size ) . toBe ( 20 ) ; // All IDs should be unique
} ) ;
} ) ;
} ) ;