Skip to main content
Glama

documcp

by tosin2013
tools.test.ts27.7 kB
// Functional tests for all MCP tools with real repository scenarios import { promises as fs } from 'fs'; import path from 'path'; import os from 'os'; import { analyzeRepository } from '../../src/tools/analyze-repository'; import { recommendSSG } from '../../src/tools/recommend-ssg'; import { generateConfig } from '../../src/tools/generate-config'; import { setupStructure } from '../../src/tools/setup-structure'; import { deployPages } from '../../src/tools/deploy-pages'; import { verifyDeployment } from '../../src/tools/verify-deployment'; describe('Functional Testing - MCP Tools', () => { let tempDir: string; let testRepos: { javascript: string; python: string; ruby: string; go: string; mixed: string; large: string; empty: string; }; beforeAll(async () => { tempDir = path.join(os.tmpdir(), 'documcp-functional-tests'); await fs.mkdir(tempDir, { recursive: true }); testRepos = { javascript: await createJavaScriptRepo(), python: await createPythonRepo(), ruby: await createRubyRepo(), go: await createGoRepo(), mixed: await createMixedLanguageRepo(), large: await createLargeRepo(), empty: await createEmptyRepo(), }; }); afterAll(async () => { try { await fs.rm(tempDir, { recursive: true, force: true }); } catch (error) { console.warn('Failed to cleanup test directory:', error); } }); describe('analyze_repository Tool', () => { it('should analyze JavaScript/TypeScript repository correctly', async () => { const result = await analyzeRepository({ path: testRepos.javascript, depth: 'standard', }); expect(result.content).toBeDefined(); expect(result.content.length).toBeGreaterThan(0); // Parse the JSON response to validate structure const analysisText = result.content.find((c) => c.text.includes('"id"')); expect(analysisText).toBeDefined(); const analysis = JSON.parse(analysisText!.text); expect(analysis.dependencies.ecosystem).toBe('javascript'); expect(analysis.structure.languages['.js']).toBeGreaterThan(0); expect(analysis.documentation.hasReadme).toBe(true); expect(analysis.recommendations.primaryLanguage).toBe('javascript'); }); it('should analyze Python repository correctly', async () => { const result = await analyzeRepository({ path: testRepos.python, depth: 'standard', }); const analysisText = result.content.find((c) => c.text.includes('"ecosystem"')); const analysis = JSON.parse(analysisText!.text); expect(analysis.dependencies.ecosystem).toBe('python'); expect(analysis.structure.languages['.py']).toBeGreaterThan(0); expect(analysis.dependencies.packages.length).toBeGreaterThan(0); }); it('should analyze Ruby repository correctly', async () => { const result = await analyzeRepository({ path: testRepos.ruby, depth: 'standard', }); const analysisText = result.content.find((c) => c.text.includes('"ecosystem"')); const analysis = JSON.parse(analysisText!.text); expect(analysis.dependencies.ecosystem).toBe('ruby'); expect(analysis.structure.languages['.rb']).toBeGreaterThan(0); }); it('should analyze Go repository correctly', async () => { const result = await analyzeRepository({ path: testRepos.go, depth: 'standard', }); const analysisText = result.content.find((c) => c.text.includes('"ecosystem"')); const analysis = JSON.parse(analysisText!.text); expect(analysis.dependencies.ecosystem).toBe('go'); expect(analysis.structure.languages['.go']).toBeGreaterThan(0); }); it('should handle different analysis depths', async () => { const quickResult = await analyzeRepository({ path: testRepos.javascript, depth: 'quick', }); const deepResult = await analyzeRepository({ path: testRepos.javascript, depth: 'deep', }); expect(quickResult.content).toBeDefined(); expect(deepResult.content).toBeDefined(); // Both should return valid results but potentially different detail levels const quickAnalysis = JSON.parse( quickResult.content.find((c) => c.text.includes('"id"'))!.text, ); const deepAnalysis = JSON.parse( deepResult.content.find((c) => c.text.includes('"id"'))!.text, ); expect(quickAnalysis.id).toBeDefined(); expect(deepAnalysis.id).toBeDefined(); }); it('should handle empty repository gracefully', async () => { const result = await analyzeRepository({ path: testRepos.empty, depth: 'standard', }); const analysisText = result.content.find((c) => c.text.includes('"totalFiles"')); const analysis = JSON.parse(analysisText!.text); expect(analysis.structure.totalFiles).toBe(1); // Only README.md expect(analysis.dependencies.ecosystem).toBe('unknown'); }); it('should handle non-existent repository path', async () => { const nonExistentPath = path.join(tempDir, 'does-not-exist'); const result = await analyzeRepository({ path: nonExistentPath, depth: 'standard', }); expect((result as any).isError).toBe(true); expect(result.content[0].text).toContain('Error:'); }); }); describe('recommend_ssg Tool', () => { it('should recommend SSG based on analysis', async () => { const result = await recommendSSG({ analysisId: 'test-analysis-123', }); expect(result.content).toBeDefined(); expect(result.content.length).toBeGreaterThan(0); // Should contain recommendation data const recommendationText = result.content.find((c) => c.text.includes('"recommended"')); expect(recommendationText).toBeDefined(); const recommendation = JSON.parse(recommendationText!.text); expect(recommendation.recommended).toBeDefined(); expect(recommendation.confidence).toBeGreaterThan(0); expect(recommendation.reasoning).toBeDefined(); expect(recommendation.alternatives).toBeDefined(); }); it('should handle preferences parameter', async () => { const result = await recommendSSG({ analysisId: 'test-analysis-456', preferences: { priority: 'simplicity', ecosystem: 'javascript', }, }); expect(result.content).toBeDefined(); const recommendationText = result.content.find((c) => c.text.includes('"recommended"')); const recommendation = JSON.parse(recommendationText!.text); expect(['jekyll', 'hugo', 'docusaurus', 'mkdocs', 'eleventy']).toContain( recommendation.recommended, ); }); }); describe('generate_config Tool', () => { let configOutputDir: string; beforeEach(async () => { configOutputDir = path.join(tempDir, 'config-output', Date.now().toString()); await fs.mkdir(configOutputDir, { recursive: true }); }); it('should generate Docusaurus configuration', async () => { const result = await generateConfig({ ssg: 'docusaurus', projectName: 'Test Docusaurus Project', projectDescription: 'A test project for Docusaurus', outputPath: configOutputDir, }); expect(result.content).toBeDefined(); // Verify files were created const docusaurusConfig = path.join(configOutputDir, 'docusaurus.config.js'); const packageJson = path.join(configOutputDir, 'package.json'); expect( await fs .access(docusaurusConfig) .then(() => true) .catch(() => false), ).toBe(true); expect( await fs .access(packageJson) .then(() => true) .catch(() => false), ).toBe(true); // Verify file contents const configContent = await fs.readFile(docusaurusConfig, 'utf-8'); expect(configContent).toContain('Test Docusaurus Project'); expect(configContent).toContain('classic'); }); it('should generate MkDocs configuration', async () => { const result = await generateConfig({ ssg: 'mkdocs', projectName: 'Test MkDocs Project', outputPath: configOutputDir, }); expect(result.content).toBeDefined(); const mkdocsConfig = path.join(configOutputDir, 'mkdocs.yml'); const requirements = path.join(configOutputDir, 'requirements.txt'); expect( await fs .access(mkdocsConfig) .then(() => true) .catch(() => false), ).toBe(true); expect( await fs .access(requirements) .then(() => true) .catch(() => false), ).toBe(true); const configContent = await fs.readFile(mkdocsConfig, 'utf-8'); expect(configContent).toContain('Test MkDocs Project'); expect(configContent).toContain('material'); }); it('should generate Hugo configuration', async () => { const result = await generateConfig({ ssg: 'hugo', projectName: 'Test Hugo Project', outputPath: configOutputDir, }); const hugoConfig = path.join(configOutputDir, 'hugo.toml'); expect( await fs .access(hugoConfig) .then(() => true) .catch(() => false), ).toBe(true); const configContent = await fs.readFile(hugoConfig, 'utf-8'); expect(configContent).toContain('Test Hugo Project'); }); it('should generate Jekyll configuration', async () => { const result = await generateConfig({ ssg: 'jekyll', projectName: 'Test Jekyll Project', outputPath: configOutputDir, }); const jekyllConfig = path.join(configOutputDir, '_config.yml'); const gemfile = path.join(configOutputDir, 'Gemfile'); expect( await fs .access(jekyllConfig) .then(() => true) .catch(() => false), ).toBe(true); expect( await fs .access(gemfile) .then(() => true) .catch(() => false), ).toBe(true); }); it('should generate Eleventy configuration', async () => { const result = await generateConfig({ ssg: 'eleventy', projectName: 'Test Eleventy Project', outputPath: configOutputDir, }); const eleventyConfig = path.join(configOutputDir, '.eleventy.js'); const packageJson = path.join(configOutputDir, 'package.json'); expect( await fs .access(eleventyConfig) .then(() => true) .catch(() => false), ).toBe(true); expect( await fs .access(packageJson) .then(() => true) .catch(() => false), ).toBe(true); }); }); describe('setup_structure Tool', () => { let structureOutputDir: string; beforeEach(async () => { structureOutputDir = path.join(tempDir, 'structure-output', Date.now().toString()); }); it('should create Diataxis structure with examples', async () => { const result = await setupStructure({ path: structureOutputDir, ssg: 'docusaurus', includeExamples: true, }); expect(result.content).toBeDefined(); // Verify directory structure const categories = ['tutorials', 'how-to', 'reference', 'explanation']; for (const category of categories) { const categoryDir = path.join(structureOutputDir, category); expect( await fs .access(categoryDir) .then(() => true) .catch(() => false), ).toBe(true); // Check for index.md const indexFile = path.join(categoryDir, 'index.md'); expect( await fs .access(indexFile) .then(() => true) .catch(() => false), ).toBe(true); // Check for example file const files = await fs.readdir(categoryDir); expect(files.length).toBeGreaterThan(1); // index.md + example file } // Check root index const rootIndex = path.join(structureOutputDir, 'index.md'); expect( await fs .access(rootIndex) .then(() => true) .catch(() => false), ).toBe(true); const rootContent = await fs.readFile(rootIndex, 'utf-8'); expect(rootContent).toContain('Diataxis'); expect(rootContent).toContain('Tutorials'); expect(rootContent).toContain('How-To Guides'); }); it('should create structure without examples', async () => { const result = await setupStructure({ path: structureOutputDir, ssg: 'mkdocs', includeExamples: false, }); expect(result.content).toBeDefined(); // Verify only index files exist (no examples) const tutorialsDir = path.join(structureOutputDir, 'tutorials'); const files = await fs.readdir(tutorialsDir); expect(files).toEqual(['index.md']); // Only index, no example }); it('should handle different SSG formats correctly', async () => { // Test Docusaurus format await setupStructure({ path: path.join(structureOutputDir, 'docusaurus'), ssg: 'docusaurus', includeExamples: true, }); const docusaurusFile = path.join(structureOutputDir, 'docusaurus', 'tutorials', 'index.md'); const docusaurusContent = await fs.readFile(docusaurusFile, 'utf-8'); expect(docusaurusContent).toContain('id: tutorials-index'); expect(docusaurusContent).toContain('sidebar_label:'); // Test Jekyll format await setupStructure({ path: path.join(structureOutputDir, 'jekyll'), ssg: 'jekyll', includeExamples: true, }); const jekyllFile = path.join(structureOutputDir, 'jekyll', 'tutorials', 'index.md'); const jekyllContent = await fs.readFile(jekyllFile, 'utf-8'); expect(jekyllContent).toContain('title:'); expect(jekyllContent).toContain('description:'); }); }); describe('deploy_pages Tool', () => { let deploymentRepoDir: string; beforeEach(async () => { deploymentRepoDir = path.join(tempDir, 'deployment-repo', Date.now().toString()); await fs.mkdir(deploymentRepoDir, { recursive: true }); }); it('should create GitHub Actions workflow for Docusaurus', async () => { const result = await deployPages({ repository: deploymentRepoDir, ssg: 'docusaurus', branch: 'gh-pages', }); expect(result.content).toBeDefined(); const workflowPath = path.join(deploymentRepoDir, '.github', 'workflows', 'deploy-docs.yml'); expect( await fs .access(workflowPath) .then(() => true) .catch(() => false), ).toBe(true); const workflowContent = await fs.readFile(workflowPath, 'utf-8'); expect(workflowContent).toContain('Deploy Docusaurus'); expect(workflowContent).toContain('npm run build'); expect(workflowContent).toContain('actions/upload-pages-artifact'); expect(workflowContent).toContain('actions/deploy-pages'); // Verify security compliance (OIDC tokens) expect(workflowContent).toContain('id-token: write'); expect(workflowContent).toContain('pages: write'); expect(workflowContent).not.toContain('GITHUB_TOKEN: ${{ secrets.'); }); it('should create workflow for MkDocs', async () => { const result = await deployPages({ repository: deploymentRepoDir, ssg: 'mkdocs', }); const workflowPath = path.join(deploymentRepoDir, '.github', 'workflows', 'deploy-docs.yml'); const workflowContent = await fs.readFile(workflowPath, 'utf-8'); expect(workflowContent).toContain('Deploy MkDocs'); expect(workflowContent).toContain('mkdocs gh-deploy'); expect(workflowContent).toContain('python'); }); it('should create workflow for Hugo', async () => { const result = await deployPages({ repository: deploymentRepoDir, ssg: 'hugo', }); const workflowContent = await fs.readFile( path.join(deploymentRepoDir, '.github', 'workflows', 'deploy-docs.yml'), 'utf-8', ); expect(workflowContent).toContain('Deploy Hugo'); expect(workflowContent).toContain('peaceiris/actions-hugo'); expect(workflowContent).toContain('hugo --minify'); }); it('should handle custom domain configuration', async () => { const result = await deployPages({ repository: deploymentRepoDir, ssg: 'jekyll', customDomain: 'docs.example.com', }); // Check CNAME file creation const cnamePath = path.join(deploymentRepoDir, 'CNAME'); expect( await fs .access(cnamePath) .then(() => true) .catch(() => false), ).toBe(true); const cnameContent = await fs.readFile(cnamePath, 'utf-8'); expect(cnameContent.trim()).toBe('docs.example.com'); // Verify result indicates custom domain was configured const resultText = result.content.map((c) => c.text).join(' '); expect(resultText).toContain('docs.example.com'); }); }); describe('verify_deployment Tool', () => { let verificationRepoDir: string; beforeEach(async () => { verificationRepoDir = path.join(tempDir, 'verification-repo', Date.now().toString()); await fs.mkdir(verificationRepoDir, { recursive: true }); }); it('should verify complete deployment setup', async () => { // Set up a complete deployment scenario await fs.mkdir(path.join(verificationRepoDir, '.github', 'workflows'), { recursive: true }); await fs.mkdir(path.join(verificationRepoDir, 'docs'), { recursive: true }); await fs.mkdir(path.join(verificationRepoDir, 'build'), { recursive: true }); // Create workflow file await fs.writeFile( path.join(verificationRepoDir, '.github', 'workflows', 'deploy-docs.yml'), 'name: Deploy Docs\non: push\njobs:\n deploy:\n runs-on: ubuntu-latest', ); // Create documentation files await fs.writeFile(path.join(verificationRepoDir, 'docs', 'index.md'), '# Documentation'); await fs.writeFile(path.join(verificationRepoDir, 'docs', 'guide.md'), '# Guide'); // Create config file await fs.writeFile( path.join(verificationRepoDir, 'docusaurus.config.js'), 'module.exports = { title: "Test" };', ); // Create build directory await fs.writeFile( path.join(verificationRepoDir, 'build', 'index.html'), '<h1>Built Site</h1>', ); const result = await verifyDeployment({ repository: verificationRepoDir, url: 'https://example.github.io/test-repo', }); expect(result.content).toBeDefined(); // Parse the verification result const verification = JSON.parse(result.content[0].text); expect(verification.summary.passed).toBeGreaterThan(0); // Should have passing checks expect( verification.checks.some((check: any) => check.message.includes('deployment workflow')), ).toBe(true); expect( verification.checks.some((check: any) => check.message.includes('documentation files')), ).toBe(true); expect( verification.checks.some((check: any) => check.message.includes('configuration')), ).toBe(true); expect(verification.checks.some((check: any) => check.message.includes('build output'))).toBe( true, ); }); it('should identify missing components', async () => { // Create minimal repo without deployment setup await fs.writeFile(path.join(verificationRepoDir, 'README.md'), '# Test Repo'); const result = await verifyDeployment({ repository: verificationRepoDir, }); const verification = JSON.parse(result.content[0].text); expect(verification.summary.failed).toBeGreaterThan(0); // Should have failing checks expect( verification.checks.some((check: any) => check.message.includes('No .github/workflows')), ).toBe(true); expect( verification.checks.some((check: any) => check.message.includes('No documentation files')), ).toBe(true); expect( verification.checks.some((check: any) => check.message.includes('No static site generator configuration'), ), ).toBe(true); }); it('should provide actionable recommendations', async () => { const result = await verifyDeployment({ repository: verificationRepoDir, }); const resultText = result.content.map((c) => c.text).join('\n'); expect(resultText).toContain('→'); // Should contain recommendation arrows expect(resultText).toContain('deploy_pages tool'); expect(resultText).toContain('setup_structure tool'); expect(resultText).toContain('generate_config tool'); }); it('should handle repository path variations', async () => { // Test with relative path const relativeResult = await verifyDeployment({ repository: '.', }); expect(relativeResult.content).toBeDefined(); // Test with absolute path const absoluteResult = await verifyDeployment({ repository: verificationRepoDir, }); expect(absoluteResult.content).toBeDefined(); // Test with HTTP URL (should default to current directory) const urlResult = await verifyDeployment({ repository: 'https://github.com/user/repo', }); expect(urlResult.content).toBeDefined(); }); }); // Helper functions to create test repositories async function createJavaScriptRepo(): Promise<string> { const repoPath = path.join(tempDir, 'javascript-repo'); await fs.mkdir(repoPath, { recursive: true }); // package.json const packageJson = { name: 'test-js-project', version: '1.0.0', description: 'Test JavaScript project', scripts: { start: 'node index.js', test: 'jest', }, dependencies: { express: '^4.18.0', lodash: '^4.17.21', }, devDependencies: { jest: '^29.0.0', '@types/node': '^20.0.0', }, }; await fs.writeFile(path.join(repoPath, 'package.json'), JSON.stringify(packageJson, null, 2)); // Source files await fs.writeFile(path.join(repoPath, 'index.js'), 'console.log("Hello World");'); await fs.writeFile(path.join(repoPath, 'utils.js'), 'module.exports = { helper: () => {} };'); await fs.writeFile(path.join(repoPath, 'app.ts'), 'const app: string = "TypeScript";'); // Test directory await fs.mkdir(path.join(repoPath, 'test'), { recursive: true }); await fs.writeFile(path.join(repoPath, 'test', 'app.test.js'), 'test("example", () => {});'); // Documentation await fs.writeFile( path.join(repoPath, 'README.md'), '# JavaScript Test Project\nA test project for JavaScript analysis.', ); await fs.writeFile( path.join(repoPath, 'CONTRIBUTING.md'), '# Contributing\nHow to contribute.', ); await fs.writeFile(path.join(repoPath, 'LICENSE'), 'MIT License'); // CI/CD await fs.mkdir(path.join(repoPath, '.github', 'workflows'), { recursive: true }); await fs.writeFile( path.join(repoPath, '.github', 'workflows', 'ci.yml'), 'name: CI\non: push\njobs:\n test:\n runs-on: ubuntu-latest', ); return repoPath; } async function createPythonRepo(): Promise<string> { const repoPath = path.join(tempDir, 'python-repo'); await fs.mkdir(repoPath, { recursive: true }); // requirements.txt await fs.writeFile( path.join(repoPath, 'requirements.txt'), 'flask>=2.0.0\nrequests>=2.25.0\nnumpy>=1.21.0', ); // Python files await fs.writeFile(path.join(repoPath, 'main.py'), 'import flask\napp = flask.Flask(__name__)'); await fs.writeFile(path.join(repoPath, 'utils.py'), 'def helper():\n pass'); // Tests await fs.mkdir(path.join(repoPath, 'tests'), { recursive: true }); await fs.writeFile( path.join(repoPath, 'tests', 'test_main.py'), 'def test_app():\n assert True', ); await fs.writeFile(path.join(repoPath, 'README.md'), '# Python Test Project'); return repoPath; } async function createRubyRepo(): Promise<string> { const repoPath = path.join(tempDir, 'ruby-repo'); await fs.mkdir(repoPath, { recursive: true }); // Gemfile await fs.writeFile( path.join(repoPath, 'Gemfile'), 'source "https://rubygems.org"\ngem "rails"', ); // Ruby files await fs.writeFile(path.join(repoPath, 'app.rb'), 'class App\nend'); await fs.writeFile(path.join(repoPath, 'helper.rb'), 'module Helper\nend'); await fs.writeFile(path.join(repoPath, 'README.md'), '# Ruby Test Project'); return repoPath; } async function createGoRepo(): Promise<string> { const repoPath = path.join(tempDir, 'go-repo'); await fs.mkdir(repoPath, { recursive: true }); // go.mod await fs.writeFile(path.join(repoPath, 'go.mod'), 'module test-go-project\ngo 1.19'); // Go files await fs.writeFile(path.join(repoPath, 'main.go'), 'package main\nfunc main() {}'); await fs.writeFile(path.join(repoPath, 'utils.go'), 'package main\nfunc helper() {}'); await fs.writeFile(path.join(repoPath, 'README.md'), '# Go Test Project'); return repoPath; } async function createMixedLanguageRepo(): Promise<string> { const repoPath = path.join(tempDir, 'mixed-repo'); await fs.mkdir(repoPath, { recursive: true }); // Multiple language files await fs.writeFile(path.join(repoPath, 'package.json'), '{"name": "mixed-project"}'); await fs.writeFile(path.join(repoPath, 'requirements.txt'), 'flask>=2.0.0'); await fs.writeFile(path.join(repoPath, 'Gemfile'), 'gem "rails"'); await fs.writeFile(path.join(repoPath, 'app.js'), 'console.log("JS");'); await fs.writeFile(path.join(repoPath, 'script.py'), 'print("Python")'); await fs.writeFile(path.join(repoPath, 'server.rb'), 'puts "Ruby"'); await fs.writeFile(path.join(repoPath, 'README.md'), '# Mixed Language Project'); return repoPath; } async function createLargeRepo(): Promise<string> { const repoPath = path.join(tempDir, 'large-repo'); await fs.mkdir(repoPath, { recursive: true }); // Create many files to simulate a large repository for (let i = 0; i < 150; i++) { const fileName = `file-${i.toString().padStart(3, '0')}.js`; await fs.writeFile(path.join(repoPath, fileName), `// File ${i}\nconsole.log(${i});`); } // Create nested directories for (let i = 0; i < 10; i++) { const dirPath = path.join(repoPath, `dir-${i}`); await fs.mkdir(dirPath, { recursive: true }); for (let j = 0; j < 20; j++) { const fileName = `nested-${j}.js`; await fs.writeFile(path.join(dirPath, fileName), `// Nested file ${i}-${j}`); } } await fs.writeFile(path.join(repoPath, 'package.json'), '{"name": "large-project"}'); await fs.writeFile(path.join(repoPath, 'README.md'), '# Large Test Project'); return repoPath; } async function createEmptyRepo(): Promise<string> { const repoPath = path.join(tempDir, 'empty-repo'); await fs.mkdir(repoPath, { recursive: true }); // Only a README file await fs.writeFile( path.join(repoPath, 'README.md'), '# Empty Project\nMinimal repository for testing.', ); return repoPath; } });

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