#
tokens: 48555/50000 30/281 files (page 3/6)
lines: off (toggle) GitHub
raw markdown copy
This is page 3 of 6. Use http://codebase.md/tiberriver256/azure-devops-mcp?lines=false&page={x} to view the full context.

# Directory Structure

```
├── .clinerules
├── .env.example
├── .eslintrc.json
├── .github
│   ├── FUNDING.yml
│   ├── release-please-config.json
│   ├── release-please-manifest.json
│   └── workflows
│       ├── main.yml
│       └── release-please.yml
├── .gitignore
├── .husky
│   ├── commit-msg
│   └── pre-commit
├── .kilocode
│   └── mcp.json
├── .prettierrc
├── .vscode
│   └── settings.json
├── CHANGELOG.md
├── commitlint.config.js
├── CONTRIBUTING.md
├── create_branch.sh
├── docs
│   ├── authentication.md
│   ├── azure-identity-authentication.md
│   ├── ci-setup.md
│   ├── examples
│   │   ├── azure-cli-authentication.env
│   │   ├── azure-identity-authentication.env
│   │   ├── pat-authentication.env
│   │   └── README.md
│   ├── testing
│   │   ├── README.md
│   │   └── setup.md
│   └── tools
│       ├── core-navigation.md
│       ├── organizations.md
│       ├── pipelines.md
│       ├── projects.md
│       ├── pull-requests.md
│       ├── README.md
│       ├── repositories.md
│       ├── resources.md
│       ├── search.md
│       ├── user-tools.md
│       ├── wiki.md
│       └── work-items.md
├── finish_task.sh
├── jest.e2e.config.js
├── jest.int.config.js
├── jest.unit.config.js
├── LICENSE
├── memory
│   └── tasks_memory_2025-05-26T16-18-03.json
├── package-lock.json
├── package.json
├── project-management
│   ├── planning
│   │   ├── architecture-guide.md
│   │   ├── azure-identity-authentication-design.md
│   │   ├── project-plan.md
│   │   ├── project-structure.md
│   │   ├── tech-stack.md
│   │   └── the-dream-team.md
│   ├── startup.xml
│   ├── tdd-cycle.xml
│   └── troubleshooter.xml
├── README.md
├── setup_env.sh
├── shrimp-rules.md
├── src
│   ├── clients
│   │   └── azure-devops.ts
│   ├── features
│   │   ├── organizations
│   │   │   ├── __test__
│   │   │   │   └── test-helpers.ts
│   │   │   ├── index.spec.unit.ts
│   │   │   ├── index.ts
│   │   │   ├── list-organizations
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── schemas.ts
│   │   │   ├── tool-definitions.ts
│   │   │   └── types.ts
│   │   ├── pipelines
│   │   │   ├── get-pipeline
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── index.spec.unit.ts
│   │   │   ├── index.ts
│   │   │   ├── list-pipelines
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── tool-definitions.ts
│   │   │   ├── trigger-pipeline
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   └── types.ts
│   │   ├── projects
│   │   │   ├── __test__
│   │   │   │   └── test-helpers.ts
│   │   │   ├── get-project
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── get-project-details
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── index.spec.unit.ts
│   │   │   ├── index.ts
│   │   │   ├── list-projects
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── schemas.ts
│   │   │   ├── tool-definitions.ts
│   │   │   └── types.ts
│   │   ├── pull-requests
│   │   │   ├── add-pull-request-comment
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   └── index.ts
│   │   │   ├── create-pull-request
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── get-pull-request-comments
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   └── index.ts
│   │   │   ├── index.spec.unit.ts
│   │   │   ├── index.ts
│   │   │   ├── list-pull-requests
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── schemas.ts
│   │   │   ├── tool-definitions.ts
│   │   │   ├── types.ts
│   │   │   └── update-pull-request
│   │   │       ├── feature.spec.int.ts
│   │   │       ├── feature.spec.unit.ts
│   │   │       ├── feature.ts
│   │   │       └── index.ts
│   │   ├── repositories
│   │   │   ├── __test__
│   │   │   │   └── test-helpers.ts
│   │   │   ├── get-all-repositories-tree
│   │   │   │   ├── __snapshots__
│   │   │   │   │   └── feature.spec.unit.ts.snap
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── get-file-content
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── get-repository
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── get-repository-details
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── index.spec.unit.ts
│   │   │   ├── index.ts
│   │   │   ├── list-repositories
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── schemas.ts
│   │   │   ├── tool-definitions.ts
│   │   │   └── types.ts
│   │   ├── search
│   │   │   ├── index.spec.unit.ts
│   │   │   ├── index.ts
│   │   │   ├── schemas.ts
│   │   │   ├── search-code
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   └── index.ts
│   │   │   ├── search-wiki
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   └── index.ts
│   │   │   ├── search-work-items
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   └── index.ts
│   │   │   ├── tool-definitions.ts
│   │   │   └── types.ts
│   │   ├── users
│   │   │   ├── get-me
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── index.spec.unit.ts
│   │   │   ├── index.ts
│   │   │   ├── schemas.ts
│   │   │   ├── tool-definitions.ts
│   │   │   └── types.ts
│   │   ├── wikis
│   │   │   ├── create-wiki
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── create-wiki-page
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── get-wiki-page
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── get-wikis
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── index.spec.unit.ts
│   │   │   ├── index.ts
│   │   │   ├── list-wiki-pages
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── tool-definitions.ts
│   │   │   └── update-wiki-page
│   │   │       ├── feature.spec.int.ts
│   │   │       ├── feature.ts
│   │   │       ├── index.ts
│   │   │       └── schema.ts
│   │   └── work-items
│   │       ├── __test__
│   │       │   ├── fixtures.ts
│   │       │   ├── test-helpers.ts
│   │       │   └── test-utils.ts
│   │       ├── create-work-item
│   │       │   ├── feature.spec.int.ts
│   │       │   ├── feature.spec.unit.ts
│   │       │   ├── feature.ts
│   │       │   ├── index.ts
│   │       │   └── schema.ts
│   │       ├── get-work-item
│   │       │   ├── feature.spec.int.ts
│   │       │   ├── feature.spec.unit.ts
│   │       │   ├── feature.ts
│   │       │   ├── index.ts
│   │       │   └── schema.ts
│   │       ├── index.spec.unit.ts
│   │       ├── index.ts
│   │       ├── list-work-items
│   │       │   ├── feature.spec.int.ts
│   │       │   ├── feature.spec.unit.ts
│   │       │   ├── feature.ts
│   │       │   ├── index.ts
│   │       │   └── schema.ts
│   │       ├── manage-work-item-link
│   │       │   ├── feature.spec.int.ts
│   │       │   ├── feature.spec.unit.ts
│   │       │   ├── feature.ts
│   │       │   ├── index.ts
│   │       │   └── schema.ts
│   │       ├── schemas.ts
│   │       ├── tool-definitions.ts
│   │       ├── types.ts
│   │       └── update-work-item
│   │           ├── feature.spec.int.ts
│   │           ├── feature.spec.unit.ts
│   │           ├── feature.ts
│   │           ├── index.ts
│   │           └── schema.ts
│   ├── index.spec.unit.ts
│   ├── index.ts
│   ├── server.spec.e2e.ts
│   ├── server.ts
│   ├── shared
│   │   ├── api
│   │   │   ├── client.ts
│   │   │   └── index.ts
│   │   ├── auth
│   │   │   ├── auth-factory.ts
│   │   │   ├── client-factory.ts
│   │   │   └── index.ts
│   │   ├── config
│   │   │   ├── index.ts
│   │   │   └── version.ts
│   │   ├── enums
│   │   │   ├── index.spec.unit.ts
│   │   │   └── index.ts
│   │   ├── errors
│   │   │   ├── azure-devops-errors.ts
│   │   │   ├── handle-request-error.ts
│   │   │   └── index.ts
│   │   ├── test
│   │   │   └── test-helpers.ts
│   │   └── types
│   │       ├── config.ts
│   │       ├── index.ts
│   │       ├── request-handler.ts
│   │       └── tool-definition.ts
│   └── utils
│       ├── environment.spec.unit.ts
│       └── environment.ts
├── tasks.json
├── tests
│   └── setup.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/src/features/search/index.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
import { isSearchRequest, handleSearchRequest } from './index';
import { searchCode } from './search-code';
import { searchWiki } from './search-wiki';
import { searchWorkItems } from './search-work-items';

// Mock the imported modules
jest.mock('./search-code', () => ({
  searchCode: jest.fn(),
}));

jest.mock('./search-wiki', () => ({
  searchWiki: jest.fn(),
}));

jest.mock('./search-work-items', () => ({
  searchWorkItems: jest.fn(),
}));

describe('Search Request Handlers', () => {
  const mockConnection = {} as WebApi;

  describe('isSearchRequest', () => {
    it('should return true for search requests', () => {
      const validTools = ['search_code', 'search_wiki', 'search_work_items'];
      validTools.forEach((tool) => {
        const request = {
          params: { name: tool, arguments: {} },
          method: 'tools/call',
        } as CallToolRequest;
        expect(isSearchRequest(request)).toBe(true);
      });
    });

    it('should return false for non-search requests', () => {
      const request = {
        params: { name: 'list_projects', arguments: {} },
        method: 'tools/call',
      } as CallToolRequest;
      expect(isSearchRequest(request)).toBe(false);
    });
  });

  describe('handleSearchRequest', () => {
    it('should handle search_code request', async () => {
      const mockSearchResults = {
        count: 2,
        results: [
          { fileName: 'file1.ts', path: '/path/to/file1.ts' },
          { fileName: 'file2.ts', path: '/path/to/file2.ts' },
        ],
      };
      (searchCode as jest.Mock).mockResolvedValue(mockSearchResults);

      const request = {
        params: {
          name: 'search_code',
          arguments: {
            searchText: 'function',
            projectId: 'project1',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      const response = await handleSearchRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(
        mockSearchResults,
      );
      expect(searchCode).toHaveBeenCalledWith(
        mockConnection,
        expect.objectContaining({
          searchText: 'function',
          projectId: 'project1',
        }),
      );
    });

    it('should handle search_wiki request', async () => {
      const mockSearchResults = {
        count: 1,
        results: [{ title: 'Wiki Page', path: '/path/to/page' }],
      };
      (searchWiki as jest.Mock).mockResolvedValue(mockSearchResults);

      const request = {
        params: {
          name: 'search_wiki',
          arguments: {
            searchText: 'documentation',
            projectId: 'project1',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      const response = await handleSearchRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(
        mockSearchResults,
      );
      expect(searchWiki).toHaveBeenCalledWith(
        mockConnection,
        expect.objectContaining({
          searchText: 'documentation',
          projectId: 'project1',
        }),
      );
    });

    it('should handle search_work_items request', async () => {
      const mockSearchResults = {
        count: 2,
        results: [
          { id: 1, title: 'Bug 1' },
          { id: 2, title: 'Feature 2' },
        ],
      };
      (searchWorkItems as jest.Mock).mockResolvedValue(mockSearchResults);

      const request = {
        params: {
          name: 'search_work_items',
          arguments: {
            searchText: 'bug',
            projectId: 'project1',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      const response = await handleSearchRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(
        mockSearchResults,
      );
      expect(searchWorkItems).toHaveBeenCalledWith(
        mockConnection,
        expect.objectContaining({
          searchText: 'bug',
          projectId: 'project1',
        }),
      );
    });

    it('should throw error for unknown tool', async () => {
      const request = {
        params: {
          name: 'unknown_tool',
          arguments: {},
        },
        method: 'tools/call',
      } as CallToolRequest;

      await expect(
        handleSearchRequest(mockConnection, request),
      ).rejects.toThrow('Unknown search tool');
    });

    it('should propagate errors from search functions', async () => {
      const mockError = new Error('Test error');
      (searchCode as jest.Mock).mockRejectedValue(mockError);

      const request = {
        params: {
          name: 'search_code',
          arguments: {
            searchText: 'function',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      await expect(
        handleSearchRequest(mockConnection, request),
      ).rejects.toThrow(mockError);
    });
  });
});

```

--------------------------------------------------------------------------------
/src/shared/auth/auth-factory.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi, getPersonalAccessTokenHandler } from 'azure-devops-node-api';
import { BearerCredentialHandler } from 'azure-devops-node-api/handlers/bearertoken';
import { DefaultAzureCredential, AzureCliCredential } from '@azure/identity';
import { AzureDevOpsAuthenticationError } from '../errors';

/**
 * Authentication methods supported by the Azure DevOps client
 */
export enum AuthenticationMethod {
  /**
   * Personal Access Token authentication
   */
  PersonalAccessToken = 'pat',

  /**
   * Azure Identity authentication (DefaultAzureCredential)
   */
  AzureIdentity = 'azure-identity',

  /**
   * Azure CLI authentication (AzureCliCredential)
   */
  AzureCli = 'azure-cli',
}

/**
 * Authentication configuration for Azure DevOps
 */
export interface AuthConfig {
  /**
   * Authentication method to use
   */
  method: AuthenticationMethod;

  /**
   * Organization URL (e.g., https://dev.azure.com/myorg)
   */
  organizationUrl: string;

  /**
   * Personal Access Token for Azure DevOps (required for PAT authentication)
   */
  personalAccessToken?: string;
}

/**
 * Azure DevOps resource ID for token acquisition
 */
const AZURE_DEVOPS_RESOURCE_ID = '499b84ac-1321-427f-aa17-267ca6975798';

/**
 * Creates an authenticated client for Azure DevOps API based on the specified authentication method
 *
 * @param config Authentication configuration
 * @returns Authenticated WebApi client
 * @throws {AzureDevOpsAuthenticationError} If authentication fails
 */
