git-branch.tool.test.ts•11.2 kB
/**
 * @fileoverview Unit tests for git-branch tool
 * @module tests/mcp-server/tools/definitions/unit/git-branch.tool.test
 */
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { container } from 'tsyringe';
import { gitBranchTool } from '@/mcp-server/tools/definitions/git-branch.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 { GitBranchResult } from '@/services/git/types.js';
import { GitProviderFactory } from '@/services/git/core/GitProviderFactory.js';
describe('git_branch 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 list operation with defaults', () => {
      const input = { path: '.' };
      const result = gitBranchTool.inputSchema.safeParse(input);
      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.data.operation).toBe('list');
        expect(result.data.force).toBe(false);
        expect(result.data.all).toBe(false);
        expect(result.data.remote).toBe(false);
      }
    });
    it('accepts create operation with branch name', () => {
      const input = { path: '.', operation: 'create', name: 'feature-branch' };
      const result = gitBranchTool.inputSchema.safeParse(input);
      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.data.operation).toBe('create');
        expect(result.data.name).toBe('feature-branch');
      }
    });
    it('accepts delete operation', () => {
      const input = { path: '.', operation: 'delete', name: 'old-branch' };
      const result = gitBranchTool.inputSchema.safeParse(input);
      expect(result.success).toBe(true);
    });
    it('accepts rename operation with newName', () => {
      const input = {
        path: '.',
        operation: 'rename',
        name: 'old-name',
        newName: 'new-name',
      };
      const result = gitBranchTool.inputSchema.safeParse(input);
      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.data.newName).toBe('new-name');
      }
    });
    it('accepts show-current operation', () => {
      const input = { path: '.', operation: 'show-current' };
      const result = gitBranchTool.inputSchema.safeParse(input);
      expect(result.success).toBe(true);
    });
  });
  describe('Tool Logic - List Operation', () => {
    it('lists branches successfully', async () => {
      const mockResult: GitBranchResult = {
        mode: 'list',
        branches: [
          {
            name: 'main',
            current: true,
            commitHash: 'abc123',
            upstream: 'origin/main',
            ahead: 0,
            behind: 0,
          },
          {
            name: 'feature',
            current: false,
            commitHash: 'def456',
          },
        ],
      };
      mockProvider.branch.mockResolvedValue(mockResult);
      const parsedInput = gitBranchTool.inputSchema.parse({
        path: '.',
        operation: 'list',
      });
      const appContext = createTestContext({ tenantId: 'test-tenant' });
      const sdkContext = createTestSdkContext();
      const result = await gitBranchTool.logic(
        parsedInput,
        appContext,
        sdkContext,
      );
      expect(mockProvider.branch).toHaveBeenCalledTimes(1);
      expect(result.success).toBe(true);
      expect(result.operation).toBe('list');
      expect(result.branches).toHaveLength(2);
      expect(result.currentBranch).toBe('main');
    });
    it('passes all flag to provider', async () => {
      const mockResult: GitBranchResult = {
        mode: 'list',
        branches: [],
      };
      mockProvider.branch.mockResolvedValue(mockResult);
      const parsedInput = gitBranchTool.inputSchema.parse({
        path: '.',
        operation: 'list',
        all: true,
      });
      const appContext = createTestContext({ tenantId: 'test-tenant' });
      const sdkContext = createTestSdkContext();
      await gitBranchTool.logic(parsedInput, appContext, sdkContext);
      const [branchOptions] = mockProvider.branch.mock.calls[0]!;
      expect(branchOptions.remote).toBe(true);
    });
  });
  describe('Tool Logic - Create Operation', () => {
    it('creates branch successfully', async () => {
      const mockResult: GitBranchResult = {
        mode: 'create',
        created: 'feature-branch',
      };
      mockProvider.branch.mockResolvedValue(mockResult);
      const parsedInput = gitBranchTool.inputSchema.parse({
        path: '.',
        operation: 'create',
        name: 'feature-branch',
      });
      const appContext = createTestContext({ tenantId: 'test-tenant' });
      const sdkContext = createTestSdkContext();
      const result = await gitBranchTool.logic(
        parsedInput,
        appContext,
        sdkContext,
      );
      expect(result.success).toBe(true);
      expect(result.operation).toBe('create');
      expect(result.message).toContain('feature-branch');
      expect(result.message).toContain('created');
    });
  });
  describe('Tool Logic - Delete Operation', () => {
    it('deletes branch successfully', async () => {
      const mockResult: GitBranchResult = {
        mode: 'delete',
        deleted: 'old-branch',
      };
      mockProvider.branch.mockResolvedValue(mockResult);
      const parsedInput = gitBranchTool.inputSchema.parse({
        path: '.',
        operation: 'delete',
        name: 'old-branch',
      });
      const appContext = createTestContext({ tenantId: 'test-tenant' });
      const sdkContext = createTestSdkContext();
      const result = await gitBranchTool.logic(
        parsedInput,
        appContext,
        sdkContext,
      );
      expect(result.success).toBe(true);
      expect(result.operation).toBe('delete');
      expect(result.message).toContain('old-branch');
      expect(result.message).toContain('deleted');
    });
  });
  describe('Tool Logic - Rename Operation', () => {
    it('renames branch successfully', async () => {
      const mockResult: GitBranchResult = {
        mode: 'rename',
        renamed: { from: 'old-name', to: 'new-name' },
      };
      mockProvider.branch.mockResolvedValue(mockResult);
      const parsedInput = gitBranchTool.inputSchema.parse({
        path: '.',
        operation: 'rename',
        name: 'old-name',
        newName: 'new-name',
      });
      const appContext = createTestContext({ tenantId: 'test-tenant' });
      const sdkContext = createTestSdkContext();
      const result = await gitBranchTool.logic(
        parsedInput,
        appContext,
        sdkContext,
      );
      expect(result.success).toBe(true);
      expect(result.operation).toBe('rename');
      expect(result.message).toContain('old-name');
      expect(result.message).toContain('new-name');
    });
  });
  describe('Response Formatter', () => {
    it('formats branch list with current branch', () => {
      const result = {
        success: true,
        operation: 'list' as const,
        branches: [
          {
            name: 'main',
            current: true,
            commitHash: 'abc123',
            upstream: 'origin/main',
            ahead: 2,
            behind: 1,
          },
          {
            name: 'develop',
            current: false,
            commitHash: 'def456',
          },
        ],
        currentBranch: 'main',
        message: undefined,
      };
      const content = gitBranchTool.responseFormatter!(result);
      assertJsonContent(content, {
        success: true,
        operation: 'list',
        currentBranch: 'main',
      });
      assertJsonField(content, 'currentBranch', 'main');
      assertJsonField(content, 'operation', 'list');
      const parsed = parseJsonContent(content) as {
        branches: Array<{ name: string; current: boolean }>;
      };
      expect(parsed.branches).toHaveLength(2);
      expect(parsed.branches[0]!.name).toBe('main');
      expect(parsed.branches[0]!.current).toBe(true);
      expect(parsed.branches[1]!.name).toBe('develop');
      assertLlmFriendlyFormat(content);
    });
    it('formats create operation result', () => {
      const result = {
        success: true,
        operation: 'create' as const,
        branches: undefined,
        currentBranch: undefined,
        message: "Branch 'feature-x' created successfully.",
      };
      const content = gitBranchTool.responseFormatter!(result);
      assertJsonContent(content, {
        success: true,
        operation: 'create',
      });
      assertJsonField(content, 'operation', 'create');
      assertJsonField(
        content,
        'message',
        "Branch 'feature-x' created successfully.",
      );
    });
    it('formats delete operation result', () => {
      const result = {
        success: true,
        operation: 'delete' as const,
        branches: undefined,
        currentBranch: undefined,
        message: "Branch 'old-branch' deleted successfully.",
      };
      const content = gitBranchTool.responseFormatter!(result);
      assertJsonContent(content, {
        success: true,
        operation: 'delete',
      });
      assertJsonField(content, 'operation', 'delete');
      assertJsonField(
        content,
        'message',
        "Branch 'old-branch' deleted successfully.",
      );
    });
  });
  describe('Tool Metadata', () => {
    it('has correct tool name', () => {
      expect(gitBranchTool.name).toBe('git_branch');
    });
    it('is marked as write operation', () => {
      expect(gitBranchTool.annotations?.readOnlyHint).toBe(false);
    });
    it('has descriptive title and description', () => {
      expect(gitBranchTool.title).toBe('Git Branch');
      expect(gitBranchTool.description).toBeTruthy();
      expect(gitBranchTool.description.toLowerCase()).toContain('branch');
    });
    it('has valid schemas', () => {
      expect(gitBranchTool.inputSchema).toBeDefined();
      expect(gitBranchTool.outputSchema).toBeDefined();
      const inputShape = gitBranchTool.inputSchema.shape;
      expect(inputShape.operation).toBeDefined();
      expect(inputShape.name).toBeDefined();
      const outputShape = gitBranchTool.outputSchema.shape;
      expect(outputShape.success).toBeDefined();
      expect(outputShape.operation).toBeDefined();
    });
  });
});