Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
version: 10
- uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '24'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile

Expand All @@ -35,14 +35,17 @@ jobs:
test:
name: Functional Tests
runs-on: ubuntu-latest
strategy:
matrix:
node: [22, 24]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: '20'
node-version: ${{ matrix.node }}
cache: 'pnpm'
- run: pnpm install --frozen-lockfile

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
],
"packageManager": "pnpm@10.27.0",
"engines": {
"node": ">=18.0.0"
"node": ">=22.0.0"
},
"keywords": [
"mcp",
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1041,7 +1041,7 @@ export function registerHandlers(target: Server): void {
type: 'text',
text: JSON.stringify({
status: 'indexing',
message: 'Index build in progress - please retry shortly'
message: 'Index build in progress. Check indexing status, then retry.'
})
}
]
Expand All @@ -1068,7 +1068,7 @@ export function registerHandlers(target: Server): void {
type: 'text',
text: JSON.stringify({
status: 'indexing',
message: 'Index rebuild in progress - please retry shortly',
message: 'Index rebuild in progress. Check indexing status, then retry.',
index: indexSignal
})
}
Expand Down
5 changes: 3 additions & 2 deletions src/tools/search-codebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export async function handle(
text: JSON.stringify(
{
status: 'indexing',
message: 'Index is still being built. Retry in a moment.',
message: 'Index build in progress. Check indexing status, then retry.',
progress: ctx.indexState.indexer?.getProgress()
},
null,
Expand Down Expand Up @@ -233,7 +233,8 @@ export async function handle(
text: JSON.stringify(
{
status: 'indexing',
message: 'Index was corrupt. Rebuild started — retry shortly.'
message:
'Index is unavailable or not ready. Indexing has started. Check indexing status, then retry.'
},
null,
2
Expand Down
30 changes: 28 additions & 2 deletions tests/multi-project-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,30 @@ describe('multi-project routing', () => {
}
});

it('reports status-check guidance when an index build is already running', async () => {
const { server } = await import('../src/index.js');
const { getOrCreateProject } = await import('../src/project-state.js');
const handler = (server as unknown as TestServer)._requestHandlers.get('tools/call');
if (!handler) throw new Error('tools/call handler not registered');

const project = getOrCreateProject(primaryRoot);
const originalStatus = project.indexState.status;
project.indexState.status = 'indexing';
try {
const response = await callTool(handler, 20, 'search_codebase', {
query: 'feature',
project: primaryRoot
});
const payload = parsePayload(response) as { status: string; message: string };

expect(payload.status).toBe('indexing');
expect(payload.message).toBe('Index build in progress. Check indexing status, then retry.');
expect(searchMocks.search).not.toHaveBeenCalled();
} finally {
project.indexState.status = originalStatus;
}
});

it('triggers a background rebuild for a corrupted explicit project without falling back to cwd', async () => {
delete process.env.CODEBASE_ROOT;
delete process.argv[2];
Expand Down Expand Up @@ -398,7 +422,7 @@ describe('multi-project routing', () => {
};

expect(payload.status).toBe('indexing');
expect(payload.message).toContain('retry shortly');
expect(payload.message).toBe('Index rebuild in progress. Check indexing status, then retry.');
expect(payload.index?.action).toBe('rebuild-started');
expect(String(payload.index?.reason || '')).toContain('Index meta');
} finally {
Expand Down Expand Up @@ -591,7 +615,9 @@ describe('multi-project routing', () => {
it('lists bounded and full context resources for active and project-scoped flows', async () => {
const { server } = await import('../src/index.js');
const toolHandler = (server as unknown as TestServer)._requestHandlers.get('tools/call');
const resourcesHandler = (server as unknown as TestServer)._requestHandlers.get('resources/list');
const resourcesHandler = (server as unknown as TestServer)._requestHandlers.get(
'resources/list'
);

if (!toolHandler || !resourcesHandler) {
throw new Error('required handlers not registered');
Expand Down
78 changes: 58 additions & 20 deletions tests/search-codebase-auto-heal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,33 +143,71 @@ describe('search_codebase auto-heal', () => {
}
});

it('fires background re-index and returns retry message when IndexCorruptedError is thrown', async () => {
const { IndexCorruptedError } = await import('../src/errors/index.js');
it.each([
[
'missing initial index metadata',
'Index meta missing or unreadable (rebuild required): ENOENT'
],
['genuine index corruption', 'LanceDB index corrupted: missing vector column']
])(
'starts background re-index and returns retry guidance for %s',
async (_scenario, errorMessage) => {
const { IndexCorruptedError } = await import('../src/errors/index.js');

searchMocks.search.mockRejectedValueOnce(
new IndexCorruptedError('LanceDB index corrupted: missing vector column')
);
searchMocks.search.mockRejectedValueOnce(new IndexCorruptedError(errorMessage));

const { server } = await import('../src/index.js');
const handler = (server as any)._requestHandlers.get('tools/call');
const { server } = await import('../src/index.js');
const handler = (server as any)._requestHandlers.get('tools/call');

const response = await handler({
jsonrpc: '2.0',
id: 1,
method: 'tools/call',
params: {
name: 'search_codebase',
arguments: {
query: 'test'
const response = await handler({
jsonrpc: '2.0',
id: 1,
method: 'tools/call',
params: {
name: 'search_codebase',
arguments: {
query: 'test'
}
}
});

const payload = JSON.parse(response.content[0].text);
expect(payload.status).toBe('indexing');
expect(payload.message).toBe(
'Index is unavailable or not ready. Indexing has started. Check indexing status, then retry.'
);
expect(searchMocks.search).toHaveBeenCalledTimes(1);
},
15000
);

it('returns status-check guidance without searching while an index build is active', async () => {
if (!tempRoot) throw new Error('tempRoot not initialized');

const { handle } = await import('../src/tools/search-codebase.js');
const baseDir = path.join(tempRoot, CODEBASE_CONTEXT_DIRNAME);
const response = await handle(
{ query: 'search something' },
{
indexState: { status: 'indexing' },
paths: {
baseDir,
memory: path.join(baseDir, 'memory.json'),
intelligence: path.join(baseDir, 'intelligence.json'),
health: path.join(baseDir, 'health.json'),
keywordIndex: path.join(baseDir, KEYWORD_INDEX_FILENAME),
vectorDb: path.join(baseDir, VECTOR_DB_DIRNAME)
},
rootPath: tempRoot,
performIndexing: vi.fn()
}
});
);

const payload = JSON.parse(response.content[0].text);
const payload = JSON.parse(response.content?.[0]?.text ?? '{}');
expect(payload.status).toBe('indexing');
expect(payload.message).toContain('retry');
expect(searchMocks.search).toHaveBeenCalledTimes(1);
}, 15000);
expect(payload.message).toBe('Index build in progress. Check indexing status, then retry.');
expect(searchMocks.search).not.toHaveBeenCalled();
});

it('returns invalid_params when search_codebase query is missing', async () => {
const { server } = await import('../src/index.js');
Expand Down
Loading