#!/usr/bin/env bun // ============================================================================ // UK CASE LAW MCP SERVER - TEST SUITE // ============================================================================ // Run with: bun run src/test.ts // ============================================================================ import { searchTna, getTnaCaseContent, citationToUri } from './tna-client.js'; import { searchCaseLaw } from './search.js'; import { formatSearchResults, formatCaseContent } from './formatters.js'; const GREEN = '\x1b[32m'; const RED = '\x1b[31m'; const YELLOW = '\x1b[33m'; const RESET = '\x1b[0m'; let passed = 0; let failed = 0; function test(name: string, fn: () => Promise | void) { return async () => { try { await fn(); console.log(`${GREEN}✓${RESET} ${name}`); passed++; } catch (error) { console.log(`${RED}✗${RESET} ${name}`); console.log(` ${RED}${error instanceof Error ? error.message : error}${RESET}`); failed++; } }; } function assert(condition: boolean, message: string) { if (!condition) throw new Error(message); } // ============================================================================ // TESTS // ============================================================================ const tests = [ // Citation parsing tests test('citationToUri: parses UKSC citation', () => { const uri = citationToUri('[2024] UKSC 1'); assert(uri === 'uksc/2024/1', `Expected 'uksc/2024/1', got '${uri}'`); }), test('citationToUri: parses EWCA Civ citation', () => { const uri = citationToUri('[2007] EWCA Civ 588'); assert(uri === 'ewca/civ/2007/588', `Expected 'ewca/civ/2007/588', got '${uri}'`); }), test('citationToUri: parses EWHC with subdivision', () => { const uri = citationToUri('[2023] EWHC 123 (Patents)'); assert(uri !== null && uri.includes('ewhc'), `Expected URI to contain 'ewhc', got '${uri}'`); }), test('citationToUri: returns null for invalid citation', () => { const uri = citationToUri('not a citation'); assert(uri === null, `Expected null, got '${uri}'`); }), // TNA API tests test('searchTna: returns results for simple query', async () => { const results = await searchTna({ query: 'patent', limit: 3 }); assert(results.length > 0, 'Expected at least 1 result'); assert(results.length <= 3, `Expected max 3 results, got ${results.length}`); }), test('searchTna: results have required fields', async () => { const results = await searchTna({ query: 'contract breach', limit: 1 }); assert(results.length > 0, 'Expected at least 1 result'); const r = results[0]!; assert(typeof r.documentUri === 'string', 'documentUri should be string'); assert(typeof r.title === 'string', 'title should be string'); assert(typeof r.court === 'string', 'court should be string'); assert(typeof r.snippet === 'string', 'snippet should be string'); }), test('searchTna: handles query with special characters', async () => { const results = await searchTna({ query: 'Thaler DABUS AI', limit: 1 }); // Should not throw assert(Array.isArray(results), 'Should return array'); }), test('searchTna: supports pagination', async () => { // Request page 1 const page1 = await searchTna({ query: 'copyright', limit: 5, page: 1 }); assert(page1.length > 0, 'Page 1 should have results'); // Request page 2 const page2 = await searchTna({ query: 'copyright', limit: 5, page: 2 }); assert(page2.length > 0, 'Page 2 should have results'); // Results should likely be different (though not guaranteed if results are static/few) // But at least we verify the parameter is accepted without error if (page1.length > 0 && page2.length > 0) { assert(page1[0]!.documentUri !== page2[0]!.documentUri, 'Page 1 and Page 2 should have different first results'); } }), test('getTnaCaseContent: fetches case by URI', async () => { // First search for a case to get a valid URI const searchResults = await searchTna({ query: 'patent', limit: 1 }); assert(searchResults.length > 0, 'Need a case to test'); const uri = searchResults[0]!.documentUri; const content = await getTnaCaseContent(uri); assert(content !== null, `Case not found for URI: ${uri}`); assert(content!.metadata.title.length > 0, 'Title should not be empty'); assert(content!.paragraphs.length > 0, 'Should have paragraphs'); }), test('getTnaCaseContent: returns null for invalid URI', async () => { const content = await getTnaCaseContent('invalid/uri/12345'); assert(content === null, 'Should return null for invalid URI'); }), test('getTnaCaseContent: handles Getty v Stability AI case', async () => { // This case was previously failing due to para.num being a number const content = await getTnaCaseContent('ewhc/ch/2025/2863'); assert(content !== null, 'Getty case should be found'); assert(content!.paragraphs.length > 0, 'Should have paragraphs'); // Check that paragraph numbers are valid for (const p of content!.paragraphs.slice(0, 5)) { assert(typeof p.number === 'number', 'Para number should be number'); assert(typeof p.text === 'string', 'Para text should be string'); } }), // Search layer tests test('searchCaseLaw: returns formatted results', async () => { const results = await searchCaseLaw({ query: 'employment unfair dismissal', limit: 5 }); assert(Array.isArray(results), 'Should return array'); }), // Formatter tests test('formatSearchResults: formats empty results', () => { const formatted = formatSearchResults([]); assert(formatted.includes('No cases found'), 'Should indicate no results'); }), test('formatSearchResults: formats results with citations', () => { const results = [{ documentUri: 'uksc/2024/1', neutralCitation: '[2024] UKSC 1', title: 'Test Case', court: 'Supreme Court', date: '2024-01-01', snippet: 'This is a test snippet', source: 'tna' as const, score: 1.0, urls: { web: 'https://example.com/web', pdf: 'https://example.com/pdf', xml: 'https://example.com/xml', }, }]; const formatted = formatSearchResults(results); assert(formatted.includes('[2024] UKSC 1'), 'Should include citation'); assert(formatted.includes('Test Case'), 'Should include title'); }), test('formatCaseContent: formats case with paragraphs', () => { const caseData = { metadata: { documentUri: 'uksc/2024/1', neutralCitation: '[2024] UKSC 1', title: 'Test v Test', court: 'uksc', courtName: 'Supreme Court', date: '2024-01-01', source: 'tna' as const, urls: { web: 'https://example.com/web', pdf: 'https://example.com/pdf', xml: 'https://example.com/xml', }, }, paragraphs: [ { number: 1, text: 'First paragraph of the judgment.' }, { number: 2, text: 'Second paragraph of the judgment.' }, ], truncated: false, remainingParagraphs: 0, }; const formatted = formatCaseContent(caseData); assert(formatted.includes('Test v Test'), 'Should include title'); assert(formatted.includes('[1]'), 'Should include paragraph number'); assert(formatted.includes('First paragraph'), 'Should include paragraph text'); }), // ============================================================================ // LEGAL RESEARCH QUESTIONS // ============================================================================ // These tests verify the search system can answer specific legal research questions. // Expected answers are documented based on external research. // Q1: Has Izmo ever taken court action in the UK before? // ANSWER: No confirmed UK court cases found. Izmo (izmocars.com) sends demand // letters via PicRights/Pixsy but primarily settles out of court. test('Legal Q1: No Izmo court cases in UK case law database', async () => { const results = await searchTna({ query: 'Izmo copyright', limit: 20 }); const izmoAsParty = results.filter(r => r.title.toLowerCase().includes('izmo') || r.snippet.toLowerCase().includes('izmo') ); // Izmo has no published UK court cases - they settle before litigation assert(izmoAsParty.length === 0, 'Expected no Izmo cases in UK courts (they settle pre-litigation)'); }), // Q2: What is the maximum penalty awarded by courts for using a photo without permission? // ANSWER: £10,000 (Nottinghamshire Health Care NHS Trust v News Group Newspapers) // Other notable: PRS v Burns £9,000, Absolute Lofts v Artisan £6,300 test('Legal Q2: Search returns photo copyright infringement cases', async () => { const results = await searchTna({ query: 'photograph copyright infringement damages', limit: 20 }); // Should return relevant cases about photo copyright assert(Array.isArray(results), 'Should return array of results'); // Note: Maximum penalty found in research: £10,000 (NHS Trust v News Group) // IPEC small claims cap is £10,000, multi-track cap is £500,000 }), // Q3: List all cases in UK IPEC involving use of images on a website // KEY CASES FROM RESEARCH: // - Absolute Lofts v Artisan Home Improvements [2015] EWHC 2608 (IPEC): £6,300 for 21 images // - Webb v Cardiff Steel Erection Limited (2018): £2,851.42 for single image // - Jonathan C.K.Webb vs VA Events Ltd: £2,716.00 // - Jason Sheldon v Daybrook House Promotions [2013] EWPCC 26: £5,682.37 test('Legal Q3: Search for IPEC website image cases', async () => { const results = await searchTna({ query: 'website image copyright IPEC', limit: 20 }); // TNA may not have all IPEC small claims cases (many unpublished) assert(Array.isArray(results), 'Should return array of results'); // Key cases documented above may not be in TNA database }), // Q4: Has Kahn Automotive ever taken court action against another company? // ANSWER: Yes - A Kahn Design Limited has multiple IP cases: // - A Kahn Design Ltd v Fast Lane Styling Europe Ltd (IP-2024-000141) - as claimant // - A Kahn Design Ltd v GRP 4X4 GLOBAL Ltd (IP-2023-000006) - as claimant // - Rolls-Royce Motor Cars Ltd v A Kahn Design Ltd (IP-2024-000067) - as defendant test('Legal Q4: Search finds Kahn Design IP cases', async () => { const results = await searchTna({ query: 'Kahn Design', limit: 20 }); // A Kahn Design has been involved in multiple IP disputes assert(Array.isArray(results), 'Should return array of results'); // Note: Recent cases (2024) may not yet be in TNA database }), // Q5: What is the biggest fine issued in the UK IPEC small claims track? // ANSWER: £10,000 is the maximum cap for small claims track // (though IPEC guide says not a "hard and fast ceiling") // Nottinghamshire Health Care NHS Trust v News Group Newspapers reached this cap test('Legal Q5: IPEC small claims track has £10,000 damages cap', async () => { const results = await searchTna({ query: 'IPEC damages copyright', limit: 20 }); assert(Array.isArray(results), 'Should return array of results'); // IPEC Small Claims Track: max £10,000 damages // IPEC Multi-Track: max £500,000 damages // Costs capped at £50,000 for multi-track }), ]; // ============================================================================ // RUN TESTS // ============================================================================ async function runTests() { console.log('\n' + YELLOW + '═'.repeat(60) + RESET); console.log(YELLOW + ' UK Case Law MCP Server - Test Suite' + RESET); console.log(YELLOW + '═'.repeat(60) + RESET + '\n'); for (const runTest of tests) { await runTest(); } console.log('\n' + '─'.repeat(60)); console.log(`Results: ${GREEN}${passed} passed${RESET}, ${failed > 0 ? RED : ''}${failed} failed${RESET}`); console.log('─'.repeat(60) + '\n'); process.exit(failed > 0 ? 1 : 0); } runTests().catch(console.error);