git-log.tool.test.ts•11.1 kB
/**
* @fileoverview Unit tests for git-log tool
* @module tests/mcp-server/tools/definitions/unit/git-log.tool.test
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { container } from 'tsyringe';
import { gitLogTool } from '@/mcp-server/tools/definitions/git-log.tool.js';
import {
GitProviderFactory as GitProviderFactoryToken,
StorageService as StorageServiceToken,
} from '@/container/tokens.js';
import {
createTestContext,
createTestSdkContext,
createMockGitProvider,
createMockStorageService,
assertJsonContent,
assertJsonField,
parseJsonContent,
assertLlmFriendlyFormat,
} from '../helpers/index.js';
import type { GitLogResult } from '@/services/git/types.js';
import { GitProviderFactory } from '@/services/git/core/GitProviderFactory.js';
describe('git_log tool', () => {
const mockProvider = createMockGitProvider();
const mockStorage = createMockStorageService();
const mockFactory = {
getProvider: vi.fn(async () => mockProvider),
} as unknown as GitProviderFactory;
beforeEach(() => {
mockProvider.resetMocks();
mockStorage.clearAll();
container.clearInstances();
container.register(GitProviderFactoryToken, { useValue: mockFactory });
container.register(StorageServiceToken, { useValue: mockStorage });
const tenantId = 'test-tenant';
const context = createTestContext({ tenantId });
mockStorage.set(`session:workingDir:${tenantId}`, '/test/repo', context);
});
describe('Input Schema', () => {
it('validates correct input with defaults', () => {
const input = { path: '.' };
const result = gitLogTool.inputSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.oneline).toBe(false);
expect(result.data.stat).toBe(false);
expect(result.data.patch).toBe(false);
expect(result.data.showSignature).toBe(false);
}
});
it('accepts maxCount limit', () => {
const input = { path: '.', maxCount: 10 };
const result = gitLogTool.inputSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.maxCount).toBe(10);
}
});
it('accepts author filter', () => {
const input = { path: '.', author: 'John Doe' };
const result = gitLogTool.inputSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.author).toBe('John Doe');
}
});
it('accepts date filters', () => {
const input = {
path: '.',
since: '2024-01-01T00:00:00Z',
until: '2024-12-31T23:59:59Z',
};
const result = gitLogTool.inputSchema.safeParse(input);
expect(result.success).toBe(true);
});
it('accepts grep pattern', () => {
const input = { path: '.', grep: 'fix:' };
const result = gitLogTool.inputSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.grep).toBe('fix:');
}
});
it('accepts boolean flags', () => {
const input = { path: '.', oneline: true, stat: true };
const result = gitLogTool.inputSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.oneline).toBe(true);
expect(result.data.stat).toBe(true);
}
});
});
describe('Tool Logic', () => {
it('retrieves commit history successfully', async () => {
const mockResult: GitLogResult = {
commits: [
{
hash: 'abc123def456',
shortHash: 'abc123d',
author: 'John Doe',
authorEmail: 'john@example.com',
timestamp: 1609459200,
subject: 'Add new feature',
body: 'This adds a great new feature',
parents: ['parent123'],
refs: ['main', 'HEAD'],
},
{
hash: 'def456ghi789',
shortHash: 'def456g',
author: 'Jane Smith',
authorEmail: 'jane@example.com',
timestamp: 1609372800,
subject: 'Fix bug',
parents: ['parent456'],
refs: [],
},
],
totalCount: 2,
};
mockProvider.log.mockResolvedValue(mockResult);
const parsedInput = gitLogTool.inputSchema.parse({ path: '.' });
const appContext = createTestContext({ tenantId: 'test-tenant' });
const sdkContext = createTestSdkContext();
const result = await gitLogTool.logic(
parsedInput,
appContext,
sdkContext,
);
expect(mockProvider.log).toHaveBeenCalledTimes(1);
const [_options, logContext] = mockProvider.log.mock.calls[0]!;
expect(logContext.workingDirectory).toBe('/test/repo');
expect(result.success).toBe(true);
expect(result.commits).toHaveLength(2);
expect(result.totalCount).toBe(2);
expect(result.commits[0]?.hash).toBe('abc123def456');
});
it('passes maxCount to provider', async () => {
const mockResult: GitLogResult = {
commits: [],
totalCount: 0,
};
mockProvider.log.mockResolvedValue(mockResult);
const parsedInput = gitLogTool.inputSchema.parse({
path: '.',
maxCount: 5,
});
const appContext = createTestContext({ tenantId: 'test-tenant' });
const sdkContext = createTestSdkContext();
await gitLogTool.logic(parsedInput, appContext, sdkContext);
const [logOptions] = mockProvider.log.mock.calls[0]!;
expect(logOptions.maxCount).toBe(5);
});
it('passes author filter to provider', async () => {
const mockResult: GitLogResult = {
commits: [],
totalCount: 0,
};
mockProvider.log.mockResolvedValue(mockResult);
const parsedInput = gitLogTool.inputSchema.parse({
path: '.',
author: 'test@example.com',
});
const appContext = createTestContext({ tenantId: 'test-tenant' });
const sdkContext = createTestSdkContext();
await gitLogTool.logic(parsedInput, appContext, sdkContext);
const [logOptions] = mockProvider.log.mock.calls[0]!;
expect(logOptions.author).toBe('test@example.com');
});
it('passes date filters to provider', async () => {
const mockResult: GitLogResult = {
commits: [],
totalCount: 0,
};
mockProvider.log.mockResolvedValue(mockResult);
const parsedInput = gitLogTool.inputSchema.parse({
path: '.',
since: '2024-01-01',
until: '2024-12-31',
});
const appContext = createTestContext({ tenantId: 'test-tenant' });
const sdkContext = createTestSdkContext();
await gitLogTool.logic(parsedInput, appContext, sdkContext);
const [logOptions] = mockProvider.log.mock.calls[0]!;
expect(logOptions.since).toBe('2024-01-01');
expect(logOptions.until).toBe('2024-12-31');
});
it('handles empty commit history', async () => {
const mockResult: GitLogResult = {
commits: [],
totalCount: 0,
};
mockProvider.log.mockResolvedValue(mockResult);
const parsedInput = gitLogTool.inputSchema.parse({ path: '.' });
const appContext = createTestContext({ tenantId: 'test-tenant' });
const sdkContext = createTestSdkContext();
const result = await gitLogTool.logic(
parsedInput,
appContext,
sdkContext,
);
expect(result.commits).toHaveLength(0);
expect(result.totalCount).toBe(0);
});
});
describe('Response Formatter', () => {
it('formats commit history with multiple commits', () => {
const result = {
success: true,
commits: [
{
hash: 'abc123def456',
shortHash: 'abc123d',
author: 'John Doe',
authorEmail: 'john@example.com',
timestamp: 1609459200,
subject: 'Add feature',
body: 'Details here',
parents: ['parent1'],
refs: ['main'],
},
{
hash: 'def456ghi789',
shortHash: 'def456g',
author: 'Jane Smith',
authorEmail: 'jane@example.com',
timestamp: 1609372800,
subject: 'Fix bug',
parents: [],
refs: [],
},
],
totalCount: 2,
};
const content = gitLogTool.responseFormatter!(result);
// Should return JSON format with commits
assertJsonContent(content, {
success: true,
totalCount: 2,
});
const parsed = parseJsonContent(content) as { commits: unknown[] };
expect(parsed.commits).toHaveLength(2);
assertJsonField(content, 'commits', expect.any(Array));
assertLlmFriendlyFormat(content);
});
it('formats empty commit history', () => {
const result = {
success: true,
commits: [],
totalCount: 0,
};
const content = gitLogTool.responseFormatter!(result);
assertJsonContent(content, {
success: true,
commits: [],
totalCount: 0,
});
assertJsonField(content, 'commits', []);
assertLlmFriendlyFormat(content, 20);
});
it('includes commit count', () => {
const result = {
success: true,
commits: [
{
hash: 'abc123',
shortHash: 'abc123',
author: 'Test',
authorEmail: 'test@test.com',
timestamp: 123,
subject: 'Test commit',
parents: [],
refs: [],
},
],
totalCount: 1,
};
const content = gitLogTool.responseFormatter!(result);
assertJsonContent(content, {
success: true,
totalCount: 1,
});
assertJsonField(content, 'totalCount', 1);
const parsed = parseJsonContent(content) as { commits: unknown[] };
expect(parsed.commits).toHaveLength(1);
});
});
describe('Tool Metadata', () => {
it('has correct tool name', () => {
expect(gitLogTool.name).toBe('git_log');
});
it('is marked as read-only operation', () => {
expect(gitLogTool.annotations?.readOnlyHint).toBe(true);
});
it('has descriptive title and description', () => {
expect(gitLogTool.title).toBe('Git Log');
expect(gitLogTool.description).toBeTruthy();
expect(gitLogTool.description.toLowerCase()).toContain('history');
});
it('has valid schemas', () => {
expect(gitLogTool.inputSchema).toBeDefined();
expect(gitLogTool.outputSchema).toBeDefined();
const inputShape = gitLogTool.inputSchema.shape;
expect(inputShape.maxCount).toBeDefined();
expect(inputShape.author).toBeDefined();
expect(inputShape.since).toBeDefined();
const outputShape = gitLogTool.outputSchema.shape;
expect(outputShape.commits).toBeDefined();
expect(outputShape.totalCount).toBeDefined();
});
});
});