import { describe, it, expect } from 'vitest' import { fuzzyMatch, searchRank, bestSearchRank } from './fuzzyMatch' describe('fuzzyMatch', () => { it('matches exact string', () => { const result = fuzzyMatch('hello', 'hello') expect(result.match).toBe(true) expect(result.score).toBeGreaterThan(0) }) it('matches case-insensitively', () => { expect(fuzzyMatch('hello', 'Hello World').match).toBe(true) }) it('matches subsequence chars in order', () => { expect(fuzzyMatch('cnt', 'Create New Type').match).toBe(true) }) it('rejects when chars are not all present', () => { expect(fuzzyMatch('xyz', 'hello').match).toBe(false) }) it('rejects when chars are out of order', () => { expect(fuzzyMatch('ba', 'abc').match).toBe(false) }) it('returns higher score for consecutive matches', () => { const consecutive = fuzzyMatch('com', 'Commit & Push') const scattered = fuzzyMatch('cmt', 'Commit & Push') expect(consecutive.score).toBeGreaterThan(scattered.score) }) it('gives bonus for word-start matches', () => { const wordStart = fuzzyMatch('cp', 'Commit Push') const midWord = fuzzyMatch('om', 'Commit Push') expect(wordStart.score).toBeGreaterThan(midWord.score) }) it('matches empty query against any string', () => { expect(fuzzyMatch('', 'anything').match).toBe(true) }) it('handles empty target', () => { expect(fuzzyMatch('a', '').match).toBe(false) }) }) describe('searchRank', () => { it('returns 0 for exact match', () => { expect(searchRank('Refactoring', 'Refactoring')).toBe(0) }) it('returns 0 for case-insensitive exact match', () => { expect(searchRank('refactoring', 'Refactoring')).toBe(0) }) it('returns 1 for prefix match', () => { expect(searchRank('Refactoring', 'Refactoring Ideas')).toBe(1) }) it('returns 1 for case-insensitive prefix match', () => { expect(searchRank('quarter', 'Quarter Review')).toBe(1) }) it('returns 2 for non-prefix fuzzy match', () => { expect(searchRank('Ideas', 'Refactoring Ideas')).toBe(2) }) }) describe('bestSearchRank', () => { it('returns 0 for title exact match', () => { expect(bestSearchRank('Refactoring', 'Refactoring', [])).toBe(0) }) it('returns 0 for title exact match with aliases present', () => { expect(bestSearchRank('Refactoring', 'Refactoring', ['refactor', 'cleanup'])).toBe(0) }) it('returns 1 for alias exact match (never 0)', () => { expect(bestSearchRank('ref', 'Refactoring Notes', ['ref'])).toBe(1) }) it('ranks alias exact match above title prefix match', () => { expect(bestSearchRank('ref', 'Reference Manual', ['ref'])).toBe(1) // title prefix would be rank 2, alias exact is rank 1 }) it('returns 2 for title prefix match (no alias boost)', () => { expect(bestSearchRank('Refactoring', 'Refactoring Ideas', [])).toBe(2) }) it('returns 3 for alias prefix match only', () => { expect(bestSearchRank('ref', 'Something Else', ['refactor'])).toBe(3) }) it('returns 4 when nothing matches as exact or prefix', () => { expect(bestSearchRank('ideas', 'Refactoring Ideas', ['thoughts'])).toBe(4) }) it('title exact match always beats alias exact match', () => { const titleExact = bestSearchRank('Refactoring', 'Refactoring', ['refactor']) const aliasExact = bestSearchRank('Refactoring', 'Refactoring Ideas', ['Refactoring']) expect(titleExact).toBeLessThan(aliasExact) }) it('handles trimmed whitespace in title and query', () => { expect(bestSearchRank('Refactoring ', ' Refactoring', [])).toBe(0) }) })