Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-Claude) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
200 lines
6.5 KiB
TypeScript
200 lines
6.5 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
import { registerQueryTool } from '../sage-query.js';
|
|
import type { RestClient } from '../../clients/rest-client.js';
|
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
|
|
function createMockRestClient(overrides: Partial<RestClient> = {}): RestClient {
|
|
return {
|
|
query: vi.fn().mockResolvedValue({
|
|
records: [],
|
|
pagination: { returned: 0, hasMore: false },
|
|
}),
|
|
read: vi.fn(),
|
|
listEntities: vi.fn(),
|
|
healthCheck: vi.fn(),
|
|
...overrides,
|
|
} as unknown as RestClient;
|
|
}
|
|
|
|
describe('registerQueryTool', () => {
|
|
let server: McpServer;
|
|
let registerToolSpy: ReturnType<typeof vi.spyOn>;
|
|
|
|
beforeEach(() => {
|
|
server = new McpServer({ name: 'test-server', version: '1.0.0' });
|
|
registerToolSpy = vi.spyOn(server, 'registerTool');
|
|
});
|
|
|
|
it('registers sage_query tool with correct metadata', () => {
|
|
const restClient = createMockRestClient();
|
|
registerQueryTool(server, restClient);
|
|
|
|
expect(registerToolSpy).toHaveBeenCalledOnce();
|
|
const [name, config] = registerToolSpy.mock.calls[0];
|
|
expect(name).toBe('sage_query');
|
|
expect(config).toMatchObject({
|
|
description: expect.stringContaining('Query Sage X3'),
|
|
annotations: {
|
|
readOnlyHint: true,
|
|
destructiveHint: false,
|
|
idempotentHint: true,
|
|
openWorldHint: true,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('has inputSchema with expected fields', () => {
|
|
const restClient = createMockRestClient();
|
|
registerQueryTool(server, restClient);
|
|
|
|
const config = registerToolSpy.mock.calls[0][1] as Record<string, unknown>;
|
|
const schema = config.inputSchema as Record<string, unknown>;
|
|
expect(schema).toHaveProperty('entity');
|
|
expect(schema).toHaveProperty('representation');
|
|
expect(schema).toHaveProperty('where');
|
|
expect(schema).toHaveProperty('orderBy');
|
|
expect(schema).toHaveProperty('count');
|
|
expect(schema).toHaveProperty('nextUrl');
|
|
expect(schema).toHaveProperty('select');
|
|
});
|
|
|
|
describe('handler', () => {
|
|
async function callHandler(
|
|
restClient: RestClient,
|
|
args: Record<string, unknown> = { entity: 'BPCUSTOMER' },
|
|
): Promise<CallToolResult> {
|
|
registerQueryTool(server, restClient);
|
|
const handler = registerToolSpy.mock.calls[0][2] as (
|
|
args: Record<string, unknown>,
|
|
) => Promise<CallToolResult>;
|
|
return handler(args);
|
|
}
|
|
|
|
it('returns paginated records on success', async () => {
|
|
const mockRecords = [
|
|
{ BPCNUM: 'C001', BPCNAM: 'Acme Corp' },
|
|
{ BPCNUM: 'C002', BPCNAM: 'Widget Inc' },
|
|
];
|
|
const restClient = createMockRestClient({
|
|
query: vi.fn().mockResolvedValue({
|
|
records: mockRecords,
|
|
pagination: { returned: 2, hasMore: true, nextUrl: 'https://x3.example.com/next' },
|
|
}),
|
|
});
|
|
|
|
const result = await callHandler(restClient);
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(result.content).toHaveLength(1);
|
|
const parsed = JSON.parse((result.content[0] as { text: string }).text);
|
|
expect(parsed.records).toEqual(mockRecords);
|
|
expect(parsed.pagination).toEqual({
|
|
returned: 2,
|
|
hasMore: true,
|
|
nextUrl: 'https://x3.example.com/next',
|
|
});
|
|
});
|
|
|
|
it('passes all query parameters to restClient.query', async () => {
|
|
const queryMock = vi.fn().mockResolvedValue({
|
|
records: [],
|
|
pagination: { returned: 0, hasMore: false },
|
|
});
|
|
const restClient = createMockRestClient({ query: queryMock });
|
|
|
|
await callHandler(restClient, {
|
|
entity: 'SINVOICE',
|
|
representation: 'SINV$query',
|
|
where: "BPCNUM eq 'C001'",
|
|
orderBy: 'ACCDAT desc',
|
|
count: 50,
|
|
select: 'NUM,BPCNUM',
|
|
});
|
|
|
|
expect(queryMock).toHaveBeenCalledWith({
|
|
entity: 'SINVOICE',
|
|
representation: 'SINV$query',
|
|
where: "BPCNUM eq 'C001'",
|
|
orderBy: 'ACCDAT desc',
|
|
count: 50,
|
|
nextUrl: undefined,
|
|
select: 'NUM,BPCNUM',
|
|
});
|
|
});
|
|
|
|
it('passes nextUrl for pagination continuation', async () => {
|
|
const queryMock = vi.fn().mockResolvedValue({
|
|
records: [],
|
|
pagination: { returned: 0, hasMore: false },
|
|
});
|
|
const restClient = createMockRestClient({ query: queryMock });
|
|
|
|
await callHandler(restClient, {
|
|
entity: 'BPCUSTOMER',
|
|
nextUrl: 'https://x3.example.com/api1/x3/erp/SEED/BPCUSTOMER?next=abc',
|
|
});
|
|
|
|
expect(queryMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
nextUrl: 'https://x3.example.com/api1/x3/erp/SEED/BPCUSTOMER?next=abc',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('returns empty records when no results found', async () => {
|
|
const restClient = createMockRestClient({
|
|
query: vi.fn().mockResolvedValue({
|
|
records: [],
|
|
pagination: { returned: 0, hasMore: false },
|
|
}),
|
|
});
|
|
|
|
const result = await callHandler(restClient);
|
|
|
|
const parsed = JSON.parse((result.content[0] as { text: string }).text);
|
|
expect(parsed.records).toEqual([]);
|
|
expect(parsed.pagination.returned).toBe(0);
|
|
expect(parsed.pagination.hasMore).toBe(false);
|
|
});
|
|
|
|
it('returns error with hint on authentication failure', async () => {
|
|
const restClient = createMockRestClient({
|
|
query: vi.fn().mockRejectedValue(new Error('HTTP 401: Unauthorized')),
|
|
});
|
|
|
|
const result = await callHandler(restClient);
|
|
|
|
expect(result.isError).toBe(true);
|
|
const text = (result.content[0] as { text: string }).text;
|
|
expect(text).toContain('401');
|
|
expect(text).toContain('Hint:');
|
|
});
|
|
|
|
it('returns error with hint on timeout', async () => {
|
|
const restClient = createMockRestClient({
|
|
query: vi.fn().mockRejectedValue(new Error('Request timeout')),
|
|
});
|
|
|
|
const result = await callHandler(restClient);
|
|
|
|
expect(result.isError).toBe(true);
|
|
const text = (result.content[0] as { text: string }).text;
|
|
expect(text).toContain('timeout');
|
|
});
|
|
|
|
it('returns error with hint on connection failure', async () => {
|
|
const restClient = createMockRestClient({
|
|
query: vi.fn().mockRejectedValue(new Error('ECONNREFUSED')),
|
|
});
|
|
|
|
const result = await callHandler(restClient);
|
|
|
|
expect(result.isError).toBe(true);
|
|
const text = (result.content[0] as { text: string }).text;
|
|
expect(text).toContain('ECONNREFUSED');
|
|
expect(text).toContain('Hint:');
|
|
});
|
|
});
|
|
});
|