export async function createAuthClient(config: AuthConfig): Promise<WebApi> {
  if (!config.organizationUrl) {
    throw new AzureDevOpsAuthenticationError('Organization URL is required');
  }

  try {
    let client: WebApi;

    switch (config.method) {
      case AuthenticationMethod.PersonalAccessToken:
        client = await createPatClient(config);
        break;
      case AuthenticationMethod.AzureIdentity:
        client = await createAzureIdentityClient(config);
        break;
      case AuthenticationMethod.AzureCli:
        client = await createAzureCliClient(config);
        break;
      default:
        throw new AzureDevOpsAuthenticationError(
          `Unsupported authentication method: ${config.method}`,
        );
    }

    // Test the connection
    const locationsApi = await client.getLocationsApi();
    await locationsApi.getResourceAreas();

    return client;
  } catch (error) {
    if (error instanceof AzureDevOpsAuthenticationError) {
      throw error;
    }
    throw new AzureDevOpsAuthenticationError(
      `Failed to authenticate with Azure DevOps: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

/**
 * Creates a client using Personal Access Token authentication
 *
 * @param config Authentication configuration
 * @returns Authenticated WebApi client
 * @throws {AzureDevOpsAuthenticationError} If PAT is missing or authentication fails
 */
async function createPatClient(config: AuthConfig): Promise<WebApi> {
  if (!config.personalAccessToken) {
    throw new AzureDevOpsAuthenticationError(
      'Personal Access Token is required',
    );
  }

  // Create authentication handler using PAT
  const authHandler = getPersonalAccessTokenHandler(config.personalAccessToken);

  // Create API client with the auth handler
  return new WebApi(config.organizationUrl, authHandler);
}

/**
 * Creates a client using DefaultAzureCredential authentication
 *
 * @param config Authentication configuration
 * @returns Authenticated WebApi client
 * @throws {AzureDevOpsAuthenticationError} If token acquisition fails
 */
async function createAzureIdentityClient(config: AuthConfig): Promise<WebApi> {
  try {
    // Create DefaultAzureCredential
    const credential = new DefaultAzureCredential();

    // Get token for Azure DevOps
    const token = await credential.getToken(
      `${AZURE_DEVOPS_RESOURCE_ID}/.default`,
    );

    if (!token || !token.token) {
      throw new Error('Failed to acquire token');
    }

    // Create bearer token handler
    const authHandler = new BearerCredentialHandler(token.token);

    // Create API client with the auth handler
    return new WebApi(config.organizationUrl, authHandler);
  } catch (error) {
    throw new AzureDevOpsAuthenticationError(
      `Failed to acquire Azure Identity token: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

/**
 * Creates a client using AzureCliCredential authentication
 *
 * @param config Authentication configuration
 * @returns Authenticated WebApi client
 * @throws {AzureDevOpsAuthenticationError} If token acquisition fails
 */
async function createAzureCliClient(config: AuthConfig): Promise<WebApi> {
  try {
    // Create AzureCliCredential
    const credential = new AzureCliCredential();

    // Get token for Azure DevOps
    const token = await credential.getToken(
      `${AZURE_DEVOPS_RESOURCE_ID}/.default`,
    );

    if (!token || !token.token) {
      throw new Error('Failed to acquire token');
    }

    // Create bearer token handler
    const authHandler = new BearerCredentialHandler(token.token);

    // Create API client with the auth handler
    return new WebApi(config.organizationUrl, authHandler);
  } catch (error) {
    throw new AzureDevOpsAuthenticationError(
      `Failed to acquire Azure CLI token: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

```

--------------------------------------------------------------------------------
/src/features/pull-requests/list-pull-requests/feature.spec.int.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import { PullRequest } from '../types';
import { listPullRequests } from './feature';
import { createPullRequest } from '../create-pull-request/feature';

import {
  getTestConnection,
  shouldSkipIntegrationTest,
} from '../../../shared/test/test-helpers';

describe('listPullRequests integration', () => {
  let connection: WebApi | null = null;
  let testPullRequest: PullRequest | null = null;
  let projectName: string;
  let repositoryName: string;

  // Generate unique branch name and PR title using timestamp
  const timestamp = Date.now();
  const randomSuffix = Math.floor(Math.random() * 1000);
  const uniqueBranchName = `test-branch-${timestamp}-${randomSuffix}`;
  const uniqueTitle = `Test PR ${timestamp}-${randomSuffix}`;

  beforeAll(async () => {
    // Get a real connection using environment variables
    connection = await getTestConnection();

    // Set up project and repository names from environment
    projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
    repositoryName = process.env.AZURE_DEVOPS_DEFAULT_REPOSITORY || '';

    // Skip setup if integration tests should be skipped
    if (shouldSkipIntegrationTest() || !connection) {
      return;
    }
  });

  afterAll(async () => {
    // Clean up created resources if needed
    if (
      testPullRequest &&
      testPullRequest.pullRequestId &&
      !shouldSkipIntegrationTest()
    ) {
      try {
        // Abandon the test pull request if it was created
        const gitApi = await connection?.getGitApi();
        if (gitApi) {
          await gitApi.updatePullRequest(
            {
              status: 2, // 2 = Abandoned
            },
            repositoryName,
            testPullRequest.pullRequestId,
            projectName,
          );
        }
      } catch (error) {
        console.error('Error cleaning up test pull request:', error);
      }
    }
  });

  test('should list pull requests from repository', async () => {
    // Skip if integration tests should be skipped
    if (shouldSkipIntegrationTest() || !connection) {
      console.log('Skipping test due to missing connection');
      return;
    }

    // Skip if repository name is not defined
    if (!repositoryName) {
      console.log('Skipping test due to missing repository name');
      return;
    }

    try {
      // Create a branch for testing
      const gitApi = await connection.getGitApi();

      // Get the default branch info
      const repository = await gitApi.getRepository(
        repositoryName,
        projectName,
      );

      if (!repository || !repository.defaultBranch) {
        throw new Error('Cannot find repository or default branch');
      }

      // Get the commit to branch from
      const commits = await gitApi.getCommits(
        repositoryName,
        {
          itemVersion: {
            versionType: 0, // commit
            version: repository.defaultBranch.replace('refs/heads/', ''),
          },
          $top: 1,
        },
        projectName,
      );

      if (!commits || commits.length === 0) {
        throw new Error('Cannot find commits in repository');
      }

      // Create a new branch
      const refUpdate = {
        name: `refs/heads/${uniqueBranchName}`,
        oldObjectId: '0000000000000000000000000000000000000000',
        newObjectId: commits[0].commitId,
      };

      const updateResult = await gitApi.updateRefs(
        [refUpdate],
        repositoryName,
        projectName,
      );

      if (
        !updateResult ||
        updateResult.length === 0 ||
        !updateResult[0].success
      ) {
        throw new Error('Failed to create new branch');
      }

      // Create a test pull request
      testPullRequest = await createPullRequest(
        connection,
        projectName,
        repositoryName,
        {
          title: uniqueTitle,
          description: 'Test pull request for integration testing',
          sourceRefName: `refs/heads/${uniqueBranchName}`,
          targetRefName: repository.defaultBranch,
          isDraft: true,
        },
      );

      // List pull requests
      const pullRequests = await listPullRequests(
        connection,
        projectName,
        repositoryName,
        { projectId: projectName, repositoryId: repositoryName },
      );

      // Verify
      expect(pullRequests).toBeDefined();
      expect(pullRequests.value).toBeDefined();
      expect(Array.isArray(pullRequests.value)).toBe(true);
      expect(typeof pullRequests.count).toBe('number');
      expect(typeof pullRequests.hasMoreResults).toBe('boolean');

      // Find our test PR in the list
      const foundPR = pullRequests.value.find(
        (pr) => pr.pullRequestId === testPullRequest?.pullRequestId,
      );
      expect(foundPR).toBeDefined();
      expect(foundPR?.title).toBe(uniqueTitle);

      // Test with filters
      const filteredPRs = await listPullRequests(
        connection,
        projectName,
        repositoryName,
        {
          projectId: projectName,
          repositoryId: repositoryName,
          status: 'active',
          top: 5,
        },
      );

      expect(filteredPRs).toBeDefined();
      expect(filteredPRs.value).toBeDefined();
      expect(Array.isArray(filteredPRs.value)).toBe(true);
      expect(filteredPRs.count).toBeGreaterThanOrEqual(0);
    } catch (error) {
      console.error('Test error:', error);
      throw error;
    }
  }, 30000); // 30 second timeout for integration test
});

```

--------------------------------------------------------------------------------
/src/features/search/search-work-items/feature.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import axios from 'axios';
import { DefaultAzureCredential, AzureCliCredential } from '@azure/identity';
import {
  AzureDevOpsError,
  AzureDevOpsResourceNotFoundError,
  AzureDevOpsValidationError,
  AzureDevOpsPermissionError,
} from '../../../shared/errors';
import {
  SearchWorkItemsOptions,
  WorkItemSearchRequest,
  WorkItemSearchResponse,
} from '../types';

/**
 * Search for work items in Azure DevOps projects
 *
 * @param connection The Azure DevOps WebApi connection
 * @param options Parameters for searching work items
 * @returns Search results with work item details and highlights
 */
export async function searchWorkItems(
  connection: WebApi,
  options: SearchWorkItemsOptions,
): Promise<WorkItemSearchResponse> {
  try {
    // Prepare the search request
    const searchRequest: WorkItemSearchRequest = {
      searchText: options.searchText,
      $skip: options.skip,
      $top: options.top,
      filters: {
        ...(options.projectId
          ? { 'System.TeamProject': [options.projectId] }
          : {}),
        ...options.filters,
      },
      includeFacets: options.includeFacets,
      $orderBy: options.orderBy,
    };

    // Get the authorization header from the connection
    const authHeader = await getAuthorizationHeader();

    // Extract organization and project from the connection URL
    const { organization, project } = extractOrgAndProject(
      connection,
      options.projectId,
    );

    // Make the search API request
    // If projectId is provided, include it in the URL, otherwise perform organization-wide search
    const searchUrl = options.projectId
      ? `https://almsearch.dev.azure.com/${organization}/${project}/_apis/search/workitemsearchresults?api-version=7.1`
      : `https://almsearch.dev.azure.com/${organization}/_apis/search/workitemsearchresults?api-version=7.1`;

    const searchResponse = await axios.post<WorkItemSearchResponse>(
      searchUrl,
      searchRequest,
      {
        headers: {
          Authorization: authHeader,
          'Content-Type': 'application/json',
        },
      },
    );

    return searchResponse.data;
  } catch (error) {
    // If it's already an AzureDevOpsError, rethrow it
    if (error instanceof AzureDevOpsError) {
      throw error;
    }

    // Handle axios errors
    if (axios.isAxiosError(error)) {
      const status = error.response?.status;
      const message = error.response?.data?.message || error.message;

      if (status === 404) {
        throw new AzureDevOpsResourceNotFoundError(
          `Resource not found: ${message}`,
        );
      } else if (status === 400) {
        throw new AzureDevOpsValidationError(
          `Invalid request: ${message}`,
          error.response?.data,
        );
      } else if (status === 401 || status === 403) {
        throw new AzureDevOpsPermissionError(`Permission denied: ${message}`);
      } else {
        // For other axios errors, wrap in a generic AzureDevOpsError
        throw new AzureDevOpsError(`Azure DevOps API error: ${message}`);
      }
      // This code is unreachable but TypeScript doesn't know that
    }

    // Otherwise, wrap it in a generic error
    throw new AzureDevOpsError(
      `Failed to search work items: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

/**
 * Extract organization and project from the connection URL
 *
 * @param connection The Azure DevOps WebApi connection
 * @param projectId The project ID or name (optional)
 * @returns The organization and project
 */
function extractOrgAndProject(
  connection: WebApi,
  projectId?: string,
): { organization: string; project: string } {
  // Extract organization from the connection URL
  const url = connection.serverUrl;
  const match = url.match(/https?:\/\/dev\.azure\.com\/([^/]+)/);
  const organization = match ? match[1] : '';

  if (!organization) {
    throw new AzureDevOpsValidationError(
      'Could not extract organization from connection URL',
    );
  }

  return {
    organization,
    project: projectId || '',
  };
}

/**
 * Get the authorization header from the connection
 *
 * @returns The authorization header
 */
async function getAuthorizationHeader(): Promise<string> {
  try {
    // For PAT authentication, we can construct the header directly
    if (
      process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'pat' &&
      process.env.AZURE_DEVOPS_PAT
    ) {
      // For PAT auth, we can construct the Basic auth header directly
      const token = process.env.AZURE_DEVOPS_PAT;
      const base64Token = Buffer.from(`:${token}`).toString('base64');
      return `Basic ${base64Token}`;
    }

    // For Azure Identity / Azure CLI auth, we need to get a token
    // using the Azure DevOps resource ID
    // Choose the appropriate credential based on auth method
    const credential =
      process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'azure-cli'
        ? new AzureCliCredential()
        : new DefaultAzureCredential();

    // Azure DevOps resource ID for token acquisition
    const AZURE_DEVOPS_RESOURCE_ID = '499b84ac-1321-427f-aa17-267ca6975798';

    // Get token for Azure DevOps
    const token = await credential.getToken(
      `${AZURE_DEVOPS_RESOURCE_ID}/.default`,
    );

    if (!token || !token.token) {
      throw new Error('Failed to acquire token for Azure DevOps');
    }

    return `Bearer ${token.token}`;
  } catch (error) {
    throw new AzureDevOpsValidationError(
      `Failed to get authorization header: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

```

--------------------------------------------------------------------------------
/docs/tools/wiki.md:
--------------------------------------------------------------------------------

```markdown
# Azure DevOps Wiki Tools

This document describes the tools available for working with Azure DevOps wikis.

## get_wikis

Lists all wikis in a project or organization.

### Description

The `get_wikis` tool retrieves all wikis available in a specified Azure DevOps project or organization. This is useful for discovering which wikis are available before working with specific wiki pages.

### Parameters

- `organizationId` (optional): The ID or name of the organization. If not provided, the default organization from environment settings will be used.
- `projectId` (optional): The ID or name of the project. If not provided, the default project from environment settings will be used.

```json
{
  "organizationId": "MyOrganization",
  "projectId": "MyProject"
}
```

### Response

The tool returns an array of wiki objects, each containing:

- `id`: The unique identifier of the wiki
- `name`: The name of the wiki
- `url`: The URL of the wiki
- Other wiki properties such as `remoteUrl` and `type`

Example response:

```json
[
  {
    "id": "wiki1-id",
    "name": "MyWiki",
    "type": "projectWiki",
    "url": "https://dev.azure.com/MyOrganization/MyProject/_wiki/wikis/MyWiki",
    "remoteUrl": "https://dev.azure.com/MyOrganization/MyProject/_git/MyWiki"
  }
]
```

## get_wiki_page

Gets the content of a specific wiki page.

### Description

The `get_wiki_page` tool retrieves the content of a specified wiki page as plain text. This is useful for viewing the content of wiki pages programmatically.

### Parameters

- `organizationId` (optional): The ID or name of the organization. If not provided, the default organization from environment settings will be used.
- `projectId` (optional): The ID or name of the project. If not provided, the default project from environment settings will be used.
- `wikiId` (required): The ID or name of the wiki containing the page.
- `pagePath` (required): The path of the page within the wiki (e.g., "/Home" or "/Folder/Page").

```json
{
  "organizationId": "MyOrganization",
  "projectId": "MyProject",
  "wikiId": "MyWiki",
  "pagePath": "/Home"
}
```

### Response

The tool returns the content of the wiki page as a string in markdown format.

Example response:

```markdown
# Welcome to the Wiki

This is the home page of the wiki.

## Getting Started

Here are some links to help you get started:
- [Documentation](/Documentation)
- [Tutorials](/Tutorials)
- [FAQ](/FAQ)
```

### Error Handling

The tool may throw the following errors:

- `AzureDevOpsResourceNotFoundError`: If the specified wiki or page does not exist
- `AzureDevOpsPermissionError`: If the authenticated user does not have permission to access the wiki
- General errors: If other unexpected errors occur during the request

### Example Usage

```typescript
// Example MCP client call
const result = await mcpClient.callTool('get_wiki_page', {
  projectId: 'MyProject',
  wikiId: 'MyWiki',
  pagePath: '/Home'
});
console.log(result);
```

### Implementation Details

This tool uses the Azure DevOps REST API to retrieve the wiki page content with the `Accept: text/plain` header to get the content directly in text format. The page path is properly encoded to handle spaces and special characters in the URL.

## list_wiki_pages

Lists all pages within a specified Azure DevOps wiki.

### Description

The `list_wiki_pages` tool retrieves a list of all pages within a specified wiki. It returns summary information for each page, including the page ID, path, URL, and order. This is useful for discovering the structure and contents of a wiki before working with specific pages.

### Parameters

- `organizationId` (optional): The ID or name of the organization. If not provided, the default organization from environment settings will be used.
- `projectId` (optional): The ID or name of the project. If not provided, the default project from environment settings will be used.
- `wikiId` (required): The ID or name of the wiki to list pages from.

```json
{
  "organizationId": "MyOrganization",
  "projectId": "MyProject",
  "wikiId": "MyWiki"
}
```

### Response

The tool returns an array of wiki page summary objects, each containing:

- `id`: The unique numeric identifier of the page
- `path`: The path of the page within the wiki (e.g., "/Home" or "/Folder/Page")
- `url`: The URL to access the page (optional)
- `order`: The display order of the page (optional)

Example response:

```json
[
  {
    "id": 1,
    "path": "/Home",
    "url": "https://dev.azure.com/MyOrganization/MyProject/_wiki/wikis/MyWiki/1/Home",
    "order": 0
  },
  {
    "id": 2,
    "path": "/Documentation",
    "url": "https://dev.azure.com/MyOrganization/MyProject/_wiki/wikis/MyWiki/2/Documentation",
    "order": 1
  },
  {
    "id": 3,
    "path": "/Documentation/Getting-Started",
    "url": "https://dev.azure.com/MyOrganization/MyProject/_wiki/wikis/MyWiki/3/Getting-Started",
    "order": 2
  }
]
```

### Error Handling

The tool may throw the following errors:

- `AzureDevOpsResourceNotFoundError`: If the specified wiki does not exist
- `AzureDevOpsPermissionError`: If the authenticated user does not have permission to access the wiki
- `AzureDevOpsError`: If other unexpected errors occur during the request

### Example Usage

```typescript
// Example MCP client call
const result = await mcpClient.callTool('list_wiki_pages', {
  projectId: 'MyProject',
  wikiId: 'MyWiki'
});
console.log(result);
```

### Implementation Details

This tool uses the Azure DevOps REST API to retrieve the list of pages within a wiki. The response is mapped to provide a consistent interface with page ID, path, URL, and order information. 
```

--------------------------------------------------------------------------------
/docs/azure-identity-authentication.md:
--------------------------------------------------------------------------------

```markdown
# Azure Identity Authentication for Azure DevOps MCP Server

This guide explains how to use Azure Identity authentication with the Azure DevOps MCP Server.

## Overview

Azure Identity authentication lets you use your existing Azure credentials to authenticate with Azure DevOps, instead of creating and managing Personal Access Tokens (PATs). This approach offers several benefits:

- **Unified authentication**: Use the same credentials for Azure and Azure DevOps
- **Enhanced security**: Support for managed identities and client certificates
- **Flexible credential types**: Multiple options for different environments
- **Automatic token management**: Handles token acquisition and renewal

## Credential Types

The Azure DevOps MCP Server supports multiple credential types through the Azure Identity SDK:

### DefaultAzureCredential

This credential type attempts multiple authentication methods in sequence until one succeeds:

1. Environment variables (EnvironmentCredential)
2. Managed Identity (ManagedIdentityCredential)
3. Azure CLI (AzureCliCredential)
4. Visual Studio Code (VisualStudioCodeCredential)
5. Azure PowerShell (AzurePowerShellCredential)

It's a great option for applications that need to work across different environments without code changes.

### AzureCliCredential

This credential type uses your Azure CLI login. It's perfect for local development when you're already using the Azure CLI.

## Configuration

### Environment Variables

To use Azure Identity authentication, set the following environment variables:

```bash
# Required
AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
AZURE_DEVOPS_AUTH_METHOD=azure-identity

# Optional
AZURE_DEVOPS_DEFAULT_PROJECT=your-project-name
```

For service principal authentication, add these environment variables:

```bash
AZURE_TENANT_ID=your-tenant-id
AZURE_CLIENT_ID=your-client-id
AZURE_CLIENT_SECRET=your-client-secret
```

### Use with Claude Desktop/Cursor AI

Add the following to your configuration file:

```json
{
  "mcpServers": {
    "azureDevOps": {
      "command": "npx",
      "args": ["-y", "@tiberriver256/mcp-server-azure-devops"],
      "env": {
        "AZURE_DEVOPS_ORG_URL": "https://dev.azure.com/your-organization",
        "AZURE_DEVOPS_AUTH_METHOD": "azure-identity",
        "AZURE_DEVOPS_DEFAULT_PROJECT": "your-project-name"
      }
    }
  }
}
```

## Authentication Methods

### Method 1: Using Azure CLI

1. Install the Azure CLI from [here](https://docs.microsoft.com/cli/azure/install-azure-cli)
2. Log in to Azure:
   ```bash
   az login
   ```
3. Set up your environment variables:
   ```bash
   AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
   AZURE_DEVOPS_AUTH_METHOD=azure-identity
   ```

### Method 2: Using Service Principal

1. Create a service principal in Azure AD:
   ```bash
   az ad sp create-for-rbac --name "MyAzureDevOpsApp"
   ```
2. Grant the service principal access to your Azure DevOps organization
3. Set up your environment variables:
   ```bash
   AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
   AZURE_DEVOPS_AUTH_METHOD=azure-identity
   AZURE_TENANT_ID=your-tenant-id
   AZURE_CLIENT_ID=your-client-id
   AZURE_CLIENT_SECRET=your-client-secret
   ```

### Method 3: Using Managed Identity (for Azure-hosted applications)

1. Enable managed identity for your Azure resource (VM, App Service, etc.)
2. Grant the managed identity access to your Azure DevOps organization
3. Set up your environment variables:
   ```bash
   AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
   AZURE_DEVOPS_AUTH_METHOD=azure-identity
   ```

## Troubleshooting

### Common Issues

#### Failed to acquire token

```
Error: Failed to authenticate with Azure Identity: CredentialUnavailableError: DefaultAzureCredential failed to retrieve a token
```

**Possible solutions:**
- Ensure you're logged in with `az login`
- Check if your managed identity is correctly configured
- Verify that service principal credentials are correct

#### Permission issues

```
Error: Failed to authenticate with Azure Identity: AuthorizationFailed: The client does not have authorization to perform action
```

**Possible solutions:**
- Ensure your identity has the necessary permissions in Azure DevOps
- Check if you need to add your identity to specific Azure DevOps project(s)

#### Network issues

```
Error: Failed to authenticate with Azure Identity: ClientAuthError: Interaction required
```

**Possible solutions:**
- Check your network connectivity
- Verify that your firewall allows connections to Azure services

## Best Practices

1. **Choose the right credential type for your environment**:
   - For local development: Azure CLI credential
   - For CI/CD pipelines: Service principal
   - For Azure-hosted applications: Managed identity

2. **Follow the principle of least privilege**:
   - Only grant the permissions needed for your use case
   - Regularly audit and review permissions

3. **Rotate credentials regularly**:
   - For service principals, rotate client secrets periodically
   - Use certificate-based authentication when possible for enhanced security

## Examples

### Basic configuration with Azure CLI

```bash
AZURE_DEVOPS_ORG_URL=https://dev.azure.com/mycompany
AZURE_DEVOPS_AUTH_METHOD=azure-identity
AZURE_DEVOPS_DEFAULT_PROJECT=MyProject
```

### Service principal authentication

```bash
AZURE_DEVOPS_ORG_URL=https://dev.azure.com/mycompany
AZURE_DEVOPS_AUTH_METHOD=azure-identity
AZURE_DEVOPS_DEFAULT_PROJECT=MyProject
AZURE_TENANT_ID=00000000-0000-0000-0000-000000000000
AZURE_CLIENT_ID=11111111-1111-1111-1111-111111111111
AZURE_CLIENT_SECRET=your-client-secret
```

```

--------------------------------------------------------------------------------
/src/features/pull-requests/add-pull-request-comment/feature.spec.int.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import { addPullRequestComment } from './feature';
import { listPullRequests } from '../list-pull-requests/feature';
import {
  getTestConnection,
  shouldSkipIntegrationTest,
} from '@/shared/test/test-helpers';

describe('addPullRequestComment integration', () => {
  let connection: WebApi | null = null;
  let projectName: string;
  let repositoryName: string;
  let pullRequestId: number;

  // Generate unique identifiers using timestamp for comment content
  const timestamp = Date.now();
  const randomSuffix = Math.floor(Math.random() * 1000);

  beforeAll(async () => {
    // Get a real connection using environment variables
    connection = await getTestConnection();

    // Set up project and repository names from environment
    projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
    repositoryName = process.env.AZURE_DEVOPS_DEFAULT_REPOSITORY || '';

    // Skip setup if integration tests should be skipped
    if (shouldSkipIntegrationTest() || !connection) {
      return;
    }

    try {
      // Find an active pull request to use for testing
      const pullRequests = await listPullRequests(
        connection,
        projectName,
        repositoryName,
        {
          projectId: projectName,
          repositoryId: repositoryName,
          status: 'active',
          top: 1,
        },
      );

      if (!pullRequests || pullRequests.value.length === 0) {
        throw new Error('No active pull requests found for testing');
      }

      pullRequestId = pullRequests.value[0].pullRequestId!;
      console.log(`Using existing pull request #${pullRequestId} for testing`);
    } catch (error) {
      console.error('Error in test setup:', error);
      throw error;
    }
  });

  test('should add a new comment thread to pull request', async () => {
    // Skip if integration tests should be skipped
    if (shouldSkipIntegrationTest() || !connection) {
      console.log('Skipping test due to missing connection');
      return;
    }

    // Skip if repository name is not defined
    if (!repositoryName) {
      console.log('Skipping test due to missing repository name');
      return;
    }

    const commentContent = `Test comment ${timestamp}-${randomSuffix}`;

    const result = await addPullRequestComment(
      connection,
      projectName,
      repositoryName,
      pullRequestId,
      {
        projectId: projectName,
        repositoryId: repositoryName,
        pullRequestId,
        content: commentContent,
        status: 'active',
      },
    );

    // Verify the comment was created
    expect(result.comment).toBeDefined();
    expect(result.comment.content).toBe(commentContent);
    expect(result.thread).toBeDefined();
    expect(result.thread!.status).toBe('active'); // Transformed to string
  }, 30000); // 30 second timeout for integration test

  test('should add a file comment to pull request', async () => {
    // Skip if integration tests should be skipped
    if (shouldSkipIntegrationTest() || !connection) {
      console.log('Skipping test due to missing connection');
      return;
    }

    // Skip if repository name is not defined
    if (!repositoryName) {
      console.log('Skipping test due to missing repository name');
      return;
    }

    const commentContent = `File comment ${timestamp}-${randomSuffix}`;
    const filePath = '/README.md'; // Assuming README.md exists in the repo
    const lineNumber = 1;

    const result = await addPullRequestComment(
      connection,
      projectName,
      repositoryName,
      pullRequestId,
      {
        projectId: projectName,
        repositoryId: repositoryName,
        pullRequestId,
        content: commentContent,
        filePath,
        lineNumber,
        status: 'active',
      },
    );

    // Verify the file comment was created
    expect(result.comment).toBeDefined();
    expect(result.comment.content).toBe(commentContent);
    expect(result.thread).toBeDefined();
    expect(result.thread!.threadContext).toBeDefined();
    expect(result.thread!.threadContext!.filePath).toBe(filePath);
    expect(result.thread!.threadContext!.rightFileStart!.line).toBe(lineNumber);
  }, 30000); // 30 second timeout for integration test

  test('should add a reply to an existing comment thread', async () => {
    // Skip if integration tests should be skipped
    if (shouldSkipIntegrationTest() || !connection) {
      console.log('Skipping test due to missing connection');
      return;
    }

    // Skip if repository name is not defined
    if (!repositoryName) {
      console.log('Skipping test due to missing repository name');
      return;
    }

    // First create a thread
    const initialComment = await addPullRequestComment(
      connection,
      projectName,
      repositoryName,
      pullRequestId,
      {
        projectId: projectName,
        repositoryId: repositoryName,
        pullRequestId,
        content: `Initial comment ${timestamp}-${randomSuffix}`,
        status: 'active',
      },
    );

    const threadId = initialComment.thread!.id!;
    const replyContent = `Reply comment ${timestamp}-${randomSuffix}`;

    // Add a reply to the thread
    const result = await addPullRequestComment(
      connection,
      projectName,
      repositoryName,
      pullRequestId,
      {
        projectId: projectName,
        repositoryId: repositoryName,
        pullRequestId,
        content: replyContent,
        threadId,
      },
    );

    // Verify the reply was created
    expect(result.comment).toBeDefined();
    expect(result.comment.content).toBe(replyContent);
    expect(result.thread).toBeUndefined(); // No thread returned for replies
  }, 30000); // 30 second timeout for integration test
});

```

--------------------------------------------------------------------------------
/src/features/work-items/create-work-item/feature.spec.int.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import { createWorkItem } from './feature';
import {
  getTestConnection,
  shouldSkipIntegrationTest,
} from '@/shared/test/test-helpers';
import { CreateWorkItemOptions } from '../types';

describe('createWorkItem integration', () => {
  let connection: WebApi | null = null;

  beforeAll(async () => {
    // Get a real connection using environment variables
    connection = await getTestConnection();
  });

  test('should create a new work item in Azure DevOps', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // Create a unique title using timestamp to avoid conflicts
    const uniqueTitle = `Test Work Item ${new Date().toISOString()}`;

    // For a true integration test, use a real project
    const projectName =
      process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
    const workItemType = 'Task'; // Assumes 'Task' type exists in the project

    const options: CreateWorkItemOptions = {
      title: uniqueTitle,
      description: 'This is a test work item created by an integration test',
      priority: 2,
    };

    // Act - make an actual API call to Azure DevOps
    const result = await createWorkItem(
      connection,
      projectName,
      workItemType,
      options,
    );

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.id).toBeDefined();

    // Verify fields match what we set
    expect(result.fields).toBeDefined();
    if (result.fields) {
      expect(result.fields['System.Title']).toBe(uniqueTitle);
      expect(result.fields['Microsoft.VSTS.Common.Priority']).toBe(2);
    }
  });

  test('should create a work item with additional fields', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // Create a unique title using timestamp to avoid conflicts
    const uniqueTitle = `Test Work Item with Fields ${new Date().toISOString()}`;

    // For a true integration test, use a real project
    const projectName =
      process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
    const workItemType = 'Task';

    const options: CreateWorkItemOptions = {
      title: uniqueTitle,
      description: 'This is a test work item with additional fields',
      priority: 1,
      additionalFields: {
        'System.Tags': 'Integration Test,Automated',
      },
    };

    // Act - make an actual API call to Azure DevOps
    const result = await createWorkItem(
      connection,
      projectName,
      workItemType,
      options,
    );

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.id).toBeDefined();

    // Verify fields match what we set
    expect(result.fields).toBeDefined();
    if (result.fields) {
      expect(result.fields['System.Title']).toBe(uniqueTitle);
      expect(result.fields['Microsoft.VSTS.Common.Priority']).toBe(1);
      // Just check that tags contain both values, order may vary
      expect(result.fields['System.Tags']).toContain('Integration Test');
      expect(result.fields['System.Tags']).toContain('Automated');
    }
  });

  test('should create a child work item with parent-child relationship', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // For a true integration test, use a real project
    const projectName =
      process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';

    // First, create a parent work item (User Story)
    const parentTitle = `Parent Story ${new Date().toISOString()}`;
    const parentOptions: CreateWorkItemOptions = {
      title: parentTitle,
      description: 'This is a parent user story',
    };

    const parentResult = await createWorkItem(
      connection,
      projectName,
      'User Story', // Assuming User Story type exists
      parentOptions,
    );

    expect(parentResult).toBeDefined();
    expect(parentResult.id).toBeDefined();
    const parentId = parentResult.id;

    // Now create a child work item (Task) with a link to the parent
    const childTitle = `Child Task ${new Date().toISOString()}`;
    const childOptions: CreateWorkItemOptions = {
      title: childTitle,
      description: 'This is a child task of a user story',
      parentId: parentId, // Reference to parent work item
    };

    const childResult = await createWorkItem(
      connection,
      projectName,
      'Task',
      childOptions,
    );

    // Assert the child work item was created
    expect(childResult).toBeDefined();
    expect(childResult.id).toBeDefined();

    // Now verify the parent-child relationship
    // We would need to fetch the relations, but for now we'll just assert
    // that the response indicates a relationship was created
    expect(childResult.relations).toBeDefined();

    // Check that at least one relation exists that points to our parent
    const parentRelation = childResult.relations?.find(
      (relation) =>
        relation.rel === 'System.LinkTypes.Hierarchy-Reverse' &&
        relation.url &&
        relation.url.includes(`/${parentId}`),
    );
    expect(parentRelation).toBeDefined();
  });
});

```

--------------------------------------------------------------------------------
/src/features/wikis/create-wiki/feature.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import {
  AzureDevOpsError,
  AzureDevOpsResourceNotFoundError,
  AzureDevOpsValidationError,
  AzureDevOpsPermissionError,
} from '../../../shared/errors';
import { createWiki } from './feature';
import { WikiType } from './schema';
import { getWikiClient } from '../../../clients/azure-devops';

// Mock the WikiClient
jest.mock('../../../clients/azure-devops');

describe('createWiki unit', () => {
  // Mock WikiClient
  const mockWikiClient = {
    createWiki: jest.fn(),
  };

  // Mock WebApi connection (kept for backward compatibility)
  const mockConnection = {} as WebApi;

  beforeEach(() => {
    // Clear mock calls between tests
    jest.clearAllMocks();
    // Setup mock response for getWikiClient
    (getWikiClient as jest.Mock).mockResolvedValue(mockWikiClient);
  });

  test('should create a project wiki', async () => {
    // Mock data
    const mockWiki = {
      id: 'wiki1',
      name: 'Project Wiki',
      projectId: 'project1',
      remoteUrl: 'https://example.com/wiki1',
      url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1',
      type: 'projectWiki',
      repositoryId: 'repo1',
      mappedPath: '/',
    };

    // Setup mock response
    mockWikiClient.createWiki.mockResolvedValue(mockWiki);

    // Call the function
    const result = await createWiki(mockConnection, {
      name: 'Project Wiki',
      projectId: 'project1',
    });

    // Assertions
    expect(getWikiClient).toHaveBeenCalledWith({ organizationId: undefined });
    expect(mockWikiClient.createWiki).toHaveBeenCalledWith('project1', {
      name: 'Project Wiki',
      projectId: 'project1',
      type: WikiType.ProjectWiki,
    });
    expect(result).toEqual(mockWiki);
  });

  test('should create a code wiki', async () => {
    // Mock data
    const mockWiki = {
      id: 'wiki2',
      name: 'Code Wiki',
      projectId: 'project1',
      repositoryId: 'repo1',
      mappedPath: '/docs',
      remoteUrl: 'https://example.com/wiki2',
      url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki2',
      type: 'codeWiki',
    };

    // Setup mock response
    mockWikiClient.createWiki.mockResolvedValue(mockWiki);

    // Call the function
    const result = await createWiki(mockConnection, {
      name: 'Code Wiki',
      projectId: 'project1',
      type: WikiType.CodeWiki,
      repositoryId: 'repo1',
      mappedPath: '/docs',
    });

    // Assertions
    expect(getWikiClient).toHaveBeenCalledWith({ organizationId: undefined });
    expect(mockWikiClient.createWiki).toHaveBeenCalledWith('project1', {
      name: 'Code Wiki',
      projectId: 'project1',
      type: WikiType.CodeWiki,
      repositoryId: 'repo1',
      mappedPath: '/docs',
      version: {
        version: 'main',
        versionType: 'branch' as const,
      },
    });
    expect(result).toEqual(mockWiki);
  });

  test('should throw validation error when repository ID is missing for code wiki', async () => {
    // Call the function and expect it to throw
    await expect(
      createWiki(mockConnection, {
        name: 'Code Wiki',
        projectId: 'project1',
        type: WikiType.CodeWiki,
        // repositoryId is missing
      }),
    ).rejects.toThrow(AzureDevOpsValidationError);

    // Assertions
    expect(getWikiClient).not.toHaveBeenCalled();
    expect(mockWikiClient.createWiki).not.toHaveBeenCalled();
  });

  test('should handle project not found error', async () => {
    // Setup mock to throw an error
    mockWikiClient.createWiki.mockRejectedValue(
      new AzureDevOpsResourceNotFoundError('Project not found'),
    );

    // Call the function and expect it to throw
    await expect(
      createWiki(mockConnection, {
        name: 'Project Wiki',
        projectId: 'nonExistentProject',
      }),
    ).rejects.toThrow(AzureDevOpsResourceNotFoundError);

    // Assertions
    expect(getWikiClient).toHaveBeenCalledWith({ organizationId: undefined });
    expect(mockWikiClient.createWiki).toHaveBeenCalled();
  });

  test('should handle repository not found error', async () => {
    // Setup mock to throw an error
    mockWikiClient.createWiki.mockRejectedValue(
      new AzureDevOpsResourceNotFoundError('Repository not found'),
    );

    // Call the function and expect it to throw
    await expect(
      createWiki(mockConnection, {
        name: 'Code Wiki',
        projectId: 'project1',
        type: WikiType.CodeWiki,
        repositoryId: 'nonExistentRepo',
      }),
    ).rejects.toThrow(AzureDevOpsResourceNotFoundError);

    // Assertions
    expect(getWikiClient).toHaveBeenCalledWith({ organizationId: undefined });
    expect(mockWikiClient.createWiki).toHaveBeenCalled();
  });

  test('should handle permission error', async () => {
    // Setup mock to throw an error
    mockWikiClient.createWiki.mockRejectedValue(
      new AzureDevOpsPermissionError('You do not have permission'),
    );

    // Call the function and expect it to throw
    await expect(
      createWiki(mockConnection, {
        name: 'Project Wiki',
        projectId: 'project1',
      }),
    ).rejects.toThrow(AzureDevOpsPermissionError);

    // Assertions
    expect(getWikiClient).toHaveBeenCalledWith({ organizationId: undefined });
    expect(mockWikiClient.createWiki).toHaveBeenCalled();
  });

  test('should handle generic errors', async () => {
    // Setup mock to throw an error
    mockWikiClient.createWiki.mockRejectedValue(new Error('Unknown error'));

    // Call the function and expect it to throw
    await expect(
      createWiki(mockConnection, {
        name: 'Project Wiki',
        projectId: 'project1',
      }),
    ).rejects.toThrow(AzureDevOpsError);

    // Assertions
    expect(getWikiClient).toHaveBeenCalledWith({ organizationId: undefined });
    expect(mockWikiClient.createWiki).toHaveBeenCalled();
  });
});

```

--------------------------------------------------------------------------------
/src/features/search/search-wiki/feature.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import axios from 'axios';
import { DefaultAzureCredential, AzureCliCredential } from '@azure/identity';
import {
  AzureDevOpsError,
  AzureDevOpsResourceNotFoundError,
  AzureDevOpsValidationError,
  AzureDevOpsPermissionError,
} from '../../../shared/errors';
import {
  SearchWikiOptions,
  WikiSearchRequest,
  WikiSearchResponse,
} from '../types';

/**
 * Search for wiki pages in Azure DevOps projects
 *
 * @param connection The Azure DevOps WebApi connection
 * @param options Parameters for searching wiki pages
 * @returns Search results for wiki pages
 */
export async function searchWiki(
  connection: WebApi,
  options: SearchWikiOptions,
): Promise<WikiSearchResponse> {
  try {
    // Prepare the search request
    const searchRequest: WikiSearchRequest = {
      searchText: options.searchText,
      $skip: options.skip,
      $top: options.top,
      filters: options.projectId
        ? {
            Project: [options.projectId],
          }
        : {},
      includeFacets: options.includeFacets,
    };

    // Add custom filters if provided
    if (
      options.filters &&
      options.filters.Project &&
      options.filters.Project.length > 0
    ) {
      if (!searchRequest.filters) {
        searchRequest.filters = {};
      }

      if (!searchRequest.filters.Project) {
        searchRequest.filters.Project = [];
      }

      searchRequest.filters.Project = [
        ...(searchRequest.filters.Project || []),
        ...options.filters.Project,
      ];
    }

    // Get the authorization header from the connection
    const authHeader = await getAuthorizationHeader();

    // Extract organization and project from the connection URL
    const { organization, project } = extractOrgAndProject(
      connection,
      options.projectId,
    );

    // Make the search API request
    // If projectId is provided, include it in the URL, otherwise perform organization-wide search
    const searchUrl = options.projectId
      ? `https://almsearch.dev.azure.com/${organization}/${project}/_apis/search/wikisearchresults?api-version=7.1`
      : `https://almsearch.dev.azure.com/${organization}/_apis/search/wikisearchresults?api-version=7.1`;

    const searchResponse = await axios.post<WikiSearchResponse>(
      searchUrl,
      searchRequest,
      {
        headers: {
          Authorization: authHeader,
          'Content-Type': 'application/json',
        },
      },
    );

    return searchResponse.data;
  } catch (error) {
    // If it's already an AzureDevOpsError, rethrow it
    if (error instanceof AzureDevOpsError) {
      throw error;
    }

    // Handle axios errors
    if (axios.isAxiosError(error)) {
      const status = error.response?.status;
      const message = error.response?.data?.message || error.message;

      if (status === 404) {
        throw new AzureDevOpsResourceNotFoundError(
          `Resource not found: ${message}`,
        );
      } else if (status === 400) {
        throw new AzureDevOpsValidationError(
          `Invalid request: ${message}`,
          error.response?.data,
        );
      } else if (status === 401 || status === 403) {
        throw new AzureDevOpsPermissionError(`Permission denied: ${message}`);
      } else {
        // For other axios errors, wrap in a generic AzureDevOpsError
        throw new AzureDevOpsError(`Azure DevOps API error: ${message}`);
      }

      // This return is never reached but helps TypeScript understand the control flow
      return null as never;
    }

    // Otherwise, wrap it in a generic error
    throw new AzureDevOpsError(
      `Failed to search wiki: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

/**
 * Extract organization and project from the connection URL
 *
 * @param connection The Azure DevOps WebApi connection
 * @param projectId The project ID or name (optional)
 * @returns The organization and project
 */
function extractOrgAndProject(
  connection: WebApi,
  projectId?: string,
): { organization: string; project: string } {
  // Extract organization from the connection URL
  const url = connection.serverUrl;
  const match = url.match(/https?:\/\/dev\.azure\.com\/([^/]+)/);
  const organization = match ? match[1] : '';

  if (!organization) {
    throw new AzureDevOpsValidationError(
      'Could not extract organization from connection URL',
    );
  }

  return {
    organization,
    project: projectId || '',
  };
}

/**
 * Get the authorization header from the connection
 *
 * @returns The authorization header
 */
async function getAuthorizationHeader(): Promise<string> {
  try {
    // For PAT authentication, we can construct the header directly
    if (
      process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'pat' &&
      process.env.AZURE_DEVOPS_PAT
    ) {
      // For PAT auth, we can construct the Basic auth header directly
      const token = process.env.AZURE_DEVOPS_PAT;
      const base64Token = Buffer.from(`:${token}`).toString('base64');
      return `Basic ${base64Token}`;
    }

    // For Azure Identity / Azure CLI auth, we need to get a token
    // using the Azure DevOps resource ID
    // Choose the appropriate credential based on auth method
    const credential =
      process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'azure-cli'
        ? new AzureCliCredential()
        : new DefaultAzureCredential();

    // Azure DevOps resource ID for token acquisition
    const AZURE_DEVOPS_RESOURCE_ID = '499b84ac-1321-427f-aa17-267ca6975798';

    // Get token for Azure DevOps
    const token = await credential.getToken(
      `${AZURE_DEVOPS_RESOURCE_ID}/.default`,
    );

    if (!token || !token.token) {
      throw new Error('Failed to acquire token for Azure DevOps');
    }

    return `Bearer ${token.token}`;
  } catch (error) {
    throw new AzureDevOpsValidationError(
      `Failed to get authorization header: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

```

--------------------------------------------------------------------------------
/src/features/repositories/get-file-content/feature.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import { IGitApi } from 'azure-devops-node-api/GitApi';
import { GitVersionType } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { AzureDevOpsResourceNotFoundError } from '../../../shared/errors';
import { getFileContent } from './feature';
import { Readable } from 'stream';

describe('getFileContent', () => {
  let mockConnection: WebApi;
  let mockGitApi: IGitApi;
  const mockRepositoryId = 'test-repo';
  const mockProjectId = 'test-project';
  const mockFilePath = '/path/to/file.txt';
  const mockFileContent = 'Test file content';
  const mockItem = {
    objectId: '123456',
    path: mockFilePath,
    url: 'https://dev.azure.com/org/project/_apis/git/repositories/repo/items/path/to/file.txt',
    gitObjectType: 'blob',
  };

  // Helper function to create a readable stream from a string
  function createReadableStream(content: string): Readable {
    const stream = new Readable();
    stream.push(content);
    stream.push(null); // Signals the end of the stream
    return stream;
  }

  beforeEach(() => {
    mockGitApi = {
      getItemContent: jest
        .fn()
        .mockResolvedValue(createReadableStream(mockFileContent)),
      getItems: jest.fn().mockResolvedValue([mockItem]),
    } as unknown as IGitApi;

    mockConnection = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    } as unknown as WebApi;
  });

  it('should get file content for a file in the default branch', async () => {
    const result = await getFileContent(
      mockConnection,
      mockProjectId,
      mockRepositoryId,
      mockFilePath,
    );

    expect(mockConnection.getGitApi).toHaveBeenCalled();
    expect(mockGitApi.getItems).toHaveBeenCalledWith(
      mockRepositoryId,
      mockProjectId,
      mockFilePath,
      expect.any(Number), // VersionControlRecursionType.OneLevel
      undefined,
      undefined,
      undefined,
      undefined,
      undefined,
    );

    expect(mockGitApi.getItemContent).toHaveBeenCalledWith(
      mockRepositoryId,
      mockFilePath,
      mockProjectId,
      undefined,
      undefined,
      undefined,
      undefined,
      false,
      undefined,
      true,
    );

    expect(result).toEqual({
      content: mockFileContent,
      isDirectory: false,
    });
  });

  it('should get file content for a file in a specific branch', async () => {
    const branchName = 'test-branch';
    const versionDescriptor = {
      versionType: GitVersionType.Branch,
      version: branchName,
      versionOptions: undefined,
    };

    const result = await getFileContent(
      mockConnection,
      mockProjectId,
      mockRepositoryId,
      mockFilePath,
      {
        versionType: GitVersionType.Branch,
        version: branchName,
      },
    );

    expect(mockConnection.getGitApi).toHaveBeenCalled();
    expect(mockGitApi.getItems).toHaveBeenCalledWith(
      mockRepositoryId,
      mockProjectId,
      mockFilePath,
      expect.any(Number), // VersionControlRecursionType.OneLevel
      undefined,
      undefined,
      undefined,
      undefined,
      versionDescriptor,
    );

    expect(mockGitApi.getItemContent).toHaveBeenCalledWith(
      mockRepositoryId,
      mockFilePath,
      mockProjectId,
      undefined,
      undefined,
      undefined,
      undefined,
      false,
      versionDescriptor,
      true,
    );

    expect(result).toEqual({
      content: mockFileContent,
      isDirectory: false,
    });
  });

  it('should throw an error if the file is not found', async () => {
    // Mock getItems to throw an error
    mockGitApi.getItems = jest
      .fn()
      .mockRejectedValue(new Error('Item not found'));

    // Mock getItemContent to throw a specific error indicating not found
    mockGitApi.getItemContent = jest
      .fn()
      .mockRejectedValue(new Error('Item not found'));

    await expect(
      getFileContent(
        mockConnection,
        mockProjectId,
        mockRepositoryId,
        '/invalid/path',
      ),
    ).rejects.toThrow(AzureDevOpsResourceNotFoundError);
  });

  it('should get directory content if the path is a directory', async () => {
    const dirPath = '/path/to/dir';
    const mockDirectoryItems = [
      {
        path: `${dirPath}/file1.txt`,
        gitObjectType: 'blob',
        isFolder: false,
      },
      {
        path: `${dirPath}/file2.md`,
        gitObjectType: 'blob',
        isFolder: false,
      },
      {
        path: `${dirPath}/subdir`,
        gitObjectType: 'tree',
        isFolder: true,
      },
    ];

    // Mock getItems to return multiple items, indicating a directory
    mockGitApi.getItems = jest.fn().mockResolvedValue(mockDirectoryItems);

    const result = await getFileContent(
      mockConnection,
      mockProjectId,
      mockRepositoryId,
      dirPath,
    );

    expect(mockConnection.getGitApi).toHaveBeenCalled();
    expect(mockGitApi.getItems).toHaveBeenCalledWith(
      mockRepositoryId,
      mockProjectId,
      dirPath,
      expect.any(Number), // VersionControlRecursionType.OneLevel
      undefined,
      undefined,
      undefined,
      undefined,
      undefined,
    );

    // Should not attempt to get file content for a directory
    expect(mockGitApi.getItemContent).not.toHaveBeenCalled();

    expect(result).toEqual({
      content: JSON.stringify(mockDirectoryItems, null, 2),
      isDirectory: true,
    });
  });

  it('should handle a directory path with trailing slash', async () => {
    const dirPath = '/path/to/dir/';
    const mockDirectoryItems = [
      {
        path: `${dirPath}file1.txt`,
        gitObjectType: 'blob',
        isFolder: false,
      },
    ];

    // Even with one item, it should be treated as a directory due to trailing slash
    mockGitApi.getItems = jest.fn().mockResolvedValue(mockDirectoryItems);

    const result = await getFileContent(
      mockConnection,
      mockProjectId,
      mockRepositoryId,
      dirPath,
    );

    expect(result.isDirectory).toBe(true);
    expect(result.content).toBe(JSON.stringify(mockDirectoryItems, null, 2));
  });
});

```

--------------------------------------------------------------------------------
/src/features/search/search-work-items/feature.spec.int.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import { searchWorkItems } from './feature';
import { getConnection } from '../../../server';
import { AzureDevOpsConfig } from '../../../shared/types';
import { AuthenticationMethod } from '../../../shared/auth';

// Skip tests if no PAT is available
const hasPat = process.env.AZURE_DEVOPS_PAT && process.env.AZURE_DEVOPS_ORG_URL;
const describeOrSkip = hasPat ? describe : describe.skip;

describeOrSkip('searchWorkItems (Integration)', () => {
  let connection: WebApi;
  let config: AzureDevOpsConfig;
  let projectId: string;

  beforeAll(async () => {
    // Set up the connection
    config = {
      organizationUrl: process.env.AZURE_DEVOPS_ORG_URL || '',
      authMethod: AuthenticationMethod.PersonalAccessToken,
      personalAccessToken: process.env.AZURE_DEVOPS_PAT || '',
      defaultProject: process.env.AZURE_DEVOPS_DEFAULT_PROJECT || '',
    };

    connection = await getConnection(config);
    projectId = config.defaultProject || '';

    // Skip tests if no default project is set
    if (!projectId) {
      console.warn('Skipping integration tests: No default project set');
    }
  }, 30000);

  it('should search for work items', async () => {
    // Skip test if no default project
    if (!projectId) {
      return;
    }

    // Act
    const result = await searchWorkItems(connection, {
      searchText: 'test',
      projectId,
      top: 10,
      includeFacets: true,
    });

    // Assert
    expect(result).toBeDefined();
    expect(typeof result.count).toBe('number');
    expect(Array.isArray(result.results)).toBe(true);

    // If there are results, verify their structure
    if (result.results.length > 0) {
      const firstResult = result.results[0];
      expect(firstResult.project).toBeDefined();
      expect(firstResult.fields).toBeDefined();
      expect(firstResult.fields['system.id']).toBeDefined();
      expect(firstResult.fields['system.title']).toBeDefined();
      expect(firstResult.hits).toBeDefined();
      expect(firstResult.url).toBeDefined();
    }

    // If facets were requested, verify their structure
    if (result.facets) {
      expect(result.facets).toBeDefined();
    }
  }, 30000);

  it('should filter work items by type', async () => {
    // Skip test if no default project
    if (!projectId) {
      return;
    }

    // Act
    const result = await searchWorkItems(connection, {
      searchText: 'test',
      projectId,
      filters: {
        'System.WorkItemType': ['Bug'],
      },
      top: 10,
    });

    // Assert
    expect(result).toBeDefined();

    // If there are results, verify they are all bugs
    if (result.results.length > 0) {
      result.results.forEach((item) => {
        expect(item.fields['system.workitemtype'].toLowerCase()).toBe('bug');
      });
    }
  }, 30000);

  it('should support pagination', async () => {
    // Skip test if no default project
    if (!projectId) {
      return;
    }

    // Act - Get first page
    const firstPage = await searchWorkItems(connection, {
      searchText: 'test',
      projectId,
      top: 5,
      skip: 0,
    });

    // If there are enough results, test pagination
    if (firstPage.count > 5) {
      // Act - Get second page
      const secondPage = await searchWorkItems(connection, {
        searchText: 'test',
        projectId,
        top: 5,
        skip: 5,
      });

      // Assert
      expect(secondPage).toBeDefined();
      expect(secondPage.results).toBeDefined();

      // Verify the pages have different items
      if (firstPage.results.length > 0 && secondPage.results.length > 0) {
        const firstPageIds = firstPage.results.map(
          (r) => r.fields['system.id'],
        );
        const secondPageIds = secondPage.results.map(
          (r) => r.fields['system.id'],
        );

        // Check that the pages don't have overlapping IDs
        const overlap = firstPageIds.filter((id) => secondPageIds.includes(id));
        expect(overlap.length).toBe(0);
      }
    }
  }, 30000);

  it('should support sorting', async () => {
    // Skip test if no default project
    if (!projectId) {
      return;
    }

    // Act - Get results sorted by creation date (newest first)
    const result = await searchWorkItems(connection, {
      searchText: 'test',
      projectId,
      orderBy: [{ field: 'System.CreatedDate', sortOrder: 'DESC' }],
      top: 10,
    });

    // Assert
    expect(result).toBeDefined();

    // If there are multiple results, verify they are sorted
    if (result.results.length > 1) {
      const dates = result.results
        .filter((r) => r.fields['system.createddate'] !== undefined)
        .map((r) =>
          new Date(r.fields['system.createddate'] as string).getTime(),
        );

      // Check that dates are in descending order
      for (let i = 0; i < dates.length - 1; i++) {
        expect(dates[i]).toBeGreaterThanOrEqual(dates[i + 1]);
      }
    }
  }, 30000);

  // Add a test to verify Azure Identity authentication if configured
  if (
    process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'azure-identity'
  ) {
    test('should search work items using Azure Identity authentication', async () => {
      // Skip if required environment variables are missing
      if (!process.env.AZURE_DEVOPS_ORG_URL || !process.env.TEST_PROJECT_ID) {
        console.log('Skipping test: required environment variables missing');
        return;
      }

      // Create a config with Azure Identity authentication
      const testConfig: AzureDevOpsConfig = {
        organizationUrl: process.env.AZURE_DEVOPS_ORG_URL,
        authMethod: AuthenticationMethod.AzureIdentity,
        defaultProject: process.env.TEST_PROJECT_ID,
      };

      // Create the connection using the config
      const connection = await getConnection(testConfig);

      // Search work items
      const result = await searchWorkItems(connection, {
        projectId: process.env.TEST_PROJECT_ID,
        searchText: 'test',
      });

      // Check that the response is properly formatted
      expect(result).toBeDefined();
      expect(result.count).toBeDefined();
      expect(Array.isArray(result.results)).toBe(true);
    });
  }
});

```

--------------------------------------------------------------------------------
/docs/tools/pipelines.md:
--------------------------------------------------------------------------------

```markdown
# Pipeline Tools

This document describes the tools available for working with Azure DevOps pipelines.

## Table of Contents

- [`list_pipelines`](#list_pipelines) - List pipelines in a project
- [`get_pipeline`](#get_pipeline) - Get details of a specific pipeline
- [`trigger_pipeline`](#trigger_pipeline) - Trigger a pipeline run

## list_pipelines

Lists pipelines in a project.

### Parameters

| Parameter   | Type   | Required | Description                                               |
| ----------- | ------ | -------- | --------------------------------------------------------- |
| `projectId` | string | No       | The ID or name of the project (Default: from environment) |
| `orderBy`   | string | No       | Order by field and direction (e.g., "createdDate desc")   |
| `top`       | number | No       | Maximum number of pipelines to return                     |

### Response

Returns an array of pipeline objects:

```json
{
  "count": 2,
  "value": [
    {
      "id": 4,
      "revision": 2,
      "name": "Node.js build pipeline",
      "folder": "\\",
      "url": "https://dev.azure.com/organization/project/_apis/pipelines/4"
    },
    {
      "id": 1,
      "revision": 1,
      "name": "Sample Pipeline",
      "folder": "\\",
      "url": "https://dev.azure.com/organization/project/_apis/pipelines/1"
    }
  ]
}
```

### Error Handling

- Returns `AzureDevOpsResourceNotFoundError` if the project does not exist
- Returns `AzureDevOpsAuthenticationError` if authentication fails
- Returns generic error messages for other failures

### Example Usage

```javascript
// Using default project from environment
const result = await callTool('list_pipelines', {});

// Specifying project and limiting results
const limitedResult = await callTool('list_pipelines', {
  projectId: 'my-project',
  top: 10,
  orderBy: 'name asc',
});
```

## get_pipeline

Gets details of a specific pipeline.

### Parameters

| Parameter         | Type   | Required | Description                                                       |
| ----------------- | ------ | -------- | ----------------------------------------------------------------- |
| `projectId`       | string | No       | The ID or name of the project (Default: from environment)         |
| `pipelineId`      | number | Yes      | The numeric ID of the pipeline to retrieve                        |
| `pipelineVersion` | number | No       | The version of the pipeline to retrieve (latest if not specified) |

### Response

Returns a pipeline object with the following structure:

```json
{
  "id": 4,
  "revision": 2,
  "name": "Node.js build pipeline",
  "folder": "\\",
  "url": "https://dev.azure.com/organization/project/_apis/pipelines/4",
  "_links": {
    "self": {
      "href": "https://dev.azure.com/organization/project/_apis/pipelines/4"
    },
    "web": {
      "href": "https://dev.azure.com/organization/project/_build/definition?definitionId=4"
    }
  },
  "configuration": {
    "path": "azure-pipelines.yml",
    "repository": {
      "id": "bd0e8130-7fba-4f3b-8559-54760b6e7248",
      "type": "azureReposGit"
    },
    "type": "yaml"
  }
}
```

### Error Handling

- Returns `AzureDevOpsResourceNotFoundError` if the pipeline or project does not exist
- Returns `AzureDevOpsAuthenticationError` if authentication fails
- Returns generic error messages for other failures

### Example Usage

```javascript
// Get latest version of a pipeline
const result = await callTool('get_pipeline', {
  pipelineId: 4,
});

// Get specific version of a pipeline
const versionResult = await callTool('get_pipeline', {
  projectId: 'my-project',
  pipelineId: 4,
  pipelineVersion: 2,
});
```

## trigger_pipeline

Triggers a run of a specific pipeline. Allows specifying the branch to run on and passing variables to customize the pipeline execution.

### Parameters

| Parameter            | Type   | Required | Description                                                           |
| -------------------- | ------ | -------- | --------------------------------------------------------------------- |
| `projectId`          | string | No       | The ID or name of the project (Default: from environment)             |
| `pipelineId`         | number | Yes      | The numeric ID of the pipeline to trigger                             |
| `branch`             | string | No       | The branch to run the pipeline on (e.g., "main", "feature/my-branch") |
| `variables`          | object | No       | Variables to pass to the pipeline run                                 |
| `templateParameters` | object | No       | Parameters for template-based pipelines                               |
| `stagesToSkip`       | array  | No       | Stages to skip in the pipeline run                                    |

#### Variables Format

```json
{
  "myVariable": {
    "value": "my-value",
    "isSecret": false
  },
  "secretVariable": {
    "value": "secret-value",
    "isSecret": true
  }
}
```

### Response

Returns a run object with details about the triggered pipeline run:

```json
{
  "id": 12345,
  "name": "20230215.1",
  "createdDate": "2023-02-15T10:30:00Z",
  "url": "https://dev.azure.com/organization/project/_apis/pipelines/runs/12345",
  "_links": {
    "self": {
      "href": "https://dev.azure.com/organization/project/_apis/pipelines/runs/12345"
    },
    "web": {
      "href": "https://dev.azure.com/organization/project/_build/results?buildId=12345"
    }
  },
  "state": 1,
  "result": null,
  "variables": {
    "myVariable": {
      "value": "my-value"
    }
  }
}
```

### Error Handling

- Returns `AzureDevOpsResourceNotFoundError` if the pipeline or project does not exist
- Returns `AzureDevOpsAuthenticationError` if authentication fails
- Returns generic error messages for other failures

### Example Usage

```javascript
// Trigger a pipeline on the default branch
// In this case, use default project from environment variables
const result = await callTool('trigger_pipeline', {
  pipelineId: 4,
});

// Trigger a pipeline on a specific branch with variables
const runWithOptions = await callTool('trigger_pipeline', {
  projectId: 'my-project',
  pipelineId: 4,
  branch: 'feature/my-branch',
  variables: {
    deployEnvironment: {
      value: 'staging',
      isSecret: false,
    },
  },
});
```

```

--------------------------------------------------------------------------------
/docs/tools/work-items.md:
--------------------------------------------------------------------------------

```markdown
# Work Item Tools

This document describes the tools available for working with Azure DevOps work items.

## Table of Contents

- [`get_work_item`](#get_work_item) - Retrieve a specific work item by ID
- [`create_work_item`](#create_work_item) - Create a new work item
- [`list_work_items`](#list_work_items) - List work items in a project

## get_work_item

Retrieves a work item by its ID.

### Parameters

| Parameter    | Type   | Required | Description                                                                       |
| ------------ | ------ | -------- | --------------------------------------------------------------------------------- |
| `workItemId` | number | Yes      | The ID of the work item to retrieve                                               |
| `expand`     | string | No       | Controls the level of detail in the response. Defaults to "All" if not specified. Other values: "Relations", "Fields", "None" |

### Response

Returns a work item object with the following structure:

```json
{
  "id": 123,
  "fields": {
    "System.Title": "Sample Work Item",
    "System.State": "Active",
    "System.AssignedTo": "[email protected]",
    "System.Description": "Description of the work item"
  },
  "url": "https://dev.azure.com/organization/project/_apis/wit/workItems/123"
}
```

### Error Handling

- Returns `AzureDevOpsResourceNotFoundError` if the work item does not exist
- Returns `AzureDevOpsAuthenticationError` if authentication fails
- Returns generic error messages for other failures

### Example Usage

```javascript
// Using default expand="All"
const result = await callTool('get_work_item', {
  workItemId: 123,
});

// Explicitly specifying expand
const minimalResult = await callTool('get_work_item', {
  workItemId: 123,
  expand: 'None'
});
```

## create_work_item

Creates a new work item in a specified project.

### Parameters

| Parameter          | Type   | Required | Description                                                         |
| ------------------ | ------ | -------- | ------------------------------------------------------------------- |
| `projectId`        | string | Yes      | The ID or name of the project where the work item will be created   |
| `workItemType`     | string | Yes      | The type of work item to create (e.g., "Task", "Bug", "User Story") |
| `title`            | string | Yes      | The title of the work item                                          |
| `description`      | string | No       | The description of the work item                                    |
| `assignedTo`       | string | No       | The email or name of the user to assign the work item to            |
| `areaPath`         | string | No       | The area path for the work item                                     |
| `iterationPath`    | string | No       | The iteration path for the work item                                |
| `priority`         | number | No       | The priority of the work item                                       |
| `additionalFields` | object | No       | Additional fields to set on the work item (key-value pairs)         |

### Response

Returns the newly created work item object:

```json
{
  "id": 124,
  "fields": {
    "System.Title": "New Work Item",
    "System.State": "New",
    "System.Description": "Description of the new work item",
    "System.AssignedTo": "[email protected]",
    "System.AreaPath": "Project\\Team",
    "System.IterationPath": "Project\\Sprint 1",
    "Microsoft.VSTS.Common.Priority": 2
  },
  "url": "https://dev.azure.com/organization/project/_apis/wit/workItems/124"
}
```

### Error Handling

- Returns validation error if required fields are missing
- Returns `AzureDevOpsAuthenticationError` if authentication fails
- Returns `AzureDevOpsResourceNotFoundError` if the project does not exist
- Returns generic error messages for other failures

### Example Usage

```javascript
const result = await callTool('create_work_item', {
  projectId: 'my-project',
  workItemType: 'User Story',
  title: 'Implement login functionality',
  description:
    'Create a secure login system with email and password authentication',
  assignedTo: '[email protected]',
  priority: 1,
  additionalFields: {
    'Custom.Field': 'Custom Value',
  },
});
```

### Implementation Details

The tool creates a JSON patch document to define the fields of the work item, then calls the Azure DevOps API to create the work item. Each field is added to the document with an 'add' operation, and the document is submitted to the API.

## list_work_items

Lists work items in a specified project.

### Parameters

| Parameter   | Type   | Required | Description                                           |
| ----------- | ------ | -------- | ----------------------------------------------------- |
| `projectId` | string | Yes      | The ID or name of the project to list work items from |
| `teamId`    | string | No       | The ID of the team to list work items for             |
| `queryId`   | string | No       | ID of a saved work item query                         |
| `wiql`      | string | No       | Work Item Query Language (WIQL) query                 |
| `top`       | number | No       | Maximum number of work items to return                |
| `skip`      | number | No       | Number of work items to skip                          |

### Response

Returns an array of work item objects:

```json
[
  {
    "id": 123,
    "fields": {
      "System.Title": "Sample Work Item",
      "System.State": "Active",
      "System.AssignedTo": "[email protected]"
    },
    "url": "https://dev.azure.com/organization/project/_apis/wit/workItems/123"
  },
  {
    "id": 124,
    "fields": {
      "System.Title": "Another Work Item",
      "System.State": "New",
      "System.AssignedTo": "[email protected]"
    },
    "url": "https://dev.azure.com/organization/project/_apis/wit/workItems/124"
  }
]
```

### Error Handling

- Returns `AzureDevOpsResourceNotFoundError` if the project does not exist
- Returns `AzureDevOpsAuthenticationError` if authentication fails
- Returns generic error messages for other failures

### Example Usage

```javascript
const result = await callTool('list_work_items', {
  projectId: 'my-project',
  wiql: "SELECT [System.Id] FROM WorkItems WHERE [System.WorkItemType] = 'Task' ORDER BY [System.CreatedDate] DESC",
  top: 10,
});
```

```

--------------------------------------------------------------------------------
/src/features/wikis/create-wiki-page/feature.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
import { createWikiPage } from './feature';
import { handleRequestError } from '../../../shared/errors/handle-request-error';

// Mock the AzureDevOpsClient
jest.mock('../../../shared/api/client');
// Mock the error handler
jest.mock('../../../shared/errors/handle-request-error', () => ({
  handleRequestError: jest.fn(),
}));

describe('createWikiPage Feature', () => {
  let client: any;
  const mockPut = jest.fn();
  const mockHandleRequestError = handleRequestError as jest.MockedFunction<
    typeof handleRequestError
  >;

  const defaultParams = {
    wikiId: 'test-wiki',
    content: 'Hello world',
    pagePath: '/',
  };

  beforeEach(() => {
    // Reset mocks for each test
    mockPut.mockReset();
    mockHandleRequestError.mockReset();

    client = {
      put: mockPut,
      defaults: {
        organizationId: 'defaultOrg',
        projectId: 'defaultProject',
      },
    };
  });

  it('should call client.put with correct URL and data for default org and project', async () => {
    mockPut.mockResolvedValue({ data: { some: 'response' } });
    await createWikiPage(defaultParams, client as any);

    expect(mockPut).toHaveBeenCalledTimes(1);
    expect(mockPut).toHaveBeenCalledWith(
      'defaultOrg/defaultProject/_apis/wiki/wikis/test-wiki/pages?path=%2F&api-version=7.1-preview.1',
      { content: 'Hello world' },
    );
  });

  it('should call client.put with correct URL when projectId is explicitly provided', async () => {
    mockPut.mockResolvedValue({ data: { some: 'response' } });
    const paramsWithProject = {
      ...defaultParams,
      projectId: 'customProject',
    };
    await createWikiPage(paramsWithProject, client as any);

    expect(mockPut).toHaveBeenCalledWith(
      'defaultOrg/customProject/_apis/wiki/wikis/test-wiki/pages?path=%2F&api-version=7.1-preview.1',
      { content: 'Hello world' },
    );
  });

  it('should call client.put with correct URL when organizationId is explicitly provided', async () => {
    mockPut.mockResolvedValue({ data: { some: 'response' } });
    const paramsWithOrg = {
      ...defaultParams,
      organizationId: 'customOrg',
    };
    await createWikiPage(paramsWithOrg, client as any);

    expect(mockPut).toHaveBeenCalledWith(
      'customOrg/defaultProject/_apis/wiki/wikis/test-wiki/pages?path=%2F&api-version=7.1-preview.1',
      { content: 'Hello world' },
    );
  });

  it('should call client.put with correct URL when projectId is null (project-level wiki)', async () => {
    mockPut.mockResolvedValue({ data: { some: 'response' } });
    const paramsWithNullProject = {
      ...defaultParams,
      projectId: null, // Explicitly null for project-level resources that don't need a project
    };

    // Client default for projectId should also be null or undefined in this scenario
    const clientWithoutProject = {
      put: mockPut,
      defaults: {
        organizationId: 'defaultOrg',
        projectId: undefined,
      },
    };

    await createWikiPage(paramsWithNullProject, clientWithoutProject as any);

    expect(mockPut).toHaveBeenCalledWith(
      'defaultOrg/_apis/wiki/wikis/test-wiki/pages?path=%2F&api-version=7.1-preview.1',
      { content: 'Hello world' },
    );
  });

  it('should correctly encode pagePath in the URL', async () => {
    mockPut.mockResolvedValue({ data: { some: 'response' } });
    const paramsWithPath = {
      ...defaultParams,
      pagePath: '/My Test Page/Sub Page',
    };
    await createWikiPage(paramsWithPath, client as any);

    expect(mockPut).toHaveBeenCalledWith(
      'defaultOrg/defaultProject/_apis/wiki/wikis/test-wiki/pages?path=%2FMy%20Test%20Page%2FSub%20Page&api-version=7.1-preview.1',
      { content: 'Hello world' },
    );
  });

  it('should use default pagePath "/" if pagePath is null', async () => {
    mockPut.mockResolvedValue({ data: { some: 'response' } });
    const paramsWithPath = {
      ...defaultParams,
      pagePath: null, // Explicitly null
    };
    await createWikiPage(paramsWithPath, client as any);

    expect(mockPut).toHaveBeenCalledWith(
      'defaultOrg/defaultProject/_apis/wiki/wikis/test-wiki/pages?path=%2F&api-version=7.1-preview.1',
      { content: 'Hello world' },
    );
  });

  it('should include comment in request body when provided', async () => {
    mockPut.mockResolvedValue({ data: { some: 'response' } });
    const paramsWithComment = {
      ...defaultParams,
      comment: 'Initial page creation',
    };
    await createWikiPage(paramsWithComment, client as any);

    expect(mockPut).toHaveBeenCalledWith(
      'defaultOrg/defaultProject/_apis/wiki/wikis/test-wiki/pages?path=%2F&api-version=7.1-preview.1',
      { content: 'Hello world', comment: 'Initial page creation' },
    );
  });

  it('should return the data from the response on success', async () => {
    const expectedResponse = { id: '123', path: '/', content: 'Hello world' };
    mockPut.mockResolvedValue({ data: expectedResponse });
    const result = await createWikiPage(defaultParams, client as any);

    expect(result).toEqual(expectedResponse);
  });

  // Skip this test for now as it requires complex mocking of environment variables
  it.skip('should throw if organizationId is not provided and not set in defaults', async () => {
    const clientWithoutOrg = {
      put: mockPut,
      defaults: {
        projectId: 'defaultProject',
        organizationId: undefined,
      },
    };

    const paramsNoOrg = {
      ...defaultParams,
      organizationId: null, // Explicitly null and no default
    };

    // This test is skipped because it requires complex mocking of environment variables
    // which is difficult to do in the current test setup
    await expect(
      createWikiPage(paramsNoOrg, clientWithoutOrg as any),
    ).rejects.toThrow(
      'Organization ID is not defined. Please provide it or set a default.',
    );
    expect(mockPut).not.toHaveBeenCalled();
  });

  it('should call handleRequestError if client.put throws an error', async () => {
    const error = new Error('API Error');
    mockPut.mockRejectedValue(error);
    mockHandleRequestError.mockImplementation(() => {
      throw new Error('Handled Error');
    });

    await expect(createWikiPage(defaultParams, client as any)).rejects.toThrow(
      'Handled Error',
    );
    expect(mockHandleRequestError).toHaveBeenCalledTimes(1);
    expect(mockHandleRequestError).toHaveBeenCalledWith(
      error,
      'Failed to create or update wiki page',
    );
  });
});

```

--------------------------------------------------------------------------------
/src/features/projects/get-project-details/feature.spec.int.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import { getProjectDetails } from './feature';
import {
  getTestConnection,
  shouldSkipIntegrationTest,
} from '@/shared/test/test-helpers';

describe('getProjectDetails integration', () => {
  let connection: WebApi | null = null;
  let projectName: string;

  beforeAll(async () => {
    // Get a real connection using environment variables
    connection = await getTestConnection();
    projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
  });

  test('should retrieve basic project details from Azure DevOps', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // Act - make an actual API call to Azure DevOps
    const result = await getProjectDetails(connection, {
      projectId: projectName,
    });

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.name).toBe(projectName);
    expect(result.id).toBeDefined();
    expect(result.url).toBeDefined();
    expect(result.state).toBeDefined();

    // Verify basic project structure
    expect(result.visibility).toBeDefined();
    expect(result.lastUpdateTime).toBeDefined();
    expect(result.capabilities).toBeDefined();
  });

  test('should retrieve project details with teams from Azure DevOps', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // Act - make an actual API call to Azure DevOps
    const result = await getProjectDetails(connection, {
      projectId: projectName,
      includeTeams: true,
    });

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.teams).toBeDefined();
    expect(Array.isArray(result.teams)).toBe(true);

    // There should be at least one team (the default team)
    if (result.teams && result.teams.length > 0) {
      const team = result.teams[0];
      expect(team.id).toBeDefined();
      expect(team.name).toBeDefined();
      expect(team.url).toBeDefined();
    }
  });

  test('should retrieve project details with process information from Azure DevOps', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // Act - make an actual API call to Azure DevOps
    const result = await getProjectDetails(connection, {
      projectId: projectName,
      includeProcess: true,
    });

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.process).toBeDefined();
    expect(result.process?.name).toBeDefined();
  });

  test('should retrieve project details with work item types from Azure DevOps', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // Act - make an actual API call to Azure DevOps
    const result = await getProjectDetails(connection, {
      projectId: projectName,
      includeProcess: true,
      includeWorkItemTypes: true,
    });

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.process).toBeDefined();
    expect(result.process?.workItemTypes).toBeDefined();
    expect(Array.isArray(result.process?.workItemTypes)).toBe(true);

    // There should be at least one work item type
    if (
      result.process?.workItemTypes &&
      result.process.workItemTypes.length > 0
    ) {
      const workItemType = result.process.workItemTypes[0];
      expect(workItemType.name).toBeDefined();
      expect(workItemType.description).toBeDefined();
      expect(workItemType.states).toBeDefined();
    }
  });

  test('should retrieve project details with fields from Azure DevOps', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // Act - make an actual API call to Azure DevOps
    const result = await getProjectDetails(connection, {
      projectId: projectName,
      includeProcess: true,
      includeWorkItemTypes: true,
      includeFields: true,
    });

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.process).toBeDefined();
    expect(result.process?.workItemTypes).toBeDefined();

    // There should be at least one work item type with fields
    if (
      result.process?.workItemTypes &&
      result.process.workItemTypes.length > 0
    ) {
      const workItemType = result.process.workItemTypes[0];
      expect(workItemType.fields).toBeDefined();
      expect(Array.isArray(workItemType.fields)).toBe(true);

      // There should be at least one field (like Title)
      if (workItemType.fields && workItemType.fields.length > 0) {
        const field = workItemType.fields[0];
        expect(field.name).toBeDefined();
        expect(field.referenceName).toBeDefined();
      }
    }
  });

  test('should throw error when project is not found', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // Use a non-existent project name
    const nonExistentProjectName = 'non-existent-project-' + Date.now();

    // Act & Assert - should throw an error for non-existent project
    await expect(
      getProjectDetails(connection, {
        projectId: nonExistentProjectName,
      }),
    ).rejects.toThrow(/not found|Failed to get project/);
  });
});

