UNPKG

@iflow-mcp/ansvar-systems-german-law-mcp

Version:

German legal research MCP server with statutes, case law, and preparatory works from gesetze-im-internet.de

199 lines (198 loc) • 9.3 kB
import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; import { LawMcpShell } from "../../src/shell/shell.js"; import { BUILTIN_ADAPTERS } from "../../src/adapters/index.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function normalizeText(text) { return text.replace(/\s+/g, " ").replace(/[\r\n]+/g, " ").trim().toLowerCase(); } function sha256(text) { return createHash("sha256").update(normalizeText(text)).digest("hex"); } function extractCitationUrls(data) { const urls = []; const text = JSON.stringify(data); const urlRegex = /https?:\/\/[^\s"'<>]+/g; let match; while ((match = urlRegex.exec(text)) !== null) { urls.push(match[0]); } return urls; } async function fetchWithTimeout(url, timeoutMs = 10_000) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { return await fetch(url, { signal: controller.signal }); } finally { clearTimeout(timer); } } function stringifyData(data) { if (typeof data === "string") return data; return JSON.stringify(data, null, 0) ?? ""; } // --------------------------------------------------------------------------- // Load fixtures & create shell // --------------------------------------------------------------------------- const fixturesPath = join(process.cwd(), "fixtures", "golden-tests.json"); const fixtureContent = readFileSync(fixturesPath, "utf-8"); const fixture = JSON.parse(fixtureContent); const shell = LawMcpShell.fromAdapters(BUILTIN_ADAPTERS); const isNightly = process.env["CONTRACT_MODE"] === "nightly"; // --------------------------------------------------------------------------- // Contract test runner // --------------------------------------------------------------------------- describe(`Contract tests: ${fixture.mcp_name}`, () => { for (const test of fixture.tests) { describe(`[${test.id}] ${test.description}`, () => { let result; it("runs without throwing", async () => { result = (await shell.handleToolCall({ name: test.tool, arguments: test.input, })); assert.ok(result, "result should be defined"); assert.equal(result.tool, test.tool); }); if (test.assertions.result_not_empty) { it("result is not empty", async () => { result ??= (await shell.handleToolCall({ name: test.tool, arguments: test.input, })); if (result.ok) { assert.ok(result.data !== undefined, "data should be defined"); } else { assert.ok(result.error !== undefined, "error should be defined"); } }); } if (test.assertions.text_contains) { for (const needle of test.assertions.text_contains) { it(`result contains text "${needle}"`, async () => { result ??= (await shell.handleToolCall({ name: test.tool, arguments: test.input, })); const haystack = stringifyData(result.data).toLowerCase(); assert.ok(haystack.includes(needle.toLowerCase()), `Expected "${needle}" in result`); }); } } if (test.assertions.any_result_contains) { for (const needle of test.assertions.any_result_contains) { it(`any result item contains "${needle}"`, async () => { result ??= (await shell.handleToolCall({ name: test.tool, arguments: test.input, })); const haystack = stringifyData(result.data).toLowerCase(); assert.ok(haystack.includes(needle.toLowerCase()), `Expected "${needle}" in result`); }); } } if (test.assertions.fields_present) { it(`result has fields: ${test.assertions.fields_present.join(", ")}`, async () => { result ??= (await shell.handleToolCall({ name: test.tool, arguments: test.input, })); assert.equal(result.ok, true); const data = result.data; assert.ok(data, "data should be defined"); for (const field of test.assertions.fields_present) { assert.ok(field in data, `Expected field "${field}" in result data`); } }); } if (test.assertions.text_not_empty) { it("result text is not empty", async () => { result ??= (await shell.handleToolCall({ name: test.tool, arguments: test.input, })); const text = stringifyData(result.data); assert.ok(text.trim().length > 0, "result text should not be empty"); }); } if (test.assertions.min_results !== undefined) { it(`returns at least ${test.assertions.min_results} results`, async () => { result ??= (await shell.handleToolCall({ name: test.tool, arguments: test.input, })); const data = result.data; const record = data; const items = Array.isArray(data) ? data : Array.isArray(record?.documents) ? record.documents : Array.isArray(record?.sources) ? record.sources : Array.isArray(record?.results) ? record.results : []; assert.ok(items.length >= test.assertions.min_results, `Expected at least ${test.assertions.min_results} results`); }); } if (test.assertions.citation_url_pattern) { it(`citation URLs match pattern: ${test.assertions.citation_url_pattern}`, async () => { result ??= (await shell.handleToolCall({ name: test.tool, arguments: test.input, })); const urls = extractCitationUrls(result.data); const pattern = new RegExp(test.assertions.citation_url_pattern); assert.ok(urls.length > 0, "Expected at least one URL"); for (const url of urls) { assert.match(url, pattern); } }); } if (test.assertions.upstream_text_hash && isNightly) { const hashAssertion = test.assertions.upstream_text_hash; it(`upstream text hash matches for ${hashAssertion.url}`, async () => { const response = await fetchWithTimeout(hashAssertion.url); assert.ok(response.ok, `Expected HTTP 200 for ${hashAssertion.url}`); const body = await response.text(); const hash = sha256(body); assert.equal(hash, hashAssertion.expected_sha256); }); } if (test.assertions.citation_resolves && isNightly) { it("citation URLs resolve (HTTP 200)", async () => { result ??= (await shell.handleToolCall({ name: test.tool, arguments: test.input, })); const urls = extractCitationUrls(result.data); assert.ok(urls.length > 0, "Expected at least one URL"); for (const url of urls) { const response = await fetchWithTimeout(url); assert.ok(response.ok, `Expected HTTP 200 for ${url}, got ${response.status}`); } }); } if (test.assertions.handles_gracefully) { it("handles gracefully (no unhandled exception)", async () => { result ??= (await shell.handleToolCall({ name: test.tool, arguments: test.input, })); assert.equal(result.tool, test.tool); }); } }); } });