Spaces:
Sleeping
Sleeping
| 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']); | |
| }); | |
| }); | |