llm-semantic-visualizer / tests /CompareMode.test.js
hbauzan's picture
feat(ui): English-only product copy and EN Compare vocab
9fd0b69
Raw
History Blame Contribute Delete
3.95 kB
import { describe, it, expect } from 'vitest';
import * as THREE from 'three';
import { Instancer } from '../src/visualizer/Instancer.js';
describe('Compare Mode Sequence Engine', () => {
it('processes and instantiates token sequences from 1 to 1024', () => {
const scene = new THREE.Scene();
const instancer = new Instancer(scene);
const mockCompareResponse = {
count: 4,
items: [
{ id: 'tok_0', index: 0, text: 'king', embedding: new Array(768).fill(0.05) },
{ id: 'tok_1', index: 1, text: 'queen', embedding: new Array(768).fill(-0.05) },
{ id: 'tok_2', index: 2, text: 'man', embedding: new Array(768).fill(0.1) },
{ id: 'tok_3', index: 3, text: 'woman', embedding: new Array(768).fill(-0.1) },
]
};
const labels = instancer.renderCompareData(mockCompareResponse, 'POINTS', null, 'ANALYSIS');
expect(labels.length).toBe(4);
expect(labels[0].text).toBe('king');
expect(labels[1].text).toBe('queen');
expect(labels[2].text).toBe('man');
expect(labels[3].text).toBe('woman');
// Group should contain ribbon meshes, baseline mesh, and points mesh
expect(instancer.activeGroup.children.length).toBeGreaterThanOrEqual(5);
});
it('returns an empty list when compare response is null or empty', () => {
const scene = new THREE.Scene();
const instancer = new Instancer(scene);
const labels = instancer.renderCompareData(null, 'POINTS', null, 'NAVIGATION');
expect(labels).toEqual([]);
expect(instancer.activeGroup.children.length).toBe(0);
});
it('supports large sequences (e.g. 50 tokens)', () => {
const scene = new THREE.Scene();
const instancer = new Instancer(scene);
const items = [];
for (let i = 0; i < 50; i++) {
items.push({
id: `tok_${i}`,
index: i,
text: `token_${i}`,
embedding: new Array(768).fill(0.01 * (i + 1))
});
}
const mockCompareResponse = { count: 50, items };
const labels = instancer.renderCompareData(mockCompareResponse, 'POINTS', null, 'ANALYSIS');
expect(labels.length).toBe(50);
expect(labels[49].text).toBe('token_49');
});
it('reorders 3D threads with in-place tween (reuse meshes, new sequenceIndex)', async () => {
const scene = new THREE.Scene();
const instancer = new Instancer(scene);
const mockCompareResponse = {
count: 3,
anchor: { index: 0, text: 'king' },
items: [
{ id: 'tok_0', index: 0, text: 'king', embedding: [0.1, -0.1, 0.2], cosine_vs_first: 1 },
{ id: 'tok_1', index: 1, text: 'queen', embedding: [0.2, 0.1, -0.1], cosine_vs_first: 0.5 },
{ id: 'tok_2', index: 2, text: 'man', embedding: [-0.1, 0.2, 0.1], cosine_vs_first: 0.2 },
],
};
instancer.renderCompareData(mockCompareResponse, 'POINTS', null, 'ANALYSIS');
const ribbonBefore = instancer.compareRuntime.threads.map((t) => t.ribbonMesh);
const yBefore = instancer.compareRuntime.threads.map((t) => {
const attr = t.ribbonMesh.geometry.attributes.position;
return attr.getY(0);
});
// Instant tween for deterministic test
const labels = await instancer.animateCompareReorder(
['tok_1', 'tok_0', 'tok_2'],
{ duration: 0 }
);
// Same ribbon mesh identities (no destroy/rebuild)
expect(instancer.compareRuntime.threads.map((t) => t.ribbonMesh)).toEqual(ribbonBefore);
expect(instancer.compareRuntime.threads.map((t) => t.sequenceIndex)).toEqual([1, 0, 2]);
const yAfter = instancer.compareRuntime.threads.map((t) => {
const attr = t.ribbonMesh.geometry.attributes.position;
return attr.getY(0);
});
// tok_0 moved from slot 0 → 1 (lower Y in ANALYSIS); tok_1 from 1 → 0 (higher Y)
expect(yAfter[0]).toBeLessThan(yBefore[0]);
expect(yAfter[1]).toBeGreaterThan(yBefore[1]);
expect(labels.map((l) => l.id)).toEqual(['tok_0', 'tok_1', 'tok_2']);
});
});