Skip to content

Commit 27d86f7

Browse files
committed
fix: annotate codebase retrieval fetch failures by stage
1 parent e9c773d commit 27d86f7

2 files changed

Lines changed: 145 additions & 28 deletions

File tree

src/mcp/tools/codebaseRetrieval.ts

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ const RETRIEVAL_PROGRESS_ORDER: RetrievalProgressStage[] = [
7878
'done',
7979
];
8080

81+
function formatStageFailureMessage(stage: RetrievalProgressStage, message: string): string {
82+
const stageLabel =
83+
stage === 'init'
84+
? 'initialization'
85+
: stage === 'done'
86+
? 'completion'
87+
: stage;
88+
return `${stageLabel} stage failed: ${message}`;
89+
}
90+
8191
interface ResultCardFeatureMemoryMatch {
8292
memory: FeatureMemory;
8393
score: number;
@@ -513,36 +523,46 @@ export async function handleCodebaseRetrieval(
513523

514524
// 5. 创建 SearchService 实例
515525
reportProgress('init', '初始化检索服务');
526+
let currentStage: RetrievalProgressStage = 'init';
516527
const snapshotId = resolveCurrentSnapshotId(projectId);
517528
const service = new SearchService(projectId, repo_path, undefined, snapshotId);
518-
const initStart = Date.now();
519-
await service.init();
520-
const initMs = Date.now() - initStart;
521-
logger.debug('SearchService 初始化完成');
522-
523-
// 6. 执行搜索
524-
const searchStart = Date.now();
525-
const contextPack = await service.buildContextPack(
526-
combinedQuery,
527-
(stage) => {
528-
if (stage === 'retrieve') {
529-
reportProgress('retrieve', '执行混合召回');
530-
} else if (stage === 'rerank') {
531-
reportProgress('rerank', '执行精排');
532-
} else if (stage === 'expand') {
533-
reportProgress('expand', '执行上下文扩展');
534-
} else if (stage === 'pack') {
535-
reportProgress('pack', '执行上下文打包');
536-
}
537-
},
538-
{
539-
technicalTerms: technical_terms,
540-
semanticQuery,
541-
lexicalQuery,
542-
responseMode: response_mode || 'expanded',
543-
},
544-
);
545-
const totalMs = initMs + (Date.now() - searchStart);
529+
let initMs = 0;
530+
let contextPack: ContextPack;
531+
let totalMs = 0;
532+
try {
533+
const initStart = Date.now();
534+
await service.init();
535+
initMs = Date.now() - initStart;
536+
logger.debug('SearchService 初始化完成');
537+
538+
// 6. 执行搜索
539+
const searchStart = Date.now();
540+
contextPack = await service.buildContextPack(
541+
combinedQuery,
542+
(stage) => {
543+
currentStage = stage;
544+
if (stage === 'retrieve') {
545+
reportProgress('retrieve', '执行混合召回');
546+
} else if (stage === 'rerank') {
547+
reportProgress('rerank', '执行精排');
548+
} else if (stage === 'expand') {
549+
reportProgress('expand', '执行上下文扩展');
550+
} else if (stage === 'pack') {
551+
reportProgress('pack', '执行上下文打包');
552+
}
553+
},
554+
{
555+
technicalTerms: technical_terms,
556+
semanticQuery,
557+
lexicalQuery,
558+
responseMode: response_mode || 'expanded',
559+
},
560+
);
561+
totalMs = initMs + (Date.now() - searchStart);
562+
} catch (err) {
563+
const error = err as Error;
564+
throw new Error(formatStageFailureMessage(currentStage, error.message), { cause: err });
565+
}
546566
if (contextPack.debug) {
547567
contextPack.debug.timingMs.init = initMs;
548568
}

tests/codebase-retrieval.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,103 @@ test('handleCodebaseRetrieval completes success path with combined query telemet
174174
}
175175
});
176176

