397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
154 lines
6.2 KiB
JavaScript
154 lines
6.2 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { encodeBase64 } from './router-embedding.mjs';
|
|
import {
|
|
cosineSimilarity,
|
|
buildIndex,
|
|
findNearestNeighbors,
|
|
majorityOutcome,
|
|
mapOutcomeToFamily,
|
|
} from './observer-embedding-index.mjs';
|
|
|
|
// Helpers — build a base64-encoded Float32Array embedding from a plain array.
|
|
function emb(arr) {
|
|
return encodeBase64(new Float32Array(arr));
|
|
}
|
|
|
|
describe('cosineSimilarity', () => {
|
|
it('returns 1 for identical unit vectors', () => {
|
|
const a = new Float32Array([1, 0, 0, 0]);
|
|
expect(cosineSimilarity(a, a)).toBeCloseTo(1, 6);
|
|
});
|
|
it('returns 0 for orthogonal vectors', () => {
|
|
const a = new Float32Array([1, 0, 0, 0]);
|
|
const b = new Float32Array([0, 1, 0, 0]);
|
|
expect(cosineSimilarity(a, b)).toBeCloseTo(0, 6);
|
|
});
|
|
it('returns negative for opposed vectors', () => {
|
|
const a = new Float32Array([1, 0, 0, 0]);
|
|
const b = new Float32Array([-1, 0, 0, 0]);
|
|
expect(cosineSimilarity(a, b)).toBeCloseTo(-1, 6);
|
|
});
|
|
it('handles unequal dimensions by returning 0 (guard against malformed input)', () => {
|
|
expect(cosineSimilarity(new Float32Array([1, 0]), new Float32Array([1, 0, 0]))).toBe(0);
|
|
});
|
|
it('returns 0 if either side is null / empty', () => {
|
|
expect(cosineSimilarity(null, new Float32Array([1]))).toBe(0);
|
|
expect(cosineSimilarity(new Float32Array([1]), null)).toBe(0);
|
|
expect(cosineSimilarity(new Float32Array([]), new Float32Array([]))).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('mapOutcomeToFamily', () => {
|
|
it('maps success / soft_success to "success"', () => {
|
|
expect(mapOutcomeToFamily('success')).toBe('success');
|
|
expect(mapOutcomeToFamily('soft_success')).toBe('success');
|
|
});
|
|
it('maps rework to "retry"', () => {
|
|
expect(mapOutcomeToFamily('rework')).toBe('retry');
|
|
});
|
|
it('maps blocked / partial to "failure"', () => {
|
|
expect(mapOutcomeToFamily('blocked')).toBe('failure');
|
|
expect(mapOutcomeToFamily('partial')).toBe('failure');
|
|
});
|
|
it('returns null for unknown / unresolved outcomes', () => {
|
|
expect(mapOutcomeToFamily('unknown')).toBeNull();
|
|
expect(mapOutcomeToFamily(null)).toBeNull();
|
|
expect(mapOutcomeToFamily('')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('buildIndex', () => {
|
|
it('includes episodes with a base64 embedding AND a resolved outcome', () => {
|
|
const eps = [
|
|
{ task_id: 'a', timestamps: { started_at: '2026-05-25T10:00:00Z' }, prompt_embedding_base64: emb([1, 0, 0, 0]), _inferredOutcome: 'success' },
|
|
{ task_id: 'b', timestamps: { started_at: '2026-05-25T11:00:00Z' }, prompt_embedding_base64: emb([0, 1, 0, 0]), _inferredOutcome: 'rework' },
|
|
];
|
|
const idx = buildIndex(eps);
|
|
expect(idx).toHaveLength(2);
|
|
expect(idx[0].task_id).toBe('a');
|
|
expect(idx[0].family).toBe('success');
|
|
expect(idx[1].family).toBe('retry');
|
|
expect(idx[0].embedding).toBeInstanceOf(Float32Array);
|
|
});
|
|
|
|
it('skips episodes without an embedding', () => {
|
|
const eps = [
|
|
{ task_id: 'a', prompt_embedding_base64: null, _inferredOutcome: 'success' },
|
|
{ task_id: 'b', prompt_embedding_base64: emb([1, 0, 0, 0]), _inferredOutcome: 'success' },
|
|
];
|
|
expect(buildIndex(eps)).toHaveLength(1);
|
|
});
|
|
|
|
it('skips episodes with unresolved outcome (unknown / null)', () => {
|
|
const eps = [
|
|
{ task_id: 'a', prompt_embedding_base64: emb([1, 0, 0, 0]), _inferredOutcome: 'unknown' },
|
|
{ task_id: 'b', prompt_embedding_base64: emb([0, 1, 0, 0]), _inferredOutcome: 'success' },
|
|
];
|
|
expect(buildIndex(eps)).toHaveLength(1);
|
|
});
|
|
|
|
it('skips episodes with broken / non-decodable embedding', () => {
|
|
const eps = [
|
|
{ task_id: 'a', prompt_embedding_base64: 'not-base64!!!', _inferredOutcome: 'success' },
|
|
{ task_id: 'b', prompt_embedding_base64: emb([1, 0, 0, 0]), _inferredOutcome: 'success' },
|
|
];
|
|
expect(buildIndex(eps)).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('findNearestNeighbors', () => {
|
|
const idx = [
|
|
{ task_id: 'a', family: 'success', embedding: new Float32Array([1, 0, 0, 0]) },
|
|
{ task_id: 'b', family: 'success', embedding: new Float32Array([0.9, 0.4, 0, 0]) },
|
|
{ task_id: 'c', family: 'retry', embedding: new Float32Array([0, 1, 0, 0]) },
|
|
{ task_id: 'd', family: 'failure', embedding: new Float32Array([0, 0, 1, 0]) },
|
|
{ task_id: 'e', family: 'success', embedding: new Float32Array([0.7, 0.7, 0, 0]) },
|
|
];
|
|
|
|
it('returns top-k by cosine similarity, highest first', () => {
|
|
const target = new Float32Array([1, 0, 0, 0]);
|
|
const nn = findNearestNeighbors(target, idx, 3);
|
|
expect(nn).toHaveLength(3);
|
|
expect(nn[0].task_id).toBe('a'); // exact match
|
|
expect(nn[0].similarity).toBeCloseTo(1, 6);
|
|
expect(['b', 'e']).toContain(nn[1].task_id); // close to e1
|
|
expect(nn[2].similarity).toBeLessThan(nn[1].similarity + 1e-6);
|
|
});
|
|
|
|
it('handles k larger than index size (returns all)', () => {
|
|
const nn = findNearestNeighbors(new Float32Array([1, 0, 0, 0]), idx, 100);
|
|
expect(nn.length).toBe(idx.length);
|
|
});
|
|
|
|
it('returns empty array if target is null / index empty', () => {
|
|
expect(findNearestNeighbors(null, idx, 3)).toEqual([]);
|
|
expect(findNearestNeighbors(new Float32Array([1, 0, 0, 0]), [], 3)).toEqual([]);
|
|
});
|
|
|
|
it('excludes a self-reference when excludeTaskId is passed', () => {
|
|
const target = new Float32Array([1, 0, 0, 0]);
|
|
const nn = findNearestNeighbors(target, idx, 3, { excludeTaskId: 'a' });
|
|
expect(nn.find((n) => n.task_id === 'a')).toBeUndefined();
|
|
expect(nn).toHaveLength(3);
|
|
});
|
|
});
|
|
|
|
describe('majorityOutcome', () => {
|
|
it('returns the dominant family when one wins outright', () => {
|
|
expect(majorityOutcome([{ family: 'success' }, { family: 'success' }, { family: 'retry' }])).toBe('success');
|
|
});
|
|
|
|
it('returns "mixed" on a tie at the top', () => {
|
|
expect(majorityOutcome([{ family: 'success' }, { family: 'retry' }])).toBe('mixed');
|
|
expect(majorityOutcome([{ family: 'success' }, { family: 'retry' }, { family: 'failure' }])).toBe('mixed');
|
|
});
|
|
|
|
it('returns "no_neighbors" on empty input', () => {
|
|
expect(majorityOutcome([])).toBe('no_neighbors');
|
|
expect(majorityOutcome(null)).toBe('no_neighbors');
|
|
});
|
|
});
|
|
|
|
// Analyzer integration covered separately in brain-retro-analyzer.test.mjs
|
|
// (similar_past_outcome_majority axis lands via analyze()).
|