Skip to main content
Glama
blade47

ShadowGit MCP Server

by blade47
repository-manager.test.ts13.1 kB
import { describe, it, expect, beforeEach, jest } from '@jest/globals'; import { RepositoryManager } from '../../src/core/repository-manager'; import * as os from 'os'; import * as path from 'path'; import { getStorageLocation, fileExists, readJsonFile } from '../../src/utils/file-utils'; // Mock the dependencies jest.mock('os'); jest.mock('../../src/utils/logger', () => ({ log: jest.fn(), })); jest.mock('../../src/utils/file-utils', () => ({ getStorageLocation: jest.fn(), fileExists: jest.fn(), readJsonFile: jest.fn(), })); describe('RepositoryManager', () => { let manager: RepositoryManager; let mockGetStorageLocation: jest.MockedFunction<typeof getStorageLocation>; let mockFileExists: jest.MockedFunction<typeof fileExists>; let mockReadJsonFile: jest.MockedFunction<typeof readJsonFile>; let mockHomedir: jest.MockedFunction<typeof os.homedir>; beforeEach(() => { jest.clearAllMocks(); mockGetStorageLocation = getStorageLocation as jest.MockedFunction<typeof getStorageLocation>; mockFileExists = fileExists as jest.MockedFunction<typeof fileExists>; mockReadJsonFile = readJsonFile as jest.MockedFunction<typeof readJsonFile>; mockHomedir = os.homedir as jest.MockedFunction<typeof os.homedir>; // Default mock behaviors mockHomedir.mockReturnValue('/home/testuser'); mockGetStorageLocation.mockReturnValue('/home/testuser/.shadowgit'); mockFileExists.mockReturnValue(true); mockReadJsonFile.mockReturnValue([ { name: 'test-repo', path: '/test/repo' }, { name: 'another-repo', path: '/another/repo' }, ]); manager = new RepositoryManager(); }); describe('getRepositories', () => { it('should load repositories from config file', () => { const repos = manager.getRepositories(); expect(repos).toHaveLength(2); expect(repos[0]).toEqual({ name: 'test-repo', path: '/test/repo' }); expect(repos[1]).toEqual({ name: 'another-repo', path: '/another/repo' }); expect(mockReadJsonFile).toHaveBeenCalledWith( path.join('/home/testuser/.shadowgit', 'repos.json'), [] ); }); it('should return empty array when config file does not exist', () => { mockReadJsonFile.mockReturnValue([]); manager = new RepositoryManager(); const repos = manager.getRepositories(); expect(repos).toEqual([]); }); it('should return empty array when config file is empty', () => { mockReadJsonFile.mockReturnValue([]); manager = new RepositoryManager(); const repos = manager.getRepositories(); expect(repos).toEqual([]); }); it('should return empty array when config file contains invalid JSON', () => { // readJsonFile handles invalid JSON and returns default value mockReadJsonFile.mockReturnValue([]); manager = new RepositoryManager(); const repos = manager.getRepositories(); expect(repos).toEqual([]); }); it('should handle config file with empty array', () => { mockReadJsonFile.mockReturnValue([]); manager = new RepositoryManager(); const repos = manager.getRepositories(); expect(repos).toEqual([]); }); it('should cache repositories after first load', () => { const repos1 = manager.getRepositories(); const repos2 = manager.getRepositories(); expect(repos1).toBe(repos2); // Same reference expect(mockReadJsonFile).toHaveBeenCalledTimes(1); // Only called once }); it('should use getStorageLocation from file-utils', () => { mockGetStorageLocation.mockReturnValue('/custom/shadowgit'); mockReadJsonFile.mockReturnValue([]); const customManager = new RepositoryManager(); customManager.getRepositories(); expect(mockReadJsonFile).toHaveBeenCalledWith( path.join('/custom/shadowgit', 'repos.json'), [] ); }); it('should handle repositories with Windows paths', () => { mockReadJsonFile.mockReturnValue([ { name: 'windows-project', path: 'C:\\Users\\Dev\\Project' }, { name: 'network-project', path: '\\\\server\\share\\repo' }, ]); manager = new RepositoryManager(); const repos = manager.getRepositories(); expect(repos[0].path).toBe('C:\\Users\\Dev\\Project'); expect(repos[1].path).toBe('\\\\server\\share\\repo'); }); it('should handle malformed repository objects', () => { mockReadJsonFile.mockReturnValue([ { name: 'valid-repo', path: '/valid/path' }, { name: 'missing-path' }, // Missing path { path: '/missing/name' }, // Missing name null, // Null entry 'string-entry', // String instead of object { name: 'another-valid', path: '/another/valid' }, ] as any); manager = new RepositoryManager(); const repos = manager.getRepositories(); // The implementation doesn't filter out invalid entries expect(repos).toHaveLength(6); expect(repos[0]).toEqual({ name: 'valid-repo', path: '/valid/path' }); expect(repos[5]).toEqual({ name: 'another-valid', path: '/another/valid' }); }); }); describe('resolveRepoPath', () => { beforeEach(() => { mockReadJsonFile.mockReturnValue([ { name: 'test-repo', path: '/test/repo' }, { name: 'another-repo', path: '/another/repo' }, { name: 'home-repo', path: '~/projects/home' }, ]); manager = new RepositoryManager(); }); it('should resolve repository by exact name', () => { mockFileExists.mockImplementation((p: any) => p === path.join('/test/repo', '.shadowgit.git')); const resolvedPath = manager.resolveRepoPath('test-repo'); expect(resolvedPath).toBe('/test/repo'); }); it('should resolve repository by another name', () => { mockFileExists.mockImplementation((p: any) => p === path.join('/another/repo', '.shadowgit.git')); const resolvedPath = manager.resolveRepoPath('another-repo'); expect(resolvedPath).toBe('/another/repo'); }); it('should return null for non-existent repository name', () => { const resolvedPath = manager.resolveRepoPath('non-existent'); expect(resolvedPath).toBeNull(); }); it('should resolve absolute path directly if it exists with .shadowgit.git', () => { mockFileExists.mockImplementation((p: any) => p === '/direct/path' || p === path.join('/direct/path', '.shadowgit.git') ); const resolvedPath = manager.resolveRepoPath('/direct/path'); expect(resolvedPath).toBe('/direct/path'); }); it('should return null for non-existent absolute path', () => { mockFileExists.mockReturnValue(false); const resolvedPath = manager.resolveRepoPath('/non/existent/path'); expect(resolvedPath).toBeNull(); }); it('should resolve repository with tilde path', () => { mockHomedir.mockReturnValue('/home/testuser'); // The repository path contains ~/projects/home which needs to be resolved to /home/testuser/projects/home // resolveRepoPath will check for .shadowgit.git in the resolved path mockFileExists.mockImplementation((p: any) => { // When checking if .shadowgit.git exists in the resolved path const resolvedPath = p.replace('~', '/home/testuser'); return resolvedPath === path.join('/home/testuser/projects/home', '.shadowgit.git'); }); const resolvedPath = manager.resolveRepoPath('home-repo'); expect(resolvedPath).toBe('/home/testuser/projects/home'); }); it('should handle empty input', () => { const resolvedPath = manager.resolveRepoPath(''); expect(resolvedPath).toBeNull(); }); it('should handle null input', () => { const resolvedPath = manager.resolveRepoPath(null as any); expect(resolvedPath).toBeNull(); }); it('should handle undefined input', () => { const resolvedPath = manager.resolveRepoPath(undefined as any); expect(resolvedPath).toBeNull(); }); it('should be case-sensitive for repository names', () => { mockFileExists.mockImplementation((p: any) => p === path.join('/test/repo', '.shadowgit.git')); const path1 = manager.resolveRepoPath('test-repo'); const path2 = manager.resolveRepoPath('Test-Repo'); const path3 = manager.resolveRepoPath('TEST-REPO'); expect(path1).toBe('/test/repo'); expect(path2).toBeNull(); expect(path3).toBeNull(); }); it('should handle Windows absolute paths', () => { mockFileExists.mockImplementation((p: any) => { // path.isAbsolute on Windows will recognize C:\ paths return p === 'C:\\Windows\\Path'; }); const resolvedPath = manager.resolveRepoPath('C:\\Windows\\Path'); // On non-Windows systems, path.isAbsolute may not recognize C:\ as absolute // So this test may return null on Unix systems if (process.platform === 'win32') { expect(resolvedPath).toBe('C:\\Windows\\Path'); } else { // On Unix, C:\ is not recognized as an absolute path expect(resolvedPath).toBeNull(); } }); it('should handle UNC paths', () => { mockFileExists.mockImplementation((p: any) => p === '\\\\server\\share'); const resolvedPath = manager.resolveRepoPath('\\\\server\\share'); // UNC paths are Windows-specific if (process.platform === 'win32') { expect(resolvedPath).toBe('\\\\server\\share'); } else { // On Unix, \\\\ is not recognized as a path expect(resolvedPath).toBeNull(); } }); it('should handle relative paths as repository names', () => { // Relative paths should be treated as repo names, not paths const resolvedPath = manager.resolveRepoPath('./relative/path'); expect(resolvedPath).toBeNull(); }); it('should handle repository names with special characters', () => { mockReadJsonFile.mockReturnValue([ { name: 'repo-with-dash', path: '/dash/repo' }, { name: 'repo_with_underscore', path: '/underscore/repo' }, { name: 'repo.with.dots', path: '/dots/repo' }, ]); mockFileExists.mockImplementation((p: any) => p === path.join('/dash/repo', '.shadowgit.git') || p === path.join('/underscore/repo', '.shadowgit.git') || p === path.join('/dots/repo', '.shadowgit.git') ); manager = new RepositoryManager(); expect(manager.resolveRepoPath('repo-with-dash')).toBe('/dash/repo'); expect(manager.resolveRepoPath('repo_with_underscore')).toBe('/underscore/repo'); expect(manager.resolveRepoPath('repo.with.dots')).toBe('/dots/repo'); }); it('should check if path is absolute using path.isAbsolute', () => { // Mock a path that path.isAbsolute would recognize const unixPath = '/absolute/unix/path'; mockFileExists.mockImplementation((p: any) => p === unixPath || p === path.join(unixPath, '.shadowgit.git') ); const resolvedPath = manager.resolveRepoPath(unixPath); if (path.isAbsolute(unixPath)) { expect(resolvedPath).toBe(unixPath); } else { expect(resolvedPath).toBeNull(); } }); it('should normalize tilde in repository paths during resolution', () => { mockHomedir.mockReturnValue('/home/user'); mockReadJsonFile.mockReturnValue([ { name: 'tilde-repo', path: '~/my/project' }, ]); // Now the implementation expands tilde before checking fileExists mockFileExists.mockImplementation((p: any) => p === path.join('/home/user/my/project', '.shadowgit.git')); manager = new RepositoryManager(); const resolvedPath = manager.resolveRepoPath('tilde-repo'); expect(resolvedPath).toBe('/home/user/my/project'); }); it('should handle tilde at different positions', () => { mockHomedir.mockReturnValue('/home/user'); mockReadJsonFile.mockReturnValue([ { name: 'repo1', path: '~/project' }, { name: 'repo2', path: '/path/~/invalid' }, // Tilde not at start ]); mockFileExists.mockImplementation((p: any) => p === path.join('/home/user/project', '.shadowgit.git') || p === path.join('/path/~/invalid', '.shadowgit.git') ); manager = new RepositoryManager(); expect(manager.resolveRepoPath('repo1')).toBe('/home/user/project'); expect(manager.resolveRepoPath('repo2')).toBe('/path/~/invalid'); // Not expanded since tilde is not at start }); }); });

Latest Blog Posts

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/blade47/shadowgit-mcp'

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