gitingest-mcp
Version:
MCP server for transforming Git repositories into LLM-friendly text digests
304 lines (253 loc) • 7.51 kB
text/typescript
import { describe, it, expect, vi } from 'vitest';
import { ingestTool } from '../../src/tools/ingest.js';
import { GitCloneTool } from '../../src/tools/git-clone.js';
import { LocalRepositoryTool } from '../../src/tools/local-repository.js';
vi.mock('../../src/tools/filter-engine.js', () => ({
FilterEngine: vi.fn().mockImplementation(() => ({
loadIgnorePatterns: vi.fn(),
shouldIncludeFile: vi.fn().mockReturnValue({ shouldInclude: true }),
})),
}));
describe('ingestTool cancellation handling', () => {
it('should handle cancellation signal from AbortController', async () => {
// Arrange
const controller = new AbortController();
vi.spyOn(LocalRepositoryTool, 'analyze').mockImplementationOnce(
(options, signal) => {
return new Promise((resolve, reject) => {
if (signal?.aborted) {
reject(new Error('Operation cancelled'));
return;
}
const abortHandler = () => {
reject(new Error('Operation cancelled'));
};
if (signal) {
signal.addEventListener('abort', abortHandler);
}
setTimeout(() => {
if (signal) {
signal.removeEventListener('abort', abortHandler);
}
resolve({
path: '/tmp/repo',
summary: {
path: '/tmp/repo',
branch: 'main',
commit: 'abc123',
fileCount: 1,
directoryCount: 1,
totalSize: 25,
tokenCount: 7,
createdAt: new Date().toISOString(),
},
files: [
{
path: 'src/index.ts',
content: 'console.log("Hello World");',
size: 25,
type: 'file' as const,
},
],
tree: {
name: '',
type: 'directory' as const,
children: [
{
name: 'src',
type: 'directory' as const,
children: [
{
name: 'index.ts',
type: 'file' as const,
size: 25,
},
],
},
],
},
});
}, 3000);
});
}
);
// Act
setTimeout(() => {
controller.abort();
}, 1000);
const result = await ingestTool({
repository: '/path/to/local/repo',
cloneDepth: 1,
sparseCheckout: false,
includeSubmodules: false,
includeGitignored: false,
useGitignore: true,
useGitingestignore: true,
maxFiles: 1000,
maxFileSize: undefined,
excludePatterns: [],
includePatterns: [],
maxTotalSize: 50 * 1024 * 1024,
token: undefined,
maxRetries: 3,
retryDelay: 1000,
timeout: 5000,
signal: controller.signal,
}).catch((error) => {
return {
isError: true,
content: [
{
type: 'text',
text: `Operation cancelled: ${error.message}`,
},
],
};
});
// Assert
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Operation cancelled');
});
it('should propagate cancellation to git clone operation', async () => {
// Arrange
const controller = new AbortController();
vi.spyOn(GitCloneTool, 'clone').mockImplementationOnce(
(options, signal) => {
return new Promise((resolve, reject) => {
if (signal?.aborted) {
reject(new Error('Git clone cancelled'));
return;
}
const abortHandler = () => {
reject(new Error('Git clone cancelled'));
};
if (signal) {
signal.addEventListener('abort', abortHandler);
}
setTimeout(() => {
if (signal) {
signal.removeEventListener('abort', abortHandler);
}
resolve({
path: '/tmp/cloned-repo',
branch: 'main',
commit: 'abc123',
isShallow: true,
});
}, 3000);
});
}
);
// Act
setTimeout(() => {
controller.abort();
}, 1000);
const result = await ingestTool({
repository: 'https://github.com/user/repo.git',
cloneDepth: 1,
sparseCheckout: false,
includeSubmodules: false,
includeGitignored: false,
useGitignore: true,
useGitingestignore: true,
maxFiles: 1000,
maxFileSize: undefined,
excludePatterns: [],
includePatterns: [],
maxTotalSize: 50 * 1024 * 1024,
token: undefined,
maxRetries: 3,
retryDelay: 1000,
timeout: 5000,
signal: controller.signal,
}).catch((error) => {
return {
isError: true,
content: [
{
type: 'text',
text: `Operation cancelled: ${error.message}`,
},
],
};
});
// Assert
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Git clone cancelled');
});
it('should handle already aborted signal', async () => {
// Arrange
const controller = new AbortController();
controller.abort();
vi.spyOn(LocalRepositoryTool, 'analyze').mockRejectedValueOnce(
new Error('Operation cancelled')
);
// Act
const result = await ingestTool({
repository: '/path/to/local/repo',
cloneDepth: 1,
sparseCheckout: false,
includeSubmodules: false,
includeGitignored: false,
useGitignore: true,
useGitingestignore: true,
maxFiles: 1000,
maxFileSize: undefined,
excludePatterns: [],
includePatterns: [],
maxTotalSize: 50 * 1024 * 1024,
token: undefined,
maxRetries: 3,
retryDelay: 1000,
timeout: 5000,
signal: controller.signal,
}).catch((error) => {
return {
isError: true,
content: [
{
type: 'text',
text: `Operation cancelled: ${error.message}`,
},
],
};
});
// Assert
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Operation cancelled');
});
it('should handle cancellation gracefully', async () => {
// Arrange
const controller = new AbortController();
vi.spyOn(GitCloneTool, 'clone').mockRejectedValueOnce(
new Error('Operation cancelled')
);
// Act
controller.abort();
const result = await ingestTool({
repository: 'https://github.com/user/repo.git',
cloneDepth: 1,
sparseCheckout: false,
includeSubmodules: false,
includeGitignored: false,
useGitignore: true,
useGitingestignore: true,
maxFiles: 1000,
maxFileSize: undefined,
excludePatterns: [],
includePatterns: [],
maxTotalSize: 50 * 1024 * 1024,
token: undefined,
maxRetries: 3,
retryDelay: 1000,
timeout: 5000,
signal: controller.signal,
}).catch((error) => ({
content: [{ type: 'text', text: error.message }],
isError: true,
}));
// Assert
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('cancelled');
});
});