fix(MULTIVERSION-0001): fix version isolation, 404 on unknown version, commit-hash lookup, and searchModeUsed

Bug 1: Thread version tag from run() into crawl() via getVersionTag() helper so
LocalCrawler and GithubCrawler receive the correct ref when indexing a named
version instead of always crawling HEAD.

Bug 2: Return HTTP 404 with code VERSION_NOT_FOUND when a requested version tag
is not found in repository_versions, instead of silently falling back to a
cross-version mixed result set.

Bug 4: Before returning 404, attempt a commit_hash prefix match (min 7 chars)
so callers can request a version by full or short SHA.

Bug 3: Change HybridSearchService.search() to return
{ results, searchModeUsed } and propagate searchModeUsed through
ContextResponseMetadata and ContextJsonResponseDto so callers can see which
strategy (keyword / semantic / hybrid / keyword_fallback) was actually used.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Giancarmine Salucci
2026-03-28 10:31:15 +01:00
parent 417c6fd072
commit 255838dcc0
9 changed files with 217 additions and 41 deletions

View File

@@ -395,7 +395,7 @@ describe('HybridSearchService', () => {
seedSnippet(client, { repositoryId: repoId, documentId: docId, content: 'hello world' });
const svc = new HybridSearchService(client, searchService, null);
const results = await svc.search('hello', { repositoryId: repoId });
const { results } = await svc.search('hello', { repositoryId: repoId });
expect(results.length).toBeGreaterThan(0);
expect(results[0].snippet.content).toBe('hello world');
@@ -406,14 +406,14 @@ describe('HybridSearchService', () => {
const provider = makeMockProvider([[1, 0]]);
const svc = new HybridSearchService(client, searchService, provider);
const results = await svc.search('alpha zero', { repositoryId: repoId, alpha: 0 });
const { results } = await svc.search('alpha zero', { repositoryId: repoId, alpha: 0 });
expect(results.length).toBeGreaterThan(0);
});
it('returns empty array when FTS5 query is blank and no provider', async () => {
const svc = new HybridSearchService(client, searchService, null);
const results = await svc.search(' ', { repositoryId: repoId });
const { results } = await svc.search(' ', { repositoryId: repoId });
expect(results).toHaveLength(0);
});
@@ -425,7 +425,7 @@ describe('HybridSearchService', () => {
});
const svc = new HybridSearchService(client, searchService, makeNoopProvider());
const results = await svc.search('noop fallback', { repositoryId: repoId });
const { results } = await svc.search('noop fallback', { repositoryId: repoId });
expect(results.length).toBeGreaterThan(0);
});
@@ -445,7 +445,7 @@ describe('HybridSearchService', () => {
const provider = makeMockProvider([[1, 0, 0, 0]]);
const svc = new HybridSearchService(client, searchService, provider);
const results = await svc.search('hybrid search', {
const { results } = await svc.search('hybrid search', {
repositoryId: repoId,
alpha: 0.5
});
@@ -464,7 +464,7 @@ describe('HybridSearchService', () => {
const provider = makeMockProvider([[1, 0]]);
const svc = new HybridSearchService(client, searchService, provider);
const results = await svc.search('deduplicate snippet', {
const { results } = await svc.search('deduplicate snippet', {
repositoryId: repoId,
alpha: 0.5
});
@@ -487,7 +487,7 @@ describe('HybridSearchService', () => {
const provider = makeMockProvider([[1, 0]]);
const svc = new HybridSearchService(client, searchService, provider);
const results = await svc.search('pagination test', {
const { results } = await svc.search('pagination test', {
repositoryId: repoId,
limit: 3,
alpha: 0.5
@@ -519,7 +519,7 @@ describe('HybridSearchService', () => {
const provider = makeMockProvider([[1, 0]]);
const svc = new HybridSearchService(client, searchService, provider);
const results = await svc.search('anything', {
const { results } = await svc.search('anything', {
repositoryId: repoId,
alpha: 1
});
@@ -543,7 +543,7 @@ describe('HybridSearchService', () => {
const provider = makeMockProvider([[1, 0]]);
const svc = new HybridSearchService(client, searchService, provider);
const results = await svc.search('metadata check', {
const { results } = await svc.search('metadata check', {
repositoryId: repoId,
alpha: 0.5
});
@@ -580,7 +580,7 @@ describe('HybridSearchService', () => {
const provider = makeMockProvider([[1, 0]]);
const svc = new HybridSearchService(client, searchService, provider);
const results = await svc.search('repository keyword', {
const { results } = await svc.search('repository keyword', {
repositoryId: repoId,
alpha: 0.5
});
@@ -607,7 +607,7 @@ describe('HybridSearchService', () => {
const provider = makeMockProvider([[1, 0]]);
const svc = new HybridSearchService(client, searchService, provider);
const codeResults = await svc.search('function example', {
const { results: codeResults } = await svc.search('function example', {
repositoryId: repoId,
type: 'code',
alpha: 0.5
@@ -632,7 +632,7 @@ describe('HybridSearchService', () => {
const svc = new HybridSearchService(client, searchService, provider);
// Should not throw and should return results.
const results = await svc.search('default alpha hybrid', { repositoryId: repoId });
const { results } = await svc.search('default alpha hybrid', { repositoryId: repoId });
expect(Array.isArray(results)).toBe(true);
});
@@ -761,7 +761,7 @@ describe('HybridSearchService', () => {
const searchService = new SearchService(client);
const hybridService = new HybridSearchService(client, searchService, mockProvider);
const results = await hybridService.search('keyword', {
const { results } = await hybridService.search('keyword', {
repositoryId: repoId,
searchMode: 'keyword'
});
@@ -820,7 +820,7 @@ describe('HybridSearchService', () => {
const searchService = new SearchService(client);
const hybridService = new HybridSearchService(client, searchService, mockProvider);
const results = await hybridService.search('semantic', {
const { results } = await hybridService.search('semantic', {
repositoryId: repoId,
searchMode: 'semantic',
profileId: 'test-profile'
@@ -848,7 +848,7 @@ describe('HybridSearchService', () => {
const searchService = new SearchService(client);
const hybridService = new HybridSearchService(client, searchService, null);
const results = await hybridService.search('test query', {
const { results } = await hybridService.search('test query', {
repositoryId: repoId,
searchMode: 'semantic'
});
@@ -867,7 +867,7 @@ describe('HybridSearchService', () => {
const searchService = new SearchService(client);
const hybridService = new HybridSearchService(client, searchService, mockProvider);
const results = await hybridService.search(' ', {
const { results } = await hybridService.search(' ', {
repositoryId: repoId,
searchMode: 'semantic'
});
@@ -885,7 +885,7 @@ describe('HybridSearchService', () => {
const searchService = new SearchService(client);
const hybridService = new HybridSearchService(client, searchService, noopProvider);
const results = await hybridService.search('test query', {
const { results } = await hybridService.search('test query', {
repositoryId: repoId,
searchMode: 'semantic'
});
@@ -951,7 +951,7 @@ describe('HybridSearchService', () => {
const hybridService = new HybridSearchService(client, searchService, mockProvider);
// Query with heavy punctuation that preprocesses to nothing.
const results = await hybridService.search('!!!@@@###', {
const { results } = await hybridService.search('!!!@@@###', {
repositoryId: repoId,
searchMode: 'auto',
profileId: 'test-profile'
@@ -978,7 +978,7 @@ describe('HybridSearchService', () => {
const searchService = new SearchService(client);
const hybridService = new HybridSearchService(client, searchService, mockProvider);
const results = await hybridService.search('hello', {
const { results } = await hybridService.search('hello', {
repositoryId: repoId,
searchMode: 'auto'
});
@@ -1038,7 +1038,7 @@ describe('HybridSearchService', () => {
const hybridService = new HybridSearchService(client, searchService, mockProvider);
// Query that won't match through FTS after punctuation normalization.
const results = await hybridService.search('%%%vector%%%', {
const { results } = await hybridService.search('%%%vector%%%', {
repositoryId: repoId,
searchMode: 'hybrid',
alpha: 0.5,
@@ -1064,7 +1064,7 @@ describe('HybridSearchService', () => {
const searchService = new SearchService(client);
const hybridService = new HybridSearchService(client, searchService, null);
const results = await hybridService.search('!!!@@@###$$$', {
const { results } = await hybridService.search('!!!@@@###$$$', {
repositoryId: repoId
});