```

--------------------------------------------------------------------------------
/src/features/pull-requests/schemas.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import { defaultProject, defaultOrg } from '../../utils/environment';

/**
 * Schema for creating a pull request
 */
export const CreatePullRequestSchema = z.object({
  projectId: z
    .string()
    .optional()
    .describe(`The ID or name of the project (Default: ${defaultProject})`),
  organizationId: z
    .string()
    .optional()
    .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
  repositoryId: z.string().describe('The ID or name of the repository'),
  title: z.string().describe('The title of the pull request'),
  description: z
    .string()
    .optional()
    .describe('The description of the pull request (markdown is supported)'),
  sourceRefName: z
    .string()
    .describe('The source branch name (e.g., refs/heads/feature-branch)'),
  targetRefName: z
    .string()
    .describe('The target branch name (e.g., refs/heads/main)'),
  reviewers: z
    .array(z.string())
    .optional()
    .describe('List of reviewer email addresses or IDs'),
  isDraft: z
    .boolean()
    .optional()
    .describe('Whether the pull request should be created as a draft'),
  workItemRefs: z
    .array(z.number())
    .optional()
    .describe('List of work item IDs to link to the pull request'),
  additionalProperties: z
    .record(z.string(), z.any())
    .optional()
    .describe('Additional properties to set on the pull request'),
});

/**
 * Schema for listing pull requests
 */
export const ListPullRequestsSchema = z.object({
  projectId: z
    .string()
    .optional()
    .describe(`The ID or name of the project (Default: ${defaultProject})`),
  organizationId: z
    .string()
    .optional()
    .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
  repositoryId: z.string().describe('The ID or name of the repository'),
  status: z
    .enum(['all', 'active', 'completed', 'abandoned'])
    .optional()
    .describe('Filter by pull request status'),
  creatorId: z
    .string()
    .optional()
    .describe('Filter by creator ID (must be a UUID string)'),
  reviewerId: z
    .string()
    .optional()
    .describe('Filter by reviewer ID (must be a UUID string)'),
  sourceRefName: z.string().optional().describe('Filter by source branch name'),
  targetRefName: z.string().optional().describe('Filter by target branch name'),
  top: z
    .number()
    .default(10)
    .describe('Maximum number of pull requests to return (default: 10)'),
  skip: z
    .number()
    .optional()
    .describe('Number of pull requests to skip for pagination'),
});

/**
 * Schema for getting pull request comments
 */
export const GetPullRequestCommentsSchema = z.object({
  projectId: z
    .string()
    .optional()
    .describe(`The ID or name of the project (Default: ${defaultProject})`),
  organizationId: z
    .string()
    .optional()
    .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
  repositoryId: z.string().describe('The ID or name of the repository'),
  pullRequestId: z.number().describe('The ID of the pull request'),
  threadId: z
    .number()
    .optional()
    .describe('The ID of the specific thread to get comments from'),
  includeDeleted: z
    .boolean()
    .optional()
    .describe('Whether to include deleted comments'),
  top: z
    .number()
    .optional()
    .describe('Maximum number of threads/comments to return'),
});

/**
 * Schema for adding a comment to a pull request
 */
export const AddPullRequestCommentSchema = z
  .object({
    projectId: z
      .string()
      .optional()
      .describe(`The ID or name of the project (Default: ${defaultProject})`),
    organizationId: z
      .string()
      .optional()
      .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
    repositoryId: z.string().describe('The ID or name of the repository'),
    pullRequestId: z.number().describe('The ID of the pull request'),
    content: z.string().describe('The content of the comment in markdown'),
    threadId: z
      .number()
      .optional()
      .describe('The ID of the thread to add the comment to'),
    parentCommentId: z
      .number()
      .optional()
      .describe(
        'ID of the parent comment when replying to an existing comment',
      ),
    filePath: z
      .string()
      .optional()
      .describe('The path of the file to comment on (for new thread on file)'),
    lineNumber: z
      .number()
      .optional()
      .describe('The line number to comment on (for new thread on file)'),
    status: z
      .enum([
        'active',
        'fixed',
        'wontFix',
        'closed',
        'pending',
        'byDesign',
        'unknown',
      ])
      .optional()
      .describe('The status to set for a new thread'),
  })
  .superRefine((data, ctx) => {
    // If we're creating a new thread (no threadId), status is required
    if (!data.threadId && !data.status) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: 'Status is required when creating a new thread',
        path: ['status'],
      });
    }
  });

/**
 * Schema for updating a pull request
 */
export const UpdatePullRequestSchema = z.object({
  projectId: z
    .string()
    .optional()
    .describe(`The ID or name of the project (Default: ${defaultProject})`),
  organizationId: z
    .string()
    .optional()
    .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
  repositoryId: z.string().describe('The ID or name of the repository'),
  pullRequestId: z.number().describe('The ID of the pull request to update'),
  title: z
    .string()
    .optional()
    .describe('The updated title of the pull request'),
  description: z
    .string()
    .optional()
    .describe('The updated description of the pull request'),
  status: z
    .enum(['active', 'abandoned', 'completed'])
    .optional()
    .describe('The updated status of the pull request'),
  isDraft: z
    .boolean()
    .optional()
    .describe(
      'Whether the pull request should be marked as a draft (true) or unmarked (false)',
    ),
  addWorkItemIds: z
    .array(z.number())
    .optional()
    .describe('List of work item IDs to link to the pull request'),
  removeWorkItemIds: z
    .array(z.number())
    .optional()
    .describe('List of work item IDs to unlink from the pull request'),
  addReviewers: z
    .array(z.string())
    .optional()
    .describe('List of reviewer email addresses or IDs to add'),
  removeReviewers: z
    .array(z.string())
    .optional()
    .describe('List of reviewer email addresses or IDs to remove'),
  additionalProperties: z
    .record(z.string(), z.any())
    .optional()
    .describe('Additional properties to update on the pull request'),
});

```

