search.test.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. /**
  2. * Search Query Utilities Tests
  3. *
  4. * Tests multi-signal scoring, kind bonuses, path relevance, and API intent detection.
  5. */
  6. import { describe, it, expect } from 'vitest';
  7. import {
  8. extractSearchTerms,
  9. scorePathRelevance,
  10. kindBonus,
  11. detectApiIntent,
  12. inferRouteDirectories,
  13. STOP_WORDS,
  14. } from '../src/search/query-utils';
  15. describe('Search Query Utilities', () => {
  16. describe('extractSearchTerms', () => {
  17. it('should extract meaningful terms from a query', () => {
  18. const terms = extractSearchTerms('find the login handler');
  19. expect(terms).toContain('login');
  20. expect(terms).toContain('handler');
  21. // 'find' and 'the' are stop words
  22. expect(terms).not.toContain('find');
  23. expect(terms).not.toContain('the');
  24. });
  25. it('should filter stop words', () => {
  26. const terms = extractSearchTerms('how does the authentication work');
  27. expect(terms).not.toContain('how');
  28. expect(terms).not.toContain('does');
  29. expect(terms).not.toContain('the');
  30. expect(terms).toContain('authentication');
  31. expect(terms).toContain('work');
  32. });
  33. it('should handle camelCase by lowercasing', () => {
  34. const terms = extractSearchTerms('UserService');
  35. expect(terms).toContain('userservice');
  36. });
  37. it('should strip punctuation', () => {
  38. const terms = extractSearchTerms('payment.process()');
  39. expect(terms).toContain('payment');
  40. expect(terms).toContain('process');
  41. });
  42. it('should return empty for all stop words', () => {
  43. const terms = extractSearchTerms('how do I get the');
  44. expect(terms).toHaveLength(0);
  45. });
  46. it('should filter single-character terms', () => {
  47. const terms = extractSearchTerms('a b c auth');
  48. expect(terms).toEqual(['auth']);
  49. });
  50. });
  51. describe('scorePathRelevance', () => {
  52. it('should score filename matches highest', () => {
  53. const score = scorePathRelevance('src/auth/login.ts', 'login');
  54. expect(score).toBeGreaterThanOrEqual(10);
  55. });
  56. it('should score directory matches', () => {
  57. const score = scorePathRelevance('src/auth/index.ts', 'auth');
  58. expect(score).toBeGreaterThanOrEqual(5);
  59. });
  60. it('should return 0 for unrelated paths', () => {
  61. const score = scorePathRelevance('src/utils/format.ts', 'payment');
  62. expect(score).toBe(0);
  63. });
  64. it('should accumulate scores for multiple matching terms', () => {
  65. const score = scorePathRelevance('src/auth/login.ts', 'auth login');
  66. // Both 'auth' (dir match) and 'login' (filename match)
  67. expect(score).toBeGreaterThanOrEqual(15);
  68. });
  69. it('should return 0 for empty query terms', () => {
  70. const score = scorePathRelevance('src/auth/login.ts', 'the a an');
  71. expect(score).toBe(0);
  72. });
  73. });
  74. describe('kindBonus', () => {
  75. it('should give functions and methods highest bonus', () => {
  76. expect(kindBonus('function')).toBe(10);
  77. expect(kindBonus('method')).toBe(10);
  78. });
  79. it('should rank functions > classes > variables > imports', () => {
  80. expect(kindBonus('function')).toBeGreaterThan(kindBonus('class'));
  81. expect(kindBonus('class')).toBeGreaterThan(kindBonus('variable'));
  82. expect(kindBonus('variable')).toBeGreaterThan(kindBonus('import'));
  83. });
  84. it('should give routes high priority', () => {
  85. expect(kindBonus('route')).toBeGreaterThanOrEqual(9);
  86. });
  87. it('should give components high priority', () => {
  88. expect(kindBonus('component')).toBeGreaterThanOrEqual(8);
  89. });
  90. it('should return 0 for parameter and file kinds', () => {
  91. expect(kindBonus('parameter')).toBe(0);
  92. expect(kindBonus('file')).toBe(0);
  93. });
  94. it('should return 0 for unknown kinds', () => {
  95. expect(kindBonus('unknown_kind' as any)).toBe(0);
  96. });
  97. });
  98. describe('detectApiIntent', () => {
  99. it('should detect API-related queries', () => {
  100. expect(detectApiIntent('find the API endpoint for users')).toBe(true);
  101. expect(detectApiIntent('where is the login route')).toBe(true);
  102. expect(detectApiIntent('show me the request handler')).toBe(true);
  103. });
  104. it('should detect HTTP method patterns', () => {
  105. expect(detectApiIntent('GET /api/users')).toBe(true);
  106. expect(detectApiIntent('post /users/create')).toBe(true);
  107. });
  108. it('should detect REST and GraphQL', () => {
  109. expect(detectApiIntent('REST API for payments')).toBe(true);
  110. expect(detectApiIntent('GraphQL resolver for orders')).toBe(true);
  111. });
  112. it('should not detect non-API queries', () => {
  113. expect(detectApiIntent('fix the login bug')).toBe(false);
  114. expect(detectApiIntent('add dark mode support')).toBe(false);
  115. });
  116. it('should detect controller and middleware mentions', () => {
  117. expect(detectApiIntent('find the auth controller')).toBe(true);
  118. expect(detectApiIntent('CORS middleware configuration')).toBe(true);
  119. });
  120. });
  121. describe('inferRouteDirectories', () => {
  122. it('should detect route directories', () => {
  123. const files = [
  124. 'src/routes/auth.ts',
  125. 'src/routes/users.ts',
  126. 'src/utils/format.ts',
  127. ];
  128. const dirs = inferRouteDirectories(files);
  129. expect(dirs).toBeDefined();
  130. if (dirs) {
  131. expect(dirs.some(d => d.includes('route'))).toBe(true);
  132. }
  133. });
  134. it('should detect controller directories', () => {
  135. const files = [
  136. 'src/controllers/AuthController.ts',
  137. 'src/models/User.ts',
  138. ];
  139. const dirs = inferRouteDirectories(files);
  140. expect(dirs).toBeDefined();
  141. if (dirs) {
  142. expect(dirs.some(d => d.includes('controller'))).toBe(true);
  143. }
  144. });
  145. it('should detect api directories', () => {
  146. const files = [
  147. 'src/api/v1/users.ts',
  148. 'src/api/v1/orders.ts',
  149. ];
  150. const dirs = inferRouteDirectories(files);
  151. expect(dirs).toBeDefined();
  152. if (dirs) {
  153. expect(dirs.some(d => d.includes('api'))).toBe(true);
  154. }
  155. });
  156. it('should return undefined when no route dirs found', () => {
  157. const files = [
  158. 'src/utils/format.ts',
  159. 'src/models/User.ts',
  160. 'src/index.ts',
  161. ];
  162. const dirs = inferRouteDirectories(files);
  163. expect(dirs).toBeUndefined();
  164. });
  165. });
  166. describe('STOP_WORDS', () => {
  167. it('should contain common English stop words', () => {
  168. expect(STOP_WORDS.has('the')).toBe(true);
  169. expect(STOP_WORDS.has('and')).toBe(true);
  170. expect(STOP_WORDS.has('or')).toBe(true);
  171. });
  172. it('should contain action verbs used in queries', () => {
  173. expect(STOP_WORDS.has('find')).toBe(true);
  174. expect(STOP_WORDS.has('show')).toBe(true);
  175. expect(STOP_WORDS.has('get')).toBe(true);
  176. expect(STOP_WORDS.has('list')).toBe(true);
  177. });
  178. it('should not contain technical terms', () => {
  179. expect(STOP_WORDS.has('function')).toBe(false);
  180. expect(STOP_WORDS.has('class')).toBe(false);
  181. expect(STOP_WORDS.has('auth')).toBe(false);
  182. });
  183. });
  184. });