/** * Tests for WAL corruption feedback in MCP error responses (#1402). */ import { beforeEach, describe, expect, it, vi } from 'vitest'; const { lbugMocks, repoMocks } = vi.hoisted(() => ({ lbugMocks: { initLbug: vi.fn().mockResolvedValue(undefined), executeQuery: vi.fn(), executeParameterized: vi.fn(), closeLbug: vi.fn().mockResolvedValue(undefined), isLbugReady: vi.fn().mockReturnValue(true), }, repoMocks: { listRegisteredRepos: vi.fn(), }, })); vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, ...lbugMocks }; }); vi.mock('../../src/mcp/core/lbug-adapter.js', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, ...lbugMocks }; }); vi.mock('../../src/storage/repo-manager.js', () => ({ listRegisteredRepos: repoMocks.listRegisteredRepos, cleanupOldKuzuFiles: vi.fn().mockResolvedValue({ found: false, needsReindex: false }), findSiblingClones: vi.fn().mockResolvedValue([]), })); vi.mock('../../src/core/git-staleness.js', () => ({ checkStaleness: vi.fn().mockReturnValue({ isStale: false, commitsBehind: 0 }), checkCwdMatch: vi.fn().mockResolvedValue({ match: 'none' }), })); vi.mock('../../src/core/search/bm25-index.js', () => ({ searchFTSFromLbug: vi.fn().mockResolvedValue([]), })); vi.mock('../../src/mcp/core/embedder.js', () => ({ embedQuery: vi.fn().mockResolvedValue([]), getEmbeddingDims: vi.fn().mockReturnValue(384), })); import { LocalBackend } from '../../src/mcp/local/local-backend.js'; const MOCK_REPO_ENTRY = { name: 'test-repo', path: '/tmp/test', storagePath: '/tmp/test/.gitnexus', indexedAt: '2026-05-01T00:00:00Z', lastCommit: 'abc1234', }; async function makeBackend(): Promise { const backend = new LocalBackend(); await backend.init(); return backend; } describe('WAL corruption feedback in MCP responses (#1402)', () => { beforeEach(() => { vi.clearAllMocks(); lbugMocks.initLbug.mockResolvedValue(undefined); lbugMocks.executeQuery.mockResolvedValue([]); lbugMocks.executeParameterized.mockResolvedValue([]); lbugMocks.isLbugReady.mockReturnValue(true); repoMocks.listRegisteredRepos.mockResolvedValue([MOCK_REPO_ENTRY]); }); it('impact returns WAL suggestion on corrupted WAL error', async () => { const backend = await makeBackend(); lbugMocks.executeParameterized.mockRejectedValueOnce( new Error('Runtime exception: Corrupted wal file. Read out invalid WAL record type.'), ); const result = await backend.callTool('impact', { repo: 'test-repo', target: 'MyClass', direction: 'upstream', }); expect(result.error).toBeDefined(); expect(result.suggestion).toBe( 'The graph query failed — try gitnexus context as a fallback', ); expect(result.recoverySuggestion).toBeDefined(); }); it('cypher returns WAL recoverySuggestion on corrupted WAL error', async () => { const backend = await makeBackend(); lbugMocks.executeParameterized.mockRejectedValueOnce(new Error('Corrupted wal file')); const result = await backend.callTool('cypher', { repo: 'test-repo', query: 'MATCH (n) RETURN n LIMIT 1', }); expect(result.error).toBe('Corrupted wal file'); expect(result.recoverySuggestion).toBeDefined(); }); it('context returns WAL recoverySuggestion on corrupted WAL error', async () => { const backend = await makeBackend(); lbugMocks.executeParameterized.mockRejectedValueOnce(new Error('Corrupted wal file')); const result = await backend.callTool('context', { repo: 'test-repo', name: 'MyClass', }); expect(result.error).toBe('Corrupted wal file'); expect(result.recoverySuggestion).toBeDefined(); }); it('non-WAL errors do not include WAL suggestion', async () => { const backend = await makeBackend(); lbugMocks.executeParameterized.mockRejectedValueOnce(new Error('Some other error')); const result = await backend.callTool('impact', { repo: 'test-repo', target: 'MyClass', direction: 'upstream', }); expect(result.error).toBeDefined(); expect(result.suggestion).toBe( 'The graph query failed — try gitnexus context as a fallback', ); }); it('context preserves non-WAL throw behavior', async () => { const backend = await makeBackend(); lbugMocks.executeParameterized.mockRejectedValueOnce(new Error('Some other error')); await expect( backend.callTool('context', { repo: 'test-repo', name: 'MyClass', }), ).rejects.toThrow('Some other error'); }); });