--------------------------------------------------------------------------------
/project-management/planning/the-dream-team.md:
--------------------------------------------------------------------------------

```markdown
Below is the **Dream Team Documentation** for building the Azure DevOps MCP server. This document outlines the ideal roles and skill sets required to ensure the project's success, from development to deployment. Each role is carefully selected to address the technical, security, and operational challenges of building a robust, AI-integrated server.

---

## Dream Team Documentation: Building the Azure DevOps MCP Server

### Overview

The Azure DevOps MCP server is a complex tool that requires a multidisciplinary team with expertise in software development, Azure DevOps, security, testing, documentation, project management, and AI integration. The following roles are essential to ensure the server is built efficiently, securely, and in alignment with the Model Context Protocol (MCP) standards.

### Key Roles and Responsibilities

#### 1. **Full-Stack Developer (Typescript/Node.js)**

- **Responsibilities**:
  - Implement the server's core functionality using Typescript and Node.js.
  - Develop and maintain MCP tools (e.g., `list_projects`, `create_work_item`).
  - Write tests as part of the implementation process (TDD).
  - Integrate with the MCP Typescript SDK and Azure DevOps APIs.
  - Write clean, modular, and efficient code following best practices.
  - Ensure code quality through comprehensive unit and integration tests.
  - Build automated testing pipelines for continuous integration.
  - Perform integration testing across components.
- **Required Skills**:
  - Proficiency in Typescript and Node.js.
  - Strong testing skills and experience with test frameworks (e.g., Jest).
  - Experience writing testable code and following TDD practices.
  - Experience with REST APIs and asynchronous programming.
  - Familiarity with Git and version control systems.
  - Understanding of modular software design.
  - Experience with API testing and mocking tools.

#### 2. **Azure DevOps API Expert**

- **Responsibilities**:
  - Guide the team on effectively using Azure DevOps REST APIs (e.g., Git, Work Item Tracking, Build).
  - Ensure the server leverages Azure DevOps features optimally (e.g., repository operations, pipelines).
  - Assist in mapping MCP tools to the correct API endpoints.
  - Troubleshoot API-related issues and optimize API usage.
  - Help develop tests for Azure DevOps API integrations.
- **Required Skills**:
  - Deep understanding of Azure DevOps services and their REST APIs.
  - Experience with Azure DevOps workflows (e.g., repositories, work items, pipelines).
  - Knowledge of Azure DevOps authentication mechanisms (PAT, AAD).
  - Ability to interpret API documentation and handle rate limits.
  - Experience testing API integrations.

#### 3. **Security Specialist**

- **Responsibilities**:
  - Design and implement secure authentication methods (PAT and AAD).
  - Ensure credentials are stored and managed securely (e.g., environment variables).
  - Scope permissions to the minimum required for each tool.
  - Implement error handling and logging without exposing sensitive data.
  - Conduct security reviews and recommend improvements.
  - Develop security tests and validation procedures.
- **Required Skills**:
  - Expertise in API security, authentication, and authorization.
  - Familiarity with Azure Active Directory and PAT management.
  - Knowledge of secure coding practices and vulnerability prevention.
  - Experience with logging, auditing, and compliance.
  - Experience with security testing tools and methodologies.

#### 4. **Technical Writer**

- **Responsibilities**:
  - Create comprehensive documentation, including setup guides, tool descriptions, and usage examples.
  - Write clear API references and troubleshooting tips.
  - Ensure documentation is accessible to both technical and non-technical users.
  - Maintain up-to-date documentation as the server evolves.
- **Required Skills**:
  - Strong technical writing and communication skills.
  - Ability to explain complex concepts simply.
  - Experience documenting APIs and developer tools.
  - Familiarity with Markdown and documentation platforms (e.g., GitHub README).

#### 5. **Project Manager**

- **Responsibilities**:
  - Coordinate the team's efforts and manage the project timeline.
  - Track progress using Azure Boards or similar tools.
  - Facilitate communication and resolve blockers.
  - Ensure the project stays on scope and meets deadlines.
  - Manage stakeholder expectations and provide status updates.
- **Required Skills**:
  - Experience in agile project management.
  - Proficiency with project tracking tools (e.g., Azure Boards, Jira).
  - Strong organizational and leadership skills.
  - Ability to manage remote or distributed teams.

#### 6. **AI Integration Consultant**

- **Responsibilities**:
  - Advise on how the server can best integrate with AI models (e.g., Claude Desktop).
  - Ensure tools are designed to support AI-driven workflows (e.g., user story to pull request).
  - Provide insights into MCP's AI integration capabilities.
  - Assist in testing AI interactions with the server.
- **Required Skills**:
  - Experience with AI model integration and workflows.
  - Understanding of the Model Context Protocol (MCP).
  - Familiarity with AI tools like Claude Desktop.
  - Ability to bridge AI and software development domains.

---

### Team Structure and Collaboration

- **Core Team**: Full-Stack Developer, Azure DevOps API Expert, Security Specialist.
- **Support Roles**: Technical Writer, Project Manager, AI Integration Consultant.
- **Collaboration**: Use Agile methodologies with bi-weekly sprints, daily stand-ups, and regular retrospectives to iterate efficiently.
- **Communication Tools**: Slack or Microsoft Teams for real-time communication, Azure Boards for task tracking, and GitHub/Azure DevOps for version control and code reviews.

---

### Why This Team?

Each role addresses a critical aspect of the project:

- The **Full-Stack Developer** builds the server using modern technologies like Typescript and Node.js, integrating testing throughout the development process.
- The **Azure DevOps API Expert** ensures seamless integration with Azure DevOps services.
- The **Security Specialist** safeguards the server against vulnerabilities.
- The **Technical Writer** makes the server user-friendly with clear documentation.
- The **Project Manager** keeps the team aligned and on schedule.
- The **AI Integration Consultant** ensures the server meets AI-driven workflow requirements.

This dream team combines technical expertise, security, integrated quality assurance, and project management to deliver a high-quality, secure, and user-friendly Azure DevOps MCP server. Testing is built into our development process, not treated as a separate concern.

```

