23f7624596
ADR-166 MCP Bridge Security Lock / Static-source security lock (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / Compose default binds loopback + Mongo has auth (push) Failing after 2s
CodeQL Advanced / Analyze (rust) (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / plugin-agent-federation bindHost default (push) Failing after 1s
ADR-166 MCP Bridge Security Lock / Runtime behavior — 401 + terminal gate + fail-closed (push) Failing after 4s
business-pods-smoke / smoke (push) Failing after 1s
all-plugins-smoke / smoke-all (push) Failing after 2s
CI/CD Pipeline / Security & Code Quality (push) Failing after 1s
CI/CD Pipeline / Test Suite (ubuntu-latest) (push) Failing after 1s
CI/CD Pipeline / Build & Package (macos-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (ubuntu-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (windows-latest) (push) Has been skipped
CI/CD Pipeline / Documentation & Examples (push) Failing after 1s
Clone Tracker (14-day rolling) / Snapshot clones for ruflo ecosystem (push) Failing after 1s
CodeQL Advanced / Analyze (actions) (push) Failing after 1s
CodeQL Advanced / Analyze (javascript-typescript) (push) Failing after 1s
federation-peer-rust / stable-noop (push) Failing after 1s
metaharness-ci / score (push) Failing after 1s
metaharness-ci / router-compat (push) Failing after 0s
metaharness-ci / similarity-tests (push) Failing after 0s
no-agentbbs-smoke / smoke-without-agentbbs (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (windows-latest) (push) Has been skipped
codex-integration-audit / Codex integration audit (push) Failing after 1s
helpers-manifest-guard / guard (push) Failing after 1s
🔗 Cross-Agent Integration Tests / 🤝 Agent Coordination Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🧠 Memory Sharing Integration (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🛡️ Fault Tolerance Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / ⚡ Performance Integration Tests (push) Has been skipped
metaharness-ci / mcp-scan (push) Failing after 1s
metaharness-ci / eject-dryrun (push) Failing after 1s
metaharness-ci / metaharness-real-data (push) Failing after 0s
no-cli-optdep-bloat-2561 / guard (push) Failing after 1s
no-metaharness-smoke / smoke-without-metaharness (push) Failing after 1s
no-phantom-agentic-flow-subpath / guard (push) Failing after 1s
🔄 Automated Rollback Manager / 🚨 Failure Detection (push) Failing after 1s
V3 CI/CD Pipeline / Plugin hooks smoke / ubuntu-latest / Node 22 (push) Failing after 1s
V3 CI/CD Pipeline / ruflo-graph-intelligence build + test smoke (#2044, ADR-123) (push) Failing after 1s
CVE Audit Gate / Audit root (critical-blocking) (push) Failing after 2s
cost-tracker-smoke / smoke (push) Failing after 3s
oia-audit-weekly / audit (push) Failing after 2s
ruflo-agent-smoke / ruflo-agent structural smoke (push) Failing after 1s
📊 Status Badges Update / 📊 Update Status Badges (push) Failing after 1s
V3 CI/CD Pipeline / Static regression guards (#2267 YAML + (push) Failing after 1s
V3 CI/CD Pipeline / Test V3 Packages (push) Failing after 0s
V3 CI/CD Pipeline / agent_execute provider routing smoke (#2042) (push) Failing after 0s
CVE Audit Gate / Audit v3 (critical-blocking) (push) Failing after 1s
federation-peer-rust / stable-native (push) Failing after 2s
🔗 Cross-Agent Integration Tests / 🚀 Integration Test Setup (push) Failing after 2s
neural-trader-smoke / runtime-smoke (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (macos-latest) (push) Has been skipped
V3 CI/CD Pipeline / Build V3 (ubuntu-latest) (push) Has been skipped
V3 CI/CD Pipeline / Type Check V3 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 24 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 22 (push) Failing after 2s
V3 CI/CD Pipeline / browser rvf create flag smoke (#2015) (push) Failing after 0s
V3 CI/CD Pipeline / Dependency review (#2046) (push) Has been skipped
V3 CI/CD Pipeline / Supply-chain audit (#2046) (push) Failing after 0s
V3 CI/CD Pipeline / witness marker drift smoke (#2021) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader portfolio CG smoke (#2068, ADR-126 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader backtest signing smoke (#2068, ADR-126 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / kg-extract type-import classification smoke (#2049) (push) Failing after 0s
V3 CI/CD Pipeline / witness verify precondition smoke (#1880) (push) Failing after 2s
V3 CI/CD Pipeline / neural-trader pipeline risk-gate smoke (#2068, ADR-126 Phase 5) (push) Failing after 0s
V3 CI/CD Pipeline / neural-trader feature attribution smoke (#2068, ADR-126 Phase 6) (push) Failing after 0s
V3 CI/CD Pipeline / plugin-registry signature verification smoke (#1922, CWE-347) (push) Failing after 4s
V3 CI/CD Pipeline / memory stats legacy-DB smoke (#2120) (push) Failing after 4s
V3 CI/CD Pipeline / github deprecated actions smoke (#2089, ADR-127 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / graph query + pathfinder smoke (ADR-130 P2+P5) (push) Has been skipped
V3 CI/CD Pipeline / graph trajectory hooks smoke (ADR-130 P3) (push) Has been skipped
V3 CI/CD Pipeline / graph plugin adapter smoke (ADR-130 P4) (push) Has been skipped
V3 CI/CD Pipeline / graph benchmark (ADR-130 P6) (push) Has been skipped
V3 CI/CD Pipeline / statusline generator delegation smoke (#2195) (push) Failing after 1s
V3 CI/CD Pipeline / wizard init regression guard (#2206 (push) Failing after 1s
V3 CI/CD Pipeline / memory no-stray-db smoke (ADR-125 P7) (push) Failing after 1s
V3 CI/CD Pipeline / github-safe injection smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github actions pin smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github attribution opt-in smoke (#2089, ADR-127 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / pre-bash hook safety smoke (#2017) (push) Failing after 1s
V3 CI/CD Pipeline / Memory import smoke / ubuntu-latest (push) Failing after 0s
V3 CI/CD Pipeline / MCP protocol smoke / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / ruvllm WASM auto-init smoke (#2086) (push) Failing after 4s
V3 CI/CD Pipeline / MCP paired-tool round-trip smoke (#1889) (push) Failing after 1s
V3 CI/CD Pipeline / Plugin package install-safety (#1902/#1903/#1904) (push) Failing after 1s
V3 CI/CD Pipeline / Tool description discoverability (ADR-112) (push) Failing after 3s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (22) (push) Failing after 1s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (24) (push) Failing after 1s
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Vector-index dimension audit (#1947) (push) Failing after 0s
V3 CI/CD Pipeline / Hook-command install safety (#1921) (push) Failing after 1s
V3 CI/CD Pipeline / ToolOutputGuardrail smoke (ADR-131, (push) Failing after 1s
V3 CI/CD Pipeline / init-bundle invariants smoke (#2095, ADR-128 Phase 5) (push) Failing after 1s
V3 CI/CD Pipeline / wasm provider bridge smoke (ADR-129 P1) (push) Failing after 2s
V3 CI/CD Pipeline / wasm gallery CRUD smoke (ADR-129 P3) (push) Failing after 1s
V3 CI/CD Pipeline / wasm plugin bridge smoke (ADR-129 P4) (push) Failing after 0s
V3 CI/CD Pipeline / wasm compose smoke (ADR-129 P2) (push) Failing after 4s
V3 CI/CD Pipeline / graph schema smoke (ADR-130 P1) (push) Failing after 0s
Validate Marketplace / validate (push) Failing after 1s
🔍 Verification Pipeline / 🚀 Setup Verification (push) Failing after 1s
🔍 Verification Pipeline / 🛡️ Security Verification (push) Has been skipped
🔍 Verification Pipeline / 📝 Code Quality (push) Has been skipped
🔍 Verification Pipeline / 🧪 Test Verification (${{ matrix.os }}, Node ${{ matrix.node }}) (push) Has been skipped
🔍 Verification Pipeline / 🏗️ Build Verification (push) Has been skipped
🔍 Verification Pipeline / 📚 Documentation Verification (push) Has been skipped
CVE Audit Gate / High-severity report (warn only) (push) Has been cancelled
🔄 Automated Rollback Manager / 🔄 Execute Rollback (push) Has been cancelled
🔄 Automated Rollback Manager / ✅ Post-Rollback Verification (push) Has been cancelled
🔄 Automated Rollback Manager / 📊 Rollback Monitoring (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / windows-latest (push) Has been cancelled
🔄 Automated Rollback Manager / ⏳ Manual Rollback Approval (push) Has been cancelled
V3 CI/CD Pipeline / MCP protocol smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Memory import smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / ubuntu-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Publish to npm (alpha) (push) Has been cancelled
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / macos-latest / Node 22 (push) Has been cancelled
V3 CI/CD Pipeline / Plugin hooks smoke / macos-latest / Node 22 (push) Has been cancelled
CI/CD Pipeline / Deploy & Release (push) Has been cancelled
CI/CD Pipeline / CI Status (push) Has been cancelled
🔗 Cross-Agent Integration Tests / 📊 Integration Test Report (push) Has been cancelled
🔄 Automated Rollback Manager / 🔍 Pre-Rollback Validation (push) Has been cancelled
🔍 Verification Pipeline / ⚡ Performance Verification (push) Has been cancelled
🔍 Verification Pipeline / 📊 Verification Report (push) Has been cancelled
518 lines
15 KiB
TypeScript
518 lines
15 KiB
TypeScript
/**
|
|
* Hyperbolic Reasoning Plugin - Bridges Tests
|
|
*
|
|
* Tests for hyperbolic geometry bridge operations
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import {
|
|
clipToBall,
|
|
poincareDistance,
|
|
mobiusAdd,
|
|
expMap,
|
|
logMap,
|
|
POINCARE_BALL_EPS,
|
|
MAX_NORM,
|
|
} from '../src/types.js';
|
|
|
|
// Mock bridge implementation for testing
|
|
class MockHyperbolicBridge {
|
|
private _initialized = false;
|
|
private _embeddings = new Map<string, Float32Array>();
|
|
private _dimension = 32;
|
|
private _curvature = -1.0;
|
|
|
|
get initialized(): boolean {
|
|
return this._initialized;
|
|
}
|
|
|
|
async initialize(): Promise<void> {
|
|
this._initialized = true;
|
|
}
|
|
|
|
async dispose(): Promise<void> {
|
|
this._initialized = false;
|
|
this._embeddings.clear();
|
|
}
|
|
|
|
async embedHierarchy(
|
|
nodes: Array<{ id: string; parent: string | null }>,
|
|
config: { dimensions?: number; curvature?: number } = {}
|
|
): Promise<Map<string, Float32Array>> {
|
|
if (!this._initialized) {
|
|
throw new Error('Bridge not initialized');
|
|
}
|
|
|
|
this._dimension = config.dimensions ?? 32;
|
|
this._curvature = config.curvature ?? -1.0;
|
|
|
|
// Build tree structure
|
|
const children = new Map<string | null, string[]>();
|
|
for (const node of nodes) {
|
|
if (!children.has(node.parent)) {
|
|
children.set(node.parent, []);
|
|
}
|
|
children.get(node.parent)!.push(node.id);
|
|
}
|
|
|
|
// Find root
|
|
const roots = children.get(null) ?? [];
|
|
|
|
// BFS embedding - nodes closer to root have smaller norms
|
|
const embeddings = new Map<string, Float32Array>();
|
|
const queue: Array<{ id: string; depth: number }> = [];
|
|
|
|
for (const root of roots) {
|
|
queue.push({ id: root, depth: 0 });
|
|
}
|
|
|
|
while (queue.length > 0) {
|
|
const { id, depth } = queue.shift()!;
|
|
|
|
// Generate embedding based on depth
|
|
const norm = Math.min(MAX_NORM, depth * 0.15);
|
|
const embedding = new Float32Array(this._dimension);
|
|
|
|
// Random direction with controlled norm
|
|
let sqSum = 0;
|
|
for (let i = 0; i < this._dimension; i++) {
|
|
embedding[i] = Math.random() - 0.5;
|
|
sqSum += embedding[i] * embedding[i];
|
|
}
|
|
const scale = norm / Math.sqrt(sqSum);
|
|
for (let i = 0; i < this._dimension; i++) {
|
|
embedding[i] *= scale;
|
|
}
|
|
|
|
embeddings.set(id, clipToBall(embedding, this._curvature));
|
|
this._embeddings.set(id, embeddings.get(id)!);
|
|
|
|
// Add children to queue
|
|
for (const child of children.get(id) ?? []) {
|
|
queue.push({ id: child, depth: depth + 1 });
|
|
}
|
|
}
|
|
|
|
return embeddings;
|
|
}
|
|
|
|
computeDistance(aId: string, bId: string): number {
|
|
const a = this._embeddings.get(aId);
|
|
const b = this._embeddings.get(bId);
|
|
if (!a || !b) {
|
|
throw new Error('Node not found');
|
|
}
|
|
return poincareDistance(a, b, this._curvature);
|
|
}
|
|
|
|
search(queryId: string, k: number): Array<{ id: string; distance: number }> {
|
|
const query = this._embeddings.get(queryId);
|
|
if (!query) {
|
|
return [];
|
|
}
|
|
|
|
const results: Array<{ id: string; distance: number }> = [];
|
|
for (const [id, embedding] of this._embeddings) {
|
|
if (id !== queryId) {
|
|
const distance = poincareDistance(query, embedding, this._curvature);
|
|
results.push({ id, distance });
|
|
}
|
|
}
|
|
|
|
results.sort((a, b) => a.distance - b.distance);
|
|
return results.slice(0, k);
|
|
}
|
|
|
|
getEmbedding(id: string): Float32Array | undefined {
|
|
return this._embeddings.get(id);
|
|
}
|
|
}
|
|
|
|
describe('MockHyperbolicBridge', () => {
|
|
let bridge: MockHyperbolicBridge;
|
|
|
|
beforeEach(() => {
|
|
bridge = new MockHyperbolicBridge();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await bridge.dispose();
|
|
});
|
|
|
|
describe('initialization', () => {
|
|
it('should start uninitialized', () => {
|
|
expect(bridge.initialized).toBe(false);
|
|
});
|
|
|
|
it('should initialize successfully', async () => {
|
|
await bridge.initialize();
|
|
expect(bridge.initialized).toBe(true);
|
|
});
|
|
|
|
it('should cleanup on dispose', async () => {
|
|
await bridge.initialize();
|
|
await bridge.dispose();
|
|
expect(bridge.initialized).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('embedHierarchy', () => {
|
|
beforeEach(async () => {
|
|
await bridge.initialize();
|
|
});
|
|
|
|
it('should embed simple hierarchy', async () => {
|
|
const nodes = [
|
|
{ id: 'root', parent: null },
|
|
{ id: 'child1', parent: 'root' },
|
|
{ id: 'child2', parent: 'root' },
|
|
];
|
|
|
|
const embeddings = await bridge.embedHierarchy(nodes);
|
|
|
|
expect(embeddings.size).toBe(3);
|
|
expect(embeddings.get('root')).toBeDefined();
|
|
expect(embeddings.get('child1')).toBeDefined();
|
|
expect(embeddings.get('child2')).toBeDefined();
|
|
});
|
|
|
|
it('should place root near origin', async () => {
|
|
const nodes = [
|
|
{ id: 'root', parent: null },
|
|
{ id: 'child', parent: 'root' },
|
|
{ id: 'grandchild', parent: 'child' },
|
|
];
|
|
|
|
const embeddings = await bridge.embedHierarchy(nodes);
|
|
|
|
const rootNorm = Math.sqrt(
|
|
Array.from(embeddings.get('root')!).reduce((s, v) => s + v * v, 0)
|
|
);
|
|
const grandchildNorm = Math.sqrt(
|
|
Array.from(embeddings.get('grandchild')!).reduce((s, v) => s + v * v, 0)
|
|
);
|
|
|
|
expect(rootNorm).toBeLessThan(grandchildNorm);
|
|
});
|
|
|
|
it('should keep all embeddings within ball', async () => {
|
|
const nodes = [
|
|
{ id: 'root', parent: null },
|
|
{ id: 'l1', parent: 'root' },
|
|
{ id: 'l2', parent: 'l1' },
|
|
{ id: 'l3', parent: 'l2' },
|
|
{ id: 'l4', parent: 'l3' },
|
|
];
|
|
|
|
const embeddings = await bridge.embedHierarchy(nodes);
|
|
|
|
for (const [_, embedding] of embeddings) {
|
|
const norm = Math.sqrt(
|
|
Array.from(embedding).reduce((s, v) => s + v * v, 0)
|
|
);
|
|
expect(norm).toBeLessThan(1);
|
|
}
|
|
});
|
|
|
|
it('should throw when not initialized', async () => {
|
|
const newBridge = new MockHyperbolicBridge();
|
|
await expect(
|
|
newBridge.embedHierarchy([{ id: 'root', parent: null }])
|
|
).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('computeDistance', () => {
|
|
beforeEach(async () => {
|
|
await bridge.initialize();
|
|
await bridge.embedHierarchy([
|
|
{ id: 'root', parent: null },
|
|
{ id: 'child1', parent: 'root' },
|
|
{ id: 'child2', parent: 'root' },
|
|
{ id: 'grandchild', parent: 'child1' },
|
|
]);
|
|
});
|
|
|
|
it('should return 0 for same node', () => {
|
|
// Note: Since same ID returns same embedding, distance is 0
|
|
const embedding = bridge.getEmbedding('root')!;
|
|
const dist = poincareDistance(embedding, embedding, -1);
|
|
expect(dist).toBeCloseTo(0, 5);
|
|
});
|
|
|
|
it('should return positive distance for different nodes', () => {
|
|
const dist = bridge.computeDistance('root', 'child1');
|
|
expect(dist).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should be symmetric', () => {
|
|
const dist1 = bridge.computeDistance('child1', 'child2');
|
|
const dist2 = bridge.computeDistance('child2', 'child1');
|
|
expect(dist1).toBeCloseTo(dist2, 5);
|
|
});
|
|
|
|
it('should satisfy triangle inequality', () => {
|
|
const distAB = bridge.computeDistance('root', 'child1');
|
|
const distBC = bridge.computeDistance('child1', 'grandchild');
|
|
const distAC = bridge.computeDistance('root', 'grandchild');
|
|
|
|
expect(distAC).toBeLessThanOrEqual(distAB + distBC + 0.001);
|
|
});
|
|
});
|
|
|
|
describe('search', () => {
|
|
beforeEach(async () => {
|
|
await bridge.initialize();
|
|
await bridge.embedHierarchy([
|
|
{ id: 'root', parent: null },
|
|
{ id: 'child1', parent: 'root' },
|
|
{ id: 'child2', parent: 'root' },
|
|
{ id: 'child3', parent: 'root' },
|
|
]);
|
|
});
|
|
|
|
it('should find k nearest neighbors', () => {
|
|
const results = bridge.search('root', 2);
|
|
|
|
expect(results.length).toBe(2);
|
|
expect(results[0].distance).toBeLessThanOrEqual(results[1].distance);
|
|
});
|
|
|
|
it('should return results sorted by distance', () => {
|
|
const results = bridge.search('child1', 3);
|
|
|
|
for (let i = 1; i < results.length; i++) {
|
|
expect(results[i].distance).toBeGreaterThanOrEqual(results[i - 1].distance);
|
|
}
|
|
});
|
|
|
|
it('should exclude query node from results', () => {
|
|
const results = bridge.search('root', 10);
|
|
|
|
expect(results.every(r => r.id !== 'root')).toBe(true);
|
|
});
|
|
|
|
it('should return empty array for unknown node', () => {
|
|
const results = bridge.search('unknown', 5);
|
|
expect(results).toEqual([]);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Hyperbolic Geometry Operations', () => {
|
|
describe('clipToBall', () => {
|
|
it('should not modify vectors within ball', () => {
|
|
const vec = new Float32Array([0.1, 0.2, 0.3]);
|
|
const curvature = -1;
|
|
const clipped = clipToBall(vec, curvature);
|
|
|
|
for (let i = 0; i < vec.length; i++) {
|
|
expect(clipped[i]).toBeCloseTo(vec[i], 5);
|
|
}
|
|
});
|
|
|
|
it('should clip vectors outside ball', () => {
|
|
const vec = new Float32Array([0.9, 0.9, 0.9]); // norm > 1
|
|
const curvature = -1;
|
|
const clipped = clipToBall(vec, curvature);
|
|
|
|
const norm = Math.sqrt(clipped.reduce((s, v) => s + v * v, 0));
|
|
expect(norm).toBeLessThan(1);
|
|
});
|
|
|
|
it('should preserve direction', () => {
|
|
const vec = new Float32Array([2, 0, 0]);
|
|
const curvature = -1;
|
|
const clipped = clipToBall(vec, curvature);
|
|
|
|
// Should be along positive x-axis
|
|
expect(clipped[0]).toBeGreaterThan(0);
|
|
expect(Math.abs(clipped[1])).toBeLessThan(1e-10);
|
|
expect(Math.abs(clipped[2])).toBeLessThan(1e-10);
|
|
});
|
|
});
|
|
|
|
describe('poincareDistance', () => {
|
|
it('should return 0 for identical points', () => {
|
|
const vec = new Float32Array([0.1, 0.2, 0.3]);
|
|
const dist = poincareDistance(vec, vec, -1);
|
|
expect(dist).toBeCloseTo(0, 5);
|
|
});
|
|
|
|
it('should increase with Euclidean distance', () => {
|
|
const origin = new Float32Array([0, 0]);
|
|
const near = new Float32Array([0.1, 0]);
|
|
const far = new Float32Array([0.5, 0]);
|
|
|
|
const distNear = poincareDistance(origin, near, -1);
|
|
const distFar = poincareDistance(origin, far, -1);
|
|
|
|
expect(distNear).toBeLessThan(distFar);
|
|
});
|
|
|
|
it('should approach infinity near boundary', () => {
|
|
const origin = new Float32Array([0, 0]);
|
|
const nearBoundary = new Float32Array([0.999, 0]);
|
|
|
|
const dist = poincareDistance(origin, nearBoundary, -1);
|
|
expect(dist).toBeGreaterThan(5); // Very large distance
|
|
});
|
|
});
|
|
|
|
describe('mobiusAdd', () => {
|
|
it('should return y when x is zero', () => {
|
|
const x = new Float32Array([0, 0]);
|
|
const y = new Float32Array([0.3, 0.4]);
|
|
const result = mobiusAdd(x, y, -1);
|
|
|
|
expect(result[0]).toBeCloseTo(y[0], 4);
|
|
expect(result[1]).toBeCloseTo(y[1], 4);
|
|
});
|
|
|
|
it('should return x when y is zero', () => {
|
|
const x = new Float32Array([0.3, 0.4]);
|
|
const y = new Float32Array([0, 0]);
|
|
const result = mobiusAdd(x, y, -1);
|
|
|
|
expect(result[0]).toBeCloseTo(x[0], 4);
|
|
expect(result[1]).toBeCloseTo(x[1], 4);
|
|
});
|
|
|
|
it('should keep result within ball', () => {
|
|
const x = new Float32Array([0.5, 0.5]);
|
|
const y = new Float32Array([0.4, 0.4]);
|
|
const result = mobiusAdd(x, y, -1);
|
|
|
|
const norm = Math.sqrt(result.reduce((s, v) => s + v * v, 0));
|
|
expect(norm).toBeLessThan(1);
|
|
});
|
|
|
|
it('should be non-commutative', () => {
|
|
const x = new Float32Array([0.3, 0]);
|
|
const y = new Float32Array([0, 0.4]);
|
|
|
|
const xy = mobiusAdd(x, y, -1);
|
|
const yx = mobiusAdd(y, x, -1);
|
|
|
|
// In hyperbolic space, addition is generally non-commutative
|
|
const same = xy[0] === yx[0] && xy[1] === yx[1];
|
|
// This may or may not be equal depending on the values
|
|
});
|
|
});
|
|
|
|
describe('expMap and logMap', () => {
|
|
it('should be inverses near origin', () => {
|
|
const v = new Float32Array([0.1, 0.1, 0.1]);
|
|
const curvature = -1;
|
|
|
|
const expV = expMap(v, curvature);
|
|
const logExpV = logMap(expV, curvature);
|
|
|
|
for (let i = 0; i < v.length; i++) {
|
|
expect(logExpV[i]).toBeCloseTo(v[i], 2);
|
|
}
|
|
});
|
|
|
|
it('should return zero for zero input', () => {
|
|
const zero = new Float32Array([0, 0, 0]);
|
|
|
|
const expZero = expMap(zero, -1);
|
|
const logZero = logMap(zero, -1);
|
|
|
|
expect(expZero.every(v => Math.abs(v) < 1e-10)).toBe(true);
|
|
expect(logZero.every(v => Math.abs(v) < 1e-10)).toBe(true);
|
|
});
|
|
|
|
it('should map to inside ball', () => {
|
|
const v = new Float32Array([1, 2, 3]); // Large tangent vector
|
|
const result = expMap(v, -1);
|
|
|
|
const norm = Math.sqrt(result.reduce((s, x) => s + x * x, 0));
|
|
expect(norm).toBeLessThan(1);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Hierarchy Comparison', () => {
|
|
function computeTreeEditDistance(
|
|
tree1: Array<{ id: string; parent: string | null }>,
|
|
tree2: Array<{ id: string; parent: string | null }>
|
|
): number {
|
|
// Simplified tree edit distance for testing
|
|
const ids1 = new Set(tree1.map(n => n.id));
|
|
const ids2 = new Set(tree2.map(n => n.id));
|
|
|
|
let distance = 0;
|
|
|
|
// Insertions (in tree2 but not tree1)
|
|
for (const id of ids2) {
|
|
if (!ids1.has(id)) distance++;
|
|
}
|
|
|
|
// Deletions (in tree1 but not tree2)
|
|
for (const id of ids1) {
|
|
if (!ids2.has(id)) distance++;
|
|
}
|
|
|
|
// Rename/move (same id, different parent)
|
|
for (const n1 of tree1) {
|
|
const n2 = tree2.find(n => n.id === n1.id);
|
|
if (n2 && n1.parent !== n2.parent) {
|
|
distance++;
|
|
}
|
|
}
|
|
|
|
return distance;
|
|
}
|
|
|
|
it('should return 0 for identical trees', () => {
|
|
const tree = [
|
|
{ id: 'root', parent: null },
|
|
{ id: 'child', parent: 'root' },
|
|
];
|
|
|
|
const dist = computeTreeEditDistance(tree, tree);
|
|
expect(dist).toBe(0);
|
|
});
|
|
|
|
it('should count insertions', () => {
|
|
const tree1 = [{ id: 'root', parent: null }];
|
|
const tree2 = [
|
|
{ id: 'root', parent: null },
|
|
{ id: 'new', parent: 'root' },
|
|
];
|
|
|
|
const dist = computeTreeEditDistance(tree1, tree2);
|
|
expect(dist).toBe(1);
|
|
});
|
|
|
|
it('should count deletions', () => {
|
|
const tree1 = [
|
|
{ id: 'root', parent: null },
|
|
{ id: 'old', parent: 'root' },
|
|
];
|
|
const tree2 = [{ id: 'root', parent: null }];
|
|
|
|
const dist = computeTreeEditDistance(tree1, tree2);
|
|
expect(dist).toBe(1);
|
|
});
|
|
|
|
it('should count parent changes', () => {
|
|
const tree1 = [
|
|
{ id: 'root', parent: null },
|
|
{ id: 'a', parent: 'root' },
|
|
{ id: 'b', parent: 'root' },
|
|
{ id: 'c', parent: 'a' },
|
|
];
|
|
const tree2 = [
|
|
{ id: 'root', parent: null },
|
|
{ id: 'a', parent: 'root' },
|
|
{ id: 'b', parent: 'root' },
|
|
{ id: 'c', parent: 'b' }, // Moved from a to b
|
|
];
|
|
|
|
const dist = computeTreeEditDistance(tree1, tree2);
|
|
expect(dist).toBe(1);
|
|
});
|
|
});
|