Skip to main content
Glama

documcp

by tosin2013
check-documentation-links.test.ts25 kB
import { checkDocumentationLinks } from '../../src/tools/check-documentation-links.js'; import { formatMCPResponse } from '../../src/types/api.js'; import { writeFile, mkdir, rm } from 'fs/promises'; import { join } from 'path'; describe('checkDocumentationLinks', () => { const testDir = join(process.cwd(), 'test-docs-temp'); beforeEach(async () => { // Create test directory structure await mkdir(testDir, { recursive: true }); await mkdir(join(testDir, 'guides'), { recursive: true }); await mkdir(join(testDir, 'api'), { recursive: true }); }); afterEach(async () => { // Clean up test directory try { await rm(testDir, { recursive: true, force: true }); } catch (error) { // Ignore cleanup errors } }); describe('Input Validation', () => { test('should use default values for optional parameters', async () => { await writeFile(join(testDir, 'README.md'), '# Test\n[Link](./guides/test.md)'); await writeFile(join(testDir, 'guides', 'test.md'), '# Guide'); const result = await checkDocumentationLinks({ documentation_path: testDir, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); const contentText = formatted.content.map((c) => c.text).join(' '); expect(contentText).toContain('"totalLinks": 1'); }); test('should validate timeout_ms parameter', async () => { const result = await checkDocumentationLinks({ documentation_path: testDir, timeout_ms: 500, // Below minimum }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(true); const contentText = formatted.content.map((c) => c.text).join(' '); expect(contentText).toContain('Number must be greater than or equal to 1000'); }); test('should validate max_concurrent_checks parameter', async () => { const result = await checkDocumentationLinks({ documentation_path: testDir, max_concurrent_checks: 25, // Above maximum }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(true); const contentText = formatted.content.map((c) => c.text).join(' '); expect(contentText).toContain('Number must be less than or equal to 20'); }); }); describe('File Scanning', () => { test('should find markdown files in nested directories', async () => { await writeFile(join(testDir, 'README.md'), '# Root'); await writeFile(join(testDir, 'guides', 'guide1.md'), '# Guide 1'); await writeFile(join(testDir, 'api', 'reference.mdx'), '# API Reference'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); const contentText = formatted.content.map((c) => c.text).join(' '); expect(contentText).toContain('"filesScanned": 3'); }); test('should handle empty documentation directory', async () => { const result = await checkDocumentationLinks({ documentation_path: testDir, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(true); const contentText = formatted.content.map((c) => c.text).join(' '); expect(contentText).toContain('No documentation files found'); }); test('should handle non-existent directory', async () => { const result = await checkDocumentationLinks({ documentation_path: '/non/existent/path', }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(true); }); }); describe('Link Extraction', () => { test('should extract markdown links', async () => { const content = `# Test Document [Internal Link](./guides/test.md) [External Link](https://example.com) `; await writeFile(join(testDir, 'test.md'), content); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, check_internal_links: true, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); const contentText = formatted.content.map((c) => c.text).join(' '); expect(contentText).toContain('"totalLinks": 1'); }); test('should extract HTML links', async () => { const content = `# Test Document <a href="./guides/test.md">Internal Link</a> <a href="https://example.com">External Link</a> `; await writeFile(join(testDir, 'test.md'), content); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); }); test('should skip mailto and tel links', async () => { const content = `# Contact [Email](mailto:test@example.com) [Phone](tel:+1234567890) [Valid Link](./test.md) `; await writeFile(join(testDir, 'contact.md'), content); await writeFile(join(testDir, 'test.md'), '# Test'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); // Should only check the valid link, not mailto/tel }); }); describe('Internal Link Checking', () => { test('should validate existing internal links', async () => { await writeFile(join(testDir, 'README.md'), '[Valid](./guides/test.md)'); await mkdir(join(testDir, 'guides'), { recursive: true }); await writeFile(join(testDir, 'guides', 'test.md'), '# Test'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, check_internal_links: true, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); const contentText = formatted.content.map((c) => c.text).join(' '); expect(contentText).toContain('"status": "valid"'); }); test('should detect broken internal links', async () => { await writeFile(join(testDir, 'README.md'), '[Broken](./missing.md)'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, check_internal_links: true, fail_on_broken_links: false, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); const contentText = formatted.content.map((c) => c.text).join(' '); expect(contentText).toContain('"status": "broken"'); }); test('should handle relative path navigation', async () => { await writeFile(join(testDir, 'guides', 'guide1.md'), '[Back](../README.md)'); await writeFile(join(testDir, 'README.md'), '# Root'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, check_internal_links: true, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); const contentText = formatted.content.map((c) => c.text).join(' '); expect(contentText).toContain('"status": "valid"'); }); test('should handle anchor links in internal files', async () => { await writeFile(join(testDir, 'README.md'), '[Section](./guide.md#section)'); await writeFile(join(testDir, 'guide.md'), '# Guide\n## Section'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, check_internal_links: true, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); }); }); describe('External Link Checking', () => { test('should skip external links when disabled', async () => { await writeFile(join(testDir, 'README.md'), '[External](https://example.com)'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); // Should have 0 links checked since external checking is disabled }); test('should respect allowed domains', async () => { await writeFile( join(testDir, 'README.md'), ` [Allowed](https://github.com/test) [Not Allowed](https://example.com) `, ); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: true, allowed_domains: ['github.com'], }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); }); test('should handle timeout for slow external links', async () => { await writeFile(join(testDir, 'README.md'), '[Slow](https://httpstat.us/200?sleep=10000)'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: true, timeout_ms: 1000, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); // Should timeout and mark as warning }); }); describe('Link Filtering', () => { test('should ignore links matching ignore patterns', async () => { await writeFile( join(testDir, 'README.md'), ` [Ignored](./temp/file.md) [Valid](./guides/test.md) `, ); await writeFile(join(testDir, 'guides', 'test.md'), '# Test'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, ignore_patterns: ['temp/'], }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); // Should only check the valid link, ignore the temp/ link }); test('should filter by link types', async () => { await writeFile( join(testDir, 'README.md'), ` [Internal](./test.md) [External](https://example.com) [Anchor](#section) `, ); await writeFile(join(testDir, 'test.md'), '# Test'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, check_internal_links: true, check_anchor_links: false, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); // Should only check internal links }); }); describe('Failure Modes', () => { test('should fail when fail_on_broken_links is true and links are broken', async () => { await writeFile(join(testDir, 'README.md'), '[Broken](./missing.md)'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, fail_on_broken_links: true, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(true); const contentText = formatted.content.map((c) => c.text).join(' '); expect(contentText).toContain('Found 1 broken links'); }); test('should not fail when fail_on_broken_links is false', async () => { await writeFile(join(testDir, 'README.md'), '[Broken](./missing.md)'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, fail_on_broken_links: false, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); }); }); describe('Report Generation', () => { test('should generate comprehensive report with summary', async () => { await writeFile( join(testDir, 'README.md'), ` [Valid Internal](./test.md) [Broken Internal](./missing.md) `, ); await writeFile(join(testDir, 'test.md'), '# Test'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, fail_on_broken_links: false, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); const contentText = formatted.content.map((c) => c.text).join(' '); expect(contentText).toContain('"summary"'); expect(contentText).toContain('"results"'); expect(contentText).toContain('"recommendations"'); expect(contentText).toContain('"totalLinks": 2'); }); test('should include execution metrics', async () => { await writeFile(join(testDir, 'README.md'), '[Test](./test.md)'); await writeFile(join(testDir, 'test.md'), '# Test'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); const contentText = formatted.content.map((c) => c.text).join(' '); expect(contentText).toContain('"executionTime"'); expect(contentText).toContain('"filesScanned"'); }); test('should provide recommendations based on results', async () => { await writeFile(join(testDir, 'README.md'), '[Valid](./test.md)'); await writeFile(join(testDir, 'test.md'), '# Test'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); const contentText = formatted.content.map((c) => c.text).join(' '); expect(contentText).toContain('All links are valid - excellent documentation quality!'); }); }); describe('Concurrency Control', () => { test('should respect max_concurrent_checks limit', async () => { // Create multiple files with links for (let i = 0; i < 10; i++) { await writeFile(join(testDir, `file${i}.md`), `[Link](./target${i}.md)`); await writeFile(join(testDir, `target${i}.md`), `# Target ${i}`); } const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, max_concurrent_checks: 2, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); // Should complete successfully with concurrency control }); }); describe('Edge Cases', () => { test('should handle files with no links', async () => { await writeFile(join(testDir, 'README.md'), '# No Links Here\nJust plain text.'); const result = await checkDocumentationLinks({ documentation_path: testDir, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); const contentText = formatted.content.map((c) => c.text).join(' '); expect(contentText).toContain('"totalLinks": 0'); }); test('should handle malformed markdown', async () => { await writeFile( join(testDir, 'README.md'), ` # Malformed [Incomplete link]( [Missing closing](test.md [Valid](./test.md) `, ); await writeFile(join(testDir, 'test.md'), '# Test'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); // Should handle malformed links gracefully }); test('should handle binary files gracefully', async () => { await writeFile(join(testDir, 'README.md'), '[Test](./test.md)'); await writeFile(join(testDir, 'test.md'), '# Test'); // Create a binary file that should be ignored await writeFile(join(testDir, 'image.png'), Buffer.from([0x89, 0x50, 0x4e, 0x47])); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); // Should ignore binary files and process markdown files }); }); describe('Advanced Branch Coverage Tests', () => { test('should handle reference links', async () => { const content = `# Test Document [Reference Link][ref1] [Another Reference][ref2] [ref1]: ./guides/test.md [ref2]: https://example.com `; await writeFile(join(testDir, 'test.md'), content); await writeFile(join(testDir, 'guides', 'test.md'), '# Guide'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); const contentText = formatted.content.map((c) => c.text).join(' '); expect(contentText).toContain('"totalLinks": 1'); }); test('should handle anchor-only links', async () => { const content = `# Test Document [Anchor Only](#section) [Valid Link](./test.md) `; await writeFile(join(testDir, 'README.md'), content); await writeFile(join(testDir, 'test.md'), '# Test'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, check_anchor_links: true, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); }); test('should handle empty URL in links', async () => { const content = `# Test Document [Empty Link]() [Valid Link](./test.md) `; await writeFile(join(testDir, 'README.md'), content); await writeFile(join(testDir, 'test.md'), '# Test'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); }); test('should handle different internal link path formats', async () => { await mkdir(join(testDir, 'subdir'), { recursive: true }); await mkdir(join(testDir, 'guides'), { recursive: true }); await writeFile( join(testDir, 'subdir', 'nested.md'), ` [Current Dir](./file.md) [Parent Dir](../README.md) [Absolute](/guides/test.md) [Relative](file.md) `, ); await writeFile(join(testDir, 'subdir', 'file.md'), '# File'); await writeFile(join(testDir, 'README.md'), '# Root'); await writeFile(join(testDir, 'guides', 'test.md'), '# Guide'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); }); test('should handle external link domain filtering', async () => { await writeFile( join(testDir, 'README.md'), ` [GitHub](https://github.com/test) [Subdomain](https://api.github.com/test) [Not Allowed](https://example.com) `, ); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: true, allowed_domains: ['github.com'], }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); }); test('should handle external link fetch errors', async () => { await writeFile( join(testDir, 'README.md'), '[Invalid URL](https://invalid-domain-that-does-not-exist-12345.com)', ); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: true, timeout_ms: 2000, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); const contentText = formatted.content.map((c) => c.text).join(' '); expect(contentText).toContain('"status": "broken"'); }); test('should handle HTTP error status codes', async () => { await writeFile(join(testDir, 'README.md'), '[Not Found](https://httpstat.us/404)'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: true, timeout_ms: 5000, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); }); test('should handle directory scanning errors', async () => { // Create a directory with restricted permissions await mkdir(join(testDir, 'restricted'), { recursive: true }); await writeFile(join(testDir, 'README.md'), '[Test](./test.md)'); await writeFile(join(testDir, 'test.md'), '# Test'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); }); test('should handle file reading errors gracefully', async () => { await writeFile(join(testDir, 'README.md'), '[Test](./test.md)'); await writeFile(join(testDir, 'test.md'), '# Test'); // Create a file that might cause reading issues await writeFile(join(testDir, 'problematic.md'), '# Test\x00\x01\x02'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); }); test('should generate recommendations for large link counts', async () => { let content = '# Test Document\n'; for (let i = 0; i < 101; i++) { content += `[Link ${i}](./file${i}.md)\n`; await writeFile(join(testDir, `file${i}.md`), `# File ${i}`); } await writeFile(join(testDir, 'README.md'), content); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); const contentText = formatted.content.map((c) => c.text).join(' '); expect(contentText).toContain( 'Consider implementing automated link checking in CI/CD pipeline', ); }); test('should handle mixed link types with warnings', async () => { await writeFile( join(testDir, 'README.md'), ` [Valid](./test.md) [Broken](./missing.md) [Timeout](https://httpstat.us/200?sleep=10000) `, ); await writeFile(join(testDir, 'test.md'), '# Test'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: true, timeout_ms: 1000, fail_on_broken_links: false, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); const contentText = formatted.content.map((c) => c.text).join(' '); expect(contentText).toContain('"brokenLinks"'); expect(contentText).toContain('"warningLinks"'); }); test('should handle node_modules and hidden directory exclusion', async () => { await mkdir(join(testDir, 'node_modules'), { recursive: true }); await mkdir(join(testDir, '.hidden'), { recursive: true }); await writeFile(join(testDir, 'node_modules', 'package.md'), '# Should be ignored'); await writeFile(join(testDir, '.hidden', 'secret.md'), '# Should be ignored'); await writeFile(join(testDir, 'README.md'), '[Test](./test.md)'); await writeFile(join(testDir, 'test.md'), '# Test'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); const contentText = formatted.content.map((c) => c.text).join(' '); expect(contentText).toContain('"filesScanned": 2'); // Only README.md and test.md }); test('should handle different markdown file extensions', async () => { await writeFile(join(testDir, 'README.md'), '[MD](./test.md)'); await writeFile(join(testDir, 'guide.mdx'), '[MDX](./test.md)'); await writeFile(join(testDir, 'doc.markdown'), '[MARKDOWN](./test.md)'); await writeFile(join(testDir, 'test.md'), '# Test'); const result = await checkDocumentationLinks({ documentation_path: testDir, check_external_links: false, }); const formatted = formatMCPResponse(result); expect(formatted.isError).toBe(false); const contentText = formatted.content.map((c) => c.text).join(' '); expect(contentText).toContain('"filesScanned": 4'); }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tosin2013/documcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server