tools.test.ts•27.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;
}
});