Skip to main content
Glama
blade47

ShadowGit MCP Server

by blade47
git-handler.test.ts11.7 kB
import { describe, it, expect, beforeEach, jest } from '@jest/globals'; import { GitHandler } from '../../src/handlers/git-handler'; import { RepositoryManager } from '../../src/core/repository-manager'; import { GitExecutor } from '../../src/core/git-executor'; // Mock the dependencies jest.mock('../../src/core/repository-manager'); jest.mock('../../src/core/git-executor'); jest.mock('../../src/utils/logger', () => ({ log: jest.fn(), })); jest.mock('fs', () => ({ existsSync: jest.fn(), })); import * as fs from 'fs'; describe('GitHandler', () => { let handler: GitHandler; let mockRepositoryManager: jest.Mocked<RepositoryManager>; let mockGitExecutor: jest.Mocked<GitExecutor>; let mockExistsSync: jest.MockedFunction<typeof fs.existsSync>; beforeEach(() => { jest.clearAllMocks(); mockRepositoryManager = new RepositoryManager() as jest.Mocked<RepositoryManager>; mockGitExecutor = new GitExecutor() as jest.Mocked<GitExecutor>; mockExistsSync = fs.existsSync as jest.MockedFunction<typeof fs.existsSync>; handler = new GitHandler(mockRepositoryManager, mockGitExecutor); }); describe('handle', () => { describe('Validation', () => { it('should require both repo and command parameters', async () => { // Missing repo let result = await handler.handle({ command: 'log' }); expect(result.content[0].text).toContain("Error: Both 'repo' and 'command' parameters are required"); // Missing command result = await handler.handle({ repo: 'test-repo' }); expect(result.content[0].text).toContain("Error: Both 'repo' and 'command' parameters are required"); // Missing both result = await handler.handle({}); expect(result.content[0].text).toContain("Error: Both 'repo' and 'command' parameters are required"); // Null result = await handler.handle(null); expect(result.content[0].text).toContain("Error: Both 'repo' and 'command' parameters are required"); }); it('should handle non-string repo parameter', async () => { const result = await handler.handle({ repo: 123 as any, command: 'log', }); expect(result.content[0].text).toContain("Error: Both 'repo' and 'command' parameters are required"); }); it('should handle non-string command parameter', async () => { const result = await handler.handle({ repo: 'test-repo', command: true as any, }); expect(result.content[0].text).toContain("Error: Both 'repo' and 'command' parameters are required"); }); }); describe('Repository Resolution', () => { it('should handle repository not found', async () => { (mockRepositoryManager as any).resolveRepoPath = jest.fn().mockReturnValue(null); (mockRepositoryManager as any).getRepositories = jest.fn().mockReturnValue([ { name: 'repo1', path: '/path/to/repo1' }, { name: 'repo2', path: '/path/to/repo2' }, ]); const result = await handler.handle({ repo: 'non-existent', command: 'log', }); expect(result.content[0].text).toContain("Error: Repository 'non-existent' not found"); expect(result.content[0].text).toContain('Available repositories:'); expect(result.content[0].text).toContain('repo1:'); expect(result.content[0].text).toContain('Path: /path/to/repo1'); expect(result.content[0].text).toContain('repo2:'); expect(result.content[0].text).toContain('Path: /path/to/repo2'); }); it('should handle no repositories configured', async () => { (mockRepositoryManager as any).resolveRepoPath = jest.fn().mockReturnValue(null); (mockRepositoryManager as any).getRepositories = jest.fn().mockReturnValue([]); const result = await handler.handle({ repo: 'test-repo', command: 'log', }); expect(result.content[0].text).toContain('No repositories found'); expect(result.content[0].text).toContain('Please add repositories to ShadowGit first'); }); }); describe('ShadowGit Directory Check', () => { beforeEach(() => { (mockRepositoryManager as any).resolveRepoPath = jest.fn().mockReturnValue('/test/repo'); }); it('should handle when GitExecutor returns error for missing .shadowgit.git', async () => { mockExistsSync.mockReturnValue(false); (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue('Error: No ShadowGit repository found at /test/repo'); const result = await handler.handle({ repo: 'test-repo', command: 'log', }); expect(result).toBeDefined(); expect(result.content).toBeDefined(); expect(result.content[0]).toBeDefined(); expect(result.content[0].text).toContain('Error'); expect(mockGitExecutor.execute).toHaveBeenCalledWith('log', '/test/repo'); }); it('should proceed when .shadowgit.git directory exists', async () => { mockExistsSync.mockReturnValue(true); (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue('commit abc1234\nAuthor: Test'); const result = await handler.handle({ repo: 'test-repo', command: 'log -1', }); expect(result).toBeDefined(); expect(result.content).toBeDefined(); expect(result.content[0]).toBeDefined(); expect(result.content[0].text).toContain('commit abc1234'); expect(mockGitExecutor.execute).toHaveBeenCalledWith('log -1', '/test/repo'); }); }); describe('Git Command Execution', () => { beforeEach(() => { (mockRepositoryManager as any).resolveRepoPath = jest.fn().mockReturnValue('/test/repo'); mockExistsSync.mockReturnValue(true); }); it('should execute valid git commands', async () => { (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue('commit abc1234\ncommit def5678'); const result = await handler.handle({ repo: 'test-repo', command: 'log --oneline -2', }); expect(result.content[0].text).toContain('commit abc1234'); expect(result.content[0].text).toContain('commit def5678'); expect(mockGitExecutor.execute).toHaveBeenCalledWith( 'log --oneline -2', '/test/repo' ); }); it('should handle empty output', async () => { (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue(''); const result = await handler.handle({ repo: 'test-repo', command: 'status', }); // Now includes workflow reminder for status command expect(result.content[0].text).toContain('Planning to Make Changes?'); }); it('should trim whitespace from output', async () => { (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue(' \n output with spaces \n '); const result = await handler.handle({ repo: 'test-repo', command: 'log', }); // Now includes workflow reminder for log command expect(result.content[0].text).toContain('output with spaces'); expect(result.content[0].text).toContain('Planning to Make Changes?'); }); it('should handle error output from GitExecutor', async () => { (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue('Error: Command not allowed'); const result = await handler.handle({ repo: 'test-repo', command: 'commit -m "test"', }); expect(result.content[0].text).toContain('Error: Command not allowed'); }); it('should handle various git commands', async () => { const commands = [ { cmd: 'log -10', output: 'log output', hasReminder: true }, { cmd: 'diff HEAD~1', output: 'diff output', hasReminder: true }, { cmd: 'show abc123', output: 'show output', hasReminder: false }, { cmd: 'blame file.txt', output: 'blame output', hasReminder: true }, { cmd: 'status', output: 'status output', hasReminder: true }, { cmd: 'branch --list', output: 'branch output', hasReminder: false }, ]; for (const { cmd, output, hasReminder } of commands) { jest.clearAllMocks(); (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue(output); const result = await handler.handle({ repo: 'test-repo', command: cmd, }); expect(result.content[0].text).toContain(output); if (hasReminder) { expect(result.content[0].text).toContain('Planning to Make Changes?'); } else { expect(result.content[0].text).toBe(output); } expect(mockGitExecutor.execute).toHaveBeenCalledWith(cmd, '/test/repo'); } }); it('should pass correct parameters for regular commands', async () => { (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue('output'); const result = await handler.handle({ repo: 'test-repo', command: 'log', }); expect(mockGitExecutor.execute).toHaveBeenCalledWith( 'log', '/test/repo' ); // Verify workflow reminder is included expect(result.content[0].text).toContain('Planning to Make Changes?'); }); it('should handle multi-line output correctly', async () => { const multiLineOutput = `commit abc1234 Author: Test User Date: Mon Jan 1 2024 First commit commit def5678 Author: Another User Date: Mon Jan 2 2024 Second commit`; (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue(multiLineOutput); const result = await handler.handle({ repo: 'test-repo', command: 'log -2', }); // Now includes workflow reminder for log command expect(result.content[0].text).toContain(multiLineOutput); expect(result.content[0].text).toContain('Planning to Make Changes?'); }); it('should handle special characters in output', async () => { const specialOutput = 'Output with $pecial "chars" `backticks` & symbols'; (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue(specialOutput); const result = await handler.handle({ repo: 'test-repo', command: 'show', }); expect(result.content[0].text).toBe(specialOutput); }); it('should handle error responses from GitExecutor', async () => { // GitExecutor returns error messages as strings, not rejected promises (mockGitExecutor as any).execute = (jest.fn() as any).mockResolvedValue('Error: Execution failed'); const result = await handler.handle({ repo: 'test-repo', command: 'log', }); // The error message is returned as-is expect(result.content[0].text).toContain('Error: Execution failed'); }); }); }); });

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