Skip to main content
Glama

documcp

by tosin2013
error-handling.test.ts19.9 kB
// Edge cases and error handling tests 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("Edge Cases and Error Handling", () => { let tempDir: string; beforeAll(async () => { tempDir = path.join(os.tmpdir(), "documcp-edge-case-tests"); await fs.mkdir(tempDir, { recursive: true }); }); afterAll(async () => { try { await fs.rm(tempDir, { recursive: true, force: true }); } catch (error) { console.warn("Failed to cleanup edge case test directory:", error); } }); describe("Input Validation Edge Cases", () => { it("should handle null and undefined inputs gracefully", async () => { // Test analyze_repository with invalid inputs const invalidInputs = [ null, undefined, {}, { path: null }, { path: undefined }, { path: "", depth: "invalid" }, ]; for (const input of invalidInputs) { try { const result = await analyzeRepository(input as any); expect((result as any).isError).toBe(true); } catch (error) { // Catching errors is also acceptable for invalid inputs expect(error).toBeDefined(); } } }); it("should validate SSG enum values strictly", async () => { const invalidSSGs = [ "react", "vue", "angular", "gatsby", "", null, undefined, ]; for (const invalidSSG of invalidSSGs) { try { const result = await generateConfig({ ssg: invalidSSG as any, projectName: "Test", outputPath: tempDir, }); expect((result as any).isError).toBe(true); } catch (error) { expect(error).toBeDefined(); } } }); it("should handle extremely long input strings", async () => { const longString = "a".repeat(10000); const result = await generateConfig({ ssg: "docusaurus", projectName: longString, projectDescription: longString, outputPath: path.join(tempDir, "long-strings"), }); // Should handle long strings without crashing expect(result.content).toBeDefined(); }); it("should handle special characters in project names", async () => { const specialNames = [ "Project with spaces", "Project-with-hyphens", "Project_with_underscores", "Project.with.dots", "Project@with#special$chars", "Проект на русском", "项目中文名", "プロジェクト日本語", ]; for (const name of specialNames) { const outputDir = path.join( tempDir, "special-chars", Buffer.from(name).toString("hex"), ); await fs.mkdir(outputDir, { recursive: true }); const result = await generateConfig({ ssg: "docusaurus", projectName: name, outputPath: outputDir, }); expect(result.content).toBeDefined(); expect((result as any).isError).toBeFalsy(); } }); }); describe("File System Edge Cases", () => { it("should handle permission-denied scenarios", async () => { if (process.platform === "win32") { // Skip on Windows as permission handling is different return; } // Create a directory with restricted permissions const restrictedDir = path.join(tempDir, "no-permissions"); await fs.mkdir(restrictedDir, { recursive: true }); try { await fs.chmod(restrictedDir, 0o000); const result = await analyzeRepository({ path: restrictedDir, depth: "standard", }); expect((result as any).isError).toBe(true); } finally { // Restore permissions for cleanup await fs.chmod(restrictedDir, 0o755); } }); it("should handle symlinks and circular references", async () => { const symlinkTest = path.join(tempDir, "symlink-test"); await fs.mkdir(symlinkTest, { recursive: true }); // Create a file const originalFile = path.join(symlinkTest, "original.txt"); await fs.writeFile(originalFile, "original content"); // Create symlink const symlinkFile = path.join(symlinkTest, "link.txt"); try { await fs.symlink(originalFile, symlinkFile); const result = await analyzeRepository({ path: symlinkTest, depth: "standard", }); expect(result.content).toBeDefined(); expect((result as any).isError).toBeFalsy(); } catch (error) { // Symlinks might not be supported on all systems console.warn("Symlink test skipped:", error); } }); it("should handle very deep directory structures", async () => { const deepTest = path.join(tempDir, "deep-structure"); let currentPath = deepTest; // Create 20 levels deep structure for (let i = 0; i < 20; i++) { currentPath = path.join(currentPath, `level-${i}`); await fs.mkdir(currentPath, { recursive: true }); await fs.writeFile( path.join(currentPath, `file-${i}.txt`), `Content ${i}`, ); } const result = await analyzeRepository({ path: deepTest, depth: "deep", }); expect(result.content).toBeDefined(); expect((result as any).isError).toBeFalsy(); }); it("should handle files with unusual extensions", async () => { const unusualFiles = path.join(tempDir, "unusual-files"); await fs.mkdir(unusualFiles, { recursive: true }); const unusualExtensions = [ "file.xyz", "file.123", "file.", ".hidden", "no-extension", "file..double.dot", "file with spaces.txt", "file-with-émojis-🚀.md", ]; for (const filename of unusualExtensions) { await fs.writeFile(path.join(unusualFiles, filename), "test content"); } const result = await analyzeRepository({ path: unusualFiles, depth: "standard", }); expect(result.content).toBeDefined(); expect((result as any).isError).toBeFalsy(); // Should count all files (excluding hidden files that start with .) const analysisData = JSON.parse( result.content.find((c) => c.text.includes('"totalFiles"'))!.text, ); // The analyze function filters out .hidden files, so we expect 7 files instead of 8 expect(analysisData.structure.totalFiles).toBe(7); // 8 files minus .hidden }); it("should handle binary files gracefully", async () => { const binaryTest = path.join(tempDir, "binary-files"); await fs.mkdir(binaryTest, { recursive: true }); // Create binary-like files const binaryData = Buffer.from([0x00, 0x01, 0x02, 0xff, 0xfe, 0xfd]); await fs.writeFile(path.join(binaryTest, "binary.bin"), binaryData); await fs.writeFile(path.join(binaryTest, "image.png"), binaryData); await fs.writeFile(path.join(binaryTest, "archive.zip"), binaryData); const result = await analyzeRepository({ path: binaryTest, depth: "standard", }); expect(result.content).toBeDefined(); expect((result as any).isError).toBeFalsy(); }); }); describe("Memory and Performance Edge Cases", () => { it("should handle repositories with many small files", async () => { const manyFilesTest = path.join(tempDir, "many-small-files"); await fs.mkdir(manyFilesTest, { recursive: true }); // Create 500 small files for (let i = 0; i < 500; i++) { await fs.writeFile( path.join(manyFilesTest, `small-${i}.txt`), `content ${i}`, ); } const startTime = Date.now(); const result = await analyzeRepository({ path: manyFilesTest, depth: "standard", }); const executionTime = Date.now() - startTime; expect(result.content).toBeDefined(); expect((result as any).isError).toBeFalsy(); expect(executionTime).toBeLessThan(10000); // Should complete within 10 seconds }); it("should handle repositories with very large files", async () => { const largeFilesTest = path.join(tempDir, "large-files"); await fs.mkdir(largeFilesTest, { recursive: true }); // Create large files (1MB each) const largeContent = "x".repeat(1024 * 1024); await fs.writeFile(path.join(largeFilesTest, "large1.txt"), largeContent); await fs.writeFile(path.join(largeFilesTest, "large2.log"), largeContent); const result = await analyzeRepository({ path: largeFilesTest, depth: "quick", // Use quick to avoid timeout }); expect(result.content).toBeDefined(); expect((result as any).isError).toBeFalsy(); }); it("should handle concurrent tool executions", async () => { const concurrentTest = path.join(tempDir, "concurrent-test"); await fs.mkdir(concurrentTest, { recursive: true }); await fs.writeFile( path.join(concurrentTest, "test.js"), 'console.log("test");', ); await fs.writeFile(path.join(concurrentTest, "README.md"), "# Test"); // Run multiple analyses concurrently const promises = Array.from({ length: 5 }, () => analyzeRepository({ path: concurrentTest, depth: "quick", }), ); const results = await Promise.all(promises); results.forEach((result) => { expect(result.content).toBeDefined(); expect((result as any).isError).toBeFalsy(); }); }); }); describe("Configuration Edge Cases", () => { it("should handle output paths with special characters", async () => { const specialPaths = [ path.join(tempDir, "path with spaces"), path.join(tempDir, "path-with-hyphens"), path.join(tempDir, "path_with_underscores"), path.join(tempDir, "path.with.dots"), ]; for (const specialPath of specialPaths) { const result = await generateConfig({ ssg: "docusaurus", projectName: "Special Path Test", outputPath: specialPath, }); expect(result.content).toBeDefined(); expect((result as any).isError).toBeFalsy(); // Verify files were actually created const files = await fs.readdir(specialPath); expect(files.length).toBeGreaterThan(0); } }); it("should handle nested output directory creation", async () => { const nestedPath = path.join( tempDir, "deeply", "nested", "output", "directory", ); const result = await generateConfig({ ssg: "mkdocs", projectName: "Nested Test", outputPath: nestedPath, }); expect(result.content).toBeDefined(); expect((result as any).isError).toBeFalsy(); // Verify nested directories were created expect( await fs .access(nestedPath) .then(() => true) .catch(() => false), ).toBe(true); }); it("should handle existing files without overwriting destructively", async () => { const existingFiles = path.join(tempDir, "existing-files"); await fs.mkdir(existingFiles, { recursive: true }); // Create existing file const existingContent = "This is existing content that should not be lost"; await fs.writeFile( path.join(existingFiles, "important.txt"), existingContent, ); const result = await generateConfig({ ssg: "docusaurus", projectName: "Existing Files Test", outputPath: existingFiles, }); expect(result.content).toBeDefined(); expect((result as any).isError).toBeFalsy(); // Verify our important file still exists expect( await fs .access(path.join(existingFiles, "important.txt")) .then(() => true) .catch(() => false), ).toBe(true); }); }); describe("Deployment Edge Cases", () => { it("should handle repositories with existing workflows", async () => { const existingWorkflow = path.join(tempDir, "existing-workflow"); await fs.mkdir(path.join(existingWorkflow, ".github", "workflows"), { recursive: true, }); // Create existing workflow await fs.writeFile( path.join(existingWorkflow, ".github", "workflows", "existing.yml"), "name: Existing Workflow\non: push", ); const result = await deployPages({ repository: existingWorkflow, ssg: "docusaurus", }); expect(result.content).toBeDefined(); expect((result as any).isError).toBeFalsy(); // Both workflows should exist const workflows = await fs.readdir( path.join(existingWorkflow, ".github", "workflows"), ); expect(workflows).toContain("existing.yml"); expect(workflows).toContain("deploy-docs.yml"); }); it("should handle custom domain validation", async () => { const customDomains = [ "docs.example.com", "my-docs.github.io", "documentation.mycompany.org", "subdomain.example.co.uk", ]; for (const domain of customDomains) { const domainTest = path.join( tempDir, "domain-test", domain.replace(/[^a-z0-9]/gi, "-"), ); const result = await deployPages({ repository: domainTest, ssg: "jekyll", customDomain: domain, }); expect(result.content).toBeDefined(); expect((result as any).isError).toBeFalsy(); // Verify CNAME file const cnameContent = await fs.readFile( path.join(domainTest, "CNAME"), "utf-8", ); expect(cnameContent.trim()).toBe(domain); } }); it("should handle repository URL variations", async () => { const urlVariations = [ "https://github.com/user/repo", "https://github.com/user/repo.git", "git@github.com:user/repo.git", "/absolute/local/path", "./relative/path", ".", ]; for (const repo of urlVariations) { const result = await verifyDeployment({ repository: repo, }); expect(result.content).toBeDefined(); expect((result as any).isError).toBeFalsy(); } }); }); describe("Unicode and Internationalization", () => { it("should handle Unicode file names and content", async () => { const unicodeTest = path.join(tempDir, "unicode-test"); await fs.mkdir(unicodeTest, { recursive: true }); const unicodeFiles = [ { name: "中文文件.md", content: "# 中文标题\n这是中文内容。" }, { name: "русский.txt", content: "Привет мир!" }, { name: "日本語.js", content: '// 日本語のコメント\nconsole.log("こんにちは");', }, { name: "émojis-🚀-test.py", content: '# -*- coding: utf-8 -*-\nprint("🚀 Unicode test")', }, ]; for (const file of unicodeFiles) { await fs.writeFile( path.join(unicodeTest, file.name), file.content, "utf8", ); } const result = await analyzeRepository({ path: unicodeTest, depth: "standard", }); expect(result.content).toBeDefined(); expect((result as any).isError).toBeFalsy(); const analysisData = JSON.parse( result.content.find((c) => c.text.includes('"totalFiles"'))!.text, ); expect(analysisData.structure.totalFiles).toBe(unicodeFiles.length); // No README created in this test }); it("should handle different line ending styles", async () => { const lineEndingTest = path.join(tempDir, "line-ending-test"); await fs.mkdir(lineEndingTest, { recursive: true }); // Create files with different line endings await fs.writeFile( path.join(lineEndingTest, "unix.txt"), "line1\nline2\nline3\n", ); await fs.writeFile( path.join(lineEndingTest, "windows.txt"), "line1\r\nline2\r\nline3\r\n", ); await fs.writeFile( path.join(lineEndingTest, "mac.txt"), "line1\rline2\rline3\r", ); await fs.writeFile( path.join(lineEndingTest, "mixed.txt"), "line1\nline2\r\nline3\rline4\n", ); const result = await analyzeRepository({ path: lineEndingTest, depth: "standard", }); expect(result.content).toBeDefined(); expect((result as any).isError).toBeFalsy(); }); }); describe("Recovery and Resilience", () => { it("should recover from partial failures gracefully", async () => { const partialFailure = path.join(tempDir, "partial-failure"); await fs.mkdir(partialFailure, { recursive: true }); // Create some valid files await fs.writeFile( path.join(partialFailure, "valid.js"), 'console.log("valid");', ); await fs.writeFile( path.join(partialFailure, "package.json"), '{"name": "test"}', ); // Create some problematic scenarios await fs.mkdir(path.join(partialFailure, "empty-dir")); const result = await analyzeRepository({ path: partialFailure, depth: "standard", }); expect(result.content).toBeDefined(); expect((result as any).isError).toBeFalsy(); // Should still provide useful analysis despite issues const analysisData = JSON.parse( result.content.find((c) => c.text.includes('"ecosystem"'))!.text, ); expect(analysisData.dependencies.ecosystem).toBe("javascript"); }); it("should provide meaningful error messages", async () => { const result = await analyzeRepository({ path: "/absolutely/does/not/exist/anywhere", depth: "standard", }); expect((result as any).isError).toBe(true); const errorText = result.content.map((c) => c.text).join(" "); // Error message should be helpful expect(errorText.toLowerCase()).toContain("error"); expect(errorText.toLowerCase()).toMatch( /resolution|solution|fix|check|ensure/, ); }); it("should handle timeout scenarios gracefully", async () => { // This test verifies that long-running operations don't hang indefinitely const longOperation = analyzeRepository({ path: tempDir, // Large temp directory depth: "deep", }); // Set a reasonable timeout with proper cleanup let timeoutId: NodeJS.Timeout | undefined; const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout( () => reject(new Error("Operation timed out")), 30000, ); // 30 seconds }); try { await Promise.race([longOperation, timeoutPromise]); } catch (error) { if ((error as Error).message === "Operation timed out") { console.warn( "Long operation test timed out - this is expected behavior", ); } else { throw error; } } finally { // Clean up the timeout to prevent Jest hanging if (timeoutId) { clearTimeout(timeoutId); } } }); }); });

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