import { DyFM_EnvironmentFlag } from '@futdevpro/fsm-dynamo'; import { DyNTS_global_settings } from '../../../_collections/global-settings.const'; import { DyNTS_GlobalService } from '../../../_services/core/global.service'; import { LVS_Search_Mode } from '../_enums/lvs-search-mode.enum'; import { LVS_SearchResult } from '../_models/lvs-search-result.interface'; import { DyNTS_LVS_VectorPersist } from '../_models/data-models/lvs-vector-persist.data-model'; import { DyNTS_LVS_VectorPersist_DataService } from './lvs-vector-persist.data-service'; import { DyNTS_LVS_PersistentVectorPool_ControlService } from './lvs-persistent-vector-pool.control-service'; /** * BFR-AM-001 — a `DyNTS_LVS_PersistentVectorPool_ControlService` (kompozíció: in-memory pool + Mongo-persist) spec-jei. * * A DB-réteget egy IN-MEMORY fake-store helyettesíti: a `DyNTS_LVS_VectorPersist_DataService.prototype` * `upsertVector`/`removeVector`/`findByCollectionKey` metódusait stub-oljuk (a `getDBService` eager * base-ctor-t is, hogy a lazy data-service-példányok ne dobjanak — memory: dynts_dataservice_eager_resolve), * így a perzisztencia memóriában modellezett, ÉLŐ Mongo nélkül (skip-guard nem szükséges). A fő bizonyíték: * **persist-then-hydrate round-trip** újraépíti a VALÓDI `LVS_VectorPool_ControlService` in-memory indexét. */ describe('| DyNTS_LVS_PersistentVectorPool_ControlService', () => { let mockDBService: jasmine.SpyObj<{ find: () => Promise; findOne: () => Promise }>; /** Fake tartós tár: `collectionKey` → (`vectorId` → rekord). A persist-stubok ezt írják/olvassák. */ let store: Map>; beforeAll(() => { if (!DyNTS_global_settings.systemShortCodeName) { (DyNTS_global_settings as { systemShortCodeName?: string }).systemShortCodeName = 'TEST'; } if (!DyNTS_global_settings.env_settings) { (DyNTS_global_settings as { env_settings?: unknown }).env_settings = { environment: DyFM_EnvironmentFlag.local, }; } }); beforeEach(() => { // A base-ctor eager getDBService-jét stub-oljuk (a lazy persist-service ne dobjon). mockDBService = jasmine.createSpyObj('DyNTS_DBService', ['find', 'findOne']); mockDBService.find.and.returnValue(Promise.resolve([])); mockDBService.findOne.and.returnValue(Promise.resolve(null)); spyOn(DyNTS_GlobalService, 'getDBService').and.returnValue(mockDBService as never); // A fake tartós tár + a persist-service prototype-stubok (memóriában modellezett Mongo). store = new Map>(); spyOn(DyNTS_LVS_VectorPersist_DataService.prototype, 'upsertVector').and.callFake( (set: { collectionKey: string; vectorId: string; embedding: number[]; metadata?: unknown }) => { let coll: Map | undefined = store.get(set.collectionKey); if (!coll) { coll = new Map(); store.set(set.collectionKey, coll); } coll.set(set.vectorId, new DyNTS_LVS_VectorPersist({ collectionKey: set.collectionKey, vectorId: set.vectorId, embedding: set.embedding, dimensions: set.embedding.length, metadata: set.metadata, })); return Promise.resolve(); }, ); spyOn(DyNTS_LVS_VectorPersist_DataService.prototype, 'removeVector').and.callFake( (collectionKey: string, vectorId: string) => { store.get(collectionKey)?.delete(vectorId); return Promise.resolve(); }, ); spyOn(DyNTS_LVS_VectorPersist_DataService.prototype, 'findByCollectionKey').and.callFake( (collectionKey: string) => Promise.resolve(Array.from(store.get(collectionKey)?.values() ?? [])), ); }); /** Egy egységvektor a tengely-irányba (cosine-self = 1) — determinisztikus keresés-ellenőrzéshez. */ const axis = (dim: number, hot: number): number[] => { const v: number[] = new Array(dim).fill(0); v[hot] = 1; return v; }; it('| should persist on addVector and store the dimension', async () => { const pool: DyNTS_LVS_PersistentVectorPool_ControlService = new DyNTS_LVS_PersistentVectorPool_ControlService({ collectionKey: 'pool-a', issuer: 'test' }); await pool.addVector('v1', [0.1, 0.2, 0.3], { tag: 'x' }); const persisted: DyNTS_LVS_VectorPersist | undefined = store.get('pool-a')?.get('v1'); expect(persisted).toBeDefined(); expect(persisted.embedding).toEqual([0.1, 0.2, 0.3]); expect(persisted.dimensions).toBe(3); expect(persisted.metadata).toEqual({ tag: 'x' }); // és a memória-pool-ban is benne van (kereshető): expect(pool.getPool().getAll().size).toBe(1); }); it('| persist-then-hydrate round-trip rebuilds the in-memory pool', async () => { // (1) Egy pool-ba 3 vektort persistálunk. const writer: DyNTS_LVS_PersistentVectorPool_ControlService = new DyNTS_LVS_PersistentVectorPool_ControlService({ collectionKey: 'pool-a', issuer: 'test' }); await writer.addVector('v0', axis(4, 0)); await writer.addVector('v1', axis(4, 1)); await writer.addVector('v2', axis(4, 2)); expect(store.get('pool-a').size).toBe(3); // (2) Egy FRISS wrapper (üres memória-pool) — szerver-restart szimuláció. const rebuilt: DyNTS_LVS_PersistentVectorPool_ControlService = new DyNTS_LVS_PersistentVectorPool_ControlService({ collectionKey: 'pool-a', issuer: 'test' }); expect(rebuilt.getPool().getAll().size).toBe(0); // (3) Hidratálás Mongo-ból → a memória-pool újraépül. const loaded: number = await rebuilt.hydrateFromMongo(); expect(loaded).toBe(3); expect(rebuilt.getPool().getAll().size).toBe(3); // (4) A keresés a hidratált pool-on a helyes vektort hozza vissza (cosine self-match = 1). const hits: LVS_SearchResult[] = rebuilt.search(axis(4, 1), 1, LVS_Search_Mode.cosineSimilarity); expect(hits.length).toBe(1); expect(hits[0].id).toBe('v1'); expect(hits[0].score).toBeCloseTo(1, 6); }); it('| should only hydrate the matching collectionKey (pool partitioning)', async () => { const a: DyNTS_LVS_PersistentVectorPool_ControlService = new DyNTS_LVS_PersistentVectorPool_ControlService({ collectionKey: 'pool-a', issuer: 'test' }); const b: DyNTS_LVS_PersistentVectorPool_ControlService = new DyNTS_LVS_PersistentVectorPool_ControlService({ collectionKey: 'pool-b', issuer: 'test' }); await a.addVector('a1', axis(3, 0)); await b.addVector('b1', axis(3, 1)); await b.addVector('b2', axis(3, 2)); const rebuiltA: DyNTS_LVS_PersistentVectorPool_ControlService = new DyNTS_LVS_PersistentVectorPool_ControlService({ collectionKey: 'pool-a', issuer: 'test' }); expect(await rebuiltA.hydrateFromMongo()).toBe(1); expect(Array.from(rebuiltA.getPool().getAll().keys())).toEqual(['a1']); }); it('| upsert (re-add) is reflected in both the pool and the store', async () => { const pool: DyNTS_LVS_PersistentVectorPool_ControlService = new DyNTS_LVS_PersistentVectorPool_ControlService({ collectionKey: 'pool-a', issuer: 'test' }); await pool.addVector('v1', [1, 0, 0]); await pool.updateVector('v1', [0, 1, 0], { v: 2 }); // a tárban EGY rekord van, a frissített értékkel + dimenzióval: expect(store.get('pool-a').size).toBe(1); const persisted: DyNTS_LVS_VectorPersist = store.get('pool-a').get('v1'); expect(persisted.embedding).toEqual([0, 1, 0]); expect(persisted.dimensions).toBe(3); expect(persisted.metadata).toEqual({ v: 2 }); // és egy friss hidratálás a frissített vektort hozza: const rebuilt: DyNTS_LVS_PersistentVectorPool_ControlService = new DyNTS_LVS_PersistentVectorPool_ControlService({ collectionKey: 'pool-a', issuer: 'test' }); await rebuilt.hydrateFromMongo(); const hits: LVS_SearchResult[] = rebuilt.search([0, 1, 0], 1, LVS_Search_Mode.cosineSimilarity); expect(hits[0].id).toBe('v1'); expect(hits[0].score).toBeCloseTo(1, 6); }); it('| removeVector is reflected in both the pool and the store', async () => { const pool: DyNTS_LVS_PersistentVectorPool_ControlService = new DyNTS_LVS_PersistentVectorPool_ControlService({ collectionKey: 'pool-a', issuer: 'test' }); await pool.addVector('v1', axis(3, 0)); await pool.addVector('v2', axis(3, 1)); expect(store.get('pool-a').size).toBe(2); await pool.removeVector('v1'); expect(store.get('pool-a').size).toBe(1); expect(store.get('pool-a').has('v1')).toBeFalse(); expect(pool.getPool().getAll().has('v1')).toBeFalse(); expect(pool.getPool().getAll().has('v2')).toBeTrue(); }); it('| hydrate(records) rebuilds the pool without a Mongo read and skips incomplete records', () => { const pool: DyNTS_LVS_PersistentVectorPool_ControlService = new DyNTS_LVS_PersistentVectorPool_ControlService({ collectionKey: 'pool-a', issuer: 'test' }); const loaded: number = pool.hydrate([ new DyNTS_LVS_VectorPersist({ collectionKey: 'pool-a', vectorId: 'v1', embedding: axis(2, 0) }), new DyNTS_LVS_VectorPersist({ collectionKey: 'pool-a', vectorId: 'v2', embedding: axis(2, 1) }), // hiányos rekordok (átugorva): new DyNTS_LVS_VectorPersist({ collectionKey: 'pool-a', vectorId: 'v3', embedding: [] }), new DyNTS_LVS_VectorPersist({ collectionKey: 'pool-a', embedding: axis(2, 0) }), ]); expect(loaded).toBe(2); expect(pool.getPool().getAll().size).toBe(2); }); });