This commit is contained in:
189
packages/hive-mind-core/tests/mind/knowledge-hive-mind.test.ts
Normal file
189
packages/hive-mind-core/tests/mind/knowledge-hive-mind.test.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user