@splunk/splunk-ui-mcp 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -5
- package/lib/constants/versions.js +3 -3
- package/lib/index.js +16 -4
- package/lib/prompts/audit/audit-prompt.d.ts +2 -0
- package/lib/prompts/audit/audit-prompt.js +11 -0
- package/lib/prompts/audit/audit.d.ts +16 -0
- package/lib/prompts/audit/audit.js +36 -0
- package/lib/prompts/audit/index.d.ts +1 -0
- package/lib/prompts/audit/index.js +1 -0
- package/lib/prompts/build-ui/build-figma-design-prompt.d.ts +2 -0
- package/lib/prompts/build-ui/build-figma-design-prompt.js +9 -0
- package/lib/prompts/build-ui/build-ui-prompt.d.ts +2 -0
- package/lib/prompts/build-ui/build-ui-prompt.js +8 -0
- package/lib/prompts/build-ui/build-ui.d.ts +16 -0
- package/lib/prompts/build-ui/build-ui.js +62 -0
- package/lib/prompts/build-ui/index.d.ts +1 -0
- package/lib/prompts/build-ui/index.js +1 -0
- package/lib/prompts/create-scaffold/create-scaffold-prompt.d.ts +2 -0
- package/lib/prompts/create-scaffold/create-scaffold-prompt.js +34 -0
- package/lib/prompts/create-scaffold/create-scaffold.d.ts +16 -0
- package/lib/prompts/create-scaffold/create-scaffold.js +60 -0
- package/lib/prompts/create-scaffold/index.d.ts +1 -0
- package/lib/prompts/create-scaffold/index.js +1 -0
- package/lib/prompts/session-feedback/index.d.ts +1 -0
- package/lib/prompts/session-feedback/index.js +1 -0
- package/lib/prompts/session-feedback/session-feedback-prompt.d.ts +2 -0
- package/lib/prompts/session-feedback/session-feedback-prompt.js +113 -0
- package/lib/prompts/session-feedback/session-feedback.d.ts +11 -0
- package/lib/prompts/session-feedback/session-feedback.js +22 -0
- package/lib/prompts/types.d.ts +8 -0
- package/lib/resources/components.js +12 -6
- package/lib/resources/design-tokens.d.ts +17 -0
- package/lib/resources/design-tokens.js +115 -0
- package/lib/tools/find_component.d.ts +19 -0
- package/lib/tools/find_component.js +93 -0
- package/lib/tools/find_design_token.d.ts +16 -0
- package/lib/tools/find_design_token.js +134 -0
- package/lib/tools/{find_icons.d.ts → find_icon.d.ts} +2 -2
- package/lib/tools/{find_icons.js → find_icon.js} +5 -5
- package/lib/utils/component-catalog.d.ts +19 -2
- package/lib/utils/component-catalog.js +77 -29
- package/lib/utils/design-token-catalog.d.ts +37 -0
- package/lib/utils/design-token-catalog.js +166 -0
- package/lib/utils/package-assets.d.ts +8 -0
- package/lib/utils/package-assets.js +35 -0
- package/package.json +16 -10
- package/lib/resources/tests/components.unit.js +0 -133
- package/lib/resources/tests/icons.unit.d.ts +0 -1
- package/lib/resources/tests/icons.unit.js +0 -161
- package/lib/tools/get_component_docs.d.ts +0 -19
- package/lib/tools/get_component_docs.js +0 -82
- package/lib/tools/tests/find_icons.unit.d.ts +0 -1
- package/lib/tools/tests/find_icons.unit.js +0 -149
- package/lib/tools/tests/get_component_docs.unit.d.ts +0 -1
- package/lib/tools/tests/get_component_docs.unit.js +0 -131
- package/lib/tools/tests/requirements.unit.d.ts +0 -1
- package/lib/tools/tests/requirements.unit.js +0 -34
- package/lib/utils/tests/component-catalog.unit.d.ts +0 -1
- package/lib/utils/tests/component-catalog.unit.js +0 -144
- /package/lib/{resources/tests/components.unit.d.ts → prompts/types.js} +0 -0
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
const NO_RESULTS_TEXT = 'No icons found. Try different keywords like "search", "notification", "user", "chart", etc.';
|
|
3
|
-
const ICON_CATALOG_FIXTURE = vi.hoisted(() => [
|
|
4
|
-
{
|
|
5
|
-
name: 'SearchIcon',
|
|
6
|
-
key: './SearchIcon',
|
|
7
|
-
category: 'Interface',
|
|
8
|
-
description: 'Use for search interactions.',
|
|
9
|
-
keywords: 'search, magnify, find',
|
|
10
|
-
variants: ['default', 'filled'],
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
name: 'AlertIcon',
|
|
14
|
-
key: './AlertIcon',
|
|
15
|
-
description: 'Communicates alert or warning states.',
|
|
16
|
-
keywords: 'alert, warning',
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
name: 'UserIcon',
|
|
20
|
-
key: './UserIcon',
|
|
21
|
-
keywords: 'profile, account',
|
|
22
|
-
},
|
|
23
|
-
{ name: 'PrimaryIcon', key: './PrimaryIcon' },
|
|
24
|
-
{ name: 'AltOne', key: './AltOne' },
|
|
25
|
-
{ name: 'AltTwo', key: './AltTwo' },
|
|
26
|
-
{ name: 'AltThree', key: './AltThree' },
|
|
27
|
-
{ name: 'AltFour', key: './AltFour' },
|
|
28
|
-
{ name: 'AltFive', key: './AltFive' },
|
|
29
|
-
{ name: 'SoloIcon', key: './SoloIcon' },
|
|
30
|
-
]);
|
|
31
|
-
const INDEXED_DATA = { indexed: true };
|
|
32
|
-
const addSearchDataMock = vi.fn();
|
|
33
|
-
const searchMock = vi.fn();
|
|
34
|
-
vi.mock('@splunk/react-icons/icon-catalog.json', () => ({
|
|
35
|
-
default: ICON_CATALOG_FIXTURE,
|
|
36
|
-
}));
|
|
37
|
-
vi.mock('@splunk/ui-utils/search', () => ({
|
|
38
|
-
addSearchData: addSearchDataMock,
|
|
39
|
-
search: searchMock,
|
|
40
|
-
}));
|
|
41
|
-
describe('find_icons tool', () => {
|
|
42
|
-
beforeEach(() => {
|
|
43
|
-
vi.resetModules();
|
|
44
|
-
addSearchDataMock.mockReset();
|
|
45
|
-
searchMock.mockReset();
|
|
46
|
-
searchMock.mockReturnValue([]);
|
|
47
|
-
addSearchDataMock.mockReturnValue(INDEXED_DATA);
|
|
48
|
-
});
|
|
49
|
-
it('exposes metadata and prepares search index', async () => {
|
|
50
|
-
const { default: tool } = await import("../find_icons.js");
|
|
51
|
-
expect(tool.name).toBe('find_icons');
|
|
52
|
-
expect('config' in tool).toBe(true);
|
|
53
|
-
expect(tool.config?.title).toBeDefined();
|
|
54
|
-
expect(tool.config?.description).toBeDefined();
|
|
55
|
-
expect(tool.config.inputSchema?.query).toBeDefined();
|
|
56
|
-
});
|
|
57
|
-
it('prepares search index', async () => {
|
|
58
|
-
addSearchDataMock.mockReturnValue({
|
|
59
|
-
mock: true,
|
|
60
|
-
});
|
|
61
|
-
await import("../find_icons.js");
|
|
62
|
-
expect(addSearchDataMock).toHaveBeenCalledWith(ICON_CATALOG_FIXTURE);
|
|
63
|
-
expect(addSearchDataMock).toHaveReturnedWith({ mock: true });
|
|
64
|
-
});
|
|
65
|
-
describe('handler', () => {
|
|
66
|
-
it('returns guidance when query is empty', async () => {
|
|
67
|
-
const { default: tool } = await import("../find_icons.js");
|
|
68
|
-
const result = tool.handler({ query: '' });
|
|
69
|
-
expect(searchMock).not.toHaveBeenCalled();
|
|
70
|
-
expect(result.content[0]?.type).toBe('text');
|
|
71
|
-
expect(result.content[0]?.text).toBe(NO_RESULTS_TEXT);
|
|
72
|
-
});
|
|
73
|
-
it('returns guidance when no icons match search', async () => {
|
|
74
|
-
const { default: tool } = await import("../find_icons.js");
|
|
75
|
-
const result = tool.handler({ query: 'alert' });
|
|
76
|
-
expect(searchMock).toHaveBeenCalledWith('alert', INDEXED_DATA);
|
|
77
|
-
expect(result.content[0]?.text).toBe(NO_RESULTS_TEXT);
|
|
78
|
-
});
|
|
79
|
-
it('returns icon details and alternative resource links when matches are found', async () => {
|
|
80
|
-
searchMock.mockReturnValue([
|
|
81
|
-
{
|
|
82
|
-
name: 'SearchIcon',
|
|
83
|
-
description: 'Use for search interactions.',
|
|
84
|
-
searchScore: { nameScore: 100, keywordsScore: 20, descriptionScore: 0 },
|
|
85
|
-
},
|
|
86
|
-
{ name: 'AlertIcon', description: 'Communicates alert or warning states.' },
|
|
87
|
-
{ name: 'UserIcon', keywords: 'profile, account' },
|
|
88
|
-
]);
|
|
89
|
-
const { default: tool } = await import("../find_icons.js");
|
|
90
|
-
const result = tool.handler({ query: 'search' });
|
|
91
|
-
expect(searchMock).toHaveBeenCalledWith('search', INDEXED_DATA);
|
|
92
|
-
const [summary, ...rest] = result.content;
|
|
93
|
-
const resourceLinks = rest.filter((item) => item?.type === 'resource_link');
|
|
94
|
-
expect(summary).toMatchObject({ type: 'text' });
|
|
95
|
-
const summaryText = summary?.text ?? '';
|
|
96
|
-
expect(summaryText).toContain('# Icons for "search"');
|
|
97
|
-
expect(summaryText).toContain('## Recommended Icon');
|
|
98
|
-
expect(summaryText).toContain('Recommended to use <SearchIcon /> for "search"');
|
|
99
|
-
expect(summaryText).toContain('- Import: `@splunk/react-icons/SearchIcon`');
|
|
100
|
-
expect(summaryText).toContain('- Category: Interface');
|
|
101
|
-
expect(summaryText).toContain('- Keywords: search, magnify, find');
|
|
102
|
-
expect(summaryText).toContain('```tsx');
|
|
103
|
-
expect(summaryText).toContain('Follow the resource links below for full metadata and usage snippets.');
|
|
104
|
-
expect(summaryText).toContain('## Alternative Icons');
|
|
105
|
-
expect(summaryText).toContain('- AlertIcon');
|
|
106
|
-
expect(summaryText).toContain('- UserIcon');
|
|
107
|
-
expect(resourceLinks).toHaveLength(3);
|
|
108
|
-
expect(resourceLinks[0]).toMatchObject({
|
|
109
|
-
name: 'SearchIcon',
|
|
110
|
-
uri: 'mcp://splunk-ui/icons/SearchIcon',
|
|
111
|
-
});
|
|
112
|
-
expect(resourceLinks[1]).toMatchObject({
|
|
113
|
-
name: 'AlertIcon',
|
|
114
|
-
uri: 'mcp://splunk-ui/icons/AlertIcon',
|
|
115
|
-
});
|
|
116
|
-
expect(resourceLinks[2]).toMatchObject({
|
|
117
|
-
name: 'UserIcon',
|
|
118
|
-
uri: 'mcp://splunk-ui/icons/UserIcon',
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
it('limits alternative icons to four entries', async () => {
|
|
122
|
-
searchMock.mockReturnValue([
|
|
123
|
-
{ name: 'PrimaryIcon' },
|
|
124
|
-
{ name: 'AltOne' },
|
|
125
|
-
{ name: 'AltTwo' },
|
|
126
|
-
{ name: 'AltThree' },
|
|
127
|
-
{ name: 'AltFour' },
|
|
128
|
-
{ name: 'AltFive' },
|
|
129
|
-
]);
|
|
130
|
-
const { default: tool } = await import("../find_icons.js");
|
|
131
|
-
const result = tool.handler({ query: 'primary' });
|
|
132
|
-
const summaryText = result.content[0]?.type === 'text' ? (result.content[0]?.text ?? '') : '';
|
|
133
|
-
expect(summaryText).toContain('- AltOne');
|
|
134
|
-
expect(summaryText).toContain('- AltTwo');
|
|
135
|
-
expect(summaryText).toContain('- AltThree');
|
|
136
|
-
expect(summaryText).toContain('- AltFour');
|
|
137
|
-
expect(summaryText).not.toContain('- AltFive');
|
|
138
|
-
});
|
|
139
|
-
it('omits alternative section when only one icon matches', async () => {
|
|
140
|
-
const { default: tool } = await import("../find_icons.js");
|
|
141
|
-
const { handler } = tool;
|
|
142
|
-
const args = { query: 'solo' };
|
|
143
|
-
searchMock.mockReturnValue([{ name: 'SoloIcon' }]);
|
|
144
|
-
const result = await handler(args);
|
|
145
|
-
const summaryText = result.content[0]?.type === 'text' ? (result.content[0]?.text ?? '') : '';
|
|
146
|
-
expect(summaryText).not.toContain('## Alternative Icons');
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
-
const MOCK_COMPONENT_LIST = [
|
|
3
|
-
{
|
|
4
|
-
name: 'Button',
|
|
5
|
-
filename: 'Button.md',
|
|
6
|
-
category: 'Base',
|
|
7
|
-
},
|
|
8
|
-
{
|
|
9
|
-
name: 'Card Layout',
|
|
10
|
-
filename: 'Card Layout.md',
|
|
11
|
-
category: 'Base',
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
name: 'Text',
|
|
15
|
-
filename: 'Text.md',
|
|
16
|
-
category: 'Data Entry',
|
|
17
|
-
},
|
|
18
|
-
];
|
|
19
|
-
const MOCK_BUTTON_DOCS = `# Button
|
|
20
|
-
|
|
21
|
-
## Overview
|
|
22
|
-
|
|
23
|
-
A button component for triggering actions.
|
|
24
|
-
|
|
25
|
-
## Examples
|
|
26
|
-
|
|
27
|
-
\`\`\`typescript
|
|
28
|
-
import Button from '@splunk/react-ui/Button';
|
|
29
|
-
|
|
30
|
-
<Button label="Click me" />
|
|
31
|
-
\`\`\`
|
|
32
|
-
|
|
33
|
-
## API
|
|
34
|
-
|
|
35
|
-
### Props
|
|
36
|
-
|
|
37
|
-
- label: string
|
|
38
|
-
- onClick: () => void
|
|
39
|
-
`;
|
|
40
|
-
// Mock the component catalog
|
|
41
|
-
vi.mock('../../utils/component-catalog.ts', () => ({
|
|
42
|
-
getComponentList: vi.fn(() => MOCK_COMPONENT_LIST),
|
|
43
|
-
componentExists: vi.fn((name) => MOCK_COMPONENT_LIST.some((c) => c.name === name)),
|
|
44
|
-
getComponentDocs: vi.fn((name) => {
|
|
45
|
-
if (name === 'Button') {
|
|
46
|
-
return MOCK_BUTTON_DOCS;
|
|
47
|
-
}
|
|
48
|
-
throw new Error(`Component "${name}" not found in catalog`);
|
|
49
|
-
}),
|
|
50
|
-
}));
|
|
51
|
-
// Mock the components resource
|
|
52
|
-
vi.mock('../../resources/components.ts', () => ({
|
|
53
|
-
createComponentResourceLink: vi.fn((name) => ({
|
|
54
|
-
type: 'resource_link',
|
|
55
|
-
uri: `mcp://splunk-ui/components/${encodeURIComponent(name)}`,
|
|
56
|
-
name,
|
|
57
|
-
})),
|
|
58
|
-
}));
|
|
59
|
-
describe('get_component_docs tool', () => {
|
|
60
|
-
beforeEach(() => {
|
|
61
|
-
vi.clearAllMocks();
|
|
62
|
-
});
|
|
63
|
-
it('returns documentation for an existing component', async () => {
|
|
64
|
-
const tool = (await import("../get_component_docs.js")).default;
|
|
65
|
-
const result = tool.handler({ componentName: 'Button' });
|
|
66
|
-
expect(result.content).toHaveLength(2);
|
|
67
|
-
expect(result.content[0]).toMatchObject({
|
|
68
|
-
type: 'text',
|
|
69
|
-
text: MOCK_BUTTON_DOCS,
|
|
70
|
-
});
|
|
71
|
-
expect(result.content[1]).toMatchObject({
|
|
72
|
-
type: 'resource_link',
|
|
73
|
-
name: 'Button',
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
it('returns error message for non-existent component', async () => {
|
|
77
|
-
const tool = (await import("../get_component_docs.js")).default;
|
|
78
|
-
const result = tool.handler({ componentName: 'NonExistent' });
|
|
79
|
-
expect(result.content).toHaveLength(1);
|
|
80
|
-
expect(result.content[0]).toMatchObject({
|
|
81
|
-
type: 'text',
|
|
82
|
-
});
|
|
83
|
-
expect(result.content[0].text).toContain('Component "NonExistent" not found');
|
|
84
|
-
expect(result.content[0].text).toContain('Available components include');
|
|
85
|
-
});
|
|
86
|
-
it('handles empty component name', async () => {
|
|
87
|
-
const tool = (await import("../get_component_docs.js")).default;
|
|
88
|
-
const result = tool.handler({ componentName: '' });
|
|
89
|
-
expect(result.content).toHaveLength(1);
|
|
90
|
-
expect(result.content[0]).toMatchObject({
|
|
91
|
-
type: 'text',
|
|
92
|
-
});
|
|
93
|
-
expect(result.content[0].text).toContain('Component name is required');
|
|
94
|
-
});
|
|
95
|
-
it('handles whitespace-only component name', async () => {
|
|
96
|
-
const tool = (await import("../get_component_docs.js")).default;
|
|
97
|
-
const result = tool.handler({ componentName: ' ' });
|
|
98
|
-
expect(result.content).toHaveLength(1);
|
|
99
|
-
expect(result.content[0]).toMatchObject({
|
|
100
|
-
type: 'text',
|
|
101
|
-
});
|
|
102
|
-
expect(result.content[0].text).toContain('Component name is required');
|
|
103
|
-
});
|
|
104
|
-
it('handles errors when reading documentation', async () => {
|
|
105
|
-
const { getComponentDocs } = await import("../../utils/component-catalog.js");
|
|
106
|
-
// Mock an error for a specific component
|
|
107
|
-
getComponentDocs.mockImplementationOnce(() => {
|
|
108
|
-
throw new Error('File system error');
|
|
109
|
-
});
|
|
110
|
-
const tool = (await import("../get_component_docs.js")).default;
|
|
111
|
-
const result = tool.handler({ componentName: 'Button' });
|
|
112
|
-
expect(result.isError).toBe(true);
|
|
113
|
-
expect(result.content).toHaveLength(1);
|
|
114
|
-
expect(result.content[0].text).toContain('Failed to retrieve documentation');
|
|
115
|
-
expect(result.content[0].text).toContain('File system error');
|
|
116
|
-
});
|
|
117
|
-
it('includes resource link in successful response', async () => {
|
|
118
|
-
const { createComponentResourceLink } = await import("../../resources/components.js");
|
|
119
|
-
const tool = (await import("../get_component_docs.js")).default;
|
|
120
|
-
tool.handler({ componentName: 'Button' });
|
|
121
|
-
expect(createComponentResourceLink).toHaveBeenCalledWith('Button');
|
|
122
|
-
});
|
|
123
|
-
it('works with components that have spaces in name', async () => {
|
|
124
|
-
const tool = (await import("../get_component_docs.js")).default;
|
|
125
|
-
// Card Layout exists in mock data
|
|
126
|
-
const result = tool.handler({ componentName: 'Card Layout' });
|
|
127
|
-
// Should try to get docs (will fail in mock, but proves it checked existence)
|
|
128
|
-
expect(result.isError).toBe(true);
|
|
129
|
-
expect(result.content[0].text).toContain('Failed to retrieve documentation');
|
|
130
|
-
});
|
|
131
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, test } from 'vitest';
|
|
2
|
-
import requirements from "../requirements.js";
|
|
3
|
-
import VERSIONS from "../../constants/versions.js";
|
|
4
|
-
describe('requirements tool', () => {
|
|
5
|
-
describe('configuration', () => {
|
|
6
|
-
it('should have correct tool name', () => {
|
|
7
|
-
expect(requirements.name).toBe('list_splunk_ui_requirements');
|
|
8
|
-
});
|
|
9
|
-
it('should have proper config structure', () => {
|
|
10
|
-
expect(requirements.config).toBeDefined();
|
|
11
|
-
expect(requirements.config.title).toBeDefined();
|
|
12
|
-
expect(requirements.config.description).toBeDefined();
|
|
13
|
-
expect(requirements.config.inputSchema).toEqual({});
|
|
14
|
-
});
|
|
15
|
-
it('should have a handler function', () => {
|
|
16
|
-
expect(typeof requirements.handler).toBe('function');
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
describe('handler', () => {
|
|
20
|
-
it('returns the correct JSON structure', () => {
|
|
21
|
-
const result = requirements.handler();
|
|
22
|
-
const text = result.content[0].text;
|
|
23
|
-
const parsed = JSON.parse(text);
|
|
24
|
-
expect(parsed).toBeDefined();
|
|
25
|
-
expect(typeof parsed).toBe('object');
|
|
26
|
-
});
|
|
27
|
-
test.each(Object.entries(VERSIONS))('should include %s and version in JSON response', (pkg, version) => {
|
|
28
|
-
const result = requirements.handler();
|
|
29
|
-
const text = result.content[0].text;
|
|
30
|
-
const parsed = JSON.parse(text);
|
|
31
|
-
expect(parsed[pkg]).toBe(version);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
const MOCK_LLMS_TXT = `# React UI (@splunk/react-ui)
|
|
3
|
-
|
|
4
|
-
A library of UI components, all independent of the Splunk Enterprise environment.
|
|
5
|
-
|
|
6
|
-
## Base
|
|
7
|
-
|
|
8
|
-
- **Anchor**
|
|
9
|
-
- **Avatar**
|
|
10
|
-
- **Button**
|
|
11
|
-
- **Card Layout**
|
|
12
|
-
|
|
13
|
-
## Content
|
|
14
|
-
|
|
15
|
-
- **Heading**
|
|
16
|
-
- **Paragraph**
|
|
17
|
-
|
|
18
|
-
## Deprecated
|
|
19
|
-
|
|
20
|
-
- **Accordion**
|
|
21
|
-
`;
|
|
22
|
-
const MOCK_BUTTON_DOCS = `# Button
|
|
23
|
-
|
|
24
|
-
## Overview
|
|
25
|
-
|
|
26
|
-
A button component for triggering actions.
|
|
27
|
-
|
|
28
|
-
## Examples
|
|
29
|
-
|
|
30
|
-
\`\`\`typescript
|
|
31
|
-
import Button from '@splunk/react-ui/Button';
|
|
32
|
-
\`\`\`
|
|
33
|
-
`;
|
|
34
|
-
// Mock file system operations
|
|
35
|
-
vi.mock('fs', () => ({
|
|
36
|
-
readFileSync: vi.fn((path) => {
|
|
37
|
-
if (path.includes('llms.txt')) {
|
|
38
|
-
return MOCK_LLMS_TXT;
|
|
39
|
-
}
|
|
40
|
-
if (path.includes('Button.md')) {
|
|
41
|
-
return MOCK_BUTTON_DOCS;
|
|
42
|
-
}
|
|
43
|
-
throw new Error(`File not found: ${path}`);
|
|
44
|
-
}),
|
|
45
|
-
}));
|
|
46
|
-
describe('component-catalog', () => {
|
|
47
|
-
beforeEach(async () => {
|
|
48
|
-
vi.resetModules();
|
|
49
|
-
const { setDocsLlmPath } = await import("../component-catalog.js");
|
|
50
|
-
setDocsLlmPath('/mocked/docs-llm');
|
|
51
|
-
});
|
|
52
|
-
describe('getComponentList', () => {
|
|
53
|
-
it('parses components from llms.txt', async () => {
|
|
54
|
-
const { getComponentList } = await import("../component-catalog.js");
|
|
55
|
-
const components = getComponentList();
|
|
56
|
-
expect(components).toHaveLength(7);
|
|
57
|
-
expect(components[0]).toEqual({
|
|
58
|
-
name: 'Anchor',
|
|
59
|
-
filename: 'Anchor.md',
|
|
60
|
-
category: 'Base',
|
|
61
|
-
});
|
|
62
|
-
expect(components[2]).toEqual({
|
|
63
|
-
name: 'Button',
|
|
64
|
-
filename: 'Button.md',
|
|
65
|
-
category: 'Base',
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
it('preserves category information', async () => {
|
|
69
|
-
const { getComponentList } = await import("../component-catalog.js");
|
|
70
|
-
const components = getComponentList();
|
|
71
|
-
const heading = components.find((c) => c.name === 'Heading');
|
|
72
|
-
expect(heading?.category).toBe('Content');
|
|
73
|
-
const accordion = components.find((c) => c.name === 'Accordion');
|
|
74
|
-
expect(accordion?.category).toBe('Deprecated');
|
|
75
|
-
});
|
|
76
|
-
it('caches the result on subsequent calls', async () => {
|
|
77
|
-
// Need to clear mocks and reload in this test to accurately count calls
|
|
78
|
-
vi.clearAllMocks();
|
|
79
|
-
vi.resetModules();
|
|
80
|
-
const { readFileSync } = await import('fs');
|
|
81
|
-
const { setDocsLlmPath, getComponentList } = await import("../component-catalog.js");
|
|
82
|
-
setDocsLlmPath('/mocked/docs-llm');
|
|
83
|
-
getComponentList();
|
|
84
|
-
getComponentList();
|
|
85
|
-
getComponentList();
|
|
86
|
-
// Should only read the llms.txt file once (cached after first read)
|
|
87
|
-
const llmsTxtCalls = readFileSync.mock.calls.filter((call) => call[0].includes('llms.txt'));
|
|
88
|
-
expect(llmsTxtCalls).toHaveLength(1);
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
describe('componentExists', () => {
|
|
92
|
-
it('returns true for components in the catalog', async () => {
|
|
93
|
-
const { componentExists } = await import("../component-catalog.js");
|
|
94
|
-
expect(componentExists('Button')).toBe(true);
|
|
95
|
-
expect(componentExists('Heading')).toBe(true);
|
|
96
|
-
});
|
|
97
|
-
it('returns false for components not in the catalog', async () => {
|
|
98
|
-
const { componentExists } = await import("../component-catalog.js");
|
|
99
|
-
expect(componentExists('NonExistent')).toBe(false);
|
|
100
|
-
expect(componentExists('Unknown')).toBe(false);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
describe('getComponentInfo', () => {
|
|
104
|
-
it('returns component info for existing components', async () => {
|
|
105
|
-
const { getComponentInfo } = await import("../component-catalog.js");
|
|
106
|
-
const buttonInfo = getComponentInfo('Button');
|
|
107
|
-
expect(buttonInfo).toEqual({
|
|
108
|
-
name: 'Button',
|
|
109
|
-
filename: 'Button.md',
|
|
110
|
-
category: 'Base',
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
it('returns undefined for non-existent components', async () => {
|
|
114
|
-
const { getComponentInfo } = await import("../component-catalog.js");
|
|
115
|
-
expect(getComponentInfo('NonExistent')).toBeUndefined();
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
describe('getComponentDocs', () => {
|
|
119
|
-
it('reads and returns markdown documentation', async () => {
|
|
120
|
-
const { getComponentDocs } = await import("../component-catalog.js");
|
|
121
|
-
const docs = getComponentDocs('Button');
|
|
122
|
-
expect(docs).toBe(MOCK_BUTTON_DOCS);
|
|
123
|
-
});
|
|
124
|
-
it('throws error for non-existent component', async () => {
|
|
125
|
-
const { getComponentDocs } = await import("../component-catalog.js");
|
|
126
|
-
expect(() => getComponentDocs('NonExistent')).toThrow('Component "NonExistent" not found in catalog');
|
|
127
|
-
});
|
|
128
|
-
it('rejects component names with path separators', async () => {
|
|
129
|
-
const { getComponentDocs } = await import("../component-catalog.js");
|
|
130
|
-
expect(() => getComponentDocs('some/path/Button')).toThrow(/Invalid component name/);
|
|
131
|
-
expect(() => getComponentDocs('some\\path\\Button')).toThrow(/Invalid component name/);
|
|
132
|
-
});
|
|
133
|
-
it('rejects component names with parent directory references', async () => {
|
|
134
|
-
const { getComponentDocs } = await import("../component-catalog.js");
|
|
135
|
-
expect(() => getComponentDocs('../../etc/passwd')).toThrow(/Invalid component name/);
|
|
136
|
-
expect(() => getComponentDocs('Button/../../../etc/passwd')).toThrow(/Invalid component name/);
|
|
137
|
-
});
|
|
138
|
-
it('rejects absolute paths', async () => {
|
|
139
|
-
const { getComponentDocs } = await import("../component-catalog.js");
|
|
140
|
-
expect(() => getComponentDocs('/etc/passwd')).toThrow(/Invalid component name/);
|
|
141
|
-
expect(() => getComponentDocs('C:\\Windows\\System32\\config\\sam')).toThrow(/Invalid component name/);
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
});
|
|
File without changes
|