import { test, expect } from '@playwright/test'; import { calculateRRFK } from '../../lib/utils'; test.describe('RRF K Calculation Tests', () => { test('should return minimum k=20 for small datasets', () => { // For small datasets (< 200 notes), k should be 20 expect(calculateRRFK(0)).toBe(20); expect(calculateRRFK(10)).toBe(20); expect(calculateRRFK(50)).toBe(20); expect(calculateRRFK(100)).toBe(20); expect(calculateRRFK(199)).toBe(20); }); test('should return k=20 for exactly 200 notes', () => { expect(calculateRRFK(200)).toBe(20); }); test('should scale k for larger datasets', () => { // k = max(20, totalNotes / 10) expect(calculateRRFK(500)).toBe(50); // 500/10 = 50 expect(calculateRRFK(1000)).toBe(100); // 1000/10 = 100 expect(calculateRRFK(250)).toBe(25); // 250/10 = 25 }); test('should handle edge cases', () => { expect(calculateRRFK(201)).toBe(20); // 201/10 = 20.1 → floor → 20, max(20,20) = 20 expect(calculateRRFK(210)).toBe(21); // 210/10 = 21 expect(calculateRRFK(10000)).toBe(1000); // 10000/10 = 1000 }); test('k should always be at least 20', () => { // Even for very small datasets, k should not be below 20 for (let i = 0; i <= 200; i++) { expect(calculateRRFK(i)).toBeGreaterThanOrEqual(20); } }); test('should be lower than old value for typical datasets', () => { // For typical user datasets (< 500 notes), new k should be lower than old k=60 expect(calculateRRFK(100)).toBeLessThan(60); expect(calculateRRFK(200)).toBeLessThan(60); expect(calculateRRFK(300)).toBe(30); // 300/10 = 30 expect(calculateRRFK(500)).toBe(50); // 500/10 = 50 }); test('should surpass old value for very large datasets', () => { // For very large datasets (> 600 notes), k should be higher than 60 expect(calculateRRFK(700)).toBe(70); // 700/10 = 70 expect(calculateRRFK(1000)).toBe(100); }); }); test.describe('RRF Ranking Behavior Tests', () => { test('RRF with lower k penalizes low ranks more', () => { // Simulate RRF scores for a note at rank 5 // Formula: score = 1 / (k + rank) const rank = 5; const scoreWithK20 = 1 / (20 + rank); // 1/25 = 0.04 const scoreWithK60 = 1 / (60 + rank); // 1/65 = 0.015 // Lower k gives higher score to low ranks expect(scoreWithK20).toBeGreaterThan(scoreWithK60); }); test('RRF favors items ranked high in both lists', () => { // Note A: rank 1 in both lists const scoreA_K20 = 1 / (20 + 1) + 1 / (20 + 1); // 2/21 ≈ 0.095 // Note B: rank 1 in one list, rank 10 in other const scoreB_K20 = 1 / (20 + 1) + 1 / (20 + 10); // 1/21 + 1/30 ≈ 0.081 // Note C: rank 5 in both lists const scoreC_K20 = 1 / (20 + 5) + 1 / (20 + 5); // 2/25 = 0.08 // Note A should have highest score (consistently high) expect(scoreA_K20).toBeGreaterThan(scoreB_K20); expect(scoreA_K20).toBeGreaterThan(scoreC_K20); }); test('k=20 vs k=60 ranking difference', () => { // Note at rank 20 const rank = 20; const scoreWithK20 = 1 / (20 + rank); // 1/40 = 0.025 const scoreWithK60 = 1 / (60 + rank); // 1/80 = 0.0125 // With k=20, rank 20 is scored 2x higher than with k=60 expect(scoreWithK20 / scoreWithK60).toBeCloseTo(2.0, 1); }); test('RRF score should decrease as rank increases', () => { const k = 20; const scoreRank1 = 1 / (k + 1); const scoreRank5 = 1 / (k + 5); const scoreRank10 = 1 / (k + 10); const scoreRank50 = 1 / (k + 50); expect(scoreRank1).toBeGreaterThan(scoreRank5); expect(scoreRank5).toBeGreaterThan(scoreRank10); expect(scoreRank10).toBeGreaterThan(scoreRank50); }); test('RRF handles missing ranks gracefully', () => { // If a note is not in a list, it gets the max rank const k = 20; const totalNotes = 100; const missingRank = totalNotes; // Treated as worst rank const scoreWithMissing = 1 / (k + missingRank); const scoreWithRank50 = 1 / (k + 50); // Missing rank should give much lower score expect(scoreWithMissing).toBeLessThan(scoreWithRank50); }); }); test.describe('RRF Adaptive Behavior Tests', () => { test('adaptive k provides better rankings for small datasets', () => { // For 50 notes: k=20 (adaptive) vs k=60 (old) const totalNotes = 50; const kAdaptive = calculateRRFK(totalNotes); // 20 const kOld = 60; // Compare scores for rank 10 (20% of dataset) const rank = 10; const scoreAdaptive = 1 / (kAdaptive + rank); const scoreOld = 1 / (kOld + rank); // Adaptive k gives higher score to mid-rank items in small datasets expect(scoreAdaptive).toBeGreaterThan(scoreOld); }); test('adaptive k scales appropriately', () => { const datasets = [10, 50, 100, 200, 500, 1000]; datasets.forEach(notes => { const k = calculateRRFK(notes); // k should always be at least 20 expect(k).toBeGreaterThanOrEqual(20); // k should scale with dataset size if (notes < 200) { expect(k).toBe(20); } else { expect(k).toBe(Math.floor(notes / 10)); } }); }); });