Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-Claude) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
217 lines
7.3 KiB
TypeScript
217 lines
7.3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
import { registerDescribeEntityTool } from '../sage-describe-entity.js';
|
|
import type { SoapClient } from '../../clients/soap-client.js';
|
|
import type { SoapResult } from '../../types/index.js';
|
|
|
|
function createMockSoapClient(overrides?: Partial<SoapClient>): SoapClient {
|
|
return {
|
|
read: vi.fn(),
|
|
query: vi.fn(),
|
|
getDescription: vi.fn(),
|
|
healthCheck: vi.fn(),
|
|
...overrides,
|
|
} as unknown as SoapClient;
|
|
}
|
|
|
|
describe('registerDescribeEntityTool', () => {
|
|
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_describe_entity tool with correct metadata', () => {
|
|
const soapClient = createMockSoapClient();
|
|
registerDescribeEntityTool(server, soapClient);
|
|
|
|
expect(registerToolSpy).toHaveBeenCalledOnce();
|
|
const [name, config] = registerToolSpy.mock.calls[0];
|
|
expect(name).toBe('sage_describe_entity');
|
|
expect(config).toMatchObject({
|
|
description: expect.stringContaining('Get field definitions'),
|
|
annotations: {
|
|
readOnlyHint: true,
|
|
destructiveHint: false,
|
|
idempotentHint: true,
|
|
openWorldHint: false,
|
|
},
|
|
});
|
|
});
|
|
|
|
describe('handler', () => {
|
|
async function callHandler(
|
|
soapClient: SoapClient,
|
|
args: { publicName: string },
|
|
): Promise<{ content: Array<{ type: string; text: string }>; isError?: boolean }> {
|
|
registerDescribeEntityTool(server, soapClient);
|
|
const handler = registerToolSpy.mock.calls[0][2] as (
|
|
args: { publicName: string },
|
|
) => Promise<{ content: Array<{ type: string; text: string }>; isError?: boolean }>;
|
|
return handler(args);
|
|
}
|
|
|
|
it('parses field definitions with @_ prefix attributes (fast-xml-parser format)', async () => {
|
|
const soapClient = createMockSoapClient({
|
|
getDescription: vi.fn().mockResolvedValue({
|
|
status: 1,
|
|
data: {
|
|
FLD: [
|
|
{ '@_NAM': 'NUM', '@_TYP': 'Char', '@_LEN': '20', '@_C_ENG': 'Invoice no.' },
|
|
{ '@_NAM': 'BPRNUM', '@_TYP': 'Char', '@_LEN': '15', '@_C_ENG': 'Customer' },
|
|
{ '@_NAM': 'AMTATI', '@_TYP': 'Decimal', '@_LEN': '17.2', '@_C_ENG': 'Amount incl. tax' },
|
|
],
|
|
},
|
|
messages: [],
|
|
technicalInfos: [],
|
|
} satisfies SoapResult),
|
|
});
|
|
|
|
const result = await callHandler(soapClient, { publicName: 'SIH' });
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.publicName).toBe('SIH');
|
|
expect(parsed.fieldCount).toBe(3);
|
|
expect(parsed.fields).toEqual([
|
|
{ name: 'NUM', type: 'Char', length: '20', label: 'Invoice no.' },
|
|
{ name: 'BPRNUM', type: 'Char', length: '15', label: 'Customer' },
|
|
{ name: 'AMTATI', type: 'Decimal', length: '17.2', label: 'Amount incl. tax' },
|
|
]);
|
|
});
|
|
|
|
it('parses field definitions with plain attribute names (JSON format)', async () => {
|
|
const soapClient = createMockSoapClient({
|
|
getDescription: vi.fn().mockResolvedValue({
|
|
status: 1,
|
|
data: {
|
|
FLD: [
|
|
{ NAM: 'SOHNUM', TYP: 'Char', LEN: '20', C_ENG: 'Order no.' },
|
|
],
|
|
},
|
|
messages: [],
|
|
technicalInfos: [],
|
|
} satisfies SoapResult),
|
|
});
|
|
|
|
const result = await callHandler(soapClient, { publicName: 'SOH' });
|
|
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.fields).toEqual([
|
|
{ name: 'SOHNUM', type: 'Char', length: '20', label: 'Order no.' },
|
|
]);
|
|
});
|
|
|
|
it('handles nested FLD under a root element', async () => {
|
|
const soapClient = createMockSoapClient({
|
|
getDescription: vi.fn().mockResolvedValue({
|
|
status: 1,
|
|
data: {
|
|
OBJECT: {
|
|
FLD: [
|
|
{ '@_NAM': 'ITMREF', '@_TYP': 'Char', '@_LEN': '20', '@_C_ENG': 'Item' },
|
|
],
|
|
},
|
|
},
|
|
messages: [],
|
|
technicalInfos: [],
|
|
} satisfies SoapResult),
|
|
});
|
|
|
|
const result = await callHandler(soapClient, { publicName: 'WITM' });
|
|
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.fields).toEqual([
|
|
{ name: 'ITMREF', type: 'Char', length: '20', label: 'Item' },
|
|
]);
|
|
});
|
|
|
|
it('handles single FLD object (not wrapped in array)', async () => {
|
|
const soapClient = createMockSoapClient({
|
|
getDescription: vi.fn().mockResolvedValue({
|
|
status: 1,
|
|
data: {
|
|
FLD: { '@_NAM': 'NUM', '@_TYP': 'Char', '@_LEN': '20', '@_C_ENG': 'Number' },
|
|
},
|
|
messages: [],
|
|
technicalInfos: [],
|
|
} satisfies SoapResult),
|
|
});
|
|
|
|
const result = await callHandler(soapClient, { publicName: 'SIH' });
|
|
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.fieldCount).toBe(1);
|
|
expect(parsed.fields[0].name).toBe('NUM');
|
|
});
|
|
|
|
it('returns empty fields for null data', async () => {
|
|
const soapClient = createMockSoapClient({
|
|
getDescription: vi.fn().mockResolvedValue({
|
|
status: 1,
|
|
data: null,
|
|
messages: [],
|
|
technicalInfos: [],
|
|
} satisfies SoapResult),
|
|
});
|
|
|
|
const result = await callHandler(soapClient, { publicName: 'SIH' });
|
|
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.fields).toEqual([]);
|
|
expect(parsed.fieldCount).toBe(0);
|
|
});
|
|
|
|
it('filters out entries with empty names', async () => {
|
|
const soapClient = createMockSoapClient({
|
|
getDescription: vi.fn().mockResolvedValue({
|
|
status: 1,
|
|
data: {
|
|
FLD: [
|
|
{ '@_NAM': 'NUM', '@_TYP': 'Char', '@_LEN': '20', '@_C_ENG': 'Number' },
|
|
{ '@_TYP': 'Char', '@_LEN': '10' },
|
|
],
|
|
},
|
|
messages: [],
|
|
technicalInfos: [],
|
|
} satisfies SoapResult),
|
|
});
|
|
|
|
const result = await callHandler(soapClient, { publicName: 'SIH' });
|
|
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.fieldCount).toBe(1);
|
|
});
|
|
|
|
it('returns error on business error (status=0)', async () => {
|
|
const soapClient = createMockSoapClient({
|
|
getDescription: vi.fn().mockResolvedValue({
|
|
status: 0,
|
|
data: null,
|
|
messages: [{ type: 3, message: 'Unknown publication' }],
|
|
technicalInfos: [],
|
|
} satisfies SoapResult),
|
|
});
|
|
|
|
const result = await callHandler(soapClient, { publicName: 'INVALID' });
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Unknown publication');
|
|
});
|
|
|
|
it('handles transport errors with classified hints', async () => {
|
|
const soapClient = createMockSoapClient({
|
|
getDescription: vi.fn().mockRejectedValue(new Error('unauthorized')),
|
|
});
|
|
|
|
const result = await callHandler(soapClient, { publicName: 'SIH' });
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('unauthorized');
|
|
expect(result.content[0].text).toContain('Hint');
|
|
});
|
|
});
|
|
});
|