--------------------------------------------------------------------------------
/src/features/repositories/get-repository-details/feature.spec.int.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import { getRepositoryDetails } from './feature';
import {
  getTestConnection,
  shouldSkipIntegrationTest,
} from '@/shared/test/test-helpers';

describe('getRepositoryDetails integration', () => {
  let connection: WebApi | null = null;
  let projectName: string;

  beforeAll(async () => {
    // Get a real connection using environment variables
    connection = await getTestConnection();
    projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
  });

  test('should retrieve repository details from Azure DevOps', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // First, get a list of repos to find one to test with
    const gitApi = await connection.getGitApi();
    const repos = await gitApi.getRepositories(projectName);

    // Skip if no repos are available
    if (!repos || repos.length === 0) {
      console.log('Skipping test: No repositories available in the project');
      return;
    }

    // Use the first repo as a test subject
    const testRepo = repos[0];

    // Act - make an actual API call to Azure DevOps
    const result = await getRepositoryDetails(connection, {
      projectId: projectName,
      repositoryId: testRepo.name || testRepo.id || '',
    });

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.repository).toBeDefined();
    expect(result.repository.id).toBe(testRepo.id);
    expect(result.repository.name).toBe(testRepo.name);
    expect(result.repository.project).toBeDefined();
    if (result.repository.project) {
      expect(result.repository.project.name).toBe(projectName);
    }
  });

  test('should retrieve repository details with statistics', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // First, get a list of repos to find one to test with
    const gitApi = await connection.getGitApi();
    const repos = await gitApi.getRepositories(projectName);

    // Skip if no repos are available
    if (!repos || repos.length === 0) {
      console.log('Skipping test: No repositories available in the project');
      return;
    }

    // Use the first repo as a test subject
    const testRepo = repos[0];

    // Act - make an actual API call to Azure DevOps
    const result = await getRepositoryDetails(connection, {
      projectId: projectName,
      repositoryId: testRepo.name || testRepo.id || '',
      includeStatistics: true,
    });

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.repository).toBeDefined();
    expect(result.repository.id).toBe(testRepo.id);
    expect(result.statistics).toBeDefined();
    expect(Array.isArray(result.statistics?.branches)).toBe(true);
  });

  test('should retrieve repository details with refs', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // First, get a list of repos to find one to test with
    const gitApi = await connection.getGitApi();
    const repos = await gitApi.getRepositories(projectName);

    // Skip if no repos are available
    if (!repos || repos.length === 0) {
      console.log('Skipping test: No repositories available in the project');
      return;
    }

    // Use the first repo as a test subject
    const testRepo = repos[0];

    // Act - make an actual API call to Azure DevOps
    const result = await getRepositoryDetails(connection, {
      projectId: projectName,
      repositoryId: testRepo.name || testRepo.id || '',
      includeRefs: true,
    });

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.repository).toBeDefined();
    expect(result.repository.id).toBe(testRepo.id);
    expect(result.refs).toBeDefined();
    expect(result.refs?.value).toBeDefined();
    expect(Array.isArray(result.refs?.value)).toBe(true);
    expect(typeof result.refs?.count).toBe('number');
  });

  test('should retrieve repository details with refs filtered by heads/', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // First, get a list of repos to find one to test with
    const gitApi = await connection.getGitApi();
    const repos = await gitApi.getRepositories(projectName);

    // Skip if no repos are available
    if (!repos || repos.length === 0) {
      console.log('Skipping test: No repositories available in the project');
      return;
    }

    // Use the first repo as a test subject
    const testRepo = repos[0];

    // Act - make an actual API call to Azure DevOps
    const result = await getRepositoryDetails(connection, {
      projectId: projectName,
      repositoryId: testRepo.name || testRepo.id || '',
      includeRefs: true,
      refFilter: 'heads/',
    });

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.repository).toBeDefined();
    expect(result.refs).toBeDefined();
    expect(result.refs?.value).toBeDefined();

    // All refs should start with refs/heads/
    if (result.refs && result.refs.value.length > 0) {
      result.refs.value.forEach((ref) => {
        expect(ref.name).toMatch(/^refs\/heads\//);
      });
    }
  });

  test('should throw error when repository is not found', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // Use a non-existent repository name
    const nonExistentRepoName = 'non-existent-repo-' + Date.now();

    // Act & Assert - should throw an error for non-existent repo
    await expect(
      getRepositoryDetails(connection, {
        projectId: projectName,
        repositoryId: nonExistentRepoName,
      }),
    ).rejects.toThrow(/not found|Failed to get repository/);
  });
});

```

--------------------------------------------------------------------------------
/src/features/pull-requests/list-pull-requests/feature.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import { listPullRequests } from './feature';
import { PullRequestStatus } from 'azure-devops-node-api/interfaces/GitInterfaces';

describe('listPullRequests', () => {
  afterEach(() => {
    jest.resetAllMocks();
  });

  test('should return pull requests successfully with pagination metadata', async () => {
    // Mock data
    const mockPullRequests = [
      {
        pullRequestId: 1,
        title: 'Test PR 1',
        description: 'Test PR description 1',
      },
      {
        pullRequestId: 2,
        title: 'Test PR 2',
        description: 'Test PR description 2',
      },
    ];

    // Setup mock connection
    const mockGitApi = {
      getPullRequests: jest.fn().mockResolvedValue(mockPullRequests),
    };

    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    };

    // Call the function with test parameters
    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const options = {
      projectId,
      repositoryId,
      status: 'active' as const,
      top: 10,
    };

    const result = await listPullRequests(
      mockConnection as WebApi,
      projectId,
      repositoryId,
      options,
    );

    // Verify results
    expect(result).toEqual({
      count: 2,
      value: mockPullRequests,
      hasMoreResults: false,
      warning: undefined,
    });
    expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1);
    expect(mockGitApi.getPullRequests).toHaveBeenCalledTimes(1);
    expect(mockGitApi.getPullRequests).toHaveBeenCalledWith(
      repositoryId,
      { status: PullRequestStatus.Active },
      projectId,
      undefined, // maxCommentLength
      0, // skip
      10, // top
    );
  });

  test('should return empty array when no pull requests exist', async () => {
    // Setup mock connection
    const mockGitApi = {
      getPullRequests: jest.fn().mockResolvedValue(null),
    };

    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    };

    // Call the function with test parameters
    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const options = { projectId, repositoryId };

    const result = await listPullRequests(
      mockConnection as WebApi,
      projectId,
      repositoryId,
      options,
    );

    // Verify results
    expect(result).toEqual({
      count: 0,
      value: [],
      hasMoreResults: false,
      warning: undefined,
    });
    expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1);
    expect(mockGitApi.getPullRequests).toHaveBeenCalledTimes(1);
  });

  test('should handle all filter options correctly', async () => {
    // Setup mock connection
    const mockGitApi = {
      getPullRequests: jest.fn().mockResolvedValue([]),
    };

    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    };

    // Call with all options
    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const options = {
      projectId,
      repositoryId,
      status: 'completed' as const,
      creatorId: 'a8a8a8a8-a8a8-a8a8-a8a8-a8a8a8a8a8a8',
      reviewerId: 'b9b9b9b9-b9b9-b9b9-b9b9-b9b9b9b9b9b9',
      sourceRefName: 'refs/heads/source-branch',
      targetRefName: 'refs/heads/target-branch',
      top: 5,
      skip: 10,
    };

    await listPullRequests(
      mockConnection as WebApi,
      projectId,
      repositoryId,
      options,
    );

    // Verify the search criteria was constructed correctly
    expect(mockGitApi.getPullRequests).toHaveBeenCalledWith(
      repositoryId,
      {
        status: PullRequestStatus.Completed,
        creatorId: 'a8a8a8a8-a8a8-a8a8-a8a8-a8a8a8a8a8a8',
        reviewerId: 'b9b9b9b9-b9b9-b9b9-b9b9-b9b9b9b9b9b9',
        sourceRefName: 'refs/heads/source-branch',
        targetRefName: 'refs/heads/target-branch',
      },
      projectId,
      undefined, // maxCommentLength
      10, // skip
      5, // top
    );
  });

  test('should throw error when API call fails', async () => {
    // Setup mock connection
    const errorMessage = 'API error';
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => ({
        getPullRequests: jest.fn().mockRejectedValue(new Error(errorMessage)),
      })),
    };

    // Call the function with test parameters
    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const options = { projectId, repositoryId };

    // Verify error handling
    await expect(
      listPullRequests(
        mockConnection as WebApi,
        projectId,
        repositoryId,
        options,
      ),
    ).rejects.toThrow(`Failed to list pull requests: ${errorMessage}`);
  });

  test('should use default pagination values when not provided', async () => {
    // Mock data
    const mockPullRequests = [
      { pullRequestId: 1, title: 'Test PR 1' },
      { pullRequestId: 2, title: 'Test PR 2' },
    ];

    // Setup mock connection
    const mockGitApi = {
      getPullRequests: jest.fn().mockResolvedValue(mockPullRequests),
    };

    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    };

    // Call the function with minimal parameters (no top or skip)
    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const options = { projectId, repositoryId };

    const result = await listPullRequests(
      mockConnection as WebApi,
      projectId,
      repositoryId,
      options,
    );

    // Verify default values were used
    expect(mockGitApi.getPullRequests).toHaveBeenCalledWith(
      repositoryId,
      {},
      projectId,
      undefined, // maxCommentLength
      0, // default skip
      10, // default top
    );

    expect(result.count).toBe(2);
    expect(result.value).toEqual(mockPullRequests);
  });

  test('should add warning when hasMoreResults is true', async () => {
    // Create exactly 10 mock pull requests to trigger hasMoreResults
    const mockPullRequests = Array(10)
      .fill(0)
      .map((_, i) => ({
        pullRequestId: i + 1,
        title: `Test PR ${i + 1}`,
      }));

    // Setup mock connection
    const mockGitApi = {
      getPullRequests: jest.fn().mockResolvedValue(mockPullRequests),
    };

    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    };

    // Call with top=10 to match the number of results
    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const options = {
      projectId,
      repositoryId,
      top: 10,
      skip: 5,
    };

    const result = await listPullRequests(
      mockConnection as WebApi,
      projectId,
      repositoryId,
      options,
    );

    // Verify hasMoreResults is true and warning is set
    expect(result.hasMoreResults).toBe(true);
    expect(result.warning).toBe(
      "Results limited to 10 items. Use 'skip: 15' to get the next page.",
    );
  });
});

```

