Skip to content

Commit 4f44efb

Browse files
committed
feat: add local PF code tools
1 parent c9e4838 commit 4f44efb

17 files changed

+1061
-1
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Mock implementation of utils.moduleResolver for Jest tests
3+
* This avoids import.meta.resolve compatibility issues in test environment
4+
*/
5+
6+
export const resolveModule = jest.fn((modulePath: string): string =>
7+
// Default mock behavior - can be overridden in individual tests
8+
`file://${modulePath}`);

src/__tests__/__snapshots__/server.test.ts.snap

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,12 @@ exports[`runServer should attempt to run server, use default tools: console 1`]
230230
[
231231
"Registered tool: componentSchemas",
232232
],
233+
[
234+
"Registered tool: getAvailableModulesTool",
235+
],
236+
[
237+
"Registered tool: getComponentSourceCode",
238+
],
233239
],
234240
"log": [
235241
[
@@ -678,6 +684,60 @@ exports[`runServer should attempt to run server, use default tools: console 1`]
678684
},
679685
[Function],
680686
],
687+
[
688+
"getAvailableModulesTool",
689+
{
690+
"description": "Retrieves a list of available Patternfly react-core modules in the current environment.",
691+
"inputSchema": {},
692+
},
693+
[Function],
694+
],
695+
[
696+
"getComponentSourceCode",
697+
{
698+
"description": "Retrieve a source code of a specified Patternfly react-core module in the current environment.",
699+
"inputSchema": {
700+
"componentName": ZodString {
701+
"_def": {
702+
"checks": [],
703+
"coerce": false,
704+
"description": "Name of the PatternFly component (e.g., "Button", "Table")",
705+
"typeName": "ZodString",
706+
},
707+
"and": [Function],
708+
"array": [Function],
709+
"brand": [Function],
710+
"catch": [Function],
711+
"default": [Function],
712+
"describe": [Function],
713+
"isNullable": [Function],
714+
"isOptional": [Function],
715+
"nullable": [Function],
716+
"nullish": [Function],
717+
"optional": [Function],
718+
"or": [Function],
719+
"parse": [Function],
720+
"parseAsync": [Function],
721+
"pipe": [Function],
722+
"promise": [Function],
723+
"readonly": [Function],
724+
"refine": [Function],
725+
"refinement": [Function],
726+
"safeParse": [Function],
727+
"safeParseAsync": [Function],
728+
"spa": [Function],
729+
"superRefine": [Function],
730+
"transform": [Function],
731+
"~standard": {
732+
"validate": [Function],
733+
"vendor": "zod",
734+
"version": 1,
735+
},
736+
},
737+
},
738+
},
739+
[Function],
740+
],
681741
],
682742
}
683743
`;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2+
3+
exports[`getAvailableModulesTool should have a consistent return structure: structure 1`] = `
4+
[
5+
"getAvailableModulesTool",
6+
{
7+
"description": "Retrieves a list of available Patternfly react-core modules in the current environment.",
8+
"inputSchema": {},
9+
},
10+
[Function],
11+
]
12+
`;
13+
14+
exports[`getAvailableModulesTool, callback should handle modules with special characters in names 1`] = `
15+
{
16+
"content": [
17+
{
18+
"text": "Button-Group;Data.Table;Nav_Item",
19+
"type": "text",
20+
},
21+
],
22+
}
23+
`;
24+
25+
exports[`getAvailableModulesTool, callback should return empty string when modules map is empty 1`] = `
26+
{
27+
"content": [
28+
{
29+
"text": "",
30+
"type": "text",
31+
},
32+
],
33+
}
34+
`;
35+
36+
exports[`getAvailableModulesTool, callback should return modules list separated by semicolons 1`] = `
37+
{
38+
"content": [
39+
{
40+
"text": "Button;Card;Table",
41+
"type": "text",
42+
},
43+
],
44+
}
45+
`;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2+
3+
exports[`getComponentSourceCode should have a consistent return structure: structure 1`] = `
4+
[
5+
"getComponentSourceCode",
6+
{
7+
"description": "Retrieve a source code of a specified Patternfly react-core module in the current environment.",
8+
"inputSchema": {
9+
"componentName": ZodString {
10+
"_def": {
11+
"checks": [],
12+
"coerce": false,
13+
"description": "Name of the PatternFly component (e.g., "Button", "Table")",
14+
"typeName": "ZodString",
15+
},
16+
"and": [Function],
17+
"array": [Function],
18+
"brand": [Function],
19+
"catch": [Function],
20+
"default": [Function],
21+
"describe": [Function],
22+
"isNullable": [Function],
23+
"isOptional": [Function],
24+
"nullable": [Function],
25+
"nullish": [Function],
26+
"optional": [Function],
27+
"or": [Function],
28+
"parse": [Function],
29+
"parseAsync": [Function],
30+
"pipe": [Function],
31+
"promise": [Function],
32+
"readonly": [Function],
33+
"refine": [Function],
34+
"refinement": [Function],
35+
"safeParse": [Function],
36+
"safeParseAsync": [Function],
37+
"spa": [Function],
38+
"superRefine": [Function],
39+
"transform": [Function],
40+
"~standard": {
41+
"validate": [Function],
42+
"vendor": "zod",
43+
"version": 1,
44+
},
45+
},
46+
},
47+
},
48+
[Function],
49+
]
50+
`;
51+
52+
exports[`getComponentSourceCode, callback successful component source retrieval should handle .tsx file when .ts file not found 1`] = `
53+
{
54+
"content": [
55+
{
56+
"text": "export const Button = () => { return <button>Click me</button>; };",
57+
"type": "text",
58+
},
59+
],
60+
}
61+
`;
62+
63+
exports[`getComponentSourceCode, callback successful component source retrieval should retrieve component source code successfully 1`] = `
64+
{
65+
"content": [
66+
{
67+
"text": "export const Button = () => { return <button>Click me</button>; };",
68+
"type": "text",
69+
},
70+
],
71+
}
72+
`;

src/__tests__/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { runServer } from '../server';
88
jest.mock('../options');
99
jest.mock('../options.context');
1010
jest.mock('../server');
11+
jest.mock('../utils.moduleResolver'); // Mock the module resolver to avoid import.meta issues
1112

1213
const mockParseCliOptions = parseCliOptions as jest.MockedFunction<typeof parseCliOptions>;
1314
const mockSetOptions = setOptions as jest.MockedFunction<typeof setOptions>;

src/__tests__/options.context.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getOptions, setOptions } from '../options.context';
66
// Mock dependencies
77
jest.mock('@modelcontextprotocol/sdk/server/mcp.js');
88
jest.mock('@modelcontextprotocol/sdk/server/stdio.js');
9+
jest.mock('../utils.moduleResolver'); // Mock the module resolver to avoid import.meta issues
910

1011
const MockMcpServer = McpServer as jest.MockedClass<typeof McpServer>;
1112
const MockStdioServerTransport = StdioServerTransport as jest.MockedClass<typeof StdioServerTransport>;

src/__tests__/server.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { type GlobalOptions } from '../options';
66
// Mock dependencies
77
jest.mock('@modelcontextprotocol/sdk/server/mcp.js');
88
jest.mock('@modelcontextprotocol/sdk/server/stdio.js');
9+
jest.mock('../utils.moduleResolver'); // Mock the module resolver to avoid import.meta issues
910

1011
const MockMcpServer = McpServer as jest.MockedClass<typeof McpServer>;
1112
const MockStdioServerTransport = StdioServerTransport as jest.MockedClass<typeof StdioServerTransport>;
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { McpError } from '@modelcontextprotocol/sdk/types.js';
2+
import { getAvailableModulesTool } from '../tool.getAvailableModules';
3+
import { getLocalModulesMap } from '../utils.getLocalModulesMap';
4+
5+
// Mock dependencies
6+
jest.mock('../utils.getLocalModulesMap');
7+
jest.mock('../utils.moduleResolver'); // Mock the module resolver to avoid import.meta issues
8+
jest.mock('../server.caching', () => ({
9+
memo: jest.fn(fn => fn)
10+
}));
11+
12+
const mockGetLocalModulesMap = getLocalModulesMap as jest.MockedFunction<typeof getLocalModulesMap>;
13+
14+
describe('getAvailableModulesTool', () => {
15+
beforeEach(() => {
16+
jest.clearAllMocks();
17+
});
18+
19+
it('should have a consistent return structure', () => {
20+
const tool = getAvailableModulesTool();
21+
22+
expect(tool).toMatchSnapshot('structure');
23+
});
24+
});
25+
26+
describe('getAvailableModulesTool, callback', () => {
27+
beforeEach(() => {
28+
jest.clearAllMocks();
29+
});
30+
31+
it('should return modules list separated by semicolons', async () => {
32+
const mockModulesMap = {
33+
Button: '/path/to/button',
34+
Card: '/path/to/card',
35+
Table: '/path/to/table'
36+
};
37+
38+
mockGetLocalModulesMap.mockResolvedValue(mockModulesMap);
39+
40+
const [, , callback] = getAvailableModulesTool();
41+
const result = await callback({});
42+
43+
expect(mockGetLocalModulesMap).toHaveBeenCalledWith('@patternfly/react-core');
44+
expect(result).toMatchSnapshot();
45+
});
46+
47+
it('should return empty string when modules map is empty', async () => {
48+
mockGetLocalModulesMap.mockResolvedValue({});
49+
50+
const [, , callback] = getAvailableModulesTool();
51+
const result = await callback({});
52+
53+
expect(result).toMatchSnapshot();
54+
});
55+
56+
it('should handle modules with special characters in names', async () => {
57+
const mockModulesMap = {
58+
'Button-Group': '/path/to/button-group',
59+
'Data.Table': '/path/to/data-table',
60+
Nav_Item: '/path/to/nav-item'
61+
};
62+
63+
mockGetLocalModulesMap.mockResolvedValue(mockModulesMap);
64+
65+
const [, , callback] = getAvailableModulesTool();
66+
const result = await callback({});
67+
68+
expect(result).toMatchSnapshot();
69+
});
70+
71+
it.each([
72+
{
73+
description: 'with Error object',
74+
error: new Error('Package not found')
75+
},
76+
{
77+
description: 'with string error',
78+
error: 'String error message'
79+
},
80+
{
81+
description: 'with null error',
82+
error: null
83+
}
84+
])('should handle getLocalModulesMap errors, $description', async ({ error }) => {
85+
mockGetLocalModulesMap.mockRejectedValue(error);
86+
87+
const [, , callback] = getAvailableModulesTool();
88+
89+
await expect(callback({})).rejects.toThrow(McpError);
90+
await expect(callback({})).rejects.toThrow('Failed to retrieve available modules');
91+
});
92+
93+
it('should always call with @patternfly/react-core package', async () => {
94+
mockGetLocalModulesMap.mockResolvedValue({ Component: '/path' });
95+
96+
const [, , callback] = getAvailableModulesTool();
97+
98+
await callback({});
99+
100+
expect(mockGetLocalModulesMap).toHaveBeenCalledWith('@patternfly/react-core');
101+
expect(mockGetLocalModulesMap).toHaveBeenCalledTimes(1);
102+
});
103+
});

0 commit comments

Comments
 (0)