diff --git a/src/brainy.ts b/src/brainy.ts index 6b756002..a5ab13cc 100644 --- a/src/brainy.ts +++ b/src/brainy.ts @@ -116,6 +116,7 @@ import { resolveJsHnswConfig, DEFAULT_RECALL } from './utils/recallPreset.js' import * as fs from 'node:fs' import { Db, type DbHost, type HistoricalQueryHandle } from './db/db.js' import { GenerationStore } from './db/generationStore.js' +import { isDeterministicEmbedMode } from './embeddings/deterministicEmbedMode.js' import { GenerationConflictError } from './db/errors.js' import { MemoryStorage } from './storage/adapters/memoryStorage.js' import type { @@ -885,9 +886,7 @@ export class Brainy implements BrainyInterface { // // `eagerEmbeddings: false` is the explicit override to force lazy init // (first-embed) even when this instance is the active embedder. - const isUnitTestMode = - process.env.BRAINY_UNIT_TEST === 'true' || - (globalThis as { __BRAINY_UNIT_TEST__?: boolean }).__BRAINY_UNIT_TEST__ === true + const isUnitTestMode = isDeterministicEmbedMode() const eager = this.config.eagerEmbeddings ?? true if ( eager && diff --git a/src/embeddings/EmbeddingManager.ts b/src/embeddings/EmbeddingManager.ts index 49cea154..2efd84d8 100644 --- a/src/embeddings/EmbeddingManager.ts +++ b/src/embeddings/EmbeddingManager.ts @@ -14,6 +14,7 @@ import { Vector, EmbeddingFunction } from '../coreTypes.js' import { WASMEmbeddingEngine } from './wasm/index.js' +import { isDeterministicEmbedMode } from './deterministicEmbedMode.js' declare global { /** @@ -77,9 +78,7 @@ export class EmbeddingManager { */ async init(): Promise { // In unit test mode, skip real model initialization - const isTestMode = - process.env.BRAINY_UNIT_TEST === 'true' || - globalThis.__BRAINY_UNIT_TEST__ + const isTestMode = isDeterministicEmbedMode() if (isTestMode) { // Production safeguard @@ -151,9 +150,7 @@ export class EmbeddingManager { */ async embed(text: string | string[] | Record): Promise { // Check for unit test environment - const isTestMode = - process.env.BRAINY_UNIT_TEST === 'true' || - globalThis.__BRAINY_UNIT_TEST__ + const isTestMode = isDeterministicEmbedMode() if (isTestMode) { if (process.env.NODE_ENV === 'production') { @@ -233,9 +230,7 @@ export class EmbeddingManager { async embedBatch(texts: string[], options?: { signal?: AbortSignal }): Promise { if (texts.length === 0) return [] - const isTestMode = - process.env.BRAINY_UNIT_TEST === 'true' || - globalThis.__BRAINY_UNIT_TEST__ + const isTestMode = isDeterministicEmbedMode() if (isTestMode) { if (process.env.NODE_ENV === 'production') { diff --git a/src/embeddings/deterministicEmbedMode.ts b/src/embeddings/deterministicEmbedMode.ts new file mode 100644 index 00000000..966264de --- /dev/null +++ b/src/embeddings/deterministicEmbedMode.ts @@ -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 + ) +} diff --git a/tests/setup-integration.ts b/tests/setup-integration.ts index ff470f54..cabca38f 100644 --- a/tests/setup-integration.ts +++ b/tests/setup-integration.ts @@ -1,60 +1,44 @@ /** - * Integration Test Setup - REAL AI functionality - * - * This setup enables real AI models for integration testing - * Requires high memory environment (16GB+ RAM) + * Integration Test Setup — Tier 1: Functional end-to-end (deterministic embedder) + * + * These tests exercise every real code path — HNSW build + search, graph + * 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 () => { - console.log('🤖 Integration Test Environment: Using REAL AI models') - console.log('⚠️ Requires 16GB+ RAM - this is normal for AI testing') - - // Set up environment for real AI testing - process.env.BRAINY_INTEGRATION_TEST = 'true' - process.env.BRAINY_MODELS_PATH = './models' - process.env.BRAINY_ALLOW_REMOTE_MODELS = 'false' // Use local models only - - // Set memory limits and optimizations - process.env.ORT_DISABLE_MEMORY_ARENA = '1' - 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 - ;(globalThis as any).__BRAINY_INTEGRATION_TEST__ = true - - // Check memory availability - 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 - - console.log(`📊 Node.js heap limit: ${availableMemoryGB.toFixed(1)}GB`) - - if (availableMemoryGB < 8) { - console.warn('⚠️ WARNING: Less than 8GB allocated for integration tests') - 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 +beforeAll(() => { + console.log('🧪 Integration (Tier 1): deterministic embedder, all other code paths real') -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 + // Deterministic embeddings — see src/embeddings/deterministicEmbedMode.ts. + process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true' + ;(globalThis as { __BRAINY_DETERMINISTIC_EMBED__?: boolean }).__BRAINY_DETERMINISTIC_EMBED__ = true -// 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`) - } -} \ No newline at end of file + // Marker retained for any test that branches on the integration environment. + ;(globalThis as { __BRAINY_INTEGRATION_TEST__?: boolean }).__BRAINY_INTEGRATION_TEST__ = true +}) + +afterAll(() => { + 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__ +}) + +/** + * No-op retained for back-compat. Tier-1 integration tests use the deterministic + * embedder, so the real model's 16 GB requirement no longer applies — there is + * nothing to gate on. (Real-model memory cost lives in the Tier-2 semantic suite.) + * + * @param _minGB - Ignored. + */ +export function requiresMemory(_minGB: number): void { + // Intentionally empty — see the doc comment above. +}