--------------------------------------------------------------------------------
/src/features/pull-requests/update-pull-request/feature.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
import { updatePullRequest } from './feature';
import { AzureDevOpsClient } from '../../../shared/auth/client-factory';
import { AzureDevOpsError } from '../../../shared/errors';

// Mock the AzureDevOpsClient
jest.mock('../../../shared/auth/client-factory');

describe('updatePullRequest', () => {
  const mockGetPullRequestById = jest.fn();
  const mockUpdatePullRequest = jest.fn();
  const mockUpdateWorkItem = jest.fn();
  const mockGetWorkItem = jest.fn();

  // Mock Git API
  const mockGitApi = {
    getPullRequestById: mockGetPullRequestById,
    updatePullRequest: mockUpdatePullRequest,
  };

  // Mock Work Item Tracking API
  const mockWorkItemTrackingApi = {
    updateWorkItem: mockUpdateWorkItem,
    getWorkItem: mockGetWorkItem,
  };

  // Mock connection
  const mockConnection = {
    getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    getWorkItemTrackingApi: jest
      .fn()
      .mockResolvedValue(mockWorkItemTrackingApi),
  };

  const mockAzureDevopsClient = {
    getWebApiClient: jest.fn().mockResolvedValue(mockConnection),
    // ...other properties if needed
  };

  beforeEach(() => {
    jest.clearAllMocks();
    (AzureDevOpsClient as unknown as jest.Mock).mockImplementation(
      () => mockAzureDevopsClient,
    );
  });

  it('should throw error when pull request does not exist', async () => {
    mockGetPullRequestById.mockResolvedValueOnce(null);

    await expect(
      updatePullRequest({
        projectId: 'project-1',
        repositoryId: 'repo1',
        pullRequestId: 123,
      }),
    ).rejects.toThrow(AzureDevOpsError);
  });

  it('should update the pull request title and description', async () => {
    mockGetPullRequestById.mockResolvedValueOnce({
      repository: { id: 'repo1' },
    });

    mockUpdatePullRequest.mockResolvedValueOnce({
      title: 'Updated Title',
      description: 'Updated Description',
    });

    const result = await updatePullRequest({
      projectId: 'project-1',
      repositoryId: 'repo1',
      pullRequestId: 123,
      title: 'Updated Title',
      description: 'Updated Description',
    });

    expect(mockUpdatePullRequest).toHaveBeenCalledWith(
      {
        title: 'Updated Title',
        description: 'Updated Description',
      },
      'repo1',
      123,
      'project-1',
    );

    expect(result).toEqual({
      title: 'Updated Title',
      description: 'Updated Description',
    });
  });

  it('should update the pull request status when status is provided', async () => {
    mockGetPullRequestById.mockResolvedValueOnce({
      repository: { id: 'repo1' },
    });

    mockUpdatePullRequest.mockResolvedValueOnce({
      status: 2, // Abandoned
    });

    const result = await updatePullRequest({
      projectId: 'project-1',
      repositoryId: 'repo1',
      pullRequestId: 123,
      status: 'abandoned',
    });

    expect(mockUpdatePullRequest).toHaveBeenCalledWith(
      {
        status: 2, // Abandoned value
      },
      'repo1',
      123,
      'project-1',
    );

    expect(result).toEqual({
      status: 2, // Abandoned
    });
  });

  it('should throw error for invalid status', async () => {
    mockGetPullRequestById.mockResolvedValueOnce({
      repository: { id: 'repo1' },
    });

    await expect(
      updatePullRequest({
        projectId: 'project-1',
        repositoryId: 'repo1',
        pullRequestId: 123,
        status: 'invalid-status' as any,
      }),
    ).rejects.toThrow(AzureDevOpsError);
  });

  it('should update the pull request draft status', async () => {
    mockGetPullRequestById.mockResolvedValueOnce({
      repository: { id: 'repo1' },
    });

    mockUpdatePullRequest.mockResolvedValueOnce({
      isDraft: true,
    });

    const result = await updatePullRequest({
      projectId: 'project-1',
      repositoryId: 'repo1',
      pullRequestId: 123,
      isDraft: true,
    });

    expect(mockUpdatePullRequest).toHaveBeenCalledWith(
      {
        isDraft: true,
      },
      'repo1',
      123,
      'project-1',
    );

    expect(result).toEqual({
      isDraft: true,
    });
  });

  it('should include additionalProperties in the update', async () => {
    mockGetPullRequestById.mockResolvedValueOnce({
      repository: { id: 'repo1' },
    });

    mockUpdatePullRequest.mockResolvedValueOnce({
      title: 'Title',
      customProperty: 'custom value',
    });

    const result = await updatePullRequest({
      projectId: 'project-1',
      repositoryId: 'repo1',
      pullRequestId: 123,
      additionalProperties: {
        customProperty: 'custom value',
      },
    });

    expect(mockUpdatePullRequest).toHaveBeenCalledWith(
      {
        customProperty: 'custom value',
      },
      'repo1',
      123,
      'project-1',
    );

    expect(result).toEqual({
      title: 'Title',
      customProperty: 'custom value',
    });
  });

  it('should handle work item links', async () => {
    // Define the artifactId that will be used
    const artifactId = 'vstfs:///Git/PullRequestId/project-1/repo1/123';

    mockGetPullRequestById.mockResolvedValueOnce({
      repository: { id: 'repo1' },
      artifactId: artifactId, // Add the artifactId to the mock response
    });

    mockUpdatePullRequest.mockResolvedValueOnce({
      pullRequestId: 123,
      repository: { id: 'repo1' },
      artifactId: artifactId,
    });

    // Mocks for work items to remove
    mockGetWorkItem.mockResolvedValueOnce({
      relations: [
        {
          rel: 'ArtifactLink',
          url: artifactId, // Use the same artifactId here
          attributes: {
            name: 'Pull Request',
          },
        },
      ],
    });

    mockGetWorkItem.mockResolvedValueOnce({
      relations: [
        {
          rel: 'ArtifactLink',
          url: artifactId, // Use the same artifactId here
          attributes: {
            name: 'Pull Request',
          },
        },
      ],
    });

    await updatePullRequest({
      projectId: 'project-1',
      repositoryId: 'repo1',
      pullRequestId: 123,
      addWorkItemIds: [456, 789],
      removeWorkItemIds: [101, 202],
    });

    // Check that updateWorkItem was called for adding work items
    expect(mockUpdateWorkItem).toHaveBeenCalledTimes(4); // 2 for add, 2 for remove
    expect(mockUpdateWorkItem).toHaveBeenCalledWith(
      null,
      [
        {
          op: 'add',
          path: '/relations/-',
          value: {
            rel: 'ArtifactLink',
            url: 'vstfs:///Git/PullRequestId/project-1/repo1/123',
            attributes: {
              name: 'Pull Request',
            },
          },
        },
      ],
      456,
    );

    // Check for removing work items
    expect(mockUpdateWorkItem).toHaveBeenCalledWith(
      null,
      [
        {
          op: 'remove',
          path: '/relations/0',
        },
      ],
      101,
    );
  });

  it('should wrap unexpected errors in a friendly error message', async () => {
    mockGetPullRequestById.mockRejectedValueOnce(new Error('Unexpected'));

    await expect(
      updatePullRequest({
        projectId: 'project-1',
        repositoryId: 'repo1',
        pullRequestId: 123,
      }),
    ).rejects.toThrow(AzureDevOpsError);
  });
});

```

--------------------------------------------------------------------------------
/project-management/planning/azure-identity-authentication-design.md:
--------------------------------------------------------------------------------

```markdown
# Azure Identity Authentication for Azure DevOps MCP Server

This document outlines the implementation approach for adding Azure Identity authentication support to the Azure DevOps MCP Server.

## Overview

The Azure DevOps MCP Server currently supports Personal Access Token (PAT) authentication. This enhancement will add support for Azure Identity authentication methods, specifically DefaultAzureCredential and AzureCliCredential, to provide more flexible authentication options for different environments.

## Azure Identity SDK

The `@azure/identity` package provides various credential types for authenticating with Azure services. For our implementation, we will focus on the following credential types:

### DefaultAzureCredential

`DefaultAzureCredential` provides a simplified authentication experience by trying multiple credential types in sequence:

1. Environment variables (EnvironmentCredential)
2. Managed Identity (ManagedIdentityCredential)
3. Azure CLI (AzureCliCredential)
4. Visual Studio Code (VisualStudioCodeCredential)
5. Azure PowerShell (AzurePowerShellCredential)
6. Interactive Browser (InteractiveBrowserCredential) - optional, disabled by default

This makes it ideal for applications that need to work in different environments (local development, Azure-hosted) without code changes.

### AzureCliCredential

`AzureCliCredential` authenticates using the Azure CLI's logged-in account. It requires the Azure CLI to be installed and the user to be logged in (`az login`). This is particularly useful for local development scenarios where developers are already using the Azure CLI.

## Implementation Approach

### 1. Authentication Abstraction Layer

Create an abstraction layer for authentication that supports both PAT and Azure Identity methods:

```typescript
// src/api/auth.ts
export interface AuthProvider {
  getConnection(): Promise<WebApi>;
  isAuthenticated(): Promise<boolean>;
}

export class PatAuthProvider implements AuthProvider {
  // Existing PAT authentication implementation
}

export class AzureIdentityAuthProvider implements AuthProvider {
  // New Azure Identity authentication implementation
}
```

### 2. Authentication Factory

Implement a factory pattern to create the appropriate authentication provider based on configuration:

```typescript
// src/api/auth.ts
export enum AuthMethod {
  PAT = 'pat',
  AZURE_IDENTITY = 'azure-identity',
}

export function createAuthProvider(config: AzureDevOpsConfig): AuthProvider {
  switch (config.authMethod) {
    case AuthMethod.AZURE_IDENTITY:
      return new AzureIdentityAuthProvider(config);
    case AuthMethod.PAT:
    default:
      return new PatAuthProvider(config);
  }
}
```

### 3. Azure Identity Authentication Provider

Implement the Azure Identity authentication provider:

```typescript
// src/api/auth.ts
export class AzureIdentityAuthProvider implements AuthProvider {
  private config: AzureDevOpsConfig;
  private connectionPromise: Promise<WebApi> | null = null;

  constructor(config: AzureDevOpsConfig) {
    this.config = config;
  }

  async getConnection(): Promise<WebApi> {
    if (!this.connectionPromise) {
      this.connectionPromise = this.createConnection();
    }
    return this.connectionPromise;
  }

  private async createConnection(): Promise<WebApi> {
    try {
      // Azure DevOps resource ID for token scope
      const azureDevOpsResourceId = '499b84ac-1321-427f-aa17-267ca6975798';

      // Create credential based on configuration
      const credential = this.createCredential();

      // Get token for Azure DevOps
      const token = await credential.getToken(
        `${azureDevOpsResourceId}/.default`,
      );

      if (!token) {
        throw new AzureDevOpsAuthenticationError(
          'Failed to acquire token from Azure Identity',
        );
      }

      // Create auth handler with token
      const authHandler = new BearerCredentialHandler(token.token);

      // Create WebApi client
      const connection = new WebApi(this.config.organizationUrl, authHandler);

      // Test the connection
      await connection.getLocationsApi();

      return connection;
    } catch (error) {
      throw new AzureDevOpsAuthenticationError(
        `Failed to authenticate with Azure Identity: ${error instanceof Error ? error.message : String(error)}`,
      );
    }
  }

  private createCredential(): TokenCredential {
    if (this.config.azureIdentityOptions?.useAzureCliCredential) {
      return new AzureCliCredential();
    }

    // Default to DefaultAzureCredential
    return new DefaultAzureCredential();
  }

  async isAuthenticated(): Promise<boolean> {
    try {
      await this.getConnection();
      return true;
    } catch {
      return false;
    }
  }
}
```

### 4. Configuration Updates

Update the configuration interface to support specifying the authentication method:

```typescript
// src/types/config.ts
export interface AzureDevOpsConfig {
  // Existing properties
  organizationUrl: string;
  personalAccessToken?: string;
  defaultProject?: string;
  apiVersion?: string;

  // New properties
  authMethod?: AuthMethod;
  azureIdentityOptions?: {
    useAzureCliCredential?: boolean;
    // Other Azure Identity options as needed
  };
}
```

### 5. Environment Variable Updates

Update the environment variable handling in `index.ts`:

```typescript
// src/index.ts
const config: AzureDevOpsConfig = {
  organizationUrl: process.env.AZURE_DEVOPS_ORG_URL || '',
  personalAccessToken: process.env.AZURE_DEVOPS_PAT,
  defaultProject: process.env.AZURE_DEVOPS_DEFAULT_PROJECT,
  apiVersion: process.env.AZURE_DEVOPS_API_VERSION,
  authMethod:
    (process.env.AZURE_DEVOPS_AUTH_METHOD as AuthMethod) || AuthMethod.PAT,
  azureIdentityOptions: {
    useAzureCliCredential:
      process.env.AZURE_DEVOPS_USE_CLI_CREDENTIAL === 'true',
  },
};
```

### 6. Client Updates

Update the `AzureDevOpsClient` class to use the authentication provider:

```typescript
// src/api/client.ts
export class AzureDevOpsClient {
  private authProvider: AuthProvider;

  constructor(config: AzureDevOpsConfig) {
    this.authProvider = createAuthProvider(config);
  }

  private async getClient(): Promise<WebApi> {
    return this.authProvider.getConnection();
  }

  // Rest of the class remains the same
}
```

## Error Handling

Implement proper error handling for Azure Identity authentication failures:

```typescript
// src/common/errors.ts
export class AzureIdentityAuthenticationError extends AzureDevOpsAuthenticationError {
  constructor(message: string) {
    super(`Azure Identity Authentication Error: ${message}`);
  }
}
```

## Configuration Examples

### PAT Authentication

```env
AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-org
AZURE_DEVOPS_PAT=your-pat
AZURE_DEVOPS_AUTH_METHOD=pat
```

### DefaultAzureCredential Authentication

```env
AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-org
AZURE_DEVOPS_AUTH_METHOD=azure-identity
# Optional environment variables for specific credential types
AZURE_TENANT_ID=your-tenant-id
AZURE_CLIENT_ID=your-client-id
AZURE_CLIENT_SECRET=your-client-secret
```

### AzureCliCredential Authentication

```env
AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-org
AZURE_DEVOPS_AUTH_METHOD=azure-cli
``` 
```

--------------------------------------------------------------------------------
/src/features/work-items/index.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
import { isWorkItemsRequest, handleWorkItemsRequest } from './';
import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
import { WebApi } from 'azure-devops-node-api';
import * as workItemModule from './';

// Mock the imported modules
jest.mock('./get-work-item', () => ({
  getWorkItem: jest.fn(),
}));

jest.mock('./list-work-items', () => ({
  listWorkItems: jest.fn(),
}));

jest.mock('./create-work-item', () => ({
  createWorkItem: jest.fn(),
}));

jest.mock('./update-work-item', () => ({
  updateWorkItem: jest.fn(),
}));

jest.mock('./manage-work-item-link', () => ({
  manageWorkItemLink: jest.fn(),
}));

// Helper function to create a valid CallToolRequest object
const createCallToolRequest = (name: string, args: any): CallToolRequest => {
  return {
    method: 'tools/call',
    params: {
      name,
      arguments: args,
    },
  } as unknown as CallToolRequest;
};

describe('Work Items Request Handlers', () => {
  describe('isWorkItemsRequest', () => {
    it('should return true for work items requests', () => {
      const workItemsRequests = [
        'get_work_item',
        'list_work_items',
        'create_work_item',
        'update_work_item',
        'manage_work_item_link',
      ];

      workItemsRequests.forEach((name) => {
        const request = createCallToolRequest(name, {});

        expect(isWorkItemsRequest(request)).toBe(true);
      });
    });

    it('should return false for non-work items requests', () => {
      const request = createCallToolRequest('get_project', {});

      expect(isWorkItemsRequest(request)).toBe(false);
    });
  });

  describe('handleWorkItemsRequest', () => {
    let mockConnection: WebApi;

    beforeEach(() => {
      mockConnection = {} as WebApi;

      // Setup mock for schema validation - with correct return types
      jest
        .spyOn(workItemModule.GetWorkItemSchema, 'parse')
        .mockImplementation(() => {
          return { workItemId: 123, expand: undefined };
        });

      jest
        .spyOn(workItemModule.ListWorkItemsSchema, 'parse')
        .mockImplementation(() => {
          return { projectId: 'myProject' };
        });

      jest
        .spyOn(workItemModule.CreateWorkItemSchema, 'parse')
        .mockImplementation(() => {
          return {
            projectId: 'myProject',
            workItemType: 'Task',
            title: 'New Task',
          };
        });

      jest
        .spyOn(workItemModule.UpdateWorkItemSchema, 'parse')
        .mockImplementation(() => {
          return {
            workItemId: 123,
            title: 'Updated Title',
          };
        });

      jest
        .spyOn(workItemModule.ManageWorkItemLinkSchema, 'parse')
        .mockImplementation(() => {
          return {
            sourceWorkItemId: 123,
            targetWorkItemId: 456,
            operation: 'add' as 'add' | 'remove' | 'update',
            relationType: 'System.LinkTypes.Hierarchy-Forward',
          };
        });

      // Setup mocks for feature functions
      jest.spyOn(workItemModule, 'getWorkItem').mockResolvedValue({ id: 123 });
      jest
        .spyOn(workItemModule, 'listWorkItems')
        .mockResolvedValue([{ id: 123 }, { id: 456 }]);
      jest
        .spyOn(workItemModule, 'createWorkItem')
        .mockResolvedValue({ id: 789 });
      jest
        .spyOn(workItemModule, 'updateWorkItem')
        .mockResolvedValue({ id: 123 });
      jest
        .spyOn(workItemModule, 'manageWorkItemLink')
        .mockResolvedValue({ id: 123 });
    });

    afterEach(() => {
      jest.resetAllMocks();
    });

    it('should handle get_work_item requests', async () => {
      const request = createCallToolRequest('get_work_item', {
        workItemId: 123,
      });

      const result = await handleWorkItemsRequest(mockConnection, request);

      expect(workItemModule.GetWorkItemSchema.parse).toHaveBeenCalledWith({
        workItemId: 123,
      });
      expect(workItemModule.getWorkItem).toHaveBeenCalledWith(
        mockConnection,
        123,
        undefined,
      );
      expect(result).toEqual({
        content: [{ type: 'text', text: JSON.stringify({ id: 123 }, null, 2) }],
      });
    });

    it('should handle list_work_items requests', async () => {
      const request = createCallToolRequest('list_work_items', {
        projectId: 'myProject',
      });

      const result = await handleWorkItemsRequest(mockConnection, request);

      expect(workItemModule.ListWorkItemsSchema.parse).toHaveBeenCalledWith({
        projectId: 'myProject',
      });
      expect(workItemModule.listWorkItems).toHaveBeenCalled();
      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: JSON.stringify([{ id: 123 }, { id: 456 }], null, 2),
          },
        ],
      });
    });

    it('should handle create_work_item requests', async () => {
      const request = createCallToolRequest('create_work_item', {
        projectId: 'myProject',
        workItemType: 'Task',
        title: 'New Task',
      });

      const result = await handleWorkItemsRequest(mockConnection, request);

      expect(workItemModule.CreateWorkItemSchema.parse).toHaveBeenCalledWith({
        projectId: 'myProject',
        workItemType: 'Task',
        title: 'New Task',
      });
      expect(workItemModule.createWorkItem).toHaveBeenCalled();
      expect(result).toEqual({
        content: [{ type: 'text', text: JSON.stringify({ id: 789 }, null, 2) }],
      });
    });

    it('should handle update_work_item requests', async () => {
      const request = createCallToolRequest('update_work_item', {
        workItemId: 123,
        title: 'Updated Title',
      });

      const result = await handleWorkItemsRequest(mockConnection, request);

      expect(workItemModule.UpdateWorkItemSchema.parse).toHaveBeenCalledWith({
        workItemId: 123,
        title: 'Updated Title',
      });
      expect(workItemModule.updateWorkItem).toHaveBeenCalled();
      expect(result).toEqual({
        content: [{ type: 'text', text: JSON.stringify({ id: 123 }, null, 2) }],
      });
    });

    it('should handle manage_work_item_link requests', async () => {
      const request = createCallToolRequest('manage_work_item_link', {
        sourceWorkItemId: 123,
        targetWorkItemId: 456,
        operation: 'add',
        relationType: 'System.LinkTypes.Hierarchy-Forward',
      });

      const result = await handleWorkItemsRequest(mockConnection, request);

      expect(
        workItemModule.ManageWorkItemLinkSchema.parse,
      ).toHaveBeenCalledWith({
        sourceWorkItemId: 123,
        targetWorkItemId: 456,
        operation: 'add',
        relationType: 'System.LinkTypes.Hierarchy-Forward',
      });
      expect(workItemModule.manageWorkItemLink).toHaveBeenCalled();
      expect(result).toEqual({
        content: [{ type: 'text', text: JSON.stringify({ id: 123 }, null, 2) }],
      });
    });

    it('should throw an error for unknown work items tools', async () => {
      const request = createCallToolRequest('unknown_tool', {});

      await expect(
        handleWorkItemsRequest(mockConnection, request),
      ).rejects.toThrow('Unknown work items tool: unknown_tool');
    });
  });
});

```

--------------------------------------------------------------------------------
/src/features/work-items/get-work-item/feature.spec.int.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import { getWorkItem } from './feature';
import {
  getTestConnection,
  shouldSkipIntegrationTest,
} from '../__test__/test-helpers';

