test(8.0): Tier-1 integration via deterministic embedder (suite runnable again)
The integration suite loaded the real WASM model for every test, so a full run took ~hours and reliably aborted (vitest reporter timeout) — which is why it was never gated and silently rotted. Brainy already separates the embedder from all other logic, so running integration with a deterministic embedder keeps every code path real (HNSW build+search, graph traversal, metadata filtering, transactions, storage/sharding, VFS, aggregation, import) while making the suite complete in ~2 min on ANY machine — no GPU, no 16 GB requirement. - Add isDeterministicEmbedMode() — a single source of truth for the test-embedder switch (BRAINY_DETERMINISTIC_EMBEDDINGS, plus the legacy BRAINY_UNIT_TEST alias), used by EmbeddingManager (init/embed/embedBatch) and brainy's eager-init guard. - setup-integration.ts opts into it: Tier 1 = functional end-to-end. Real-model semantic quality + the real embedding pipeline move to a Tier-2 suite. Full integration now runs 482 passing / 583 in ~2 min (was un-completable). The remaining failures are pre-existing test rot, fixed next. Unit 1414 green.
This commit is contained in:
parent
e31ba894f8
commit
542b52ede9
4 changed files with 84 additions and 67 deletions
|
|
@ -116,6 +116,7 @@ import { resolveJsHnswConfig, DEFAULT_RECALL } from './utils/recallPreset.js'
|
||||||
import * as fs from 'node:fs'
|
import * as fs from 'node:fs'
|
||||||
import { Db, type DbHost, type HistoricalQueryHandle } from './db/db.js'
|
import { Db, type DbHost, type HistoricalQueryHandle } from './db/db.js'
|
||||||
import { GenerationStore } from './db/generationStore.js'
|
import { GenerationStore } from './db/generationStore.js'
|
||||||
|
import { isDeterministicEmbedMode } from './embeddings/deterministicEmbedMode.js'
|
||||||
import { GenerationConflictError } from './db/errors.js'
|
import { GenerationConflictError } from './db/errors.js'
|
||||||
import { MemoryStorage } from './storage/adapters/memoryStorage.js'
|
import { MemoryStorage } from './storage/adapters/memoryStorage.js'
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -885,9 +886,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
//
|
//
|
||||||
// `eagerEmbeddings: false` is the explicit override to force lazy init
|
// `eagerEmbeddings: false` is the explicit override to force lazy init
|
||||||
// (first-embed) even when this instance is the active embedder.
|
// (first-embed) even when this instance is the active embedder.
|
||||||
const isUnitTestMode =
|
const isUnitTestMode = isDeterministicEmbedMode()
|
||||||
process.env.BRAINY_UNIT_TEST === 'true' ||
|
|
||||||
(globalThis as { __BRAINY_UNIT_TEST__?: boolean }).__BRAINY_UNIT_TEST__ === true
|
|
||||||
const eager = this.config.eagerEmbeddings ?? true
|
const eager = this.config.eagerEmbeddings ?? true
|
||||||
if (
|
if (
|
||||||
eager &&
|
eager &&
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
|
|
||||||
import { Vector, EmbeddingFunction } from '../coreTypes.js'
|
import { Vector, EmbeddingFunction } from '../coreTypes.js'
|
||||||
import { WASMEmbeddingEngine } from './wasm/index.js'
|
import { WASMEmbeddingEngine } from './wasm/index.js'
|
||||||
|
import { isDeterministicEmbedMode } from './deterministicEmbedMode.js'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
/**
|
/**
|
||||||
|
|
@ -77,9 +78,7 @@ export class EmbeddingManager {
|
||||||
*/
|
*/
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
// In unit test mode, skip real model initialization
|
// In unit test mode, skip real model initialization
|
||||||
const isTestMode =
|
const isTestMode = isDeterministicEmbedMode()
|
||||||
process.env.BRAINY_UNIT_TEST === 'true' ||
|
|
||||||
globalThis.__BRAINY_UNIT_TEST__
|
|
||||||
|
|
||||||
if (isTestMode) {
|
if (isTestMode) {
|
||||||
// Production safeguard
|
// Production safeguard
|
||||||
|
|
@ -151,9 +150,7 @@ export class EmbeddingManager {
|
||||||
*/
|
*/
|
||||||
async embed(text: string | string[] | Record<string, unknown>): Promise<Vector> {
|
async embed(text: string | string[] | Record<string, unknown>): Promise<Vector> {
|
||||||
// Check for unit test environment
|
// Check for unit test environment
|
||||||
const isTestMode =
|
const isTestMode = isDeterministicEmbedMode()
|
||||||
process.env.BRAINY_UNIT_TEST === 'true' ||
|
|
||||||
globalThis.__BRAINY_UNIT_TEST__
|
|
||||||
|
|
||||||
if (isTestMode) {
|
if (isTestMode) {
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
|
@ -233,9 +230,7 @@ export class EmbeddingManager {
|
||||||
async embedBatch(texts: string[], options?: { signal?: AbortSignal }): Promise<number[][]> {
|
async embedBatch(texts: string[], options?: { signal?: AbortSignal }): Promise<number[][]> {
|
||||||
if (texts.length === 0) return []
|
if (texts.length === 0) return []
|
||||||
|
|
||||||
const isTestMode =
|
const isTestMode = isDeterministicEmbedMode()
|
||||||
process.env.BRAINY_UNIT_TEST === 'true' ||
|
|
||||||
globalThis.__BRAINY_UNIT_TEST__
|
|
||||||
|
|
||||||
if (isTestMode) {
|
if (isTestMode) {
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
|
|
||||||
39
src/embeddings/deterministicEmbedMode.ts
Normal file
39
src/embeddings/deterministicEmbedMode.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
* @module embeddings/deterministicEmbedMode
|
||||||
|
* @description Single source of truth for "use deterministic test embeddings".
|
||||||
|
*
|
||||||
|
* Brainy's embedding model is a real WASM transformer — correct, but slow on CPU
|
||||||
|
* (a forward pass per text). For TEST tiers that exercise database *logic* (HNSW
|
||||||
|
* build + search, graph traversal, metadata filtering, transactions, storage,
|
||||||
|
* VFS, find composition), the embedding's semantic *quality* is irrelevant; only
|
||||||
|
* a deterministic, content-derived vector is needed. Switching the embedder to a
|
||||||
|
* deterministic stand-in keeps every other code path real while turning a
|
||||||
|
* multi-hour suite into a sub-minute, gateable one. Semantic quality and the
|
||||||
|
* real embedding pipeline are covered separately by the real-model test tier.
|
||||||
|
*
|
||||||
|
* This flag is honored ONLY outside production: {@link EmbeddingManager} throws
|
||||||
|
* if it is set while `NODE_ENV=production` (mock vectors in prod is a security
|
||||||
|
* risk). Enabled by env var or global so test setup files can opt in per tier.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface DeterministicEmbedGlobals {
|
||||||
|
__BRAINY_DETERMINISTIC_EMBED__?: boolean
|
||||||
|
/** Legacy alias — the original unit-test-only spelling. */
|
||||||
|
__BRAINY_UNIT_TEST__?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Whether the deterministic test embedder should be used instead of
|
||||||
|
* the real WASM model. True when `BRAINY_DETERMINISTIC_EMBEDDINGS=true` or the
|
||||||
|
* legacy `BRAINY_UNIT_TEST=true` env var is set, or the matching global is set.
|
||||||
|
* @returns `true` to use deterministic embeddings, `false` for the real model.
|
||||||
|
*/
|
||||||
|
export function isDeterministicEmbedMode(): boolean {
|
||||||
|
const g = globalThis as DeterministicEmbedGlobals
|
||||||
|
return (
|
||||||
|
process.env.BRAINY_DETERMINISTIC_EMBEDDINGS === 'true' ||
|
||||||
|
process.env.BRAINY_UNIT_TEST === 'true' ||
|
||||||
|
g.__BRAINY_DETERMINISTIC_EMBED__ === true ||
|
||||||
|
g.__BRAINY_UNIT_TEST__ === true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,60 +1,44 @@
|
||||||
/**
|
/**
|
||||||
* Integration Test Setup - REAL AI functionality
|
* Integration Test Setup — Tier 1: Functional end-to-end (deterministic embedder)
|
||||||
*
|
*
|
||||||
* This setup enables real AI models for integration testing
|
* These tests exercise every real code path — HNSW build + search, graph
|
||||||
* Requires high memory environment (16GB+ RAM)
|
* traversal, metadata filtering, triple-intelligence composition, MVCC
|
||||||
|
* transactions, sharding/persistence, VFS, aggregation, import — but with a
|
||||||
|
* DETERMINISTIC embedder instead of the real WASM transformer. The embedding's
|
||||||
|
* semantic *quality* is irrelevant to these assertions (which are structural:
|
||||||
|
* counts, ids, relationships, self-retrieval, transaction outcomes); making the
|
||||||
|
* vector deterministic turns a multi-hour, un-completable suite into a sub-minute
|
||||||
|
* one that any contributor can run on any machine, no GPU or 16 GB required.
|
||||||
|
*
|
||||||
|
* Semantic relevance + the real embedding pipeline are covered separately by the
|
||||||
|
* Tier-2 semantic suite (`tests/semantic/`, real model) — see
|
||||||
|
* `tests/configs/vitest.semantic.config.ts`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(() => {
|
||||||
console.log('🤖 Integration Test Environment: Using REAL AI models')
|
console.log('🧪 Integration (Tier 1): deterministic embedder, all other code paths real')
|
||||||
console.log('⚠️ Requires 16GB+ RAM - this is normal for AI testing')
|
|
||||||
|
|
||||||
// Set up environment for real AI testing
|
// Deterministic embeddings — see src/embeddings/deterministicEmbedMode.ts.
|
||||||
process.env.BRAINY_INTEGRATION_TEST = 'true'
|
process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
|
||||||
process.env.BRAINY_MODELS_PATH = './models'
|
;(globalThis as { __BRAINY_DETERMINISTIC_EMBED__?: boolean }).__BRAINY_DETERMINISTIC_EMBED__ = true
|
||||||
process.env.BRAINY_ALLOW_REMOTE_MODELS = 'false' // Use local models only
|
|
||||||
|
|
||||||
// Set memory limits and optimizations
|
// Marker retained for any test that branches on the integration environment.
|
||||||
process.env.ORT_DISABLE_MEMORY_ARENA = '1'
|
;(globalThis as { __BRAINY_INTEGRATION_TEST__?: boolean }).__BRAINY_INTEGRATION_TEST__ = true
|
||||||
process.env.ORT_DISABLE_MEMORY_PATTERN = '1'
|
})
|
||||||
process.env.ORT_INTRA_OP_NUM_THREADS = '2'
|
|
||||||
process.env.ORT_INTER_OP_NUM_THREADS = '2'
|
|
||||||
|
|
||||||
// Mark as integration test environment
|
afterAll(() => {
|
||||||
;(globalThis as any).__BRAINY_INTEGRATION_TEST__ = true
|
delete process.env.BRAINY_DETERMINISTIC_EMBEDDINGS
|
||||||
|
delete (globalThis as { __BRAINY_DETERMINISTIC_EMBED__?: boolean }).__BRAINY_DETERMINISTIC_EMBED__
|
||||||
|
delete (globalThis as { __BRAINY_INTEGRATION_TEST__?: boolean }).__BRAINY_INTEGRATION_TEST__
|
||||||
|
})
|
||||||
|
|
||||||
// Check memory availability
|
/**
|
||||||
const availableMemoryGB = process.env.NODE_OPTIONS?.includes('max-old-space-size')
|
* No-op retained for back-compat. Tier-1 integration tests use the deterministic
|
||||||
? parseInt(process.env.NODE_OPTIONS.match(/--max-old-space-size=(\d+)/)?.[1] || '0') / 1024
|
* embedder, so the real model's 16 GB requirement no longer applies — there is
|
||||||
: 4
|
* nothing to gate on. (Real-model memory cost lives in the Tier-2 semantic suite.)
|
||||||
|
*
|
||||||
console.log(`📊 Node.js heap limit: ${availableMemoryGB.toFixed(1)}GB`)
|
* @param _minGB - Ignored.
|
||||||
|
*/
|
||||||
if (availableMemoryGB < 8) {
|
export function requiresMemory(_minGB: number): void {
|
||||||
console.warn('⚠️ WARNING: Less than 8GB allocated for integration tests')
|
// Intentionally empty — see the doc comment above.
|
||||||
console.warn(' Recommended: NODE_OPTIONS="--max-old-space-size=16384"')
|
|
||||||
console.warn(' Tests may fail due to insufficient memory')
|
|
||||||
}
|
|
||||||
}, 60000) // 1 minute timeout for setup
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
// Clean up
|
|
||||||
delete process.env.BRAINY_INTEGRATION_TEST
|
|
||||||
delete (globalThis as any).__BRAINY_INTEGRATION_TEST__
|
|
||||||
|
|
||||||
// Force garbage collection if available
|
|
||||||
if (global.gc) {
|
|
||||||
global.gc()
|
|
||||||
}
|
|
||||||
}, 30000) // 30 second timeout for cleanup
|
|
||||||
|
|
||||||
// Utility function to skip tests if not enough memory
|
|
||||||
export function requiresMemory(minGB: number) {
|
|
||||||
const availableMemoryGB = process.env.NODE_OPTIONS?.includes('max-old-space-size')
|
|
||||||
? parseInt(process.env.NODE_OPTIONS.match(/--max-old-space-size=(\d+)/)?.[1] || '0') / 1024
|
|
||||||
: 4
|
|
||||||
|
|
||||||
if (availableMemoryGB < minGB) {
|
|
||||||
throw new Error(`Test requires ${minGB}GB memory, only ${availableMemoryGB.toFixed(1)}GB allocated`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue