#
tokens: 49805/50000 22/281 files (page 4/8)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 of 8. Use http://codebase.md/tiberriver256/azure-devops-mcp?lines=true&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

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

```markdown
  1 | # Azure Identity Authentication for Azure DevOps MCP Server
  2 | 
  3 | This guide explains how to use Azure Identity authentication with the Azure DevOps MCP Server.
  4 | 
  5 | ## Overview
  6 | 
  7 | 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:
  8 | 
  9 | - **Unified authentication**: Use the same credentials for Azure and Azure DevOps
 10 | - **Enhanced security**: Support for managed identities and client certificates
 11 | - **Flexible credential types**: Multiple options for different environments
 12 | - **Automatic token management**: Handles token acquisition and renewal
 13 | 
 14 | ## Credential Types
 15 | 
 16 | The Azure DevOps MCP Server supports multiple credential types through the Azure Identity SDK:
 17 | 
 18 | ### DefaultAzureCredential
 19 | 
 20 | This credential type attempts multiple authentication methods in sequence until one succeeds:
 21 | 
 22 | 1. Environment variables (EnvironmentCredential)
 23 | 2. Managed Identity (ManagedIdentityCredential)
 24 | 3. Azure CLI (AzureCliCredential)
 25 | 4. Visual Studio Code (VisualStudioCodeCredential)
 26 | 5. Azure PowerShell (AzurePowerShellCredential)
 27 | 
 28 | It's a great option for applications that need to work across different environments without code changes.
 29 | 
 30 | ### AzureCliCredential
 31 | 
 32 | This credential type uses your Azure CLI login. It's perfect for local development when you're already using the Azure CLI.
 33 | 
 34 | ## Configuration
 35 | 
 36 | ### Environment Variables
 37 | 
 38 | To use Azure Identity authentication, set the following environment variables:
 39 | 
 40 | ```bash
 41 | # Required
 42 | AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
 43 | AZURE_DEVOPS_AUTH_METHOD=azure-identity
 44 | 
 45 | # Optional
 46 | AZURE_DEVOPS_DEFAULT_PROJECT=your-project-name
 47 | ```
 48 | 
 49 | For service principal authentication, add these environment variables:
 50 | 
 51 | ```bash
 52 | AZURE_TENANT_ID=your-tenant-id
 53 | AZURE_CLIENT_ID=your-client-id
 54 | AZURE_CLIENT_SECRET=your-client-secret
 55 | ```
 56 | 
 57 | ### Use with Claude Desktop/Cursor AI
 58 | 
 59 | Add the following to your configuration file:
 60 | 
 61 | ```json
 62 | {
 63 |   "mcpServers": {
 64 |     "azureDevOps": {
 65 |       "command": "npx",
 66 |       "args": ["-y", "@tiberriver256/mcp-server-azure-devops"],
 67 |       "env": {
 68 |         "AZURE_DEVOPS_ORG_URL": "https://dev.azure.com/your-organization",
 69 |         "AZURE_DEVOPS_AUTH_METHOD": "azure-identity",
 70 |         "AZURE_DEVOPS_DEFAULT_PROJECT": "your-project-name"
 71 |       }
 72 |     }
 73 |   }
 74 | }
 75 | ```
 76 | 
 77 | ## Authentication Methods
 78 | 
 79 | ### Method 1: Using Azure CLI
 80 | 
 81 | 1. Install the Azure CLI from [here](https://docs.microsoft.com/cli/azure/install-azure-cli)
 82 | 2. Log in to Azure:
 83 |    ```bash
 84 |    az login
 85 |    ```
 86 | 3. Set up your environment variables:
 87 |    ```bash
 88 |    AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
 89 |    AZURE_DEVOPS_AUTH_METHOD=azure-identity
 90 |    ```
 91 | 
 92 | ### Method 2: Using Service Principal
 93 | 
 94 | 1. Create a service principal in Azure AD:
 95 |    ```bash
 96 |    az ad sp create-for-rbac --name "MyAzureDevOpsApp"
 97 |    ```
 98 | 2. Grant the service principal access to your Azure DevOps organization
 99 | 3. Set up your environment variables:
100 |    ```bash
101 |    AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
102 |    AZURE_DEVOPS_AUTH_METHOD=azure-identity
103 |    AZURE_TENANT_ID=your-tenant-id
104 |    AZURE_CLIENT_ID=your-client-id
105 |    AZURE_CLIENT_SECRET=your-client-secret
106 |    ```
107 | 
108 | ### Method 3: Using Managed Identity (for Azure-hosted applications)
109 | 
110 | 1. Enable managed identity for your Azure resource (VM, App Service, etc.)
111 | 2. Grant the managed identity access to your Azure DevOps organization
112 | 3. Set up your environment variables:
113 |    ```bash
114 |    AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
115 |    AZURE_DEVOPS_AUTH_METHOD=azure-identity
116 |    ```
117 | 
118 | ## Troubleshooting
119 | 
120 | ### Common Issues
121 | 
122 | #### Failed to acquire token
123 | 
124 | ```
125 | Error: Failed to authenticate with Azure Identity: CredentialUnavailableError: DefaultAzureCredential failed to retrieve a token
126 | ```
127 | 
128 | **Possible solutions:**
129 | - Ensure you're logged in with `az login`
130 | - Check if your managed identity is correctly configured
131 | - Verify that service principal credentials are correct
132 | 
133 | #### Permission issues
134 | 
135 | ```
136 | Error: Failed to authenticate with Azure Identity: AuthorizationFailed: The client does not have authorization to perform action
137 | ```
138 | 
139 | **Possible solutions:**
140 | - Ensure your identity has the necessary permissions in Azure DevOps
141 | - Check if you need to add your identity to specific Azure DevOps project(s)
142 | 
143 | #### Network issues
144 | 
145 | ```
146 | Error: Failed to authenticate with Azure Identity: ClientAuthError: Interaction required
147 | ```
148 | 
149 | **Possible solutions:**
150 | - Check your network connectivity
151 | - Verify that your firewall allows connections to Azure services
152 | 
153 | ## Best Practices
154 | 
155 | 1. **Choose the right credential type for your environment**:
156 |    - For local development: Azure CLI credential
157 |    - For CI/CD pipelines: Service principal
158 |    - For Azure-hosted applications: Managed identity
159 | 
160 | 2. **Follow the principle of least privilege**:
161 |    - Only grant the permissions needed for your use case
162 |    - Regularly audit and review permissions
163 | 
164 | 3. **Rotate credentials regularly**:
165 |    - For service principals, rotate client secrets periodically
166 |    - Use certificate-based authentication when possible for enhanced security
167 | 
168 | ## Examples
169 | 
170 | ### Basic configuration with Azure CLI
171 | 
172 | ```bash
173 | AZURE_DEVOPS_ORG_URL=https://dev.azure.com/mycompany
174 | AZURE_DEVOPS_AUTH_METHOD=azure-identity
175 | AZURE_DEVOPS_DEFAULT_PROJECT=MyProject
176 | ```
177 | 
178 | ### Service principal authentication
179 | 
180 | ```bash
181 | AZURE_DEVOPS_ORG_URL=https://dev.azure.com/mycompany
182 | AZURE_DEVOPS_AUTH_METHOD=azure-identity
183 | AZURE_DEVOPS_DEFAULT_PROJECT=MyProject
184 | AZURE_TENANT_ID=00000000-0000-0000-0000-000000000000
185 | AZURE_CLIENT_ID=11111111-1111-1111-1111-111111111111
186 | AZURE_CLIENT_SECRET=your-client-secret
187 | ```
188 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { addPullRequestComment } from './feature';
  3 | import { listPullRequests } from '../list-pull-requests/feature';
  4 | import {
  5 |   getTestConnection,
  6 |   shouldSkipIntegrationTest,
  7 | } from '@/shared/test/test-helpers';
  8 | 
  9 | describe('addPullRequestComment integration', () => {
 10 |   let connection: WebApi | null = null;
 11 |   let projectName: string;
 12 |   let repositoryName: string;
 13 |   let pullRequestId: number;
 14 | 
 15 |   // Generate unique identifiers using timestamp for comment content
 16 |   const timestamp = Date.now();
 17 |   const randomSuffix = Math.floor(Math.random() * 1000);
 18 | 
 19 |   beforeAll(async () => {
 20 |     // Get a real connection using environment variables
 21 |     connection = await getTestConnection();
 22 | 
 23 |     // Set up project and repository names from environment
 24 |     projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
 25 |     repositoryName = process.env.AZURE_DEVOPS_DEFAULT_REPOSITORY || '';
 26 | 
 27 |     // Skip setup if integration tests should be skipped
 28 |     if (shouldSkipIntegrationTest() || !connection) {
 29 |       return;
 30 |     }
 31 | 
 32 |     try {
 33 |       // Find an active pull request to use for testing
 34 |       const pullRequests = await listPullRequests(
 35 |         connection,
 36 |         projectName,
 37 |         repositoryName,
 38 |         {
 39 |           projectId: projectName,
 40 |           repositoryId: repositoryName,
 41 |           status: 'active',
 42 |           top: 1,
 43 |         },
 44 |       );
 45 | 
 46 |       if (!pullRequests || pullRequests.value.length === 0) {
 47 |         throw new Error('No active pull requests found for testing');
 48 |       }
 49 | 
 50 |       pullRequestId = pullRequests.value[0].pullRequestId!;
 51 |       console.log(`Using existing pull request #${pullRequestId} for testing`);
 52 |     } catch (error) {
 53 |       console.error('Error in test setup:', error);
 54 |       throw error;
 55 |     }
 56 |   });
 57 | 
 58 |   test('should add a new comment thread to pull request', async () => {
 59 |     // Skip if integration tests should be skipped
 60 |     if (shouldSkipIntegrationTest() || !connection) {
 61 |       console.log('Skipping test due to missing connection');
 62 |       return;
 63 |     }
 64 | 
 65 |     // Skip if repository name is not defined
 66 |     if (!repositoryName) {
 67 |       console.log('Skipping test due to missing repository name');
 68 |       return;
 69 |     }
 70 | 
 71 |     const commentContent = `Test comment ${timestamp}-${randomSuffix}`;
 72 | 
 73 |     const result = await addPullRequestComment(
 74 |       connection,
 75 |       projectName,
 76 |       repositoryName,
 77 |       pullRequestId,
 78 |       {
 79 |         projectId: projectName,
 80 |         repositoryId: repositoryName,
 81 |         pullRequestId,
 82 |         content: commentContent,
 83 |         status: 'active',
 84 |       },
 85 |     );
 86 | 
 87 |     // Verify the comment was created
 88 |     expect(result.comment).toBeDefined();
 89 |     expect(result.comment.content).toBe(commentContent);
 90 |     expect(result.thread).toBeDefined();
 91 |     expect(result.thread!.status).toBe('active'); // Transformed to string
 92 |   }, 30000); // 30 second timeout for integration test
 93 | 
 94 |   test('should add a file comment to pull request', async () => {
 95 |     // Skip if integration tests should be skipped
 96 |     if (shouldSkipIntegrationTest() || !connection) {
 97 |       console.log('Skipping test due to missing connection');
 98 |       return;
 99 |     }
100 | 
101 |     // Skip if repository name is not defined
102 |     if (!repositoryName) {
103 |       console.log('Skipping test due to missing repository name');
104 |       return;
105 |     }
106 | 
107 |     const commentContent = `File comment ${timestamp}-${randomSuffix}`;
108 |     const filePath = '/README.md'; // Assuming README.md exists in the repo
109 |     const lineNumber = 1;
110 | 
111 |     const result = await addPullRequestComment(
112 |       connection,
113 |       projectName,
114 |       repositoryName,
115 |       pullRequestId,
116 |       {
117 |         projectId: projectName,
118 |         repositoryId: repositoryName,
119 |         pullRequestId,
120 |         content: commentContent,
121 |         filePath,
122 |         lineNumber,
123 |         status: 'active',
124 |       },
125 |     );
126 | 
127 |     // Verify the file comment was created
128 |     expect(result.comment).toBeDefined();
129 |     expect(result.comment.content).toBe(commentContent);
130 |     expect(result.thread).toBeDefined();
131 |     expect(result.thread!.threadContext).toBeDefined();
132 |     expect(result.thread!.threadContext!.filePath).toBe(filePath);
133 |     expect(result.thread!.threadContext!.rightFileStart!.line).toBe(lineNumber);
134 |   }, 30000); // 30 second timeout for integration test
135 | 
136 |   test('should add a reply to an existing comment thread', async () => {
137 |     // Skip if integration tests should be skipped
138 |     if (shouldSkipIntegrationTest() || !connection) {
139 |       console.log('Skipping test due to missing connection');
140 |       return;
141 |     }
142 | 
143 |     // Skip if repository name is not defined
144 |     if (!repositoryName) {
145 |       console.log('Skipping test due to missing repository name');
146 |       return;
147 |     }
148 | 
149 |     // First create a thread
150 |     const initialComment = await addPullRequestComment(
151 |       connection,
152 |       projectName,
153 |       repositoryName,
154 |       pullRequestId,
155 |       {
156 |         projectId: projectName,
157 |         repositoryId: repositoryName,
158 |         pullRequestId,
159 |         content: `Initial comment ${timestamp}-${randomSuffix}`,
160 |         status: 'active',
161 |       },
162 |     );
163 | 
164 |     const threadId = initialComment.thread!.id!;
165 |     const replyContent = `Reply comment ${timestamp}-${randomSuffix}`;
166 | 
167 |     // Add a reply to the thread
168 |     const result = await addPullRequestComment(
169 |       connection,
170 |       projectName,
171 |       repositoryName,
172 |       pullRequestId,
173 |       {
174 |         projectId: projectName,
175 |         repositoryId: repositoryName,
176 |         pullRequestId,
177 |         content: replyContent,
178 |         threadId,
179 |       },
180 |     );
181 | 
182 |     // Verify the reply was created
183 |     expect(result.comment).toBeDefined();
184 |     expect(result.comment.content).toBe(replyContent);
185 |     expect(result.thread).toBeUndefined(); // No thread returned for replies
186 |   }, 30000); // 30 second timeout for integration test
187 | });
188 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { createWorkItem } from './feature';
  3 | import {
  4 |   getTestConnection,
  5 |   shouldSkipIntegrationTest,
  6 | } from '@/shared/test/test-helpers';
  7 | import { CreateWorkItemOptions } from '../types';
  8 | 
  9 | describe('createWorkItem integration', () => {
 10 |   let connection: WebApi | null = null;
 11 | 
 12 |   beforeAll(async () => {
 13 |     // Get a real connection using environment variables
 14 |     connection = await getTestConnection();
 15 |   });
 16 | 
 17 |   test('should create a new work item in Azure DevOps', async () => {
 18 |     // Skip if no connection is available
 19 |     if (shouldSkipIntegrationTest()) {
 20 |       return;
 21 |     }
 22 | 
 23 |     // This connection must be available if we didn't skip
 24 |     if (!connection) {
 25 |       throw new Error(
 26 |         'Connection should be available when test is not skipped',
 27 |       );
 28 |     }
 29 | 
 30 |     // Create a unique title using timestamp to avoid conflicts
 31 |     const uniqueTitle = `Test Work Item ${new Date().toISOString()}`;
 32 | 
 33 |     // For a true integration test, use a real project
 34 |     const projectName =
 35 |       process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
 36 |     const workItemType = 'Task'; // Assumes 'Task' type exists in the project
 37 | 
 38 |     const options: CreateWorkItemOptions = {
 39 |       title: uniqueTitle,
 40 |       description: 'This is a test work item created by an integration test',
 41 |       priority: 2,
 42 |     };
 43 | 
 44 |     // Act - make an actual API call to Azure DevOps
 45 |     const result = await createWorkItem(
 46 |       connection,
 47 |       projectName,
 48 |       workItemType,
 49 |       options,
 50 |     );
 51 | 
 52 |     // Assert on the actual response
 53 |     expect(result).toBeDefined();
 54 |     expect(result.id).toBeDefined();
 55 | 
 56 |     // Verify fields match what we set
 57 |     expect(result.fields).toBeDefined();
 58 |     if (result.fields) {
 59 |       expect(result.fields['System.Title']).toBe(uniqueTitle);
 60 |       expect(result.fields['Microsoft.VSTS.Common.Priority']).toBe(2);
 61 |     }
 62 |   });
 63 | 
 64 |   test('should create a work item with additional fields', async () => {
 65 |     // Skip if no connection is available
 66 |     if (shouldSkipIntegrationTest()) {
 67 |       return;
 68 |     }
 69 | 
 70 |     // This connection must be available if we didn't skip
 71 |     if (!connection) {
 72 |       throw new Error(
 73 |         'Connection should be available when test is not skipped',
 74 |       );
 75 |     }
 76 | 
 77 |     // Create a unique title using timestamp to avoid conflicts
 78 |     const uniqueTitle = `Test Work Item with Fields ${new Date().toISOString()}`;
 79 | 
 80 |     // For a true integration test, use a real project
 81 |     const projectName =
 82 |       process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
 83 |     const workItemType = 'Task';
 84 | 
 85 |     const options: CreateWorkItemOptions = {
 86 |       title: uniqueTitle,
 87 |       description: 'This is a test work item with additional fields',
 88 |       priority: 1,
 89 |       additionalFields: {
 90 |         'System.Tags': 'Integration Test,Automated',
 91 |       },
 92 |     };
 93 | 
 94 |     // Act - make an actual API call to Azure DevOps
 95 |     const result = await createWorkItem(
 96 |       connection,
 97 |       projectName,
 98 |       workItemType,
 99 |       options,
100 |     );
101 | 
102 |     // Assert on the actual response
103 |     expect(result).toBeDefined();
104 |     expect(result.id).toBeDefined();
105 | 
106 |     // Verify fields match what we set
107 |     expect(result.fields).toBeDefined();
108 |     if (result.fields) {
109 |       expect(result.fields['System.Title']).toBe(uniqueTitle);
110 |       expect(result.fields['Microsoft.VSTS.Common.Priority']).toBe(1);
111 |       // Just check that tags contain both values, order may vary
112 |       expect(result.fields['System.Tags']).toContain('Integration Test');
113 |       expect(result.fields['System.Tags']).toContain('Automated');
114 |     }
115 |   });
116 | 
117 |   test('should create a child work item with parent-child relationship', async () => {
118 |     // Skip if no connection is available
119 |     if (shouldSkipIntegrationTest()) {
120 |       return;
121 |     }
122 | 
123 |     // This connection must be available if we didn't skip
124 |     if (!connection) {
125 |       throw new Error(
126 |         'Connection should be available when test is not skipped',
127 |       );
128 |     }
129 | 
130 |     // For a true integration test, use a real project
131 |     const projectName =
132 |       process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
133 | 
134 |     // First, create a parent work item (User Story)
135 |     const parentTitle = `Parent Story ${new Date().toISOString()}`;
136 |     const parentOptions: CreateWorkItemOptions = {
137 |       title: parentTitle,
138 |       description: 'This is a parent user story',
139 |     };
140 | 
141 |     const parentResult = await createWorkItem(
142 |       connection,
143 |       projectName,
144 |       'User Story', // Assuming User Story type exists
145 |       parentOptions,
146 |     );
147 | 
148 |     expect(parentResult).toBeDefined();
149 |     expect(parentResult.id).toBeDefined();
150 |     const parentId = parentResult.id;
151 | 
152 |     // Now create a child work item (Task) with a link to the parent
153 |     const childTitle = `Child Task ${new Date().toISOString()}`;
154 |     const childOptions: CreateWorkItemOptions = {
155 |       title: childTitle,
156 |       description: 'This is a child task of a user story',
157 |       parentId: parentId, // Reference to parent work item
158 |     };
159 | 
160 |     const childResult = await createWorkItem(
161 |       connection,
162 |       projectName,
163 |       'Task',
164 |       childOptions,
165 |     );
166 | 
167 |     // Assert the child work item was created
168 |     expect(childResult).toBeDefined();
169 |     expect(childResult.id).toBeDefined();
170 | 
171 |     // Now verify the parent-child relationship
172 |     // We would need to fetch the relations, but for now we'll just assert
173 |     // that the response indicates a relationship was created
174 |     expect(childResult.relations).toBeDefined();
175 | 
176 |     // Check that at least one relation exists that points to our parent
177 |     const parentRelation = childResult.relations?.find(
178 |       (relation) =>
179 |         relation.rel === 'System.LinkTypes.Hierarchy-Reverse' &&
180 |         relation.url &&
181 |         relation.url.includes(`/${parentId}`),
182 |     );
183 |     expect(parentRelation).toBeDefined();
184 |   });
185 | });
186 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import {
  3 |   AzureDevOpsError,
  4 |   AzureDevOpsResourceNotFoundError,
  5 |   AzureDevOpsValidationError,
  6 |   AzureDevOpsPermissionError,
  7 | } from '../../../shared/errors';
  8 | import { createWiki } from './feature';
  9 | import { WikiType } from './schema';
 10 | import { getWikiClient } from '../../../clients/azure-devops';
 11 | 
 12 | // Mock the WikiClient
 13 | jest.mock('../../../clients/azure-devops');
 14 | 
 15 | describe('createWiki unit', () => {
 16 |   // Mock WikiClient
 17 |   const mockWikiClient = {
 18 |     createWiki: jest.fn(),
 19 |   };
 20 | 
 21 |   // Mock WebApi connection (kept for backward compatibility)
 22 |   const mockConnection = {} as WebApi;
 23 | 
 24 |   beforeEach(() => {
 25 |     // Clear mock calls between tests
 26 |     jest.clearAllMocks();
 27 |     // Setup mock response for getWikiClient
 28 |     (getWikiClient as jest.Mock).mockResolvedValue(mockWikiClient);
 29 |   });
 30 | 
 31 |   test('should create a project wiki', async () => {
 32 |     // Mock data
 33 |     const mockWiki = {
 34 |       id: 'wiki1',
 35 |       name: 'Project Wiki',
 36 |       projectId: 'project1',
 37 |       remoteUrl: 'https://example.com/wiki1',
 38 |       url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1',
 39 |       type: 'projectWiki',
 40 |       repositoryId: 'repo1',
 41 |       mappedPath: '/',
 42 |     };
 43 | 
 44 |     // Setup mock response
 45 |     mockWikiClient.createWiki.mockResolvedValue(mockWiki);
 46 | 
 47 |     // Call the function
 48 |     const result = await createWiki(mockConnection, {
 49 |       name: 'Project Wiki',
 50 |       projectId: 'project1',
 51 |     });
 52 | 
 53 |     // Assertions
 54 |     expect(getWikiClient).toHaveBeenCalledWith({ organizationId: undefined });
 55 |     expect(mockWikiClient.createWiki).toHaveBeenCalledWith('project1', {
 56 |       name: 'Project Wiki',
 57 |       projectId: 'project1',
 58 |       type: WikiType.ProjectWiki,
 59 |     });
 60 |     expect(result).toEqual(mockWiki);
 61 |   });
 62 | 
 63 |   test('should create a code wiki', async () => {
 64 |     // Mock data
 65 |     const mockWiki = {
 66 |       id: 'wiki2',
 67 |       name: 'Code Wiki',
 68 |       projectId: 'project1',
 69 |       repositoryId: 'repo1',
 70 |       mappedPath: '/docs',
 71 |       remoteUrl: 'https://example.com/wiki2',
 72 |       url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki2',
 73 |       type: 'codeWiki',
 74 |     };
 75 | 
 76 |     // Setup mock response
 77 |     mockWikiClient.createWiki.mockResolvedValue(mockWiki);
 78 | 
 79 |     // Call the function
 80 |     const result = await createWiki(mockConnection, {
 81 |       name: 'Code Wiki',
 82 |       projectId: 'project1',
 83 |       type: WikiType.CodeWiki,
 84 |       repositoryId: 'repo1',
 85 |       mappedPath: '/docs',
 86 |     });
 87 | 
 88 |     // Assertions
 89 |     expect(getWikiClient).toHaveBeenCalledWith({ organizationId: undefined });
 90 |     expect(mockWikiClient.createWiki).toHaveBeenCalledWith('project1', {
 91 |       name: 'Code Wiki',
 92 |       projectId: 'project1',
 93 |       type: WikiType.CodeWiki,
 94 |       repositoryId: 'repo1',
 95 |       mappedPath: '/docs',
 96 |       version: {
 97 |         version: 'main',
 98 |         versionType: 'branch' as const,
 99 |       },
100 |     });
101 |     expect(result).toEqual(mockWiki);
102 |   });
103 | 
104 |   test('should throw validation error when repository ID is missing for code wiki', async () => {
105 |     // Call the function and expect it to throw
106 |     await expect(
107 |       createWiki(mockConnection, {
108 |         name: 'Code Wiki',
109 |         projectId: 'project1',
110 |         type: WikiType.CodeWiki,
111 |         // repositoryId is missing
112 |       }),
113 |     ).rejects.toThrow(AzureDevOpsValidationError);
114 | 
115 |     // Assertions
116 |     expect(getWikiClient).not.toHaveBeenCalled();
117 |     expect(mockWikiClient.createWiki).not.toHaveBeenCalled();
118 |   });
119 | 
120 |   test('should handle project not found error', async () => {
121 |     // Setup mock to throw an error
122 |     mockWikiClient.createWiki.mockRejectedValue(
123 |       new AzureDevOpsResourceNotFoundError('Project not found'),
124 |     );
125 | 
126 |     // Call the function and expect it to throw
127 |     await expect(
128 |       createWiki(mockConnection, {
129 |         name: 'Project Wiki',
130 |         projectId: 'nonExistentProject',
131 |       }),
132 |     ).rejects.toThrow(AzureDevOpsResourceNotFoundError);
133 | 
134 |     // Assertions
135 |     expect(getWikiClient).toHaveBeenCalledWith({ organizationId: undefined });
136 |     expect(mockWikiClient.createWiki).toHaveBeenCalled();
137 |   });
138 | 
139 |   test('should handle repository not found error', async () => {
140 |     // Setup mock to throw an error
141 |     mockWikiClient.createWiki.mockRejectedValue(
142 |       new AzureDevOpsResourceNotFoundError('Repository not found'),
143 |     );
144 | 
145 |     // Call the function and expect it to throw
146 |     await expect(
147 |       createWiki(mockConnection, {
148 |         name: 'Code Wiki',
149 |         projectId: 'project1',
150 |         type: WikiType.CodeWiki,
151 |         repositoryId: 'nonExistentRepo',
152 |       }),
153 |     ).rejects.toThrow(AzureDevOpsResourceNotFoundError);
154 | 
155 |     // Assertions
156 |     expect(getWikiClient).toHaveBeenCalledWith({ organizationId: undefined });
157 |     expect(mockWikiClient.createWiki).toHaveBeenCalled();
158 |   });
159 | 
160 |   test('should handle permission error', async () => {
161 |     // Setup mock to throw an error
162 |     mockWikiClient.createWiki.mockRejectedValue(
163 |       new AzureDevOpsPermissionError('You do not have permission'),
164 |     );
165 | 
166 |     // Call the function and expect it to throw
167 |     await expect(
168 |       createWiki(mockConnection, {
169 |         name: 'Project Wiki',
170 |         projectId: 'project1',
171 |       }),
172 |     ).rejects.toThrow(AzureDevOpsPermissionError);
173 | 
174 |     // Assertions
175 |     expect(getWikiClient).toHaveBeenCalledWith({ organizationId: undefined });
176 |     expect(mockWikiClient.createWiki).toHaveBeenCalled();
177 |   });
178 | 
179 |   test('should handle generic errors', async () => {
180 |     // Setup mock to throw an error
181 |     mockWikiClient.createWiki.mockRejectedValue(new Error('Unknown error'));
182 | 
183 |     // Call the function and expect it to throw
184 |     await expect(
185 |       createWiki(mockConnection, {
186 |         name: 'Project Wiki',
187 |         projectId: 'project1',
188 |       }),
189 |     ).rejects.toThrow(AzureDevOpsError);
190 | 
191 |     // Assertions
192 |     expect(getWikiClient).toHaveBeenCalledWith({ organizationId: undefined });
193 |     expect(mockWikiClient.createWiki).toHaveBeenCalled();
194 |   });
195 | });
196 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import axios from 'axios';
  3 | import { DefaultAzureCredential, AzureCliCredential } from '@azure/identity';
  4 | import {
  5 |   AzureDevOpsError,
  6 |   AzureDevOpsResourceNotFoundError,
  7 |   AzureDevOpsValidationError,
  8 |   AzureDevOpsPermissionError,
  9 | } from '../../../shared/errors';
 10 | import {
 11 |   SearchWikiOptions,
 12 |   WikiSearchRequest,
 13 |   WikiSearchResponse,
 14 | } from '../types';
 15 | 
 16 | /**
 17 |  * Search for wiki pages in Azure DevOps projects
 18 |  *
 19 |  * @param connection The Azure DevOps WebApi connection
 20 |  * @param options Parameters for searching wiki pages
 21 |  * @returns Search results for wiki pages
 22 |  */
 23 | export async function searchWiki(
 24 |   connection: WebApi,
 25 |   options: SearchWikiOptions,
 26 | ): Promise<WikiSearchResponse> {
 27 |   try {
 28 |     // Prepare the search request
 29 |     const searchRequest: WikiSearchRequest = {
 30 |       searchText: options.searchText,
 31 |       $skip: options.skip,
 32 |       $top: options.top,
 33 |       filters: options.projectId
 34 |         ? {
 35 |             Project: [options.projectId],
 36 |           }
 37 |         : {},
 38 |       includeFacets: options.includeFacets,
 39 |     };
 40 | 
 41 |     // Add custom filters if provided
 42 |     if (
 43 |       options.filters &&
 44 |       options.filters.Project &&
 45 |       options.filters.Project.length > 0
 46 |     ) {
 47 |       if (!searchRequest.filters) {
 48 |         searchRequest.filters = {};
 49 |       }
 50 | 
 51 |       if (!searchRequest.filters.Project) {
 52 |         searchRequest.filters.Project = [];
 53 |       }
 54 | 
 55 |       searchRequest.filters.Project = [
 56 |         ...(searchRequest.filters.Project || []),
 57 |         ...options.filters.Project,
 58 |       ];
 59 |     }
 60 | 
 61 |     // Get the authorization header from the connection
 62 |     const authHeader = await getAuthorizationHeader();
 63 | 
 64 |     // Extract organization and project from the connection URL
 65 |     const { organization, project } = extractOrgAndProject(
 66 |       connection,
 67 |       options.projectId,
 68 |     );
 69 | 
 70 |     // Make the search API request
 71 |     // If projectId is provided, include it in the URL, otherwise perform organization-wide search
 72 |     const searchUrl = options.projectId
 73 |       ? `https://almsearch.dev.azure.com/${organization}/${project}/_apis/search/wikisearchresults?api-version=7.1`
 74 |       : `https://almsearch.dev.azure.com/${organization}/_apis/search/wikisearchresults?api-version=7.1`;
 75 | 
 76 |     const searchResponse = await axios.post<WikiSearchResponse>(
 77 |       searchUrl,
 78 |       searchRequest,
 79 |       {
 80 |         headers: {
 81 |           Authorization: authHeader,
 82 |           'Content-Type': 'application/json',
 83 |         },
 84 |       },
 85 |     );
 86 | 
 87 |     return searchResponse.data;
 88 |   } catch (error) {
 89 |     // If it's already an AzureDevOpsError, rethrow it
 90 |     if (error instanceof AzureDevOpsError) {
 91 |       throw error;
 92 |     }
 93 | 
 94 |     // Handle axios errors
 95 |     if (axios.isAxiosError(error)) {
 96 |       const status = error.response?.status;
 97 |       const message = error.response?.data?.message || error.message;
 98 | 
 99 |       if (status === 404) {
100 |         throw new AzureDevOpsResourceNotFoundError(
101 |           `Resource not found: ${message}`,
102 |         );
103 |       } else if (status === 400) {
104 |         throw new AzureDevOpsValidationError(
105 |           `Invalid request: ${message}`,
106 |           error.response?.data,
107 |         );
108 |       } else if (status === 401 || status === 403) {
109 |         throw new AzureDevOpsPermissionError(`Permission denied: ${message}`);
110 |       } else {
111 |         // For other axios errors, wrap in a generic AzureDevOpsError
112 |         throw new AzureDevOpsError(`Azure DevOps API error: ${message}`);
113 |       }
114 | 
115 |       // This return is never reached but helps TypeScript understand the control flow
116 |       return null as never;
117 |     }
118 | 
119 |     // Otherwise, wrap it in a generic error
120 |     throw new AzureDevOpsError(
121 |       `Failed to search wiki: ${error instanceof Error ? error.message : String(error)}`,
122 |     );
123 |   }
124 | }
125 | 
126 | /**
127 |  * Extract organization and project from the connection URL
128 |  *
129 |  * @param connection The Azure DevOps WebApi connection
130 |  * @param projectId The project ID or name (optional)
131 |  * @returns The organization and project
132 |  */
133 | function extractOrgAndProject(
134 |   connection: WebApi,
135 |   projectId?: string,
136 | ): { organization: string; project: string } {
137 |   // Extract organization from the connection URL
138 |   const url = connection.serverUrl;
139 |   const match = url.match(/https?:\/\/dev\.azure\.com\/([^/]+)/);
140 |   const organization = match ? match[1] : '';
141 | 
142 |   if (!organization) {
143 |     throw new AzureDevOpsValidationError(
144 |       'Could not extract organization from connection URL',
145 |     );
146 |   }
147 | 
148 |   return {
149 |     organization,
150 |     project: projectId || '',
151 |   };
152 | }
153 | 
154 | /**
155 |  * Get the authorization header from the connection
156 |  *
157 |  * @returns The authorization header
158 |  */
159 | async function getAuthorizationHeader(): Promise<string> {
160 |   try {
161 |     // For PAT authentication, we can construct the header directly
162 |     if (
163 |       process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'pat' &&
164 |       process.env.AZURE_DEVOPS_PAT
165 |     ) {
166 |       // For PAT auth, we can construct the Basic auth header directly
167 |       const token = process.env.AZURE_DEVOPS_PAT;
168 |       const base64Token = Buffer.from(`:${token}`).toString('base64');
169 |       return `Basic ${base64Token}`;
170 |     }
171 | 
172 |     // For Azure Identity / Azure CLI auth, we need to get a token
173 |     // using the Azure DevOps resource ID
174 |     // Choose the appropriate credential based on auth method
175 |     const credential =
176 |       process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'azure-cli'
177 |         ? new AzureCliCredential()
178 |         : new DefaultAzureCredential();
179 | 
180 |     // Azure DevOps resource ID for token acquisition
181 |     const AZURE_DEVOPS_RESOURCE_ID = '499b84ac-1321-427f-aa17-267ca6975798';
182 | 
183 |     // Get token for Azure DevOps
184 |     const token = await credential.getToken(
185 |       `${AZURE_DEVOPS_RESOURCE_ID}/.default`,
186 |     );
187 | 
188 |     if (!token || !token.token) {
189 |       throw new Error('Failed to acquire token for Azure DevOps');
190 |     }
191 | 
192 |     return `Bearer ${token.token}`;
193 |   } catch (error) {
194 |     throw new AzureDevOpsValidationError(
195 |       `Failed to get authorization header: ${error instanceof Error ? error.message : String(error)}`,
196 |     );
197 |   }
198 | }
199 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { IGitApi } from 'azure-devops-node-api/GitApi';
  3 | import { GitVersionType } from 'azure-devops-node-api/interfaces/GitInterfaces';
  4 | import { AzureDevOpsResourceNotFoundError } from '../../../shared/errors';
  5 | import { getFileContent } from './feature';
  6 | import { Readable } from 'stream';
  7 | 
  8 | describe('getFileContent', () => {
  9 |   let mockConnection: WebApi;
 10 |   let mockGitApi: IGitApi;
 11 |   const mockRepositoryId = 'test-repo';
 12 |   const mockProjectId = 'test-project';
 13 |   const mockFilePath = '/path/to/file.txt';
 14 |   const mockFileContent = 'Test file content';
 15 |   const mockItem = {
 16 |     objectId: '123456',
 17 |     path: mockFilePath,
 18 |     url: 'https://dev.azure.com/org/project/_apis/git/repositories/repo/items/path/to/file.txt',
 19 |     gitObjectType: 'blob',
 20 |   };
 21 | 
 22 |   // Helper function to create a readable stream from a string
 23 |   function createReadableStream(content: string): Readable {
 24 |     const stream = new Readable();
 25 |     stream.push(content);
 26 |     stream.push(null); // Signals the end of the stream
 27 |     return stream;
 28 |   }
 29 | 
 30 |   beforeEach(() => {
 31 |     mockGitApi = {
 32 |       getItemContent: jest
 33 |         .fn()
 34 |         .mockResolvedValue(createReadableStream(mockFileContent)),
 35 |       getItems: jest.fn().mockResolvedValue([mockItem]),
 36 |     } as unknown as IGitApi;
 37 | 
 38 |     mockConnection = {
 39 |       getGitApi: jest.fn().mockResolvedValue(mockGitApi),
 40 |     } as unknown as WebApi;
 41 |   });
 42 | 
 43 |   it('should get file content for a file in the default branch', async () => {
 44 |     const result = await getFileContent(
 45 |       mockConnection,
 46 |       mockProjectId,
 47 |       mockRepositoryId,
 48 |       mockFilePath,
 49 |     );
 50 | 
 51 |     expect(mockConnection.getGitApi).toHaveBeenCalled();
 52 |     expect(mockGitApi.getItems).toHaveBeenCalledWith(
 53 |       mockRepositoryId,
 54 |       mockProjectId,
 55 |       mockFilePath,
 56 |       expect.any(Number), // VersionControlRecursionType.OneLevel
 57 |       undefined,
 58 |       undefined,
 59 |       undefined,
 60 |       undefined,
 61 |       undefined,
 62 |     );
 63 | 
 64 |     expect(mockGitApi.getItemContent).toHaveBeenCalledWith(
 65 |       mockRepositoryId,
 66 |       mockFilePath,
 67 |       mockProjectId,
 68 |       undefined,
 69 |       undefined,
 70 |       undefined,
 71 |       undefined,
 72 |       false,
 73 |       undefined,
 74 |       true,
 75 |     );
 76 | 
 77 |     expect(result).toEqual({
 78 |       content: mockFileContent,
 79 |       isDirectory: false,
 80 |     });
 81 |   });
 82 | 
 83 |   it('should get file content for a file in a specific branch', async () => {
 84 |     const branchName = 'test-branch';
 85 |     const versionDescriptor = {
 86 |       versionType: GitVersionType.Branch,
 87 |       version: branchName,
 88 |       versionOptions: undefined,
 89 |     };
 90 | 
 91 |     const result = await getFileContent(
 92 |       mockConnection,
 93 |       mockProjectId,
 94 |       mockRepositoryId,
 95 |       mockFilePath,
 96 |       {
 97 |         versionType: GitVersionType.Branch,
 98 |         version: branchName,
 99 |       },
100 |     );
101 | 
102 |     expect(mockConnection.getGitApi).toHaveBeenCalled();
103 |     expect(mockGitApi.getItems).toHaveBeenCalledWith(
104 |       mockRepositoryId,
105 |       mockProjectId,
106 |       mockFilePath,
107 |       expect.any(Number), // VersionControlRecursionType.OneLevel
108 |       undefined,
109 |       undefined,
110 |       undefined,
111 |       undefined,
112 |       versionDescriptor,
113 |     );
114 | 
115 |     expect(mockGitApi.getItemContent).toHaveBeenCalledWith(
116 |       mockRepositoryId,
117 |       mockFilePath,
118 |       mockProjectId,
119 |       undefined,
120 |       undefined,
121 |       undefined,
122 |       undefined,
123 |       false,
124 |       versionDescriptor,
125 |       true,
126 |     );
127 | 
128 |     expect(result).toEqual({
129 |       content: mockFileContent,
130 |       isDirectory: false,
131 |     });
132 |   });
133 | 
134 |   it('should throw an error if the file is not found', async () => {
135 |     // Mock getItems to throw an error
136 |     mockGitApi.getItems = jest
137 |       .fn()
138 |       .mockRejectedValue(new Error('Item not found'));
139 | 
140 |     // Mock getItemContent to throw a specific error indicating not found
141 |     mockGitApi.getItemContent = jest
142 |       .fn()
143 |       .mockRejectedValue(new Error('Item not found'));
144 | 
145 |     await expect(
146 |       getFileContent(
147 |         mockConnection,
148 |         mockProjectId,
149 |         mockRepositoryId,
150 |         '/invalid/path',
151 |       ),
152 |     ).rejects.toThrow(AzureDevOpsResourceNotFoundError);
153 |   });
154 | 
155 |   it('should get directory content if the path is a directory', async () => {
156 |     const dirPath = '/path/to/dir';
157 |     const mockDirectoryItems = [
158 |       {
159 |         path: `${dirPath}/file1.txt`,
160 |         gitObjectType: 'blob',
161 |         isFolder: false,
162 |       },
163 |       {
164 |         path: `${dirPath}/file2.md`,
165 |         gitObjectType: 'blob',
166 |         isFolder: false,
167 |       },
168 |       {
169 |         path: `${dirPath}/subdir`,
170 |         gitObjectType: 'tree',
171 |         isFolder: true,
172 |       },
173 |     ];
174 | 
175 |     // Mock getItems to return multiple items, indicating a directory
176 |     mockGitApi.getItems = jest.fn().mockResolvedValue(mockDirectoryItems);
177 | 
178 |     const result = await getFileContent(
179 |       mockConnection,
180 |       mockProjectId,
181 |       mockRepositoryId,
182 |       dirPath,
183 |     );
184 | 
185 |     expect(mockConnection.getGitApi).toHaveBeenCalled();
186 |     expect(mockGitApi.getItems).toHaveBeenCalledWith(
187 |       mockRepositoryId,
188 |       mockProjectId,
189 |       dirPath,
190 |       expect.any(Number), // VersionControlRecursionType.OneLevel
191 |       undefined,
192 |       undefined,
193 |       undefined,
194 |       undefined,
195 |       undefined,
196 |     );
197 | 
198 |     // Should not attempt to get file content for a directory
199 |     expect(mockGitApi.getItemContent).not.toHaveBeenCalled();
200 | 
201 |     expect(result).toEqual({
202 |       content: JSON.stringify(mockDirectoryItems, null, 2),
203 |       isDirectory: true,
204 |     });
205 |   });
206 | 
207 |   it('should handle a directory path with trailing slash', async () => {
208 |     const dirPath = '/path/to/dir/';
209 |     const mockDirectoryItems = [
210 |       {
211 |         path: `${dirPath}file1.txt`,
212 |         gitObjectType: 'blob',
213 |         isFolder: false,
214 |       },
215 |     ];
216 | 
217 |     // Even with one item, it should be treated as a directory due to trailing slash
218 |     mockGitApi.getItems = jest.fn().mockResolvedValue(mockDirectoryItems);
219 | 
220 |     const result = await getFileContent(
221 |       mockConnection,
222 |       mockProjectId,
223 |       mockRepositoryId,
224 |       dirPath,
225 |     );
226 | 
227 |     expect(result.isDirectory).toBe(true);
228 |     expect(result.content).toBe(JSON.stringify(mockDirectoryItems, null, 2));
229 |   });
230 | });
231 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { searchWorkItems } from './feature';
  3 | import { getConnection } from '../../../server';
  4 | import { AzureDevOpsConfig } from '../../../shared/types';
  5 | import { AuthenticationMethod } from '../../../shared/auth';
  6 | 
  7 | // Skip tests if no PAT is available
  8 | const hasPat = process.env.AZURE_DEVOPS_PAT && process.env.AZURE_DEVOPS_ORG_URL;
  9 | const describeOrSkip = hasPat ? describe : describe.skip;
 10 | 
 11 | describeOrSkip('searchWorkItems (Integration)', () => {
 12 |   let connection: WebApi;
 13 |   let config: AzureDevOpsConfig;
 14 |   let projectId: string;
 15 | 
 16 |   beforeAll(async () => {
 17 |     // Set up the connection
 18 |     config = {
 19 |       organizationUrl: process.env.AZURE_DEVOPS_ORG_URL || '',
 20 |       authMethod: AuthenticationMethod.PersonalAccessToken,
 21 |       personalAccessToken: process.env.AZURE_DEVOPS_PAT || '',
 22 |       defaultProject: process.env.AZURE_DEVOPS_DEFAULT_PROJECT || '',
 23 |     };
 24 | 
 25 |     connection = await getConnection(config);
 26 |     projectId = config.defaultProject || '';
 27 | 
 28 |     // Skip tests if no default project is set
 29 |     if (!projectId) {
 30 |       console.warn('Skipping integration tests: No default project set');
 31 |     }
 32 |   }, 30000);
 33 | 
 34 |   it('should search for work items', async () => {
 35 |     // Skip test if no default project
 36 |     if (!projectId) {
 37 |       return;
 38 |     }
 39 | 
 40 |     // Act
 41 |     const result = await searchWorkItems(connection, {
 42 |       searchText: 'test',
 43 |       projectId,
 44 |       top: 10,
 45 |       includeFacets: true,
 46 |     });
 47 | 
 48 |     // Assert
 49 |     expect(result).toBeDefined();
 50 |     expect(typeof result.count).toBe('number');
 51 |     expect(Array.isArray(result.results)).toBe(true);
 52 | 
 53 |     // If there are results, verify their structure
 54 |     if (result.results.length > 0) {
 55 |       const firstResult = result.results[0];
 56 |       expect(firstResult.project).toBeDefined();
 57 |       expect(firstResult.fields).toBeDefined();
 58 |       expect(firstResult.fields['system.id']).toBeDefined();
 59 |       expect(firstResult.fields['system.title']).toBeDefined();
 60 |       expect(firstResult.hits).toBeDefined();
 61 |       expect(firstResult.url).toBeDefined();
 62 |     }
 63 | 
 64 |     // If facets were requested, verify their structure
 65 |     if (result.facets) {
 66 |       expect(result.facets).toBeDefined();
 67 |     }
 68 |   }, 30000);
 69 | 
 70 |   it('should filter work items by type', async () => {
 71 |     // Skip test if no default project
 72 |     if (!projectId) {
 73 |       return;
 74 |     }
 75 | 
 76 |     // Act
 77 |     const result = await searchWorkItems(connection, {
 78 |       searchText: 'test',
 79 |       projectId,
 80 |       filters: {
 81 |         'System.WorkItemType': ['Bug'],
 82 |       },
 83 |       top: 10,
 84 |     });
 85 | 
 86 |     // Assert
 87 |     expect(result).toBeDefined();
 88 | 
 89 |     // If there are results, verify they are all bugs
 90 |     if (result.results.length > 0) {
 91 |       result.results.forEach((item) => {
 92 |         expect(item.fields['system.workitemtype'].toLowerCase()).toBe('bug');
 93 |       });
 94 |     }
 95 |   }, 30000);
 96 | 
 97 |   it('should support pagination', async () => {
 98 |     // Skip test if no default project
 99 |     if (!projectId) {
100 |       return;
101 |     }
102 | 
103 |     // Act - Get first page
104 |     const firstPage = await searchWorkItems(connection, {
105 |       searchText: 'test',
106 |       projectId,
107 |       top: 5,
108 |       skip: 0,
109 |     });
110 | 
111 |     // If there are enough results, test pagination
112 |     if (firstPage.count > 5) {
113 |       // Act - Get second page
114 |       const secondPage = await searchWorkItems(connection, {
115 |         searchText: 'test',
116 |         projectId,
117 |         top: 5,
118 |         skip: 5,
119 |       });
120 | 
121 |       // Assert
122 |       expect(secondPage).toBeDefined();
123 |       expect(secondPage.results).toBeDefined();
124 | 
125 |       // Verify the pages have different items
126 |       if (firstPage.results.length > 0 && secondPage.results.length > 0) {
127 |         const firstPageIds = firstPage.results.map(
128 |           (r) => r.fields['system.id'],
129 |         );
130 |         const secondPageIds = secondPage.results.map(
131 |           (r) => r.fields['system.id'],
132 |         );
133 | 
134 |         // Check that the pages don't have overlapping IDs
135 |         const overlap = firstPageIds.filter((id) => secondPageIds.includes(id));
136 |         expect(overlap.length).toBe(0);
137 |       }
138 |     }
139 |   }, 30000);
140 | 
141 |   it('should support sorting', async () => {
142 |     // Skip test if no default project
143 |     if (!projectId) {
144 |       return;
145 |     }
146 | 
147 |     // Act - Get results sorted by creation date (newest first)
148 |     const result = await searchWorkItems(connection, {
149 |       searchText: 'test',
150 |       projectId,
151 |       orderBy: [{ field: 'System.CreatedDate', sortOrder: 'DESC' }],
152 |       top: 10,
153 |     });
154 | 
155 |     // Assert
156 |     expect(result).toBeDefined();
157 | 
158 |     // If there are multiple results, verify they are sorted
159 |     if (result.results.length > 1) {
160 |       const dates = result.results
161 |         .filter((r) => r.fields['system.createddate'] !== undefined)
162 |         .map((r) =>
163 |           new Date(r.fields['system.createddate'] as string).getTime(),
164 |         );
165 | 
166 |       // Check that dates are in descending order
167 |       for (let i = 0; i < dates.length - 1; i++) {
168 |         expect(dates[i]).toBeGreaterThanOrEqual(dates[i + 1]);
169 |       }
170 |     }
171 |   }, 30000);
172 | 
173 |   // Add a test to verify Azure Identity authentication if configured
174 |   if (
175 |     process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'azure-identity'
176 |   ) {
177 |     test('should search work items using Azure Identity authentication', async () => {
178 |       // Skip if required environment variables are missing
179 |       if (!process.env.AZURE_DEVOPS_ORG_URL || !process.env.TEST_PROJECT_ID) {
180 |         console.log('Skipping test: required environment variables missing');
181 |         return;
182 |       }
183 | 
184 |       // Create a config with Azure Identity authentication
185 |       const testConfig: AzureDevOpsConfig = {
186 |         organizationUrl: process.env.AZURE_DEVOPS_ORG_URL,
187 |         authMethod: AuthenticationMethod.AzureIdentity,
188 |         defaultProject: process.env.TEST_PROJECT_ID,
189 |       };
190 | 
191 |       // Create the connection using the config
192 |       const connection = await getConnection(testConfig);
193 | 
194 |       // Search work items
195 |       const result = await searchWorkItems(connection, {
196 |         projectId: process.env.TEST_PROJECT_ID,
197 |         searchText: 'test',
198 |       });
199 | 
200 |       // Check that the response is properly formatted
201 |       expect(result).toBeDefined();
202 |       expect(result.count).toBeDefined();
203 |       expect(Array.isArray(result.results)).toBe(true);
204 |     });
205 |   }
206 | });
207 | 
```

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

```markdown
  1 | # Pipeline Tools
  2 | 
  3 | This document describes the tools available for working with Azure DevOps pipelines.
  4 | 
  5 | ## Table of Contents
  6 | 
  7 | - [`list_pipelines`](#list_pipelines) - List pipelines in a project
  8 | - [`get_pipeline`](#get_pipeline) - Get details of a specific pipeline
  9 | - [`trigger_pipeline`](#trigger_pipeline) - Trigger a pipeline run
 10 | 
 11 | ## list_pipelines
 12 | 
 13 | Lists pipelines in a project.
 14 | 
 15 | ### Parameters
 16 | 
 17 | | Parameter   | Type   | Required | Description                                               |
 18 | | ----------- | ------ | -------- | --------------------------------------------------------- |
 19 | | `projectId` | string | No       | The ID or name of the project (Default: from environment) |
 20 | | `orderBy`   | string | No       | Order by field and direction (e.g., "createdDate desc")   |
 21 | | `top`       | number | No       | Maximum number of pipelines to return                     |
 22 | 
 23 | ### Response
 24 | 
 25 | Returns an array of pipeline objects:
 26 | 
 27 | ```json
 28 | {
 29 |   "count": 2,
 30 |   "value": [
 31 |     {
 32 |       "id": 4,
 33 |       "revision": 2,
 34 |       "name": "Node.js build pipeline",
 35 |       "folder": "\\",
 36 |       "url": "https://dev.azure.com/organization/project/_apis/pipelines/4"
 37 |     },
 38 |     {
 39 |       "id": 1,
 40 |       "revision": 1,
 41 |       "name": "Sample Pipeline",
 42 |       "folder": "\\",
 43 |       "url": "https://dev.azure.com/organization/project/_apis/pipelines/1"
 44 |     }
 45 |   ]
 46 | }
 47 | ```
 48 | 
 49 | ### Error Handling
 50 | 
 51 | - Returns `AzureDevOpsResourceNotFoundError` if the project does not exist
 52 | - Returns `AzureDevOpsAuthenticationError` if authentication fails
 53 | - Returns generic error messages for other failures
 54 | 
 55 | ### Example Usage
 56 | 
 57 | ```javascript
 58 | // Using default project from environment
 59 | const result = await callTool('list_pipelines', {});
 60 | 
 61 | // Specifying project and limiting results
 62 | const limitedResult = await callTool('list_pipelines', {
 63 |   projectId: 'my-project',
 64 |   top: 10,
 65 |   orderBy: 'name asc',
 66 | });
 67 | ```
 68 | 
 69 | ## get_pipeline
 70 | 
 71 | Gets details of a specific pipeline.
 72 | 
 73 | ### Parameters
 74 | 
 75 | | Parameter         | Type   | Required | Description                                                       |
 76 | | ----------------- | ------ | -------- | ----------------------------------------------------------------- |
 77 | | `projectId`       | string | No       | The ID or name of the project (Default: from environment)         |
 78 | | `pipelineId`      | number | Yes      | The numeric ID of the pipeline to retrieve                        |
 79 | | `pipelineVersion` | number | No       | The version of the pipeline to retrieve (latest if not specified) |
 80 | 
 81 | ### Response
 82 | 
 83 | Returns a pipeline object with the following structure:
 84 | 
 85 | ```json
 86 | {
 87 |   "id": 4,
 88 |   "revision": 2,
 89 |   "name": "Node.js build pipeline",
 90 |   "folder": "\\",
 91 |   "url": "https://dev.azure.com/organization/project/_apis/pipelines/4",
 92 |   "_links": {
 93 |     "self": {
 94 |       "href": "https://dev.azure.com/organization/project/_apis/pipelines/4"
 95 |     },
 96 |     "web": {
 97 |       "href": "https://dev.azure.com/organization/project/_build/definition?definitionId=4"
 98 |     }
 99 |   },
100 |   "configuration": {
101 |     "path": "azure-pipelines.yml",
102 |     "repository": {
103 |       "id": "bd0e8130-7fba-4f3b-8559-54760b6e7248",
104 |       "type": "azureReposGit"
105 |     },
106 |     "type": "yaml"
107 |   }
108 | }
109 | ```
110 | 
111 | ### Error Handling
112 | 
113 | - Returns `AzureDevOpsResourceNotFoundError` if the pipeline or project does not exist
114 | - Returns `AzureDevOpsAuthenticationError` if authentication fails
115 | - Returns generic error messages for other failures
116 | 
117 | ### Example Usage
118 | 
119 | ```javascript
120 | // Get latest version of a pipeline
121 | const result = await callTool('get_pipeline', {
122 |   pipelineId: 4,
123 | });
124 | 
125 | // Get specific version of a pipeline
126 | const versionResult = await callTool('get_pipeline', {
127 |   projectId: 'my-project',
128 |   pipelineId: 4,
129 |   pipelineVersion: 2,
130 | });
131 | ```
132 | 
133 | ## trigger_pipeline
134 | 
135 | Triggers a run of a specific pipeline. Allows specifying the branch to run on and passing variables to customize the pipeline execution.
136 | 
137 | ### Parameters
138 | 
139 | | Parameter            | Type   | Required | Description                                                           |
140 | | -------------------- | ------ | -------- | --------------------------------------------------------------------- |
141 | | `projectId`          | string | No       | The ID or name of the project (Default: from environment)             |
142 | | `pipelineId`         | number | Yes      | The numeric ID of the pipeline to trigger                             |
143 | | `branch`             | string | No       | The branch to run the pipeline on (e.g., "main", "feature/my-branch") |
144 | | `variables`          | object | No       | Variables to pass to the pipeline run                                 |
145 | | `templateParameters` | object | No       | Parameters for template-based pipelines                               |
146 | | `stagesToSkip`       | array  | No       | Stages to skip in the pipeline run                                    |
147 | 
148 | #### Variables Format
149 | 
150 | ```json
151 | {
152 |   "myVariable": {
153 |     "value": "my-value",
154 |     "isSecret": false
155 |   },
156 |   "secretVariable": {
157 |     "value": "secret-value",
158 |     "isSecret": true
159 |   }
160 | }
161 | ```
162 | 
163 | ### Response
164 | 
165 | Returns a run object with details about the triggered pipeline run:
166 | 
167 | ```json
168 | {
169 |   "id": 12345,
170 |   "name": "20230215.1",
171 |   "createdDate": "2023-02-15T10:30:00Z",
172 |   "url": "https://dev.azure.com/organization/project/_apis/pipelines/runs/12345",
173 |   "_links": {
174 |     "self": {
175 |       "href": "https://dev.azure.com/organization/project/_apis/pipelines/runs/12345"
176 |     },
177 |     "web": {
178 |       "href": "https://dev.azure.com/organization/project/_build/results?buildId=12345"
179 |     }
180 |   },
181 |   "state": 1,
182 |   "result": null,
183 |   "variables": {
184 |     "myVariable": {
185 |       "value": "my-value"
186 |     }
187 |   }
188 | }
189 | ```
190 | 
191 | ### Error Handling
192 | 
193 | - Returns `AzureDevOpsResourceNotFoundError` if the pipeline or project does not exist
194 | - Returns `AzureDevOpsAuthenticationError` if authentication fails
195 | - Returns generic error messages for other failures
196 | 
197 | ### Example Usage
198 | 
199 | ```javascript
200 | // Trigger a pipeline on the default branch
201 | // In this case, use default project from environment variables
202 | const result = await callTool('trigger_pipeline', {
203 |   pipelineId: 4,
204 | });
205 | 
206 | // Trigger a pipeline on a specific branch with variables
207 | const runWithOptions = await callTool('trigger_pipeline', {
208 |   projectId: 'my-project',
209 |   pipelineId: 4,
210 |   branch: 'feature/my-branch',
211 |   variables: {
212 |     deployEnvironment: {
213 |       value: 'staging',
214 |       isSecret: false,
215 |     },
216 |   },
217 | });
218 | ```
219 | 
```

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

```markdown
  1 | # Work Item Tools
  2 | 
  3 | This document describes the tools available for working with Azure DevOps work items.
  4 | 
  5 | ## Table of Contents
  6 | 
  7 | - [`get_work_item`](#get_work_item) - Retrieve a specific work item by ID
  8 | - [`create_work_item`](#create_work_item) - Create a new work item
  9 | - [`list_work_items`](#list_work_items) - List work items in a project
 10 | 
 11 | ## get_work_item
 12 | 
 13 | Retrieves a work item by its ID.
 14 | 
 15 | ### Parameters
 16 | 
 17 | | Parameter    | Type   | Required | Description                                                                       |
 18 | | ------------ | ------ | -------- | --------------------------------------------------------------------------------- |
 19 | | `workItemId` | number | Yes      | The ID of the work item to retrieve                                               |
 20 | | `expand`     | string | No       | Controls the level of detail in the response. Defaults to "All" if not specified. Other values: "Relations", "Fields", "None" |
 21 | 
 22 | ### Response
 23 | 
 24 | Returns a work item object with the following structure:
 25 | 
 26 | ```json
 27 | {
 28 |   "id": 123,
 29 |   "fields": {
 30 |     "System.Title": "Sample Work Item",
 31 |     "System.State": "Active",
 32 |     "System.AssignedTo": "[email protected]",
 33 |     "System.Description": "Description of the work item"
 34 |   },
 35 |   "url": "https://dev.azure.com/organization/project/_apis/wit/workItems/123"
 36 | }
 37 | ```
 38 | 
 39 | ### Error Handling
 40 | 
 41 | - Returns `AzureDevOpsResourceNotFoundError` if the work item does not exist
 42 | - Returns `AzureDevOpsAuthenticationError` if authentication fails
 43 | - Returns generic error messages for other failures
 44 | 
 45 | ### Example Usage
 46 | 
 47 | ```javascript
 48 | // Using default expand="All"
 49 | const result = await callTool('get_work_item', {
 50 |   workItemId: 123,
 51 | });
 52 | 
 53 | // Explicitly specifying expand
 54 | const minimalResult = await callTool('get_work_item', {
 55 |   workItemId: 123,
 56 |   expand: 'None'
 57 | });
 58 | ```
 59 | 
 60 | ## create_work_item
 61 | 
 62 | Creates a new work item in a specified project.
 63 | 
 64 | ### Parameters
 65 | 
 66 | | Parameter          | Type   | Required | Description                                                         |
 67 | | ------------------ | ------ | -------- | ------------------------------------------------------------------- |
 68 | | `projectId`        | string | Yes      | The ID or name of the project where the work item will be created   |
 69 | | `workItemType`     | string | Yes      | The type of work item to create (e.g., "Task", "Bug", "User Story") |
 70 | | `title`            | string | Yes      | The title of the work item                                          |
 71 | | `description`      | string | No       | The description of the work item                                    |
 72 | | `assignedTo`       | string | No       | The email or name of the user to assign the work item to            |
 73 | | `areaPath`         | string | No       | The area path for the work item                                     |
 74 | | `iterationPath`    | string | No       | The iteration path for the work item                                |
 75 | | `priority`         | number | No       | The priority of the work item                                       |
 76 | | `additionalFields` | object | No       | Additional fields to set on the work item (key-value pairs)         |
 77 | 
 78 | ### Response
 79 | 
 80 | Returns the newly created work item object:
 81 | 
 82 | ```json
 83 | {
 84 |   "id": 124,
 85 |   "fields": {
 86 |     "System.Title": "New Work Item",
 87 |     "System.State": "New",
 88 |     "System.Description": "Description of the new work item",
 89 |     "System.AssignedTo": "[email protected]",
 90 |     "System.AreaPath": "Project\\Team",
 91 |     "System.IterationPath": "Project\\Sprint 1",
 92 |     "Microsoft.VSTS.Common.Priority": 2
 93 |   },
 94 |   "url": "https://dev.azure.com/organization/project/_apis/wit/workItems/124"
 95 | }
 96 | ```
 97 | 
 98 | ### Error Handling
 99 | 
100 | - Returns validation error if required fields are missing
101 | - Returns `AzureDevOpsAuthenticationError` if authentication fails
102 | - Returns `AzureDevOpsResourceNotFoundError` if the project does not exist
103 | - Returns generic error messages for other failures
104 | 
105 | ### Example Usage
106 | 
107 | ```javascript
108 | const result = await callTool('create_work_item', {
109 |   projectId: 'my-project',
110 |   workItemType: 'User Story',
111 |   title: 'Implement login functionality',
112 |   description:
113 |     'Create a secure login system with email and password authentication',
114 |   assignedTo: '[email protected]',
115 |   priority: 1,
116 |   additionalFields: {
117 |     'Custom.Field': 'Custom Value',
118 |   },
119 | });
120 | ```
121 | 
122 | ### Implementation Details
123 | 
124 | 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.
125 | 
126 | ## list_work_items
127 | 
128 | Lists work items in a specified project.
129 | 
130 | ### Parameters
131 | 
132 | | Parameter   | Type   | Required | Description                                           |
133 | | ----------- | ------ | -------- | ----------------------------------------------------- |
134 | | `projectId` | string | Yes      | The ID or name of the project to list work items from |
135 | | `teamId`    | string | No       | The ID of the team to list work items for             |
136 | | `queryId`   | string | No       | ID of a saved work item query                         |
137 | | `wiql`      | string | No       | Work Item Query Language (WIQL) query                 |
138 | | `top`       | number | No       | Maximum number of work items to return                |
139 | | `skip`      | number | No       | Number of work items to skip                          |
140 | 
141 | ### Response
142 | 
143 | Returns an array of work item objects:
144 | 
145 | ```json
146 | [
147 |   {
148 |     "id": 123,
149 |     "fields": {
150 |       "System.Title": "Sample Work Item",
151 |       "System.State": "Active",
152 |       "System.AssignedTo": "[email protected]"
153 |     },
154 |     "url": "https://dev.azure.com/organization/project/_apis/wit/workItems/123"
155 |   },
156 |   {
157 |     "id": 124,
158 |     "fields": {
159 |       "System.Title": "Another Work Item",
160 |       "System.State": "New",
161 |       "System.AssignedTo": "[email protected]"
162 |     },
163 |     "url": "https://dev.azure.com/organization/project/_apis/wit/workItems/124"
164 |   }
165 | ]
166 | ```
167 | 
168 | ### Error Handling
169 | 
170 | - Returns `AzureDevOpsResourceNotFoundError` if the project does not exist
171 | - Returns `AzureDevOpsAuthenticationError` if authentication fails
172 | - Returns generic error messages for other failures
173 | 
174 | ### Example Usage
175 | 
176 | ```javascript
177 | const result = await callTool('list_work_items', {
178 |   projectId: 'my-project',
179 |   wiql: "SELECT [System.Id] FROM WorkItems WHERE [System.WorkItemType] = 'Task' ORDER BY [System.CreatedDate] DESC",
180 |   top: 10,
181 | });
182 | ```
183 | 
```

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

```typescript
  1 | import { createWikiPage } from './feature';
  2 | import { handleRequestError } from '../../../shared/errors/handle-request-error';
  3 | 
  4 | // Mock the AzureDevOpsClient
  5 | jest.mock('../../../shared/api/client');
  6 | // Mock the error handler
  7 | jest.mock('../../../shared/errors/handle-request-error', () => ({
  8 |   handleRequestError: jest.fn(),
  9 | }));
 10 | 
 11 | describe('createWikiPage Feature', () => {
 12 |   let client: any;
 13 |   const mockPut = jest.fn();
 14 |   const mockHandleRequestError = handleRequestError as jest.MockedFunction<
 15 |     typeof handleRequestError
 16 |   >;
 17 | 
 18 |   const defaultParams = {
 19 |     wikiId: 'test-wiki',
 20 |     content: 'Hello world',
 21 |     pagePath: '/',
 22 |   };
 23 | 
 24 |   beforeEach(() => {
 25 |     // Reset mocks for each test
 26 |     mockPut.mockReset();
 27 |     mockHandleRequestError.mockReset();
 28 | 
 29 |     client = {
 30 |       put: mockPut,
 31 |       defaults: {
 32 |         organizationId: 'defaultOrg',
 33 |         projectId: 'defaultProject',
 34 |       },
 35 |     };
 36 |   });
 37 | 
 38 |   it('should call client.put with correct URL and data for default org and project', async () => {
 39 |     mockPut.mockResolvedValue({ data: { some: 'response' } });
 40 |     await createWikiPage(defaultParams, client as any);
 41 | 
 42 |     expect(mockPut).toHaveBeenCalledTimes(1);
 43 |     expect(mockPut).toHaveBeenCalledWith(
 44 |       'defaultOrg/defaultProject/_apis/wiki/wikis/test-wiki/pages?path=%2F&api-version=7.1-preview.1',
 45 |       { content: 'Hello world' },
 46 |     );
 47 |   });
 48 | 
 49 |   it('should call client.put with correct URL when projectId is explicitly provided', async () => {
 50 |     mockPut.mockResolvedValue({ data: { some: 'response' } });
 51 |     const paramsWithProject = {
 52 |       ...defaultParams,
 53 |       projectId: 'customProject',
 54 |     };
 55 |     await createWikiPage(paramsWithProject, client as any);
 56 | 
 57 |     expect(mockPut).toHaveBeenCalledWith(
 58 |       'defaultOrg/customProject/_apis/wiki/wikis/test-wiki/pages?path=%2F&api-version=7.1-preview.1',
 59 |       { content: 'Hello world' },
 60 |     );
 61 |   });
 62 | 
 63 |   it('should call client.put with correct URL when organizationId is explicitly provided', async () => {
 64 |     mockPut.mockResolvedValue({ data: { some: 'response' } });
 65 |     const paramsWithOrg = {
 66 |       ...defaultParams,
 67 |       organizationId: 'customOrg',
 68 |     };
 69 |     await createWikiPage(paramsWithOrg, client as any);
 70 | 
 71 |     expect(mockPut).toHaveBeenCalledWith(
 72 |       'customOrg/defaultProject/_apis/wiki/wikis/test-wiki/pages?path=%2F&api-version=7.1-preview.1',
 73 |       { content: 'Hello world' },
 74 |     );
 75 |   });
 76 | 
 77 |   it('should call client.put with correct URL when projectId is null (project-level wiki)', async () => {
 78 |     mockPut.mockResolvedValue({ data: { some: 'response' } });
 79 |     const paramsWithNullProject = {
 80 |       ...defaultParams,
 81 |       projectId: null, // Explicitly null for project-level resources that don't need a project
 82 |     };
 83 | 
 84 |     // Client default for projectId should also be null or undefined in this scenario
 85 |     const clientWithoutProject = {
 86 |       put: mockPut,
 87 |       defaults: {
 88 |         organizationId: 'defaultOrg',
 89 |         projectId: undefined,
 90 |       },
 91 |     };
 92 | 
 93 |     await createWikiPage(paramsWithNullProject, clientWithoutProject as any);
 94 | 
 95 |     expect(mockPut).toHaveBeenCalledWith(
 96 |       'defaultOrg/_apis/wiki/wikis/test-wiki/pages?path=%2F&api-version=7.1-preview.1',
 97 |       { content: 'Hello world' },
 98 |     );
 99 |   });
100 | 
101 |   it('should correctly encode pagePath in the URL', async () => {
102 |     mockPut.mockResolvedValue({ data: { some: 'response' } });
103 |     const paramsWithPath = {
104 |       ...defaultParams,
105 |       pagePath: '/My Test Page/Sub Page',
106 |     };
107 |     await createWikiPage(paramsWithPath, client as any);
108 | 
109 |     expect(mockPut).toHaveBeenCalledWith(
110 |       'defaultOrg/defaultProject/_apis/wiki/wikis/test-wiki/pages?path=%2FMy%20Test%20Page%2FSub%20Page&api-version=7.1-preview.1',
111 |       { content: 'Hello world' },
112 |     );
113 |   });
114 | 
115 |   it('should use default pagePath "/" if pagePath is null', async () => {
116 |     mockPut.mockResolvedValue({ data: { some: 'response' } });
117 |     const paramsWithPath = {
118 |       ...defaultParams,
119 |       pagePath: null, // Explicitly null
120 |     };
121 |     await createWikiPage(paramsWithPath, client as any);
122 | 
123 |     expect(mockPut).toHaveBeenCalledWith(
124 |       'defaultOrg/defaultProject/_apis/wiki/wikis/test-wiki/pages?path=%2F&api-version=7.1-preview.1',
125 |       { content: 'Hello world' },
126 |     );
127 |   });
128 | 
129 |   it('should include comment in request body when provided', async () => {
130 |     mockPut.mockResolvedValue({ data: { some: 'response' } });
131 |     const paramsWithComment = {
132 |       ...defaultParams,
133 |       comment: 'Initial page creation',
134 |     };
135 |     await createWikiPage(paramsWithComment, client as any);
136 | 
137 |     expect(mockPut).toHaveBeenCalledWith(
138 |       'defaultOrg/defaultProject/_apis/wiki/wikis/test-wiki/pages?path=%2F&api-version=7.1-preview.1',
139 |       { content: 'Hello world', comment: 'Initial page creation' },
140 |     );
141 |   });
142 | 
143 |   it('should return the data from the response on success', async () => {
144 |     const expectedResponse = { id: '123', path: '/', content: 'Hello world' };
145 |     mockPut.mockResolvedValue({ data: expectedResponse });
146 |     const result = await createWikiPage(defaultParams, client as any);
147 | 
148 |     expect(result).toEqual(expectedResponse);
149 |   });
150 | 
151 |   // Skip this test for now as it requires complex mocking of environment variables
152 |   it.skip('should throw if organizationId is not provided and not set in defaults', async () => {
153 |     const clientWithoutOrg = {
154 |       put: mockPut,
155 |       defaults: {
156 |         projectId: 'defaultProject',
157 |         organizationId: undefined,
158 |       },
159 |     };
160 | 
161 |     const paramsNoOrg = {
162 |       ...defaultParams,
163 |       organizationId: null, // Explicitly null and no default
164 |     };
165 | 
166 |     // This test is skipped because it requires complex mocking of environment variables
167 |     // which is difficult to do in the current test setup
168 |     await expect(
169 |       createWikiPage(paramsNoOrg, clientWithoutOrg as any),
170 |     ).rejects.toThrow(
171 |       'Organization ID is not defined. Please provide it or set a default.',
172 |     );
173 |     expect(mockPut).not.toHaveBeenCalled();
174 |   });
175 | 
176 |   it('should call handleRequestError if client.put throws an error', async () => {
177 |     const error = new Error('API Error');
178 |     mockPut.mockRejectedValue(error);
179 |     mockHandleRequestError.mockImplementation(() => {
180 |       throw new Error('Handled Error');
181 |     });
182 | 
183 |     await expect(createWikiPage(defaultParams, client as any)).rejects.toThrow(
184 |       'Handled Error',
185 |     );
186 |     expect(mockHandleRequestError).toHaveBeenCalledTimes(1);
187 |     expect(mockHandleRequestError).toHaveBeenCalledWith(
188 |       error,
189 |       'Failed to create or update wiki page',
190 |     );
191 |   });
192 | });
193 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { getProjectDetails } from './feature';
  3 | import {
  4 |   getTestConnection,
  5 |   shouldSkipIntegrationTest,
  6 | } from '@/shared/test/test-helpers';
  7 | 
  8 | describe('getProjectDetails integration', () => {
  9 |   let connection: WebApi | null = null;
 10 |   let projectName: string;
 11 | 
 12 |   beforeAll(async () => {
 13 |     // Get a real connection using environment variables
 14 |     connection = await getTestConnection();
 15 |     projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
 16 |   });
 17 | 
 18 |   test('should retrieve basic project details from Azure DevOps', async () => {
 19 |     // Skip if no connection is available
 20 |     if (shouldSkipIntegrationTest()) {
 21 |       return;
 22 |     }
 23 | 
 24 |     // This connection must be available if we didn't skip
 25 |     if (!connection) {
 26 |       throw new Error(
 27 |         'Connection should be available when test is not skipped',
 28 |       );
 29 |     }
 30 | 
 31 |     // Act - make an actual API call to Azure DevOps
 32 |     const result = await getProjectDetails(connection, {
 33 |       projectId: projectName,
 34 |     });
 35 | 
 36 |     // Assert on the actual response
 37 |     expect(result).toBeDefined();
 38 |     expect(result.name).toBe(projectName);
 39 |     expect(result.id).toBeDefined();
 40 |     expect(result.url).toBeDefined();
 41 |     expect(result.state).toBeDefined();
 42 | 
 43 |     // Verify basic project structure
 44 |     expect(result.visibility).toBeDefined();
 45 |     expect(result.lastUpdateTime).toBeDefined();
 46 |     expect(result.capabilities).toBeDefined();
 47 |   });
 48 | 
 49 |   test('should retrieve project details with teams from Azure DevOps', async () => {
 50 |     // Skip if no connection is available
 51 |     if (shouldSkipIntegrationTest()) {
 52 |       return;
 53 |     }
 54 | 
 55 |     // This connection must be available if we didn't skip
 56 |     if (!connection) {
 57 |       throw new Error(
 58 |         'Connection should be available when test is not skipped',
 59 |       );
 60 |     }
 61 | 
 62 |     // Act - make an actual API call to Azure DevOps
 63 |     const result = await getProjectDetails(connection, {
 64 |       projectId: projectName,
 65 |       includeTeams: true,
 66 |     });
 67 | 
 68 |     // Assert on the actual response
 69 |     expect(result).toBeDefined();
 70 |     expect(result.teams).toBeDefined();
 71 |     expect(Array.isArray(result.teams)).toBe(true);
 72 | 
 73 |     // There should be at least one team (the default team)
 74 |     if (result.teams && result.teams.length > 0) {
 75 |       const team = result.teams[0];
 76 |       expect(team.id).toBeDefined();
 77 |       expect(team.name).toBeDefined();
 78 |       expect(team.url).toBeDefined();
 79 |     }
 80 |   });
 81 | 
 82 |   test('should retrieve project details with process information from Azure DevOps', async () => {
 83 |     // Skip if no connection is available
 84 |     if (shouldSkipIntegrationTest()) {
 85 |       return;
 86 |     }
 87 | 
 88 |     // This connection must be available if we didn't skip
 89 |     if (!connection) {
 90 |       throw new Error(
 91 |         'Connection should be available when test is not skipped',
 92 |       );
 93 |     }
 94 | 
 95 |     // Act - make an actual API call to Azure DevOps
 96 |     const result = await getProjectDetails(connection, {
 97 |       projectId: projectName,
 98 |       includeProcess: true,
 99 |     });
100 | 
101 |     // Assert on the actual response
102 |     expect(result).toBeDefined();
103 |     expect(result.process).toBeDefined();
104 |     expect(result.process?.name).toBeDefined();
105 |   });
106 | 
107 |   test('should retrieve project details with work item types from Azure DevOps', async () => {
108 |     // Skip if no connection is available
109 |     if (shouldSkipIntegrationTest()) {
110 |       return;
111 |     }
112 | 
113 |     // This connection must be available if we didn't skip
114 |     if (!connection) {
115 |       throw new Error(
116 |         'Connection should be available when test is not skipped',
117 |       );
118 |     }
119 | 
120 |     // Act - make an actual API call to Azure DevOps
121 |     const result = await getProjectDetails(connection, {
122 |       projectId: projectName,
123 |       includeProcess: true,
124 |       includeWorkItemTypes: true,
125 |     });
126 | 
127 |     // Assert on the actual response
128 |     expect(result).toBeDefined();
129 |     expect(result.process).toBeDefined();
130 |     expect(result.process?.workItemTypes).toBeDefined();
131 |     expect(Array.isArray(result.process?.workItemTypes)).toBe(true);
132 | 
133 |     // There should be at least one work item type
134 |     if (
135 |       result.process?.workItemTypes &&
136 |       result.process.workItemTypes.length > 0
137 |     ) {
138 |       const workItemType = result.process.workItemTypes[0];
139 |       expect(workItemType.name).toBeDefined();
140 |       expect(workItemType.description).toBeDefined();
141 |       expect(workItemType.states).toBeDefined();
142 |     }
143 |   });
144 | 
145 |   test('should retrieve project details with fields from Azure DevOps', async () => {
146 |     // Skip if no connection is available
147 |     if (shouldSkipIntegrationTest()) {
148 |       return;
149 |     }
150 | 
151 |     // This connection must be available if we didn't skip
152 |     if (!connection) {
153 |       throw new Error(
154 |         'Connection should be available when test is not skipped',
155 |       );
156 |     }
157 | 
158 |     // Act - make an actual API call to Azure DevOps
159 |     const result = await getProjectDetails(connection, {
160 |       projectId: projectName,
161 |       includeProcess: true,
162 |       includeWorkItemTypes: true,
163 |       includeFields: true,
164 |     });
165 | 
166 |     // Assert on the actual response
167 |     expect(result).toBeDefined();
168 |     expect(result.process).toBeDefined();
169 |     expect(result.process?.workItemTypes).toBeDefined();
170 | 
171 |     // There should be at least one work item type with fields
172 |     if (
173 |       result.process?.workItemTypes &&
174 |       result.process.workItemTypes.length > 0
175 |     ) {
176 |       const workItemType = result.process.workItemTypes[0];
177 |       expect(workItemType.fields).toBeDefined();
178 |       expect(Array.isArray(workItemType.fields)).toBe(true);
179 | 
180 |       // There should be at least one field (like Title)
181 |       if (workItemType.fields && workItemType.fields.length > 0) {
182 |         const field = workItemType.fields[0];
183 |         expect(field.name).toBeDefined();
184 |         expect(field.referenceName).toBeDefined();
185 |       }
186 |     }
187 |   });
188 | 
189 |   test('should throw error when project is not found', async () => {
190 |     // Skip if no connection is available
191 |     if (shouldSkipIntegrationTest()) {
192 |       return;
193 |     }
194 | 
195 |     // This connection must be available if we didn't skip
196 |     if (!connection) {
197 |       throw new Error(
198 |         'Connection should be available when test is not skipped',
199 |       );
200 |     }
201 | 
202 |     // Use a non-existent project name
203 |     const nonExistentProjectName = 'non-existent-project-' + Date.now();
204 | 
205 |     // Act & Assert - should throw an error for non-existent project
206 |     await expect(
207 |       getProjectDetails(connection, {
208 |         projectId: nonExistentProjectName,
209 |       }),
210 |     ).rejects.toThrow(/not found|Failed to get project/);
211 |   });
212 | });
213 | 
```

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

```typescript
  1 | import { z } from 'zod';
  2 | import { defaultProject, defaultOrg } from '../../utils/environment';
  3 | 
  4 | /**
  5 |  * Schema for creating a pull request
  6 |  */
  7 | export const CreatePullRequestSchema = z.object({
  8 |   projectId: z
  9 |     .string()
 10 |     .optional()
 11 |     .describe(`The ID or name of the project (Default: ${defaultProject})`),
 12 |   organizationId: z
 13 |     .string()
 14 |     .optional()
 15 |     .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
 16 |   repositoryId: z.string().describe('The ID or name of the repository'),
 17 |   title: z.string().describe('The title of the pull request'),
 18 |   description: z
 19 |     .string()
 20 |     .optional()
 21 |     .describe('The description of the pull request (markdown is supported)'),
 22 |   sourceRefName: z
 23 |     .string()
 24 |     .describe('The source branch name (e.g., refs/heads/feature-branch)'),
 25 |   targetRefName: z
 26 |     .string()
 27 |     .describe('The target branch name (e.g., refs/heads/main)'),
 28 |   reviewers: z
 29 |     .array(z.string())
 30 |     .optional()
 31 |     .describe('List of reviewer email addresses or IDs'),
 32 |   isDraft: z
 33 |     .boolean()
 34 |     .optional()
 35 |     .describe('Whether the pull request should be created as a draft'),
 36 |   workItemRefs: z
 37 |     .array(z.number())
 38 |     .optional()
 39 |     .describe('List of work item IDs to link to the pull request'),
 40 |   additionalProperties: z
 41 |     .record(z.string(), z.any())
 42 |     .optional()
 43 |     .describe('Additional properties to set on the pull request'),
 44 | });
 45 | 
 46 | /**
 47 |  * Schema for listing pull requests
 48 |  */
 49 | export const ListPullRequestsSchema = z.object({
 50 |   projectId: z
 51 |     .string()
 52 |     .optional()
 53 |     .describe(`The ID or name of the project (Default: ${defaultProject})`),
 54 |   organizationId: z
 55 |     .string()
 56 |     .optional()
 57 |     .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
 58 |   repositoryId: z.string().describe('The ID or name of the repository'),
 59 |   status: z
 60 |     .enum(['all', 'active', 'completed', 'abandoned'])
 61 |     .optional()
 62 |     .describe('Filter by pull request status'),
 63 |   creatorId: z
 64 |     .string()
 65 |     .optional()
 66 |     .describe('Filter by creator ID (must be a UUID string)'),
 67 |   reviewerId: z
 68 |     .string()
 69 |     .optional()
 70 |     .describe('Filter by reviewer ID (must be a UUID string)'),
 71 |   sourceRefName: z.string().optional().describe('Filter by source branch name'),
 72 |   targetRefName: z.string().optional().describe('Filter by target branch name'),
 73 |   top: z
 74 |     .number()
 75 |     .default(10)
 76 |     .describe('Maximum number of pull requests to return (default: 10)'),
 77 |   skip: z
 78 |     .number()
 79 |     .optional()
 80 |     .describe('Number of pull requests to skip for pagination'),
 81 | });
 82 | 
 83 | /**
 84 |  * Schema for getting pull request comments
 85 |  */
 86 | export const GetPullRequestCommentsSchema = z.object({
 87 |   projectId: z
 88 |     .string()
 89 |     .optional()
 90 |     .describe(`The ID or name of the project (Default: ${defaultProject})`),
 91 |   organizationId: z
 92 |     .string()
 93 |     .optional()
 94 |     .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
 95 |   repositoryId: z.string().describe('The ID or name of the repository'),
 96 |   pullRequestId: z.number().describe('The ID of the pull request'),
 97 |   threadId: z
 98 |     .number()
 99 |     .optional()
100 |     .describe('The ID of the specific thread to get comments from'),
101 |   includeDeleted: z
102 |     .boolean()
103 |     .optional()
104 |     .describe('Whether to include deleted comments'),
105 |   top: z
106 |     .number()
107 |     .optional()
108 |     .describe('Maximum number of threads/comments to return'),
109 | });
110 | 
111 | /**
112 |  * Schema for adding a comment to a pull request
113 |  */
114 | export const AddPullRequestCommentSchema = z
115 |   .object({
116 |     projectId: z
117 |       .string()
118 |       .optional()
119 |       .describe(`The ID or name of the project (Default: ${defaultProject})`),
120 |     organizationId: z
121 |       .string()
122 |       .optional()
123 |       .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
124 |     repositoryId: z.string().describe('The ID or name of the repository'),
125 |     pullRequestId: z.number().describe('The ID of the pull request'),
126 |     content: z.string().describe('The content of the comment in markdown'),
127 |     threadId: z
128 |       .number()
129 |       .optional()
130 |       .describe('The ID of the thread to add the comment to'),
131 |     parentCommentId: z
132 |       .number()
133 |       .optional()
134 |       .describe(
135 |         'ID of the parent comment when replying to an existing comment',
136 |       ),
137 |     filePath: z
138 |       .string()
139 |       .optional()
140 |       .describe('The path of the file to comment on (for new thread on file)'),
141 |     lineNumber: z
142 |       .number()
143 |       .optional()
144 |       .describe('The line number to comment on (for new thread on file)'),
145 |     status: z
146 |       .enum([
147 |         'active',
148 |         'fixed',
149 |         'wontFix',
150 |         'closed',
151 |         'pending',
152 |         'byDesign',
153 |         'unknown',
154 |       ])
155 |       .optional()
156 |       .describe('The status to set for a new thread'),
157 |   })
158 |   .superRefine((data, ctx) => {
159 |     // If we're creating a new thread (no threadId), status is required
160 |     if (!data.threadId && !data.status) {
161 |       ctx.addIssue({
162 |         code: z.ZodIssueCode.custom,
163 |         message: 'Status is required when creating a new thread',
164 |         path: ['status'],
165 |       });
166 |     }
167 |   });
168 | 
169 | /**
170 |  * Schema for updating a pull request
171 |  */
172 | export const UpdatePullRequestSchema = z.object({
173 |   projectId: z
174 |     .string()
175 |     .optional()
176 |     .describe(`The ID or name of the project (Default: ${defaultProject})`),
177 |   organizationId: z
178 |     .string()
179 |     .optional()
180 |     .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
181 |   repositoryId: z.string().describe('The ID or name of the repository'),
182 |   pullRequestId: z.number().describe('The ID of the pull request to update'),
183 |   title: z
184 |     .string()
185 |     .optional()
186 |     .describe('The updated title of the pull request'),
187 |   description: z
188 |     .string()
189 |     .optional()
190 |     .describe('The updated description of the pull request'),
191 |   status: z
192 |     .enum(['active', 'abandoned', 'completed'])
193 |     .optional()
194 |     .describe('The updated status of the pull request'),
195 |   isDraft: z
196 |     .boolean()
197 |     .optional()
198 |     .describe(
199 |       'Whether the pull request should be marked as a draft (true) or unmarked (false)',
200 |     ),
201 |   addWorkItemIds: z
202 |     .array(z.number())
203 |     .optional()
204 |     .describe('List of work item IDs to link to the pull request'),
205 |   removeWorkItemIds: z
206 |     .array(z.number())
207 |     .optional()
208 |     .describe('List of work item IDs to unlink from the pull request'),
209 |   addReviewers: z
210 |     .array(z.string())
211 |     .optional()
212 |     .describe('List of reviewer email addresses or IDs to add'),
213 |   removeReviewers: z
214 |     .array(z.string())
215 |     .optional()
216 |     .describe('List of reviewer email addresses or IDs to remove'),
217 |   additionalProperties: z
218 |     .record(z.string(), z.any())
219 |     .optional()
220 |     .describe('Additional properties to update on the pull request'),
221 | });
222 | 
```

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

```markdown
  1 | 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.
  2 | 
  3 | ---
  4 | 
  5 | ## Dream Team Documentation: Building the Azure DevOps MCP Server
  6 | 
  7 | ### Overview
  8 | 
  9 | 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.
 10 | 
 11 | ### Key Roles and Responsibilities
 12 | 
 13 | #### 1. **Full-Stack Developer (Typescript/Node.js)**
 14 | 
 15 | - **Responsibilities**:
 16 |   - Implement the server's core functionality using Typescript and Node.js.
 17 |   - Develop and maintain MCP tools (e.g., `list_projects`, `create_work_item`).
 18 |   - Write tests as part of the implementation process (TDD).
 19 |   - Integrate with the MCP Typescript SDK and Azure DevOps APIs.
 20 |   - Write clean, modular, and efficient code following best practices.
 21 |   - Ensure code quality through comprehensive unit and integration tests.
 22 |   - Build automated testing pipelines for continuous integration.
 23 |   - Perform integration testing across components.
 24 | - **Required Skills**:
 25 |   - Proficiency in Typescript and Node.js.
 26 |   - Strong testing skills and experience with test frameworks (e.g., Jest).
 27 |   - Experience writing testable code and following TDD practices.
 28 |   - Experience with REST APIs and asynchronous programming.
 29 |   - Familiarity with Git and version control systems.
 30 |   - Understanding of modular software design.
 31 |   - Experience with API testing and mocking tools.
 32 | 
 33 | #### 2. **Azure DevOps API Expert**
 34 | 
 35 | - **Responsibilities**:
 36 |   - Guide the team on effectively using Azure DevOps REST APIs (e.g., Git, Work Item Tracking, Build).
 37 |   - Ensure the server leverages Azure DevOps features optimally (e.g., repository operations, pipelines).
 38 |   - Assist in mapping MCP tools to the correct API endpoints.
 39 |   - Troubleshoot API-related issues and optimize API usage.
 40 |   - Help develop tests for Azure DevOps API integrations.
 41 | - **Required Skills**:
 42 |   - Deep understanding of Azure DevOps services and their REST APIs.
 43 |   - Experience with Azure DevOps workflows (e.g., repositories, work items, pipelines).
 44 |   - Knowledge of Azure DevOps authentication mechanisms (PAT, AAD).
 45 |   - Ability to interpret API documentation and handle rate limits.
 46 |   - Experience testing API integrations.
 47 | 
 48 | #### 3. **Security Specialist**
 49 | 
 50 | - **Responsibilities**:
 51 |   - Design and implement secure authentication methods (PAT and AAD).
 52 |   - Ensure credentials are stored and managed securely (e.g., environment variables).
 53 |   - Scope permissions to the minimum required for each tool.
 54 |   - Implement error handling and logging without exposing sensitive data.
 55 |   - Conduct security reviews and recommend improvements.
 56 |   - Develop security tests and validation procedures.
 57 | - **Required Skills**:
 58 |   - Expertise in API security, authentication, and authorization.
 59 |   - Familiarity with Azure Active Directory and PAT management.
 60 |   - Knowledge of secure coding practices and vulnerability prevention.
 61 |   - Experience with logging, auditing, and compliance.
 62 |   - Experience with security testing tools and methodologies.
 63 | 
 64 | #### 4. **Technical Writer**
 65 | 
 66 | - **Responsibilities**:
 67 |   - Create comprehensive documentation, including setup guides, tool descriptions, and usage examples.
 68 |   - Write clear API references and troubleshooting tips.
 69 |   - Ensure documentation is accessible to both technical and non-technical users.
 70 |   - Maintain up-to-date documentation as the server evolves.
 71 | - **Required Skills**:
 72 |   - Strong technical writing and communication skills.
 73 |   - Ability to explain complex concepts simply.
 74 |   - Experience documenting APIs and developer tools.
 75 |   - Familiarity with Markdown and documentation platforms (e.g., GitHub README).
 76 | 
 77 | #### 5. **Project Manager**
 78 | 
 79 | - **Responsibilities**:
 80 |   - Coordinate the team's efforts and manage the project timeline.
 81 |   - Track progress using Azure Boards or similar tools.
 82 |   - Facilitate communication and resolve blockers.
 83 |   - Ensure the project stays on scope and meets deadlines.
 84 |   - Manage stakeholder expectations and provide status updates.
 85 | - **Required Skills**:
 86 |   - Experience in agile project management.
 87 |   - Proficiency with project tracking tools (e.g., Azure Boards, Jira).
 88 |   - Strong organizational and leadership skills.
 89 |   - Ability to manage remote or distributed teams.
 90 | 
 91 | #### 6. **AI Integration Consultant**
 92 | 
 93 | - **Responsibilities**:
 94 |   - Advise on how the server can best integrate with AI models (e.g., Claude Desktop).
 95 |   - Ensure tools are designed to support AI-driven workflows (e.g., user story to pull request).
 96 |   - Provide insights into MCP's AI integration capabilities.
 97 |   - Assist in testing AI interactions with the server.
 98 | - **Required Skills**:
 99 |   - Experience with AI model integration and workflows.
100 |   - Understanding of the Model Context Protocol (MCP).
101 |   - Familiarity with AI tools like Claude Desktop.
102 |   - Ability to bridge AI and software development domains.
103 | 
104 | ---
105 | 
106 | ### Team Structure and Collaboration
107 | 
108 | - **Core Team**: Full-Stack Developer, Azure DevOps API Expert, Security Specialist.
109 | - **Support Roles**: Technical Writer, Project Manager, AI Integration Consultant.
110 | - **Collaboration**: Use Agile methodologies with bi-weekly sprints, daily stand-ups, and regular retrospectives to iterate efficiently.
111 | - **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.
112 | 
113 | ---
114 | 
115 | ### Why This Team?
116 | 
117 | Each role addresses a critical aspect of the project:
118 | 
119 | - The **Full-Stack Developer** builds the server using modern technologies like Typescript and Node.js, integrating testing throughout the development process.
120 | - The **Azure DevOps API Expert** ensures seamless integration with Azure DevOps services.
121 | - The **Security Specialist** safeguards the server against vulnerabilities.
122 | - The **Technical Writer** makes the server user-friendly with clear documentation.
123 | - The **Project Manager** keeps the team aligned and on schedule.
124 | - The **AI Integration Consultant** ensures the server meets AI-driven workflow requirements.
125 | 
126 | 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.
127 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { getRepositoryDetails } from './feature';
  3 | import {
  4 |   getTestConnection,
  5 |   shouldSkipIntegrationTest,
  6 | } from '@/shared/test/test-helpers';
  7 | 
  8 | describe('getRepositoryDetails integration', () => {
  9 |   let connection: WebApi | null = null;
 10 |   let projectName: string;
 11 | 
 12 |   beforeAll(async () => {
 13 |     // Get a real connection using environment variables
 14 |     connection = await getTestConnection();
 15 |     projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
 16 |   });
 17 | 
 18 |   test('should retrieve repository details from Azure DevOps', async () => {
 19 |     // Skip if no connection is available
 20 |     if (shouldSkipIntegrationTest()) {
 21 |       return;
 22 |     }
 23 | 
 24 |     // This connection must be available if we didn't skip
 25 |     if (!connection) {
 26 |       throw new Error(
 27 |         'Connection should be available when test is not skipped',
 28 |       );
 29 |     }
 30 | 
 31 |     // First, get a list of repos to find one to test with
 32 |     const gitApi = await connection.getGitApi();
 33 |     const repos = await gitApi.getRepositories(projectName);
 34 | 
 35 |     // Skip if no repos are available
 36 |     if (!repos || repos.length === 0) {
 37 |       console.log('Skipping test: No repositories available in the project');
 38 |       return;
 39 |     }
 40 | 
 41 |     // Use the first repo as a test subject
 42 |     const testRepo = repos[0];
 43 | 
 44 |     // Act - make an actual API call to Azure DevOps
 45 |     const result = await getRepositoryDetails(connection, {
 46 |       projectId: projectName,
 47 |       repositoryId: testRepo.name || testRepo.id || '',
 48 |     });
 49 | 
 50 |     // Assert on the actual response
 51 |     expect(result).toBeDefined();
 52 |     expect(result.repository).toBeDefined();
 53 |     expect(result.repository.id).toBe(testRepo.id);
 54 |     expect(result.repository.name).toBe(testRepo.name);
 55 |     expect(result.repository.project).toBeDefined();
 56 |     if (result.repository.project) {
 57 |       expect(result.repository.project.name).toBe(projectName);
 58 |     }
 59 |   });
 60 | 
 61 |   test('should retrieve repository details with statistics', async () => {
 62 |     // Skip if no connection is available
 63 |     if (shouldSkipIntegrationTest()) {
 64 |       return;
 65 |     }
 66 | 
 67 |     // This connection must be available if we didn't skip
 68 |     if (!connection) {
 69 |       throw new Error(
 70 |         'Connection should be available when test is not skipped',
 71 |       );
 72 |     }
 73 | 
 74 |     // First, get a list of repos to find one to test with
 75 |     const gitApi = await connection.getGitApi();
 76 |     const repos = await gitApi.getRepositories(projectName);
 77 | 
 78 |     // Skip if no repos are available
 79 |     if (!repos || repos.length === 0) {
 80 |       console.log('Skipping test: No repositories available in the project');
 81 |       return;
 82 |     }
 83 | 
 84 |     // Use the first repo as a test subject
 85 |     const testRepo = repos[0];
 86 | 
 87 |     // Act - make an actual API call to Azure DevOps
 88 |     const result = await getRepositoryDetails(connection, {
 89 |       projectId: projectName,
 90 |       repositoryId: testRepo.name || testRepo.id || '',
 91 |       includeStatistics: true,
 92 |     });
 93 | 
 94 |     // Assert on the actual response
 95 |     expect(result).toBeDefined();
 96 |     expect(result.repository).toBeDefined();
 97 |     expect(result.repository.id).toBe(testRepo.id);
 98 |     expect(result.statistics).toBeDefined();
 99 |     expect(Array.isArray(result.statistics?.branches)).toBe(true);
100 |   });
101 | 
102 |   test('should retrieve repository details with refs', async () => {
103 |     // Skip if no connection is available
104 |     if (shouldSkipIntegrationTest()) {
105 |       return;
106 |     }
107 | 
108 |     // This connection must be available if we didn't skip
109 |     if (!connection) {
110 |       throw new Error(
111 |         'Connection should be available when test is not skipped',
112 |       );
113 |     }
114 | 
115 |     // First, get a list of repos to find one to test with
116 |     const gitApi = await connection.getGitApi();
117 |     const repos = await gitApi.getRepositories(projectName);
118 | 
119 |     // Skip if no repos are available
120 |     if (!repos || repos.length === 0) {
121 |       console.log('Skipping test: No repositories available in the project');
122 |       return;
123 |     }
124 | 
125 |     // Use the first repo as a test subject
126 |     const testRepo = repos[0];
127 | 
128 |     // Act - make an actual API call to Azure DevOps
129 |     const result = await getRepositoryDetails(connection, {
130 |       projectId: projectName,
131 |       repositoryId: testRepo.name || testRepo.id || '',
132 |       includeRefs: true,
133 |     });
134 | 
135 |     // Assert on the actual response
136 |     expect(result).toBeDefined();
137 |     expect(result.repository).toBeDefined();
138 |     expect(result.repository.id).toBe(testRepo.id);
139 |     expect(result.refs).toBeDefined();
140 |     expect(result.refs?.value).toBeDefined();
141 |     expect(Array.isArray(result.refs?.value)).toBe(true);
142 |     expect(typeof result.refs?.count).toBe('number');
143 |   });
144 | 
145 |   test('should retrieve repository details with refs filtered by heads/', async () => {
146 |     // Skip if no connection is available
147 |     if (shouldSkipIntegrationTest()) {
148 |       return;
149 |     }
150 | 
151 |     // This connection must be available if we didn't skip
152 |     if (!connection) {
153 |       throw new Error(
154 |         'Connection should be available when test is not skipped',
155 |       );
156 |     }
157 | 
158 |     // First, get a list of repos to find one to test with
159 |     const gitApi = await connection.getGitApi();
160 |     const repos = await gitApi.getRepositories(projectName);
161 | 
162 |     // Skip if no repos are available
163 |     if (!repos || repos.length === 0) {
164 |       console.log('Skipping test: No repositories available in the project');
165 |       return;
166 |     }
167 | 
168 |     // Use the first repo as a test subject
169 |     const testRepo = repos[0];
170 | 
171 |     // Act - make an actual API call to Azure DevOps
172 |     const result = await getRepositoryDetails(connection, {
173 |       projectId: projectName,
174 |       repositoryId: testRepo.name || testRepo.id || '',
175 |       includeRefs: true,
176 |       refFilter: 'heads/',
177 |     });
178 | 
179 |     // Assert on the actual response
180 |     expect(result).toBeDefined();
181 |     expect(result.repository).toBeDefined();
182 |     expect(result.refs).toBeDefined();
183 |     expect(result.refs?.value).toBeDefined();
184 | 
185 |     // All refs should start with refs/heads/
186 |     if (result.refs && result.refs.value.length > 0) {
187 |       result.refs.value.forEach((ref) => {
188 |         expect(ref.name).toMatch(/^refs\/heads\//);
189 |       });
190 |     }
191 |   });
192 | 
193 |   test('should throw error when repository is not found', async () => {
194 |     // Skip if no connection is available
195 |     if (shouldSkipIntegrationTest()) {
196 |       return;
197 |     }
198 | 
199 |     // This connection must be available if we didn't skip
200 |     if (!connection) {
201 |       throw new Error(
202 |         'Connection should be available when test is not skipped',
203 |       );
204 |     }
205 | 
206 |     // Use a non-existent repository name
207 |     const nonExistentRepoName = 'non-existent-repo-' + Date.now();
208 | 
209 |     // Act & Assert - should throw an error for non-existent repo
210 |     await expect(
211 |       getRepositoryDetails(connection, {
212 |         projectId: projectName,
213 |         repositoryId: nonExistentRepoName,
214 |       }),
215 |     ).rejects.toThrow(/not found|Failed to get repository/);
216 |   });
217 | });
218 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { listPullRequests } from './feature';
  3 | import { PullRequestStatus } from 'azure-devops-node-api/interfaces/GitInterfaces';
  4 | 
  5 | describe('listPullRequests', () => {
  6 |   afterEach(() => {
  7 |     jest.resetAllMocks();
  8 |   });
  9 | 
 10 |   test('should return pull requests successfully with pagination metadata', async () => {
 11 |     // Mock data
 12 |     const mockPullRequests = [
 13 |       {
 14 |         pullRequestId: 1,
 15 |         title: 'Test PR 1',
 16 |         description: 'Test PR description 1',
 17 |       },
 18 |       {
 19 |         pullRequestId: 2,
 20 |         title: 'Test PR 2',
 21 |         description: 'Test PR description 2',
 22 |       },
 23 |     ];
 24 | 
 25 |     // Setup mock connection
 26 |     const mockGitApi = {
 27 |       getPullRequests: jest.fn().mockResolvedValue(mockPullRequests),
 28 |     };
 29 | 
 30 |     const mockConnection: any = {
 31 |       getGitApi: jest.fn().mockResolvedValue(mockGitApi),
 32 |     };
 33 | 
 34 |     // Call the function with test parameters
 35 |     const projectId = 'test-project';
 36 |     const repositoryId = 'test-repo';
 37 |     const options = {
 38 |       projectId,
 39 |       repositoryId,
 40 |       status: 'active' as const,
 41 |       top: 10,
 42 |     };
 43 | 
 44 |     const result = await listPullRequests(
 45 |       mockConnection as WebApi,
 46 |       projectId,
 47 |       repositoryId,
 48 |       options,
 49 |     );
 50 | 
 51 |     // Verify results
 52 |     expect(result).toEqual({
 53 |       count: 2,
 54 |       value: mockPullRequests,
 55 |       hasMoreResults: false,
 56 |       warning: undefined,
 57 |     });
 58 |     expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1);
 59 |     expect(mockGitApi.getPullRequests).toHaveBeenCalledTimes(1);
 60 |     expect(mockGitApi.getPullRequests).toHaveBeenCalledWith(
 61 |       repositoryId,
 62 |       { status: PullRequestStatus.Active },
 63 |       projectId,
 64 |       undefined, // maxCommentLength
 65 |       0, // skip
 66 |       10, // top
 67 |     );
 68 |   });
 69 | 
 70 |   test('should return empty array when no pull requests exist', async () => {
 71 |     // Setup mock connection
 72 |     const mockGitApi = {
 73 |       getPullRequests: jest.fn().mockResolvedValue(null),
 74 |     };
 75 | 
 76 |     const mockConnection: any = {
 77 |       getGitApi: jest.fn().mockResolvedValue(mockGitApi),
 78 |     };
 79 | 
 80 |     // Call the function with test parameters
 81 |     const projectId = 'test-project';
 82 |     const repositoryId = 'test-repo';
 83 |     const options = { projectId, repositoryId };
 84 | 
 85 |     const result = await listPullRequests(
 86 |       mockConnection as WebApi,
 87 |       projectId,
 88 |       repositoryId,
 89 |       options,
 90 |     );
 91 | 
 92 |     // Verify results
 93 |     expect(result).toEqual({
 94 |       count: 0,
 95 |       value: [],
 96 |       hasMoreResults: false,
 97 |       warning: undefined,
 98 |     });
 99 |     expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1);
100 |     expect(mockGitApi.getPullRequests).toHaveBeenCalledTimes(1);
101 |   });
102 | 
103 |   test('should handle all filter options correctly', async () => {
104 |     // Setup mock connection
105 |     const mockGitApi = {
106 |       getPullRequests: jest.fn().mockResolvedValue([]),
107 |     };
108 | 
109 |     const mockConnection: any = {
110 |       getGitApi: jest.fn().mockResolvedValue(mockGitApi),
111 |     };
112 | 
113 |     // Call with all options
114 |     const projectId = 'test-project';
115 |     const repositoryId = 'test-repo';
116 |     const options = {
117 |       projectId,
118 |       repositoryId,
119 |       status: 'completed' as const,
120 |       creatorId: 'a8a8a8a8-a8a8-a8a8-a8a8-a8a8a8a8a8a8',
121 |       reviewerId: 'b9b9b9b9-b9b9-b9b9-b9b9-b9b9b9b9b9b9',
122 |       sourceRefName: 'refs/heads/source-branch',
123 |       targetRefName: 'refs/heads/target-branch',
124 |       top: 5,
125 |       skip: 10,
126 |     };
127 | 
128 |     await listPullRequests(
129 |       mockConnection as WebApi,
130 |       projectId,
131 |       repositoryId,
132 |       options,
133 |     );
134 | 
135 |     // Verify the search criteria was constructed correctly
136 |     expect(mockGitApi.getPullRequests).toHaveBeenCalledWith(
137 |       repositoryId,
138 |       {
139 |         status: PullRequestStatus.Completed,
140 |         creatorId: 'a8a8a8a8-a8a8-a8a8-a8a8-a8a8a8a8a8a8',
141 |         reviewerId: 'b9b9b9b9-b9b9-b9b9-b9b9-b9b9b9b9b9b9',
142 |         sourceRefName: 'refs/heads/source-branch',
143 |         targetRefName: 'refs/heads/target-branch',
144 |       },
145 |       projectId,
146 |       undefined, // maxCommentLength
147 |       10, // skip
148 |       5, // top
149 |     );
150 |   });
151 | 
152 |   test('should throw error when API call fails', async () => {
153 |     // Setup mock connection
154 |     const errorMessage = 'API error';
155 |     const mockConnection: any = {
156 |       getGitApi: jest.fn().mockImplementation(() => ({
157 |         getPullRequests: jest.fn().mockRejectedValue(new Error(errorMessage)),
158 |       })),
159 |     };
160 | 
161 |     // Call the function with test parameters
162 |     const projectId = 'test-project';
163 |     const repositoryId = 'test-repo';
164 |     const options = { projectId, repositoryId };
165 | 
166 |     // Verify error handling
167 |     await expect(
168 |       listPullRequests(
169 |         mockConnection as WebApi,
170 |         projectId,
171 |         repositoryId,
172 |         options,
173 |       ),
174 |     ).rejects.toThrow(`Failed to list pull requests: ${errorMessage}`);
175 |   });
176 | 
177 |   test('should use default pagination values when not provided', async () => {
178 |     // Mock data
179 |     const mockPullRequests = [
180 |       { pullRequestId: 1, title: 'Test PR 1' },
181 |       { pullRequestId: 2, title: 'Test PR 2' },
182 |     ];
183 | 
184 |     // Setup mock connection
185 |     const mockGitApi = {
186 |       getPullRequests: jest.fn().mockResolvedValue(mockPullRequests),
187 |     };
188 | 
189 |     const mockConnection: any = {
190 |       getGitApi: jest.fn().mockResolvedValue(mockGitApi),
191 |     };
192 | 
193 |     // Call the function with minimal parameters (no top or skip)
194 |     const projectId = 'test-project';
195 |     const repositoryId = 'test-repo';
196 |     const options = { projectId, repositoryId };
197 | 
198 |     const result = await listPullRequests(
199 |       mockConnection as WebApi,
200 |       projectId,
201 |       repositoryId,
202 |       options,
203 |     );
204 | 
205 |     // Verify default values were used
206 |     expect(mockGitApi.getPullRequests).toHaveBeenCalledWith(
207 |       repositoryId,
208 |       {},
209 |       projectId,
210 |       undefined, // maxCommentLength
211 |       0, // default skip
212 |       10, // default top
213 |     );
214 | 
215 |     expect(result.count).toBe(2);
216 |     expect(result.value).toEqual(mockPullRequests);
217 |   });
218 | 
219 |   test('should add warning when hasMoreResults is true', async () => {
220 |     // Create exactly 10 mock pull requests to trigger hasMoreResults
221 |     const mockPullRequests = Array(10)
222 |       .fill(0)
223 |       .map((_, i) => ({
224 |         pullRequestId: i + 1,
225 |         title: `Test PR ${i + 1}`,
226 |       }));
227 | 
228 |     // Setup mock connection
229 |     const mockGitApi = {
230 |       getPullRequests: jest.fn().mockResolvedValue(mockPullRequests),
231 |     };
232 | 
233 |     const mockConnection: any = {
234 |       getGitApi: jest.fn().mockResolvedValue(mockGitApi),
235 |     };
236 | 
237 |     // Call with top=10 to match the number of results
238 |     const projectId = 'test-project';
239 |     const repositoryId = 'test-repo';
240 |     const options = {
241 |       projectId,
242 |       repositoryId,
243 |       top: 10,
244 |       skip: 5,
245 |     };
246 | 
247 |     const result = await listPullRequests(
248 |       mockConnection as WebApi,
249 |       projectId,
250 |       repositoryId,
251 |       options,
252 |     );
253 | 
254 |     // Verify hasMoreResults is true and warning is set
255 |     expect(result.hasMoreResults).toBe(true);
256 |     expect(result.warning).toBe(
257 |       "Results limited to 10 items. Use 'skip: 15' to get the next page.",
258 |     );
259 |   });
260 | });
261 | 
```

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

```typescript
  1 | import { updatePullRequest } from './feature';
  2 | import { AzureDevOpsClient } from '../../../shared/auth/client-factory';
  3 | import { AzureDevOpsError } from '../../../shared/errors';
  4 | 
  5 | // Mock the AzureDevOpsClient
  6 | jest.mock('../../../shared/auth/client-factory');
  7 | 
  8 | describe('updatePullRequest', () => {
  9 |   const mockGetPullRequestById = jest.fn();
 10 |   const mockUpdatePullRequest = jest.fn();
 11 |   const mockUpdateWorkItem = jest.fn();
 12 |   const mockGetWorkItem = jest.fn();
 13 | 
 14 |   // Mock Git API
 15 |   const mockGitApi = {
 16 |     getPullRequestById: mockGetPullRequestById,
 17 |     updatePullRequest: mockUpdatePullRequest,
 18 |   };
 19 | 
 20 |   // Mock Work Item Tracking API
 21 |   const mockWorkItemTrackingApi = {
 22 |     updateWorkItem: mockUpdateWorkItem,
 23 |     getWorkItem: mockGetWorkItem,
 24 |   };
 25 | 
 26 |   // Mock connection
 27 |   const mockConnection = {
 28 |     getGitApi: jest.fn().mockResolvedValue(mockGitApi),
 29 |     getWorkItemTrackingApi: jest
 30 |       .fn()
 31 |       .mockResolvedValue(mockWorkItemTrackingApi),
 32 |   };
 33 | 
 34 |   const mockAzureDevopsClient = {
 35 |     getWebApiClient: jest.fn().mockResolvedValue(mockConnection),
 36 |     // ...other properties if needed
 37 |   };
 38 | 
 39 |   beforeEach(() => {
 40 |     jest.clearAllMocks();
 41 |     (AzureDevOpsClient as unknown as jest.Mock).mockImplementation(
 42 |       () => mockAzureDevopsClient,
 43 |     );
 44 |   });
 45 | 
 46 |   it('should throw error when pull request does not exist', async () => {
 47 |     mockGetPullRequestById.mockResolvedValueOnce(null);
 48 | 
 49 |     await expect(
 50 |       updatePullRequest({
 51 |         projectId: 'project-1',
 52 |         repositoryId: 'repo1',
 53 |         pullRequestId: 123,
 54 |       }),
 55 |     ).rejects.toThrow(AzureDevOpsError);
 56 |   });
 57 | 
 58 |   it('should update the pull request title and description', async () => {
 59 |     mockGetPullRequestById.mockResolvedValueOnce({
 60 |       repository: { id: 'repo1' },
 61 |     });
 62 | 
 63 |     mockUpdatePullRequest.mockResolvedValueOnce({
 64 |       title: 'Updated Title',
 65 |       description: 'Updated Description',
 66 |     });
 67 | 
 68 |     const result = await updatePullRequest({
 69 |       projectId: 'project-1',
 70 |       repositoryId: 'repo1',
 71 |       pullRequestId: 123,
 72 |       title: 'Updated Title',
 73 |       description: 'Updated Description',
 74 |     });
 75 | 
 76 |     expect(mockUpdatePullRequest).toHaveBeenCalledWith(
 77 |       {
 78 |         title: 'Updated Title',
 79 |         description: 'Updated Description',
 80 |       },
 81 |       'repo1',
 82 |       123,
 83 |       'project-1',
 84 |     );
 85 | 
 86 |     expect(result).toEqual({
 87 |       title: 'Updated Title',
 88 |       description: 'Updated Description',
 89 |     });
 90 |   });
 91 | 
 92 |   it('should update the pull request status when status is provided', async () => {
 93 |     mockGetPullRequestById.mockResolvedValueOnce({
 94 |       repository: { id: 'repo1' },
 95 |     });
 96 | 
 97 |     mockUpdatePullRequest.mockResolvedValueOnce({
 98 |       status: 2, // Abandoned
 99 |     });
100 | 
101 |     const result = await updatePullRequest({
102 |       projectId: 'project-1',
103 |       repositoryId: 'repo1',
104 |       pullRequestId: 123,
105 |       status: 'abandoned',
106 |     });
107 | 
108 |     expect(mockUpdatePullRequest).toHaveBeenCalledWith(
109 |       {
110 |         status: 2, // Abandoned value
111 |       },
112 |       'repo1',
113 |       123,
114 |       'project-1',
115 |     );
116 | 
117 |     expect(result).toEqual({
118 |       status: 2, // Abandoned
119 |     });
120 |   });
121 | 
122 |   it('should throw error for invalid status', async () => {
123 |     mockGetPullRequestById.mockResolvedValueOnce({
124 |       repository: { id: 'repo1' },
125 |     });
126 | 
127 |     await expect(
128 |       updatePullRequest({
129 |         projectId: 'project-1',
130 |         repositoryId: 'repo1',
131 |         pullRequestId: 123,
132 |         status: 'invalid-status' as any,
133 |       }),
134 |     ).rejects.toThrow(AzureDevOpsError);
135 |   });
136 | 
137 |   it('should update the pull request draft status', async () => {
138 |     mockGetPullRequestById.mockResolvedValueOnce({
139 |       repository: { id: 'repo1' },
140 |     });
141 | 
142 |     mockUpdatePullRequest.mockResolvedValueOnce({
143 |       isDraft: true,
144 |     });
145 | 
146 |     const result = await updatePullRequest({
147 |       projectId: 'project-1',
148 |       repositoryId: 'repo1',
149 |       pullRequestId: 123,
150 |       isDraft: true,
151 |     });
152 | 
153 |     expect(mockUpdatePullRequest).toHaveBeenCalledWith(
154 |       {
155 |         isDraft: true,
156 |       },
157 |       'repo1',
158 |       123,
159 |       'project-1',
160 |     );
161 | 
162 |     expect(result).toEqual({
163 |       isDraft: true,
164 |     });
165 |   });
166 | 
167 |   it('should include additionalProperties in the update', async () => {
168 |     mockGetPullRequestById.mockResolvedValueOnce({
169 |       repository: { id: 'repo1' },
170 |     });
171 | 
172 |     mockUpdatePullRequest.mockResolvedValueOnce({
173 |       title: 'Title',
174 |       customProperty: 'custom value',
175 |     });
176 | 
177 |     const result = await updatePullRequest({
178 |       projectId: 'project-1',
179 |       repositoryId: 'repo1',
180 |       pullRequestId: 123,
181 |       additionalProperties: {
182 |         customProperty: 'custom value',
183 |       },
184 |     });
185 | 
186 |     expect(mockUpdatePullRequest).toHaveBeenCalledWith(
187 |       {
188 |         customProperty: 'custom value',
189 |       },
190 |       'repo1',
191 |       123,
192 |       'project-1',
193 |     );
194 | 
195 |     expect(result).toEqual({
196 |       title: 'Title',
197 |       customProperty: 'custom value',
198 |     });
199 |   });
200 | 
201 |   it('should handle work item links', async () => {
202 |     // Define the artifactId that will be used
203 |     const artifactId = 'vstfs:///Git/PullRequestId/project-1/repo1/123';
204 | 
205 |     mockGetPullRequestById.mockResolvedValueOnce({
206 |       repository: { id: 'repo1' },
207 |       artifactId: artifactId, // Add the artifactId to the mock response
208 |     });
209 | 
210 |     mockUpdatePullRequest.mockResolvedValueOnce({
211 |       pullRequestId: 123,
212 |       repository: { id: 'repo1' },
213 |       artifactId: artifactId,
214 |     });
215 | 
216 |     // Mocks for work items to remove
217 |     mockGetWorkItem.mockResolvedValueOnce({
218 |       relations: [
219 |         {
220 |           rel: 'ArtifactLink',
221 |           url: artifactId, // Use the same artifactId here
222 |           attributes: {
223 |             name: 'Pull Request',
224 |           },
225 |         },
226 |       ],
227 |     });
228 | 
229 |     mockGetWorkItem.mockResolvedValueOnce({
230 |       relations: [
231 |         {
232 |           rel: 'ArtifactLink',
233 |           url: artifactId, // Use the same artifactId here
234 |           attributes: {
235 |             name: 'Pull Request',
236 |           },
237 |         },
238 |       ],
239 |     });
240 | 
241 |     await updatePullRequest({
242 |       projectId: 'project-1',
243 |       repositoryId: 'repo1',
244 |       pullRequestId: 123,
245 |       addWorkItemIds: [456, 789],
246 |       removeWorkItemIds: [101, 202],
247 |     });
248 | 
249 |     // Check that updateWorkItem was called for adding work items
250 |     expect(mockUpdateWorkItem).toHaveBeenCalledTimes(4); // 2 for add, 2 for remove
251 |     expect(mockUpdateWorkItem).toHaveBeenCalledWith(
252 |       null,
253 |       [
254 |         {
255 |           op: 'add',
256 |           path: '/relations/-',
257 |           value: {
258 |             rel: 'ArtifactLink',
259 |             url: 'vstfs:///Git/PullRequestId/project-1/repo1/123',
260 |             attributes: {
261 |               name: 'Pull Request',
262 |             },
263 |           },
264 |         },
265 |       ],
266 |       456,
267 |     );
268 | 
269 |     // Check for removing work items
270 |     expect(mockUpdateWorkItem).toHaveBeenCalledWith(
271 |       null,
272 |       [
273 |         {
274 |           op: 'remove',
275 |           path: '/relations/0',
276 |         },
277 |       ],
278 |       101,
279 |     );
280 |   });
281 | 
282 |   it('should wrap unexpected errors in a friendly error message', async () => {
283 |     mockGetPullRequestById.mockRejectedValueOnce(new Error('Unexpected'));
284 | 
285 |     await expect(
286 |       updatePullRequest({
287 |         projectId: 'project-1',
288 |         repositoryId: 'repo1',
289 |         pullRequestId: 123,
290 |       }),
291 |     ).rejects.toThrow(AzureDevOpsError);
292 |   });
293 | });
294 | 
```

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

```markdown
  1 | # Azure Identity Authentication for Azure DevOps MCP Server
  2 | 
  3 | This document outlines the implementation approach for adding Azure Identity authentication support to the Azure DevOps MCP Server.
  4 | 
  5 | ## Overview
  6 | 
  7 | 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.
  8 | 
  9 | ## Azure Identity SDK
 10 | 
 11 | The `@azure/identity` package provides various credential types for authenticating with Azure services. For our implementation, we will focus on the following credential types:
 12 | 
 13 | ### DefaultAzureCredential
 14 | 
 15 | `DefaultAzureCredential` provides a simplified authentication experience by trying multiple credential types in sequence:
 16 | 
 17 | 1. Environment variables (EnvironmentCredential)
 18 | 2. Managed Identity (ManagedIdentityCredential)
 19 | 3. Azure CLI (AzureCliCredential)
 20 | 4. Visual Studio Code (VisualStudioCodeCredential)
 21 | 5. Azure PowerShell (AzurePowerShellCredential)
 22 | 6. Interactive Browser (InteractiveBrowserCredential) - optional, disabled by default
 23 | 
 24 | This makes it ideal for applications that need to work in different environments (local development, Azure-hosted) without code changes.
 25 | 
 26 | ### AzureCliCredential
 27 | 
 28 | `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.
 29 | 
 30 | ## Implementation Approach
 31 | 
 32 | ### 1. Authentication Abstraction Layer
 33 | 
 34 | Create an abstraction layer for authentication that supports both PAT and Azure Identity methods:
 35 | 
 36 | ```typescript
 37 | // src/api/auth.ts
 38 | export interface AuthProvider {
 39 |   getConnection(): Promise<WebApi>;
 40 |   isAuthenticated(): Promise<boolean>;
 41 | }
 42 | 
 43 | export class PatAuthProvider implements AuthProvider {
 44 |   // Existing PAT authentication implementation
 45 | }
 46 | 
 47 | export class AzureIdentityAuthProvider implements AuthProvider {
 48 |   // New Azure Identity authentication implementation
 49 | }
 50 | ```
 51 | 
 52 | ### 2. Authentication Factory
 53 | 
 54 | Implement a factory pattern to create the appropriate authentication provider based on configuration:
 55 | 
 56 | ```typescript
 57 | // src/api/auth.ts
 58 | export enum AuthMethod {
 59 |   PAT = 'pat',
 60 |   AZURE_IDENTITY = 'azure-identity',
 61 | }
 62 | 
 63 | export function createAuthProvider(config: AzureDevOpsConfig): AuthProvider {
 64 |   switch (config.authMethod) {
 65 |     case AuthMethod.AZURE_IDENTITY:
 66 |       return new AzureIdentityAuthProvider(config);
 67 |     case AuthMethod.PAT:
 68 |     default:
 69 |       return new PatAuthProvider(config);
 70 |   }
 71 | }
 72 | ```
 73 | 
 74 | ### 3. Azure Identity Authentication Provider
 75 | 
 76 | Implement the Azure Identity authentication provider:
 77 | 
 78 | ```typescript
 79 | // src/api/auth.ts
 80 | export class AzureIdentityAuthProvider implements AuthProvider {
 81 |   private config: AzureDevOpsConfig;
 82 |   private connectionPromise: Promise<WebApi> | null = null;
 83 | 
 84 |   constructor(config: AzureDevOpsConfig) {
 85 |     this.config = config;
 86 |   }
 87 | 
 88 |   async getConnection(): Promise<WebApi> {
 89 |     if (!this.connectionPromise) {
 90 |       this.connectionPromise = this.createConnection();
 91 |     }
 92 |     return this.connectionPromise;
 93 |   }
 94 | 
 95 |   private async createConnection(): Promise<WebApi> {
 96 |     try {
 97 |       // Azure DevOps resource ID for token scope
 98 |       const azureDevOpsResourceId = '499b84ac-1321-427f-aa17-267ca6975798';
 99 | 
100 |       // Create credential based on configuration
101 |       const credential = this.createCredential();
102 | 
103 |       // Get token for Azure DevOps
104 |       const token = await credential.getToken(
105 |         `${azureDevOpsResourceId}/.default`,
106 |       );
107 | 
108 |       if (!token) {
109 |         throw new AzureDevOpsAuthenticationError(
110 |           'Failed to acquire token from Azure Identity',
111 |         );
112 |       }
113 | 
114 |       // Create auth handler with token
115 |       const authHandler = new BearerCredentialHandler(token.token);
116 | 
117 |       // Create WebApi client
118 |       const connection = new WebApi(this.config.organizationUrl, authHandler);
119 | 
120 |       // Test the connection
121 |       await connection.getLocationsApi();
122 | 
123 |       return connection;
124 |     } catch (error) {
125 |       throw new AzureDevOpsAuthenticationError(
126 |         `Failed to authenticate with Azure Identity: ${error instanceof Error ? error.message : String(error)}`,
127 |       );
128 |     }
129 |   }
130 | 
131 |   private createCredential(): TokenCredential {
132 |     if (this.config.azureIdentityOptions?.useAzureCliCredential) {
133 |       return new AzureCliCredential();
134 |     }
135 | 
136 |     // Default to DefaultAzureCredential
137 |     return new DefaultAzureCredential();
138 |   }
139 | 
140 |   async isAuthenticated(): Promise<boolean> {
141 |     try {
142 |       await this.getConnection();
143 |       return true;
144 |     } catch {
145 |       return false;
146 |     }
147 |   }
148 | }
149 | ```
150 | 
151 | ### 4. Configuration Updates
152 | 
153 | Update the configuration interface to support specifying the authentication method:
154 | 
155 | ```typescript
156 | // src/types/config.ts
157 | export interface AzureDevOpsConfig {
158 |   // Existing properties
159 |   organizationUrl: string;
160 |   personalAccessToken?: string;
161 |   defaultProject?: string;
162 |   apiVersion?: string;
163 | 
164 |   // New properties
165 |   authMethod?: AuthMethod;
166 |   azureIdentityOptions?: {
167 |     useAzureCliCredential?: boolean;
168 |     // Other Azure Identity options as needed
169 |   };
170 | }
171 | ```
172 | 
173 | ### 5. Environment Variable Updates
174 | 
175 | Update the environment variable handling in `index.ts`:
176 | 
177 | ```typescript
178 | // src/index.ts
179 | const config: AzureDevOpsConfig = {
180 |   organizationUrl: process.env.AZURE_DEVOPS_ORG_URL || '',
181 |   personalAccessToken: process.env.AZURE_DEVOPS_PAT,
182 |   defaultProject: process.env.AZURE_DEVOPS_DEFAULT_PROJECT,
183 |   apiVersion: process.env.AZURE_DEVOPS_API_VERSION,
184 |   authMethod:
185 |     (process.env.AZURE_DEVOPS_AUTH_METHOD as AuthMethod) || AuthMethod.PAT,
186 |   azureIdentityOptions: {
187 |     useAzureCliCredential:
188 |       process.env.AZURE_DEVOPS_USE_CLI_CREDENTIAL === 'true',
189 |   },
190 | };
191 | ```
192 | 
193 | ### 6. Client Updates
194 | 
195 | Update the `AzureDevOpsClient` class to use the authentication provider:
196 | 
197 | ```typescript
198 | // src/api/client.ts
199 | export class AzureDevOpsClient {
200 |   private authProvider: AuthProvider;
201 | 
202 |   constructor(config: AzureDevOpsConfig) {
203 |     this.authProvider = createAuthProvider(config);
204 |   }
205 | 
206 |   private async getClient(): Promise<WebApi> {
207 |     return this.authProvider.getConnection();
208 |   }
209 | 
210 |   // Rest of the class remains the same
211 | }
212 | ```
213 | 
214 | ## Error Handling
215 | 
216 | Implement proper error handling for Azure Identity authentication failures:
217 | 
218 | ```typescript
219 | // src/common/errors.ts
220 | export class AzureIdentityAuthenticationError extends AzureDevOpsAuthenticationError {
221 |   constructor(message: string) {
222 |     super(`Azure Identity Authentication Error: ${message}`);
223 |   }
224 | }
225 | ```
226 | 
227 | ## Configuration Examples
228 | 
229 | ### PAT Authentication
230 | 
231 | ```env
232 | AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-org
233 | AZURE_DEVOPS_PAT=your-pat
234 | AZURE_DEVOPS_AUTH_METHOD=pat
235 | ```
236 | 
237 | ### DefaultAzureCredential Authentication
238 | 
239 | ```env
240 | AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-org
241 | AZURE_DEVOPS_AUTH_METHOD=azure-identity
242 | # Optional environment variables for specific credential types
243 | AZURE_TENANT_ID=your-tenant-id
244 | AZURE_CLIENT_ID=your-client-id
245 | AZURE_CLIENT_SECRET=your-client-secret
246 | ```
247 | 
248 | ### AzureCliCredential Authentication
249 | 
250 | ```env
251 | AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-org
252 | AZURE_DEVOPS_AUTH_METHOD=azure-cli
253 | ``` 
```

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

```typescript
  1 | import { isWorkItemsRequest, handleWorkItemsRequest } from './';
  2 | import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
  3 | import { WebApi } from 'azure-devops-node-api';
  4 | import * as workItemModule from './';
  5 | 
  6 | // Mock the imported modules
  7 | jest.mock('./get-work-item', () => ({
  8 |   getWorkItem: jest.fn(),
  9 | }));
 10 | 
 11 | jest.mock('./list-work-items', () => ({
 12 |   listWorkItems: jest.fn(),
 13 | }));
 14 | 
 15 | jest.mock('./create-work-item', () => ({
 16 |   createWorkItem: jest.fn(),
 17 | }));
 18 | 
 19 | jest.mock('./update-work-item', () => ({
 20 |   updateWorkItem: jest.fn(),
 21 | }));
 22 | 
 23 | jest.mock('./manage-work-item-link', () => ({
 24 |   manageWorkItemLink: jest.fn(),
 25 | }));
 26 | 
 27 | // Helper function to create a valid CallToolRequest object
 28 | const createCallToolRequest = (name: string, args: any): CallToolRequest => {
 29 |   return {
 30 |     method: 'tools/call',
 31 |     params: {
 32 |       name,
 33 |       arguments: args,
 34 |     },
 35 |   } as unknown as CallToolRequest;
 36 | };
 37 | 
 38 | describe('Work Items Request Handlers', () => {
 39 |   describe('isWorkItemsRequest', () => {
 40 |     it('should return true for work items requests', () => {
 41 |       const workItemsRequests = [
 42 |         'get_work_item',
 43 |         'list_work_items',
 44 |         'create_work_item',
 45 |         'update_work_item',
 46 |         'manage_work_item_link',
 47 |       ];
 48 | 
 49 |       workItemsRequests.forEach((name) => {
 50 |         const request = createCallToolRequest(name, {});
 51 | 
 52 |         expect(isWorkItemsRequest(request)).toBe(true);
 53 |       });
 54 |     });
 55 | 
 56 |     it('should return false for non-work items requests', () => {
 57 |       const request = createCallToolRequest('get_project', {});
 58 | 
 59 |       expect(isWorkItemsRequest(request)).toBe(false);
 60 |     });
 61 |   });
 62 | 
 63 |   describe('handleWorkItemsRequest', () => {
 64 |     let mockConnection: WebApi;
 65 | 
 66 |     beforeEach(() => {
 67 |       mockConnection = {} as WebApi;
 68 | 
 69 |       // Setup mock for schema validation - with correct return types
 70 |       jest
 71 |         .spyOn(workItemModule.GetWorkItemSchema, 'parse')
 72 |         .mockImplementation(() => {
 73 |           return { workItemId: 123, expand: undefined };
 74 |         });
 75 | 
 76 |       jest
 77 |         .spyOn(workItemModule.ListWorkItemsSchema, 'parse')
 78 |         .mockImplementation(() => {
 79 |           return { projectId: 'myProject' };
 80 |         });
 81 | 
 82 |       jest
 83 |         .spyOn(workItemModule.CreateWorkItemSchema, 'parse')
 84 |         .mockImplementation(() => {
 85 |           return {
 86 |             projectId: 'myProject',
 87 |             workItemType: 'Task',
 88 |             title: 'New Task',
 89 |           };
 90 |         });
 91 | 
 92 |       jest
 93 |         .spyOn(workItemModule.UpdateWorkItemSchema, 'parse')
 94 |         .mockImplementation(() => {
 95 |           return {
 96 |             workItemId: 123,
 97 |             title: 'Updated Title',
 98 |           };
 99 |         });
100 | 
101 |       jest
102 |         .spyOn(workItemModule.ManageWorkItemLinkSchema, 'parse')
103 |         .mockImplementation(() => {
104 |           return {
105 |             sourceWorkItemId: 123,
106 |             targetWorkItemId: 456,
107 |             operation: 'add' as 'add' | 'remove' | 'update',
108 |             relationType: 'System.LinkTypes.Hierarchy-Forward',
109 |           };
110 |         });
111 | 
112 |       // Setup mocks for feature functions
113 |       jest.spyOn(workItemModule, 'getWorkItem').mockResolvedValue({ id: 123 });
114 |       jest
115 |         .spyOn(workItemModule, 'listWorkItems')
116 |         .mockResolvedValue([{ id: 123 }, { id: 456 }]);
117 |       jest
118 |         .spyOn(workItemModule, 'createWorkItem')
119 |         .mockResolvedValue({ id: 789 });
120 |       jest
121 |         .spyOn(workItemModule, 'updateWorkItem')
122 |         .mockResolvedValue({ id: 123 });
123 |       jest
124 |         .spyOn(workItemModule, 'manageWorkItemLink')
125 |         .mockResolvedValue({ id: 123 });
126 |     });
127 | 
128 |     afterEach(() => {
129 |       jest.resetAllMocks();
130 |     });
131 | 
132 |     it('should handle get_work_item requests', async () => {
133 |       const request = createCallToolRequest('get_work_item', {
134 |         workItemId: 123,
135 |       });
136 | 
137 |       const result = await handleWorkItemsRequest(mockConnection, request);
138 | 
139 |       expect(workItemModule.GetWorkItemSchema.parse).toHaveBeenCalledWith({
140 |         workItemId: 123,
141 |       });
142 |       expect(workItemModule.getWorkItem).toHaveBeenCalledWith(
143 |         mockConnection,
144 |         123,
145 |         undefined,
146 |       );
147 |       expect(result).toEqual({
148 |         content: [{ type: 'text', text: JSON.stringify({ id: 123 }, null, 2) }],
149 |       });
150 |     });
151 | 
152 |     it('should handle list_work_items requests', async () => {
153 |       const request = createCallToolRequest('list_work_items', {
154 |         projectId: 'myProject',
155 |       });
156 | 
157 |       const result = await handleWorkItemsRequest(mockConnection, request);
158 | 
159 |       expect(workItemModule.ListWorkItemsSchema.parse).toHaveBeenCalledWith({
160 |         projectId: 'myProject',
161 |       });
162 |       expect(workItemModule.listWorkItems).toHaveBeenCalled();
163 |       expect(result).toEqual({
164 |         content: [
165 |           {
166 |             type: 'text',
167 |             text: JSON.stringify([{ id: 123 }, { id: 456 }], null, 2),
168 |           },
169 |         ],
170 |       });
171 |     });
172 | 
173 |     it('should handle create_work_item requests', async () => {
174 |       const request = createCallToolRequest('create_work_item', {
175 |         projectId: 'myProject',
176 |         workItemType: 'Task',
177 |         title: 'New Task',
178 |       });
179 | 
180 |       const result = await handleWorkItemsRequest(mockConnection, request);
181 | 
182 |       expect(workItemModule.CreateWorkItemSchema.parse).toHaveBeenCalledWith({
183 |         projectId: 'myProject',
184 |         workItemType: 'Task',
185 |         title: 'New Task',
186 |       });
187 |       expect(workItemModule.createWorkItem).toHaveBeenCalled();
188 |       expect(result).toEqual({
189 |         content: [{ type: 'text', text: JSON.stringify({ id: 789 }, null, 2) }],
190 |       });
191 |     });
192 | 
193 |     it('should handle update_work_item requests', async () => {
194 |       const request = createCallToolRequest('update_work_item', {
195 |         workItemId: 123,
196 |         title: 'Updated Title',
197 |       });
198 | 
199 |       const result = await handleWorkItemsRequest(mockConnection, request);
200 | 
201 |       expect(workItemModule.UpdateWorkItemSchema.parse).toHaveBeenCalledWith({
202 |         workItemId: 123,
203 |         title: 'Updated Title',
204 |       });
205 |       expect(workItemModule.updateWorkItem).toHaveBeenCalled();
206 |       expect(result).toEqual({
207 |         content: [{ type: 'text', text: JSON.stringify({ id: 123 }, null, 2) }],
208 |       });
209 |     });
210 | 
211 |     it('should handle manage_work_item_link requests', async () => {
212 |       const request = createCallToolRequest('manage_work_item_link', {
213 |         sourceWorkItemId: 123,
214 |         targetWorkItemId: 456,
215 |         operation: 'add',
216 |         relationType: 'System.LinkTypes.Hierarchy-Forward',
217 |       });
218 | 
219 |       const result = await handleWorkItemsRequest(mockConnection, request);
220 | 
221 |       expect(
222 |         workItemModule.ManageWorkItemLinkSchema.parse,
223 |       ).toHaveBeenCalledWith({
224 |         sourceWorkItemId: 123,
225 |         targetWorkItemId: 456,
226 |         operation: 'add',
227 |         relationType: 'System.LinkTypes.Hierarchy-Forward',
228 |       });
229 |       expect(workItemModule.manageWorkItemLink).toHaveBeenCalled();
230 |       expect(result).toEqual({
231 |         content: [{ type: 'text', text: JSON.stringify({ id: 123 }, null, 2) }],
232 |       });
233 |     });
234 | 
235 |     it('should throw an error for unknown work items tools', async () => {
236 |       const request = createCallToolRequest('unknown_tool', {});
237 | 
238 |       await expect(
239 |         handleWorkItemsRequest(mockConnection, request),
240 |       ).rejects.toThrow('Unknown work items tool: unknown_tool');
241 |     });
242 |   });
243 | });
244 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { getWorkItem } from './feature';
  3 | import {
  4 |   getTestConnection,
  5 |   shouldSkipIntegrationTest,
  6 | } from '../__test__/test-helpers';
  7 | 
  8 | import { AzureDevOpsResourceNotFoundError } from '../../../shared/errors';
  9 | import { createWorkItem } from '../create-work-item/feature';
 10 | import { manageWorkItemLink } from '../manage-work-item-link/feature';
 11 | import { CreateWorkItemOptions } from '../types';
 12 | 
 13 | describe('getWorkItem integration', () => {
 14 |   let connection: WebApi | null = null;
 15 |   let testWorkItemId: number | null = null;
 16 |   let linkedWorkItemId: number | null = null;
 17 |   let projectName: string;
 18 | 
 19 |   beforeAll(async () => {
 20 |     // Get a real connection using environment variables
 21 |     connection = await getTestConnection();
 22 |     projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
 23 | 
 24 |     // Skip setup if integration tests should be skipped
 25 |     if (shouldSkipIntegrationTest() || !connection) {
 26 |       return;
 27 |     }
 28 | 
 29 |     try {
 30 |       // Create a test work item
 31 |       const uniqueTitle = `Test Work Item ${new Date().toISOString()}`;
 32 |       const options: CreateWorkItemOptions = {
 33 |         title: uniqueTitle,
 34 |         description: 'Test work item for get-work-item integration tests',
 35 |       };
 36 | 
 37 |       const testWorkItem = await createWorkItem(
 38 |         connection,
 39 |         projectName,
 40 |         'Task',
 41 |         options,
 42 |       );
 43 | 
 44 |       // Create another work item to link to the first one
 45 |       const linkedItemOptions: CreateWorkItemOptions = {
 46 |         title: `Linked Work Item ${new Date().toISOString()}`,
 47 |         description: 'Linked work item for get-work-item integration tests',
 48 |       };
 49 | 
 50 |       const linkedWorkItem = await createWorkItem(
 51 |         connection,
 52 |         projectName,
 53 |         'Task',
 54 |         linkedItemOptions,
 55 |       );
 56 | 
 57 |       if (testWorkItem?.id && linkedWorkItem?.id) {
 58 |         testWorkItemId = testWorkItem.id;
 59 |         linkedWorkItemId = linkedWorkItem.id;
 60 | 
 61 |         // Create a link between the two work items
 62 |         await manageWorkItemLink(connection, projectName, {
 63 |           sourceWorkItemId: testWorkItemId,
 64 |           targetWorkItemId: linkedWorkItemId,
 65 |           operation: 'add',
 66 |           relationType: 'System.LinkTypes.Related',
 67 |           comment: 'Link created for get-work-item integration tests',
 68 |         });
 69 |       }
 70 |     } catch (error) {
 71 |       console.error('Failed to create test work items:', error);
 72 |     }
 73 |   });
 74 | 
 75 |   test('should retrieve a real work item from Azure DevOps with default expand=all', async () => {
 76 |     // Skip if no connection is available
 77 |     if (shouldSkipIntegrationTest() || !connection || !testWorkItemId) {
 78 |       return;
 79 |     }
 80 | 
 81 |     // Act - get work item by ID
 82 |     const result = await getWorkItem(connection, testWorkItemId);
 83 | 
 84 |     // Assert
 85 |     expect(result).toBeDefined();
 86 |     expect(result.id).toBe(testWorkItemId);
 87 | 
 88 |     // Verify expanded fields and data are present
 89 |     expect(result.fields).toBeDefined();
 90 |     expect(result._links).toBeDefined();
 91 | 
 92 |     // With expand=all and a linked item, relations should be defined
 93 |     expect(result.relations).toBeDefined();
 94 | 
 95 |     if (result.fields) {
 96 |       // Verify common fields that should be present with expand=all
 97 |       expect(result.fields['System.Title']).toBeDefined();
 98 |       expect(result.fields['System.State']).toBeDefined();
 99 |       expect(result.fields['System.CreatedDate']).toBeDefined();
100 |       expect(result.fields['System.ChangedDate']).toBeDefined();
101 |     }
102 |   });
103 | 
104 |   test('should retrieve work item with expanded relations', async () => {
105 |     // Skip if no connection is available
106 |     if (shouldSkipIntegrationTest() || !connection || !testWorkItemId) {
107 |       return;
108 |     }
109 | 
110 |     // Act - get work item with relations expansion
111 |     const result = await getWorkItem(connection, testWorkItemId, 'relations');
112 | 
113 |     // Assert
114 |     expect(result).toBeDefined();
115 |     expect(result.id).toBe(testWorkItemId);
116 | 
117 |     // When using expand=relations on a work item with links, relations should be defined
118 |     expect(result.relations).toBeDefined();
119 | 
120 |     // Verify we can access the related work item
121 |     if (result.relations && result.relations.length > 0) {
122 |       const relation = result.relations[0];
123 |       expect(relation.rel).toBe('System.LinkTypes.Related');
124 |       expect(relation.url).toContain(linkedWorkItemId?.toString());
125 |     }
126 | 
127 |     // Verify fields exist
128 |     expect(result.fields).toBeDefined();
129 |     if (result.fields) {
130 |       expect(result.fields['System.Title']).toBeDefined();
131 |     }
132 |   });
133 | 
134 |   test('should retrieve work item with minimal fields when using expand=none', async () => {
135 |     if (shouldSkipIntegrationTest() || !connection || !testWorkItemId) {
136 |       return;
137 |     }
138 | 
139 |     // Act - get work item with no expansion
140 |     const result = await getWorkItem(connection, testWorkItemId, 'none');
141 | 
142 |     // Assert
143 |     expect(result).toBeDefined();
144 |     expect(result.id).toBe(testWorkItemId);
145 |     expect(result.fields).toBeDefined();
146 | 
147 |     // With expand=none, we should still get _links but no relations
148 |     // The Azure DevOps API still returns _links even with expand=none
149 |     expect(result.relations).toBeUndefined();
150 |   });
151 | 
152 |   test('should throw AzureDevOpsResourceNotFoundError for non-existent work item', async () => {
153 |     if (shouldSkipIntegrationTest() || !connection) {
154 |       return;
155 |     }
156 | 
157 |     // Use a very large ID that's unlikely to exist
158 |     const nonExistentId = 999999999;
159 | 
160 |     // Assert that it throws the correct error
161 |     await expect(getWorkItem(connection, nonExistentId)).rejects.toThrow(
162 |       AzureDevOpsResourceNotFoundError,
163 |     );
164 |   });
165 | 
166 |   test('should include all possible fields with null values for empty fields', async () => {
167 |     // Skip if no connection is available
168 |     if (shouldSkipIntegrationTest() || !connection || !testWorkItemId) {
169 |       return;
170 |     }
171 | 
172 |     // Act - get work item by ID
173 |     const result = await getWorkItem(connection, testWorkItemId);
174 | 
175 |     // Assert
176 |     expect(result).toBeDefined();
177 |     expect(result.fields).toBeDefined();
178 | 
179 |     if (result.fields) {
180 |       // Get a direct connection to WorkItemTrackingApi to fetch field info for comparison
181 |       const witApi = await connection.getWorkItemTrackingApi();
182 |       const projectName = result.fields['System.TeamProject'];
183 |       const workItemType = result.fields['System.WorkItemType'];
184 | 
185 |       expect(projectName).toBeDefined();
186 |       expect(workItemType).toBeDefined();
187 | 
188 |       if (projectName && workItemType) {
189 |         // Get all possible field references for this work item type
190 |         const allFields = await witApi.getWorkItemTypeFieldsWithReferences(
191 |           projectName.toString(),
192 |           workItemType.toString(),
193 |         );
194 | 
195 |         // Check that all fields from the reference are present in the result
196 |         // Some might be null, but they should exist in the fields object
197 |         for (const field of allFields) {
198 |           if (field.referenceName) {
199 |             expect(Object.keys(result.fields)).toContain(field.referenceName);
200 |           }
201 |         }
202 | 
203 |         // There should be at least one field with a null value
204 |         // (This is a probabilistic test but very likely to pass since work items
205 |         // typically have many optional fields that aren't filled in)
206 |         const hasNullField = Object.values(result.fields).some(
207 |           (value) => value === null,
208 |         );
209 |         expect(hasNullField).toBe(true);
210 |       }
211 |     }
212 |   });
213 | });
214 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
  3 | import { isWikisRequest, handleWikisRequest } from './index';
  4 | import { getWikis, GetWikisSchema } from './get-wikis';
  5 | import { getWikiPage, GetWikiPageSchema } from './get-wiki-page';
  6 | import { createWiki, CreateWikiSchema, WikiType } from './create-wiki';
  7 | import { updateWikiPage, UpdateWikiPageSchema } from './update-wiki-page';
  8 | 
  9 | // Mock the imported modules
 10 | jest.mock('./get-wikis', () => ({
 11 |   getWikis: jest.fn(),
 12 |   GetWikisSchema: {
 13 |     parse: jest.fn(),
 14 |   },
 15 | }));
 16 | 
 17 | jest.mock('./get-wiki-page', () => ({
 18 |   getWikiPage: jest.fn(),
 19 |   GetWikiPageSchema: {
 20 |     parse: jest.fn(),
 21 |   },
 22 | }));
 23 | 
 24 | jest.mock('./create-wiki', () => ({
 25 |   createWiki: jest.fn(),
 26 |   CreateWikiSchema: {
 27 |     parse: jest.fn(),
 28 |   },
 29 |   WikiType: {
 30 |     ProjectWiki: 'projectWiki',
 31 |     CodeWiki: 'codeWiki',
 32 |   },
 33 | }));
 34 | 
 35 | jest.mock('./update-wiki-page', () => ({
 36 |   updateWikiPage: jest.fn(),
 37 |   UpdateWikiPageSchema: {
 38 |     parse: jest.fn(),
 39 |   },
 40 | }));
 41 | 
 42 | describe('Wikis Request Handlers', () => {
 43 |   const mockConnection = {} as WebApi;
 44 | 
 45 |   describe('isWikisRequest', () => {
 46 |     it('should return true for wikis requests', () => {
 47 |       const validTools = [
 48 |         'get_wikis',
 49 |         'get_wiki_page',
 50 |         'create_wiki',
 51 |         'update_wiki_page',
 52 |       ];
 53 |       validTools.forEach((tool) => {
 54 |         const request = {
 55 |           params: { name: tool, arguments: {} },
 56 |           method: 'tools/call',
 57 |         } as CallToolRequest;
 58 |         expect(isWikisRequest(request)).toBe(true);
 59 |       });
 60 |     });
 61 | 
 62 |     it('should return false for non-wikis requests', () => {
 63 |       const request = {
 64 |         params: { name: 'list_projects', arguments: {} },
 65 |         method: 'tools/call',
 66 |       } as CallToolRequest;
 67 |       expect(isWikisRequest(request)).toBe(false);
 68 |     });
 69 |   });
 70 | 
 71 |   describe('handleWikisRequest', () => {
 72 |     it('should handle get_wikis request', async () => {
 73 |       const mockWikis = [
 74 |         { id: 'wiki1', name: 'Wiki 1' },
 75 |         { id: 'wiki2', name: 'Wiki 2' },
 76 |       ];
 77 |       (getWikis as jest.Mock).mockResolvedValue(mockWikis);
 78 | 
 79 |       const request = {
 80 |         params: {
 81 |           name: 'get_wikis',
 82 |           arguments: {
 83 |             projectId: 'project1',
 84 |           },
 85 |         },
 86 |         method: 'tools/call',
 87 |       } as CallToolRequest;
 88 | 
 89 |       // Mock the arguments object after parsing
 90 |       (GetWikisSchema.parse as jest.Mock).mockReturnValue({
 91 |         projectId: 'project1',
 92 |       });
 93 | 
 94 |       const response = await handleWikisRequest(mockConnection, request);
 95 |       expect(response.content).toHaveLength(1);
 96 |       expect(JSON.parse(response.content[0].text as string)).toEqual(mockWikis);
 97 |       expect(getWikis).toHaveBeenCalledWith(
 98 |         mockConnection,
 99 |         expect.objectContaining({
100 |           projectId: 'project1',
101 |         }),
102 |       );
103 |     });
104 | 
105 |     it('should handle get_wiki_page request', async () => {
106 |       const mockWikiContent = '# Wiki Page\n\nThis is a wiki page content.';
107 |       (getWikiPage as jest.Mock).mockResolvedValue(mockWikiContent);
108 | 
109 |       const request = {
110 |         params: {
111 |           name: 'get_wiki_page',
112 |           arguments: {
113 |             projectId: 'project1',
114 |             wikiId: 'wiki1',
115 |             pagePath: '/Home',
116 |           },
117 |         },
118 |         method: 'tools/call',
119 |       } as CallToolRequest;
120 | 
121 |       // Mock the arguments object after parsing
122 |       (GetWikiPageSchema.parse as jest.Mock).mockReturnValue({
123 |         projectId: 'project1',
124 |         wikiId: 'wiki1',
125 |         pagePath: '/Home',
126 |       });
127 | 
128 |       const response = await handleWikisRequest(mockConnection, request);
129 |       expect(response.content).toHaveLength(1);
130 |       expect(response.content[0].text as string).toEqual(mockWikiContent);
131 |       expect(getWikiPage).toHaveBeenCalledWith(
132 |         expect.objectContaining({
133 |           projectId: 'project1',
134 |           wikiId: 'wiki1',
135 |           pagePath: '/Home',
136 |         }),
137 |       );
138 |     });
139 | 
140 |     it('should handle create_wiki request', async () => {
141 |       const mockWiki = { id: 'wiki1', name: 'New Wiki' };
142 |       (createWiki as jest.Mock).mockResolvedValue(mockWiki);
143 | 
144 |       const request = {
145 |         params: {
146 |           name: 'create_wiki',
147 |           arguments: {
148 |             projectId: 'project1',
149 |             name: 'New Wiki',
150 |             type: WikiType.ProjectWiki,
151 |           },
152 |         },
153 |         method: 'tools/call',
154 |       } as CallToolRequest;
155 | 
156 |       // Mock the arguments object after parsing
157 |       (CreateWikiSchema.parse as jest.Mock).mockReturnValue({
158 |         projectId: 'project1',
159 |         name: 'New Wiki',
160 |         type: WikiType.ProjectWiki,
161 |         mappedPath: null, // Required field in the schema
162 |       });
163 | 
164 |       const response = await handleWikisRequest(mockConnection, request);
165 |       expect(response.content).toHaveLength(1);
166 |       expect(JSON.parse(response.content[0].text as string)).toEqual(mockWiki);
167 |       expect(createWiki).toHaveBeenCalledWith(
168 |         mockConnection,
169 |         expect.objectContaining({
170 |           projectId: 'project1',
171 |           name: 'New Wiki',
172 |           type: WikiType.ProjectWiki,
173 |         }),
174 |       );
175 |     });
176 | 
177 |     it('should handle update_wiki_page request', async () => {
178 |       const mockUpdateResult = { id: 'page1', content: 'Updated content' };
179 |       (updateWikiPage as jest.Mock).mockResolvedValue(mockUpdateResult);
180 | 
181 |       const request = {
182 |         params: {
183 |           name: 'update_wiki_page',
184 |           arguments: {
185 |             projectId: 'project1',
186 |             wikiId: 'wiki1',
187 |             pagePath: '/Home',
188 |             content: 'Updated content',
189 |             comment: 'Update home page',
190 |           },
191 |         },
192 |         method: 'tools/call',
193 |       } as CallToolRequest;
194 | 
195 |       // Mock the arguments object after parsing
196 |       (UpdateWikiPageSchema.parse as jest.Mock).mockReturnValue({
197 |         projectId: 'project1',
198 |         wikiId: 'wiki1',
199 |         pagePath: '/Home',
200 |         content: 'Updated content',
201 |         comment: 'Update home page',
202 |       });
203 | 
204 |       const response = await handleWikisRequest(mockConnection, request);
205 |       expect(response.content).toHaveLength(1);
206 |       expect(JSON.parse(response.content[0].text as string)).toEqual(
207 |         mockUpdateResult,
208 |       );
209 |       expect(updateWikiPage).toHaveBeenCalledWith(
210 |         expect.objectContaining({
211 |           projectId: 'project1',
212 |           wikiId: 'wiki1',
213 |           pagePath: '/Home',
214 |           content: 'Updated content',
215 |           comment: 'Update home page',
216 |         }),
217 |       );
218 |     });
219 | 
220 |     it('should throw error for unknown tool', async () => {
221 |       const request = {
222 |         params: {
223 |           name: 'unknown_tool',
224 |           arguments: {},
225 |         },
226 |         method: 'tools/call',
227 |       } as CallToolRequest;
228 | 
229 |       await expect(handleWikisRequest(mockConnection, request)).rejects.toThrow(
230 |         'Unknown wikis tool',
231 |       );
232 |     });
233 | 
234 |     it('should propagate errors from wiki functions', async () => {
235 |       const mockError = new Error('Test error');
236 |       (getWikis as jest.Mock).mockRejectedValue(mockError);
237 | 
238 |       const request = {
239 |         params: {
240 |           name: 'get_wikis',
241 |           arguments: {
242 |             projectId: 'project1',
243 |           },
244 |         },
245 |         method: 'tools/call',
246 |       } as CallToolRequest;
247 | 
248 |       // Mock the arguments object after parsing
249 |       (GetWikisSchema.parse as jest.Mock).mockReturnValue({
250 |         projectId: 'project1',
251 |       });
252 | 
253 |       await expect(handleWikisRequest(mockConnection, request)).rejects.toThrow(
254 |         mockError,
255 |       );
256 |     });
257 |   });
258 | });
259 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
  3 | import { isPullRequestsRequest, handlePullRequestsRequest } from './index';
  4 | import { createPullRequest } from './create-pull-request';
  5 | import { listPullRequests } from './list-pull-requests';
  6 | import { getPullRequestComments } from './get-pull-request-comments';
  7 | import { addPullRequestComment } from './add-pull-request-comment';
  8 | import { AddPullRequestCommentSchema } from './schemas';
  9 | 
 10 | // Mock the imported modules
 11 | jest.mock('./create-pull-request', () => ({
 12 |   createPullRequest: jest.fn(),
 13 | }));
 14 | 
 15 | jest.mock('./list-pull-requests', () => ({
 16 |   listPullRequests: jest.fn(),
 17 | }));
 18 | 
 19 | jest.mock('./get-pull-request-comments', () => ({
 20 |   getPullRequestComments: jest.fn(),
 21 | }));
 22 | 
 23 | jest.mock('./add-pull-request-comment', () => ({
 24 |   addPullRequestComment: jest.fn(),
 25 | }));
 26 | 
 27 | describe('Pull Requests Request Handlers', () => {
 28 |   const mockConnection = {} as WebApi;
 29 | 
 30 |   describe('isPullRequestsRequest', () => {
 31 |     it('should return true for pull requests tools', () => {
 32 |       const validTools = [
 33 |         'create_pull_request',
 34 |         'list_pull_requests',
 35 |         'get_pull_request_comments',
 36 |         'add_pull_request_comment',
 37 |       ];
 38 |       validTools.forEach((tool) => {
 39 |         const request = {
 40 |           params: { name: tool, arguments: {} },
 41 |           method: 'tools/call',
 42 |         } as CallToolRequest;
 43 |         expect(isPullRequestsRequest(request)).toBe(true);
 44 |       });
 45 |     });
 46 | 
 47 |     it('should return false for non-pull requests tools', () => {
 48 |       const request = {
 49 |         params: { name: 'list_projects', arguments: {} },
 50 |         method: 'tools/call',
 51 |       } as CallToolRequest;
 52 |       expect(isPullRequestsRequest(request)).toBe(false);
 53 |     });
 54 |   });
 55 | 
 56 |   describe('handlePullRequestsRequest', () => {
 57 |     it('should handle create_pull_request request', async () => {
 58 |       const mockPullRequest = { id: 1, title: 'Test PR' };
 59 |       (createPullRequest as jest.Mock).mockResolvedValue(mockPullRequest);
 60 | 
 61 |       const request = {
 62 |         params: {
 63 |           name: 'create_pull_request',
 64 |           arguments: {
 65 |             repositoryId: 'test-repo',
 66 |             title: 'Test PR',
 67 |             sourceRefName: 'refs/heads/feature',
 68 |             targetRefName: 'refs/heads/main',
 69 |           },
 70 |         },
 71 |         method: 'tools/call',
 72 |       } as CallToolRequest;
 73 | 
 74 |       const response = await handlePullRequestsRequest(mockConnection, request);
 75 |       expect(response.content).toHaveLength(1);
 76 |       expect(JSON.parse(response.content[0].text as string)).toEqual(
 77 |         mockPullRequest,
 78 |       );
 79 |       expect(createPullRequest).toHaveBeenCalledWith(
 80 |         mockConnection,
 81 |         expect.any(String),
 82 |         'test-repo',
 83 |         expect.objectContaining({
 84 |           title: 'Test PR',
 85 |           sourceRefName: 'refs/heads/feature',
 86 |           targetRefName: 'refs/heads/main',
 87 |         }),
 88 |       );
 89 |     });
 90 | 
 91 |     it('should handle list_pull_requests request', async () => {
 92 |       const mockPullRequests = {
 93 |         count: 2,
 94 |         value: [
 95 |           { id: 1, title: 'PR 1' },
 96 |           { id: 2, title: 'PR 2' },
 97 |         ],
 98 |         hasMoreResults: false,
 99 |       };
100 |       (listPullRequests as jest.Mock).mockResolvedValue(mockPullRequests);
101 | 
102 |       const request = {
103 |         params: {
104 |           name: 'list_pull_requests',
105 |           arguments: {
106 |             repositoryId: 'test-repo',
107 |             status: 'active',
108 |           },
109 |         },
110 |         method: 'tools/call',
111 |       } as CallToolRequest;
112 | 
113 |       const response = await handlePullRequestsRequest(mockConnection, request);
114 |       expect(response.content).toHaveLength(1);
115 |       expect(JSON.parse(response.content[0].text as string)).toEqual(
116 |         mockPullRequests,
117 |       );
118 |       expect(listPullRequests).toHaveBeenCalledWith(
119 |         mockConnection,
120 |         expect.any(String),
121 |         'test-repo',
122 |         expect.objectContaining({
123 |           status: 'active',
124 |         }),
125 |       );
126 |     });
127 | 
128 |     it('should handle get_pull_request_comments request', async () => {
129 |       const mockComments = {
130 |         threads: [
131 |           {
132 |             id: 1,
133 |             comments: [{ id: 1, content: 'Comment 1' }],
134 |           },
135 |         ],
136 |       };
137 |       (getPullRequestComments as jest.Mock).mockResolvedValue(mockComments);
138 | 
139 |       const request = {
140 |         params: {
141 |           name: 'get_pull_request_comments',
142 |           arguments: {
143 |             repositoryId: 'test-repo',
144 |             pullRequestId: 123,
145 |           },
146 |         },
147 |         method: 'tools/call',
148 |       } as CallToolRequest;
149 | 
150 |       const response = await handlePullRequestsRequest(mockConnection, request);
151 |       expect(response.content).toHaveLength(1);
152 |       expect(JSON.parse(response.content[0].text as string)).toEqual(
153 |         mockComments,
154 |       );
155 |       expect(getPullRequestComments).toHaveBeenCalledWith(
156 |         mockConnection,
157 |         expect.any(String),
158 |         'test-repo',
159 |         123,
160 |         expect.objectContaining({
161 |           pullRequestId: 123,
162 |         }),
163 |       );
164 |     });
165 | 
166 |     it('should handle add_pull_request_comment request', async () => {
167 |       const mockResult = {
168 |         comment: { id: 1, content: 'New comment' },
169 |         thread: { id: 1 },
170 |       };
171 |       (addPullRequestComment as jest.Mock).mockResolvedValue(mockResult);
172 | 
173 |       const request = {
174 |         params: {
175 |           name: 'add_pull_request_comment',
176 |           arguments: {
177 |             repositoryId: 'test-repo',
178 |             pullRequestId: 123,
179 |             content: 'New comment',
180 |             status: 'active', // Status is required when creating a new thread
181 |           },
182 |         },
183 |         method: 'tools/call',
184 |       } as CallToolRequest;
185 | 
186 |       // Mock the schema parsing
187 |       const mockParsedArgs = {
188 |         repositoryId: 'test-repo',
189 |         pullRequestId: 123,
190 |         content: 'New comment',
191 |         status: 'active',
192 |       };
193 | 
194 |       // Use a different approach for mocking
195 |       const originalParse = AddPullRequestCommentSchema.parse;
196 |       AddPullRequestCommentSchema.parse = jest
197 |         .fn()
198 |         .mockReturnValue(mockParsedArgs);
199 | 
200 |       const response = await handlePullRequestsRequest(mockConnection, request);
201 |       expect(response.content).toHaveLength(1);
202 |       expect(JSON.parse(response.content[0].text as string)).toEqual(
203 |         mockResult,
204 |       );
205 |       expect(addPullRequestComment).toHaveBeenCalledWith(
206 |         mockConnection,
207 |         expect.any(String),
208 |         'test-repo',
209 |         123,
210 |         expect.objectContaining({
211 |           content: 'New comment',
212 |         }),
213 |       );
214 | 
215 |       // Restore the original parse function
216 |       AddPullRequestCommentSchema.parse = originalParse;
217 |     });
218 | 
219 |     it('should throw error for unknown tool', async () => {
220 |       const request = {
221 |         params: {
222 |           name: 'unknown_tool',
223 |           arguments: {},
224 |         },
225 |         method: 'tools/call',
226 |       } as CallToolRequest;
227 | 
228 |       await expect(
229 |         handlePullRequestsRequest(mockConnection, request),
230 |       ).rejects.toThrow('Unknown pull requests tool');
231 |     });
232 | 
233 |     it('should propagate errors from pull request functions', async () => {
234 |       const mockError = new Error('Test error');
235 |       (listPullRequests as jest.Mock).mockRejectedValue(mockError);
236 | 
237 |       const request = {
238 |         params: {
239 |           name: 'list_pull_requests',
240 |           arguments: {
241 |             repositoryId: 'test-repo',
242 |           },
243 |         },
244 |         method: 'tools/call',
245 |       } as CallToolRequest;
246 | 
247 |       await expect(
248 |         handlePullRequestsRequest(mockConnection, request),
249 |       ).rejects.toThrow(mockError);
250 |     });
251 |   });
252 | });
253 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { addPullRequestComment } from './feature';
  3 | import {
  4 |   Comment,
  5 |   CommentThreadStatus,
  6 |   CommentType,
  7 |   GitPullRequestCommentThread,
  8 | } from 'azure-devops-node-api/interfaces/GitInterfaces';
  9 | 
 10 | describe('addPullRequestComment', () => {
 11 |   afterEach(() => {
 12 |     jest.resetAllMocks();
 13 |   });
 14 | 
 15 |   test('should add a comment to an existing thread successfully', async () => {
 16 |     // Mock data for a new comment
 17 |     const mockComment: Comment = {
 18 |       id: 101,
 19 |       content: 'This is a reply comment',
 20 |       commentType: CommentType.Text,
 21 |       author: {
 22 |         displayName: 'Test User',
 23 |         id: 'test-user-id',
 24 |       },
 25 |       publishedDate: new Date(),
 26 |     };
 27 | 
 28 |     // Setup mock connection
 29 |     const mockGitApi = {
 30 |       createComment: jest.fn().mockResolvedValue(mockComment),
 31 |       createThread: jest.fn(),
 32 |     };
 33 | 
 34 |     const mockConnection: any = {
 35 |       getGitApi: jest.fn().mockResolvedValue(mockGitApi),
 36 |     };
 37 | 
 38 |     // Call the function with test parameters
 39 |     const projectId = 'test-project';
 40 |     const repositoryId = 'test-repo';
 41 |     const pullRequestId = 123;
 42 |     const threadId = 456;
 43 |     const options = {
 44 |       projectId,
 45 |       repositoryId,
 46 |       pullRequestId,
 47 |       threadId,
 48 |       content: 'This is a reply comment',
 49 |     };
 50 | 
 51 |     const result = await addPullRequestComment(
 52 |       mockConnection as WebApi,
 53 |       projectId,
 54 |       repositoryId,
 55 |       pullRequestId,
 56 |       options,
 57 |     );
 58 | 
 59 |     // Verify results (with transformed commentType)
 60 |     expect(result).toEqual({
 61 |       comment: {
 62 |         ...mockComment,
 63 |         commentType: 'text', // Transform enum to string
 64 |       },
 65 |     });
 66 |     expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1);
 67 |     expect(mockGitApi.createComment).toHaveBeenCalledTimes(1);
 68 |     expect(mockGitApi.createComment).toHaveBeenCalledWith(
 69 |       expect.objectContaining({ content: 'This is a reply comment' }),
 70 |       repositoryId,
 71 |       pullRequestId,
 72 |       threadId,
 73 |       projectId,
 74 |     );
 75 |     expect(mockGitApi.createThread).not.toHaveBeenCalled();
 76 |   });
 77 | 
 78 |   test('should create a new thread with a comment successfully', async () => {
 79 |     // Mock data for a new thread with comment
 80 |     const mockComment: Comment = {
 81 |       id: 100,
 82 |       content: 'This is a new comment',
 83 |       commentType: CommentType.Text,
 84 |       author: {
 85 |         displayName: 'Test User',
 86 |         id: 'test-user-id',
 87 |       },
 88 |       publishedDate: new Date(),
 89 |     };
 90 | 
 91 |     const mockThread: GitPullRequestCommentThread = {
 92 |       id: 789,
 93 |       comments: [mockComment],
 94 |       status: CommentThreadStatus.Active,
 95 |     };
 96 | 
 97 |     // Setup mock connection
 98 |     const mockGitApi = {
 99 |       createComment: jest.fn(),
100 |       createThread: jest.fn().mockResolvedValue(mockThread),
101 |     };
102 | 
103 |     const mockConnection: any = {
104 |       getGitApi: jest.fn().mockResolvedValue(mockGitApi),
105 |     };
106 | 
107 |     // Call the function with test parameters
108 |     const projectId = 'test-project';
109 |     const repositoryId = 'test-repo';
110 |     const pullRequestId = 123;
111 |     const options = {
112 |       projectId,
113 |       repositoryId,
114 |       pullRequestId,
115 |       content: 'This is a new comment',
116 |       status: 'active' as const,
117 |     };
118 | 
119 |     const result = await addPullRequestComment(
120 |       mockConnection as WebApi,
121 |       projectId,
122 |       repositoryId,
123 |       pullRequestId,
124 |       options,
125 |     );
126 | 
127 |     // Verify results
128 |     expect(result).toEqual({
129 |       comment: {
130 |         ...mockComment,
131 |         commentType: 'text',
132 |       },
133 |       thread: {
134 |         ...mockThread,
135 |         status: 'active',
136 |         comments: mockThread.comments?.map((comment) => ({
137 |           ...comment,
138 |           commentType: 'text',
139 |         })),
140 |       },
141 |     });
142 |     expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1);
143 |     expect(mockGitApi.createThread).toHaveBeenCalledTimes(1);
144 |     expect(mockGitApi.createThread).toHaveBeenCalledWith(
145 |       expect.objectContaining({
146 |         comments: [
147 |           expect.objectContaining({ content: 'This is a new comment' }),
148 |         ],
149 |         status: CommentThreadStatus.Active,
150 |       }),
151 |       repositoryId,
152 |       pullRequestId,
153 |       projectId,
154 |     );
155 |     expect(mockGitApi.createComment).not.toHaveBeenCalled();
156 |   });
157 | 
158 |   test('should create a new thread on a file with line number', async () => {
159 |     // Mock data for a new thread with comment on file
160 |     const mockComment: Comment = {
161 |       id: 100,
162 |       content: 'This code needs improvement',
163 |       commentType: CommentType.Text,
164 |       author: {
165 |         displayName: 'Test User',
166 |         id: 'test-user-id',
167 |       },
168 |       publishedDate: new Date(),
169 |     };
170 | 
171 |     const mockThread: GitPullRequestCommentThread = {
172 |       id: 789,
173 |       status: CommentThreadStatus.Active, // Add missing status
174 |       comments: [mockComment],
175 |       threadContext: {
176 |         filePath: '/src/app.ts',
177 |         rightFileStart: {
178 |           line: 42,
179 |           offset: 1,
180 |         },
181 |         rightFileEnd: {
182 |           line: 42,
183 |           offset: 1,
184 |         },
185 |       },
186 |     };
187 | 
188 |     // Setup mock connection
189 |     const mockGitApi = {
190 |       createComment: jest.fn(),
191 |       createThread: jest.fn().mockResolvedValue(mockThread),
192 |     };
193 | 
194 |     const mockConnection: any = {
195 |       getGitApi: jest.fn().mockResolvedValue(mockGitApi),
196 |     };
197 | 
198 |     // Call the function with test parameters
199 |     const projectId = 'test-project';
200 |     const repositoryId = 'test-repo';
201 |     const pullRequestId = 123;
202 |     const options = {
203 |       projectId,
204 |       repositoryId,
205 |       pullRequestId,
206 |       content: 'This code needs improvement',
207 |       filePath: '/src/app.ts',
208 |       lineNumber: 42,
209 |     };
210 | 
211 |     const result = await addPullRequestComment(
212 |       mockConnection as WebApi,
213 |       projectId,
214 |       repositoryId,
215 |       pullRequestId,
216 |       options,
217 |     );
218 | 
219 |     // Verify results
220 |     expect(result).toEqual({
221 |       comment: {
222 |         ...mockComment,
223 |         commentType: 'text',
224 |       },
225 |       thread: {
226 |         ...mockThread,
227 |         status: 'active',
228 |         comments: mockThread.comments?.map((comment) => ({
229 |           ...comment,
230 |           commentType: 'text',
231 |         })),
232 |       },
233 |     });
234 |     expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1);
235 |     expect(mockGitApi.createThread).toHaveBeenCalledTimes(1);
236 |     expect(mockGitApi.createThread).toHaveBeenCalledWith(
237 |       expect.objectContaining({
238 |         comments: [
239 |           expect.objectContaining({ content: 'This code needs improvement' }),
240 |         ],
241 |         threadContext: expect.objectContaining({
242 |           filePath: '/src/app.ts',
243 |           rightFileStart: expect.objectContaining({ line: 42 }),
244 |           rightFileEnd: expect.objectContaining({ line: 42 }),
245 |         }),
246 |       }),
247 |       repositoryId,
248 |       pullRequestId,
249 |       projectId,
250 |     );
251 |     expect(mockGitApi.createComment).not.toHaveBeenCalled();
252 |   });
253 | 
254 |   test('should handle error when API call fails', async () => {
255 |     // Setup mock connection with error
256 |     const errorMessage = 'API error';
257 |     const mockGitApi = {
258 |       createComment: jest.fn().mockRejectedValue(new Error(errorMessage)),
259 |       createThread: jest.fn(),
260 |     };
261 | 
262 |     const mockConnection: any = {
263 |       getGitApi: jest.fn().mockResolvedValue(mockGitApi),
264 |     };
265 | 
266 |     // Call the function with test parameters
267 |     const projectId = 'test-project';
268 |     const repositoryId = 'test-repo';
269 |     const pullRequestId = 123;
270 |     const threadId = 456;
271 |     const options = {
272 |       projectId,
273 |       repositoryId,
274 |       pullRequestId,
275 |       threadId,
276 |       content: 'This is a reply comment',
277 |     };
278 | 
279 |     // Verify error handling
280 |     await expect(
281 |       addPullRequestComment(
282 |         mockConnection as WebApi,
283 |         projectId,
284 |         repositoryId,
285 |         pullRequestId,
286 |         options,
287 |       ),
288 |     ).rejects.toThrow(`Failed to add pull request comment: ${errorMessage}`);
289 |   });
290 | });
291 | 
```
Page 4/8FirstPrevNextLast