import { AzureDevOpsResourceNotFoundError } from '../../../shared/errors';
import { createWorkItem } from '../create-work-item/feature';
import { manageWorkItemLink } from '../manage-work-item-link/feature';
import { CreateWorkItemOptions } from '../types';

describe('getWorkItem integration', () => {
  let connection: WebApi | null = null;
  let testWorkItemId: number | null = null;
  let linkedWorkItemId: number | null = null;
  let projectName: string;

  beforeAll(async () => {
    // Get a real connection using environment variables
    connection = await getTestConnection();
    projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';

    // Skip setup if integration tests should be skipped
    if (shouldSkipIntegrationTest() || !connection) {
      return;
    }

    try {
      // Create a test work item
      const uniqueTitle = `Test Work Item ${new Date().toISOString()}`;
      const options: CreateWorkItemOptions = {
        title: uniqueTitle,
        description: 'Test work item for get-work-item integration tests',
      };

      const testWorkItem = await createWorkItem(
        connection,
        projectName,
        'Task',
        options,
      );

      // Create another work item to link to the first one
      const linkedItemOptions: CreateWorkItemOptions = {
        title: `Linked Work Item ${new Date().toISOString()}`,
        description: 'Linked work item for get-work-item integration tests',
      };

      const linkedWorkItem = await createWorkItem(
        connection,
        projectName,
        'Task',
        linkedItemOptions,
      );

      if (testWorkItem?.id && linkedWorkItem?.id) {
        testWorkItemId = testWorkItem.id;
        linkedWorkItemId = linkedWorkItem.id;

        // Create a link between the two work items
        await manageWorkItemLink(connection, projectName, {
          sourceWorkItemId: testWorkItemId,
          targetWorkItemId: linkedWorkItemId,
          operation: 'add',
          relationType: 'System.LinkTypes.Related',
          comment: 'Link created for get-work-item integration tests',
        });
      }
    } catch (error) {
      console.error('Failed to create test work items:', error);
    }
  });

  test('should retrieve a real work item from Azure DevOps with default expand=all', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest() || !connection || !testWorkItemId) {
      return;
    }

    // Act - get work item by ID
    const result = await getWorkItem(connection, testWorkItemId);

    // Assert
    expect(result).toBeDefined();
    expect(result.id).toBe(testWorkItemId);

    // Verify expanded fields and data are present
    expect(result.fields).toBeDefined();
    expect(result._links).toBeDefined();

    // With expand=all and a linked item, relations should be defined
    expect(result.relations).toBeDefined();

    if (result.fields) {
      // Verify common fields that should be present with expand=all
      expect(result.fields['System.Title']).toBeDefined();
      expect(result.fields['System.State']).toBeDefined();
      expect(result.fields['System.CreatedDate']).toBeDefined();
      expect(result.fields['System.ChangedDate']).toBeDefined();
    }
  });

  test('should retrieve work item with expanded relations', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest() || !connection || !testWorkItemId) {
      return;
    }

    // Act - get work item with relations expansion
    const result = await getWorkItem(connection, testWorkItemId, 'relations');

    // Assert
    expect(result).toBeDefined();
    expect(result.id).toBe(testWorkItemId);

    // When using expand=relations on a work item with links, relations should be defined
    expect(result.relations).toBeDefined();

    // Verify we can access the related work item
    if (result.relations && result.relations.length > 0) {
      const relation = result.relations[0];
      expect(relation.rel).toBe('System.LinkTypes.Related');
      expect(relation.url).toContain(linkedWorkItemId?.toString());
    }

    // Verify fields exist
    expect(result.fields).toBeDefined();
    if (result.fields) {
      expect(result.fields['System.Title']).toBeDefined();
    }
  });

  test('should retrieve work item with minimal fields when using expand=none', async () => {
    if (shouldSkipIntegrationTest() || !connection || !testWorkItemId) {
      return;
    }

    // Act - get work item with no expansion
    const result = await getWorkItem(connection, testWorkItemId, 'none');

    // Assert
    expect(result).toBeDefined();
    expect(result.id).toBe(testWorkItemId);
    expect(result.fields).toBeDefined();

    // With expand=none, we should still get _links but no relations
    // The Azure DevOps API still returns _links even with expand=none
    expect(result.relations).toBeUndefined();
  });

  test('should throw AzureDevOpsResourceNotFoundError for non-existent work item', async () => {
    if (shouldSkipIntegrationTest() || !connection) {
      return;
    }

    // Use a very large ID that's unlikely to exist
    const nonExistentId = 999999999;

    // Assert that it throws the correct error
    await expect(getWorkItem(connection, nonExistentId)).rejects.toThrow(
      AzureDevOpsResourceNotFoundError,
    );
  });

  test('should include all possible fields with null values for empty fields', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest() || !connection || !testWorkItemId) {
      return;
    }

    // Act - get work item by ID
    const result = await getWorkItem(connection, testWorkItemId);

    // Assert
    expect(result).toBeDefined();
    expect(result.fields).toBeDefined();

    if (result.fields) {
      // Get a direct connection to WorkItemTrackingApi to fetch field info for comparison
      const witApi = await connection.getWorkItemTrackingApi();
      const projectName = result.fields['System.TeamProject'];
      const workItemType = result.fields['System.WorkItemType'];

      expect(projectName).toBeDefined();
      expect(workItemType).toBeDefined();

      if (projectName && workItemType) {
        // Get all possible field references for this work item type
        const allFields = await witApi.getWorkItemTypeFieldsWithReferences(
          projectName.toString(),
          workItemType.toString(),
        );

        // Check that all fields from the reference are present in the result
        // Some might be null, but they should exist in the fields object
        for (const field of allFields) {
          if (field.referenceName) {
            expect(Object.keys(result.fields)).toContain(field.referenceName);
          }
        }

        // There should be at least one field with a null value
        // (This is a probabilistic test but very likely to pass since work items
        // typically have many optional fields that aren't filled in)
        const hasNullField = Object.values(result.fields).some(
          (value) => value === null,
        );
        expect(hasNullField).toBe(true);
      }
    }
  });
});

```

--------------------------------------------------------------------------------
/src/features/wikis/index.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
import { isWikisRequest, handleWikisRequest } from './index';
import { getWikis, GetWikisSchema } from './get-wikis';
import { getWikiPage, GetWikiPageSchema } from './get-wiki-page';
import { createWiki, CreateWikiSchema, WikiType } from './create-wiki';
import { updateWikiPage, UpdateWikiPageSchema } from './update-wiki-page';

// Mock the imported modules
jest.mock('./get-wikis', () => ({
  getWikis: jest.fn(),
  GetWikisSchema: {
    parse: jest.fn(),
  },
}));

jest.mock('./get-wiki-page', () => ({
  getWikiPage: jest.fn(),
  GetWikiPageSchema: {
    parse: jest.fn(),
  },
}));

jest.mock('./create-wiki', () => ({
  createWiki: jest.fn(),
  CreateWikiSchema: {
    parse: jest.fn(),
  },
  WikiType: {
    ProjectWiki: 'projectWiki',
    CodeWiki: 'codeWiki',
  },
}));

jest.mock('./update-wiki-page', () => ({
  updateWikiPage: jest.fn(),
  UpdateWikiPageSchema: {
    parse: jest.fn(),
  },
}));

describe('Wikis Request Handlers', () => {
  const mockConnection = {} as WebApi;

  describe('isWikisRequest', () => {
    it('should return true for wikis requests', () => {
      const validTools = [
        'get_wikis',
        'get_wiki_page',
        'create_wiki',
        'update_wiki_page',
      ];
      validTools.forEach((tool) => {
        const request = {
          params: { name: tool, arguments: {} },
          method: 'tools/call',
        } as CallToolRequest;
        expect(isWikisRequest(request)).toBe(true);
      });
    });

    it('should return false for non-wikis requests', () => {
      const request = {
        params: { name: 'list_projects', arguments: {} },
        method: 'tools/call',
      } as CallToolRequest;
      expect(isWikisRequest(request)).toBe(false);
    });
  });

  describe('handleWikisRequest', () => {
    it('should handle get_wikis request', async () => {
      const mockWikis = [
        { id: 'wiki1', name: 'Wiki 1' },
        { id: 'wiki2', name: 'Wiki 2' },
      ];
      (getWikis as jest.Mock).mockResolvedValue(mockWikis);

      const request = {
        params: {
          name: 'get_wikis',
          arguments: {
            projectId: 'project1',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      // Mock the arguments object after parsing
      (GetWikisSchema.parse as jest.Mock).mockReturnValue({
        projectId: 'project1',
      });

      const response = await handleWikisRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(mockWikis);
      expect(getWikis).toHaveBeenCalledWith(
        mockConnection,
        expect.objectContaining({
          projectId: 'project1',
        }),
      );
    });

    it('should handle get_wiki_page request', async () => {
      const mockWikiContent = '# Wiki Page\n\nThis is a wiki page content.';
      (getWikiPage as jest.Mock).mockResolvedValue(mockWikiContent);

      const request = {
        params: {
          name: 'get_wiki_page',
          arguments: {
            projectId: 'project1',
            wikiId: 'wiki1',
            pagePath: '/Home',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      // Mock the arguments object after parsing
      (GetWikiPageSchema.parse as jest.Mock).mockReturnValue({
        projectId: 'project1',
        wikiId: 'wiki1',
        pagePath: '/Home',
      });

      const response = await handleWikisRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(response.content[0].text as string).toEqual(mockWikiContent);
      expect(getWikiPage).toHaveBeenCalledWith(
        expect.objectContaining({
          projectId: 'project1',
          wikiId: 'wiki1',
          pagePath: '/Home',
        }),
      );
    });

    it('should handle create_wiki request', async () => {
      const mockWiki = { id: 'wiki1', name: 'New Wiki' };
      (createWiki as jest.Mock).mockResolvedValue(mockWiki);

      const request = {
        params: {
          name: 'create_wiki',
          arguments: {
            projectId: 'project1',
            name: 'New Wiki',
            type: WikiType.ProjectWiki,
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      // Mock the arguments object after parsing
      (CreateWikiSchema.parse as jest.Mock).mockReturnValue({
        projectId: 'project1',
        name: 'New Wiki',
        type: WikiType.ProjectWiki,
        mappedPath: null, // Required field in the schema
      });

      const response = await handleWikisRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(mockWiki);
      expect(createWiki).toHaveBeenCalledWith(
        mockConnection,
        expect.objectContaining({
          projectId: 'project1',
          name: 'New Wiki',
          type: WikiType.ProjectWiki,
        }),
      );
    });

    it('should handle update_wiki_page request', async () => {
      const mockUpdateResult = { id: 'page1', content: 'Updated content' };
      (updateWikiPage as jest.Mock).mockResolvedValue(mockUpdateResult);

      const request = {
        params: {
          name: 'update_wiki_page',
          arguments: {
            projectId: 'project1',
            wikiId: 'wiki1',
            pagePath: '/Home',
            content: 'Updated content',
            comment: 'Update home page',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      // Mock the arguments object after parsing
      (UpdateWikiPageSchema.parse as jest.Mock).mockReturnValue({
        projectId: 'project1',
        wikiId: 'wiki1',
        pagePath: '/Home',
        content: 'Updated content',
        comment: 'Update home page',
      });

      const response = await handleWikisRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(
        mockUpdateResult,
      );
      expect(updateWikiPage).toHaveBeenCalledWith(
        expect.objectContaining({
          projectId: 'project1',
          wikiId: 'wiki1',
          pagePath: '/Home',
          content: 'Updated content',
          comment: 'Update home page',
        }),
      );
    });

    it('should throw error for unknown tool', async () => {
      const request = {
        params: {
          name: 'unknown_tool',
          arguments: {},
        },
        method: 'tools/call',
      } as CallToolRequest;

      await expect(handleWikisRequest(mockConnection, request)).rejects.toThrow(
        'Unknown wikis tool',
      );
    });

    it('should propagate errors from wiki functions', async () => {
      const mockError = new Error('Test error');
      (getWikis as jest.Mock).mockRejectedValue(mockError);

      const request = {
        params: {
          name: 'get_wikis',
          arguments: {
            projectId: 'project1',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      // Mock the arguments object after parsing
      (GetWikisSchema.parse as jest.Mock).mockReturnValue({
        projectId: 'project1',
      });

      await expect(handleWikisRequest(mockConnection, request)).rejects.toThrow(
        mockError,
      );
    });
  });
});

```

--------------------------------------------------------------------------------
/src/features/pull-requests/index.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
import { isPullRequestsRequest, handlePullRequestsRequest } from './index';
import { createPullRequest } from './create-pull-request';
import { listPullRequests } from './list-pull-requests';
import { getPullRequestComments } from './get-pull-request-comments';
import { addPullRequestComment } from './add-pull-request-comment';
import { AddPullRequestCommentSchema } from './schemas';

// Mock the imported modules
jest.mock('./create-pull-request', () => ({
  createPullRequest: jest.fn(),
}));

jest.mock('./list-pull-requests', () => ({
  listPullRequests: jest.fn(),
}));

jest.mock('./get-pull-request-comments', () => ({
  getPullRequestComments: jest.fn(),
}));

jest.mock('./add-pull-request-comment', () => ({
  addPullRequestComment: jest.fn(),
}));

