From 96c888133e683bbbaaf933ad53a62d56b26e0fb6 Mon Sep 17 00:00:00 2001 From: Kiko Beats <2096101+Kikobeats@users.noreply.github.com> Date: Sat, 12 Sep 2026 17:23:19 +0200 Subject: [PATCH] test(mcp): opt-in live output schema conformance tests Invoke real tools against the free Microlink endpoint and validate structuredContent against each tool's declared outputSchema. Catches contract drift between schemas and the real API that stubbed unit tests cannot (the null media/content/embed cases all came from live behavior). Skipped unless LIVE=1, so CI and local runs are unaffected. Run them with pnpm test:live. Signed-off-by: Kiko Beats <2096101+Kikobeats@users.noreply.github.com> --- packages/mcp/package.json | 3 +- packages/mcp/test/live.test.js | 81 ++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 packages/mcp/test/live.test.js diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 190d885..6f032bd 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -59,7 +59,8 @@ "scripts": { "postinstall": "node scripts/postinstall.js", "start": "node src/index.js", - "test": "node --test test/*.test.js" + "test": "node --test test/*.test.js", + "test:live": "LIVE=1 node --test test/live.test.js" }, "license": "MIT", "publishConfig": { diff --git a/packages/mcp/test/live.test.js b/packages/mcp/test/live.test.js new file mode 100644 index 0000000..d8523bc --- /dev/null +++ b/packages/mcp/test/live.test.js @@ -0,0 +1,81 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { z } from 'zod' + +import { tools } from '../src/tools/index.js' + +// Opt-in live conformance tests: invoke real tools against the free Microlink +// endpoint and validate structuredContent against each tool's declared +// outputSchema. They catch contract drift between the schemas and the real +// API that unit tests with stubbed responses cannot. +// +// Run with: LIVE=1 pnpm test (or: pnpm test:live) +// Without LIVE=1 every test in this file is skipped, so CI is unaffected. +const LIVE = process.env.LIVE === '1' +const run = { skip: !LIVE, timeout: 120000 } + +const configs = {} +const handlers = {} +tools({ + registerTool: (name, config, handler) => { + configs[name] = config + handlers[name] = handler + } +}) + +async function invoke (name, args) { + const res = await handlers[name](args, {}) + assert.equal(res.isError, false, `${name} failed: ${res.content?.[0]?.text}`) + const schema = z.object(configs[name].outputSchema) + const parsed = schema.safeParse(res.structuredContent) + assert.ok(parsed.success, `${name}: ${parsed.error?.message}`) + return res.structuredContent.data +} + +test('live: metadata matches its output schema', run, async () => { + const data = await invoke('microlink_metadata', { + url: 'https://example.com' + }) + assert.equal(typeof data, 'object') +}) + +test('live: markdown matches its output schema', run, async () => { + await invoke('microlink_markdown', { url: 'https://example.com' }) +}) + +test('live: logo matches its output schema (asset or null)', run, async () => { + await invoke('microlink_logo', { url: 'https://github.com' }) +}) + +test('live: links matches its output schema', run, async () => { + const data = await invoke('microlink_links', { url: 'https://example.com' }) + assert.ok(Array.isArray(data)) +}) + +test('live: screenshot matches its output schema', run, async () => { + const data = await invoke('microlink_screenshot', { + url: 'https://example.com' + }) + assert.equal(typeof data.url, 'string') +}) + +test('live: embed matches its output schema (embed or null)', run, async () => { + await invoke('microlink_embed', { + url: 'https://www.youtube.com/watch?v=aqz-KE-bpKQ' + }) +}) + +test('live: technologies matches its output schema', run, async () => { + const data = await invoke('microlink_technologies', { + url: 'https://example.com' + }) + assert.ok(Array.isArray(data)) +}) + +test('live: extract matches its output schema', run, async () => { + await invoke('microlink_extract', { + url: 'https://example.com', + data: { title: { selector: 'h1' } } + }) +})