@lando-labs/lando-ds-mcp 4.0.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/CHANGELOG.md +526 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +170 -0
- package/build/data/ds-meta.json +1 -0
- package/build/data/ds-meta.schema.json +282 -0
- package/build/data/ds-meta.verbose.json +1 -0
- package/build/data/icon-metadata.d.ts +64 -0
- package/build/data/icon-metadata.d.ts.map +1 -0
- package/build/data/icon-metadata.js +1128 -0
- package/build/data/icon-metadata.js.map +1 -0
- package/build/data/lucide-icon-names.d.ts +12 -0
- package/build/data/lucide-icon-names.d.ts.map +1 -0
- package/build/data/lucide-icon-names.js +1653 -0
- package/build/data/lucide-icon-names.js.map +1 -0
- package/build/data/lucide-icons.d.ts +41 -0
- package/build/data/lucide-icons.d.ts.map +1 -0
- package/build/data/lucide-icons.js +80 -0
- package/build/data/lucide-icons.js.map +1 -0
- package/build/data/rn-tokens-snapshot.d.ts +753 -0
- package/build/data/rn-tokens-snapshot.d.ts.map +1 -0
- package/build/data/rn-tokens-snapshot.js +286 -0
- package/build/data/rn-tokens-snapshot.js.map +1 -0
- package/build/index.d.ts +18 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +411 -0
- package/build/index.js.map +1 -0
- package/build/prompts/index.d.ts +16 -0
- package/build/prompts/index.d.ts.map +1 -0
- package/build/prompts/index.js +145 -0
- package/build/prompts/index.js.map +1 -0
- package/build/resources/code-examples.d.ts +6 -0
- package/build/resources/code-examples.d.ts.map +1 -0
- package/build/resources/code-examples.js +278 -0
- package/build/resources/code-examples.js.map +1 -0
- package/build/resources/design-tokens.d.ts +6 -0
- package/build/resources/design-tokens.d.ts.map +1 -0
- package/build/resources/design-tokens.js +128 -0
- package/build/resources/design-tokens.js.map +1 -0
- package/build/resources/index.d.ts +19 -0
- package/build/resources/index.d.ts.map +1 -0
- package/build/resources/index.js +80 -0
- package/build/resources/index.js.map +1 -0
- package/build/test.d.ts +12 -0
- package/build/test.d.ts.map +1 -0
- package/build/test.js +680 -0
- package/build/test.js.map +1 -0
- package/build/tools/compose-components.d.ts +40 -0
- package/build/tools/compose-components.d.ts.map +1 -0
- package/build/tools/compose-components.js +61 -0
- package/build/tools/compose-components.js.map +1 -0
- package/build/tools/get-animation-examples.d.ts +30 -0
- package/build/tools/get-animation-examples.d.ts.map +1 -0
- package/build/tools/get-animation-examples.js +484 -0
- package/build/tools/get-animation-examples.js.map +1 -0
- package/build/tools/get-component-capabilities.d.ts +43 -0
- package/build/tools/get-component-capabilities.d.ts.map +1 -0
- package/build/tools/get-component-capabilities.js +108 -0
- package/build/tools/get-component-capabilities.js.map +1 -0
- package/build/tools/get-component-props.d.ts +23 -0
- package/build/tools/get-component-props.d.ts.map +1 -0
- package/build/tools/get-component-props.js +46 -0
- package/build/tools/get-component-props.js.map +1 -0
- package/build/tools/get-component-reference.d.ts +25 -0
- package/build/tools/get-component-reference.d.ts.map +1 -0
- package/build/tools/get-component-reference.js +112 -0
- package/build/tools/get-component-reference.js.map +1 -0
- package/build/tools/get-component.d.ts +25 -0
- package/build/tools/get-component.d.ts.map +1 -0
- package/build/tools/get-component.js +60 -0
- package/build/tools/get-component.js.map +1 -0
- package/build/tools/get-composition-hints.d.ts +22 -0
- package/build/tools/get-composition-hints.d.ts.map +1 -0
- package/build/tools/get-composition-hints.js +39 -0
- package/build/tools/get-composition-hints.js.map +1 -0
- package/build/tools/get-design-tokens.d.ts +24 -0
- package/build/tools/get-design-tokens.d.ts.map +1 -0
- package/build/tools/get-design-tokens.js +96 -0
- package/build/tools/get-design-tokens.js.map +1 -0
- package/build/tools/get-ds-metadata.d.ts +16 -0
- package/build/tools/get-ds-metadata.d.ts.map +1 -0
- package/build/tools/get-ds-metadata.js +65 -0
- package/build/tools/get-ds-metadata.js.map +1 -0
- package/build/tools/get-hook.d.ts +19 -0
- package/build/tools/get-hook.d.ts.map +1 -0
- package/build/tools/get-hook.js +46 -0
- package/build/tools/get-hook.js.map +1 -0
- package/build/tools/get-theme-presets.d.ts +13 -0
- package/build/tools/get-theme-presets.d.ts.map +1 -0
- package/build/tools/get-theme-presets.js +41 -0
- package/build/tools/get-theme-presets.js.map +1 -0
- package/build/tools/list-components.d.ts +29 -0
- package/build/tools/list-components.d.ts.map +1 -0
- package/build/tools/list-components.js +88 -0
- package/build/tools/list-components.js.map +1 -0
- package/build/tools/list-hooks.d.ts +22 -0
- package/build/tools/list-hooks.d.ts.map +1 -0
- package/build/tools/list-hooks.js +80 -0
- package/build/tools/list-hooks.js.map +1 -0
- package/build/tools/list-icons.d.ts +28 -0
- package/build/tools/list-icons.d.ts.map +1 -0
- package/build/tools/list-icons.js +161 -0
- package/build/tools/list-icons.js.map +1 -0
- package/build/tools/platform-transform.d.ts +64 -0
- package/build/tools/platform-transform.d.ts.map +1 -0
- package/build/tools/platform-transform.js +117 -0
- package/build/tools/platform-transform.js.map +1 -0
- package/build/tools/platform.d.ts +76 -0
- package/build/tools/platform.d.ts.map +1 -0
- package/build/tools/platform.js +80 -0
- package/build/tools/platform.js.map +1 -0
- package/build/tools/search-icons.d.ts +28 -0
- package/build/tools/search-icons.d.ts.map +1 -0
- package/build/tools/search-icons.js +257 -0
- package/build/tools/search-icons.js.map +1 -0
- package/build/utils/error-envelope.d.ts +14 -0
- package/build/utils/error-envelope.d.ts.map +1 -0
- package/build/utils/error-envelope.js +35 -0
- package/build/utils/error-envelope.js.map +1 -0
- package/build/utils/icon-library.d.ts +17 -0
- package/build/utils/icon-library.d.ts.map +1 -0
- package/build/utils/icon-library.js +24 -0
- package/build/utils/icon-library.js.map +1 -0
- package/build/utils/jsx-safety.d.ts +47 -0
- package/build/utils/jsx-safety.d.ts.map +1 -0
- package/build/utils/jsx-safety.js +63 -0
- package/build/utils/jsx-safety.js.map +1 -0
- package/build/utils/meta-loader.d.ts +37 -0
- package/build/utils/meta-loader.d.ts.map +1 -0
- package/build/utils/meta-loader.js +114 -0
- package/build/utils/meta-loader.js.map +1 -0
- package/package.json +60 -0
package/build/test.js
ADDED
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Script for @lando-labs/lando-ds-mcp v4.0.0
|
|
3
|
+
*
|
|
4
|
+
* Coverage:
|
|
5
|
+
* - Meta loader (schema compat, accessors)
|
|
6
|
+
* - Each of the 15 tools: Zod schema validation + smoke test on output
|
|
7
|
+
* - JSX safety regressions (carried from v2.7.1)
|
|
8
|
+
* - Error envelope classification (carried from v2.7.1)
|
|
9
|
+
* - Icon library (Lucide catalog)
|
|
10
|
+
*/
|
|
11
|
+
import { getLightMeta, getVerboseMeta, getVerboseComponent, getComponentNames, getDsPackageName, getDsVersion, getSchemaVersion, getHookNames, getHook, } from './utils/meta-loader.js';
|
|
12
|
+
import { listComponents, listComponentsSchema } from './tools/list-components.js';
|
|
13
|
+
import { getComponent, getComponentSchema } from './tools/get-component.js';
|
|
14
|
+
import { getComponentProps, getComponentPropsSchema } from './tools/get-component-props.js';
|
|
15
|
+
import { getComponentReference, getComponentReferenceSchema } from './tools/get-component-reference.js';
|
|
16
|
+
import { composeComponents, composeComponentsSchema } from './tools/compose-components.js';
|
|
17
|
+
import { getDesignTokens, getDesignTokensSchema } from './tools/get-design-tokens.js';
|
|
18
|
+
import { getAnimationExamples, getAnimationExamplesSchema } from './tools/get-animation-examples.js';
|
|
19
|
+
import { listIcons, listIconsSchema } from './tools/list-icons.js';
|
|
20
|
+
import { searchIcons, searchIconsSchema } from './tools/search-icons.js';
|
|
21
|
+
import { getComponentCapabilities, getComponentCapabilitiesSchema } from './tools/get-component-capabilities.js';
|
|
22
|
+
import { getCompositionHints, getCompositionHintsSchema } from './tools/get-composition-hints.js';
|
|
23
|
+
import { getThemePresets, getThemePresetsSchema } from './tools/get-theme-presets.js';
|
|
24
|
+
import { getDsMetadata, getDsMetadataSchema } from './tools/get-ds-metadata.js';
|
|
25
|
+
import { listHooks, listHooksSchema } from './tools/list-hooks.js';
|
|
26
|
+
import { getHook_tool, getHookSchema } from './tools/get-hook.js';
|
|
27
|
+
import { handleError, _resetErrorIdSeq } from './utils/error-envelope.js';
|
|
28
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
29
|
+
import { z } from 'zod';
|
|
30
|
+
import { lucideIconNames, isLucideIconName, searchLucideIconNames } from './data/lucide-icons.js';
|
|
31
|
+
const colors = { reset: '\x1b[0m', green: '\x1b[32m', red: '\x1b[31m', blue: '\x1b[34m', cyan: '\x1b[36m' };
|
|
32
|
+
const log = (msg, color = 'reset') => console.log(`${colors[color]}${msg}${colors.reset}`);
|
|
33
|
+
let passed = 0;
|
|
34
|
+
let failed = 0;
|
|
35
|
+
async function test(name, fn) {
|
|
36
|
+
try {
|
|
37
|
+
await fn();
|
|
38
|
+
passed++;
|
|
39
|
+
log(`✓ ${name}`, 'green');
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
failed++;
|
|
43
|
+
log(`✗ ${name}`, 'red');
|
|
44
|
+
console.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Pick a stable component name for tests that need one. Fall back to first
|
|
48
|
+
// available if Button isn't in the loaded meta for some reason.
|
|
49
|
+
const SAMPLE_COMPONENT = getComponentNames().includes('Button') ? 'Button' : getComponentNames()[0];
|
|
50
|
+
async function run() {
|
|
51
|
+
log('\n===========================================', 'cyan');
|
|
52
|
+
log(`v3.0.0 test suite — DS ${getDsPackageName()}@${getDsVersion()} (schema ${getSchemaVersion()})`, 'cyan');
|
|
53
|
+
log('===========================================\n', 'cyan');
|
|
54
|
+
// ==========================================================================
|
|
55
|
+
log('Version sync (from #11)', 'blue');
|
|
56
|
+
// ==========================================================================
|
|
57
|
+
// These tests protect against the class of drift that shipped v2.5.0/v2.6.0
|
|
58
|
+
// together — where package.json, src/index.ts version comment, Server
|
|
59
|
+
// registration version, and startup banner disagreed.
|
|
60
|
+
//
|
|
61
|
+
// Read package.json + src/index.ts source at test time (fresh file reads,
|
|
62
|
+
// not the compiled build), extract versions from both, assert equal.
|
|
63
|
+
await test('version-sync — src/index.ts JSDoc comment matches package.json version', async () => {
|
|
64
|
+
const fs = await import('fs');
|
|
65
|
+
const url = await import('url');
|
|
66
|
+
const path = await import('path');
|
|
67
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
68
|
+
const pkg = JSON.parse(fs.readFileSync(path.resolve(here, '../package.json'), 'utf-8'));
|
|
69
|
+
const src = fs.readFileSync(path.resolve(here, '../src/index.ts'), 'utf-8');
|
|
70
|
+
const m = src.match(/^ \* Version: (\d+\.\d+\.\d+)$/m);
|
|
71
|
+
if (!m)
|
|
72
|
+
throw new Error('could not find "Version: X.Y.Z" comment in src/index.ts');
|
|
73
|
+
if (m[1] !== pkg.version)
|
|
74
|
+
throw new Error(`src/index.ts JSDoc says v${m[1]}, package.json says v${pkg.version}`);
|
|
75
|
+
});
|
|
76
|
+
await test('version-sync — Server({ version }) matches package.json version', async () => {
|
|
77
|
+
const fs = await import('fs');
|
|
78
|
+
const url = await import('url');
|
|
79
|
+
const path = await import('path');
|
|
80
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
81
|
+
const pkg = JSON.parse(fs.readFileSync(path.resolve(here, '../package.json'), 'utf-8'));
|
|
82
|
+
const src = fs.readFileSync(path.resolve(here, '../src/index.ts'), 'utf-8');
|
|
83
|
+
const m = src.match(/version: ['"](\d+\.\d+\.\d+)['"]/);
|
|
84
|
+
if (!m)
|
|
85
|
+
throw new Error('could not find `version: "X.Y.Z"` in src/index.ts (Server registration)');
|
|
86
|
+
if (m[1] !== pkg.version)
|
|
87
|
+
throw new Error(`Server({ version }) says v${m[1]}, package.json says v${pkg.version}`);
|
|
88
|
+
});
|
|
89
|
+
await test('version-sync — startup banner matches package.json version', async () => {
|
|
90
|
+
const fs = await import('fs');
|
|
91
|
+
const url = await import('url');
|
|
92
|
+
const path = await import('path');
|
|
93
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
94
|
+
const pkg = JSON.parse(fs.readFileSync(path.resolve(here, '../package.json'), 'utf-8'));
|
|
95
|
+
const src = fs.readFileSync(path.resolve(here, '../src/index.ts'), 'utf-8');
|
|
96
|
+
const m = src.match(/`Version: (\d+\.\d+\.\d+) \(backed by @lando-labs\/lando-ds-meta/);
|
|
97
|
+
if (!m)
|
|
98
|
+
throw new Error('could not find "Version: X.Y.Z (backed by @lando-labs/lando-ds-meta" startup banner in src/index.ts');
|
|
99
|
+
if (m[1] !== pkg.version)
|
|
100
|
+
throw new Error(`Startup banner says v${m[1]}, package.json says v${pkg.version}`);
|
|
101
|
+
});
|
|
102
|
+
await test('version-sync — CHANGELOG top entry matches package.json version', async () => {
|
|
103
|
+
const fs = await import('fs');
|
|
104
|
+
const url = await import('url');
|
|
105
|
+
const path = await import('path');
|
|
106
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
107
|
+
const pkg = JSON.parse(fs.readFileSync(path.resolve(here, '../package.json'), 'utf-8'));
|
|
108
|
+
const changelog = fs.readFileSync(path.resolve(here, '../CHANGELOG.md'), 'utf-8');
|
|
109
|
+
// Find the first `## [X.Y.Z] - YYYY-MM-DD` heading — that's the current release
|
|
110
|
+
const m = changelog.match(/^## \[(\d+\.\d+\.\d+)\] - \d{4}-\d{2}-\d{2}$/m);
|
|
111
|
+
if (!m)
|
|
112
|
+
throw new Error('could not find "## [X.Y.Z] - YYYY-MM-DD" entry in CHANGELOG.md');
|
|
113
|
+
if (m[1] !== pkg.version)
|
|
114
|
+
throw new Error(`CHANGELOG top entry says v${m[1]}, package.json says v${pkg.version}`);
|
|
115
|
+
});
|
|
116
|
+
await test('version-sync — README component count matches loaded meta', async () => {
|
|
117
|
+
const fs = await import('fs');
|
|
118
|
+
const url = await import('url');
|
|
119
|
+
const path = await import('path');
|
|
120
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
121
|
+
const readme = fs.readFileSync(path.resolve(here, '../README.md'), 'utf-8');
|
|
122
|
+
const expected = Object.keys(getLightMeta().components).length;
|
|
123
|
+
// README says "127 React components" in the positioning statement.
|
|
124
|
+
// Assert the number in that phrase matches the loaded meta.
|
|
125
|
+
const m = readme.match(/(\d+) React components/);
|
|
126
|
+
if (!m)
|
|
127
|
+
throw new Error('README does not mention "N React components" — check positioning statement');
|
|
128
|
+
if (Number(m[1]) !== expected) {
|
|
129
|
+
throw new Error(`README says "${m[1]} React components", meta has ${expected}`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
// ==========================================================================
|
|
133
|
+
log('Meta loader', 'blue');
|
|
134
|
+
// ==========================================================================
|
|
135
|
+
await test('meta.$schemaVersion is in the supported 1.x range', async () => {
|
|
136
|
+
const v = getLightMeta().$schemaVersion;
|
|
137
|
+
if (!/^1\.\d+$/.test(v))
|
|
138
|
+
throw new Error(`unexpected schema version: ${v}`);
|
|
139
|
+
});
|
|
140
|
+
await test('meta.package.name is @lando-labs/lando-ds', async () => {
|
|
141
|
+
if (getDsPackageName() !== '@lando-labs/lando-ds')
|
|
142
|
+
throw new Error('package name mismatch');
|
|
143
|
+
});
|
|
144
|
+
await test('meta has 100+ components', async () => {
|
|
145
|
+
if (getComponentNames().length < 100)
|
|
146
|
+
throw new Error(`only ${getComponentNames().length} components`);
|
|
147
|
+
});
|
|
148
|
+
await test('verbose meta lazy-loads on first access', async () => {
|
|
149
|
+
const v = getVerboseMeta();
|
|
150
|
+
if (!v.components[SAMPLE_COMPONENT]?.description)
|
|
151
|
+
throw new Error('verbose meta missing description');
|
|
152
|
+
});
|
|
153
|
+
await test('getVerboseComponent returns example code for Button (if present)', async () => {
|
|
154
|
+
const v = getVerboseComponent(SAMPLE_COMPONENT);
|
|
155
|
+
if (!v)
|
|
156
|
+
throw new Error('component not in verbose meta');
|
|
157
|
+
// examples may be [] for some components — that's allowed.
|
|
158
|
+
if (!Array.isArray(v.examples))
|
|
159
|
+
throw new Error('examples not an array');
|
|
160
|
+
});
|
|
161
|
+
await test('meta.capabilities lists are populated', async () => {
|
|
162
|
+
const c = getLightMeta().capabilities;
|
|
163
|
+
if (c.rscSafe.length + c.clientOnly.length === 0)
|
|
164
|
+
throw new Error('capabilities empty');
|
|
165
|
+
});
|
|
166
|
+
// ==========================================================================
|
|
167
|
+
log('\nlist_components', 'blue');
|
|
168
|
+
// ==========================================================================
|
|
169
|
+
await test('list_components — no filters returns all components', async () => {
|
|
170
|
+
const out = await listComponents({});
|
|
171
|
+
if (!out.includes(`Total: ${getComponentNames().length}`))
|
|
172
|
+
throw new Error('missing total count');
|
|
173
|
+
});
|
|
174
|
+
await test('list_components — deprecated=false excludes deprecated', async () => {
|
|
175
|
+
const out = await listComponents({ deprecated: false });
|
|
176
|
+
if (!out.includes('Total:'))
|
|
177
|
+
throw new Error('missing total');
|
|
178
|
+
});
|
|
179
|
+
await test('list_components schema — rejects unknown kind', async () => {
|
|
180
|
+
const r = listComponentsSchema.safeParse({ kind: 'hook' });
|
|
181
|
+
if (r.success)
|
|
182
|
+
throw new Error('should reject');
|
|
183
|
+
});
|
|
184
|
+
// ==========================================================================
|
|
185
|
+
log('\nget_component', 'blue');
|
|
186
|
+
// ==========================================================================
|
|
187
|
+
await test('get_component — PascalCase name works', async () => {
|
|
188
|
+
const out = await getComponent({ componentName: SAMPLE_COMPONENT });
|
|
189
|
+
if (!out.includes(`<${SAMPLE_COMPONENT}`))
|
|
190
|
+
throw new Error('missing JSX element');
|
|
191
|
+
if (!out.includes(`from '@lando-labs/lando-ds'`))
|
|
192
|
+
throw new Error('missing import');
|
|
193
|
+
});
|
|
194
|
+
await test('get_component — with props renders them', async () => {
|
|
195
|
+
const out = await getComponent({ componentName: SAMPLE_COMPONENT, props: { variant: 'primary' } });
|
|
196
|
+
if (!out.includes('variant="primary"'))
|
|
197
|
+
throw new Error('prop not rendered');
|
|
198
|
+
});
|
|
199
|
+
await test('get_component — rejects lowercase name (schema)', async () => {
|
|
200
|
+
const r = getComponentSchema.safeParse({ componentName: 'button' });
|
|
201
|
+
if (r.success)
|
|
202
|
+
throw new Error('should reject lowercase');
|
|
203
|
+
});
|
|
204
|
+
await test('get_component — rejects unknown component (schema)', async () => {
|
|
205
|
+
const r = getComponentSchema.safeParse({ componentName: 'NotAComponent' });
|
|
206
|
+
if (r.success)
|
|
207
|
+
throw new Error('should reject unknown');
|
|
208
|
+
});
|
|
209
|
+
// ==========================================================================
|
|
210
|
+
log('\nget_component_props', 'blue');
|
|
211
|
+
// ==========================================================================
|
|
212
|
+
await test('get_component_props — returns TS-style interface', async () => {
|
|
213
|
+
const out = await getComponentProps({ componentName: SAMPLE_COMPONENT });
|
|
214
|
+
if (!out.includes(`interface ${SAMPLE_COMPONENT}Props`))
|
|
215
|
+
throw new Error('missing interface');
|
|
216
|
+
});
|
|
217
|
+
await test('get_component_props schema — rejects missing componentName', async () => {
|
|
218
|
+
const r = getComponentPropsSchema.safeParse({});
|
|
219
|
+
if (r.success)
|
|
220
|
+
throw new Error('should require componentName');
|
|
221
|
+
});
|
|
222
|
+
// ==========================================================================
|
|
223
|
+
log('\nget_component_reference', 'blue');
|
|
224
|
+
// ==========================================================================
|
|
225
|
+
await test('get_component_reference — default include set', async () => {
|
|
226
|
+
const out = await getComponentReference({ componentName: SAMPLE_COMPONENT });
|
|
227
|
+
if (!out.includes('## Description') && !out.includes('## Props'))
|
|
228
|
+
throw new Error('missing default sections');
|
|
229
|
+
});
|
|
230
|
+
await test('get_component_reference — include: ["examples"] only', async () => {
|
|
231
|
+
const out = await getComponentReference({ componentName: SAMPLE_COMPONENT, include: ['examples'] });
|
|
232
|
+
if (out.includes('## Description') || out.includes('## Props'))
|
|
233
|
+
throw new Error('include filter not honored');
|
|
234
|
+
if (!out.includes('## Examples'))
|
|
235
|
+
throw new Error('missing examples section');
|
|
236
|
+
});
|
|
237
|
+
await test('get_component_reference schema — rejects invalid include value', async () => {
|
|
238
|
+
const r = getComponentReferenceSchema.safeParse({ componentName: SAMPLE_COMPONENT, include: ['bogus'] });
|
|
239
|
+
if (r.success)
|
|
240
|
+
throw new Error('should reject bogus include');
|
|
241
|
+
});
|
|
242
|
+
// ==========================================================================
|
|
243
|
+
log('\nHTML-attrs footer — authoritative extends (DS meta 1.2, v3.1.1)', 'blue');
|
|
244
|
+
// ==========================================================================
|
|
245
|
+
// Semantics changed in v3.1.1: any component with a non-empty `extends`
|
|
246
|
+
// chain in meta gets the footer, regardless of own-props count. Old
|
|
247
|
+
// heuristic (v3.0.1) only footered sparse-props + polymorphic/withRef.
|
|
248
|
+
await test('get_component_props — authoritative footer for ThemeBuilder shows HTMLAttributes<HTMLDivElement>', async () => {
|
|
249
|
+
const out = await getComponentProps({ componentName: 'ThemeBuilder' });
|
|
250
|
+
if (!out.includes('Also accepts everything in')) {
|
|
251
|
+
throw new Error('expected authoritative extends footer');
|
|
252
|
+
}
|
|
253
|
+
if (!out.includes('HTMLAttributes<HTMLDivElement>')) {
|
|
254
|
+
throw new Error('footer should render the exact extends chain from meta');
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
await test('get_component_props — authoritative footer for Button shows HTMLAttributes<HTMLButtonElement>', async () => {
|
|
258
|
+
// Button has 6+ own props but still extends HTMLAttributes — old
|
|
259
|
+
// heuristic hid this; authoritative version surfaces it.
|
|
260
|
+
const out = await getComponentProps({ componentName: 'Button' });
|
|
261
|
+
if (!out.includes('Also accepts everything in')) {
|
|
262
|
+
throw new Error('expected authoritative extends footer on Button');
|
|
263
|
+
}
|
|
264
|
+
if (!out.includes('HTMLButtonElement')) {
|
|
265
|
+
throw new Error('footer should mention the correct HTML element for Button');
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
await test('get_component_reference — authoritative footer in Props section for ThemeBuilder', async () => {
|
|
269
|
+
const out = await getComponentReference({ componentName: 'ThemeBuilder', include: ['props'] });
|
|
270
|
+
if (!out.includes('Also accepts everything in')) {
|
|
271
|
+
throw new Error('expected authoritative footer in get_component_reference Props section');
|
|
272
|
+
}
|
|
273
|
+
if (!out.includes('HTMLDivElement')) {
|
|
274
|
+
throw new Error('footer missing extends chain content');
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
await test('get_component_reference — footer ABSENT when Props section not included', async () => {
|
|
278
|
+
const out = await getComponentReference({ componentName: 'ThemeBuilder', include: ['description'] });
|
|
279
|
+
if (out.includes('Also accepts everything in')) {
|
|
280
|
+
throw new Error('footer should only render when Props section is included');
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
// ==========================================================================
|
|
284
|
+
log('\ncompose_components', 'blue');
|
|
285
|
+
// ==========================================================================
|
|
286
|
+
await test('compose_components — merges imports from meta', async () => {
|
|
287
|
+
const out = await composeComponents({
|
|
288
|
+
components: [
|
|
289
|
+
{ componentName: SAMPLE_COMPONENT },
|
|
290
|
+
{ componentName: SAMPLE_COMPONENT },
|
|
291
|
+
],
|
|
292
|
+
});
|
|
293
|
+
if (!out.includes(`from '@lando-labs/lando-ds'`))
|
|
294
|
+
throw new Error('missing merged import');
|
|
295
|
+
// Same name twice deduplicates in the import
|
|
296
|
+
if (out.match(/import \{ [^}]+ \}/)[0].split(',').length > 1)
|
|
297
|
+
throw new Error('duplicate names not deduped');
|
|
298
|
+
});
|
|
299
|
+
await test('compose_components schema — rejects > 25 components', async () => {
|
|
300
|
+
const many = Array.from({ length: 26 }, () => ({ componentName: SAMPLE_COMPONENT }));
|
|
301
|
+
const r = composeComponentsSchema.safeParse({ components: many });
|
|
302
|
+
if (r.success)
|
|
303
|
+
throw new Error('should reject >25');
|
|
304
|
+
});
|
|
305
|
+
await test('compose_components schema — rejects unknown component', async () => {
|
|
306
|
+
const r = composeComponentsSchema.safeParse({ components: [{ componentName: 'NotReal' }] });
|
|
307
|
+
if (r.success)
|
|
308
|
+
throw new Error('should reject unknown');
|
|
309
|
+
});
|
|
310
|
+
// ==========================================================================
|
|
311
|
+
log('\nget_design_tokens', 'blue');
|
|
312
|
+
// ==========================================================================
|
|
313
|
+
await test('get_design_tokens — default format is css', async () => {
|
|
314
|
+
const out = await getDesignTokens({});
|
|
315
|
+
if (!out.includes('Format: `css`'))
|
|
316
|
+
throw new Error('default not css');
|
|
317
|
+
});
|
|
318
|
+
await test('get_design_tokens — format: "hex" returns hex values', async () => {
|
|
319
|
+
const out = await getDesignTokens({ format: 'hex' });
|
|
320
|
+
if (!out.includes('Format: `hex`'))
|
|
321
|
+
throw new Error('hex format not applied');
|
|
322
|
+
if (!/#[0-9A-Fa-f]{6}/.test(out))
|
|
323
|
+
throw new Error('no hex values in output');
|
|
324
|
+
});
|
|
325
|
+
await test('get_design_tokens — format: "both" returns var refs + hex', async () => {
|
|
326
|
+
const out = await getDesignTokens({ format: 'both' });
|
|
327
|
+
if (!out.includes('var(--'))
|
|
328
|
+
throw new Error('missing var refs');
|
|
329
|
+
if (!/#[0-9A-Fa-f]{6}/.test(out))
|
|
330
|
+
throw new Error('missing hex');
|
|
331
|
+
});
|
|
332
|
+
await test('get_design_tokens schema — rejects bogus format', async () => {
|
|
333
|
+
const r = getDesignTokensSchema.safeParse({ format: 'srgb' });
|
|
334
|
+
if (r.success)
|
|
335
|
+
throw new Error('should reject bogus format');
|
|
336
|
+
});
|
|
337
|
+
// ==========================================================================
|
|
338
|
+
log('\nget_animation_examples', 'blue');
|
|
339
|
+
// ==========================================================================
|
|
340
|
+
await test('get_animation_examples — no ocean framing in v3.0.0', async () => {
|
|
341
|
+
const out = await getAnimationExamples({});
|
|
342
|
+
if (out.includes('Ocean-Themed Animation Examples'))
|
|
343
|
+
throw new Error('ocean-themed title survived');
|
|
344
|
+
if (out.includes('like ocean waves'))
|
|
345
|
+
throw new Error('ocean-waves guidance survived');
|
|
346
|
+
});
|
|
347
|
+
await test('get_animation_examples — specific animationType', async () => {
|
|
348
|
+
const out = await getAnimationExamples({ animationType: 'fade-in' });
|
|
349
|
+
if (!out.toLowerCase().includes('fade'))
|
|
350
|
+
throw new Error('animation not returned');
|
|
351
|
+
});
|
|
352
|
+
await test('get_animation_examples schema — rejects invalid animationType', async () => {
|
|
353
|
+
const r = getAnimationExamplesSchema.safeParse({ animationType: 'flip' });
|
|
354
|
+
if (r.success)
|
|
355
|
+
throw new Error('should reject flip');
|
|
356
|
+
});
|
|
357
|
+
// ==========================================================================
|
|
358
|
+
log('\nlist_icons', 'blue');
|
|
359
|
+
// ==========================================================================
|
|
360
|
+
await test('list_icons — default (all) returns DS-brand + Lucide-curated', async () => {
|
|
361
|
+
const out = await listIcons({});
|
|
362
|
+
if (!out.includes('DS Brand Icons') && !out.includes('Lucide Curated')) {
|
|
363
|
+
throw new Error('missing at least one source section');
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
await test('list_icons — source=ds-brand filters to registry', async () => {
|
|
367
|
+
const out = await listIcons({ source: 'ds-brand' });
|
|
368
|
+
if (!out.includes('DS Brand Icons'))
|
|
369
|
+
throw new Error('missing brand section');
|
|
370
|
+
if (out.includes('Lucide Curated'))
|
|
371
|
+
throw new Error('lucide-curated leaked in');
|
|
372
|
+
});
|
|
373
|
+
await test('list_icons — source=lucide-curated filters to curated', async () => {
|
|
374
|
+
const out = await listIcons({ source: 'lucide-curated' });
|
|
375
|
+
if (out.includes('DS Brand Icons'))
|
|
376
|
+
throw new Error('brand leaked in');
|
|
377
|
+
});
|
|
378
|
+
await test('list_icons schema — rejects old "custom" source value', async () => {
|
|
379
|
+
const r = listIconsSchema.safeParse({ source: 'custom' });
|
|
380
|
+
if (r.success)
|
|
381
|
+
throw new Error('should reject old value');
|
|
382
|
+
});
|
|
383
|
+
// ==========================================================================
|
|
384
|
+
log('\nsearch_icons', 'blue');
|
|
385
|
+
// ==========================================================================
|
|
386
|
+
await test('search_icons — ocean purpose mapping removed', async () => {
|
|
387
|
+
const out = await searchIcons({ query: 'ocean' });
|
|
388
|
+
// The ocean-specific curated hits are gone; results may still turn up
|
|
389
|
+
// via general fuzzy match on the word, but the top result should NOT
|
|
390
|
+
// be seeded from a purpose mapping keyed on "ocean theme".
|
|
391
|
+
if (out.includes('Ocean-Themed') || out.includes('ocean waves')) {
|
|
392
|
+
throw new Error('ocean framing survived');
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
await test('search_icons — finds User icons for "user profile"', async () => {
|
|
396
|
+
const out = await searchIcons({ query: 'user profile' });
|
|
397
|
+
if (!/User/.test(out))
|
|
398
|
+
throw new Error('missing User match');
|
|
399
|
+
});
|
|
400
|
+
await test('search_icons schema — requires query', async () => {
|
|
401
|
+
const r = searchIconsSchema.safeParse({ limit: 5 });
|
|
402
|
+
if (r.success)
|
|
403
|
+
throw new Error('should require query');
|
|
404
|
+
});
|
|
405
|
+
// ==========================================================================
|
|
406
|
+
log('\nget_component_capabilities', 'blue');
|
|
407
|
+
// ==========================================================================
|
|
408
|
+
await test('get_component_capabilities — no args returns matrix summary', async () => {
|
|
409
|
+
const out = await getComponentCapabilities({});
|
|
410
|
+
if (!out.includes('## Summary'))
|
|
411
|
+
throw new Error('missing summary section');
|
|
412
|
+
});
|
|
413
|
+
await test('get_component_capabilities — single component by name', async () => {
|
|
414
|
+
const out = await getComponentCapabilities({ component: SAMPLE_COMPONENT });
|
|
415
|
+
if (!out.includes(`Capability: ${SAMPLE_COMPONENT}`))
|
|
416
|
+
throw new Error('single-component branch not taken');
|
|
417
|
+
});
|
|
418
|
+
await test('get_component_capabilities — polymorphic=true filter', async () => {
|
|
419
|
+
const out = await getComponentCapabilities({ polymorphic: true });
|
|
420
|
+
if (!out.includes('polymorphic=true'))
|
|
421
|
+
throw new Error('filter not surfaced in output');
|
|
422
|
+
});
|
|
423
|
+
await test('get_component_capabilities schema — accepts all new axes', async () => {
|
|
424
|
+
const r = getComponentCapabilitiesSchema.safeParse({
|
|
425
|
+
rscSafe: true, clientOnly: false, polymorphic: true, withRef: true, deprecated: false,
|
|
426
|
+
});
|
|
427
|
+
if (!r.success)
|
|
428
|
+
throw new Error('should accept expanded axes');
|
|
429
|
+
});
|
|
430
|
+
// ==========================================================================
|
|
431
|
+
log('\nget_composition_hints', 'blue');
|
|
432
|
+
// ==========================================================================
|
|
433
|
+
await test('get_composition_hints — returns hints or friendly none message', async () => {
|
|
434
|
+
const out = await getCompositionHints({ componentName: SAMPLE_COMPONENT });
|
|
435
|
+
if (!out.includes('# Composition Hints:'))
|
|
436
|
+
throw new Error('missing header');
|
|
437
|
+
});
|
|
438
|
+
await test('get_composition_hints schema — requires componentName', async () => {
|
|
439
|
+
const r = getCompositionHintsSchema.safeParse({});
|
|
440
|
+
if (r.success)
|
|
441
|
+
throw new Error('should require componentName');
|
|
442
|
+
});
|
|
443
|
+
// ==========================================================================
|
|
444
|
+
log('\nget_theme_presets', 'blue');
|
|
445
|
+
// ==========================================================================
|
|
446
|
+
await test('get_theme_presets — reads from meta.themePresets (schema 1.1+)', async () => {
|
|
447
|
+
const out = await getThemePresets({});
|
|
448
|
+
// brand-neutral is the DS's declared default as of v0.43.0
|
|
449
|
+
if (!out.includes('`brand-neutral`'))
|
|
450
|
+
throw new Error('missing brand-neutral preset');
|
|
451
|
+
if (!out.includes('_(default)_'))
|
|
452
|
+
throw new Error('default preset badge missing');
|
|
453
|
+
if (!out.includes('**Default preset**: `brand-neutral`'))
|
|
454
|
+
throw new Error('default preset header missing');
|
|
455
|
+
});
|
|
456
|
+
await test('get_theme_presets — surfaces additional presets beyond hard-coded pair', async () => {
|
|
457
|
+
// v3.0.0 hard-coded only lando + brand-neutral. Meta-backed rendering
|
|
458
|
+
// must surface every preset the DS ships (forest, midnight, rose, slate, ...).
|
|
459
|
+
const out = await getThemePresets({});
|
|
460
|
+
if (!out.includes('`forest`'))
|
|
461
|
+
throw new Error('missing forest preset — meta-backed rendering not active');
|
|
462
|
+
});
|
|
463
|
+
await test('get_theme_presets schema — accepts empty args', async () => {
|
|
464
|
+
const r = getThemePresetsSchema.safeParse({});
|
|
465
|
+
if (!r.success)
|
|
466
|
+
throw new Error('should accept {}');
|
|
467
|
+
});
|
|
468
|
+
// ==========================================================================
|
|
469
|
+
log('\nget_ds_metadata', 'blue');
|
|
470
|
+
// ==========================================================================
|
|
471
|
+
await test('get_ds_metadata — surfaces package version and schema', async () => {
|
|
472
|
+
const out = await getDsMetadata({});
|
|
473
|
+
if (!out.includes(`v${getDsVersion()}`))
|
|
474
|
+
throw new Error('missing version');
|
|
475
|
+
if (!out.includes(`v${getSchemaVersion()}`))
|
|
476
|
+
throw new Error('missing schema version');
|
|
477
|
+
});
|
|
478
|
+
await test('get_ds_metadata — component/capability totals present', async () => {
|
|
479
|
+
const out = await getDsMetadata({});
|
|
480
|
+
if (!out.includes('Total components'))
|
|
481
|
+
throw new Error('missing totals');
|
|
482
|
+
if (!out.toLowerCase().includes('rsc-safe'))
|
|
483
|
+
throw new Error('missing capability breakdown');
|
|
484
|
+
});
|
|
485
|
+
await test('get_ds_metadata schema — accepts empty args', async () => {
|
|
486
|
+
const r = getDsMetadataSchema.safeParse({});
|
|
487
|
+
if (!r.success)
|
|
488
|
+
throw new Error('should accept {}');
|
|
489
|
+
});
|
|
490
|
+
await test('get_ds_metadata — surfaces hook totals when meta has hooks', async () => {
|
|
491
|
+
// Only assert when the loaded meta actually ships hooks (schema 1.3+).
|
|
492
|
+
if (getHookNames().length === 0)
|
|
493
|
+
return;
|
|
494
|
+
const out = await getDsMetadata({});
|
|
495
|
+
if (!out.includes('Hook totals'))
|
|
496
|
+
throw new Error('missing hook totals section');
|
|
497
|
+
if (!out.includes('list_hooks'))
|
|
498
|
+
throw new Error('hook tools pointer missing');
|
|
499
|
+
});
|
|
500
|
+
// ==========================================================================
|
|
501
|
+
log('\nHooks (meta schema 1.3, DS#504)', 'blue');
|
|
502
|
+
// ==========================================================================
|
|
503
|
+
await test('meta-loader — getHookNames returns 1+ hook names', async () => {
|
|
504
|
+
// Anchor on the fact that DS v0.56.0+ ships hooks. If this fails after a
|
|
505
|
+
// content refresh, it means the DS regressed on hook emission.
|
|
506
|
+
const names = getHookNames();
|
|
507
|
+
if (names.length === 0) {
|
|
508
|
+
throw new Error(`no hooks in meta — DS meta schema is v${getSchemaVersion()}, expected hooks starting at 1.3`);
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
await test('meta-loader — hook names are sorted', async () => {
|
|
512
|
+
const names = getHookNames();
|
|
513
|
+
const sorted = [...names].sort();
|
|
514
|
+
if (JSON.stringify(names) !== JSON.stringify(sorted))
|
|
515
|
+
throw new Error('not sorted');
|
|
516
|
+
});
|
|
517
|
+
await test('meta-loader — getHook returns undefined for unknown hook', async () => {
|
|
518
|
+
if (getHook('useNotAHookName') !== undefined)
|
|
519
|
+
throw new Error('should be undefined');
|
|
520
|
+
});
|
|
521
|
+
await test('meta-loader — every emitted hook has required schema 1.3 fields', async () => {
|
|
522
|
+
for (const name of getHookNames()) {
|
|
523
|
+
const h = getHook(name);
|
|
524
|
+
const missing = ['kind', 'category', 'serverSafe', 'useClient', 'subpath', 'description', 'signature', 'returns']
|
|
525
|
+
.filter((k) => !(k in h));
|
|
526
|
+
if (missing.length > 0)
|
|
527
|
+
throw new Error(`${name} missing: ${missing.join(', ')}`);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
await test('list_hooks — no filters returns all hooks', async () => {
|
|
531
|
+
const out = await listHooks({});
|
|
532
|
+
const total = getHookNames().length;
|
|
533
|
+
if (!out.includes(`Total: ${total} of ${total} hooks`))
|
|
534
|
+
throw new Error('total mismatch');
|
|
535
|
+
});
|
|
536
|
+
await test('list_hooks — category filter narrows result', async () => {
|
|
537
|
+
// Pick a real category from the first hook so the test doesn't hardcode.
|
|
538
|
+
const anyHook = getHook(getHookNames()[0]);
|
|
539
|
+
const out = await listHooks({ category: anyHook.category });
|
|
540
|
+
if (!out.includes(`category="${anyHook.category}"`))
|
|
541
|
+
throw new Error('filter not surfaced');
|
|
542
|
+
// Bogus category should return the "no matches" state, not throw.
|
|
543
|
+
const empty = await listHooks({ category: 'this-category-does-not-exist' });
|
|
544
|
+
if (!empty.includes('_No hooks match these filters._'))
|
|
545
|
+
throw new Error('empty-result message missing');
|
|
546
|
+
});
|
|
547
|
+
await test('list_hooks — serverSafe=true filter surfaced in output', async () => {
|
|
548
|
+
const out = await listHooks({ serverSafe: true });
|
|
549
|
+
if (!out.includes('serverSafe=true'))
|
|
550
|
+
throw new Error('filter not surfaced');
|
|
551
|
+
});
|
|
552
|
+
await test('list_hooks schema — accepts empty args', async () => {
|
|
553
|
+
const r = listHooksSchema.safeParse({});
|
|
554
|
+
if (!r.success)
|
|
555
|
+
throw new Error('should accept {}');
|
|
556
|
+
});
|
|
557
|
+
await test('get_hook — smoke on the first available hook', async () => {
|
|
558
|
+
const first = getHookNames()[0];
|
|
559
|
+
const out = await getHook_tool({ hookName: first });
|
|
560
|
+
if (!out.startsWith(`# ${first}`))
|
|
561
|
+
throw new Error('missing header');
|
|
562
|
+
if (!out.includes('**Signature**:'))
|
|
563
|
+
throw new Error('missing signature line');
|
|
564
|
+
if (!out.includes('**Returns**:'))
|
|
565
|
+
throw new Error('missing returns line');
|
|
566
|
+
if (!out.includes('## Import'))
|
|
567
|
+
throw new Error('missing import section');
|
|
568
|
+
if (!out.includes(`import { ${first} }`))
|
|
569
|
+
throw new Error('missing import statement');
|
|
570
|
+
});
|
|
571
|
+
await test('get_hook — RSC safety tag reflects hook.serverSafe', async () => {
|
|
572
|
+
const first = getHookNames()[0];
|
|
573
|
+
const h = getHook(first);
|
|
574
|
+
const out = await getHook_tool({ hookName: first });
|
|
575
|
+
const expected = h.serverSafe ? 'rsc-safe' : 'client-only';
|
|
576
|
+
if (!out.includes(expected))
|
|
577
|
+
throw new Error(`expected "${expected}" tag`);
|
|
578
|
+
});
|
|
579
|
+
await test('get_hook schema — rejects unknown hookName when enum is populated', async () => {
|
|
580
|
+
// The Zod enum is derived from meta at module load; if hooks are present,
|
|
581
|
+
// an obviously wrong name must fail validation.
|
|
582
|
+
if (getHookNames().length === 0)
|
|
583
|
+
return;
|
|
584
|
+
const r = getHookSchema.safeParse({ hookName: 'useDefinitelyNotARealHook' });
|
|
585
|
+
if (r.success)
|
|
586
|
+
throw new Error('should reject unknown hook name');
|
|
587
|
+
});
|
|
588
|
+
await test('get_hook schema — requires hookName', async () => {
|
|
589
|
+
const r = getHookSchema.safeParse({});
|
|
590
|
+
if (r.success)
|
|
591
|
+
throw new Error('should require hookName');
|
|
592
|
+
});
|
|
593
|
+
// ==========================================================================
|
|
594
|
+
log('\nJSX safety regressions (from v2.7.1 #19)', 'blue');
|
|
595
|
+
// ==========================================================================
|
|
596
|
+
await test('get_component rejects JSX-breaking char in string prop', async () => {
|
|
597
|
+
try {
|
|
598
|
+
await getComponent({ componentName: SAMPLE_COMPONENT, props: { title: 'x"; alert(1)' } });
|
|
599
|
+
throw new Error('should have thrown');
|
|
600
|
+
}
|
|
601
|
+
catch (err) {
|
|
602
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
603
|
+
if (!msg.includes('JSX-unsafe character'))
|
|
604
|
+
throw new Error(`wrong error: ${msg}`);
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
await test('get_component rejects invalid prop key', async () => {
|
|
608
|
+
try {
|
|
609
|
+
await getComponent({ componentName: SAMPLE_COMPONENT, props: { 'onClick={alert(1)}': 'x' } });
|
|
610
|
+
throw new Error('should have thrown');
|
|
611
|
+
}
|
|
612
|
+
catch (err) {
|
|
613
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
614
|
+
if (!msg.includes('not a valid JSX attribute identifier'))
|
|
615
|
+
throw new Error(`wrong error: ${msg}`);
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
await test('get_component schema — .max(8_000) on children', async () => {
|
|
619
|
+
const big = 'x'.repeat(10_001);
|
|
620
|
+
const r = getComponentSchema.safeParse({ componentName: SAMPLE_COMPONENT, children: big });
|
|
621
|
+
if (r.success)
|
|
622
|
+
throw new Error('should reject >8000-char children');
|
|
623
|
+
});
|
|
624
|
+
// ==========================================================================
|
|
625
|
+
log('\nError envelope (from v2.7.1 #15)', 'blue');
|
|
626
|
+
// ==========================================================================
|
|
627
|
+
await test('handleError — passes McpError through unchanged', async () => {
|
|
628
|
+
const original = new McpError(ErrorCode.InvalidParams, 'nope');
|
|
629
|
+
const out = handleError(original, () => { });
|
|
630
|
+
if (out !== original)
|
|
631
|
+
throw new Error('should be identity-equal');
|
|
632
|
+
});
|
|
633
|
+
await test('handleError — classifies ZodError as InvalidParams', async () => {
|
|
634
|
+
const zerr = z.string().safeParse(123);
|
|
635
|
+
if (zerr.success)
|
|
636
|
+
throw new Error('test setup failed');
|
|
637
|
+
const out = handleError(zerr.error, () => { });
|
|
638
|
+
if (out.code !== ErrorCode.InvalidParams)
|
|
639
|
+
throw new Error(`code mismatch: ${out.code}`);
|
|
640
|
+
if (!out.message.includes('Invalid arguments:'))
|
|
641
|
+
throw new Error('payload missing');
|
|
642
|
+
});
|
|
643
|
+
await test('handleError — redacts paths in unknown errors', async () => {
|
|
644
|
+
_resetErrorIdSeq();
|
|
645
|
+
const leaky = new Error('ENOENT: /Users/lando/secret/path.json');
|
|
646
|
+
const out = handleError(leaky, () => { });
|
|
647
|
+
if (out.message.includes('/Users/'))
|
|
648
|
+
throw new Error('path leaked');
|
|
649
|
+
if (!/Internal error \(id=[a-z0-9]+\)$/.test(out.message))
|
|
650
|
+
throw new Error('opaque envelope missing');
|
|
651
|
+
});
|
|
652
|
+
// ==========================================================================
|
|
653
|
+
log('\nLucide catalog (helpers)', 'blue');
|
|
654
|
+
// ==========================================================================
|
|
655
|
+
await test('lucide catalog — thousands of names', async () => {
|
|
656
|
+
if (lucideIconNames.length < 1000)
|
|
657
|
+
throw new Error('too few lucide names');
|
|
658
|
+
});
|
|
659
|
+
await test('isLucideIconName — case-insensitive match', async () => {
|
|
660
|
+
if (!isLucideIconName('menu') || !isLucideIconName('Menu'))
|
|
661
|
+
throw new Error('case sensitivity broken');
|
|
662
|
+
});
|
|
663
|
+
await test('searchLucideIconNames — exact match ranks first', async () => {
|
|
664
|
+
const results = searchLucideIconNames('Calendar', 5);
|
|
665
|
+
if (results[0] !== 'Calendar')
|
|
666
|
+
throw new Error(`expected Calendar first, got ${results[0]}`);
|
|
667
|
+
});
|
|
668
|
+
// ==========================================================================
|
|
669
|
+
log('\n===========================================', 'cyan');
|
|
670
|
+
log(`Passed: ${passed} Failed: ${failed}`, failed === 0 ? 'green' : 'red');
|
|
671
|
+
log('===========================================\n', 'cyan');
|
|
672
|
+
if (failed > 0)
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
675
|
+
run().catch((error) => {
|
|
676
|
+
log('\nFatal error during test run:', 'red');
|
|
677
|
+
console.error(error);
|
|
678
|
+
process.exit(1);
|
|
679
|
+
});
|
|
680
|
+
//# sourceMappingURL=test.js.map
|