query-paths.test.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. /**
  2. * File-path recognition in explore queries (src/search/query-paths.ts).
  3. *
  4. * The originating bug: an agent named two SvelteKit route files by exact path
  5. * (`src/routes/m/projects/[id]/runs/[runId]/+page.svelte`) and the explore
  6. * pipeline shredded them — the seeding tokenizer splits on brackets, so the
  7. * fragments `runId`/`scope` seeded as "named symbols" and headlined the blast
  8. * radius, while FTS admitted every sibling `+page.svelte` off the `page`/`runs`
  9. * fragments. These tests pin the module that stops that: path spans resolve
  10. * against the indexed file list, matching files pin, and the spans leave the
  11. * query. Resolution IS the detector — slash-bearing non-paths stay untouched.
  12. */
  13. import { describe, it, expect } from 'vitest';
  14. import { extractQueryPaths, queryMightContainPaths } from '../src/search/query-paths';
  15. const INDEX = [
  16. 'src/routes/m/projects/[id]/runs/[runId]/+page.svelte',
  17. 'src/routes/m/projects/[id]/chat/[scope]/+page.svelte',
  18. 'src/routes/m/projects/[id]/+page.svelte',
  19. 'src/routes/(protected)/chat-window/+page.svelte',
  20. 'src/lib/chat-manager.ts',
  21. 'src/lib/task-runner-manager.ts',
  22. 'src/lib/stores/sqlite-store.ts',
  23. 'src/lib/stores/postgresql-store.ts',
  24. ];
  25. describe('queryMightContainPaths — the cheap pre-gate', () => {
  26. it('fires on slashes and dotted basenames', () => {
  27. expect(queryMightContainPaths('look at src/lib/chat-manager.ts')).toBe(true);
  28. expect(queryMightContainPaths('look at chat-manager.ts please')).toBe(true);
  29. });
  30. it('stays quiet on plain prose and Class.method spans', () => {
  31. expect(queryMightContainPaths('how does the scroll pinning work')).toBe(false);
  32. // `.isPackaged` is 10 chars — past the 8-char extension cap.
  33. expect(queryMightContainPaths('what reads app.isPackaged here')).toBe(false);
  34. });
  35. });
  36. describe('extractQueryPaths — resolution and stripping', () => {
  37. it('resolves a bracketed SvelteKit path and strips it from the query', () => {
  38. const q = 'auto-scroll logic in src/routes/m/projects/[id]/runs/[runId]/+page.svelte — atBottom tracking';
  39. const out = extractQueryPaths(q, INDEX);
  40. expect(out.pinnedFiles).toEqual(['src/routes/m/projects/[id]/runs/[runId]/+page.svelte']);
  41. expect(out.strippedQuery).not.toContain('+page.svelte');
  42. expect(out.strippedQuery).not.toContain('runId');
  43. expect(out.strippedQuery).toContain('atBottom tracking');
  44. expect(out.unresolvedPathSpans).toEqual([]);
  45. });
  46. it('pins multiple named files in appearance order', () => {
  47. const q = 'compare src/routes/m/projects/[id]/chat/[scope]/+page.svelte and src/routes/m/projects/[id]/runs/[runId]/+page.svelte';
  48. const out = extractQueryPaths(q, INDEX);
  49. expect(out.pinnedFiles).toEqual([
  50. 'src/routes/m/projects/[id]/chat/[scope]/+page.svelte',
  51. 'src/routes/m/projects/[id]/runs/[runId]/+page.svelte',
  52. ]);
  53. });
  54. it('resolves a (protected) route-group path — parens are path characters', () => {
  55. const out = extractQueryPaths('read src/routes/(protected)/chat-window/+page.svelte', INDEX);
  56. expect(out.pinnedFiles).toEqual(['src/routes/(protected)/chat-window/+page.svelte']);
  57. });
  58. it('resolves an absolute path by walking suffixes to the indexed relative path', () => {
  59. const q = 'fix /Users/colby/dev/beads-live-dashboard/src/lib/chat-manager.ts';
  60. const out = extractQueryPaths(q, INDEX);
  61. expect(out.pinnedFiles).toEqual(['src/lib/chat-manager.ts']);
  62. });
  63. it('resolves a unique basename and a partial path', () => {
  64. expect(extractQueryPaths('see chat-manager.ts', INDEX).pinnedFiles)
  65. .toEqual(['src/lib/chat-manager.ts']);
  66. expect(extractQueryPaths('see stores/sqlite-store.ts', INDEX).pinnedFiles)
  67. .toEqual(['src/lib/stores/sqlite-store.ts']);
  68. });
  69. it('strips wrapping punctuation and line references', () => {
  70. const out = extractQueryPaths('the bug (see `src/lib/chat-manager.ts:243`).', INDEX);
  71. expect(out.pinnedFiles).toEqual(['src/lib/chat-manager.ts']);
  72. const hash = extractQueryPaths('regression at src/lib/task-runner-manager.ts#L88-L120', INDEX);
  73. expect(hash.pinnedFiles).toEqual(['src/lib/task-runner-manager.ts']);
  74. });
  75. it('treats an over-ambiguous basename as unresolved — stripped and reported', () => {
  76. const out = extractQueryPaths('why do all +page.svelte files flash', INDEX);
  77. expect(out.pinnedFiles).toEqual([]);
  78. expect(out.unresolvedPathSpans).toEqual(['+page.svelte']);
  79. expect(out.strippedQuery).toBe('why do all files flash');
  80. });
  81. it('strips and reports a clearly-path-shaped span that matches nothing', () => {
  82. const out = extractQueryPaths('crash in src/routes/gone/missing-page.svelte on load', INDEX);
  83. expect(out.pinnedFiles).toEqual([]);
  84. expect(out.unresolvedPathSpans).toEqual(['src/routes/gone/missing-page.svelte']);
  85. expect(out.strippedQuery).toBe('crash in on load');
  86. });
  87. it('leaves slash-bearing non-paths alone', () => {
  88. const q = 'does gen_server:call/2 block and/or timeout';
  89. const out = extractQueryPaths(q, INDEX);
  90. expect(out.pinnedFiles).toEqual([]);
  91. expect(out.unresolvedPathSpans).toEqual([]);
  92. expect(out.strippedQuery).toBe(q);
  93. });
  94. it('dedupes a path named twice and honors maxPins', () => {
  95. const twice = extractQueryPaths(
  96. 'src/lib/chat-manager.ts wraps src/lib/chat-manager.ts', INDEX,
  97. );
  98. expect(twice.pinnedFiles).toEqual(['src/lib/chat-manager.ts']);
  99. const capped = extractQueryPaths(
  100. 'src/lib/chat-manager.ts src/lib/task-runner-manager.ts', INDEX, { maxPins: 1 },
  101. );
  102. expect(capped.pinnedFiles).toEqual(['src/lib/chat-manager.ts']);
  103. });
  104. it('matches case-insensitively but returns the indexed spelling', () => {
  105. const out = extractQueryPaths('SRC/LIB/CHAT-MANAGER.TS', INDEX);
  106. expect(out.pinnedFiles).toEqual(['src/lib/chat-manager.ts']);
  107. });
  108. it('passes through untouched when nothing resolves', () => {
  109. const q = 'plain prose question about scrolling';
  110. const out = extractQueryPaths(q, INDEX);
  111. expect(out).toEqual({ strippedQuery: q, pinnedFiles: [], unresolvedPathSpans: [] });
  112. });
  113. });