Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions tests/docs-fetch-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { describe, it, expect } from 'vitest';
import { formatPageContent } from '../src/mcp/tools/docs-fetch.js';
import type { ProcessedDoc } from '../src/types/index.js';

const sampleDoc: ProcessedDoc = {
route: '/docs/test',
title: 'Test Page',
description: 'A test page',
markdown: '# Test\n\nSome content here.',
headings: [
{ level: 1, text: 'Test', id: 'test', startOffset: 0, endOffset: 10 },
{ level: 2, text: 'Section', id: 'section', startOffset: 11, endOffset: 30 },
],
};

describe('formatPageContent', () => {
it('returns "Page not found" for null doc', () => {
const result = formatPageContent(null);
expect(result).toContain('Page not found');
});

it('includes title, description, TOC, and markdown for a full doc', () => {
const result = formatPageContent(sampleDoc);

expect(result).toContain('# Test Page');
expect(result).toContain('> A test page');
expect(result).toContain('## Contents');
expect(result).toContain('- [Test](#test)');
expect(result).toContain('- [Section](#section)');
expect(result).toContain('Some content here.');
});

it('omits description blockquote when description is empty', () => {
const doc: ProcessedDoc = {
...sampleDoc,
description: '',
};
const result = formatPageContent(doc);

expect(result).not.toContain('> ');
expect(result).toContain('# Test Page');
});

it('omits Contents section when there are no headings', () => {
const doc: ProcessedDoc = {
...sampleDoc,
headings: [],
};
const result = formatPageContent(doc);

expect(result).not.toContain('## Contents');
expect(result).not.toContain('---');
expect(result).toContain('# Test Page');
expect(result).toContain('Some content here.');
});

it('excludes headings deeper than level 3 from TOC', () => {
const doc: ProcessedDoc = {
...sampleDoc,
headings: [
{ level: 1, text: 'Top', id: 'top', startOffset: 0, endOffset: 5 },
{ level: 2, text: 'Sub', id: 'sub', startOffset: 6, endOffset: 12 },
{ level: 3, text: 'SubSub', id: 'subsub', startOffset: 13, endOffset: 20 },
{ level: 4, text: 'Deep', id: 'deep', startOffset: 21, endOffset: 28 },
{ level: 5, text: 'Deeper', id: 'deeper', startOffset: 29, endOffset: 36 },
],
};
const result = formatPageContent(doc);

expect(result).toContain('- [Top](#top)');
expect(result).toContain('- [Sub](#sub)');
expect(result).toContain('- [SubSub](#subsub)');
expect(result).not.toContain('Deep');
expect(result).not.toContain('Deeper');
});

it('indents TOC entries by heading level', () => {
const doc: ProcessedDoc = {
...sampleDoc,
headings: [
{ level: 1, text: 'H1', id: 'h1', startOffset: 0, endOffset: 5 },
{ level: 2, text: 'H2', id: 'h2', startOffset: 6, endOffset: 12 },
{ level: 3, text: 'H3', id: 'h3', startOffset: 13, endOffset: 20 },
],
};
const result = formatPageContent(doc);
const lines = result.split('\n');

const h1Line = lines.find((l) => l.includes('[H1]'));
const h2Line = lines.find((l) => l.includes('[H2]'));
const h3Line = lines.find((l) => l.includes('[H3]'));

// level 1: no indent (0 repeats of ' ')
expect(h1Line).toBe('- [H1](#h1)');
// level 2: 2-space indent
expect(h2Line).toBe(' - [H2](#h2)');
// level 3: 4-space indent
expect(h3Line).toBe(' - [H3](#h3)');
});
});
123 changes: 123 additions & 0 deletions tests/server-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { McpDocsServer } from '../src/mcp/server.js';
import { FlexSearchIndexer } from '../src/providers/indexers/flexsearch-indexer.js';
import type { ProcessedDoc, McpServerDataConfig, McpServerFileConfig } from '../src/types/index.js';
import type { ProviderContext } from '../src/providers/types.js';

const mockDocs: ProcessedDoc[] = [
{
route: '/docs/getting-started',
title: 'Getting Started',
description: 'Learn how to get started',
markdown: '# Getting Started\n\nWelcome to the docs.',
headings: [
{ level: 1, text: 'Getting Started', id: 'getting-started', startOffset: 0, endOffset: 40 },
],
},
{
route: '/docs/api',
title: 'API Reference',
description: 'API docs',
markdown: '# API Reference\n\nEndpoints listed here.',
headings: [
{ level: 1, text: 'API Reference', id: 'api-reference', startOffset: 0, endOffset: 38 },
],
},
];

const mockProviderContext: ProviderContext = {
baseUrl: 'https://example.com',
serverName: 'test-server',
serverVersion: '1.0.0',
outputDir: '/tmp/test',
};

async function buildArtifacts() {
const indexer = new FlexSearchIndexer();
await indexer.initialize(mockProviderContext);
await indexer.indexDocuments(mockDocs);
return indexer.finalize();
}

describe('McpDocsServer', () => {
let artifacts: Map<string, unknown>;
let dataConfig: McpServerDataConfig;

beforeEach(async () => {
artifacts = await buildArtifacts();
dataConfig = {
name: 'test-docs',
version: '2.0.0',
baseUrl: 'https://example.com',
docs: artifacts.get('docs.json') as Record<string, ProcessedDoc>,
searchIndexData: artifacts.get('search-index.json') as Record<string, unknown>,
};
});

describe('constructor', () => {
it('creates a server with data-based config', () => {
const server = new McpDocsServer(dataConfig);
expect(server).toBeInstanceOf(McpDocsServer);
});

it('creates a server with file-based config', () => {
const fileConfig: McpServerFileConfig = {
name: 'file-docs',
version: '1.0.0',
docsPath: '/tmp/docs.json',
indexPath: '/tmp/search-index.json',
};
const server = new McpDocsServer(fileConfig);
expect(server).toBeInstanceOf(McpDocsServer);
});
});

describe('initialize()', () => {
it('initializes successfully with pre-loaded data', async () => {
const server = new McpDocsServer(dataConfig);
await expect(server.initialize()).resolves.not.toThrow();
});

it('is idempotent — second call returns immediately', async () => {
const server = new McpDocsServer(dataConfig);
await server.initialize();
// Second call should resolve without error
await expect(server.initialize()).resolves.not.toThrow();
});
});

describe('getStatus()', () => {
it('returns correct status after initialization', async () => {
const server = new McpDocsServer(dataConfig);
await server.initialize();

const status = await server.getStatus();
expect(status.name).toBe('test-docs');
expect(status.version).toBe('2.0.0');
expect(status.initialized).toBe(true);
expect(status.docCount).toBe(2);
expect(status.baseUrl).toBe('https://example.com');
expect(status.searchProvider).toBe('flexsearch');
});

it('returns defaults when version is omitted', async () => {
const config: McpServerDataConfig = {
...dataConfig,
version: undefined,
};
const server = new McpDocsServer(config);
await server.initialize();

const status = await server.getStatus();
expect(status.version).toBe('1.0.0');
});

it('returns initialized false and docCount 0 before init', async () => {
const server = new McpDocsServer(dataConfig);
const status = await server.getStatus();

expect(status.initialized).toBe(false);
expect(status.docCount).toBe(0);
});
});
});
Loading