/** * KnowledgeGraph tests — full-file port from * hive-mind/packages/core/src/mind/knowledge.test.ts. * * Memory Sync Repair Step 2. Source: hive-mind file at HEAD c363257. * * Filename suffix `-hive-mind` keeps this distinct from waggle-os's own * `knowledge.test.ts`. Hive-mind covers: * - bfsDistances shortcut-vs-via-path edge case * - getEntitiesValidAt at distinct time instants * - getEntityTypeCounts/getEntityCount summary surface * - setValidationSchema enforcement on createRelation (allowedRelations) * — surfaces waggle-os covers differently. * * Adapted imports: `./db.js`, `./knowledge.js` → `../../src/mind/...`. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { rmSync, existsSync } from 'node:fs'; import { MindDB } from '../../src/mind/db.js'; import { KnowledgeGraph, type ValidationSchema } from '../../src/mind/knowledge.js'; describe('KnowledgeGraph (hive-mind port)', () => { let dbPath: string; let db: MindDB; let kg: KnowledgeGraph; beforeEach(() => { dbPath = join(tmpdir(), `waggle-mind-kg-test-${Date.now()}-${Math.random()}.mind`); db = new MindDB(dbPath); kg = new KnowledgeGraph(db); }); afterEach(() => { db.close(); if (existsSync(dbPath)) rmSync(dbPath); for (const suffix of ['-shm', '-wal']) { if (existsSync(dbPath + suffix)) rmSync(dbPath + suffix); } }); it('createEntity stores type/name/properties and getEntity round-trips', () => { const e = kg.createEntity('person', 'Ada', { role: 'engineer' }); expect(e.entity_type).toBe('person'); expect(e.name).toBe('Ada'); expect(JSON.parse(e.properties)).toEqual({ role: 'engineer' }); expect(e.valid_to).toBeNull(); const loaded = kg.getEntity(e.id); expect(loaded?.id).toBe(e.id); expect(loaded?.name).toBe('Ada'); }); it('updateEntity rewrites name and properties', () => { const e = kg.createEntity('person', 'Ada', { role: 'engineer' }); const updated = kg.updateEntity(e.id, { name: 'Ada Lovelace', properties: { role: 'mathematician' } }); expect(updated.name).toBe('Ada Lovelace'); expect(JSON.parse(updated.properties)).toEqual({ role: 'mathematician' }); }); it('retireEntity sets valid_to and excludes the entity from active listings', () => { const a = kg.createEntity('person', 'Alice', {}); const b = kg.createEntity('person', 'Bob', {}); kg.retireEntity(a.id); const active = kg.getEntitiesByType('person'); expect(active.map((e) => e.id)).toEqual([b.id]); expect(kg.getEntity(a.id)?.valid_to).not.toBeNull(); }); it('createRelation + getRelationsFrom/getRelationsTo round-trip', () => { const alice = kg.createEntity('person', 'Alice', {}); const acme = kg.createEntity('org', 'Acme', {}); const rel = kg.createRelation(alice.id, acme.id, 'works_at', 0.9, { since: '2020' }); expect(rel.confidence).toBe(0.9); expect(JSON.parse(rel.properties)).toEqual({ since: '2020' }); const fromAlice = kg.getRelationsFrom(alice.id); expect(fromAlice).toHaveLength(1); expect(fromAlice[0].target_id).toBe(acme.id); const toAcme = kg.getRelationsTo(acme.id, 'works_at'); expect(toAcme).toHaveLength(1); expect(toAcme[0].source_id).toBe(alice.id); }); it('retireRelation hides the edge from active queries', () => { const a = kg.createEntity('person', 'A', {}); const b = kg.createEntity('person', 'B', {}); const rel = kg.createRelation(a.id, b.id, 'knows'); kg.retireRelation(rel.id); expect(kg.getRelationsFrom(a.id)).toEqual([]); expect(kg.getRelationsTo(b.id)).toEqual([]); }); it('traverse does typed-edge BFS bounded by maxDepth', () => { const a = kg.createEntity('person', 'A', {}); const b = kg.createEntity('person', 'B', {}); const c = kg.createEntity('person', 'C', {}); const d = kg.createEntity('person', 'D', {}); kg.createRelation(a.id, b.id, 'knows'); kg.createRelation(b.id, c.id, 'knows'); kg.createRelation(c.id, d.id, 'knows'); kg.createRelation(a.id, d.id, 'dislikes'); // Different type — must not appear. const hop1 = kg.traverse(a.id, 'knows', 1).map((e) => e.name); expect(hop1).toEqual(['B']); const hop3 = kg.traverse(a.id, 'knows', 3).map((e) => e.name).sort(); expect(hop3).toEqual(['B', 'C', 'D']); }); it('bfsDistances returns shortest distance to each reachable entity', () => { const a = kg.createEntity('t', 'A', {}); const b = kg.createEntity('t', 'B', {}); const c = kg.createEntity('t', 'C', {}); const d = kg.createEntity('t', 'D', {}); kg.createRelation(a.id, b.id, 'edge'); kg.createRelation(b.id, c.id, 'edge'); kg.createRelation(a.id, c.id, 'edge'); // shortcut: A→C direct const distances = kg.bfsDistances(a.id, 3); expect(distances.get(b.id)).toBe(1); expect(distances.get(c.id)).toBe(1); // shortcut wins over via-B path expect(distances.has(d.id)).toBe(false); }); it('getEntitiesValidAt reconstructs the graph at a given instant', () => { const past = '2020-01-01T00:00:00Z'; const now = '2025-01-01T00:00:00Z'; const oldEntity = kg.createEntity('t', 'Old', {}, { valid_from: past, valid_to: '2023-01-01T00:00:00Z' }); const current = kg.createEntity('t', 'Current', {}, { valid_from: past }); const asOf2022 = kg.getEntitiesValidAt('2022-01-01T00:00:00Z').map((e) => e.name).sort(); expect(asOf2022).toEqual(['Current', 'Old']); const asOfNow = kg.getEntitiesValidAt(now).map((e) => e.name); expect(asOfNow).toEqual(['Current']); expect(oldEntity.id).not.toBe(current.id); }); it('searchEntities LIKE matches by name substring', () => { kg.createEntity('t', 'Alice', {}); kg.createEntity('t', 'Alicia', {}); kg.createEntity('t', 'Bob', {}); const hits = kg.searchEntities('Ali').map((e) => e.name).sort(); expect(hits).toEqual(['Alice', 'Alicia']); }); it('getEntityTypeCounts + getEntityCount summarize the active graph', () => { kg.createEntity('person', 'A', {}); kg.createEntity('person', 'B', {}); kg.createEntity('org', 'Acme', {}); const retired = kg.createEntity('org', 'Defunct', {}); kg.retireEntity(retired.id); expect(kg.getEntityCount()).toBe(3); const counts = kg.getEntityTypeCounts(); const map = new Map(counts.map((c) => [c.type, c.count])); expect(map.get('person')).toBe(2); expect(map.get('org')).toBe(1); }); it('validation schema enforces required props and allowed relations', () => { const schema: ValidationSchema = { person: { required: ['email'], allowedRelations: ['works_at'] }, }; kg.setValidationSchema(schema); expect(() => kg.createEntity('person', 'Alice', {})).toThrow(/required property 'email'/); const alice = kg.createEntity('person', 'Alice', { email: 'a@x' }); const acme = kg.createEntity('org', 'Acme', {}); expect(() => kg.createRelation(alice.id, acme.id, 'dislikes')).toThrow( /relation 'dislikes' not allowed/, ); const ok = kg.createRelation(alice.id, acme.id, 'works_at'); expect(ok.relation_type).toBe('works_at'); }); });