forked from patternfly/patternfly-mcp
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtool.patternFlyDocs.test.ts
More file actions
227 lines (198 loc) · 7.37 KB
/
Copy pathtool.patternFlyDocs.test.ts
File metadata and controls
227 lines (198 loc) · 7.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
import { McpError } from '@modelcontextprotocol/sdk/types.js';
import { processDocsFunction } from '../server.getResources';
import { getPatternFlyComponentSchema, getPatternFlyMcpResources, setCategoryDisplayLabel } from '../patternFly.getResources';
import { searchPatternFly } from '../patternFly.search';
import { isPlainObject } from '../server.helpers';
import { usePatternFlyDocsTool } from '../tool.patternFlyDocs';
import { DEFAULT_OPTIONS } from '../options.defaults';
// Mock dependencies
jest.mock('../server.getResources');
jest.mock('../patternFly.getResources');
jest.mock('../patternFly.search');
jest.mock('../server.caching', () => ({
memo: jest.fn(fn => fn)
}));
const mockProcessDocs = processDocsFunction as jest.MockedFunction<typeof processDocsFunction>;
const mockComponentSchema = getPatternFlyComponentSchema as jest.MockedFunction<typeof getPatternFlyComponentSchema>;
const mockGetResources = getPatternFlyMcpResources as jest.MockedFunction<typeof getPatternFlyMcpResources>;
const mockSearch = searchPatternFly as jest.MockedFunction<typeof searchPatternFly>;
const mockSchema = getPatternFlyComponentSchema as jest.MockedFunction<typeof getPatternFlyComponentSchema>;
const mockSetCategoryLabel = setCategoryDisplayLabel as jest.MockedFunction<typeof setCategoryDisplayLabel>;
describe('usePatternFlyDocsTool', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should have a consistent return structure', () => {
const tool = usePatternFlyDocsTool();
expect({
name: tool[0],
schema: isPlainObject(tool[1]),
callback: tool[2]
}).toMatchSnapshot('structure');
});
});
describe('usePatternFlyDocsTool, callback', () => {
beforeEach(() => {
jest.clearAllMocks();
mockGetResources.mockResolvedValue({
latestVersion: 'v6',
latestSchemasVersion: 'v6',
byPath: {
'components/loremButton.md': { name: 'button', version: 'v6', displayName: 'Button' }
}
} as any);
mockSearch.mockResolvedValue({
exactMatches: [{ entries: [{ path: 'components/loremButton.md' }] }],
searchResults: []
} as any);
mockSetCategoryLabel.mockImplementation(entry => entry?.category || 'Documentation');
});
it.each([
{
description: 'single file, mock path',
processedValue: {
path: 'components/button.md',
content: 'single documentation content'
},
urlList: ['components/button.md']
},
{
description: 'multiple files, mock paths',
processedValue: {
path: 'components/button.md',
content: 'combined documentation content'
},
urlList: ['components/button.md', 'components/card.md', 'components/table.md']
},
{
description: 'with invalid urlList',
processedValue: {
path: 'invalid-path',
content: 'Failed to load'
},
urlList: ['invalid-url']
},
{
description: 'with name and actual path',
processedValue: {
path: 'documentation:chatbot/README.md',
content: 'chatbot documentation content'
},
name: 'chatbot'
}
])('should attempt to parse parameters, $description', async ({ processedValue, urlList, name }) => {
mockProcessDocs.mockResolvedValue([processedValue] as any);
const [_name, _schema, callback] = usePatternFlyDocsTool();
const result = await callback({ urlList, name });
expect(mockProcessDocs).toHaveBeenCalledTimes(1);
expect(result.content[0].text).toBeDefined();
expect(result.content[0].text.split('\n')[0]).toMatchSnapshot();
});
it.each([
{
description: 'with missing or undefined urlList',
error: 'Provide either a string',
urlList: undefined
},
{
description: 'with null urlList',
error: 'Provide either a string',
urlList: null
},
{
description: 'when urlList is not an array',
error: 'Provide either a string',
urlList: 'not-an-array'
},
{
description: 'with empty files',
error: 'array must contain strings',
urlList: ['components/button.md', '', ' ', 'components/card.md', 'components/table.md']
},
{
description: 'with empty urlList',
error: 'Provide either a string',
urlList: []
},
{
description: 'with empty strings in a urlList',
error: 'array must contain strings',
urlList: ['', ' ']
},
{
description: 'with both urlList and name',
error: 'Provide either a string',
urlList: ['components/button.md'],
name: 'lorem ipsum'
}
])('should handle errors, $description', async ({ error, urlList, name }) => {
const [_name, _schema, callback] = usePatternFlyDocsTool();
await expect(callback({ urlList, name })).rejects.toThrow(McpError);
await expect(callback({ urlList, name })).rejects.toThrow(error);
});
it('should handle processing errors', async () => {
mockProcessDocs.mockRejectedValue(new Error('File not found'));
const [_name, _schema, callback] = usePatternFlyDocsTool();
await expect(callback({ urlList: ['https://www.patternfly.org//missing.md'] })).rejects.toThrow(McpError);
await expect(callback({ urlList: ['https://www.patternfly.org/missing.md'] })).rejects.toThrow('Failed to fetch documentation');
});
it('should have a specific markdown format', async () => {
mockProcessDocs.mockResolvedValue([
{
path: 'components/loremButton.md',
content: 'lorem documentation content'
},
{
path: 'components/ipsumButton.md',
content: 'ipsum documentation content'
}
] as any);
mockComponentSchema.mockResolvedValue({
$schema: 'http://json-schema.org/draft-07/schema#',
title: 'Button',
type: 'object',
properties: {
variant: { type: 'string' }
}
} as any);
const [_name, _schema, callback] = usePatternFlyDocsTool();
const result = await callback({ name: 'button' });
expect(result.content).toMatchSnapshot('Button');
});
it('usePatternFlyDocs resolves patternfly:// URIs in name', async () => {
mockSearch.mockResolvedValue({
exactMatches: [{
name: 'ToolbarFilter',
isSchemasAvailable: true,
entries: [{ name: 'ToolbarFilter', version: 'v6', path: '' }]
}],
searchResults: []
} as any);
mockSchema.mockResolvedValue({
name: 'ToolbarFilter',
schema: { type: 'object' }
} as any);
mockProcessDocs.mockResolvedValue([]);
const [_name, _schema, callback] = usePatternFlyDocsTool(DEFAULT_OPTIONS);
const result = await callback({ name: 'patternfly://docs/ToolbarFilter' }) as any;
expect(result.content[0].text).toContain('Component Schema for ToolbarFilter');
});
it('usePatternFlyDocs resolves patternfly:// URIs in urlList', async () => {
mockSearch.mockResolvedValue({
exactMatches: [{
name: 'ToolbarFilter',
isSchemasAvailable: true,
entries: [{ name: 'ToolbarFilter', version: 'v6', path: '' }]
}],
searchResults: []
} as any);
mockSchema.mockResolvedValue({
name: 'ToolbarFilter',
schema: { type: 'object' }
} as any);
mockProcessDocs.mockResolvedValue([]);
const [_name, _schema, callback] = usePatternFlyDocsTool(DEFAULT_OPTIONS);
const result = await callback({ urlList: ['patternfly://docs/ToolbarFilter'] }) as any;
expect(result.content[0].text).toContain('Component Schema for ToolbarFilter');
});
});