describe('Pull Requests Request Handlers', () => {
  const mockConnection = {} as WebApi;

  describe('isPullRequestsRequest', () => {
    it('should return true for pull requests tools', () => {
      const validTools = [
        'create_pull_request',
        'list_pull_requests',
        'get_pull_request_comments',
        'add_pull_request_comment',
      ];
      validTools.forEach((tool) => {
        const request = {
          params: { name: tool, arguments: {} },
          method: 'tools/call',
        } as CallToolRequest;
        expect(isPullRequestsRequest(request)).toBe(true);
      });
    });

    it('should return false for non-pull requests tools', () => {
      const request = {
        params: { name: 'list_projects', arguments: {} },
        method: 'tools/call',
      } as CallToolRequest;
      expect(isPullRequestsRequest(request)).toBe(false);
    });
  });

  describe('handlePullRequestsRequest', () => {
    it('should handle create_pull_request request', async () => {
      const mockPullRequest = { id: 1, title: 'Test PR' };
      (createPullRequest as jest.Mock).mockResolvedValue(mockPullRequest);

      const request = {
        params: {
          name: 'create_pull_request',
          arguments: {
            repositoryId: 'test-repo',
            title: 'Test PR',
            sourceRefName: 'refs/heads/feature',
            targetRefName: 'refs/heads/main',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      const response = await handlePullRequestsRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(
        mockPullRequest,
      );
      expect(createPullRequest).toHaveBeenCalledWith(
        mockConnection,
        expect.any(String),
        'test-repo',
        expect.objectContaining({
          title: 'Test PR',
          sourceRefName: 'refs/heads/feature',
          targetRefName: 'refs/heads/main',
        }),
      );
    });

    it('should handle list_pull_requests request', async () => {
      const mockPullRequests = {
        count: 2,
        value: [
          { id: 1, title: 'PR 1' },
          { id: 2, title: 'PR 2' },
        ],
        hasMoreResults: false,
      };
      (listPullRequests as jest.Mock).mockResolvedValue(mockPullRequests);

      const request = {
        params: {
          name: 'list_pull_requests',
          arguments: {
            repositoryId: 'test-repo',
            status: 'active',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      const response = await handlePullRequestsRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(
        mockPullRequests,
      );
      expect(listPullRequests).toHaveBeenCalledWith(
        mockConnection,
        expect.any(String),
        'test-repo',
        expect.objectContaining({
          status: 'active',
        }),
      );
    });

    it('should handle get_pull_request_comments request', async () => {
      const mockComments = {
        threads: [
          {
            id: 1,
            comments: [{ id: 1, content: 'Comment 1' }],
          },
        ],
      };
      (getPullRequestComments as jest.Mock).mockResolvedValue(mockComments);

      const request = {
        params: {
          name: 'get_pull_request_comments',
          arguments: {
            repositoryId: 'test-repo',
            pullRequestId: 123,
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      const response = await handlePullRequestsRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(
        mockComments,
      );
      expect(getPullRequestComments).toHaveBeenCalledWith(
        mockConnection,
        expect.any(String),
        'test-repo',
        123,
        expect.objectContaining({
          pullRequestId: 123,
        }),
      );
    });

    it('should handle add_pull_request_comment request', async () => {
      const mockResult = {
        comment: { id: 1, content: 'New comment' },
        thread: { id: 1 },
      };
      (addPullRequestComment as jest.Mock).mockResolvedValue(mockResult);

      const request = {
        params: {
          name: 'add_pull_request_comment',
          arguments: {
            repositoryId: 'test-repo',
            pullRequestId: 123,
            content: 'New comment',
            status: 'active', // Status is required when creating a new thread
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      // Mock the schema parsing
      const mockParsedArgs = {
        repositoryId: 'test-repo',
        pullRequestId: 123,
        content: 'New comment',
        status: 'active',
      };

      // Use a different approach for mocking
      const originalParse = AddPullRequestCommentSchema.parse;
      AddPullRequestCommentSchema.parse = jest
        .fn()
        .mockReturnValue(mockParsedArgs);

      const response = await handlePullRequestsRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(
        mockResult,
      );
      expect(addPullRequestComment).toHaveBeenCalledWith(
        mockConnection,
        expect.any(String),
        'test-repo',
        123,
        expect.objectContaining({
          content: 'New comment',
        }),
      );

      // Restore the original parse function
      AddPullRequestCommentSchema.parse = originalParse;
    });

    it('should throw error for unknown tool', async () => {
      const request = {
        params: {
          name: 'unknown_tool',
          arguments: {},
        },
        method: 'tools/call',
      } as CallToolRequest;

      await expect(
        handlePullRequestsRequest(mockConnection, request),
      ).rejects.toThrow('Unknown pull requests tool');
    });

    it('should propagate errors from pull request functions', async () => {
      const mockError = new Error('Test error');
      (listPullRequests as jest.Mock).mockRejectedValue(mockError);

      const request = {
        params: {
          name: 'list_pull_requests',
          arguments: {
            repositoryId: 'test-repo',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      await expect(
        handlePullRequestsRequest(mockConnection, request),
      ).rejects.toThrow(mockError);
    });
  });
});

```

--------------------------------------------------------------------------------
/src/features/pull-requests/add-pull-request-comment/feature.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import { addPullRequestComment } from './feature';
import {
  Comment,
  CommentThreadStatus,
  CommentType,
  GitPullRequestCommentThread,
} from 'azure-devops-node-api/interfaces/GitInterfaces';

describe('addPullRequestComment', () => {
  afterEach(() => {
    jest.resetAllMocks();
  });

  test('should add a comment to an existing thread successfully', async () => {
    // Mock data for a new comment
    const mockComment: Comment = {
      id: 101,
      content: 'This is a reply comment',
      commentType: CommentType.Text,
      author: {
        displayName: 'Test User',
        id: 'test-user-id',
      },
      publishedDate: new Date(),
    };

    // Setup mock connection
    const mockGitApi = {
      createComment: jest.fn().mockResolvedValue(mockComment),
      createThread: jest.fn(),
    };

    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    };

    // Call the function with test parameters
    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const pullRequestId = 123;
    const threadId = 456;
    const options = {
      projectId,
      repositoryId,
      pullRequestId,
      threadId,
      content: 'This is a reply comment',
    };

    const result = await addPullRequestComment(
      mockConnection as WebApi,
      projectId,
      repositoryId,
      pullRequestId,
      options,
    );

    // Verify results (with transformed commentType)
    expect(result).toEqual({
      comment: {
        ...mockComment,
        commentType: 'text', // Transform enum to string
      },
    });
    expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1);
    expect(mockGitApi.createComment).toHaveBeenCalledTimes(1);
    expect(mockGitApi.createComment).toHaveBeenCalledWith(
      expect.objectContaining({ content: 'This is a reply comment' }),
      repositoryId,
      pullRequestId,
      threadId,
      projectId,
    );
    expect(mockGitApi.createThread).not.toHaveBeenCalled();
  });

  test('should create a new thread with a comment successfully', async () => {
    // Mock data for a new thread with comment
    const mockComment: Comment = {
      id: 100,
      content: 'This is a new comment',
      commentType: CommentType.Text,
      author: {
        displayName: 'Test User',
        id: 'test-user-id',
      },
      publishedDate: new Date(),
    };

    const mockThread: GitPullRequestCommentThread = {
      id: 789,
      comments: [mockComment],
      status: CommentThreadStatus.Active,
    };

    // Setup mock connection
    const mockGitApi = {
      createComment: jest.fn(),
      createThread: jest.fn().mockResolvedValue(mockThread),
    };

    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    };

    // Call the function with test parameters
    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const pullRequestId = 123;
    const options = {
      projectId,
      repositoryId,
      pullRequestId,
      content: 'This is a new comment',
      status: 'active' as const,
    };

    const result = await addPullRequestComment(
      mockConnection as WebApi,
      projectId,
      repositoryId,
      pullRequestId,
      options,
    );

    // Verify results
    expect(result).toEqual({
      comment: {
        ...mockComment,
        commentType: 'text',
      },
      thread: {
        ...mockThread,
        status: 'active',
        comments: mockThread.comments?.map((comment) => ({
          ...comment,
          commentType: 'text',
        })),
      },
    });
    expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1);
    expect(mockGitApi.createThread).toHaveBeenCalledTimes(1);
    expect(mockGitApi.createThread).toHaveBeenCalledWith(
      expect.objectContaining({
        comments: [
          expect.objectContaining({ content: 'This is a new comment' }),
        ],
        status: CommentThreadStatus.Active,
      }),
      repositoryId,
      pullRequestId,
      projectId,
    );
    expect(mockGitApi.createComment).not.toHaveBeenCalled();
  });

  test('should create a new thread on a file with line number', async () => {
    // Mock data for a new thread with comment on file
    const mockComment: Comment = {
      id: 100,
      content: 'This code needs improvement',
      commentType: CommentType.Text,
      author: {
        displayName: 'Test User',
        id: 'test-user-id',
      },
      publishedDate: new Date(),
    };

    const mockThread: GitPullRequestCommentThread = {
      id: 789,
      status: CommentThreadStatus.Active, // Add missing status
      comments: [mockComment],
      threadContext: {
        filePath: '/src/app.ts',
        rightFileStart: {
          line: 42,
          offset: 1,
        },
        rightFileEnd: {
          line: 42,
          offset: 1,
        },
      },
    };

    // Setup mock connection
    const mockGitApi = {
      createComment: jest.fn(),
      createThread: jest.fn().mockResolvedValue(mockThread),
    };

    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    };

    // Call the function with test parameters
    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const pullRequestId = 123;
    const options = {
      projectId,
      repositoryId,
      pullRequestId,
      content: 'This code needs improvement',
      filePath: '/src/app.ts',
      lineNumber: 42,
    };

    const result = await addPullRequestComment(
      mockConnection as WebApi,
      projectId,
      repositoryId,
      pullRequestId,
      options,
    );

    // Verify results
    expect(result).toEqual({
      comment: {
        ...mockComment,
        commentType: 'text',
      },
      thread: {
        ...mockThread,
        status: 'active',
        comments: mockThread.comments?.map((comment) => ({
          ...comment,
          commentType: 'text',
        })),
      },
    });
    expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1);
    expect(mockGitApi.createThread).toHaveBeenCalledTimes(1);
    expect(mockGitApi.createThread).toHaveBeenCalledWith(
      expect.objectContaining({
        comments: [
          expect.objectContaining({ content: 'This code needs improvement' }),
        ],
        threadContext: expect.objectContaining({
          filePath: '/src/app.ts',
          rightFileStart: expect.objectContaining({ line: 42 }),
          rightFileEnd: expect.objectContaining({ line: 42 }),
        }),
      }),
      repositoryId,
      pullRequestId,
      projectId,
    );
    expect(mockGitApi.createComment).not.toHaveBeenCalled();
  });

  test('should handle error when API call fails', async () => {
    // Setup mock connection with error
    const errorMessage = 'API error';
    const mockGitApi = {
      createComment: jest.fn().mockRejectedValue(new Error(errorMessage)),
      createThread: jest.fn(),
    };

    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    };

    // Call the function with test parameters
    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const pullRequestId = 123;
    const threadId = 456;
    const options = {
      projectId,
      repositoryId,
      pullRequestId,
      threadId,
      content: 'This is a reply comment',
    };

    // Verify error handling
    await expect(
      addPullRequestComment(
        mockConnection as WebApi,
        projectId,
        repositoryId,
        pullRequestId,
        options,
      ),
    ).rejects.toThrow(`Failed to add pull request comment: ${errorMessage}`);
  });
});

```

--------------------------------------------------------------------------------
/project-management/troubleshooter.xml:
--------------------------------------------------------------------------------

```
<TroubleshootingGuide>
    <Flowchart>
        graph TD
        A[Start: Identify Issue] --> B[Gather Information]
        B --> C[Form Hypotheses]
        C --> D[Test Hypotheses]
        D -->|Issue Resolved?| E[Implement Solution]
        D -->|Issue Persists| B
        E --> F[Verify Fix]
        F -->|Success| G[Document &amp; Conclude]
        F -->|Failure| B
    </Flowchart>
    <Introduction>
        Troubleshooting is an essential skill that remains vital across all fields, from software
        development to mechanical engineering and household repairs. It’s the art of systematically
        identifying and resolving problems, a competency that never goes obsolete due to its
        universal relevance. Whether you’re debugging a crashing application, fixing a car that
        won’t start, or repairing a leaky faucet, troubleshooting empowers you to tackle challenges
        methodically. This guide provides a framework that adapts to any domain, highlighting the
        importance of a structured approach and a problem-solving mindset.
    </Introduction>
    <Preparation>
        Preparation is the foundation of effective troubleshooting. Before addressing any issue,
        take these steps:
        1. **Gather Necessary Tools:** Equip yourself with the right resources—debugging software
        for coding, wrenches and multimeters for machinery, or a toolkit with pliers and tape for
        home repairs.
        2. **Understand the System:** Study the system or device involved. Review code
        documentation, machine schematics, or appliance manuals to grasp how it should function.
        3. **Ensure Safety:** Prioritize safety by disconnecting power, wearing protective gear, or
        shutting off water supplies as needed.
        4. **Document the Initial State:** Note symptoms, error messages, or unusual behaviors
        (e.g., a software error code, a grinding noise from an engine, or water pooling under a
        sink) to establish a baseline.
        Proper preparation minimizes guesswork and sets the stage for efficient problem-solving.
    </Preparation>
    <Diagnosis>
        Diagnosis is the core of troubleshooting, requiring a systematic approach to uncover the
        root cause:
        1. **Gather Information:** Collect data through observation or tools—check software logs,
        listen for mechanical noises, or inspect pipes for leaks.
        2. **Form Hypotheses:** Develop theories about the cause based on evidence. For a software
        bug, suspect a recent code change; for a car, consider a dead battery; for a leak, think of
        a loose seal.
        3. **Test Hypotheses:** Conduct targeted tests—run a software debug session, measure battery
        voltage with a multimeter, or tighten a pipe fitting and check for drips.
        4. **Analyze Results:** Assess test outcomes to confirm or adjust your hypotheses. If the
        issue persists, gather more data and refine your theories.
        For example, in software, replicate a crash and trace it to a faulty loop; in mechanical
        engineering, test a pump after hearing a whine; in household repairs, turn on water to
        locate a drip’s source. This process is iterative—loop back as needed until the problem is
        clear.
    </Diagnosis>
    <SolutionImplementation>
        With the cause identified, implement a solution methodically:
        1. **Prioritize Fixes:** Address critical issues first—fix a server outage before a minor UI
        glitch, replace a broken engine belt before tuning performance, or stop a major leak before
        patching a crack.
        2. **Apply the Solution:** Execute the fix—patch the code and deploy it, install a new part,
        or replace a worn washer. Follow best practices or guidelines specific to the domain.
        3. **Test the Solution:** Verify the fix works—run the software, start the engine, or turn
        on the tap to ensure functionality.
        4. **Document Changes:** Record actions taken, like code updates in a changelog, parts
        swapped in a maintenance log, or repair steps in a notebook, for future reference.
        Examples include deploying a software update and checking for crashes, replacing a car
        alternator and testing the charge, or sealing a pipe and ensuring no leaks remain. Precision
        here prevents new issues.
    </SolutionImplementation>
    <Verification>
        Verification confirms the issue is resolved and the system is stable:
        1. **Perform Functional Tests:** Run the system normally—execute software features, drive
        the car, or use the repaired appliance.
        2. **Check for Side Effects:** Look for unintended outcomes, like new software errors,
        engine vibrations, or damp spots near a fix.
        3. **Monitor Over Time:** Observe performance longer-term—watch software logs for a day, run
        machinery through cycles, or check a pipe after hours of use.
        4. **Get User Feedback:** If applicable, ask users (e.g., software testers, car owners, or
        household members) to confirm the problem is gone.
        For instance, monitor a web app post-fix for uptime, test a repaired tractor under load, or
        ensure a faucet stays dry overnight. Thorough verification ensures lasting success.
    </Verification>
    <CommonMistakes>
        Avoid these pitfalls to troubleshoot effectively:
        1. **Jumping to Conclusions:** Assuming a software crash is a server issue without logs, or
        replacing an engine part without testing, wastes time—always validate hypotheses.
        2. **Neglecting Documentation:** Skipping notes on code changes or repair steps complicates
        future fixes—keep detailed records.
        3. **Overlooking Simple Solutions:** A reboot might fix a software glitch, or a loose bolt
        could be the mechanical issue—check the obvious first.
        4. **Insufficient Testing:** Deploying a patch without full tests, or assuming a pipe is
        fixed after a quick look, risks recurrence—test rigorously.
        5. **Ignoring Safety:** Debugging live circuits or repairing plumbing without shutoffs
        invites danger—prioritize safety always.
        Awareness of these errors keeps your process on track.
    </CommonMistakes>
    <MindsetTips>
        Cultivate this mindset for troubleshooting success:
        1. **Patience:** Problems may resist quick fixes—stay calm when a bug eludes you or a repair
        takes hours.
        2. **Attention to Detail:** Notice subtle clues—a log timestamp, a faint hum, or a drip
        pattern can crack the case.
        3. **Persistence:** If a fix fails, keep testing—don’t abandon a software trace or a machine
        teardown midstream.
        4. **Open-Mindedness:** A bug might stem from an overlooked module, or a leak from an
        unexpected joint—stay flexible.
        5. **Learning Orientation:** Each challenge teaches something—log a new coding trick, a
        mechanical quirk, or a repair tip.
        6. **Collaboration:** Seek input—a colleague might spot a code flaw or a neighbor recall a
        similar fix.
        This mindset turns obstacles into opportunities.
    </MindsetTips>
    <Conclusion>
        Troubleshooting is a timeless skill that equips you to solve problems anywhere—from
        codebases to engines to homes. This guide’s systematic approach—preparing diligently,
        diagnosing precisely, implementing thoughtfully, and verifying completely—ensures success
        across domains. Avoiding common mistakes and embracing a resilient mindset amplify your
        effectiveness. Mastering troubleshooting not only boosts professional prowess but also
        fosters everyday resourcefulness, making it a skill worth honing for life.
    </Conclusion>
</TroubleshootingGuide>
```

--------------------------------------------------------------------------------
/project-management/startup.xml:
--------------------------------------------------------------------------------

```
<AiTaskAgent>
  <GlobalRule alwaysApply="true">If an ANY point you get stuck, review troubleshooter.xml to help you troubleshoot the problem.</GlobalRule>
  <GlobalRule alwaysApply="true">All new code creation should ALWAYS follow tdd-cycle.xml</GlobalRule>
  <GlobalRule alwaysApply="true">Tasks in the GitHub project board at https://github.com/users/Tiberriver256/projects/1 are sorted in order of priority - ALWAYS pick the task from the top of the backlog column.</GlobalRule>
  <GlobalRule alwaysApply="true">Always use the GitHub CLI (gh) for project and issue management. If documentation is needed, use browser_navigate to access the documentation. Always use a markdown file for writing/updating issues rather than trying to work with cli args.</GlobalRule>
  <GlobalRule alwaysApply="true">There is a strict WIP limit of 1. If any issue is in the Research, Implementation, or In Review status, that issue MUST be completed before starting a new one from Backlog.</GlobalRule>
  <GlobalRule alwaysApply="true">We are always operating as the GitHub user 'Tiberriver256'. All issues must be assigned to 'Tiberriver256' before starting work on them.</GlobalRule>
  <GlobalRule alwaysApply="true">To update a project item status, first get the project ID with `gh project list --owner Tiberriver256 --format json`, then get the item ID with `gh project item-list [project-id] --format json`, and finally update the status with `gh project item-edit --id [item-id] --project-id [project-id] --field-id PVTSSF_lAHOAGqmtM4A2BrBzgrZoeI --single-select-option-id [status-id]`. Status IDs are: Backlog (f75ad846), Research (61e4505c), Implementation (47fc9ee4), In review (df73e18b), Done (98236657).</GlobalRule>
  <GlobalRule alwaysApply="true">To create a GitHub issue: 1) Create a markdown file for the issue body (e.g., `issue_body.md`), 2) Use `gh issue create --title "Issue Title" --body-file issue_body.md --label "enhancement"` to create the issue, 3) Assign it with `gh issue edit [issue-number] --add-assignee Tiberriver256`, 4) Add status label with `gh issue edit [issue-number] --add-label "status:research"`, 5) Add to project with `gh project item-add [project-id] --owner Tiberriver256 --url [issue-url]`, and 6) Update status in project using the project item-edit command as described above.</GlobalRule>
  <InitialSetup order="1">
    <Step order="1">Read the dream team documentation at project-management/planning/the-dream-team.md to understand the team structure and roles</Step>
    <Step order="2">Read all files in the project-management/planning directory to understand the project architecture, features, and structure</Step>
    <Step order="3">Check if there is any issue in the GitHub project board at https://github.com/users/Tiberriver256/projects/1 with a status of "Research", "Implementation", or "In Review". Use 'gh project item-list' to check the current issues and their status.</Step>
    <Step order="4">
      If there is any issue in "Research", "Implementation", or "In Review" status, ensure it is assigned to 'Tiberriver256' and work on that issue, moving directly into the appropriate phase of TaskWorkflow.
      If not, take the FIRST issue from the top of the "Backlog" status in the project board at https://github.com/users/Tiberriver256/projects/1, assign it to 'Tiberriver256', and update its status to "Research". 
      Remember that issues are sorted by priority with most important at the top. Add a comment with your implementation approach and planned sub-tasks if needed. Use the GitHub CLI (gh) for all project and issue management.
    </Step>
    <Step order="5">Create a new branch for the current task, branching from the latest main branch. Use a descriptive name for the branch, related to the task, by running ./create_branch.sh &lt;branch_name&gt;.</Step>
    <Step order="6">Read tdd-cycle.xml to understand the TDD cycle.</Step>
    <Step order="7">Read all files in the docs/testing directory to understand the testing strategy.</Step>
    <Step order="8">Start the research phase of TaskWorkflow.</Step>
  </InitialSetup>
  
  <TaskWorkflow order="2">
    <Phase name="Research" order="1">
      <Step order="1">Make sure the issue is assigned to 'Tiberriver256' and its status is set to "Research" in the GitHub project board.</Step>
      <Step order="2">Research the selected GitHub issue thoroughly</Step>
      <Step order="3">Create notes in a comment on the GitHub issue about your approach. Use the GitHub CLI (gh) for interacting with issues.</Step>
      <Step order="4">Break down the task into sub-tasks only if necessary (prefer simplicity)</Step>
      <Step order="5">If the task is straightforward, keep it as a single task</Step>
    </Phase>
    
    <Phase name="Planning" order="2">
      <STOPPING_POINT order="1">Present your sub-tasks (if any) and approach for approval</STOPPING_POINT>
    </Phase>
    
    <Phase name="Implementation" order="3">
      <Step order="1">Update the issue status to "Implementation" in the GitHub project board, ensuring it remains assigned to 'Tiberriver256'.</Step>
      <Step order="2">Assume the role and persona of the team member assigned to the task</Step>
      <Step order="3">If multiple roles are involved, simulate pair/mob programming</Step>
      <Step order="4">Use Test-Driven Development for all coding tasks</Step>
      <Step order="5">Create any necessary readme.md files for documentation or reference</Step>
    </Phase>
    
    <Phase name="Completion" order="4">
      <Step order="1">Create a pull request and update the issue status to "In Review" in the GitHub project board, ensuring it remains assigned to 'Tiberriver256'.</Step>
      <STOPPING_POINT order="2">Present your work for review</STOPPING_POINT>
      <Step order="3">Address any feedback, and present for re-review; Continue in this manner until approved</Step>
      <Step order="4">When the task is approved, run ./finish_task.sh "PR Title" "PR Description" to commit, push, and update the PR for the repository at https://github.com/Tiberriver256/mcp-server-azure-devops</Step>
      <Step order="5">After the PR is merged, update the issue status to "Done" and close the GitHub issue with an appropriate comment summarizing the work done. Use the GitHub CLI (gh) to close issues.</Step>
      <Step order="6">Wait for feedback before starting a new task</Step>
    </Phase>
  </TaskWorkflow>
  
  <WorkingPrinciples>
    <Principle>Use the tree command when exploring directory structures</Principle>
    <Principle>Follow KISS (Keep It Stupid Simple) and YAGNI (You Aren't Gonna Need It) principles</Principle>
    <Principle>Focus on delivery rather than over-engineering or gold-plating features</Principle>
    <Principle>Implement Test-Driven Development for all code</Principle>
    <Principle>Use the GitHub CLI (gh) for any GitHub-related tasks including project and issue management. Documentation is available at:
      - GitHub Projects CLI: https://cli.github.com/manual/gh_project
      - GitHub Issues CLI: https://cli.github.com/manual/gh_issue</Principle>
    <Principle>If GitHub CLI documentation is needed, use browser_navigate to access documentation</Principle>
    <Principle>Use Puppeteer if web browsing is required</Principle>
    <Principle>If any task is unclear, stop and ask for clarification before proceeding</Principle>
    <Principle>Always take tasks from the top of the GitHub project backlog column at https://github.com/users/Tiberriver256/projects/1 as they are sorted in priority order</Principle>
    <Principle>Strictly adhere to the WIP limit of 1 - only one issue should be in Research, Implementation, or In Review status at any time</Principle>
    <Principle>Move issues through the status workflow: Backlog → Research → Implementation → In Review → Done</Principle>
    <Principle>All work is performed as the GitHub user 'Tiberriver256'. Ensure all issues you work on are assigned to this user.</Principle>
  </WorkingPrinciples>
</AiTaskAgent>

```

--------------------------------------------------------------------------------
/src/features/repositories/index.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
import { isRepositoriesRequest, handleRepositoriesRequest } from './index';
import { getRepository } from './get-repository';
import { getRepositoryDetails } from './get-repository-details';
import { listRepositories } from './list-repositories';
import { getFileContent } from './get-file-content';
import {
  getAllRepositoriesTree,
  formatRepositoryTree,
} from './get-all-repositories-tree';
import { GitVersionType } from 'azure-devops-node-api/interfaces/GitInterfaces';

// Mock the imported modules
jest.mock('./get-repository', () => ({
  getRepository: jest.fn(),
}));

jest.mock('./get-repository-details', () => ({
  getRepositoryDetails: jest.fn(),
}));

jest.mock('./list-repositories', () => ({
  listRepositories: jest.fn(),
}));

jest.mock('./get-file-content', () => ({
  getFileContent: jest.fn(),
}));

jest.mock('./get-all-repositories-tree', () => ({
  getAllRepositoriesTree: jest.fn(),
  formatRepositoryTree: jest.fn(),
}));

describe('Repositories Request Handlers', () => {
  const mockConnection = {} as WebApi;

  describe('isRepositoriesRequest', () => {
    it('should return true for repositories requests', () => {
      const validTools = [
        'get_repository',
        'get_repository_details',
        'list_repositories',
        'get_file_content',
        'get_all_repositories_tree',
      ];
      validTools.forEach((tool) => {
        const request = {
          params: { name: tool, arguments: {} },
          method: 'tools/call',
        } as CallToolRequest;
        expect(isRepositoriesRequest(request)).toBe(true);
      });
    });

    it('should return false for non-repositories requests', () => {
      const request = {
        params: { name: 'list_projects', arguments: {} },
        method: 'tools/call',
      } as CallToolRequest;
      expect(isRepositoriesRequest(request)).toBe(false);
    });
  });

  describe('handleRepositoriesRequest', () => {
    it('should handle get_repository request', async () => {
      const mockRepository = { id: 'repo1', name: 'Repository 1' };
      (getRepository as jest.Mock).mockResolvedValue(mockRepository);

      const request = {
        params: {
          name: 'get_repository',
          arguments: {
            repositoryId: 'repo1',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      const response = await handleRepositoriesRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(
        mockRepository,
      );
      expect(getRepository).toHaveBeenCalledWith(
        mockConnection,
        expect.any(String),
        'repo1',
      );
    });

    it('should handle get_repository_details request', async () => {
      const mockRepositoryDetails = {
        repository: { id: 'repo1', name: 'Repository 1' },
        statistics: { branches: [] },
        refs: { value: [], count: 0 },
      };
      (getRepositoryDetails as jest.Mock).mockResolvedValue(
        mockRepositoryDetails,
      );

      const request = {
        params: {
          name: 'get_repository_details',
          arguments: {
            repositoryId: 'repo1',
            includeStatistics: true,
            includeRefs: true,
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      const response = await handleRepositoriesRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(
        mockRepositoryDetails,
      );
      expect(getRepositoryDetails).toHaveBeenCalledWith(
        mockConnection,
        expect.objectContaining({
          repositoryId: 'repo1',
          includeStatistics: true,
          includeRefs: true,
        }),
      );
    });

    it('should handle list_repositories request', async () => {
      const mockRepositories = [
        { id: 'repo1', name: 'Repository 1' },
        { id: 'repo2', name: 'Repository 2' },
      ];
      (listRepositories as jest.Mock).mockResolvedValue(mockRepositories);

      const request = {
        params: {
          name: 'list_repositories',
          arguments: {
            projectId: 'project1',
            includeLinks: true,
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      const response = await handleRepositoriesRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(
        mockRepositories,
      );
      expect(listRepositories).toHaveBeenCalledWith(
        mockConnection,
        expect.objectContaining({
          projectId: 'project1',
          includeLinks: true,
        }),
      );
    });

    it('should handle get_file_content request', async () => {
      const mockFileContent = { content: 'file content', isFolder: false };
      (getFileContent as jest.Mock).mockResolvedValue(mockFileContent);

      const request = {
        params: {
          name: 'get_file_content',
          arguments: {
            repositoryId: 'repo1',
            path: '/path/to/file',
            version: 'main',
            versionType: 'branch',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      const response = await handleRepositoriesRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(
        mockFileContent,
      );
      expect(getFileContent).toHaveBeenCalledWith(
        mockConnection,
        expect.any(String),
        'repo1',
        '/path/to/file',
        { versionType: GitVersionType.Branch, version: 'main' },
      );
    });

    it('should handle get_all_repositories_tree request', async () => {
      const mockTreeResponse = {
        repositories: [
          {
            name: 'repo1',
            tree: [
              { name: 'file1', path: '/file1', isFolder: false, level: 0 },
            ],
            stats: { directories: 0, files: 1 },
          },
        ],
      };
      (getAllRepositoriesTree as jest.Mock).mockResolvedValue(mockTreeResponse);
      (formatRepositoryTree as jest.Mock).mockReturnValue('repo1\n  file1\n');

      const request = {
        params: {
          name: 'get_all_repositories_tree',
          arguments: {
            projectId: 'project1',
            depth: 2,
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      const response = await handleRepositoriesRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(response.content[0].text as string).toContain('repo1');
      expect(getAllRepositoriesTree).toHaveBeenCalledWith(
        mockConnection,
        expect.objectContaining({
          projectId: 'project1',
          depth: 2,
        }),
      );
      expect(formatRepositoryTree).toHaveBeenCalledWith(
        'repo1',
        expect.any(Array),
        expect.any(Object),
        undefined,
      );
    });

    it('should throw error for unknown tool', async () => {
      const request = {
        params: {
          name: 'unknown_tool',
          arguments: {},
        },
        method: 'tools/call',
      } as CallToolRequest;

      await expect(
        handleRepositoriesRequest(mockConnection, request),
      ).rejects.toThrow('Unknown repositories tool');
    });

    it('should propagate errors from repository functions', async () => {
      const mockError = new Error('Test error');
      (listRepositories as jest.Mock).mockRejectedValue(mockError);

      const request = {
        params: {
          name: 'list_repositories',
          arguments: {
            projectId: 'project1',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      await expect(
        handleRepositoriesRequest(mockConnection, request),
      ).rejects.toThrow(mockError);
    });
  });
});

```
Page 3/6FirstPrevNextLast