mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
Adapt Cypher guidance from articultur/GitNexus@17f71ea using canonical schema constants and native backend regression tests.
187 lines
6.3 KiB
TypeScript
187 lines
6.3 KiB
TypeScript
/**
|
|
* 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<LocalBackend> {
|
|
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 <symbol> 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 <symbol> 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');
|
|
});
|
|
|
|
it('cypher keeps WAL recovery ahead of schema-looking diagnostics', async () => {
|
|
const backend = await makeBackend();
|
|
const message = 'Binder exception: Table Functon does not exist. Corrupted wal file';
|
|
lbugMocks.executeParameterized.mockRejectedValueOnce(new Error(message));
|
|
const result = await backend.callTool('cypher', {
|
|
repo: 'test-repo',
|
|
statement: 'MATCH (n) RETURN n',
|
|
});
|
|
expect(result.error).toBe(message);
|
|
expect(result.recoverySuggestion).toBeDefined();
|
|
expect(result).not.toHaveProperty('hint');
|
|
});
|
|
|
|
it('cypher keeps wrapped read-only failures ahead of schema-looking diagnostics', async () => {
|
|
const backend = await makeBackend();
|
|
lbugMocks.executeParameterized.mockRejectedValueOnce(
|
|
new Error('Binder exception: Table Functon does not exist.', {
|
|
cause: new Error('Cannot execute write operations in a read-only database!'),
|
|
}),
|
|
);
|
|
const result = await backend.callTool('cypher', {
|
|
repo: 'test-repo',
|
|
statement: 'CREATE (:Function)',
|
|
});
|
|
expect(result.error).toMatch(/^Write operations .* are not allowed/);
|
|
expect(result).not.toHaveProperty('hint');
|
|
expect(result).not.toHaveProperty('recoverySuggestion');
|
|
});
|
|
|
|
it('cypher preserves unrelated failures without adding a schema hint', async () => {
|
|
const backend = await makeBackend();
|
|
lbugMocks.executeParameterized.mockRejectedValueOnce(new Error('Connection failed'));
|
|
const result = await backend.callTool('cypher', {
|
|
repo: 'test-repo',
|
|
statement: 'MATCH (n) RETURN n',
|
|
});
|
|
expect(result.error).toBe('Connection failed');
|
|
expect(result).not.toHaveProperty('hint');
|
|
});
|
|
});
|