177+
test('handleCodebaseRetrieval annotates retrieval-stage fetch failures', async () => {
178+
const baseDir = createTempBaseDir();
179+
const repoDir = path.join(baseDir, 'repo');
180+
fs.mkdirSync(repoDir, { recursive: true });
181+
182+
const previousEnv = {
183+
CONTEXTATLAS_BASE_DIR: process.env.CONTEXTATLAS_BASE_DIR,
184+
MCP_AUTO_INDEX: process.env.MCP_AUTO_INDEX,
185+
EMBEDDINGS_API_KEY: process.env.EMBEDDINGS_API_KEY,
186+
EMBEDDINGS_BASE_URL: process.env.EMBEDDINGS_BASE_URL,
187+
EMBEDDINGS_MODEL: process.env.EMBEDDINGS_MODEL,
188+
RERANK_API_KEY: process.env.RERANK_API_KEY,
189+
RERANK_BASE_URL: process.env.RERANK_BASE_URL,
190+
RERANK_MODEL: process.env.RERANK_MODEL,
191+
};
192+
193+
process.env.CONTEXTATLAS_BASE_DIR = baseDir;
194+
process.env.MCP_AUTO_INDEX = 'false';
195+
process.env.EMBEDDINGS_API_KEY = 'test-key';
196+
process.env.EMBEDDINGS_BASE_URL = 'http://127.0.0.1/embeddings';
197+
process.env.EMBEDDINGS_MODEL = 'test-embedding-model';
198+
process.env.RERANK_API_KEY = 'test-key';
199+
process.env.RERANK_BASE_URL = 'http://127.0.0.1/rerank';
200+
process.env.RERANK_MODEL = 'test-rerank-model';
201+
202+
const projectId = generateProjectId(repoDir);
203+
const db = initDb(projectId);
204+
db.close();
205+
206+
const originalInit = SearchService.prototype.init;
207+
const originalBuildContextPack = SearchService.prototype.buildContextPack;
208+
209+
SearchService.prototype.init = async function initMock(): Promise<void> {};
210+
SearchService.prototype.buildContextPack = (async function buildContextPackMock(
211+
_query,
212+
onStage,
213+
) {
214+
onStage?.('retrieve');
215+
throw new TypeError('fetch failed');
216+
}) as typeof SearchService.prototype.buildContextPack;
217+
218+
try {
219+
await assert.rejects(
220+
() =>
221+
handleCodebaseRetrieval({
222+
repo_path: repoDir,
223+
information_request: 'SearchService retrieval flow',
224+
}),
225+
/retrieve stage failed: fetch failed/,
226+
);
227+
} finally {
228+
SearchService.prototype.init = originalInit;
229+
SearchService.prototype.buildContextPack = originalBuildContextPack;
230+
if (previousEnv.CONTEXTATLAS_BASE_DIR === undefined) {
231+
delete process.env.CONTEXTATLAS_BASE_DIR;
232+
} else {
233+
process.env.CONTEXTATLAS_BASE_DIR = previousEnv.CONTEXTATLAS_BASE_DIR;
234+
}
235+
if (previousEnv.MCP_AUTO_INDEX === undefined) {
236+
delete process.env.MCP_AUTO_INDEX;
237+
} else {
238+
process.env.MCP_AUTO_INDEX = previousEnv.MCP_AUTO_INDEX;
239+
}
240+
if (previousEnv.EMBEDDINGS_API_KEY === undefined) {
241+
delete process.env.EMBEDDINGS_API_KEY;
242+
} else {
243+
process.env.EMBEDDINGS_API_KEY = previousEnv.EMBEDDINGS_API_KEY;
244+
}
245+
if (previousEnv.EMBEDDINGS_BASE_URL === undefined) {
246+
delete process.env.EMBEDDINGS_BASE_URL;
247+
} else {
248+
process.env.EMBEDDINGS_BASE_URL = previousEnv.EMBEDDINGS_BASE_URL;
249+
}
250+
if (previousEnv.EMBEDDINGS_MODEL === undefined) {
251+
delete process.env.EMBEDDINGS_MODEL;
252+
} else {
253+
process.env.EMBEDDINGS_MODEL = previousEnv.EMBEDDINGS_MODEL;
254+
}
255+
if (previousEnv.RERANK_API_KEY === undefined) {
256+
delete process.env.RERANK_API_KEY;
257+
} else {
258+
process.env.RERANK_API_KEY = previousEnv.RERANK_API_KEY;
259+
}
260+
if (previousEnv.RERANK_BASE_URL === undefined) {
261+
delete process.env.RERANK_BASE_URL;
262+
} else {
263+
process.env.RERANK_BASE_URL = previousEnv.RERANK_BASE_URL;
264+
}
265+
if (previousEnv.RERANK_MODEL === undefined) {
266+
delete process.env.RERANK_MODEL;
267+
} else {
268+
process.env.RERANK_MODEL = previousEnv.RERANK_MODEL;
269+
}
270+
fs.rmSync(baseDir, { recursive: true, force: true });
271+
}
272+
});
273+
177274
test('handleCodebaseRetrieval formats default result card with memory and decision context', async () => {
178275
const baseDir = createTempBaseDir();
179276
const repoDir = path.join(baseDir, 'repo');

0 commit comments

Comments
 (0)