diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 34ffed9..6b42c9a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,6 +29,9 @@ jobs: - name: Check formatting run: npm run format + - name: Typecheck src and tests + run: npm run typecheck + - name: Compile typescript run: npm run build diff --git a/CHANGELOG.md b/CHANGELOG.md index abe153a..1a582da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,10 @@ - `style_comparison_tool`'s `StyleComparisonUIResource` is untouched and stays separate — it renders two synced `mapboxgl.Map` instances under a swipe/compare slider, a genuinely different UI shape from "one map, different content," not just another mode to fold in. - The GeoJSON-preview path keeps its existing eager map bootstrap (drawing the default Standard style immediately, before any tool result arrives, so an overlay has something to render onto right away); the style-preview path now reuses that same map instance via `setStyle()` when it exists, falling back to constructing its own (as before) when it doesn't. Verified live against the real Mapbox API in a real browser: the GeoJSON overlay path and the style-swap-in-place path (loading a real custom style by name) both work end-to-end on the merged resource. +### Maintenance + +- **Tests are typechecked in CI (#143).** `npm run typecheck` checks `src` and `test` together and now runs before the build. The tests had 247 type errors, all already on main: 238 read `.text` off a content block without checking it was text (now `textOf`, `resourceTextOf` and `promptTextOf` in `test/utils/resultTestUtils.ts`, which fail clearly on the wrong content type), the `StyleBuilderTool` tests typed raw input with the parsed type that makes `render_type` required, and one import used a bare `src/` path. `tsconfig.test.json` gets `noEmit` so a plain `tsc` run can't write `.js` files next to the sources. + ### Dependencies - Bumped `@modelcontextprotocol/sdk` to `1.30.0`. Not adopting the `2026-07-28` spec revision this release covers (stateless request/response model, elicitation replaced by Multi Round-Trip Requests, Sampling deprecated) — that's a separate migration, tracked in #130, given this repo's own elicitation-based features depend on the mechanism being replaced. Regenerated `patches/@modelcontextprotocol+sdk+1.30.0.patch` (previously pinned to `1.29.0`) — same patch content, applies cleanly to the new version, verified live against the built server. diff --git a/package.json b/package.json index 3034649..7df87ec 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "spellcheck": "cspell \"*.md\" \"src/**/*.ts\" \"test/**/*.ts\" \"examples/**/*.ts\"", "sync-manifest": "node scripts/sync-manifest-version.cjs", "test": "vitest", + "typecheck": "tsc -p tsconfig.test.json", "tracing:jaeger:start": "docker run --rm -d --name jaeger -p 16686:16686 -p 14250:14250 -p 4317:4317 -p 4318:4318 jaegertracing/all-in-one:latest", "tracing:jaeger:stop": "docker stop jaeger", "tracing:verify": "node -e \"console.log('🔍 Verifying tracing setup with Jaeger...\\n1. Copy .env.example to .env and add your MAPBOX_ACCESS_TOKEN\\n2. Start Jaeger: npm run tracing:jaeger:start\\n3. Run server: npm run inspect:build\\n4. Check traces at: http://localhost:16686')\"" diff --git a/test/prompts/AnalyzeGeojsonPrompt.test.ts b/test/prompts/AnalyzeGeojsonPrompt.test.ts index 1fd9928..8088889 100644 --- a/test/prompts/AnalyzeGeojsonPrompt.test.ts +++ b/test/prompts/AnalyzeGeojsonPrompt.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect } from 'vitest'; import { AnalyzeGeojsonPrompt } from '../../src/prompts/AnalyzeGeojsonPrompt.js'; +import { promptTextOf } from '../utils/resultTestUtils.js'; describe('AnalyzeGeojsonPrompt', () => { const prompt = new AnalyzeGeojsonPrompt(); @@ -50,7 +51,7 @@ describe('AnalyzeGeojsonPrompt', () => { expect(result.messages[0].role).toBe('user'); expect(result.messages[0].content.type).toBe('text'); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Analyze the provided GeoJSON'); expect(text).toContain('bounding_box_tool'); expect(text).toContain('geojson_preview_tool'); @@ -61,7 +62,7 @@ describe('AnalyzeGeojsonPrompt', () => { geojson_data: sampleGeojson }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Calculate bounding box'); expect(text).toContain('bounding_box_tool'); }); @@ -72,7 +73,7 @@ describe('AnalyzeGeojsonPrompt', () => { show_bounds: 'false' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); // Should still mention visualization but with different step number expect(text).toContain('Generate visualization'); }); @@ -83,7 +84,7 @@ describe('AnalyzeGeojsonPrompt', () => { convert_coordinates: 'true' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('coordinate_conversion_tool'); expect(text).toContain('Web Mercator'); expect(text).toContain('EPSG:3857'); @@ -94,7 +95,7 @@ describe('AnalyzeGeojsonPrompt', () => { geojson_data: sampleGeojson }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain(sampleGeojson); }); @@ -109,7 +110,7 @@ describe('AnalyzeGeojsonPrompt', () => { geojson_data: sampleGeojson }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Analysis guidelines'); expect(text).toContain('longitude, latitude'); expect(text).toContain('right-hand rule'); diff --git a/test/prompts/BuildCustomMapPrompt.test.ts b/test/prompts/BuildCustomMapPrompt.test.ts index e2489f1..9299622 100644 --- a/test/prompts/BuildCustomMapPrompt.test.ts +++ b/test/prompts/BuildCustomMapPrompt.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect } from 'vitest'; import { BuildCustomMapPrompt } from '../../src/prompts/BuildCustomMapPrompt.js'; +import { promptTextOf } from '../utils/resultTestUtils.js'; describe('BuildCustomMapPrompt', () => { const prompt = new BuildCustomMapPrompt(); @@ -37,7 +38,7 @@ describe('BuildCustomMapPrompt', () => { expect(result.messages[0].role).toBe('user'); expect(result.messages[0].content.type).toBe('text'); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('dark cyberpunk'); expect(text).toContain('style_builder_tool'); expect(text).toContain('create_style_tool'); @@ -50,7 +51,7 @@ describe('BuildCustomMapPrompt', () => { emphasis: 'parks and green spaces' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('parks and green spaces'); expect(text).toContain('emphasizing'); }); @@ -62,7 +63,7 @@ describe('BuildCustomMapPrompt', () => { preview_zoom: '15' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Tokyo'); expect(text).toContain('15'); }); @@ -78,7 +79,7 @@ describe('BuildCustomMapPrompt', () => { theme: 'retro 80s neon' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Theme interpretation tips'); expect(text).toContain('Dark cyberpunk'); expect(text).toContain('Nature-focused'); diff --git a/test/prompts/CreateAndPreviewStylePrompt.test.ts b/test/prompts/CreateAndPreviewStylePrompt.test.ts index 924a7d6..38f508b 100644 --- a/test/prompts/CreateAndPreviewStylePrompt.test.ts +++ b/test/prompts/CreateAndPreviewStylePrompt.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect } from 'vitest'; import { CreateAndPreviewStylePrompt } from '../../src/prompts/CreateAndPreviewStylePrompt.js'; +import { promptTextOf } from '../utils/resultTestUtils.js'; describe('CreateAndPreviewStylePrompt', () => { const prompt = new CreateAndPreviewStylePrompt(); @@ -44,7 +45,7 @@ describe('CreateAndPreviewStylePrompt', () => { expect(result.messages[0].role).toBe('user'); expect(result.messages[0].content.type).toBe('text'); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Test Style'); expect(text).toContain('list_tokens_tool'); expect(text).toContain('create_style_tool'); @@ -53,7 +54,7 @@ describe('CreateAndPreviewStylePrompt', () => { it('defaults base_style to "standard", not the Classic "streets-v12"', () => { const result = prompt.execute({ style_name: 'Test Style' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('base_style: "standard"'); expect(text).not.toContain('streets-v12'); @@ -65,7 +66,7 @@ describe('CreateAndPreviewStylePrompt', () => { style_name: 'Test Style', base_style: 'streets-v12' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('base_style: "streets-v12"'); }); @@ -79,7 +80,7 @@ describe('CreateAndPreviewStylePrompt', () => { preview_zoom: '14' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('A beautiful test style'); expect(text).toContain('dark-v11'); expect(text).toContain('San Francisco'); diff --git a/test/prompts/DebugMapboxIntegrationPrompt.test.ts b/test/prompts/DebugMapboxIntegrationPrompt.test.ts index 8bb6292..8e39e6f 100644 --- a/test/prompts/DebugMapboxIntegrationPrompt.test.ts +++ b/test/prompts/DebugMapboxIntegrationPrompt.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect } from 'vitest'; import { DebugMapboxIntegrationPrompt } from '../../src/prompts/DebugMapboxIntegrationPrompt.js'; +import { promptTextOf } from '../utils/resultTestUtils.js'; describe('DebugMapboxIntegrationPrompt', () => { const prompt = new DebugMapboxIntegrationPrompt(); @@ -39,7 +40,7 @@ describe('DebugMapboxIntegrationPrompt', () => { expect(result.messages[0].role).toBe('user'); expect(result.messages[0].content.type).toBe('text'); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Map not loading'); expect(text).toContain('list_tokens_tool'); expect(text).toContain('Phase 1'); @@ -51,7 +52,7 @@ describe('DebugMapboxIntegrationPrompt', () => { error_message: '401 Unauthorized' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('401 Unauthorized'); }); @@ -61,7 +62,7 @@ describe('DebugMapboxIntegrationPrompt', () => { error_message: '401 unauthorized' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('401 Unauthorized'); expect(text).toContain('Token not set'); expect(text).toContain('Invalid token'); @@ -73,7 +74,7 @@ describe('DebugMapboxIntegrationPrompt', () => { error_message: '403 Forbidden' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('403 Forbidden'); expect(text).toContain('URL restriction'); expect(text).toContain('Missing scope'); @@ -85,7 +86,7 @@ describe('DebugMapboxIntegrationPrompt', () => { style_id: 'my-style-id' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('my-style-id'); expect(text).toContain('list_styles_tool'); expect(text).toContain('Style Verification'); @@ -97,7 +98,7 @@ describe('DebugMapboxIntegrationPrompt', () => { environment: 'production' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('production'); }); @@ -107,7 +108,7 @@ describe('DebugMapboxIntegrationPrompt', () => { error_message: 'Layer source not found' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Layer/Source Error Solutions'); }); @@ -116,7 +117,7 @@ describe('DebugMapboxIntegrationPrompt', () => { issue_description: 'Something is wrong' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('General debugging'); expect(text).toContain('browser console'); }); @@ -126,7 +127,7 @@ describe('DebugMapboxIntegrationPrompt', () => { issue_description: 'Issue' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Phase 1'); expect(text).toContain('Phase 2'); expect(text).toContain('Phase 3'); diff --git a/test/prompts/DesignDataDrivenStylePrompt.test.ts b/test/prompts/DesignDataDrivenStylePrompt.test.ts index 6b7893a..f998bdc 100644 --- a/test/prompts/DesignDataDrivenStylePrompt.test.ts +++ b/test/prompts/DesignDataDrivenStylePrompt.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect } from 'vitest'; import { DesignDataDrivenStylePrompt } from '../../src/prompts/DesignDataDrivenStylePrompt.js'; +import { promptTextOf } from '../utils/resultTestUtils.js'; describe('DesignDataDrivenStylePrompt', () => { const prompt = new DesignDataDrivenStylePrompt(); @@ -42,7 +43,7 @@ describe('DesignDataDrivenStylePrompt', () => { expect(result.messages[0].role).toBe('user'); expect(result.messages[0].content.type).toBe('text'); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Population Map'); expect(text).toContain('City populations'); expect(text).toContain('population'); @@ -55,7 +56,7 @@ describe('DesignDataDrivenStylePrompt', () => { property_name: 'value' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('interpolate'); expect(text).toContain('["get", "value"]'); expect(text).toContain('Expression types'); @@ -69,7 +70,7 @@ describe('DesignDataDrivenStylePrompt', () => { visualization_type: 'color' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('fill-color'); expect(text).toContain('Color by Value'); }); @@ -82,7 +83,7 @@ describe('DesignDataDrivenStylePrompt', () => { visualization_type: 'size' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('circle-radius'); expect(text).toContain('Size by Value'); }); @@ -95,7 +96,7 @@ describe('DesignDataDrivenStylePrompt', () => { visualization_type: 'both' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('fill-color'); expect(text).toContain('circle-radius'); }); @@ -108,7 +109,7 @@ describe('DesignDataDrivenStylePrompt', () => { visualization_type: 'heatmap' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('heatmap'); expect(text).toContain('heatmap-weight'); expect(text).toContain('heatmap-color'); @@ -121,7 +122,7 @@ describe('DesignDataDrivenStylePrompt', () => { property_name: 'value' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('sequential'); }); @@ -133,7 +134,7 @@ describe('DesignDataDrivenStylePrompt', () => { color_scheme: 'diverging' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Diverging'); expect(text).toContain('midpoint'); }); @@ -146,7 +147,7 @@ describe('DesignDataDrivenStylePrompt', () => { color_scheme: 'categorical' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('match'); expect(text).toContain('Category'); }); @@ -158,7 +159,7 @@ describe('DesignDataDrivenStylePrompt', () => { property_name: 'value' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Step 1'); expect(text).toContain('Step 2'); expect(text).toContain('Step 3'); @@ -176,7 +177,7 @@ describe('DesignDataDrivenStylePrompt', () => { property_name: 'value' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Advanced Expressions'); expect(text).toContain('Zoom-Based'); expect(text).toContain('Conditional'); @@ -189,7 +190,7 @@ describe('DesignDataDrivenStylePrompt', () => { property_name: 'value' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Best Practices'); expect(text).toContain('DO:'); expect(text).toContain("DON'T:"); @@ -202,7 +203,7 @@ describe('DesignDataDrivenStylePrompt', () => { property_name: 'value' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('style_builder_tool'); expect(text).toContain('create_style_tool'); }); diff --git a/test/prompts/PrepareStyleForProductionPrompt.test.ts b/test/prompts/PrepareStyleForProductionPrompt.test.ts index ff5c1d9..6f6994a 100644 --- a/test/prompts/PrepareStyleForProductionPrompt.test.ts +++ b/test/prompts/PrepareStyleForProductionPrompt.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect } from 'vitest'; import { PrepareStyleForProductionPrompt } from '../../src/prompts/PrepareStyleForProductionPrompt.js'; +import { promptTextOf } from '../utils/resultTestUtils.js'; describe('PrepareStyleForProductionPrompt', () => { const prompt = new PrepareStyleForProductionPrompt(); @@ -39,7 +40,7 @@ describe('PrepareStyleForProductionPrompt', () => { expect(result.messages[0].role).toBe('user'); expect(result.messages[0].content.type).toBe('text'); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('username/style-id'); expect(text).toContain('retrieve_style_tool'); expect(text).toContain('validate_expression_tool'); @@ -53,7 +54,7 @@ describe('PrepareStyleForProductionPrompt', () => { style_id_or_json: styleJson }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain(styleJson); expect(text).toContain('parse it directly'); expect(text).not.toContain('retrieve_style_tool'); @@ -64,7 +65,7 @@ describe('PrepareStyleForProductionPrompt', () => { style_id_or_json: 'test/style' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('validate_expression_tool'); expect(text).toContain('validate_geojson_tool'); expect(text).toContain('check_color_contrast_tool'); @@ -78,7 +79,7 @@ describe('PrepareStyleForProductionPrompt', () => { style_id_or_json: 'test/style' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('optimize_style_tool'); expect(text).toContain('Step 5: Optimize the Style'); expect(text).not.toContain('Skipped per user request'); @@ -90,7 +91,7 @@ describe('PrepareStyleForProductionPrompt', () => { skip_optimization: 'true' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Step 5: Style Optimization'); expect(text).toContain('Skipped per user request'); expect(text).not.toContain('optimize_style_tool'); @@ -101,7 +102,7 @@ describe('PrepareStyleForProductionPrompt', () => { style_id_or_json: 'test/style' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('WCAG AA'); expect(text).toContain('level: "AA"'); }); @@ -112,7 +113,7 @@ describe('PrepareStyleForProductionPrompt', () => { wcag_level: 'AAA' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('WCAG AAA'); expect(text).toContain('level: "AAA"'); }); @@ -122,7 +123,7 @@ describe('PrepareStyleForProductionPrompt', () => { style_id_or_json: 'test/style' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Production Quality Report'); expect(text).toContain('Expression Validation'); expect(text).toContain('GeoJSON Validation'); @@ -136,7 +137,7 @@ describe('PrepareStyleForProductionPrompt', () => { style_id_or_json: 'test/style' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('If Issues Are Found'); expect(text).toContain('Expression errors:'); expect(text).toContain('GeoJSON errors:'); @@ -161,7 +162,7 @@ describe('PrepareStyleForProductionPrompt', () => { style_id_or_json: 'test/style' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); const step1Index = text.indexOf('Step 1: Load the Style'); const step2Index = text.indexOf('Step 2: Validate All Expressions'); const step3Index = text.indexOf('Step 3: Validate GeoJSON Sources'); diff --git a/test/prompts/SetupMapboxProjectPrompt.test.ts b/test/prompts/SetupMapboxProjectPrompt.test.ts index 8857d79..64dee4a 100644 --- a/test/prompts/SetupMapboxProjectPrompt.test.ts +++ b/test/prompts/SetupMapboxProjectPrompt.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect } from 'vitest'; import { SetupMapboxProjectPrompt } from '../../src/prompts/SetupMapboxProjectPrompt.js'; +import { promptTextOf } from '../utils/resultTestUtils.js'; describe('SetupMapboxProjectPrompt', () => { const prompt = new SetupMapboxProjectPrompt(); @@ -39,7 +40,7 @@ describe('SetupMapboxProjectPrompt', () => { expect(result.messages[0].role).toBe('user'); expect(result.messages[0].content.type).toBe('text'); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('MyApp'); expect(text).toContain('create_token_tool'); expect(text).toContain('style_builder_tool'); @@ -50,7 +51,7 @@ describe('SetupMapboxProjectPrompt', () => { project_name: 'MyApp' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Development'); expect(text).toContain('localhost'); }); @@ -62,7 +63,7 @@ describe('SetupMapboxProjectPrompt', () => { production_domain: 'myapp.com' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('myapp.com'); expect(text).toContain('Production Token'); }); @@ -73,7 +74,7 @@ describe('SetupMapboxProjectPrompt', () => { project_type: 'web' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('No URL Restrictions'); expect(text).toContain('less secure'); }); @@ -84,7 +85,7 @@ describe('SetupMapboxProjectPrompt', () => { project_type: 'fullstack' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Backend'); expect(text).toContain('SECRET'); expect(text).toContain('environment variables'); @@ -96,7 +97,7 @@ describe('SetupMapboxProjectPrompt', () => { project_type: 'mobile' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Mobile'); expect(text).toContain('vision:read'); }); @@ -107,7 +108,7 @@ describe('SetupMapboxProjectPrompt', () => { style_theme: 'dark' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('dark'); }); diff --git a/test/resources/resourceRegistry.test.ts b/test/resources/resourceRegistry.test.ts index 2867d1a..5f45020 100644 --- a/test/resources/resourceRegistry.test.ts +++ b/test/resources/resourceRegistry.test.ts @@ -4,13 +4,17 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import { Client } from '@modelcontextprotocol/client'; import { McpServer, InMemoryTransport } from '@modelcontextprotocol/server'; -import type { JSONRPCMessage } from '@modelcontextprotocol/server'; +import type { + JSONRPCMessage, + ServerContext +} from '@modelcontextprotocol/server'; import { RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server'; import { getAllResources, installResources } from '../../src/resources/resourceRegistry.js'; import { MapPreviewUIResource } from '../../src/resources/ui-apps/MapPreviewUIResource.js'; +import { resourceTextOf } from '../utils/resultTestUtils.js'; function makeToken(prefix: 'sk' | 'pk' | 'tk', username: string): string { const payload = Buffer.from(JSON.stringify({ u: username })).toString( @@ -66,7 +70,8 @@ describe('installResources', () => { spies.forEach((spy) => { expect(spy).toHaveBeenCalledTimes(1); - expect(spy.mock.calls[0][1].http?.authInfo?.token).toBe('caller-token'); + const ctx = spy.mock.calls[0][1] as ServerContext; + expect(ctx.http?.authInfo?.token).toBe('caller-token'); }); await client.close(); }); @@ -104,7 +109,7 @@ describe('installResources', () => { ); const result = await client.readResource({ uri: resource.uri }); - const html = result.contents[0].text as string; + const html = resourceTextOf(result); const embedded = html.match(/var TOKEN = '([^']*)'/)?.[1] ?? ''; expect(fetchMock).toHaveBeenCalledTimes(1); diff --git a/test/resources/ui-apps/MapPreviewUIResource.test.ts b/test/resources/ui-apps/MapPreviewUIResource.test.ts index 4197773..1642dde 100644 --- a/test/resources/ui-apps/MapPreviewUIResource.test.ts +++ b/test/resources/ui-apps/MapPreviewUIResource.test.ts @@ -4,6 +4,7 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import * as vm from 'node:vm'; import { MapPreviewUIResource } from '../../../src/resources/ui-apps/MapPreviewUIResource.js'; +import { resourceTextOf } from '../../utils/resultTestUtils.js'; const uri = new URL('ui://mapbox/map-preview/index.html'); @@ -30,7 +31,7 @@ async function readHtml( token?: string ): Promise { const result = await resource['readCallback'](uri, extra(token)); - return result.contents[0].text as string; + return resourceTextOf(result); } // Stub global fetch to mirror real Mapbox behaviour: POST tokens/v2/{username} @@ -118,7 +119,7 @@ describe('MapPreviewUIResource — AGI-905 cross-account token leak', () => { ); expect(result.contents).toHaveLength(1); - expect(embeddedToken(result.contents[0].text as string)).toBe(''); + expect(embeddedToken(resourceTextOf(result))).toBe(''); }); it('passes a pk token through unchanged without minting', async () => { @@ -167,10 +168,10 @@ async function renderHtml(accessToken?: string): Promise { const resource = new MapPreviewUIResource(); if (accessToken?.startsWith('pk.')) { const result = await resource['readCallback'](uri, extra(accessToken)); - return result.contents[0].text as string; + return resourceTextOf(result); } const result = await resource['readCallback'](uri, extra()); - return result.contents[0].text as string; + return resourceTextOf(result); } function runScriptSandbox( diff --git a/test/security/path-traversal.test.ts b/test/security/path-traversal.test.ts index 75e0366..ae1960f 100644 --- a/test/security/path-traversal.test.ts +++ b/test/security/path-traversal.test.ts @@ -13,6 +13,7 @@ import { TilequeryTool } from '../../src/tools/tilequery-tool/TilequeryTool.js'; import { DeleteStyleTool } from '../../src/tools/delete-style-tool/DeleteStyleTool.js'; import { PreviewStyleTool } from '../../src/tools/preview-style-tool/PreviewStyleTool.js'; import { setupHttpRequest } from '../utils/httpPipelineUtils.js'; +import { textOf } from '../utils/resultTestUtils.js'; // 25-char alphanumeric — a real Mapbox style ID format const VALID_STYLE_ID = 'cmojrmkc9002t01ry96yi6h48'; @@ -206,7 +207,7 @@ describe('path traversal security', () => { accessToken: maliciousPublicToken }); - const url = result.content[0].text as string; + const url = textOf(result); expect(url).toContain('user%2Fattacker'); expect(url).not.toMatch(/\/styles\/v1\/user\/attacker\//); }); diff --git a/test/tools/check-color-contrast-tool/CheckColorContrastTool.test.ts b/test/tools/check-color-contrast-tool/CheckColorContrastTool.test.ts index deacd45..b3af067 100644 --- a/test/tools/check-color-contrast-tool/CheckColorContrastTool.test.ts +++ b/test/tools/check-color-contrast-tool/CheckColorContrastTool.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { CheckColorContrastTool } from '../../../src/tools/check-color-contrast-tool/CheckColorContrastTool.js'; +import { textOf } from '../../utils/resultTestUtils.js'; describe('CheckColorContrastTool', () => { let tool: CheckColorContrastTool; @@ -40,7 +41,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBe(21); expect(parsed.passes).toBe(true); expect(parsed.level).toBe('AA'); @@ -56,7 +57,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBe(21); expect(parsed.passes).toBe(true); }); @@ -74,7 +75,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBeGreaterThanOrEqual(4.5); expect(parsed.passes).toBe(true); expect(parsed.minimumRequired).toBe(4.5); @@ -91,7 +92,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBeGreaterThanOrEqual(3.0); expect(parsed.passes).toBe(true); expect(parsed.minimumRequired).toBe(3.0); @@ -108,7 +109,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBeLessThan(4.5); expect(parsed.passes).toBe(false); expect(parsed.recommendations).toBeDefined(); @@ -128,7 +129,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBeGreaterThanOrEqual(7.0); expect(parsed.passes).toBe(true); expect(parsed.minimumRequired).toBe(7.0); @@ -145,7 +146,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBeGreaterThanOrEqual(4.5); expect(parsed.passes).toBe(true); expect(parsed.minimumRequired).toBe(4.5); @@ -162,7 +163,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.passes).toBe(false); expect(parsed.recommendations).toBeDefined(); // Should suggest it meets AA @@ -182,7 +183,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBe(21); }); @@ -195,7 +196,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBe(21); }); @@ -208,7 +209,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBe(21); }); @@ -221,7 +222,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBe(21); }); @@ -234,7 +235,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBe(21); }); @@ -247,7 +248,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBe(21); }); @@ -260,7 +261,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBe(21); }); }); @@ -274,7 +275,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Invalid foreground color'); + expect(textOf(result)).toContain('Invalid foreground color'); }); it('should handle invalid background color', async () => { @@ -285,7 +286,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Invalid background color'); + expect(textOf(result)).toContain('Invalid background color'); }); it('should handle malformed hex colors', async () => { @@ -309,7 +310,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.level).toBe('AA'); }); @@ -322,7 +323,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.fontSize).toBe('normal'); }); }); @@ -337,7 +338,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.recommendations).toBeUndefined(); }); @@ -352,7 +353,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.recommendations).toBeDefined(); expect(parsed.recommendations.length).toBeGreaterThan(0); expect( @@ -371,7 +372,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); if (!parsed.passes) { expect( parsed.recommendations.some((r: string) => r.includes('large text')) @@ -390,7 +391,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.wcagRequirements).toBeDefined(); expect(parsed.wcagRequirements.AA.normal).toBe(4.5); expect(parsed.wcagRequirements.AA.large).toBe(3.0); @@ -409,7 +410,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBeGreaterThan(1); }); @@ -422,7 +423,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBeGreaterThan(1); }); }); diff --git a/test/tools/compare-styles-tool/CompareStylesTool.test.ts b/test/tools/compare-styles-tool/CompareStylesTool.test.ts index 9eb58d8..74077aa 100644 --- a/test/tools/compare-styles-tool/CompareStylesTool.test.ts +++ b/test/tools/compare-styles-tool/CompareStylesTool.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { CompareStylesTool } from '../../../src/tools/compare-styles-tool/CompareStylesTool.js'; +import { textOf } from '../../utils/resultTestUtils.js'; describe('CompareStylesTool', () => { let tool: CompareStylesTool; @@ -47,7 +48,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(true); expect(parsed.differences).toHaveLength(0); expect(parsed.summary.totalDifferences).toBe(0); @@ -75,7 +76,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(false); expect(parsed.differences).toHaveLength(1); expect(parsed.differences[0].type).toBe('added'); @@ -103,7 +104,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(false); expect(parsed.differences).toHaveLength(1); expect(parsed.differences[0].type).toBe('removed'); @@ -130,7 +131,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(false); expect(parsed.differences).toHaveLength(1); expect(parsed.differences[0].type).toBe('modified'); @@ -169,7 +170,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(false); expect( parsed.differences.some((d: any) => d.path.includes('background')) @@ -202,7 +203,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(false); expect( parsed.differences.some( @@ -244,7 +245,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(false); expect(parsed.differences.some((d: any) => d.type === 'modified')).toBe( true @@ -283,7 +284,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(true); expect(parsed.differences).toHaveLength(0); }); @@ -311,7 +312,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(false); expect(parsed.differences).toHaveLength(1); expect(parsed.differences[0].path).toBe('id'); @@ -334,7 +335,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(true); }); }); @@ -348,7 +349,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Error parsing style A'); + expect(textOf(result)).toContain('Error parsing style A'); }); it('should handle invalid JSON for style B', async () => { @@ -359,7 +360,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Error parsing style B'); + expect(textOf(result)).toContain('Error parsing style B'); }); }); @@ -393,7 +394,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(false); expect( parsed.differences.some((d: any) => @@ -429,7 +430,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.summary.totalDifferences).toBeGreaterThan(0); expect(parsed.summary.added).toBeGreaterThan(0); expect(parsed.summary.removed).toBeGreaterThan(0); diff --git a/test/tools/create-token-tool/CreateTokenTool.test.ts b/test/tools/create-token-tool/CreateTokenTool.test.ts index c3b64c8..fb73c87 100644 --- a/test/tools/create-token-tool/CreateTokenTool.test.ts +++ b/test/tools/create-token-tool/CreateTokenTool.test.ts @@ -8,7 +8,7 @@ import { } from '../../utils/httpPipelineUtils.js'; import { MapboxApiBasedTool } from '../../../src/tools/MapboxApiBasedTool.js'; import { CreateTokenTool } from '../../../src/tools/create-token-tool/CreateTokenTool.js'; -import { HttpRequest } from 'src/utils/types.js'; +import type { HttpRequest } from '../../../src/utils/types.js'; // Create a token with username in the payload const payload = Buffer.from(JSON.stringify({ u: 'testuser' })).toString( diff --git a/test/tools/get-feedback-tool/GetFeedbackTool.test.ts b/test/tools/get-feedback-tool/GetFeedbackTool.test.ts index 00ef244..06d2561 100644 --- a/test/tools/get-feedback-tool/GetFeedbackTool.test.ts +++ b/test/tools/get-feedback-tool/GetFeedbackTool.test.ts @@ -7,6 +7,7 @@ import { assertHeadersSent } from '../../utils/httpPipelineUtils.js'; import { GetFeedbackTool } from '../../../src/tools/get-feedback-tool/GetFeedbackTool.js'; +import { textOf } from '../../utils/resultTestUtils.js'; const mockToken = 'sk.eyJ1IjoidGVzdC11c2VyIiwiYSI6InRlc3QtYXBpIn0.signature'; @@ -125,10 +126,10 @@ describe('GetFeedbackTool', () => { expect(result.isError).toBe(false); expect(result.content[0].type).toBe('text'); - expect(result.content[0].text).toContain(mockItem.id); - expect(result.content[0].text).toContain(mockItem.status); - expect(result.content[0].text).toContain(mockItem.category); - expect(result.content[0].text).toContain(mockItem.feedback); + expect(textOf(result)).toContain(mockItem.id); + expect(textOf(result)).toContain(mockItem.status); + expect(textOf(result)).toContain(mockItem.category); + expect(textOf(result)).toContain(mockItem.feedback); expect(result.structuredContent).toEqual(mockItem); }); @@ -159,7 +160,7 @@ describe('GetFeedbackTool', () => { expect(result.isError).toBe(false); expect(result.content[0].type).toBe('text'); - const parsed = JSON.parse(result.content[0].text as string); + const parsed = JSON.parse(textOf(result)); expect(parsed).toEqual(mockItem); }); @@ -175,9 +176,9 @@ describe('GetFeedbackTool', () => { }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Failed to get feedback'); - expect(result.content[0].text).toContain('404'); - expect(result.content[0].text).toContain('Not Found'); + expect(textOf(result)).toContain('Failed to get feedback'); + expect(textOf(result)).toContain('404'); + expect(textOf(result)).toContain('Not Found'); assertHeadersSent(mockHttpRequest); }); @@ -229,7 +230,7 @@ describe('GetFeedbackTool', () => { // 1. Returning the data as-is (if format succeeds) // 2. Returning an error response (if formatting fails) // Both are acceptable graceful handling - expect(typeof result.content[0].text).toBe('string'); + expect(typeof textOf(result)).toBe('string'); }); it('validates feedback_id format', async () => { @@ -256,7 +257,7 @@ describe('GetFeedbackTool', () => { // Should fail validation expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('invalid_type'); - expect(result.content[0].text).toContain('feedback_id'); + expect(textOf(result)).toContain('invalid_type'); + expect(textOf(result)).toContain('feedback_id'); }); }); diff --git a/test/tools/list-feedback-tool/ListFeedbackTool.test.ts b/test/tools/list-feedback-tool/ListFeedbackTool.test.ts index 9ddc803..c77198d 100644 --- a/test/tools/list-feedback-tool/ListFeedbackTool.test.ts +++ b/test/tools/list-feedback-tool/ListFeedbackTool.test.ts @@ -7,6 +7,7 @@ import { assertHeadersSent } from '../../utils/httpPipelineUtils.js'; import { ListFeedbackTool } from '../../../src/tools/list-feedback-tool/ListFeedbackTool.js'; +import { textOf } from '../../utils/resultTestUtils.js'; const mockToken = 'sk.eyJ1IjoidGVzdC11c2VyIiwiYSI6InRlc3QtYXBpIn0.signature'; @@ -164,13 +165,9 @@ describe('ListFeedbackTool', () => { expect(result.isError).toBe(false); expect(result.content[0].type).toBe('text'); - expect(result.content[0].text).toContain('Found 2 feedback item(s)'); - expect(result.content[0].text).toContain( - '40eae4c7-b157-4b49-a091-7e1099bba77e' - ); - expect(result.content[0].text).toContain( - '8b1eec47-a3f2-4a6d-a2d9-5e8c1f4a9b0c' - ); + expect(textOf(result)).toContain('Found 2 feedback item(s)'); + expect(textOf(result)).toContain('40eae4c7-b157-4b49-a091-7e1099bba77e'); + expect(textOf(result)).toContain('8b1eec47-a3f2-4a6d-a2d9-5e8c1f4a9b0c'); expect(result.structuredContent).toEqual(mockResponse); }); @@ -186,7 +183,7 @@ describe('ListFeedbackTool', () => { const result = await new ListFeedbackTool({ httpRequest }).run({}); expect(result.isError).toBe(false); - expect(result.content[0].text).toContain('No feedback items found'); + expect(textOf(result)).toContain('No feedback items found'); }); it('handles list response with pagination info', async () => { @@ -218,10 +215,10 @@ describe('ListFeedbackTool', () => { const result = await new ListFeedbackTool({ httpRequest }).run({}); expect(result.isError).toBe(false); - expect(result.content[0].text).toContain('Has previous page'); - expect(result.content[0].text).toContain('Has next page'); - expect(result.content[0].text).toContain('cursor1'); - expect(result.content[0].text).toContain('cursor2'); + expect(textOf(result)).toContain('Has previous page'); + expect(textOf(result)).toContain('Has next page'); + expect(textOf(result)).toContain('cursor1'); + expect(textOf(result)).toContain('cursor2'); }); it('returns JSON format when requested', async () => { @@ -256,7 +253,7 @@ describe('ListFeedbackTool', () => { expect(result.isError).toBe(false); expect(result.content[0].type).toBe('text'); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed).toEqual(mockResponse); }); @@ -270,9 +267,9 @@ describe('ListFeedbackTool', () => { const result = await new ListFeedbackTool({ httpRequest }).run({}); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Failed to list feedback'); - expect(result.content[0].text).toContain('404'); - expect(result.content[0].text).toContain('Not Found'); + expect(textOf(result)).toContain('Failed to list feedback'); + expect(textOf(result)).toContain('404'); + expect(textOf(result)).toContain('Not Found'); assertHeadersSent(mockHttpRequest); }); diff --git a/test/tools/optimize-style-tool/OptimizeStyleTool.test.ts b/test/tools/optimize-style-tool/OptimizeStyleTool.test.ts index f9d7855..6b2be25 100644 --- a/test/tools/optimize-style-tool/OptimizeStyleTool.test.ts +++ b/test/tools/optimize-style-tool/OptimizeStyleTool.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { OptimizeStyleTool } from '../../../src/tools/optimize-style-tool/OptimizeStyleTool.js'; +import { textOf } from '../../utils/resultTestUtils.js'; describe('OptimizeStyleTool', () => { let tool: OptimizeStyleTool; @@ -41,7 +42,7 @@ describe('OptimizeStyleTool', () => { const result = await tool.run({ style }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle).toBeDefined(); expect(parsed.optimizations).toBeDefined(); expect(parsed.summary).toBeDefined(); @@ -57,7 +58,7 @@ describe('OptimizeStyleTool', () => { const result = await tool.run({ style: JSON.stringify(style) }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle).toBeDefined(); }); }); @@ -85,7 +86,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle.sources).toHaveProperty('used-source'); expect(parsed.optimizedStyle.sources).not.toHaveProperty('unused-source'); expect(parsed.optimizations).toHaveLength(1); @@ -111,7 +112,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizations).toHaveLength(0); }); }); @@ -146,7 +147,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle.layers).toHaveLength(2); expect(parsed.optimizations[0].count).toBe(1); }); @@ -167,7 +168,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle.layers).toHaveLength(2); expect(parsed.optimizations).toHaveLength(0); }); @@ -194,7 +195,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle.layers[0].filter).toBe(true); expect(parsed.optimizations[0].count).toBeGreaterThan(0); }); @@ -219,7 +220,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle.layers[0].filter).toBe(false); }); @@ -243,7 +244,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle.layers[0].filter).toBe(true); }); @@ -267,7 +268,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle.layers[0].filter).toBe(false); }); @@ -292,7 +293,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle.layers[0].paint['fill-opacity']).toBe(true); }); }); @@ -323,7 +324,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle.layers).toHaveLength(1); expect(parsed.optimizedStyle.layers[0].id).toBe('non-empty-layer'); expect(parsed.optimizations[0].count).toBe(1); @@ -349,7 +350,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle.layers).toHaveLength(1); expect(parsed.optimizations).toHaveLength(0); }); @@ -382,7 +383,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); // Should identify the group of layers with identical filters expect(parsed.optimizations[0].count).toBeGreaterThan(0); }); @@ -423,7 +424,7 @@ describe('OptimizeStyleTool', () => { const result = await tool.run({ style }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); // Should have removed unused source, duplicate layer, empty layer, and simplified expressions expect(parsed.optimizations.length).toBeGreaterThan(0); expect(parsed.summary.totalOptimizations).toBeGreaterThan(0); @@ -443,7 +444,7 @@ describe('OptimizeStyleTool', () => { const result = await tool.run({ style }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.summary.originalSize).toBeGreaterThan(0); expect(parsed.summary.optimizedSize).toBeGreaterThan(0); expect(parsed.summary.sizeSaved).toBeGreaterThanOrEqual(0); @@ -464,7 +465,7 @@ describe('OptimizeStyleTool', () => { const result = await tool.run({ style }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.summary.percentReduction).toBeGreaterThan(0); }); }); @@ -473,7 +474,7 @@ describe('OptimizeStyleTool', () => { it('should handle invalid JSON string', async () => { const result = await tool.run({ style: '{invalid json' }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Error parsing style'); + expect(textOf(result)).toContain('Error parsing style'); }); }); @@ -497,7 +498,7 @@ describe('OptimizeStyleTool', () => { const result = await tool.run({ style }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.summary.totalOptimizations).toBe(0); expect(parsed.summary.sizeSaved).toBe(0); }); diff --git a/test/tools/style-builder-tool/StyleBuilderTool.test.ts b/test/tools/style-builder-tool/StyleBuilderTool.test.ts index 97ce8ca..d477ee3 100644 --- a/test/tools/style-builder-tool/StyleBuilderTool.test.ts +++ b/test/tools/style-builder-tool/StyleBuilderTool.test.ts @@ -3,7 +3,12 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { StyleBuilderTool } from '../../../src/tools/style-builder-tool/StyleBuilderTool.js'; -import type { StyleBuilderToolInput } from '../../../src/tools/style-builder-tool/StyleBuilderTool.input.schema.js'; +import type { z } from 'zod'; +import type { StyleBuilderToolSchema } from '../../../src/tools/style-builder-tool/StyleBuilderTool.input.schema.js'; +import { textOf } from '../../utils/resultTestUtils.js'; + +// Raw tool input, before the schema fills in defaults like render_type. +type StyleBuilderToolInput = z.input; describe('StyleBuilderTool', () => { let tool: StyleBuilderTool; @@ -37,7 +42,7 @@ describe('StyleBuilderTool', () => { expect(result.isError).toBe(false); expect(result.content[0].type).toBe('text'); - const text = result.content[0].text as string; + const text = textOf(result); expect(text).toContain('Style Built Successfully'); expect(text).toContain('Test Style'); expect(text).toContain('"#0066ff"'); @@ -57,7 +62,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); expect(text).toContain('Mode:** dark'); expect(text).toContain('#000000'); }); @@ -80,7 +85,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); expect(result.isError).toBe(false); expect(text).toContain('#ff0000'); @@ -103,7 +108,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); expect(result.isError).toBe(false); expect(text).toContain('Highlighted'); @@ -124,7 +129,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); expect(result.isError).toBe(false); expect(text).toContain('Hidden'); @@ -144,7 +149,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); expect(result.isError).toBe(false); expect(text).toContain('Shown'); @@ -169,7 +174,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); expect(result.isError).toBe(false); @@ -211,7 +216,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); expect(result.isError).toBe(false); @@ -254,7 +259,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); expect(jsonMatch).toBeTruthy(); @@ -290,7 +295,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -322,7 +327,7 @@ describe('StyleBuilderTool', () => { // Should return help message, not error expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); expect(text).toContain('not found'); expect(text).toContain('Available source layers'); }); @@ -345,7 +350,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -380,7 +385,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -419,7 +424,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -459,7 +464,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -493,7 +498,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -534,7 +539,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const styleJson = JSON.parse(text.match(/```json\n([\s\S]*?)\n```/)![1]); const transitLayer = styleJson.layers.find((l: any) => @@ -570,7 +575,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const styleJson = JSON.parse(text.match(/```json\n([\s\S]*?)\n```/)![1]); const transitLayer = styleJson.layers.find((l: any) => @@ -608,7 +613,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const styleJson = JSON.parse(text.match(/```json\n([\s\S]*?)\n```/)![1]); const roadsLayer = styleJson.layers.find((l: any) => @@ -640,7 +645,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -673,7 +678,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -709,7 +714,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -740,7 +745,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -803,7 +808,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); expect(text).toContain('Standard Config:** 15 properties set'); expect(text).toContain('Theme: faded'); @@ -845,7 +850,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -897,7 +902,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -938,7 +943,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -979,7 +984,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); // No longer expecting auto-correction since we're using the correct layer expect(text).toContain('Style Built Successfully'); @@ -1022,7 +1027,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); expect(text).toContain( 'Determined source layer "poi_label" from filter properties' ); @@ -1064,7 +1069,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); expect(text).toContain('Layers Configured:** 4'); expect(text).toContain('Set to #0066ff'); diff --git a/test/tools/validate-expression-tool/ValidateExpressionTool.test.ts b/test/tools/validate-expression-tool/ValidateExpressionTool.test.ts index 1da6dd8..94cc527 100644 --- a/test/tools/validate-expression-tool/ValidateExpressionTool.test.ts +++ b/test/tools/validate-expression-tool/ValidateExpressionTool.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { ValidateExpressionTool } from '../../../src/tools/validate-expression-tool/ValidateExpressionTool.js'; +import { textOf } from '../../utils/resultTestUtils.js'; describe('ValidateExpressionTool', () => { let tool: ValidateExpressionTool; @@ -39,7 +40,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); expect(parsed.metadata.expressionType).toBe('literal'); @@ -53,7 +54,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); }); @@ -66,7 +67,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); }); }); @@ -80,7 +81,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); expect(parsed.metadata.expressionType).toBe('get'); @@ -99,7 +100,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); }); @@ -120,7 +121,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); }); @@ -133,7 +134,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); }); @@ -145,7 +146,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); }); @@ -157,7 +158,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); }); @@ -169,7 +170,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); }); @@ -181,7 +182,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); }); @@ -193,7 +194,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); }); }); @@ -207,7 +208,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain( 'Expression array cannot be empty' @@ -222,7 +223,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain( 'Expression operator must be a string' @@ -237,7 +238,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain('Unknown expression operator'); }); @@ -250,7 +251,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain('Expected arguments of type'); }); @@ -263,7 +264,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain('Expected arguments of type'); }); @@ -289,7 +290,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); }); @@ -307,7 +308,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect( parsed.errors.some((e: any) => @@ -331,7 +332,7 @@ describe('ValidateExpressionTool', () => { expect(Date.now() - start).toBeLessThan(1000); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect( parsed.errors.some((e: any) => @@ -353,7 +354,7 @@ describe('ValidateExpressionTool', () => { expect(Date.now() - start).toBeLessThan(1000); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect( parsed.errors.some((e: any) => @@ -368,7 +369,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run({ expression }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('exceeds maximum length'); + expect(textOf(result)).toContain('exceeds maximum length'); }); }); @@ -385,7 +386,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.metadata.depth).toBeGreaterThan(0); }); @@ -402,7 +403,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.warnings.length).toBeGreaterThan(0); expect( @@ -425,7 +426,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); }); }); @@ -438,7 +439,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Error parsing expression'); + expect(textOf(result)).toContain('Error parsing expression'); }); }); @@ -451,7 +452,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.metadata).toBeDefined(); expect(parsed.metadata.expressionType).toBe('get'); expect(parsed.metadata.returnType).toBe('any'); @@ -466,7 +467,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.metadata.depth).toBe(1); }); }); diff --git a/test/tools/validate-geojson-tool/ValidateGeojsonTool.test.ts b/test/tools/validate-geojson-tool/ValidateGeojsonTool.test.ts index b9970e9..08498fc 100644 --- a/test/tools/validate-geojson-tool/ValidateGeojsonTool.test.ts +++ b/test/tools/validate-geojson-tool/ValidateGeojsonTool.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { ValidateGeojsonTool } from '../../../src/tools/validate-geojson-tool/ValidateGeojsonTool.js'; +import { textOf } from '../../utils/resultTestUtils.js'; describe('ValidateGeojsonTool', () => { let tool: ValidateGeojsonTool; @@ -42,7 +43,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); expect(parsed.statistics.type).toBe('Point'); @@ -66,7 +67,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); expect(parsed.statistics.type).toBe('LineString'); @@ -91,7 +92,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); expect(parsed.warnings).toHaveLength(0); @@ -115,7 +116,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); expect(parsed.statistics.featureCount).toBe(1); @@ -152,7 +153,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); expect(parsed.statistics.featureCount).toBe(2); @@ -183,7 +184,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); }); @@ -199,7 +200,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); }); }); @@ -215,7 +216,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors).toHaveLength(1); expect(parsed.errors[0].message).toContain( @@ -234,7 +235,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain('Invalid GeoJSON type'); }); @@ -250,7 +251,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain( 'Feature missing "geometry" property' @@ -271,7 +272,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.warnings).toHaveLength(1); expect(parsed.warnings[0].message).toContain( @@ -289,7 +290,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain( 'FeatureCollection missing "features" array' @@ -307,7 +308,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.warnings).toHaveLength(1); expect(parsed.warnings[0].message).toContain( @@ -327,7 +328,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain( 'missing "coordinates" property' @@ -345,7 +346,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain('must be an array'); }); @@ -361,7 +362,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain( 'must have at least 2 elements' @@ -379,7 +380,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain('coordinates must be numbers'); }); @@ -395,7 +396,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.warnings).toHaveLength(1); expect(parsed.warnings[0].message).toContain('Longitude'); @@ -413,7 +414,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.warnings).toHaveLength(1); expect(parsed.warnings[0].message).toContain('Latitude'); @@ -431,7 +432,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect( parsed.errors.some((e: any) => @@ -457,7 +458,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain( 'must have at least 4 positions' @@ -482,7 +483,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.warnings).toHaveLength(1); expect(parsed.warnings[0].message).toContain( @@ -499,7 +500,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Error parsing GeoJSON'); + expect(textOf(result)).toContain('Error parsing GeoJSON'); }); }); @@ -519,7 +520,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.statistics.bbox).toEqual([99.0, -1.0, 101.0, 1.0]); }); @@ -550,7 +551,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.statistics.featureCount).toBe(3); }); @@ -598,7 +599,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.statistics.geometryTypes).toContain('Point'); expect(parsed.statistics.geometryTypes).toContain('LineString'); expect(parsed.statistics.geometryTypes).toContain('Polygon'); diff --git a/test/tools/validate-style-tool/ValidateStyleTool.test.ts b/test/tools/validate-style-tool/ValidateStyleTool.test.ts index 3920e7c..a1d4391 100644 --- a/test/tools/validate-style-tool/ValidateStyleTool.test.ts +++ b/test/tools/validate-style-tool/ValidateStyleTool.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { ValidateStyleTool } from '../../../src/tools/validate-style-tool/ValidateStyleTool.js'; +import { textOf } from '../../utils/resultTestUtils.js'; describe('ValidateStyleTool', () => { let tool: ValidateStyleTool; @@ -38,7 +39,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style: validStyle }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse.valid).toBe(true); expect(parsedResponse.errors).toHaveLength(0); expect(parsedResponse.summary.layerCount).toBe(1); @@ -54,7 +55,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style: invalidStyle }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse.valid).toBe(false); expect(parsedResponse.errors).toContainEqual( expect.objectContaining({ @@ -73,7 +74,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style: invalidStyle }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse.valid).toBe(false); expect(parsedResponse.errors).toContainEqual( expect.objectContaining({ @@ -99,7 +100,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style: invalidStyle }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse.valid).toBe(false); expect(parsedResponse.errors).toContainEqual( expect.objectContaining({ @@ -135,7 +136,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style: invalidStyle }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse.valid).toBe(false); expect(parsedResponse.errors).toContainEqual( expect.objectContaining({ @@ -161,7 +162,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style: invalidStyle }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse.valid).toBe(false); expect(parsedResponse.errors).toContainEqual( expect.objectContaining({ @@ -183,7 +184,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style: JSON.stringify(validStyle) }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse).toHaveProperty('valid'); expect(parsedResponse).toHaveProperty('errors'); }); @@ -192,7 +193,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style: '{ invalid json }' }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Error parsing style JSON'); + expect(textOf(result)).toContain('Error parsing style JSON'); }); it('should provide warnings for missing sprite and glyphs', async () => { @@ -205,7 +206,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse.info).toContainEqual( expect.objectContaining({ severity: 'info', @@ -241,7 +242,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse.summary).toEqual({ version: 8, layerCount: 2, @@ -281,7 +282,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse.valid).toBe(false); expect( parsedResponse.errors.some((e: any) => @@ -326,7 +327,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse.valid).toBe(true); expect(parsedResponse.errors).toHaveLength(0); }); @@ -355,7 +356,7 @@ describe('ValidateStyleTool', () => { expect(Date.now() - start).toBeLessThan(1000); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('exceeds maximum nesting depth'); + expect(textOf(result)).toContain('exceeds maximum nesting depth'); }); it('should reject a style with a wide (shallow but huge) expression instead of exhausting memory', async () => { @@ -382,7 +383,7 @@ describe('ValidateStyleTool', () => { expect(Date.now() - start).toBeLessThan(1000); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('exceeding maximum size'); + expect(textOf(result)).toContain('exceeding maximum size'); }); it('should reject a JSON string style exceeding the maximum length', async () => { diff --git a/test/utils/resultTestUtils.ts b/test/utils/resultTestUtils.ts new file mode 100644 index 0000000..f00506b --- /dev/null +++ b/test/utils/resultTestUtils.ts @@ -0,0 +1,46 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import type { + CallToolResult, + GetPromptResult, + ReadResourceResult +} from '@modelcontextprotocol/server'; + +/** + * The text of a tool result's content block. Throws if that block isn't + * text, so a test fails on the wrong content type rather than on undefined. + */ +export function textOf(result: CallToolResult, index = 0): string { + const block = result.content[index]; + if (block?.type !== 'text') { + throw new Error( + `content[${index}] is ${block?.type ?? 'missing'}, not text` + ); + } + return block.text; +} + +/** + * The text of a resource read's contents entry. Throws if it's a blob. + */ +export function resourceTextOf(result: ReadResourceResult, index = 0): string { + const entry = result.contents[index]; + if (!entry || !('text' in entry)) { + throw new Error(`contents[${index}] is not text`); + } + return entry.text; +} + +/** + * The text of a prompt message. Throws if the message content isn't text. + */ +export function promptTextOf(result: GetPromptResult, index = 0): string { + const content = result.messages[index]?.content; + if (content?.type !== 'text') { + throw new Error( + `messages[${index}] is ${content?.type ?? 'missing'}, not text` + ); + } + return content.text; +} diff --git a/tsconfig.test.json b/tsconfig.test.json index 64c3b90..88cfbab 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -2,6 +2,7 @@ "extends": "./tsconfig.base.json", "compilerOptions": { "skipLibCheck": true, + "noEmit": true, "lib": ["ESNext"], "resolveJsonModule": true },