@translated/lara-mcp
Version:
Lara API official MCP server
118 lines (117 loc) • 5.7 kB
JavaScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { CallTool } from '../../mcp/tools.js';
import { createMockTranslator } from '../utils/mocks.js';
import { Translator } from '@translated/lara';
import { InvalidInputError } from '../../exception.js';
const actualLara = await vi.importActual('@translated/lara');
const { LaraApiError, TimeoutError: LaraTimeoutError } = actualLara;
vi.mock('@translated/lara', async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
Translator: vi.fn(() => createMockTranslator()),
};
});
function makeRequest(name, args = {}) {
return {
method: 'tools/call',
params: { name, arguments: args },
};
}
describe('CallTool error handling', () => {
let mockTranslator;
beforeEach(() => {
mockTranslator = new Translator();
});
it('should surface LaraApiError message', async () => {
const apiError = new LaraApiError(404, 'not_found', 'Memory not found');
mockTranslator.memories.delete.mockRejectedValue(apiError);
const request = makeRequest('delete_memory', { id: 'mem_abc123' });
const promise = CallTool(request, mockTranslator);
await expect(promise).rejects.toThrow(InvalidInputError);
await expect(promise).rejects.toThrow('Memory not found');
});
it('should surface timeout error message', async () => {
const timeoutError = new LaraTimeoutError('Request timed out');
mockTranslator.memories.delete.mockRejectedValue(timeoutError);
const request = makeRequest('delete_memory', { id: 'mem_abc123' });
const promise = CallTool(request, mockTranslator);
await expect(promise).rejects.toThrow(InvalidInputError);
await expect(promise).rejects.toThrow('The translation request timed out. Try again or increase the timeout.');
});
it('should include field names and reasons in Zod validation errors', async () => {
const request = makeRequest('delete_memory', { id: 123 });
const promise = CallTool(request, mockTranslator);
await expect(promise).rejects.toThrow(InvalidInputError);
await expect(promise).rejects.toThrow(/Invalid input:.*id/);
});
it('should use "arguments" label for root-level Zod errors', async () => {
// Passing a non-object triggers a root-level Zod error with empty path
const request = makeRequest('delete_memory', 'not-an-object');
const promise = CallTool(request, mockTranslator);
await expect(promise).rejects.toThrow(InvalidInputError);
await expect(promise).rejects.toThrow(/Invalid input: arguments:/);
});
it('should preserve InvalidInputError as-is', async () => {
mockTranslator.memories.addTranslation.mockImplementation(() => {
throw new InvalidInputError('Custom validation error from handler');
});
const request = makeRequest('add_translation', {
id: ['mem_abc123'],
source: 'en-US',
target: 'it-IT',
sentence: 'Hello',
translation: 'Ciao',
tuid: 'tu_1',
sentence_before: 'Hi',
sentence_after: 'Goodbye',
});
const promise = CallTool(request, mockTranslator);
await expect(promise).rejects.toThrow(InvalidInputError);
await expect(promise).rejects.toThrow('Custom validation error from handler');
});
it('should return structuredContent and narration for handler tools', async () => {
mockTranslator.translate.mockResolvedValue({
translation: 'Ciao',
sourceLanguage: 'en-US',
});
const request = makeRequest('translate', {
text: [{ text: 'Hello', translatable: true }],
target: 'it-IT',
});
const result = await CallTool(request, mockTranslator);
expect(result).toHaveProperty('structuredContent');
expect(result.structuredContent).toEqual({ value: 'Ciao' });
expect(Array.isArray(result.content)).toBe(true);
expect(result.content[0]).toHaveProperty('type', 'text');
expect(result.content[0].text).toContain('it-IT');
});
it('should wrap array results under items in structuredContent for lister tools', async () => {
mockTranslator.memories.list.mockResolvedValue([
{ id: 'mem_1', name: 'First' },
{ id: 'mem_2', name: 'Second' },
]);
const request = makeRequest('list_memories', {});
const result = await CallTool(request, mockTranslator);
expect(result.structuredContent).toEqual({
items: [
{ id: 'mem_1', name: 'First' },
{ id: 'mem_2', name: 'Second' },
],
});
expect(result.content[0].text).toContain('2');
});
it('should return generic message for unknown errors', async () => {
mockTranslator.memories.delete.mockRejectedValue(new TypeError('Something unexpected'));
const request = makeRequest('delete_memory', { id: 'mem_abc123' });
const promise = CallTool(request, mockTranslator);
await expect(promise).rejects.toThrow(InvalidInputError);
await expect(promise).rejects.toThrow('An error occurred while processing your request');
});
it('should throw InvalidInputError for unknown tool names', async () => {
const request = makeRequest('nonexistent_tool', {});
const promise = CallTool(request, mockTranslator);
await expect(promise).rejects.toThrow(InvalidInputError);
await expect(promise).rejects.toThrow('Tool nonexistent_tool not found');
});
});