diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ab3970ff..35ebffc3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,5 +26,8 @@ jobs: - name: Compile typescript run: npm run build + - name: Typecheck src and tests + run: npm run typecheck + - name: Run tests run: npm test \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2605d6e8..15595608 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,9 @@ ### Maintenance - **Removed build output committed under `src/` (#297).** `src/utils/versionUtils-cjs.cjs`, `.d.cts` and their source maps were compiled output from a `tsc` run that wrote next to the sources, committed back in #96. Only `versionUtils-cjs.cts` is source: tshy swaps it in for `versionUtils.ts` in the CommonJS build. `.gitignore` now keeps `.js`, `.cjs`, `.mjs`, `.d.*ts` and `.map` files out of `src/` and `test/`. +- **Tests are typechecked in CI (#298).** `tsconfig.test.json` covers `src/` and `test/`, but nothing ran it, so test type errors piled up: about 200 on main, plus about 250 more from SDK v2 typing `structuredContent` as an open object (#296). They're all fixed, and `npm run typecheck` now runs in CI before the tests. `tsconfig.test.json` also gets `noEmit`, since running it without that writes `.js` files next to the sources (see #297). + - **New helpers** in `test/utils/resultTestUtils.ts`: `structuredOf(result)` reads structured content as the tool's own output type, so a test that checks a field the schema doesn't have fails to compile. `textOf` and `resourceTextOf` read text content and throw on the wrong content type. + - **Tests that were wrong:** the tuple check in `input-schema-validation.test.ts` never looked inside arrays under zod 4 (`ZodArray._def.type` is now the string `'array'`), so a tuple nested in an array would have passed. It uses zod 4's accessors now and covers that case. `structured-content.test.ts` imported a file that never existed. `cleanResponseData.test.ts` used an old directions input shape and now builds it with `DirectionsInputSchema.parse`. A `|| Infinity` fallback in a SimplifyTool test meant one comparison could never fail. ## 0.16.1-dev - 2026-10-16 diff --git a/package.json b/package.json index 7d953a1d..92278d11 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "inspect:build": "npm run build && npx @modelcontextprotocol/inspector -e MAPBOX_ACCESS_TOKEN=\"$MAPBOX_ACCESS_TOKEN\" node dist/esm/index.js", "inspect:dev": "npx @modelcontextprotocol/inspector -e MAPBOX_ACCESS_TOKEN=\"$MAPBOX_ACCESS_TOKEN\" npx -y tsx src/index.ts", "dev:http": "npx -y tsx scripts/dev-http-server.ts", + "typecheck": "tsc -p tsconfig.test.json", "lint": "eslint \"./src/**/*.{ts,tsx}\" \"./test/**/*.{ts,tsx}\" \"./examples/**/*.{ts,tsx}\"", "lint:fix": "eslint \"./src/**/*.{ts,tsx}\" \"./test/**/*.{ts,tsx}\" \"./examples/**/*.{ts,tsx}\" --fix", "postinstall": "patch-package || (cd ../../.. && node ./node_modules/patch-package/index.js --patch-dir ./node_modules/@mapbox/mcp-server/patches) || true", diff --git a/test/prompts/BasePrompt.test.ts b/test/prompts/BasePrompt.test.ts index 67d3869c..bd791ce2 100644 --- a/test/prompts/BasePrompt.test.ts +++ b/test/prompts/BasePrompt.test.ts @@ -8,8 +8,23 @@ import type { PromptMessage } from '@modelcontextprotocol/server'; +// Makes validateArguments public so tests can call it directly. +abstract class TestableBasePrompt extends BasePrompt { + override validateArguments(args: Record): void { + super.validateArguments(args); + } +} + +// The text of a prompt message. Throws if the content isn't text. +function messageText(message: PromptMessage): string { + if (message.content.type !== 'text') { + throw new Error(`message content is ${message.content.type}, not text`); + } + return message.content.text; +} + // Create a concrete test implementation of BasePrompt -class TestPrompt extends BasePrompt { +class TestPrompt extends TestableBasePrompt { readonly name = 'test-prompt'; readonly description = 'A test prompt for unit testing'; readonly arguments: PromptArgument[] = [ @@ -173,7 +188,7 @@ describe('BasePrompt', () => { expect(message.role).toBeDefined(); expect(message.content).toBeDefined(); expect(message.content.type).toBe('text'); - expect(typeof message.content.text).toBe('string'); + expect(typeof messageText(message)).toBe('string'); }); }); @@ -194,10 +209,10 @@ describe('BasePrompt', () => { }; const messages = prompt.getMessages(args); - const messageText = messages[0].content.text; + const text = messageText(messages[0]); - expect(messageText).toContain('test_required'); - expect(messageText).toContain('test_optional'); + expect(text).toContain('test_required'); + expect(text).toContain('test_optional'); }); test('handles missing optional arguments gracefully', () => { @@ -206,16 +221,16 @@ describe('BasePrompt', () => { }; const messages = prompt.getMessages(args); - const messageText = messages[0].content.text; + const text = messageText(messages[0]); - expect(messageText).toContain('test_required'); - expect(messageText).toContain('not provided'); + expect(text).toContain('test_required'); + expect(text).toContain('not provided'); }); }); describe('edge cases', () => { test('validates empty args object when no arguments are required', () => { - class NoArgsPrompt extends BasePrompt { + class NoArgsPrompt extends TestableBasePrompt { readonly name = 'no-args'; readonly description = 'No arguments needed'; readonly arguments: PromptArgument[] = []; @@ -239,7 +254,7 @@ describe('BasePrompt', () => { }); test('validates all required arguments', () => { - class MultiRequiredPrompt extends BasePrompt { + class MultiRequiredPrompt extends TestableBasePrompt { readonly name = 'multi-required'; readonly description = 'Multiple required arguments'; readonly arguments: PromptArgument[] = [ diff --git a/test/resources/category-list/CategoryListResource.test.ts b/test/resources/category-list/CategoryListResource.test.ts index 2d0f450e..31dda987 100644 --- a/test/resources/category-list/CategoryListResource.test.ts +++ b/test/resources/category-list/CategoryListResource.test.ts @@ -9,6 +9,7 @@ import { assertHeadersSent } from '../../utils/httpPipelineUtils.js'; import { CategoryListResource } from '../../../src/resources/category-list/CategoryListResource.js'; +import { resourceTextOf } from '../../utils/resultTestUtils.js'; describe('CategoryListResource', () => { afterEach(() => { @@ -111,7 +112,7 @@ describe('CategoryListResource', () => { expect(result.contents[0].uri).toBe('mapbox://categories'); expect(result.contents[0].mimeType).toBe('application/json'); - const parsed = JSON.parse(result.contents[0].text!); + const parsed = JSON.parse(resourceTextOf(result)); expect(parsed.listItems).toEqual(['restaurant', 'hotel']); expect(parsed.version).toBe('25:test'); expect(parsed.attribution).toBe('Test Attribution'); @@ -131,7 +132,7 @@ describe('CategoryListResource', () => { ); expect(result.contents).toHaveLength(1); - const parsed = JSON.parse(result.contents[0].text!); + const parsed = JSON.parse(resourceTextOf(result)); expect(parsed.listItems).toEqual([]); }); diff --git a/test/resources/compute/ComputeResource.test.ts b/test/resources/compute/ComputeResource.test.ts index 25af05cf..43a2cee5 100644 --- a/test/resources/compute/ComputeResource.test.ts +++ b/test/resources/compute/ComputeResource.test.ts @@ -4,6 +4,7 @@ import { describe, it, expect } from 'vitest'; import { ComputeResource } from '../../../src/resources/compute/ComputeResource.js'; import { buildComputeRef } from '../../../src/utils/computeRef.js'; +import { resourceTextOf } from '../../utils/resultTestUtils.js'; describe('ComputeResource', () => { it('resolves a union compute ref via the real resources/read path with zero prior server-side state', async () => { @@ -47,7 +48,7 @@ describe('ComputeResource', () => { const result = await resource.read(ref); expect(result.contents[0].mimeType).toBe('application/json'); - const payload = JSON.parse(result.contents[0].text as string); + const payload = JSON.parse(resourceTextOf(result)); expect(payload.layers).toHaveLength(6); expect(payload.summary).toBe('Union of 2 polygons'); }); @@ -90,7 +91,7 @@ describe('ComputeResource', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const asAccountB = { http: { authInfo: { token: 'irrelevant' } } } as any; const result = await resource.read(ref, asAccountB); - const payload = JSON.parse(result.contents[0].text as string); + const payload = JSON.parse(resourceTextOf(result)); expect(payload.summary).toBe('Intersection of two polygons'); }); @@ -99,6 +100,6 @@ describe('ComputeResource', () => { const result = await resource.read('mapbox://compute/union?data=!!!'); expect(result.contents[0].mimeType).toBe('text/plain'); - expect(result.contents[0].text).toContain('malformed or unsupported'); + expect(resourceTextOf(result)).toContain('malformed or unsupported'); }); }); diff --git a/test/resources/inline-payload/InlinePayloadResource.test.ts b/test/resources/inline-payload/InlinePayloadResource.test.ts index 091d075d..ffda1409 100644 --- a/test/resources/inline-payload/InlinePayloadResource.test.ts +++ b/test/resources/inline-payload/InlinePayloadResource.test.ts @@ -4,6 +4,7 @@ import { describe, it, expect } from 'vitest'; import { InlinePayloadResource } from '../../../src/resources/inline-payload/InlinePayloadResource.js'; import { buildInlinePayloadRef } from '../../../src/utils/inlinePayloadRef.js'; +import { resourceTextOf } from '../../utils/resultTestUtils.js'; describe('InlinePayloadResource', () => { it("resolves render_map_tool's self-describing merged ref via the real resources/read path with zero prior server-side state", async () => { @@ -33,7 +34,7 @@ describe('InlinePayloadResource', () => { const result = await resource.read(ref); expect(result.contents[0].mimeType).toBe('application/json'); - const payload = JSON.parse(result.contents[0].text as string); + const payload = JSON.parse(resourceTextOf(result)); expect(payload.summary).toBe('Test trip'); expect(payload.layers).toHaveLength(1); expect(payload.markers).toHaveLength(1); @@ -44,6 +45,6 @@ describe('InlinePayloadResource', () => { const result = await resource.read('mapbox://inline/payload?data=!!!'); expect(result.contents[0].mimeType).toBe('text/plain'); - expect(result.contents[0].text).toContain('malformed'); + expect(resourceTextOf(result)).toContain('malformed'); }); }); diff --git a/test/resources/temporary/TemporaryDataResource.test.ts b/test/resources/temporary/TemporaryDataResource.test.ts index 97dfb4b3..730d3c09 100644 --- a/test/resources/temporary/TemporaryDataResource.test.ts +++ b/test/resources/temporary/TemporaryDataResource.test.ts @@ -12,6 +12,15 @@ import { getOwnerKeyFromToken, getUserNameFromToken } from '../../../src/utils/jwtUtils.js'; +import type { ReadResourceResult } from '@modelcontextprotocol/server'; +import { resourceTextOf } from '../../utils/resultTestUtils.js'; + +// The blob of a resource read's first contents entry, or undefined when +// it's text. +function blobOf(result: ReadResourceResult): string | undefined { + const entry = result.contents[0]; + return 'blob' in entry ? entry.blob : undefined; +} // Build a Mapbox-style 3-part JWT whose payload carries the username (`u`). function tokenFor(username: string): string { @@ -82,7 +91,7 @@ describe('TemporaryDataResource — AGI-890 cross-account access control', () => seedTextResource(uri, 'accountA', { route: 'A-secret-geometry' }); const result = await resource.read(uri, extraFor(tokenFor('accountA'))); - const text = result.contents[0].text as string; + const text = resourceTextOf(result); expect(text).toContain('A-secret-geometry'); }); @@ -92,7 +101,7 @@ describe('TemporaryDataResource — AGI-890 cross-account access control', () => seedTextResource(uri, 'accountA', { route: 'A-secret-geometry' }); const result = await resource.read(uri, extraFor(tokenFor('accountB'))); - const text = result.contents[0].text as string; + const text = resourceTextOf(result); expect(text).toBe(NOT_FOUND); expect(text).not.toContain('A-secret-geometry'); @@ -117,8 +126,8 @@ describe('TemporaryDataResource — AGI-890 cross-account access control', () => expect(crossAccount.contents[0].mimeType).toBe( missing.contents[0].mimeType ); - expect(crossAccount.contents[0].text).toBe(missing.contents[0].text); - expect(crossAccount.contents[0].text).toBe(NOT_FOUND); + expect(resourceTextOf(crossAccount)).toBe(resourceTextOf(missing)); + expect(resourceTextOf(crossAccount)).toBe(NOT_FOUND); }); it('fails closed when the reader has no token', async () => { @@ -126,7 +135,7 @@ describe('TemporaryDataResource — AGI-890 cross-account access control', () => seedTextResource(uri, 'accountA', { route: 'A-secret-geometry' }); const result = await resource.read(uri, extraFor(undefined)); - expect(result.contents[0].text).toBe(NOT_FOUND); + expect(resourceTextOf(result)).toBe(NOT_FOUND); }); it('fails closed when the resource has no owner recorded', async () => { @@ -139,7 +148,7 @@ describe('TemporaryDataResource — AGI-890 cross-account access control', () => }); const result = await resource.read(uri, extraFor(tokenFor('accountA'))); - expect(result.contents[0].text).toBe(NOT_FOUND); + expect(resourceTextOf(result)).toBe(NOT_FOUND); }); it('falls back to the env token so stdio/single-user reads still work', async () => { @@ -150,7 +159,7 @@ describe('TemporaryDataResource — AGI-890 cross-account access control', () => // No authInfo on the request (stdio) -> requester resolved from env token. const result = await resource.read(uri, extraFor(undefined)); - expect(result.contents[0].text as string).toContain('local-data'); + expect(resourceTextOf(result)).toContain('local-data'); }); it('returns image blobs to the owner and not-found to others', async () => { @@ -165,12 +174,12 @@ describe('TemporaryDataResource — AGI-890 cross-account access control', () => }); const owner = await resource.read(uri, extraFor(tokenFor('accountA'))); - expect(owner.contents[0].blob).toBe('BASE64IMAGEDATA'); + expect(blobOf(owner)).toBe('BASE64IMAGEDATA'); expect(owner.contents[0].mimeType).toBe('image/png'); const other = await resource.read(uri, extraFor(tokenFor('accountB'))); - expect(other.contents[0].blob).toBeUndefined(); - expect(other.contents[0].text).toBe(NOT_FOUND); + expect(blobOf(other)).toBeUndefined(); + expect(resourceTextOf(other)).toBe(NOT_FOUND); }); it('resolves a storeMapPayload ref via the real resources/read path (regression)', async () => { @@ -185,7 +194,7 @@ describe('TemporaryDataResource — AGI-890 cross-account access control', () => ); const result = await resource.read(ref, extraFor(tokenFor('accountA'))); - const parsed = JSON.parse(result.contents[0].text as string); + const parsed = JSON.parse(resourceTextOf(result)); expect(parsed.summary).toBe('Test route'); }); @@ -210,8 +219,8 @@ describe('TemporaryDataResource — AGI-890 cross-account access control', () => const result = await resource.read(uri, extraFor(forged)); - expect(result.contents[0].text).toBe(NOT_FOUND); - expect(result.contents[0].text).not.toContain('A-secret-geometry'); + expect(resourceTextOf(result)).toBe(NOT_FOUND); + expect(resourceTextOf(result)).not.toContain('A-secret-geometry'); }); it('does NOT return an image blob to a token forging the owner’s username', async () => { @@ -230,8 +239,8 @@ describe('TemporaryDataResource — AGI-890 cross-account access control', () => extraFor(forgedTokenFor('victim_user')) ); - expect(result.contents[0].blob).toBeUndefined(); - expect(result.contents[0].text).toBe(NOT_FOUND); + expect(blobOf(result)).toBeUndefined(); + expect(resourceTextOf(result)).toBe(NOT_FOUND); }); it('does NOT resolve a map-payload ref for a token forging the owner’s username', async () => { diff --git a/test/resources/ui-apps/MapAppUIResource.test.ts b/test/resources/ui-apps/MapAppUIResource.test.ts index 01eedcdc..c5ff098c 100644 --- a/test/resources/ui-apps/MapAppUIResource.test.ts +++ b/test/resources/ui-apps/MapAppUIResource.test.ts @@ -7,6 +7,8 @@ const SK_TOKEN = 'sk.eyJzdWIiOiJ0ZXN0IiwidSI6InRlc3R1c2VyIn0.signature'; import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest'; import { MapAppUIResource } from '../../../src/resources/ui-apps/MapAppUIResource.js'; import { __resetMapboxPublicTokenCache } from '../../../src/utils/mapboxPublicToken.js'; +import { resourceTextOf } from '../../utils/resultTestUtils.js'; +import type { HttpRequest } from '../../../src/utils/types.js'; const fakeTokenList = [ { @@ -39,7 +41,8 @@ describe('MapAppUIResource', () => { }); it('serves HTML at ui://mapbox/map-app/index.html with the mcp-app mime type', async () => { - const httpRequest = vi.fn(async (url: string) => { + const httpRequest = vi.fn(async (input) => { + const url = String(input); if (url.includes('tokens/v2/')) return makeOkJson(fakeTokenList) as Response; throw new Error(`Unexpected URL: ${url}`); @@ -57,11 +60,10 @@ describe('MapAppUIResource', () => { const entry = result.contents[0]; expect(entry.mimeType).toBe('text/html;profile=mcp-app'); expect(entry.uri).toBe('ui://mapbox/map-app/index.html'); - expect(typeof entry.text).toBe('string'); - expect(entry.text as string).toContain( - 'pk.eyJ1IjoidGVzdHVzZXIifQ.fake-public-token' - ); - expect(entry.text as string).toContain('mapbox-gl.js'); + const text = resourceTextOf(result); + expect(typeof text).toBe('string'); + expect(text).toContain('pk.eyJ1IjoidGVzdHVzZXIifQ.fake-public-token'); + expect(text).toContain('mapbox-gl.js'); const meta = (entry as { _meta?: unknown })._meta as | { ui?: { csp?: Record } } @@ -86,7 +88,7 @@ describe('MapAppUIResource', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - expect(result.contents[0].text as string).toContain( + expect(resourceTextOf(result)).toContain( 'No Mapbox public token available' ); }); diff --git a/test/resources/ui-apps/mapAppHtml.script.test.ts b/test/resources/ui-apps/mapAppHtml.script.test.ts index b7b72ee0..90c42733 100644 --- a/test/resources/ui-apps/mapAppHtml.script.test.ts +++ b/test/resources/ui-apps/mapAppHtml.script.test.ts @@ -681,7 +681,7 @@ describe('mapAppHtml directions self-fetch', () => { map.addLayer = addLayerSpy; map.addSource = addSourceSpy; - const fetchSpy = vi.fn(async () => ({ + const fetchSpy = vi.fn(async (_url: string) => ({ ok: true, json: async () => ({ routes: [ @@ -938,7 +938,7 @@ describe('mapAppHtml isochrone self-fetch', () => { map.addLayer = addLayerSpy; map.addSource = addSourceSpy; - const fetchSpy = vi.fn(async () => ({ + const fetchSpy = vi.fn(async (_url: string) => ({ ok: true, json: async () => ({ type: 'FeatureCollection', @@ -1044,7 +1044,7 @@ describe('mapAppHtml map matching self-fetch', () => { map.addLayer = addLayerSpy; map.addSource = addSourceSpy; - const fetchSpy = vi.fn(async () => ({ + const fetchSpy = vi.fn(async (_url: string) => ({ ok: true, json: async () => ({ code: 'Ok', @@ -1187,7 +1187,7 @@ describe('mapAppHtml search self-fetch', () => { map.addLayer = addLayerSpy; map.addSource = addSourceSpy; - const fetchSpy = vi.fn(async () => ({ + const fetchSpy = vi.fn(async (_url: string) => ({ ok: true, json: async () => ({ type: 'FeatureCollection', @@ -1320,7 +1320,7 @@ describe('mapAppHtml category search self-fetch', () => { map.addLayer = addLayerSpy; map.addSource = addSourceSpy; - const fetchSpy = vi.fn(async () => ({ + const fetchSpy = vi.fn(async (_url: string) => ({ ok: true, json: async () => ({ type: 'FeatureCollection', @@ -1405,7 +1405,7 @@ describe('mapAppHtml optimization self-fetch', () => { map.addLayer = addLayerSpy; map.addSource = addSourceSpy; - const fetchSpy = vi.fn(async () => ({ + const fetchSpy = vi.fn(async (_url: string) => ({ ok: true, json: async () => ({ code: 'Ok', @@ -2339,7 +2339,7 @@ describe('mapAppHtml results side panel: self-fetch (category_search)', () => { describe('mapAppHtml results side panel: inline markers', () => { it('renders and enriches the panel from inline markers that carry an id, with no selfFetch entries at all', async () => { const { sendToolResult, setFetchImpl, sidePanelEl } = loadScriptSandbox(); - const fetchSpy = vi.fn(async () => ({ + const fetchSpy = vi.fn(async (_url: string) => ({ ok: true, json: async () => ({ results: [] }) })); diff --git a/test/tools/area-tool/AreaTool.test.ts b/test/tools/area-tool/AreaTool.test.ts index a78e0c22..5e349b05 100644 --- a/test/tools/area-tool/AreaTool.test.ts +++ b/test/tools/area-tool/AreaTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { AreaTool } from '../../../src/tools/area-tool/AreaTool.js'; +import type { AreaOutput } from '../../../src/tools/area-tool/AreaTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('AreaTool', () => { const tool = new AreaTool(); @@ -20,12 +22,13 @@ describe('AreaTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('meters'); + expect(data.units).toBe('meters'); // Should be approximately 1,237,000 square meters (varies by latitude) - expect(result.structuredContent?.area).toBeGreaterThan(1000000); - expect(result.structuredContent?.area).toBeLessThan(1500000); + expect(data.area).toBeGreaterThan(1000000); + expect(data.area).toBeLessThan(1500000); }); it('should calculate area in square kilometers', async () => { @@ -41,12 +44,13 @@ describe('AreaTool', () => { ], units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('kilometers'); + expect(data.units).toBe('kilometers'); // Should be approximately 1.2 square kilometers - expect(result.structuredContent?.area).toBeGreaterThan(1); - expect(result.structuredContent?.area).toBeLessThan(1.5); + expect(data.area).toBeGreaterThan(1); + expect(data.area).toBeLessThan(1.5); }); it('should calculate area in square feet', async () => { @@ -62,10 +66,11 @@ describe('AreaTool', () => { ], units: 'feet' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('feet'); - expect(result.structuredContent?.area).toBeGreaterThan(100000); + expect(data.units).toBe('feet'); + expect(data.area).toBeGreaterThan(100000); }); it('should calculate area in square miles', async () => { @@ -81,12 +86,13 @@ describe('AreaTool', () => { ], units: 'miles' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('miles'); + expect(data.units).toBe('miles'); // Should be roughly 47 square miles - expect(result.structuredContent?.area).toBeGreaterThan(40); - expect(result.structuredContent?.area).toBeLessThan(50); + expect(data.area).toBeGreaterThan(40); + expect(data.area).toBeLessThan(50); }); it('should calculate area in acres', async () => { @@ -102,12 +108,13 @@ describe('AreaTool', () => { ], units: 'acres' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('acres'); + expect(data.units).toBe('acres'); // Should be approximately 305 acres - expect(result.structuredContent?.area).toBeGreaterThan(250); - expect(result.structuredContent?.area).toBeLessThan(350); + expect(data.area).toBeGreaterThan(250); + expect(data.area).toBeLessThan(350); }); it('should calculate area in hectares', async () => { @@ -123,12 +130,13 @@ describe('AreaTool', () => { ], units: 'hectares' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('hectares'); + expect(data.units).toBe('hectares'); // Should be approximately 124 hectares - expect(result.structuredContent?.area).toBeGreaterThan(100); - expect(result.structuredContent?.area).toBeLessThan(150); + expect(data.area).toBeGreaterThan(100); + expect(data.area).toBeLessThan(150); }); it('should calculate area of triangle', async () => { @@ -143,11 +151,12 @@ describe('AreaTool', () => { ], units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); // Triangle should have roughly half the area of the rectangle - expect(result.structuredContent?.area).toBeGreaterThan(0.5); - expect(result.structuredContent?.area).toBeLessThan(1); + expect(data.area).toBeGreaterThan(0.5); + expect(data.area).toBeLessThan(1); }); it('should calculate area of polygon with hole', async () => { @@ -172,11 +181,12 @@ describe('AreaTool', () => { ], units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); // Area should be outer minus inner (roughly 4 - 1 = 3 km²) - expect(result.structuredContent?.area).toBeGreaterThan(2.5); - expect(result.structuredContent?.area).toBeLessThan(4); + expect(data.area).toBeGreaterThan(2.5); + expect(data.area).toBeLessThan(4); }); it('should calculate area of multipolygon', async () => { @@ -205,11 +215,12 @@ describe('AreaTool', () => { ], units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); // Total area should be roughly 2.4 km² - expect(result.structuredContent?.area).toBeGreaterThan(2); - expect(result.structuredContent?.area).toBeLessThan(3); + expect(data.area).toBeGreaterThan(2); + expect(data.area).toBeLessThan(3); }); it('should calculate area of irregular polygon', async () => { @@ -227,10 +238,11 @@ describe('AreaTool', () => { ], units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.area).toBeGreaterThan(3); - expect(result.structuredContent?.area).toBeLessThan(7); + expect(data.area).toBeGreaterThan(3); + expect(data.area).toBeLessThan(7); }); it('should round area to 3 decimal places', async () => { @@ -246,9 +258,10 @@ describe('AreaTool', () => { ], units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - const area = result.structuredContent?.area as number; + const area = data.area as number; const decimalPlaces = (area.toString().split('.')[1] || '').length; expect(decimalPlaces).toBeLessThanOrEqual(3); }); @@ -266,11 +279,12 @@ describe('AreaTool', () => { ], units: 'meters' }); + const data = structuredOf(result); expect(result.isError).toBe(false); // Should be roughly 123 square meters - expect(result.structuredContent?.area).toBeGreaterThan(100); - expect(result.structuredContent?.area).toBeLessThan(200); + expect(data.area).toBeGreaterThan(100); + expect(data.area).toBeLessThan(200); }); it('should handle large polygons', async () => { @@ -287,10 +301,11 @@ describe('AreaTool', () => { ], units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); // Should be hundreds of thousands of square kilometers - expect(result.structuredContent?.area).toBeGreaterThan(500000); + expect(data.area).toBeGreaterThan(500000); }); it('should calculate area in southern hemisphere', async () => { @@ -306,9 +321,10 @@ describe('AreaTool', () => { ], units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.area).toBeGreaterThan(900000); + expect(data.area).toBeGreaterThan(900000); }); it('should handle polygon near poles', async () => { @@ -324,11 +340,12 @@ describe('AreaTool', () => { ], units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); // Area should be much smaller near poles due to convergence of meridians - expect(result.structuredContent?.area).toBeGreaterThan(0); - expect(result.structuredContent?.area).toBeLessThan(20000); + expect(data.area).toBeGreaterThan(0); + expect(data.area).toBeLessThan(20000); }); it('should calculate zero area for degenerate polygon (line)', async () => { @@ -342,12 +359,13 @@ describe('AreaTool', () => { ], units: 'kilometers' }); + const data = structuredOf(result); // Degenerate polygon may error or return 0 area if (result.isError) { expect(result.isError).toBe(true); } else { - expect(result.structuredContent?.area).toBe(0); + expect(data.area).toBe(0); } }); }); diff --git a/test/tools/bearing-tool/BearingTool.test.ts b/test/tools/bearing-tool/BearingTool.test.ts index b4defefd..36eecca6 100644 --- a/test/tools/bearing-tool/BearingTool.test.ts +++ b/test/tools/bearing-tool/BearingTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { BearingTool } from '../../../src/tools/bearing-tool/BearingTool.js'; +import type { BearingOutput } from '../../../src/tools/bearing-tool/BearingTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('BearingTool', () => { const tool = new BearingTool(); @@ -12,14 +14,15 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 0 }, to: { longitude: 0, latitude: 1 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bearing).toBeCloseTo(0, 1); - expect(result.structuredContent?.from).toEqual({ + expect(data.bearing).toBeCloseTo(0, 1); + expect(data.from).toEqual({ longitude: 0, latitude: 0 }); - expect(result.structuredContent?.to).toEqual({ + expect(data.to).toEqual({ longitude: 0, latitude: 1 }); @@ -30,9 +33,10 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 0 }, to: { longitude: 1, latitude: 0 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bearing).toBeCloseTo(90, 1); + expect(data.bearing).toBeCloseTo(90, 1); }); it('should calculate bearing for south direction (180°)', async () => { @@ -40,9 +44,10 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 0 }, to: { longitude: 0, latitude: -1 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bearing).toBeCloseTo(180, 1); + expect(data.bearing).toBeCloseTo(180, 1); }); it('should calculate bearing for west direction (270°)', async () => { @@ -50,9 +55,10 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 0 }, to: { longitude: -1, latitude: 0 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bearing).toBeCloseTo(270, 1); + expect(data.bearing).toBeCloseTo(270, 1); }); it('should calculate bearing for northeast direction (45°)', async () => { @@ -60,10 +66,11 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 0 }, to: { longitude: 1, latitude: 1 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bearing).toBeGreaterThan(30); - expect(result.structuredContent?.bearing).toBeLessThan(60); + expect(data.bearing).toBeGreaterThan(30); + expect(data.bearing).toBeLessThan(60); }); it('should calculate bearing for southwest direction (225°)', async () => { @@ -71,10 +78,11 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 0 }, to: { longitude: -1, latitude: -1 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bearing).toBeGreaterThan(210); - expect(result.structuredContent?.bearing).toBeLessThan(240); + expect(data.bearing).toBeGreaterThan(210); + expect(data.bearing).toBeLessThan(240); }); it('should calculate bearing between real cities (SF to LA)', async () => { @@ -82,11 +90,12 @@ describe('BearingTool', () => { from: { longitude: -122.4194, latitude: 37.7749 }, // San Francisco to: { longitude: -118.2437, latitude: 34.0522 } // Los Angeles }); + const data = structuredOf(result); expect(result.isError).toBe(false); // SF to LA should be roughly southeast - expect(result.structuredContent?.bearing).toBeGreaterThan(100); - expect(result.structuredContent?.bearing).toBeLessThan(150); + expect(data.bearing).toBeGreaterThan(100); + expect(data.bearing).toBeLessThan(150); }); it('should calculate bearing between real cities (NY to London)', async () => { @@ -94,11 +103,12 @@ describe('BearingTool', () => { from: { longitude: -74.006, latitude: 40.7128 }, // New York to: { longitude: -0.1276, latitude: 51.5074 } // London }); + const data = structuredOf(result); expect(result.isError).toBe(false); // NY to London should be roughly northeast - expect(result.structuredContent?.bearing).toBeGreaterThan(40); - expect(result.structuredContent?.bearing).toBeLessThan(90); + expect(data.bearing).toBeGreaterThan(40); + expect(data.bearing).toBeLessThan(90); }); it('should handle same point as zero bearing', async () => { @@ -106,9 +116,10 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 0 }, to: { longitude: 0, latitude: 0 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bearing).toBe(0); + expect(data.bearing).toBe(0); }); it('should handle bearing across the antimeridian', async () => { @@ -116,11 +127,12 @@ describe('BearingTool', () => { from: { longitude: 179, latitude: 0 }, to: { longitude: -179, latitude: 0 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); // Should be roughly east (90°) - expect(result.structuredContent?.bearing).toBeGreaterThan(70); - expect(result.structuredContent?.bearing).toBeLessThan(110); + expect(data.bearing).toBeGreaterThan(70); + expect(data.bearing).toBeLessThan(110); }); it('should handle bearing near poles', async () => { @@ -128,11 +140,12 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 85 }, to: { longitude: 90, latitude: 85 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); // Near poles, bearings are distorted - just check it's a valid bearing - expect(result.structuredContent?.bearing).toBeGreaterThanOrEqual(0); - expect(result.structuredContent?.bearing).toBeLessThan(360); + expect(data.bearing).toBeGreaterThanOrEqual(0); + expect(data.bearing).toBeLessThan(360); }); it('should round bearing to 2 decimal places', async () => { @@ -140,9 +153,10 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 0 }, to: { longitude: 0.123456, latitude: 0.654321 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); - const bearing = result.structuredContent?.bearing as number; + const bearing = data.bearing as number; const decimalPlaces = (bearing.toString().split('.')[1] || '').length; expect(decimalPlaces).toBeLessThanOrEqual(2); }); @@ -152,12 +166,13 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 0 }, to: { longitude: -1, latitude: 1 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); // Should be northwest (between 270-360) - expect(result.structuredContent?.bearing).toBeGreaterThanOrEqual(0); - expect(result.structuredContent?.bearing).toBeLessThan(360); - expect(result.structuredContent?.bearing).toBeGreaterThan(300); + expect(data.bearing).toBeGreaterThanOrEqual(0); + expect(data.bearing).toBeLessThan(360); + expect(data.bearing).toBeGreaterThan(300); }); it('should handle negative coordinates', async () => { @@ -165,10 +180,11 @@ describe('BearingTool', () => { from: { longitude: -100, latitude: -30 }, to: { longitude: -90, latitude: -20 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bearing).toBeGreaterThanOrEqual(0); - expect(result.structuredContent?.bearing).toBeLessThan(360); + expect(data.bearing).toBeGreaterThanOrEqual(0); + expect(data.bearing).toBeLessThan(360); }); it('should calculate reverse bearing correctly', async () => { @@ -176,14 +192,16 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 0 }, to: { longitude: 0, latitude: 1 } }); + const forwardResultData = structuredOf(forwardResult); const reverseResult = await tool.run({ from: { longitude: 0, latitude: 1 }, to: { longitude: 0, latitude: 0 } }); + const reverseResultData = structuredOf(reverseResult); - const forwardBearing = forwardResult.structuredContent?.bearing as number; - const reverseBearing = reverseResult.structuredContent?.bearing as number; + const forwardBearing = forwardResultData.bearing as number; + const reverseBearing = reverseResultData.bearing as number; // Reverse bearing should be ~180° different const diff = Math.abs(forwardBearing - reverseBearing); diff --git a/test/tools/bounding-box-tool/BoundingBoxTool.test.ts b/test/tools/bounding-box-tool/BoundingBoxTool.test.ts index 0033d585..357b9133 100644 --- a/test/tools/bounding-box-tool/BoundingBoxTool.test.ts +++ b/test/tools/bounding-box-tool/BoundingBoxTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { BoundingBoxTool } from '../../../src/tools/bounding-box-tool/BoundingBoxTool.js'; +import type { BoundingBoxOutput } from '../../../src/tools/bounding-box-tool/BoundingBoxTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('BoundingBoxTool', () => { const tool = new BoundingBoxTool(); @@ -11,9 +13,10 @@ describe('BoundingBoxTool', () => { const result = await tool.run({ geometry: [10, 20] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([10, 20, 10, 20]); + expect(data.bbox).toEqual([10, 20, 10, 20]); }); it('should calculate bbox for a linestring', async () => { @@ -24,9 +27,10 @@ describe('BoundingBoxTool', () => { [5, 10] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([0, 0, 10, 10]); + expect(data.bbox).toEqual([0, 0, 10, 10]); }); it('should calculate bbox for a polygon', async () => { @@ -41,9 +45,10 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([0, 0, 10, 10]); + expect(data.bbox).toEqual([0, 0, 10, 10]); }); it('should calculate bbox for a polygon with hole', async () => { @@ -67,10 +72,11 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); // BBox should be based on outer ring only - expect(result.structuredContent?.bbox).toEqual([-10, -10, 10, 10]); + expect(data.bbox).toEqual([-10, -10, 10, 10]); }); it('should calculate bbox for a multipolygon', async () => { @@ -98,10 +104,11 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); // BBox should encompass both polygons - expect(result.structuredContent?.bbox).toEqual([0, 0, 20, 20]); + expect(data.bbox).toEqual([0, 0, 20, 20]); }); it('should calculate bbox for irregular polygon', async () => { @@ -117,9 +124,10 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([1, 2, 9, 9]); + expect(data.bbox).toEqual([1, 2, 9, 9]); }); it('should calculate bbox for negative coordinates', async () => { @@ -134,9 +142,10 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([-120, -30, -110, -20]); + expect(data.bbox).toEqual([-120, -30, -110, -20]); }); it('should calculate bbox crossing the equator and prime meridian', async () => { @@ -151,9 +160,10 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([-10, -10, 10, 10]); + expect(data.bbox).toEqual([-10, -10, 10, 10]); }); it('should calculate bbox for real world coordinates (San Francisco)', async () => { @@ -168,12 +178,13 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox[0]).toBeCloseTo(-122.52, 5); - expect(result.structuredContent?.bbox[1]).toBeCloseTo(37.7, 5); - expect(result.structuredContent?.bbox[2]).toBeCloseTo(-122.35, 5); - expect(result.structuredContent?.bbox[3]).toBeCloseTo(37.82, 5); + expect(data.bbox[0]).toBeCloseTo(-122.52, 5); + expect(data.bbox[1]).toBeCloseTo(37.7, 5); + expect(data.bbox[2]).toBeCloseTo(-122.35, 5); + expect(data.bbox[3]).toBeCloseTo(37.82, 5); }); it('should calculate bbox near poles', async () => { @@ -188,9 +199,10 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([0, 85, 90, 89]); + expect(data.bbox).toEqual([0, 85, 90, 89]); }); it('should round bbox coordinates to 6 decimal places', async () => { @@ -205,9 +217,10 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - const bbox = result.structuredContent?.bbox as number[]; + const bbox = data.bbox as number[]; bbox.forEach((coord) => { const decimalPlaces = (coord.toString().split('.')[1] || '').length; expect(decimalPlaces).toBeLessThanOrEqual(6); @@ -226,9 +239,10 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([0, 0, 0.0001, 0.0001]); + expect(data.bbox).toEqual([0, 0, 0.0001, 0.0001]); }); it('should calculate bbox for very large geometries', async () => { @@ -243,9 +257,10 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([-180, -90, 180, 90]); + expect(data.bbox).toEqual([-180, -90, 180, 90]); }); it('should calculate bbox for diagonal line', async () => { @@ -255,9 +270,10 @@ describe('BoundingBoxTool', () => { [10, 10] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([0, 0, 10, 10]); + expect(data.bbox).toEqual([0, 0, 10, 10]); }); it('should calculate bbox for horizontal line', async () => { @@ -267,9 +283,10 @@ describe('BoundingBoxTool', () => { [10, 5] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([0, 5, 10, 5]); + expect(data.bbox).toEqual([0, 5, 10, 5]); }); it('should calculate bbox for vertical line', async () => { @@ -279,9 +296,10 @@ describe('BoundingBoxTool', () => { [5, 10] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([5, 0, 5, 10]); + expect(data.bbox).toEqual([5, 0, 5, 10]); }); it('should calculate bbox for zigzag linestring', async () => { @@ -294,9 +312,10 @@ describe('BoundingBoxTool', () => { [20, 0] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([0, 0, 20, 10]); + expect(data.bbox).toEqual([0, 0, 20, 10]); }); it('should handle triangle polygon', async () => { @@ -310,9 +329,10 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([0, 0, 10, 10]); + expect(data.bbox).toEqual([0, 0, 10, 10]); }); it('should calculate bbox format as [minLon, minLat, maxLon, maxLat]', async () => { @@ -322,9 +342,10 @@ describe('BoundingBoxTool', () => { [30, 40] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - const bbox = result.structuredContent?.bbox as number[]; + const bbox = data.bbox as number[]; // Verify format: [west, south, east, north] expect(bbox[0]).toBeLessThanOrEqual(bbox[2]); // minLon <= maxLon expect(bbox[1]).toBeLessThanOrEqual(bbox[3]); // minLat <= maxLat diff --git a/test/tools/buffer-tool/BufferTool.test.ts b/test/tools/buffer-tool/BufferTool.test.ts index 413ec0f8..8d369f69 100644 --- a/test/tools/buffer-tool/BufferTool.test.ts +++ b/test/tools/buffer-tool/BufferTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { BufferTool } from '../../../src/tools/buffer-tool/BufferTool.js'; +import type { BufferOutput } from '../../../src/tools/buffer-tool/BufferTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('BufferTool', () => { const tool = new BufferTool(); @@ -12,18 +14,17 @@ describe('BufferTool', () => { geometry: [0, 0], distance: 1 }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.distance).toBe(1); - expect(result.structuredContent?.units).toBe('kilometers'); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); - expect(Array.isArray(result.structuredContent?.bufferedPolygon)).toBe(true); + expect(data.distance).toBe(1); + expect(data.units).toBe('kilometers'); + expect(data.bufferedPolygon).toBeDefined(); + expect(Array.isArray(data.bufferedPolygon)).toBe(true); // Should have at least one ring - expect(result.structuredContent?.bufferedPolygon.length).toBeGreaterThan(0); + expect(data.bufferedPolygon.length).toBeGreaterThan(0); // Outer ring should have multiple vertices (typically 64 for a circle) - expect(result.structuredContent?.bufferedPolygon[0].length).toBeGreaterThan( - 10 - ); + expect(data.bufferedPolygon[0].length).toBeGreaterThan(10); }); it('should create buffer around a point in meters', async () => { @@ -32,11 +33,12 @@ describe('BufferTool', () => { distance: 1000, units: 'meters' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.distance).toBe(1000); - expect(result.structuredContent?.units).toBe('meters'); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.distance).toBe(1000); + expect(data.units).toBe('meters'); + expect(data.bufferedPolygon).toBeDefined(); }); it('should create buffer around a point in miles', async () => { @@ -45,11 +47,12 @@ describe('BufferTool', () => { distance: 5, units: 'miles' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.distance).toBe(5); - expect(result.structuredContent?.units).toBe('miles'); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.distance).toBe(5); + expect(data.units).toBe('miles'); + expect(data.bufferedPolygon).toBeDefined(); }); it('should create buffer around a point in feet', async () => { @@ -58,11 +61,12 @@ describe('BufferTool', () => { distance: 5000, units: 'feet' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.distance).toBe(5000); - expect(result.structuredContent?.units).toBe('feet'); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.distance).toBe(5000); + expect(data.units).toBe('feet'); + expect(data.bufferedPolygon).toBeDefined(); }); it('should create buffer around a linestring', async () => { @@ -74,13 +78,12 @@ describe('BufferTool', () => { distance: 0.5, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); // Buffer around line should have more vertices than around point - expect(result.structuredContent?.bufferedPolygon[0].length).toBeGreaterThan( - 10 - ); + expect(data.bufferedPolygon[0].length).toBeGreaterThan(10); }); it('should create buffer around a multi-segment linestring', async () => { @@ -94,9 +97,10 @@ describe('BufferTool', () => { distance: 0.1, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); }); it('should create buffer around a polygon', async () => { @@ -113,13 +117,12 @@ describe('BufferTool', () => { distance: 0.5, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); // Buffer around polygon should expand it - expect(result.structuredContent?.bufferedPolygon[0].length).toBeGreaterThan( - 4 - ); + expect(data.bufferedPolygon[0].length).toBeGreaterThan(4); }); it('should create buffer around a small polygon', async () => { @@ -136,9 +139,10 @@ describe('BufferTool', () => { distance: 100, units: 'meters' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); }); it('should handle different buffer distances', async () => { @@ -147,19 +151,21 @@ describe('BufferTool', () => { distance: 1, units: 'kilometers' }); + const result1Data = structuredOf(result1); const result2 = await tool.run({ geometry: [0, 0], distance: 5, units: 'kilometers' }); + const result2Data = structuredOf(result2); expect(result1.isError).toBe(false); expect(result2.isError).toBe(false); // Larger buffer should have polygon that extends further from center // We can't directly compare areas, but both should succeed - expect(result1.structuredContent?.bufferedPolygon).toBeDefined(); - expect(result2.structuredContent?.bufferedPolygon).toBeDefined(); + expect(result1Data.bufferedPolygon).toBeDefined(); + expect(result2Data.bufferedPolygon).toBeDefined(); }); it('should create buffer around real world location (NYC)', async () => { @@ -168,11 +174,12 @@ describe('BufferTool', () => { distance: 10, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); // Check that buffer is centered roughly around NYC coordinates - const firstCoord = result.structuredContent?.bufferedPolygon[0][0]; + const firstCoord = data.bufferedPolygon[0][0]; expect(firstCoord).toBeDefined(); // Coordinates should be within reasonable range of NYC if (firstCoord) { @@ -189,9 +196,10 @@ describe('BufferTool', () => { distance: 5, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); }); it('should create buffer near the equator', async () => { @@ -200,9 +208,10 @@ describe('BufferTool', () => { distance: 10, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); }); it('should create buffer with very small distance', async () => { @@ -211,9 +220,10 @@ describe('BufferTool', () => { distance: 10, units: 'meters' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); }); it('should create buffer with large distance', async () => { @@ -222,9 +232,10 @@ describe('BufferTool', () => { distance: 100, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); }); it('should return closed polygon (first and last coordinates match)', async () => { @@ -233,9 +244,10 @@ describe('BufferTool', () => { distance: 1, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - const outerRing = result.structuredContent?.bufferedPolygon[0]; + const outerRing = data.bufferedPolygon[0]; expect(outerRing).toBeDefined(); if (outerRing) { const firstCoord = outerRing[0]; @@ -251,9 +263,10 @@ describe('BufferTool', () => { distance: 1, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - const outerRing = result.structuredContent?.bufferedPolygon[0]; + const outerRing = data.bufferedPolygon[0]; expect(outerRing).toBeDefined(); // Circle should have many vertices for smooth appearance (typically 64) if (outerRing) { @@ -274,9 +287,10 @@ describe('BufferTool', () => { distance: 0.1, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); }); it('should create buffer around irregular polygon', async () => { @@ -294,9 +308,10 @@ describe('BufferTool', () => { distance: 0.5, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); }); it('should handle buffer near poles', async () => { @@ -305,8 +320,9 @@ describe('BufferTool', () => { distance: 10, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); }); }); diff --git a/test/tools/centroid-tool/CentroidTool.test.ts b/test/tools/centroid-tool/CentroidTool.test.ts index f72a152a..8b18a228 100644 --- a/test/tools/centroid-tool/CentroidTool.test.ts +++ b/test/tools/centroid-tool/CentroidTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { CentroidTool } from '../../../src/tools/centroid-tool/CentroidTool.js'; +import type { CentroidOutput } from '../../../src/tools/centroid-tool/CentroidTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('CentroidTool', () => { const tool = new CentroidTool(); @@ -21,8 +23,9 @@ describe('CentroidTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.centroid.longitude).toBeCloseTo(0, 1); - expect(result.structuredContent?.centroid.latitude).toBeCloseTo(0, 1); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeCloseTo(0, 1); + expect(data.centroid.latitude).toBeCloseTo(0, 1); }); it('should calculate centroid of a triangle', async () => { @@ -39,8 +42,9 @@ describe('CentroidTool', () => { expect(result.isError).toBe(false); // Centroid of triangle should be at (3, 2) - expect(result.structuredContent?.centroid.longitude).toBeCloseTo(3, 1); - expect(result.structuredContent?.centroid.latitude).toBeCloseTo(2, 1); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeCloseTo(3, 1); + expect(data.centroid.latitude).toBeCloseTo(2, 1); }); it('should calculate centroid of a rectangle', async () => { @@ -58,8 +62,9 @@ describe('CentroidTool', () => { expect(result.isError).toBe(false); // Centroid should be at (5, 2.5) - expect(result.structuredContent?.centroid.longitude).toBeCloseTo(5, 1); - expect(result.structuredContent?.centroid.latitude).toBeCloseTo(2.5, 1); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeCloseTo(5, 1); + expect(data.centroid.latitude).toBeCloseTo(2.5, 1); }); it('should calculate centroid of polygon with real coordinates', async () => { @@ -78,8 +83,9 @@ describe('CentroidTool', () => { expect(result.isError).toBe(false); // Centroid should be roughly in the middle of SF - expect(result.structuredContent?.centroid.longitude).toBeCloseTo(-122.4, 1); - expect(result.structuredContent?.centroid.latitude).toBeCloseTo(37.75, 1); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeCloseTo(-122.4, 1); + expect(data.centroid.latitude).toBeCloseTo(37.75, 1); }); it('should calculate centroid of polygon with hole', async () => { @@ -106,8 +112,9 @@ describe('CentroidTool', () => { expect(result.isError).toBe(false); // Centroid should still be at center, even though there's a hole - expect(result.structuredContent?.centroid.longitude).toBeCloseTo(0, 1); - expect(result.structuredContent?.centroid.latitude).toBeCloseTo(0, 1); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeCloseTo(0, 1); + expect(data.centroid.latitude).toBeCloseTo(0, 1); }); it('should calculate centroid of multipolygon', async () => { @@ -138,9 +145,10 @@ describe('CentroidTool', () => { expect(result.isError).toBe(false); // Centroid should be between the two squares - expect(result.structuredContent?.centroid.longitude).toBeGreaterThan(1); - expect(result.structuredContent?.centroid.longitude).toBeLessThan(4); - expect(result.structuredContent?.centroid.latitude).toBeCloseTo(1, 1); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeGreaterThan(1); + expect(data.centroid.longitude).toBeLessThan(4); + expect(data.centroid.latitude).toBeCloseTo(1, 1); }); it('should handle polygon in southern hemisphere', async () => { @@ -157,8 +165,9 @@ describe('CentroidTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.centroid.longitude).toBeCloseTo(-55, 1); - expect(result.structuredContent?.centroid.latitude).toBeCloseTo(-25, 1); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeCloseTo(-55, 1); + expect(data.centroid.latitude).toBeCloseTo(-25, 1); }); it('should handle L-shaped polygon', async () => { @@ -178,10 +187,11 @@ describe('CentroidTool', () => { expect(result.isError).toBe(false); // Centroid should be somewhere in the L shape - expect(result.structuredContent?.centroid.longitude).toBeGreaterThan(0); - expect(result.structuredContent?.centroid.longitude).toBeLessThan(2); - expect(result.structuredContent?.centroid.latitude).toBeGreaterThan(0); - expect(result.structuredContent?.centroid.latitude).toBeLessThan(2); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeGreaterThan(0); + expect(data.centroid.longitude).toBeLessThan(2); + expect(data.centroid.latitude).toBeGreaterThan(0); + expect(data.centroid.latitude).toBeLessThan(2); }); it('should round coordinates to 6 decimal places', async () => { @@ -198,8 +208,9 @@ describe('CentroidTool', () => { }); expect(result.isError).toBe(false); - const lon = result.structuredContent?.centroid.longitude as number; - const lat = result.structuredContent?.centroid.latitude as number; + const data = structuredOf(result); + const lon = data.centroid.longitude; + const lat = data.centroid.latitude; const lonDecimals = (lon.toString().split('.')[1] || '').length; const latDecimals = (lat.toString().split('.')[1] || '').length; expect(lonDecimals).toBeLessThanOrEqual(6); @@ -223,8 +234,9 @@ describe('CentroidTool', () => { expect(result.isError).toBe(false); // Centroid should be at center - expect(result.structuredContent?.centroid.longitude).toBeCloseTo(0, 1); - expect(result.structuredContent?.centroid.latitude).toBeCloseTo(0, 1); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeCloseTo(0, 1); + expect(data.centroid.latitude).toBeCloseTo(0, 1); }); it('should handle polygon crossing antimeridian', async () => { @@ -243,16 +255,11 @@ describe('CentroidTool', () => { expect(result.isError).toBe(false); // Centroid calculation may vary with antimeridian crossing // Just check it's a valid coordinate - expect(result.structuredContent?.centroid.latitude).toBeGreaterThanOrEqual( - -90 - ); - expect(result.structuredContent?.centroid.latitude).toBeLessThanOrEqual(90); - expect(result.structuredContent?.centroid.longitude).toBeGreaterThanOrEqual( - -180 - ); - expect(result.structuredContent?.centroid.longitude).toBeLessThanOrEqual( - 180 - ); + const data = structuredOf(result); + expect(data.centroid.latitude).toBeGreaterThanOrEqual(-90); + expect(data.centroid.latitude).toBeLessThanOrEqual(90); + expect(data.centroid.longitude).toBeGreaterThanOrEqual(-180); + expect(data.centroid.longitude).toBeLessThanOrEqual(180); }); it('should handle very small polygon', async () => { @@ -269,8 +276,9 @@ describe('CentroidTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.centroid.longitude).toBeCloseTo(0.0005, 4); - expect(result.structuredContent?.centroid.latitude).toBeCloseTo(0.0005, 4); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeCloseTo(0.0005, 4); + expect(data.centroid.latitude).toBeCloseTo(0.0005, 4); }); it('should handle large polygon', async () => { @@ -287,8 +295,9 @@ describe('CentroidTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.centroid.longitude).toBeCloseTo(-100, 1); - expect(result.structuredContent?.centroid.latitude).toBeCloseTo(35, 1); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeCloseTo(-100, 1); + expect(data.centroid.latitude).toBeCloseTo(35, 1); }); it('should handle complex irregular polygon', async () => { @@ -308,9 +317,10 @@ describe('CentroidTool', () => { expect(result.isError).toBe(false); // Centroid should be somewhere inside or near the polygon - expect(result.structuredContent?.centroid.longitude).toBeGreaterThan(0); - expect(result.structuredContent?.centroid.longitude).toBeLessThan(5); - expect(result.structuredContent?.centroid.latitude).toBeGreaterThan(0); - expect(result.structuredContent?.centroid.latitude).toBeLessThan(5); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeGreaterThan(0); + expect(data.centroid.longitude).toBeLessThan(5); + expect(data.centroid.latitude).toBeGreaterThan(0); + expect(data.centroid.latitude).toBeLessThan(5); }); }); diff --git a/test/tools/convex-tool/ConvexTool.test.ts b/test/tools/convex-tool/ConvexTool.test.ts index 646b0697..12135091 100644 --- a/test/tools/convex-tool/ConvexTool.test.ts +++ b/test/tools/convex-tool/ConvexTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { ConvexTool } from '../../../src/tools/convex-tool/ConvexTool.js'; +import type { ConvexOutput } from '../../../src/tools/convex-tool/ConvexTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('ConvexTool', () => { const tool = new ConvexTool(); @@ -19,12 +21,12 @@ describe('ConvexTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.geometry).not.toBeNull(); - expect(result.structuredContent?.num_points).toBe(5); + expect(structuredOf(result).geometry).not.toBeNull(); + expect(structuredOf(result).num_points).toBe(5); // Hull of a square is a polygon - expect((result.structuredContent?.geometry as { type: string })?.type).toBe( - 'Polygon' - ); + expect( + (structuredOf(result).geometry as { type: string })?.type + ).toBe('Polygon'); }); it('returns null geometry for collinear points', async () => { @@ -37,8 +39,8 @@ describe('ConvexTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.geometry).toBeNull(); - expect(result.structuredContent?.num_points).toBe(3); + expect(structuredOf(result).geometry).toBeNull(); + expect(structuredOf(result).num_points).toBe(3); }); it('works with real-world points', async () => { @@ -54,8 +56,8 @@ describe('ConvexTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.geometry).not.toBeNull(); - expect(result.structuredContent?.num_points).toBe(5); + expect(structuredOf(result).geometry).not.toBeNull(); + expect(structuredOf(result).num_points).toBe(5); }); it('rejects fewer than three points', async () => { diff --git a/test/tools/destination-tool/DestinationTool.test.ts b/test/tools/destination-tool/DestinationTool.test.ts index a2b5ebcf..3c24061b 100644 --- a/test/tools/destination-tool/DestinationTool.test.ts +++ b/test/tools/destination-tool/DestinationTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { DestinationTool } from '../../../src/tools/destination-tool/DestinationTool.js'; +import type { DestinationOutput } from '../../../src/tools/destination-tool/DestinationTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('DestinationTool', () => { const tool = new DestinationTool(); @@ -17,8 +19,12 @@ describe('DestinationTool', () => { expect(result.isError).toBe(false); // Should end up roughly 1 degree north - expect(result.structuredContent?.destination.latitude).toBeCloseTo(1, 0); - expect(result.structuredContent?.destination.longitude).toBeCloseTo(0, 1); + expect( + structuredOf(result).destination.latitude + ).toBeCloseTo(1, 0); + expect( + structuredOf(result).destination.longitude + ).toBeCloseTo(0, 1); }); it('calculates destination heading east', async () => { @@ -30,8 +36,12 @@ describe('DestinationTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.destination.longitude).toBeGreaterThan(0); - expect(result.structuredContent?.destination.latitude).toBeCloseTo(0, 1); + expect( + structuredOf(result).destination.longitude + ).toBeGreaterThan(0); + expect( + structuredOf(result).destination.latitude + ).toBeCloseTo(0, 1); }); it('calculates destination in kilometers', async () => { @@ -44,10 +54,12 @@ describe('DestinationTool', () => { expect(result.isError).toBe(false); // 100km north should increase latitude by ~0.9 degrees - expect(result.structuredContent?.destination.latitude).toBeGreaterThan(38); - expect(result.structuredContent?.units).toBe('kilometers'); - expect(result.structuredContent?.distance).toBe(100); - expect(result.structuredContent?.bearing).toBe(0); + expect( + structuredOf(result).destination.latitude + ).toBeGreaterThan(38); + expect(structuredOf(result).units).toBe('kilometers'); + expect(structuredOf(result).distance).toBe(100); + expect(structuredOf(result).bearing).toBe(0); }); it('defaults to meters units', async () => { @@ -58,7 +70,7 @@ describe('DestinationTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('meters'); + expect(structuredOf(result).units).toBe('meters'); }); it('calculates destination heading south (negative bearing)', async () => { @@ -71,7 +83,9 @@ describe('DestinationTool', () => { expect(result.isError).toBe(false); // Heading south should decrease latitude - expect(result.structuredContent?.destination.latitude).toBeLessThan(10); + expect( + structuredOf(result).destination.latitude + ).toBeLessThan(10); }); it('rejects bearing out of range', async () => { diff --git a/test/tools/difference-tool/DifferenceTool.test.ts b/test/tools/difference-tool/DifferenceTool.test.ts index 26dc728a..8a3c9dc5 100644 --- a/test/tools/difference-tool/DifferenceTool.test.ts +++ b/test/tools/difference-tool/DifferenceTool.test.ts @@ -3,7 +3,8 @@ import { describe, it, expect } from 'vitest'; import { DifferenceTool } from '../../../src/tools/difference-tool/DifferenceTool.js'; -import { tokenFor } from '../../utils/tokenTestUtils.js'; +import type { DifferenceOutput } from '../../../src/tools/difference-tool/DifferenceTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('DifferenceTool', () => { const tool = new DifferenceTool(); @@ -31,8 +32,8 @@ describe('DifferenceTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.has_difference).toBe(true); - expect(result.structuredContent?.geometry).not.toBeNull(); + expect(structuredOf(result).has_difference).toBe(true); + expect(structuredOf(result).geometry).not.toBeNull(); }); it('returns has_difference=false when polygon2 fully covers polygon1', async () => { @@ -58,8 +59,8 @@ describe('DifferenceTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.has_difference).toBe(false); - expect(result.structuredContent?.geometry).toBeNull(); + expect(structuredOf(result).has_difference).toBe(false); + expect(structuredOf(result).geometry).toBeNull(); }); it('returns full polygon1 when polygons do not overlap', async () => { @@ -85,8 +86,8 @@ describe('DifferenceTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.has_difference).toBe(true); - expect(result.structuredContent?.geometry).not.toBeNull(); + expect(structuredOf(result).has_difference).toBe(true); + expect(structuredOf(result).geometry).not.toBeNull(); }); it('respects holes in polygon1', async () => { @@ -124,9 +125,9 @@ describe('DifferenceTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.has_difference).toBe(true); + expect(structuredOf(result).has_difference).toBe(true); // Result should still have a hole in it - expect(result.structuredContent?.geometry).not.toBeNull(); + expect(structuredOf(result).geometry).not.toBeNull(); }); it('text content describes the result', async () => { @@ -180,31 +181,26 @@ describe('DifferenceTool', () => { }); it('stores a mapboxRender payload with input fills + difference result', async () => { - const token = tokenFor('account-test-difference'); - const result = await tool.run( - { - polygon1: [ - [ - [0, 0], - [4, 0], - [4, 4], - [0, 4], - [0, 0] - ] - ], - polygon2: [ - [ - [1, 1], - [3, 1], - [3, 3], - [1, 3], - [1, 1] - ] + const result = await tool.run({ + polygon1: [ + [ + [0, 0], + [4, 0], + [4, 4], + [0, 4], + [0, 0] ] - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - { http: { authInfo: { token } } } as any - ); + ], + polygon2: [ + [ + [1, 1], + [3, 1], + [3, 3], + [1, 3], + [1, 1] + ] + ] + }); expect(result.isError).toBe(false); const sc = result.structuredContent as { mapboxRender?: { ref?: string } }; diff --git a/test/tools/directions-tool/DirectionsTool.test.ts b/test/tools/directions-tool/DirectionsTool.test.ts index 1bb1c479..3dacd703 100644 --- a/test/tools/directions-tool/DirectionsTool.test.ts +++ b/test/tools/directions-tool/DirectionsTool.test.ts @@ -13,6 +13,7 @@ import * as cleanResponseModule from '../../../src/tools/directions-tool/cleanRe import { temporaryResourceManager } from '../../../src/utils/temporaryResourceManager.js'; import { tokenFor } from '../../utils/tokenTestUtils.js'; import { resolveResponseRef } from '../../../src/utils/responseRef.js'; +import type { HttpRequest } from '../../../src/utils/types.js'; describe('DirectionsTool', () => { beforeEach(() => { @@ -1157,8 +1158,8 @@ describe('DirectionsTool', () => { ) }; - return vi.fn(async (url: string) => { - const response = url.includes('geometries=geojson') + return vi.fn(async (input) => { + const response = String(input).includes('geometries=geojson') ? withGeometry : withoutGeometry; return { diff --git a/test/tools/directions-tool/cleanResponseData.test.ts b/test/tools/directions-tool/cleanResponseData.test.ts index a257bc96..8a30c918 100644 --- a/test/tools/directions-tool/cleanResponseData.test.ts +++ b/test/tools/directions-tool/cleanResponseData.test.ts @@ -3,18 +3,31 @@ import { describe, it, expect } from 'vitest'; import { cleanResponseData } from '../../../src/tools/directions-tool/cleanResponseData.js'; +import { DirectionsInputSchema } from '../../../src/tools/directions-tool/DirectionsTool.input.schema.js'; describe('cleanResponseData', () => { - // Create a complete mock input that satisfies the DirectionsInputSchema - const mockInput = { + // A full, parsed DirectionsInputSchema input. cleanResponseData only reads + // `geometries`. + const mockInput = DirectionsInputSchema.parse({ coordinates: [ - [0, 0], - [1, 1] - ] as [number, number][], // Use a proper tuple type - routing_profile: 'driving-traffic' as const, - geometries: 'none' as const, + { longitude: 0, latitude: 0 }, + { longitude: 1, latitude: 1 } + ], + routing_profile: 'mapbox/driving-traffic', + geometries: 'none', alternatives: false - }; + }); + + // The cleaned response types routes and waypoints as optional; these fail + // the test, rather than the type check, when they're missing. + function routesOf(result: ReturnType) { + if (!result.routes) throw new Error('result has no routes'); + return result.routes; + } + function waypointsOf(result: ReturnType) { + if (!result.waypoints) throw new Error('result has no waypoints'); + return result.waypoints; + } it('should remove unnecessary keys', () => { const mockData = { @@ -33,7 +46,7 @@ describe('cleanResponseData', () => { waypoints: [ { name: 'Test', - location: [-73.989, 40.733], + location: [-73.989, 40.733] as [number, number], distance: 10.5 } ], @@ -41,10 +54,10 @@ describe('cleanResponseData', () => { }; const result = cleanResponseData(mockInput, mockData); - expect(result.waypoints[0].location).toBeUndefined(); - expect(result.waypoints[0].snap_location).toEqual([-73.989, 40.733]); - expect(result.waypoints[0].distance).toBeUndefined(); - expect(result.waypoints[0].snap_distance).toBe(11); // Rounded from 10.5 + expect(waypointsOf(result)[0].location).toBeUndefined(); + expect(waypointsOf(result)[0].snap_location).toEqual([-73.989, 40.733]); + expect(waypointsOf(result)[0].distance).toBeUndefined(); + expect(waypointsOf(result)[0].snap_distance).toBe(11); // Rounded from 10.5 }); it('should round duration and distance', () => { @@ -59,8 +72,8 @@ describe('cleanResponseData', () => { }; const result = cleanResponseData(mockInput, mockData); - expect(result.routes[0].duration).toBe(1235); - expect(result.routes[0].distance).toBe(5679); + expect(routesOf(result)[0].duration).toBe(1235); + expect(routesOf(result)[0].distance).toBe(5679); }); it('should delete geometry when geometries is set to none', () => { @@ -74,7 +87,7 @@ describe('cleanResponseData', () => { }; const result = cleanResponseData(mockInput, mockData); - expect(result.routes[0].geometry).toBeUndefined(); + expect(routesOf(result)[0].geometry).toBeUndefined(); }); it('should keep geometry when geometries is not none', () => { @@ -94,7 +107,7 @@ describe('cleanResponseData', () => { }; const result = cleanResponseData(geojsonInput, mockData); - expect(result.routes[0].geometry).toBe('someGeometryData'); + expect(routesOf(result)[0].geometry).toBe('someGeometryData'); }); it('should process route data correctly', () => { @@ -126,20 +139,20 @@ describe('cleanResponseData', () => { const result = cleanResponseData(mockInput, mockData); // Check if data is properly processed - expect(result.routes[0].weight_name).toBeUndefined(); - expect(result.routes[0].weight).toBeUndefined(); - expect(result.routes[0].leg_summaries).toEqual([ + expect(routesOf(result)[0].weight_name).toBeUndefined(); + expect(routesOf(result)[0].weight).toBeUndefined(); + expect(routesOf(result)[0].leg_summaries).toEqual([ 'First leg summary', 'Second leg summary' ]); - expect(result.routes[0].duration_typical).toBeUndefined(); - expect(result.routes[0].duration_under_typical_traffic_conditions).toBe( + expect(routesOf(result)[0].duration_typical).toBeUndefined(); + expect(routesOf(result)[0].duration_under_typical_traffic_conditions).toBe( 1300 ); - expect(result.routes[0].weight_typical).toBeUndefined(); - expect(result.routes[0].intersecting_admins).toEqual(['USA', 'CAN']); - expect(result.routes[0].num_legs).toBe(2); - expect(result.routes[0].legs).toBeUndefined(); + expect(routesOf(result)[0].weight_typical).toBeUndefined(); + expect(routesOf(result)[0].intersecting_admins).toEqual(['USA', 'CAN']); + expect(routesOf(result)[0].num_legs).toBe(2); + expect(routesOf(result)[0].legs).toBeUndefined(); }); it('should handle notifications and collect unique messages', () => { @@ -168,7 +181,7 @@ describe('cleanResponseData', () => { const result = cleanResponseData(mockInput, mockData); - expect(result.routes[0].notifications_summary).toEqual([ + expect(routesOf(result)[0].notifications_summary).toEqual([ 'Traffic ahead', 'Road closure', 'Construction zone' @@ -201,8 +214,8 @@ describe('cleanResponseData', () => { const result = cleanResponseData(mockInput, mockData); - expect(result.routes[0].incidents_summary).toHaveLength(1); - expect(result.routes[0].incidents_summary[0]).toEqual({ + expect(routesOf(result)[0].incidents_summary).toHaveLength(1); + expect(routesOf(result)[0].incidents_summary![0]).toEqual({ type: 'accident', end_time: '2023-05-01T12:00:00Z', long_description: 'Multiple vehicle collision', @@ -210,7 +223,9 @@ describe('cleanResponseData', () => { affected_road_names: ['Main St'], length: 500 }); - expect(result.routes[0].incidents_summary[0].extra_field).toBeUndefined(); + expect(routesOf(result)[0].incidents_summary![0]).not.toHaveProperty( + 'extra_field' + ); }); it('should collect voice instructions when within limits', () => { @@ -241,7 +256,7 @@ describe('cleanResponseData', () => { const result = cleanResponseData(mockInput, mockData); - expect(result.routes[0].instructions).toEqual([ + expect(routesOf(result)[0].instructions).toEqual([ 'Turn right in 100 meters', 'Turn right now', 'Continue straight for 500 meters' @@ -273,7 +288,7 @@ describe('cleanResponseData', () => { // With 6 steps and 2 instructions each, we'd have 12 instructions total // The function should exclude them since it's > 10 - expect(result.routes[0].instructions).toBeUndefined(); + expect(routesOf(result)[0].instructions).toBeUndefined(); }); it('should calculate congestion information correctly', () => { @@ -295,7 +310,7 @@ describe('cleanResponseData', () => { const result = cleanResponseData(mockInput, mockData); - expect(result.routes[0].congestion_information).toEqual({ + expect(routesOf(result)[0].congestion_information).toEqual({ length_severe: 100, length_heavy: 200, length_moderate: 300, @@ -328,6 +343,6 @@ describe('cleanResponseData', () => { // Total distance = 100 + 200 + 300 = 600 // Average m/s = 14000/600 = 23.33 // Average km/h = 23.33 * 3.6 = 84 (rounded) - expect(result.routes[0].average_speed_kph).toBe(84); + expect(routesOf(result)[0].average_speed_kph).toBe(84); }); }); diff --git a/test/tools/distance-tool/DistanceTool.test.ts b/test/tools/distance-tool/DistanceTool.test.ts index 22f7e94a..1f724de2 100644 --- a/test/tools/distance-tool/DistanceTool.test.ts +++ b/test/tools/distance-tool/DistanceTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { DistanceTool } from '../../../src/tools/distance-tool/DistanceTool.js'; +import type { DistanceOutput } from '../../../src/tools/distance-tool/DistanceTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('DistanceTool', () => { const tool = new DistanceTool(); @@ -13,14 +15,15 @@ describe('DistanceTool', () => { to: { longitude: -118.2437, latitude: 34.0522 }, // Los Angeles units: 'kilometers' }); + const data = structuredOf(result); - expect(result.structuredContent?.distance).toBeCloseTo(559, 0); - expect(result.structuredContent?.units).toBe('kilometers'); - expect(result.structuredContent?.from).toEqual({ + expect(data.distance).toBeCloseTo(559, 0); + expect(data.units).toBe('kilometers'); + expect(data.from).toEqual({ longitude: -122.4194, latitude: 37.7749 }); - expect(result.structuredContent?.to).toEqual({ + expect(data.to).toEqual({ longitude: -118.2437, latitude: 34.0522 }); @@ -32,9 +35,10 @@ describe('DistanceTool', () => { to: { longitude: -118.2437, latitude: 34.0522 }, // Los Angeles units: 'miles' }); + const data = structuredOf(result); - expect(result.structuredContent?.distance).toBeCloseTo(347, 0); - expect(result.structuredContent?.units).toBe('miles'); + expect(data.distance).toBeCloseTo(347, 0); + expect(data.units).toBe('miles'); }); it('should calculate distance in meters', async () => { @@ -43,9 +47,10 @@ describe('DistanceTool', () => { to: { longitude: -118.2437, latitude: 34.0522 }, // Los Angeles units: 'meters' }); + const data = structuredOf(result); - expect(result.structuredContent?.distance).toBeGreaterThan(559000); - expect(result.structuredContent?.units).toBe('meters'); + expect(data.distance).toBeGreaterThan(559000); + expect(data.units).toBe('meters'); }); it('should default to kilometers when units not specified', async () => { @@ -53,8 +58,9 @@ describe('DistanceTool', () => { from: { longitude: -122.4194, latitude: 37.7749 }, to: { longitude: -118.2437, latitude: 34.0522 } }); + const data = structuredOf(result); - expect(result.structuredContent?.units).toBe('kilometers'); + expect(data.units).toBe('kilometers'); }); it('should calculate short distances accurately', async () => { @@ -63,8 +69,9 @@ describe('DistanceTool', () => { to: { longitude: 0, latitude: 0.01 }, // ~1.11 km units: 'meters' }); + const data = structuredOf(result); - expect(result.structuredContent?.distance).toBeCloseTo(1112, 0); + expect(data.distance).toBeCloseTo(1112, 0); }); it('should handle cross-hemisphere distances', async () => { @@ -73,8 +80,9 @@ describe('DistanceTool', () => { to: { longitude: 139.691706, latitude: 35.689487 }, // Tokyo units: 'kilometers' }); + const data = structuredOf(result); - expect(result.structuredContent?.distance).toBeCloseTo(10850, 0); + expect(data.distance).toBeCloseTo(10850, 0); }); it('should calculate distance for same point as zero', async () => { @@ -83,8 +91,9 @@ describe('DistanceTool', () => { to: { longitude: 0, latitude: 0 }, units: 'kilometers' }); + const data = structuredOf(result); - expect(result.structuredContent?.distance).toBe(0); + expect(data.distance).toBe(0); }); it('should round distance to 3 decimal places', async () => { @@ -93,8 +102,9 @@ describe('DistanceTool', () => { to: { longitude: 0.001, latitude: 0.001 }, units: 'kilometers' }); + const data = structuredOf(result); - const distance = result.structuredContent?.distance as number; + const distance = data.distance as number; const decimalPlaces = (distance.toString().split('.')[1] || '').length; expect(decimalPlaces).toBeLessThanOrEqual(3); }); diff --git a/test/tools/input-schema-validation.test.ts b/test/tools/input-schema-validation.test.ts index 4c239f01..8a04432d 100644 --- a/test/tools/input-schema-validation.test.ts +++ b/test/tools/input-schema-validation.test.ts @@ -27,26 +27,24 @@ function detectTupleUsage(schema: z.ZodType): string[] { ); } - // Traverse nested schemas + // Traverse nested schemas. In zod 4, ZodArray's _def.type is the string + // 'array', not the item schema, so use the public accessors. if (node instanceof z.ZodArray) { - if (node._def.type) { - traverse(node._def.type, `${path}[item]`); - } + traverse(node.element as z.ZodType, `${path}[item]`); } else if (node instanceof z.ZodObject) { - const shape = node._def.shape; - for (const [key, value] of Object.entries(shape)) { + for (const [key, value] of Object.entries(node.shape)) { traverse(value as z.ZodType, path ? `${path}.${key}` : key); } } else if (node instanceof z.ZodUnion) { - node._def.options.forEach((option: z.ZodType, index: number) => { - traverse(option, `${path}[union_option_${index}]`); + node.options.forEach((option, index) => { + traverse(option as z.ZodType, `${path}[union_option_${index}]`); }); - } else if (node instanceof z.ZodOptional) { - traverse(node._def.innerType, path); - } else if (node instanceof z.ZodDefault) { - traverse(node._def.innerType, path); - } else if (node instanceof z.ZodNullable) { - traverse(node._def.innerType, path); + } else if ( + node instanceof z.ZodOptional || + node instanceof z.ZodDefault || + node instanceof z.ZodNullable + ) { + traverse(node.unwrap() as z.ZodType, path); } } @@ -77,12 +75,14 @@ describe('Input Schema Validation - No Tuples', () => { coordinates: z.tuple([z.number(), z.number()]), data: z.object({ nestedTuple: z.tuple([z.string(), z.boolean()]) - }) + }), + path: z.array(z.tuple([z.number(), z.number()])).optional() }); const tupleIssues = detectTupleUsage(schemaWithTuple); - expect(tupleIssues).toHaveLength(2); + expect(tupleIssues).toHaveLength(3); expect(tupleIssues[0]).toContain('coordinates: z.tuple() detected'); expect(tupleIssues[1]).toContain('data.nestedTuple: z.tuple() detected'); + expect(tupleIssues[2]).toContain('path[item]: z.tuple() detected'); }); }); diff --git a/test/tools/intersect-tool/IntersectTool.test.ts b/test/tools/intersect-tool/IntersectTool.test.ts index 9e74f4eb..89656629 100644 --- a/test/tools/intersect-tool/IntersectTool.test.ts +++ b/test/tools/intersect-tool/IntersectTool.test.ts @@ -3,7 +3,8 @@ import { describe, it, expect } from 'vitest'; import { IntersectTool } from '../../../src/tools/intersect-tool/IntersectTool.js'; -import { tokenFor } from '../../utils/tokenTestUtils.js'; +import type { IntersectOutput } from '../../../src/tools/intersect-tool/IntersectTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('IntersectTool', () => { const tool = new IntersectTool(); @@ -31,8 +32,8 @@ describe('IntersectTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.intersects).toBe(true); - expect(result.structuredContent?.geometry).not.toBeNull(); + expect(structuredOf(result).intersects).toBe(true); + expect(structuredOf(result).geometry).not.toBeNull(); }); it('returns intersects=false for non-overlapping polygons', async () => { @@ -58,8 +59,8 @@ describe('IntersectTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.intersects).toBe(false); - expect(result.structuredContent?.geometry).toBeNull(); + expect(structuredOf(result).intersects).toBe(false); + expect(structuredOf(result).geometry).toBeNull(); }); it('returns intersection for one polygon fully inside another', async () => { @@ -85,9 +86,9 @@ describe('IntersectTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.intersects).toBe(true); + expect(structuredOf(result).intersects).toBe(true); // Intersection should equal the smaller polygon - expect(result.structuredContent?.geometry).not.toBeNull(); + expect(structuredOf(result).geometry).not.toBeNull(); }); it('respects holes when computing intersection', async () => { @@ -126,7 +127,7 @@ describe('IntersectTool', () => { expect(result.isError).toBe(false); // The hole excludes that area from polygon1, so no shared area - expect(result.structuredContent?.intersects).toBe(false); + expect(structuredOf(result).intersects).toBe(false); }); it('text content describes intersection result', async () => { @@ -180,31 +181,26 @@ describe('IntersectTool', () => { }); it('stores a mapboxRender payload with input fills + intersection result', async () => { - const token = tokenFor('account-test-intersect'); - const result = await tool.run( - { - polygon1: [ - [ - [0, 0], - [2, 0], - [2, 2], - [0, 2], - [0, 0] - ] - ], - polygon2: [ - [ - [1, 1], - [3, 1], - [3, 3], - [1, 3], - [1, 1] - ] + const result = await tool.run({ + polygon1: [ + [ + [0, 0], + [2, 0], + [2, 2], + [0, 2], + [0, 0] ] - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - { http: { authInfo: { token } } } as any - ); + ], + polygon2: [ + [ + [1, 1], + [3, 1], + [3, 3], + [1, 3], + [1, 1] + ] + ] + }); expect(result.isError).toBe(false); const sc = result.structuredContent as { mapboxRender?: { ref?: string } }; diff --git a/test/tools/length-tool/LengthTool.test.ts b/test/tools/length-tool/LengthTool.test.ts index 86066ad4..d4da7d32 100644 --- a/test/tools/length-tool/LengthTool.test.ts +++ b/test/tools/length-tool/LengthTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { LengthTool } from '../../../src/tools/length-tool/LengthTool.js'; +import type { LengthOutput } from '../../../src/tools/length-tool/LengthTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('LengthTool', () => { const tool = new LengthTool(); @@ -17,9 +19,9 @@ describe('LengthTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.length).toBeCloseTo(111, 0); - expect(result.structuredContent?.units).toBe('kilometers'); - expect(result.structuredContent?.num_coordinates).toBe(2); + expect(structuredOf(result).length).toBeCloseTo(111, 0); + expect(structuredOf(result).units).toBe('kilometers'); + expect(structuredOf(result).num_coordinates).toBe(2); }); it('defaults to meters', async () => { @@ -31,8 +33,8 @@ describe('LengthTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('meters'); - expect(result.structuredContent?.length).toBeGreaterThan(0); + expect(structuredOf(result).units).toBe('meters'); + expect(structuredOf(result).length).toBeGreaterThan(0); }); it('measures in miles', async () => { @@ -45,8 +47,8 @@ describe('LengthTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.length).toBeCloseTo(347, 0); - expect(result.structuredContent?.units).toBe('miles'); + expect(structuredOf(result).length).toBeCloseTo(347, 0); + expect(structuredOf(result).units).toBe('miles'); }); it('accumulates length for multi-segment line', async () => { @@ -68,11 +70,11 @@ describe('LengthTool', () => { }); // Two-hop path through the same points should equal single hop - expect(twoHops.structuredContent?.length).toBeCloseTo( - singleHop.structuredContent?.length as number, + expect(structuredOf(twoHops).length).toBeCloseTo( + structuredOf(singleHop).length as number, 1 ); - expect(twoHops.structuredContent?.num_coordinates).toBe(3); + expect(structuredOf(twoHops).num_coordinates).toBe(3); }); it('rounds to 3 decimal places', async () => { @@ -85,7 +87,7 @@ describe('LengthTool', () => { }); expect(result.isError).toBe(false); - const len = result.structuredContent?.length as number; + const len = structuredOf(result).length as number; const decimals = (len.toString().split('.')[1] || '').length; expect(decimals).toBeLessThanOrEqual(3); }); diff --git a/test/tools/midpoint-tool/MidpointTool.test.ts b/test/tools/midpoint-tool/MidpointTool.test.ts index 95276a23..e5f5d6da 100644 --- a/test/tools/midpoint-tool/MidpointTool.test.ts +++ b/test/tools/midpoint-tool/MidpointTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { MidpointTool } from '../../../src/tools/midpoint-tool/MidpointTool.js'; +import type { MidpointOutput } from '../../../src/tools/midpoint-tool/MidpointTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('MidpointTool', () => { const tool = new MidpointTool(); @@ -14,13 +16,14 @@ describe('MidpointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.midpoint.longitude).toBeCloseTo(5, 1); - expect(result.structuredContent?.midpoint.latitude).toBeCloseTo(0, 1); - expect(result.structuredContent?.from).toEqual({ + const data = structuredOf(result); + expect(data.midpoint.longitude).toBeCloseTo(5, 1); + expect(data.midpoint.latitude).toBeCloseTo(0, 1); + expect(data.from).toEqual({ longitude: 0, latitude: 0 }); - expect(result.structuredContent?.to).toEqual({ + expect(data.to).toEqual({ longitude: 10, latitude: 0 }); @@ -33,8 +36,9 @@ describe('MidpointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.midpoint.longitude).toBeCloseTo(0, 1); - expect(result.structuredContent?.midpoint.latitude).toBeCloseTo(5, 1); + const data = structuredOf(result); + expect(data.midpoint.longitude).toBeCloseTo(0, 1); + expect(data.midpoint.latitude).toBeCloseTo(5, 1); }); it('should calculate midpoint for diagonal coordinates', async () => { @@ -44,8 +48,9 @@ describe('MidpointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.midpoint.longitude).toBeCloseTo(5, 1); - expect(result.structuredContent?.midpoint.latitude).toBeCloseTo(5, 1); + const data = structuredOf(result); + expect(data.midpoint.longitude).toBeCloseTo(5, 1); + expect(data.midpoint.latitude).toBeCloseTo(5, 1); }); it('should calculate midpoint between San Francisco and Los Angeles', async () => { @@ -56,10 +61,11 @@ describe('MidpointTool', () => { expect(result.isError).toBe(false); // Midpoint should be roughly between the two cities - expect(result.structuredContent?.midpoint.longitude).toBeGreaterThan(-121); - expect(result.structuredContent?.midpoint.longitude).toBeLessThan(-119); - expect(result.structuredContent?.midpoint.latitude).toBeGreaterThan(35); - expect(result.structuredContent?.midpoint.latitude).toBeLessThan(37); + const data = structuredOf(result); + expect(data.midpoint.longitude).toBeGreaterThan(-121); + expect(data.midpoint.longitude).toBeLessThan(-119); + expect(data.midpoint.latitude).toBeGreaterThan(35); + expect(data.midpoint.latitude).toBeLessThan(37); }); it('should calculate midpoint between New York and London', async () => { @@ -70,10 +76,11 @@ describe('MidpointTool', () => { expect(result.isError).toBe(false); // Midpoint should be somewhere over the Atlantic - expect(result.structuredContent?.midpoint.longitude).toBeGreaterThan(-74); - expect(result.structuredContent?.midpoint.longitude).toBeLessThan(0); - expect(result.structuredContent?.midpoint.latitude).toBeGreaterThan(40); - expect(result.structuredContent?.midpoint.latitude).toBeLessThan(60); + const data = structuredOf(result); + expect(data.midpoint.longitude).toBeGreaterThan(-74); + expect(data.midpoint.longitude).toBeLessThan(0); + expect(data.midpoint.latitude).toBeGreaterThan(40); + expect(data.midpoint.latitude).toBeLessThan(60); }); it('should handle same point as same midpoint', async () => { @@ -83,8 +90,9 @@ describe('MidpointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.midpoint.longitude).toBe(10); - expect(result.structuredContent?.midpoint.latitude).toBe(20); + const data = structuredOf(result); + expect(data.midpoint.longitude).toBe(10); + expect(data.midpoint.latitude).toBe(20); }); it('should handle midpoint across the antimeridian (180°)', async () => { @@ -95,10 +103,9 @@ describe('MidpointTool', () => { expect(result.isError).toBe(false); // Midpoint should be near 180° or -180° - expect( - Math.abs(result.structuredContent?.midpoint.longitude || 0) - ).toBeGreaterThan(175); - expect(result.structuredContent?.midpoint.latitude).toBeCloseTo(0, 1); + const data = structuredOf(result); + expect(Math.abs(data.midpoint.longitude)).toBeGreaterThan(175); + expect(data.midpoint.latitude).toBeCloseTo(0, 1); }); it('should handle midpoint near the north pole', async () => { @@ -108,8 +115,9 @@ describe('MidpointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.midpoint.latitude).toBeGreaterThan(84); - expect(result.structuredContent?.midpoint.latitude).toBeLessThan(90); + const data = structuredOf(result); + expect(data.midpoint.latitude).toBeGreaterThan(84); + expect(data.midpoint.latitude).toBeLessThan(90); }); it('should handle midpoint in southern hemisphere', async () => { @@ -120,10 +128,11 @@ describe('MidpointTool', () => { expect(result.isError).toBe(false); // Midpoint should be between the two points - expect(result.structuredContent?.midpoint.longitude).toBeGreaterThan(-60); - expect(result.structuredContent?.midpoint.longitude).toBeLessThan(-50); - expect(result.structuredContent?.midpoint.latitude).toBeGreaterThan(-40); - expect(result.structuredContent?.midpoint.latitude).toBeLessThan(-30); + const data = structuredOf(result); + expect(data.midpoint.longitude).toBeGreaterThan(-60); + expect(data.midpoint.longitude).toBeLessThan(-50); + expect(data.midpoint.latitude).toBeGreaterThan(-40); + expect(data.midpoint.latitude).toBeLessThan(-30); }); it('should handle midpoint across hemispheres (N-S)', async () => { @@ -133,8 +142,9 @@ describe('MidpointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.midpoint.longitude).toBeCloseTo(0, 1); - expect(result.structuredContent?.midpoint.latitude).toBeCloseTo(0, 1); + const data = structuredOf(result); + expect(data.midpoint.longitude).toBeCloseTo(0, 1); + expect(data.midpoint.latitude).toBeCloseTo(0, 1); }); it('should handle midpoint across hemispheres (E-W)', async () => { @@ -144,8 +154,9 @@ describe('MidpointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.midpoint.longitude).toBeCloseTo(0, 1); - expect(result.structuredContent?.midpoint.latitude).toBeCloseTo(0, 1); + const data = structuredOf(result); + expect(data.midpoint.longitude).toBeCloseTo(0, 1); + expect(data.midpoint.latitude).toBeCloseTo(0, 1); }); it('should round coordinates to 6 decimal places', async () => { @@ -155,8 +166,9 @@ describe('MidpointTool', () => { }); expect(result.isError).toBe(false); - const lon = result.structuredContent?.midpoint.longitude as number; - const lat = result.structuredContent?.midpoint.latitude as number; + const data = structuredOf(result); + const lon = data.midpoint.longitude; + const lat = data.midpoint.latitude; const lonDecimals = (lon.toString().split('.')[1] || '').length; const latDecimals = (lat.toString().split('.')[1] || '').length; expect(lonDecimals).toBeLessThanOrEqual(6); @@ -170,7 +182,8 @@ describe('MidpointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.midpoint.latitude).toBeCloseTo(0, 1); + const data = structuredOf(result); + expect(data.midpoint.latitude).toBeCloseTo(0, 1); }); it('should be commutative (same midpoint regardless of order)', async () => { @@ -184,14 +197,10 @@ describe('MidpointTool', () => { to: { longitude: 10, latitude: 20 } }); - expect(result1.structuredContent?.midpoint.longitude).toBeCloseTo( - result2.structuredContent?.midpoint.longitude || 0, - 5 - ); - expect(result1.structuredContent?.midpoint.latitude).toBeCloseTo( - result2.structuredContent?.midpoint.latitude || 0, - 5 - ); + const data1 = structuredOf(result1); + const data2 = structuredOf(result2); + expect(data1.midpoint.longitude).toBeCloseTo(data2.midpoint.longitude, 5); + expect(data1.midpoint.latitude).toBeCloseTo(data2.midpoint.latitude, 5); }); it('should handle short distances accurately', async () => { @@ -201,7 +210,8 @@ describe('MidpointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.midpoint.longitude).toBeCloseTo(0.0005, 4); - expect(result.structuredContent?.midpoint.latitude).toBeCloseTo(0.0005, 4); + const data = structuredOf(result); + expect(data.midpoint.longitude).toBeCloseTo(0.0005, 4); + expect(data.midpoint.latitude).toBeCloseTo(0.0005, 4); }); }); diff --git a/test/tools/nearest-point-on-line-tool/NearestPointOnLineTool.test.ts b/test/tools/nearest-point-on-line-tool/NearestPointOnLineTool.test.ts index a5a766ed..84579a05 100644 --- a/test/tools/nearest-point-on-line-tool/NearestPointOnLineTool.test.ts +++ b/test/tools/nearest-point-on-line-tool/NearestPointOnLineTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { NearestPointOnLineTool } from '../../../src/tools/nearest-point-on-line-tool/NearestPointOnLineTool.js'; +import type { NearestPointOnLineOutput } from '../../../src/tools/nearest-point-on-line-tool/NearestPointOnLineTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('NearestPointOnLineTool', () => { const tool = new NearestPointOnLineTool(); @@ -19,9 +21,15 @@ describe('NearestPointOnLineTool', () => { expect(result.isError).toBe(false); // Nearest point on the line should be directly below the input point - expect(result.structuredContent?.nearest.longitude).toBeCloseTo(0.5, 1); - expect(result.structuredContent?.nearest.latitude).toBeCloseTo(0, 1); - expect(result.structuredContent?.distance).toBeGreaterThan(0); + expect( + structuredOf(result).nearest.longitude + ).toBeCloseTo(0.5, 1); + expect( + structuredOf(result).nearest.latitude + ).toBeCloseTo(0, 1); + expect( + structuredOf(result).distance + ).toBeGreaterThan(0); }); it('returns zero distance when point is on the line', async () => { @@ -35,7 +43,10 @@ describe('NearestPointOnLineTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.distance).toBeCloseTo(0, 1); + expect(structuredOf(result).distance).toBeCloseTo( + 0, + 1 + ); }); it('defaults to kilometers units', async () => { @@ -48,7 +59,9 @@ describe('NearestPointOnLineTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('kilometers'); + expect(structuredOf(result).units).toBe( + 'kilometers' + ); }); it('returns distance in meters when specified', async () => { @@ -70,11 +83,11 @@ describe('NearestPointOnLineTool', () => { units: 'meters' }); - expect(m.structuredContent?.distance).toBeCloseTo( - (km.structuredContent?.distance as number) * 1000, + expect(structuredOf(m).distance).toBeCloseTo( + (structuredOf(km).distance as number) * 1000, 0 ); - expect(m.structuredContent?.units).toBe('meters'); + expect(structuredOf(m).units).toBe('meters'); }); it('returns location along the line', async () => { @@ -90,7 +103,10 @@ describe('NearestPointOnLineTool', () => { expect(result.isError).toBe(false); // location should be roughly half the line length const lineLength = 111; // ~111km for 1 degree at equator - expect(result.structuredContent?.location).toBeCloseTo(lineLength / 2, -1); + expect(structuredOf(result).location).toBeCloseTo( + lineLength / 2, + -1 + ); }); it('works with a multi-segment line', async () => { @@ -106,8 +122,12 @@ describe('NearestPointOnLineTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.nearest).toBeDefined(); - expect(result.structuredContent?.distance).toBeGreaterThanOrEqual(0); + expect( + structuredOf(result).nearest + ).toBeDefined(); + expect( + structuredOf(result).distance + ).toBeGreaterThanOrEqual(0); }); it('rejects a line with fewer than two coordinates', async () => { diff --git a/test/tools/nearest-point-tool/NearestPointTool.test.ts b/test/tools/nearest-point-tool/NearestPointTool.test.ts index 2f20f5fc..85c93bf7 100644 --- a/test/tools/nearest-point-tool/NearestPointTool.test.ts +++ b/test/tools/nearest-point-tool/NearestPointTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { NearestPointTool } from '../../../src/tools/nearest-point-tool/NearestPointTool.js'; +import type { NearestPointOutput } from '../../../src/tools/nearest-point-tool/NearestPointTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('NearestPointTool', () => { const tool = new NearestPointTool(); @@ -18,11 +20,11 @@ describe('NearestPointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.nearest).toMatchObject({ + expect(structuredOf(result).nearest).toMatchObject({ longitude: 1, latitude: 0 }); - expect(result.structuredContent?.index).toBe(0); + expect(structuredOf(result).index).toBe(0); }); it('returns correct index for nearest point', async () => { @@ -36,8 +38,8 @@ describe('NearestPointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.index).toBe(2); - expect(result.structuredContent?.nearest).toMatchObject({ + expect(structuredOf(result).index).toBe(2); + expect(structuredOf(result).nearest).toMatchObject({ longitude: 0.5, latitude: 0 }); @@ -50,8 +52,10 @@ describe('NearestPointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('meters'); - expect(result.structuredContent?.distance).toBeGreaterThan(0); + expect(structuredOf(result).units).toBe('meters'); + expect(structuredOf(result).distance).toBeGreaterThan( + 0 + ); }); it('returns distance in kilometers when specified', async () => { @@ -65,10 +69,13 @@ describe('NearestPointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('kilometers'); + expect(structuredOf(result).units).toBe('kilometers'); // LA is closer to SF than NY - expect(result.structuredContent?.index).toBe(0); - expect(result.structuredContent?.distance).toBeCloseTo(559, 0); + expect(structuredOf(result).index).toBe(0); + expect(structuredOf(result).distance).toBeCloseTo( + 559, + 0 + ); }); it('handles a single candidate point', async () => { @@ -78,7 +85,7 @@ describe('NearestPointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.index).toBe(0); + expect(structuredOf(result).index).toBe(0); }); it('rejects empty points array', async () => { diff --git a/test/tools/optimization-tool/OptimizationTool.test.ts b/test/tools/optimization-tool/OptimizationTool.test.ts index 60d46823..75110782 100644 --- a/test/tools/optimization-tool/OptimizationTool.test.ts +++ b/test/tools/optimization-tool/OptimizationTool.test.ts @@ -9,6 +9,7 @@ import { setupHttpRequest } from '../../utils/httpPipelineUtils.js'; import { OptimizationTool } from '../../../src/tools/optimization-tool/OptimizationTool.js'; import { tokenFor } from '../../utils/tokenTestUtils.js'; +import { textOf } from '../../utils/resultTestUtils.js'; // Sample V1 API response const sampleV1Response = { @@ -178,9 +179,7 @@ describe('OptimizationTool V1 API', () => { }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain( - 'At least 2 coordinates are required' - ); + expect(textOf(result)).toContain('At least 2 coordinates are required'); }); it('should validate maximum coordinates (12)', async () => { @@ -199,7 +198,7 @@ describe('OptimizationTool V1 API', () => { const result = await tool.run({ coordinates: coords }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Maximum 12 coordinates allowed'); + expect(textOf(result)).toContain('Maximum 12 coordinates allowed'); }); it('should handle API errors gracefully', async () => { @@ -222,8 +221,8 @@ describe('OptimizationTool V1 API', () => { }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Optimization API error'); - expect(result.content[0].text).toContain('Invalid coordinates'); + expect(textOf(result)).toContain('Optimization API error'); + expect(textOf(result)).toContain('Invalid coordinates'); }); it('should handle API error codes in response', async () => { @@ -245,7 +244,7 @@ describe('OptimizationTool V1 API', () => { }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('No route found'); + expect(textOf(result)).toContain('No route found'); }); it('should format output text with duration and distance', async () => { @@ -265,7 +264,7 @@ describe('OptimizationTool V1 API', () => { }); expect(result.isError).toBe(false); - const text = result.content[0].text; + const text = textOf(result); expect(text).toContain('Optimized route'); expect(text).toContain('3 waypoints'); expect(text).toContain('minutes'); @@ -301,7 +300,7 @@ describe('OptimizationTool V1 API', () => { /^mapbox:\/\/selffetch\/optimization\?data=/ ); - const text = (result.content[0] as { text: string }).text; + const text = textOf(result); expect(text).toContain('render_map_tool'); expect(text).toContain(sc.mapboxRender!.ref!); diff --git a/test/tools/points-within-polygon-tool/PointsWithinPolygonTool.test.ts b/test/tools/points-within-polygon-tool/PointsWithinPolygonTool.test.ts index a694b9d1..278847fc 100644 --- a/test/tools/points-within-polygon-tool/PointsWithinPolygonTool.test.ts +++ b/test/tools/points-within-polygon-tool/PointsWithinPolygonTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { PointsWithinPolygonTool } from '../../../src/tools/points-within-polygon-tool/PointsWithinPolygonTool.js'; +import type { PointsWithinPolygonOutput } from '../../../src/tools/points-within-polygon-tool/PointsWithinPolygonTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; // Simple unit square polygon [0,0] → [1,1] const SQUARE = [ @@ -26,11 +28,12 @@ describe('PointsWithinPolygonTool', () => { ], polygon: SQUARE }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.count).toBe(1); - expect(result.structuredContent?.total).toBe(2); - expect(result.structuredContent?.points_within[0]).toMatchObject({ + expect(data.count).toBe(1); + expect(data.total).toBe(2); + expect(data.points_within[0]).toMatchObject({ longitude: 0.5, latitude: 0.5 }); @@ -45,10 +48,11 @@ describe('PointsWithinPolygonTool', () => { ], polygon: SQUARE }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.count).toBe(3); - expect(result.structuredContent?.total).toBe(3); + expect(data.count).toBe(3); + expect(data.total).toBe(3); }); it('returns empty when no points are inside', async () => { @@ -59,11 +63,12 @@ describe('PointsWithinPolygonTool', () => { ], polygon: SQUARE }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.count).toBe(0); - expect(result.structuredContent?.total).toBe(2); - expect(result.structuredContent?.points_within).toHaveLength(0); + expect(data.count).toBe(0); + expect(data.total).toBe(2); + expect(data.points_within).toHaveLength(0); }); it('handles a single point inside', async () => { @@ -71,9 +76,10 @@ describe('PointsWithinPolygonTool', () => { points: [{ longitude: 0.5, latitude: 0.5 }], polygon: SQUARE }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.count).toBe(1); + expect(data.count).toBe(1); }); it('works with a real-world polygon', async () => { @@ -95,9 +101,10 @@ describe('PointsWithinPolygonTool', () => { ], polygon: manhattan }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.count).toBe(1); + expect(data.count).toBe(1); }); it('excludes points that fall inside a hole', async () => { @@ -127,14 +134,13 @@ describe('PointsWithinPolygonTool', () => { ], polygon: polygonWithHole }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.count).toBe(2); - expect(result.structuredContent?.total).toBe(3); + expect(data.count).toBe(2); + expect(data.total).toBe(3); // The hole point (2,2) should not be returned - const lngs = result.structuredContent?.points_within.map( - (p) => p.longitude - ); + const lngs = data.points_within.map((p) => p.longitude); expect(lngs).not.toContain(2); }); diff --git a/test/tools/simplify-tool/SimplifyTool.test.ts b/test/tools/simplify-tool/SimplifyTool.test.ts index 33635278..37509150 100644 --- a/test/tools/simplify-tool/SimplifyTool.test.ts +++ b/test/tools/simplify-tool/SimplifyTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { SimplifyTool } from '../../../src/tools/simplify-tool/SimplifyTool.js'; +import type { SimplifyOutput } from '../../../src/tools/simplify-tool/SimplifyTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('SimplifyTool', () => { const tool = new SimplifyTool(); @@ -26,11 +28,12 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.originalVertexCount).toBe(11); - expect(result.structuredContent?.simplifiedVertexCount).toBeLessThan(11); - expect(result.structuredContent?.reductionPercentage).toBeGreaterThan(0); - expect(result.structuredContent?.simplified).toBeDefined(); - expect(Array.isArray(result.structuredContent?.simplified)).toBe(true); + const data = structuredOf(result); + expect(data.originalVertexCount).toBe(11); + expect(data.simplifiedVertexCount).toBeLessThan(11); + expect(data.reductionPercentage).toBeGreaterThan(0); + expect(data.simplified).toBeDefined(); + expect(Array.isArray(data.simplified)).toBe(true); }); it('should simplify a linestring with custom tolerance', async () => { @@ -49,8 +52,9 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.originalVertexCount).toBe(6); - expect(result.structuredContent?.simplifiedVertexCount).toBeLessThan(6); + const data = structuredOf(result); + expect(data.originalVertexCount).toBe(6); + expect(data.simplifiedVertexCount).toBeLessThan(6); }); it('should simplify more aggressively with higher tolerance', async () => { @@ -77,10 +81,10 @@ describe('SimplifyTool', () => { expect(resultLow.isError).toBe(false); expect(resultHigh.isError).toBe(false); // Higher tolerance should result in fewer vertices - expect( - resultHigh.structuredContent?.simplifiedVertexCount - ).toBeLessThanOrEqual( - resultLow.structuredContent?.simplifiedVertexCount || Infinity + const dataHigh = structuredOf(resultHigh); + const dataLow = structuredOf(resultLow); + expect(dataHigh.simplifiedVertexCount).toBeLessThanOrEqual( + dataLow.simplifiedVertexCount ); }); @@ -103,11 +107,12 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.originalVertexCount).toBe(9); - expect(result.structuredContent?.simplifiedVertexCount).toBeLessThan(9); - expect(result.structuredContent?.simplified).toBeDefined(); + const data = structuredOf(result); + expect(data.originalVertexCount).toBe(9); + expect(data.simplifiedVertexCount).toBeLessThan(9); + expect(data.simplified).toBeDefined(); // Result should still be a polygon (array of array of coordinates) - expect(Array.isArray(result.structuredContent?.simplified)).toBe(true); + expect(Array.isArray(data.simplified)).toBe(true); }); it('should use high quality mode', async () => { @@ -126,12 +131,9 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.simplifiedVertexCount).toBeLessThanOrEqual( - 5 - ); - expect( - result.structuredContent?.simplifiedVertexCount - ).toBeGreaterThanOrEqual(2); + const data = structuredOf(result); + expect(data.simplifiedVertexCount).toBeLessThanOrEqual(5); + expect(data.simplifiedVertexCount).toBeGreaterThanOrEqual(2); }); it('should handle straight line (minimal simplification needed)', async () => { @@ -147,10 +149,11 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.originalVertexCount).toBe(5); + const data = structuredOf(result); + expect(data.originalVertexCount).toBe(5); // Straight line should simplify to just endpoints - expect(result.structuredContent?.simplifiedVertexCount).toBe(2); - expect(result.structuredContent?.reductionPercentage).toBe(60); + expect(data.simplifiedVertexCount).toBe(2); + expect(data.reductionPercentage).toBe(60); }); it('should handle complex curved line', async () => { @@ -166,9 +169,10 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.originalVertexCount).toBe(21); - expect(result.structuredContent?.simplifiedVertexCount).toBeLessThan(21); - expect(result.structuredContent?.reductionPercentage).toBeGreaterThan(0); + const data = structuredOf(result); + expect(data.originalVertexCount).toBe(21); + expect(data.simplifiedVertexCount).toBeLessThan(21); + expect(data.reductionPercentage).toBeGreaterThan(0); }); it('should calculate reduction percentage correctly', async () => { @@ -184,14 +188,13 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - const original = result.structuredContent?.originalVertexCount || 0; - const simplified = result.structuredContent?.simplifiedVertexCount || 0; + const data = structuredOf(result); + const original = data.originalVertexCount; + const simplified = data.simplifiedVertexCount; const expectedReduction = Math.round( ((original - simplified) / original) * 100 ); - expect(result.structuredContent?.reductionPercentage).toBe( - expectedReduction - ); + expect(data.reductionPercentage).toBe(expectedReduction); }); it('should preserve first and last points', async () => { @@ -209,10 +212,8 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - const simplified = result.structuredContent?.simplified as [ - number, - number - ][]; + const data = structuredOf(result); + const simplified = data.simplified as [number, number][]; expect(simplified[0]).toEqual([0, 0]); expect(simplified[simplified.length - 1]).toEqual([4, 0]); }); @@ -231,7 +232,8 @@ describe('SimplifyTool', () => { expect(result.isError).toBe(false); // Very small tolerance should preserve most vertices - expect(result.structuredContent?.simplifiedVertexCount).toBeGreaterThan(2); + const data = structuredOf(result); + expect(data.simplifiedVertexCount).toBeGreaterThan(2); }); it('should handle polygon with hole', async () => { @@ -267,8 +269,9 @@ describe('SimplifyTool', () => { expect(result.isError).toBe(false); // Original: 9 + 9 = 18 vertices - expect(result.structuredContent?.originalVertexCount).toBe(18); - expect(result.structuredContent?.simplifiedVertexCount).toBeLessThan(18); + const data = structuredOf(result); + expect(data.originalVertexCount).toBe(18); + expect(data.simplifiedVertexCount).toBeLessThan(18); }); it('should handle real world coordinates', async () => { @@ -284,10 +287,9 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.originalVertexCount).toBe(5); - expect(result.structuredContent?.simplifiedVertexCount).toBeLessThanOrEqual( - 5 - ); + const data = structuredOf(result); + expect(data.originalVertexCount).toBe(5); + expect(data.simplifiedVertexCount).toBeLessThanOrEqual(5); }); it('should handle square polygon (should simplify to 5 points including closure)', async () => { @@ -305,10 +307,11 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.originalVertexCount).toBe(5); + const data = structuredOf(result); + expect(data.originalVertexCount).toBe(5); // Perfect square should not simplify much (all corners are significant) - expect(result.structuredContent?.simplifiedVertexCount).toBe(5); - expect(result.structuredContent?.reductionPercentage).toBe(0); + expect(data.simplifiedVertexCount).toBe(5); + expect(data.reductionPercentage).toBe(0); }); it('should handle triangle (minimal vertices, no simplification)', async () => { @@ -325,10 +328,11 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.originalVertexCount).toBe(4); + const data = structuredOf(result); + expect(data.originalVertexCount).toBe(4); // Triangle should not simplify (all vertices are needed) - expect(result.structuredContent?.simplifiedVertexCount).toBe(4); - expect(result.structuredContent?.reductionPercentage).toBe(0); + expect(data.simplifiedVertexCount).toBe(4); + expect(data.reductionPercentage).toBe(0); }); it('should handle negative coordinates', async () => { @@ -344,7 +348,8 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.simplifiedVertexCount).toBeLessThan(5); + const data = structuredOf(result); + expect(data.simplifiedVertexCount).toBeLessThan(5); }); it('should return simplified geometry in same format as input', async () => { @@ -358,10 +363,9 @@ describe('SimplifyTool', () => { }); expect(lineResult.isError).toBe(false); - expect(Array.isArray(lineResult.structuredContent?.simplified)).toBe(true); - expect(Array.isArray(lineResult.structuredContent?.simplified[0])).toBe( - true - ); + const lineData = structuredOf(lineResult); + expect(Array.isArray(lineData.simplified)).toBe(true); + expect(Array.isArray(lineData.simplified[0])).toBe(true); // Test polygon const polyResult = await tool.run({ @@ -377,9 +381,8 @@ describe('SimplifyTool', () => { }); expect(polyResult.isError).toBe(false); - expect(Array.isArray(polyResult.structuredContent?.simplified)).toBe(true); - expect(Array.isArray(polyResult.structuredContent?.simplified[0])).toBe( - true - ); + const polyData = structuredOf(polyResult); + expect(Array.isArray(polyData.simplified)).toBe(true); + expect(Array.isArray(polyData.simplified[0])).toBe(true); }); }); diff --git a/test/tools/structured-content.test.ts b/test/tools/structured-content.test.ts index 75c4dfea..ad98094d 100644 --- a/test/tools/structured-content.test.ts +++ b/test/tools/structured-content.test.ts @@ -3,8 +3,10 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { MapboxApiBasedTool } from '../../src/tools/MapboxApiBasedTool.js'; -import type { OutputSchema } from '../../src/tools/MapboxApiBasedTool.output.schema.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import { z } from 'zod'; +import { setupHttpRequest } from '../utils/httpPipelineUtils.js'; +import { structuredOf } from '../utils/resultTestUtils.js'; const TestInputSchema = z.object({ test: z.string() @@ -22,13 +24,16 @@ class TestTool extends MapboxApiBasedTool { }; constructor() { - super({ inputSchema: TestInputSchema }); + super({ + inputSchema: TestInputSchema, + httpRequest: setupHttpRequest().httpRequest + }); } protected async execute( input: z.infer, _accessToken: string - ): Promise> { + ): Promise { // Return different types based on input if (input.test === 'object') { const data = { @@ -90,7 +95,7 @@ describe('MapboxApiBasedTool Structured Content', () => { 'This is structured content' ); expect(result.structuredContent).toHaveProperty('data'); - expect(result.structuredContent?.data).toEqual({ + expect(structuredOf<{ data: unknown }>(result).data).toEqual({ value: 42, success: true }); diff --git a/test/tools/union-tool/UnionTool.test.ts b/test/tools/union-tool/UnionTool.test.ts index 10339619..12c6a88d 100644 --- a/test/tools/union-tool/UnionTool.test.ts +++ b/test/tools/union-tool/UnionTool.test.ts @@ -3,7 +3,8 @@ import { describe, it, expect } from 'vitest'; import { UnionTool } from '../../../src/tools/union-tool/UnionTool.js'; -import { tokenFor } from '../../utils/tokenTestUtils.js'; +import type { UnionOutput } from '../../../src/tools/union-tool/UnionTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('UnionTool', () => { const tool = new UnionTool(); @@ -35,8 +36,8 @@ describe('UnionTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.type).toBe('Polygon'); - expect(result.structuredContent?.geometry).not.toBeNull(); + expect(structuredOf(result).type).toBe('Polygon'); + expect(structuredOf(result).geometry).not.toBeNull(); }); it('returns MultiPolygon for non-overlapping polygons', async () => { @@ -66,7 +67,7 @@ describe('UnionTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.type).toBe('MultiPolygon'); + expect(structuredOf(result).type).toBe('MultiPolygon'); }); it('merges three polygons', async () => { @@ -103,7 +104,7 @@ describe('UnionTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.geometry).not.toBeNull(); + expect(structuredOf(result).geometry).not.toBeNull(); }); it('handles polygons with holes', async () => { @@ -141,8 +142,8 @@ describe('UnionTool', () => { expect(result.isError).toBe(false); // Union should fill the hole, resulting in a solid polygon with no holes - expect(result.structuredContent?.type).toBe('Polygon'); - expect(result.structuredContent?.geometry).not.toBeNull(); + expect(structuredOf(result).type).toBe('Polygon'); + expect(structuredOf(result).geometry).not.toBeNull(); }); it('rejects fewer than two polygons', async () => { @@ -164,33 +165,28 @@ describe('UnionTool', () => { }); it('stores a mapboxRender payload with input fills + result fill', async () => { - const token = tokenFor('account-test-union'); - const result = await tool.run( - { - polygons: [ + const result = await tool.run({ + polygons: [ + [ [ - [ - [0, 0], - [2, 0], - [2, 2], - [0, 2], - [0, 0] - ] - ], + [0, 0], + [2, 0], + [2, 2], + [0, 2], + [0, 0] + ] + ], + [ [ - [ - [1, 1], - [3, 1], - [3, 3], - [1, 3], - [1, 1] - ] + [1, 1], + [3, 1], + [3, 3], + [1, 3], + [1, 1] ] ] - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - { http: { authInfo: { token } } } as any - ); + ] + }); expect(result.isError).toBe(false); const sc = result.structuredContent as { mapboxRender?: { ref?: string } }; diff --git a/test/utils/directionsRef.test.ts b/test/utils/directionsRef.test.ts index 6dc2f6d6..397c0614 100644 --- a/test/utils/directionsRef.test.ts +++ b/test/utils/directionsRef.test.ts @@ -7,6 +7,7 @@ import { describe, it, expect, vi } from 'vitest'; import { DirectionsTool } from '../../src/tools/directions-tool/DirectionsTool.js'; import { directionsRequestFromRef } from '../../src/utils/directionsRef.js'; import { buildSelfFetchRef } from '../../src/utils/selfFetchRef.js'; +import type { HttpRequest } from '../../src/utils/types.js'; const coordinates = [ { longitude: -122.4194, latitude: 37.7749 }, @@ -35,7 +36,8 @@ function longRoute() { async function runDirections(input: Record) { const urls: string[] = []; - const httpRequest = vi.fn(async (url: string) => { + const httpRequest = vi.fn(async (input) => { + const url = String(input); urls.push(url); return new Response(JSON.stringify(longRoute()), { headers: { 'content-type': 'application/json' } diff --git a/test/utils/mapboxPublicToken.test.ts b/test/utils/mapboxPublicToken.test.ts index 770a4543..4ca977ca 100644 --- a/test/utils/mapboxPublicToken.test.ts +++ b/test/utils/mapboxPublicToken.test.ts @@ -14,6 +14,7 @@ import { resolveMapboxPublicToken, __resetMapboxPublicTokenCache } from '../../src/utils/mapboxPublicToken.js'; +import type { HttpRequest } from '../../src/utils/types.js'; const API_ENDPOINT = 'https://api.mapbox.com/'; const DEFAULT_PK = 'pk.eyJ1IjoidGVzdHVzZXIifQ.fake-public-token'; @@ -60,7 +61,8 @@ describe('resolveMapboxPublicToken', () => { }); it('resolves the default public token for a tk.* (OAuth) bearer', async () => { - const httpRequest = vi.fn(async (url: string) => { + const httpRequest = vi.fn(async (input) => { + const url = String(input); if (url.includes('tokens/v2/')) return makeOkJson(fakeTokenList) as Response; throw new Error(`Unexpected URL: ${url}`); @@ -79,7 +81,8 @@ describe('resolveMapboxPublicToken', () => { }); it('resolves the default public token for an sk.* bearer (regression)', async () => { - const httpRequest = vi.fn(async (url: string) => { + const httpRequest = vi.fn(async (input) => { + const url = String(input); if (url.includes('tokens/v2/')) return makeOkJson(fakeTokenList) as Response; throw new Error(`Unexpected URL: ${url}`); @@ -145,7 +148,8 @@ describe('resolveMapboxPublicToken', () => { }); it('caches the resolved token per-user and skips a second API call for the same user', async () => { - const httpRequest = vi.fn(async (url: string) => { + const httpRequest = vi.fn(async (input) => { + const url = String(input); if (url.includes('tokens/v2/')) return makeOkJson(fakeTokenList) as Response; throw new Error(`Unexpected URL: ${url}`); @@ -169,7 +173,8 @@ describe('resolveMapboxPublicToken', () => { it("never returns one user's cached token to a different user (cross-tenant isolation)", async () => { const otherUserPk = 'pk.eyJ1Ijoib3RoZXJ1c2VyIn0.other-users-token'; - const httpRequest = vi.fn(async (url: string) => { + const httpRequest = vi.fn(async (input) => { + const url = String(input); if (url.includes('tokens/v2/testuser')) return makeOkJson(fakeTokenList) as Response; if (url.includes('tokens/v2/otheruser')) diff --git a/test/utils/resultTestUtils.ts b/test/utils/resultTestUtils.ts new file mode 100644 index 00000000..2f94266d --- /dev/null +++ b/test/utils/resultTestUtils.ts @@ -0,0 +1,41 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import type { + CallToolResult, + ReadResourceResult +} from '@modelcontextprotocol/server'; + +/** + * A tool result's `structuredContent`, typed as the tool's output type (for + * example `structuredOf(result)`). The SDK types it as an + * open object, so tests otherwise can't reach the fields they check. + */ +export function structuredOf(result: CallToolResult): T { + return result.structuredContent as T; +} + +/** + * 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; +} diff --git a/tsconfig.test.json b/tsconfig.test.json index 64c3b90e..aeec02cf 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -1,9 +1,10 @@ -{ +{ "extends": "./tsconfig.base.json", "compilerOptions": { "skipLibCheck": true, + "noEmit": true, "lib": ["ESNext"], "resolveJsonModule": true }, "include": ["test", "src"] -} \ No newline at end of file +}