Skip to main content
Glama
patcher.test.ts6.86 kB
import { Patcher } from '../patcher'; import * as fs from 'fs/promises'; import * as path from 'path'; import { spawn } from 'child_process'; // Mock child_process spawn jest.mock('child_process'); const mockSpawn = spawn as jest.MockedFunction<typeof spawn>; // Mock fs promises jest.mock('fs/promises'); const mockFs = fs as jest.Mocked<typeof fs>; describe('Patcher', () => { let patcher: Patcher; let mockProjectRoot: string; beforeEach(() => { mockProjectRoot = '/test/project'; patcher = new Patcher(mockProjectRoot); // Reset mocks jest.clearAllMocks(); // Mock HOME environment variable process.env.HOME = '/home/test'; }); afterEach(() => { delete process.env.HOME; }); describe('applyPatch', () => { it('should apply a simple unified diff patch', async () => { // Mock git commands to succeed mockSpawn.mockImplementation(() => { const mockProcess = { on: jest.fn((event, callback) => { if (event === 'close') { setTimeout(() => callback(0), 0); // Exit code 0 = success } }), stdout: { on: jest.fn((event, callback) => { if (event === 'data') { setTimeout(() => callback(Buffer.from('success output')), 0); } }) }, stderr: { on: jest.fn() } }; return mockProcess as any; }); // Mock file system operations mockFs.writeFile.mockResolvedValue(undefined); const unifiedDiff = ` --- a/scripts/fighter.gd +++ b/scripts/fighter.gd @@ -42,7 +42,7 @@ func _ready(): health = 100 - damage = get_damage() # This function doesn't exist + damage = calculate_damage() # Fixed function name setup_animations() `; const result = await patcher.applyPatch(unifiedDiff); expect(result.success).toBe(true); expect(result.branch_name).toMatch(/^ai\/fix-\d{8}-\d{6}$/); expect(mockFs.writeFile).toHaveBeenCalledWith( expect.stringContaining('patch.diff'), expect.stringContaining('calculate_damage') ); }); it('should extract patches from sentinel fences', async () => { mockSpawn.mockImplementation(() => ({ on: jest.fn((event, callback) => { if (event === 'close') callback(0); }), stdout: { on: jest.fn() }, stderr: { on: jest.fn() } } as any)); mockFs.writeFile.mockResolvedValue(undefined); const fencedPatch = ` Some explanatory text here... *** begin patch # Root cause: Function name was incorrect *** update file: scripts/fighter.gd @@ -42,7 +42,7 @@ func _ready(): health = 100 - damage = get_damage() + damage = calculate_damage() setup_animations() *** end patch More text after... `; const result = await patcher.applyPatch(fencedPatch); expect(result.success).toBe(true); expect(mockFs.writeFile).toHaveBeenCalledWith( expect.stringContaining('patch.diff'), expect.stringMatching(/calculate_damage/) ); }); it('should handle git command failures', async () => { // Mock git command to fail mockSpawn.mockImplementation(() => ({ on: jest.fn((event, callback) => { if (event === 'close') callback(1); // Exit code 1 = failure }), stdout: { on: jest.fn() }, stderr: { on: jest.fn((event, callback) => { if (event === 'data') callback(Buffer.from('Git apply failed')); }) } } as any)); const result = await patcher.applyPatch('invalid diff'); expect(result.success).toBe(false); expect(result.error).toContain('Git apply failed'); }); it('should handle stashing and unstashing dirty repos', async () => { let gitCallCount = 0; mockSpawn.mockImplementation((cmd, args) => { gitCallCount++; // First call: git status (returns dirty) if (args?.[0] === 'status' && args?.[1] === '--porcelain') { return { on: jest.fn((event, callback) => { if (event === 'close') callback(0); }), stdout: { on: jest.fn((event, callback) => { if (event === 'data') callback(Buffer.from('M some_file.gd')); }) }, stderr: { on: jest.fn() } } as any; } // Other git commands succeed return { on: jest.fn((event, callback) => { if (event === 'close') callback(0); }), stdout: { on: jest.fn() }, stderr: { on: jest.fn() } } as any; }); mockFs.writeFile.mockResolvedValue(undefined); const result = await patcher.applyPatch('--- a/test.gd\n+++ b/test.gd\n@@ -1 +1 @@\n-old\n+new'); expect(result.success).toBe(true); // Should have called git stash and git stash pop expect(mockSpawn).toHaveBeenCalledWith('git', ['stash', 'push', '-m', 'Sentinel auto-stash'], expect.any(Object)); expect(mockSpawn).toHaveBeenCalledWith('git', ['stash', 'pop'], expect.any(Object)); }); }); describe('getCurrentBranch', () => { it('should return current git branch', async () => { mockSpawn.mockImplementation(() => ({ on: jest.fn((event, callback) => { if (event === 'close') callback(0); }), stdout: { on: jest.fn((event, callback) => { if (event === 'data') callback(Buffer.from('feature-branch\n')); }) }, stderr: { on: jest.fn() } } as any)); const branch = await patcher.getCurrentBranch(); expect(branch).toBe('feature-branch'); }); it('should return "main" as fallback on error', async () => { mockSpawn.mockImplementation(() => ({ on: jest.fn((event, callback) => { if (event === 'close') callback(1); // Failure }), stdout: { on: jest.fn() }, stderr: { on: jest.fn() } } as any)); const branch = await patcher.getCurrentBranch(); expect(branch).toBe('main'); }); }); describe('getRecentDiff', () => { it('should return git diff output', async () => { const mockDiff = '--- a/file.gd\n+++ b/file.gd\n@@ -1 +1 @@\n-old\n+new'; mockSpawn.mockImplementation(() => ({ on: jest.fn((event, callback) => { if (event === 'close') callback(0); }), stdout: { on: jest.fn((event, callback) => { if (event === 'data') callback(Buffer.from(mockDiff)); }) }, stderr: { on: jest.fn() } } as any)); const diff = await patcher.getRecentDiff(); expect(diff).toBe(mockDiff); }); }); });

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/Snack-JPG/Godot-Sentinel-MCP'

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