mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
110 lines
4.2 KiB
TypeScript
110 lines
4.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import type { GraphEdge, GraphNode } from '../../utils/tauriCommands';
|
|
import {
|
|
buildGraph,
|
|
CONTACT_COLOR,
|
|
createSimulation,
|
|
LEAF_COLOR,
|
|
LEVEL_COLOR,
|
|
levelColor,
|
|
nodeColor,
|
|
nodeGlows,
|
|
nodeRadius,
|
|
pickNode,
|
|
type SimNode,
|
|
supportsWebGL,
|
|
} from './memoryGraphLayout';
|
|
|
|
function summary(overrides: Partial<GraphNode> = {}): GraphNode {
|
|
return { kind: 'summary', id: 's', label: 'S', level: 0, parent_id: null, ...overrides };
|
|
}
|
|
function chunk(overrides: Partial<GraphNode> = {}): GraphNode {
|
|
return { kind: 'chunk', id: 'c', label: 'C', ...overrides };
|
|
}
|
|
function contact(overrides: Partial<GraphNode> = {}): GraphNode {
|
|
return { kind: 'contact', id: 'p', label: 'P', entity_kind: 'person', ...overrides };
|
|
}
|
|
|
|
describe('memoryGraphLayout', () => {
|
|
it('colours summaries by level, wrapping the palette', () => {
|
|
expect(levelColor(0)).toBe(LEVEL_COLOR[0]);
|
|
expect(levelColor(2)).toBe(LEVEL_COLOR[2]);
|
|
expect(levelColor(LEVEL_COLOR.length)).toBe(LEVEL_COLOR[0]); // wraps
|
|
expect(levelColor(null)).toBe(LEAF_COLOR);
|
|
expect(levelColor(-5)).toBe(LEVEL_COLOR[0]); // clamped to 0
|
|
});
|
|
|
|
it('nodeColor branches on kind', () => {
|
|
expect(nodeColor(summary({ level: 1 }))).toBe(LEVEL_COLOR[1]);
|
|
expect(nodeColor(chunk())).toBe(LEAF_COLOR);
|
|
expect(nodeColor(contact())).toBe(CONTACT_COLOR);
|
|
});
|
|
|
|
it('nodeRadius grows with summary level and is fixed for chunk/contact', () => {
|
|
expect(nodeRadius(summary({ level: 0 }))).toBe(5);
|
|
expect(nodeRadius(summary({ level: 3 }))).toBeCloseTo(12.5);
|
|
expect(nodeRadius(summary({ level: 99 }))).toBe(252.5);
|
|
expect(nodeRadius(contact())).toBe(9);
|
|
expect(nodeRadius(chunk())).toBe(3);
|
|
});
|
|
|
|
it('only summary/contact nodes glow', () => {
|
|
expect(nodeGlows(summary())).toBe(true);
|
|
expect(nodeGlows(contact())).toBe(true);
|
|
expect(nodeGlows(chunk())).toBe(false);
|
|
});
|
|
|
|
it('buildGraph derives parent_id edges in tree mode and drops danglers', () => {
|
|
const nodes = [
|
|
summary({ id: 'root', parent_id: null }),
|
|
summary({ id: 'child', level: 1, parent_id: 'root' }),
|
|
chunk({ id: 'leaf', parent_id: 'child' }),
|
|
chunk({ id: 'orphan', parent_id: 'missing' }), // dangling → dropped
|
|
];
|
|
const { simNodes, links } = buildGraph(nodes, [], 'tree');
|
|
expect(simNodes).toHaveLength(4);
|
|
expect(simNodes.every(n => typeof n.x === 'number' && typeof n.y === 'number')).toBe(true);
|
|
const pairs = links.map(l => `${String(l.source)}->${String(l.target)}`);
|
|
expect(pairs).toContain('child->root');
|
|
expect(pairs).toContain('leaf->child');
|
|
expect(pairs).not.toContain('orphan->missing');
|
|
});
|
|
|
|
it('buildGraph uses explicit edges in contacts mode and drops danglers', () => {
|
|
const nodes = [chunk({ id: 'c1' }), contact({ id: 'p1' })];
|
|
const edges: GraphEdge[] = [
|
|
{ from: 'c1', to: 'p1' },
|
|
{ from: 'c1', to: 'ghost' }, // dangling endpoint → dropped
|
|
];
|
|
const { links } = buildGraph(nodes, edges, 'contacts');
|
|
expect(links).toHaveLength(1);
|
|
expect(String(links[0].source)).toBe('c1');
|
|
expect(String(links[0].target)).toBe('p1');
|
|
});
|
|
|
|
it('createSimulation resolves link ids to node objects and converges', () => {
|
|
const nodes = [summary({ id: 'root' }), summary({ id: 'child', parent_id: 'root' })];
|
|
const { simNodes, links } = buildGraph(nodes, [], 'tree');
|
|
const sim = createSimulation(simNodes, links);
|
|
for (let i = 0; i < 50; i++) sim.tick();
|
|
// forceLink replaces string ids with the actual node objects.
|
|
expect((links[0].source as SimNode).id).toBe('child');
|
|
expect((links[0].target as SimNode).id).toBe('root');
|
|
expect(Number.isFinite(simNodes[0].x)).toBe(true);
|
|
sim.stop();
|
|
});
|
|
|
|
it('pickNode returns the nearest node within its disc, else null', () => {
|
|
const a: SimNode = { ...summary({ id: 'a', level: 0 }), x: 0, y: 0 };
|
|
const b: SimNode = { ...summary({ id: 'b', level: 0 }), x: 100, y: 0 };
|
|
expect(pickNode([a, b], 1, 1)?.id).toBe('a');
|
|
expect(pickNode([a, b], 99, 0)?.id).toBe('b');
|
|
expect(pickNode([a, b], 50, 50)).toBeNull(); // outside both discs
|
|
});
|
|
|
|
it('supportsWebGL is false under jsdom (no GL context)', () => {
|
|
expect(supportsWebGL()).toBe(false);
|
|
});
|
|
});
|