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

# Directory Structure

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

# Files

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import axios from 'axios';
import { DefaultAzureCredential, AzureCliCredential } from '@azure/identity';
import {
  AzureDevOpsError,
  AzureDevOpsResourceNotFoundError,
  AzureDevOpsValidationError,
  AzureDevOpsPermissionError,
  AzureDevOpsAuthenticationError,
} from '../../../shared/errors';
import {
  SearchCodeOptions,
  CodeSearchRequest,
  CodeSearchResponse,
  CodeSearchResult,
} from '../types';
import { GitVersionType } from 'azure-devops-node-api/interfaces/GitInterfaces';

/**
 * Search for code in Azure DevOps repositories
 *
 * @param connection The Azure DevOps WebApi connection
 * @param options Parameters for searching code
 * @returns Search results with optional file content
 */
export async function searchCode(
  connection: WebApi,
  options: SearchCodeOptions,
): Promise<CodeSearchResponse> {
  try {
    // When includeContent is true, limit results to prevent timeouts
    const top = options.includeContent
      ? Math.min(options.top || 10, 10)
      : options.top;

    // Get the project ID (either provided or default)
    const projectId =
      options.projectId || process.env.AZURE_DEVOPS_DEFAULT_PROJECT;

    if (!projectId) {
      throw new AzureDevOpsValidationError(
        'Project ID is required. Either provide a projectId or set the AZURE_DEVOPS_DEFAULT_PROJECT environment variable.',
      );
    }

    // Prepare the search request
    const searchRequest: CodeSearchRequest = {
      searchText: options.searchText,
      $skip: options.skip,
      $top: top, // Use limited top value when includeContent is true
      filters: {
        Project: [projectId],
        ...(options.filters || {}),
      },
      includeFacets: true,
      includeSnippet: options.includeSnippet,
    };

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

    // Extract organization from the connection URL
    const { organization } = extractOrgFromUrl(connection);

    // Make the search API request with the project ID
    const searchUrl = `https://almsearch.dev.azure.com/${organization}/${projectId}/_apis/search/codesearchresults?api-version=7.1`;

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

    const results = searchResponse.data;

    // If includeContent is true, fetch the content for each result
    if (options.includeContent && results.results.length > 0) {
      await enrichResultsWithContent(connection, results.results);
    }

    return results;
  } catch (error) {
    if (error instanceof AzureDevOpsError) {
      throw error;
    }

    if (axios.isAxiosError(error)) {
      const status = error.response?.status;
      if (status === 404) {
        throw new AzureDevOpsResourceNotFoundError(
          'Repository or project not found',
          { cause: error },
        );
      }
      if (status === 400) {
        throw new AzureDevOpsValidationError(
          'Invalid search parameters',
          error.response?.data,
          { cause: error },
        );
      }
      if (status === 401) {
        throw new AzureDevOpsAuthenticationError('Authentication failed', {
          cause: error,
        });
      }
      if (status === 403) {
        throw new AzureDevOpsPermissionError(
          'Permission denied to access repository',
          { cause: error },
        );
      }
    }

    throw new AzureDevOpsError('Failed to search code', { cause: error });
  }
}

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

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

  return {
    organization,
  };
}

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

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

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

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

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

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

/**
 * Enrich search results with file content
 *
 * @param connection The Azure DevOps WebApi connection
 * @param results The search results to enrich
 */
async function enrichResultsWithContent(
  connection: WebApi,
  results: CodeSearchResult[],
): Promise<void> {
  try {
    const gitApi = await connection.getGitApi();

    // Process each result in parallel
    await Promise.all(
      results.map(async (result) => {
        try {
          // Get the file content using the Git API
          // Pass only the required parameters to avoid the "path" and "scopePath" conflict
          const contentStream = await gitApi.getItemContent(
            result.repository.id,
            result.path,
            result.project.name,
            undefined, // No version descriptor object
            undefined, // No recursion level
            undefined, // Don't include content metadata
            undefined, // No latest processed change
            false, // Don't download
            {
              version: result.versions[0]?.changeId,
              versionType: GitVersionType.Commit,
            }, // Version descriptor
            true, // Include content
          );

          // Convert the stream to a string and store it in the result
          if (contentStream) {
            // Since getItemContent always returns NodeJS.ReadableStream, we need to read the stream
            const chunks: Buffer[] = [];

            // Listen for data events to collect chunks
            contentStream.on('data', (chunk) => {
              chunks.push(Buffer.from(chunk));
            });

            // Use a promise to wait for the stream to finish
            result.content = await new Promise<string>((resolve, reject) => {
              contentStream.on('end', () => {
                // Concatenate all chunks and convert to string
                const buffer = Buffer.concat(chunks);
                resolve(buffer.toString('utf8'));
              });

              contentStream.on('error', (err) => {
                reject(err);
              });
            });
          }
        } catch (error) {
          // Log the error but don't fail the entire operation
          console.error(
            `Failed to fetch content for ${result.path}: ${error instanceof Error ? error.message : String(error)}`,
          );
        }
      }),
    );
  } catch (error) {
    // Log the error but don't fail the entire operation
    console.error(
      `Failed to enrich results with content: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

```

--------------------------------------------------------------------------------
/src/shared/api/client.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import { ICoreApi } from 'azure-devops-node-api/CoreApi';
import { IGitApi } from 'azure-devops-node-api/GitApi';
import { IWorkItemTrackingApi } from 'azure-devops-node-api/WorkItemTrackingApi';
import { IBuildApi } from 'azure-devops-node-api/BuildApi';
import { ITestApi } from 'azure-devops-node-api/TestApi';
import { IReleaseApi } from 'azure-devops-node-api/ReleaseApi';
import { ITaskAgentApi } from 'azure-devops-node-api/TaskAgentApi';
import { ITaskApi } from 'azure-devops-node-api/TaskApi';
import { AzureDevOpsError, AzureDevOpsAuthenticationError } from '../errors';
import { AuthenticationMethod } from '../auth';
import { AzureDevOpsClient as SharedClient } from '../auth/client-factory';

export interface AzureDevOpsClientConfig {
  orgUrl: string;
  pat: string;
}

/**
 * Azure DevOps Client
 *
 * Provides access to Azure DevOps APIs
 */
export class AzureDevOpsClient {
  private config: AzureDevOpsClientConfig;
  private clientPromise: Promise<WebApi> | null = null;

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

  /**
   * Get the authenticated Azure DevOps client
   *
   * @returns The authenticated WebApi client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  private async getClient(): Promise<WebApi> {
    if (!this.clientPromise) {
      this.clientPromise = (async () => {
        try {
          const sharedClient = new SharedClient({
            method: AuthenticationMethod.PersonalAccessToken,
            organizationUrl: this.config.orgUrl,
            personalAccessToken: this.config.pat,
          });
          return await sharedClient.getWebApiClient();
        } catch (error) {
          // If it's already an AzureDevOpsError, rethrow it
          if (error instanceof AzureDevOpsError) {
            throw error;
          }
          // Otherwise, wrap it in an AzureDevOpsAuthenticationError
          throw new AzureDevOpsAuthenticationError(
            error instanceof Error
              ? `Authentication failed: ${error.message}`
              : 'Authentication failed: Unknown error',
          );
        }
      })();
    }
    return this.clientPromise;
  }

  /**
   * Check if the client is authenticated
   *
   * @returns True if the client is authenticated
   */
  public async isAuthenticated(): Promise<boolean> {
    try {
      const client = await this.getClient();
      return !!client;
    } catch {
      // Any error means we're not authenticated
      return false;
    }
  }

  /**
   * Get the Core API
   *
   * @returns The Core API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getCoreApi(): Promise<ICoreApi> {
    try {
      const client = await this.getClient();
      return await client.getCoreApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Core API: ${error.message}`
          : 'Failed to get Core API: Unknown error',
      );
    }
  }

  /**
   * Get the Git API
   *
   * @returns The Git API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getGitApi(): Promise<IGitApi> {
    try {
      const client = await this.getClient();
      return await client.getGitApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Git API: ${error.message}`
          : 'Failed to get Git API: Unknown error',
      );
    }
  }

  /**
   * Get the Work Item Tracking API
   *
   * @returns The Work Item Tracking API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getWorkItemTrackingApi(): Promise<IWorkItemTrackingApi> {
    try {
      const client = await this.getClient();
      return await client.getWorkItemTrackingApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Work Item Tracking API: ${error.message}`
          : 'Failed to get Work Item Tracking API: Unknown error',
      );
    }
  }

  /**
   * Get the Build API
   *
   * @returns The Build API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getBuildApi(): Promise<IBuildApi> {
    try {
      const client = await this.getClient();
      return await client.getBuildApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Build API: ${error.message}`
          : 'Failed to get Build API: Unknown error',
      );
    }
  }

  /**
   * Get the Test API
   *
   * @returns The Test API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getTestApi(): Promise<ITestApi> {
    try {
      const client = await this.getClient();
      return await client.getTestApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Test API: ${error.message}`
          : 'Failed to get Test API: Unknown error',
      );
    }
  }

  /**
   * Get the Release API
   *
   * @returns The Release API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getReleaseApi(): Promise<IReleaseApi> {
    try {
      const client = await this.getClient();
      return await client.getReleaseApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Release API: ${error.message}`
          : 'Failed to get Release API: Unknown error',
      );
    }
  }

  /**
   * Get the Task Agent API
   *
   * @returns The Task Agent API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getTaskAgentApi(): Promise<ITaskAgentApi> {
    try {
      const client = await this.getClient();
      return await client.getTaskAgentApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Task Agent API: ${error.message}`
          : 'Failed to get Task Agent API: Unknown error',
      );
    }
  }

  /**
   * Get the Task API
   *
   * @returns The Task API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getTaskApi(): Promise<ITaskApi> {
    try {
      const client = await this.getClient();
      return await client.getTaskApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Task API: ${error.message}`
          : 'Failed to get Task API: Unknown error',
      );
    }
  }
}

```

--------------------------------------------------------------------------------
/src/server.spec.e2e.ts:
--------------------------------------------------------------------------------

```typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { spawn } from 'child_process';
import { join } from 'path';
import dotenv from 'dotenv';
import { Organization } from './features/organizations/types';
import fs from 'fs';

// Load environment variables from .env file
dotenv.config();

describe('Azure DevOps MCP Server E2E Tests', () => {
  let client: Client;
  let serverProcess: ReturnType<typeof spawn>;
  let transport: StdioClientTransport;
  let tempEnvFile: string | null = null;

  beforeAll(async () => {
    // Debug: Log environment variables
    console.error('E2E TEST ENVIRONMENT VARIABLES:');
    console.error(
      `AZURE_DEVOPS_ORG_URL: ${process.env.AZURE_DEVOPS_ORG_URL || 'NOT SET'}`,
    );
    console.error(
      `AZURE_DEVOPS_PAT: ${process.env.AZURE_DEVOPS_PAT ? 'SET (hidden value)' : 'NOT SET'}`,
    );
    console.error(
      `AZURE_DEVOPS_DEFAULT_PROJECT: ${process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'NOT SET'}`,
    );
    console.error(
      `AZURE_DEVOPS_AUTH_METHOD: ${process.env.AZURE_DEVOPS_AUTH_METHOD || 'NOT SET'}`,
    );

    // Start the MCP server process
    const serverPath = join(process.cwd(), 'dist', 'index.js');

    // Create a temporary .env file for testing if needed
    const orgUrl = process.env.AZURE_DEVOPS_ORG_URL || '';
    const pat = process.env.AZURE_DEVOPS_PAT || '';
    const defaultProject = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || '';
    const authMethod = process.env.AZURE_DEVOPS_AUTH_METHOD || 'pat';

    if (orgUrl) {
      // Create a temporary .env file for the test
      tempEnvFile = join(process.cwd(), '.env.e2e-test');

      const envFileContent = `
AZURE_DEVOPS_ORG_URL=${orgUrl}
AZURE_DEVOPS_PAT=${pat}
AZURE_DEVOPS_DEFAULT_PROJECT=${defaultProject}
AZURE_DEVOPS_AUTH_METHOD=${authMethod}
`;

      fs.writeFileSync(tempEnvFile, envFileContent);
      console.error(`Created temporary .env file at ${tempEnvFile}`);

      // Start server with explicit file path to the temp .env file
      serverProcess = spawn('node', ['-r', 'dotenv/config', serverPath], {
        env: {
          ...process.env,
          NODE_ENV: 'test',
          DOTENV_CONFIG_PATH: tempEnvFile,
        },
      });
    } else {
      throw new Error(
        'Cannot start server: AZURE_DEVOPS_ORG_URL is not set in the environment',
      );
    }

    // Capture server output for debugging
    if (serverProcess && serverProcess.stderr) {
      serverProcess.stderr.on('data', (data) => {
        console.error(`Server error: ${data.toString()}`);
      });
    }

    // Give the server a moment to start
    await new Promise((resolve) => setTimeout(resolve, 1000));

    // Connect the MCP client to the server
    transport = new StdioClientTransport({
      command: 'node',
      args: ['-r', 'dotenv/config', serverPath],
      env: {
        ...process.env,
        NODE_ENV: 'test',
        DOTENV_CONFIG_PATH: tempEnvFile,
      },
    });

    client = new Client(
      {
        name: 'e2e-test-client',
        version: '1.0.0',
      },
      {
        capabilities: {
          tools: {},
        },
      },
    );

    await client.connect(transport);
  });

  afterAll(async () => {
    // Clean up the client transport
    if (transport) {
      await transport.close();
    }

    // Clean up the client
    if (client) {
      await client.close();
    }

    // Clean up the server process
    if (serverProcess) {
      serverProcess.kill();
    }

    // Clean up temporary env file
    if (tempEnvFile && fs.existsSync(tempEnvFile)) {
      fs.unlinkSync(tempEnvFile);
      console.error(`Deleted temporary .env file at ${tempEnvFile}`);
    }

    // Force exit to clean up any remaining handles
    await new Promise<void>((resolve) => {
      setTimeout(() => {
        resolve();
      }, 500);
    });
  });

  describe('Organizations', () => {
    test('should list organizations', async () => {
      // Arrange
      // No specific arrangement needed for this test as we're just listing organizations

      // Act
      const result = await client.callTool({
        name: 'list_organizations',
        arguments: {},
      });

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

      // Access the content safely
      const content = result.content as Array<{ type: string; text: string }>;
      expect(content).toBeDefined();
      expect(content.length).toBeGreaterThan(0);

      // Parse the result content
      const resultText = content[0].text;
      const organizations: Organization[] = JSON.parse(resultText);

      // Verify the response structure
      expect(Array.isArray(organizations)).toBe(true);
      if (organizations.length > 0) {
        const firstOrg = organizations[0];
        expect(firstOrg).toHaveProperty('id');
        expect(firstOrg).toHaveProperty('name');
        expect(firstOrg).toHaveProperty('url');
      }
    });
  });

  describe('Parameterless Tools', () => {
    test('should call list_organizations without arguments', async () => {
      // Act - call the tool without providing arguments
      const result = await client.callTool({
        name: 'list_organizations',
        // No arguments provided
        arguments: {},
      });

      // Assert
      expect(result).toBeDefined();
      const content = result.content as Array<{ type: string; text: string }>;
      expect(content).toBeDefined();
      expect(content.length).toBeGreaterThan(0);

      // Verify we got a valid JSON response
      const resultText = content[0].text;
      const organizations = JSON.parse(resultText);
      expect(Array.isArray(organizations)).toBe(true);
    });

    test('should call get_me without arguments', async () => {
      // Act - call the tool without providing arguments
      const result = await client.callTool({
        name: 'get_me',
        // No arguments provided
        arguments: {},
      });

      // Assert
      expect(result).toBeDefined();
      const content = result.content as Array<{ type: string; text: string }>;
      expect(content).toBeDefined();
      expect(content.length).toBeGreaterThan(0);

      // Verify we got a valid JSON response with user info
      const resultText = content[0].text;
      const userInfo = JSON.parse(resultText);
      expect(userInfo).toHaveProperty('id');
      expect(userInfo).toHaveProperty('displayName');
    });
  });

  describe('Tools with Optional Parameters', () => {
    test('should call list_projects without arguments', async () => {
      // Act - call the tool without providing arguments
      const result = await client.callTool({
        name: 'list_projects',
        // No arguments provided
        arguments: {},
      });

      // Assert
      expect(result).toBeDefined();
      const content = result.content as Array<{ type: string; text: string }>;
      expect(content).toBeDefined();
      expect(content.length).toBeGreaterThan(0);

      // Verify we got a valid JSON response
      const resultText = content[0].text;
      const projects = JSON.parse(resultText);
      expect(Array.isArray(projects)).toBe(true);
    });

    test('should call get_project without arguments', async () => {
      // Act - call the tool without providing arguments
      const result = await client.callTool({
        name: 'get_project',
        // No arguments provided
        arguments: {},
      });

      // Assert
      expect(result).toBeDefined();
      const content = result.content as Array<{ type: string; text: string }>;
      expect(content).toBeDefined();
      expect(content.length).toBeGreaterThan(0);

      // Verify we got a valid JSON response with project info
      const resultText = content[0].text;
      const project = JSON.parse(resultText);
      expect(project).toHaveProperty('id');
      expect(project).toHaveProperty('name');
    });

    test('should call list_repositories without arguments', async () => {
      // Act - call the tool without providing arguments
      const result = await client.callTool({
        name: 'list_repositories',
        // No arguments provided
        arguments: {},
      });

      // Assert
      expect(result).toBeDefined();
      const content = result.content as Array<{ type: string; text: string }>;
      expect(content).toBeDefined();
      expect(content.length).toBeGreaterThan(0);

      // Verify we got a valid JSON response
      const resultText = content[0].text;
      const repositories = JSON.parse(resultText);
      expect(Array.isArray(repositories)).toBe(true);
    });
  });
});

```

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

```typescript
import { getRepositoryDetails } from './feature';
import { GitVersionType } from 'azure-devops-node-api/interfaces/GitInterfaces';
import {
  AzureDevOpsError,
  AzureDevOpsResourceNotFoundError,
} from '../../../shared/errors';
import { GitRepository, GitBranchStats, GitRef } from '../types';

// Unit tests should only focus on isolated logic
// No real connections, HTTP requests, or dependencies
describe('getRepositoryDetails unit', () => {
  // Mock repository data
  const mockRepository: GitRepository = {
    id: 'repo-id',
    name: 'test-repo',
    url: 'https://dev.azure.com/org/project/_apis/git/repositories/repo-id',
    project: {
      id: 'project-id',
      name: 'test-project',
    },
    defaultBranch: 'refs/heads/main',
    size: 1024,
    remoteUrl: 'https://dev.azure.com/org/project/_git/test-repo',
    sshUrl: '[email protected]:v3/org/project/test-repo',
    webUrl: 'https://dev.azure.com/org/project/_git/test-repo',
  };

  // Mock branch stats data
  const mockBranchStats: GitBranchStats[] = [
    {
      name: 'refs/heads/main',
      aheadCount: 0,
      behindCount: 0,
      isBaseVersion: true,
      commit: {
        commitId: 'commit-id',
        author: {
          name: 'Test User',
          email: '[email protected]',
          date: new Date(),
        },
        committer: {
          name: 'Test User',
          email: '[email protected]',
          date: new Date(),
        },
        comment: 'Test commit',
      },
    },
  ];

  // Mock refs data
  const mockRefs: GitRef[] = [
    {
      name: 'refs/heads/main',
      objectId: 'commit-id',
      creator: {
        displayName: 'Test User',
        id: 'user-id',
      },
      url: 'https://dev.azure.com/org/project/_apis/git/repositories/repo-id/refs/heads/main',
    },
  ];

  test('should return basic repository information when no additional options are specified', async () => {
    // Arrange
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => ({
        getRepository: jest.fn().mockResolvedValue(mockRepository),
      })),
    };

    // Act
    const result = await getRepositoryDetails(mockConnection, {
      projectId: 'test-project',
      repositoryId: 'test-repo',
    });

    // Assert
    expect(result).toBeDefined();
    expect(result.repository).toEqual(mockRepository);
    expect(result.statistics).toBeUndefined();
    expect(result.refs).toBeUndefined();
  });

  test('should include branch statistics when includeStatistics is true', async () => {
    // Arrange
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => ({
        getRepository: jest.fn().mockResolvedValue(mockRepository),
        getBranches: jest.fn().mockResolvedValue(mockBranchStats),
      })),
    };

    // Act
    const result = await getRepositoryDetails(mockConnection, {
      projectId: 'test-project',
      repositoryId: 'test-repo',
      includeStatistics: true,
    });

    // Assert
    expect(result).toBeDefined();
    expect(result.repository).toEqual(mockRepository);
    expect(result.statistics).toBeDefined();
    expect(result.statistics?.branches).toEqual(mockBranchStats);
    expect(result.refs).toBeUndefined();
  });

  test('should include refs when includeRefs is true', async () => {
    // Arrange
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => ({
        getRepository: jest.fn().mockResolvedValue(mockRepository),
        getRefs: jest.fn().mockResolvedValue(mockRefs),
      })),
    };

    // Act
    const result = await getRepositoryDetails(mockConnection, {
      projectId: 'test-project',
      repositoryId: 'test-repo',
      includeRefs: true,
    });

    // Assert
    expect(result).toBeDefined();
    expect(result.repository).toEqual(mockRepository);
    expect(result.statistics).toBeUndefined();
    expect(result.refs).toBeDefined();
    expect(result.refs?.value).toEqual(mockRefs);
    expect(result.refs?.count).toBe(mockRefs.length);
  });

  test('should include both statistics and refs when both options are true', async () => {
    // Arrange
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => ({
        getRepository: jest.fn().mockResolvedValue(mockRepository),
        getBranches: jest.fn().mockResolvedValue(mockBranchStats),
        getRefs: jest.fn().mockResolvedValue(mockRefs),
      })),
    };

    // Act
    const result = await getRepositoryDetails(mockConnection, {
      projectId: 'test-project',
      repositoryId: 'test-repo',
      includeStatistics: true,
      includeRefs: true,
    });

    // Assert
    expect(result).toBeDefined();
    expect(result.repository).toEqual(mockRepository);
    expect(result.statistics).toBeDefined();
    expect(result.statistics?.branches).toEqual(mockBranchStats);
    expect(result.refs).toBeDefined();
    expect(result.refs?.value).toEqual(mockRefs);
    expect(result.refs?.count).toBe(mockRefs.length);
  });

  test('should pass refFilter to getRefs when provided', async () => {
    // Arrange
    const getRefs = jest.fn().mockResolvedValue(mockRefs);
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => ({
        getRepository: jest.fn().mockResolvedValue(mockRepository),
        getRefs,
      })),
    };

    // Act
    await getRepositoryDetails(mockConnection, {
      projectId: 'test-project',
      repositoryId: 'test-repo',
      includeRefs: true,
      refFilter: 'heads/',
    });

    // Assert
    expect(getRefs).toHaveBeenCalledWith(
      mockRepository.id,
      'test-project',
      'heads/',
    );
  });

  test('should pass branchName to getBranches when provided', async () => {
    // Arrange
    const getBranches = jest.fn().mockResolvedValue(mockBranchStats);
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => ({
        getRepository: jest.fn().mockResolvedValue(mockRepository),
        getBranches,
      })),
    };

    // Act
    await getRepositoryDetails(mockConnection, {
      projectId: 'test-project',
      repositoryId: 'test-repo',
      includeStatistics: true,
      branchName: 'main',
    });

    // Assert
    expect(getBranches).toHaveBeenCalledWith(
      mockRepository.id,
      'test-project',
      {
        version: 'main',
        versionType: GitVersionType.Branch,
      },
    );
  });

  test('should propagate resource not found errors', async () => {
    // Arrange
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => ({
        getRepository: jest.fn().mockResolvedValue(null), // Simulate repository not found
      })),
    };

    // Act & Assert
    await expect(
      getRepositoryDetails(mockConnection, {
        projectId: 'test-project',
        repositoryId: 'non-existent-repo',
      }),
    ).rejects.toThrow(AzureDevOpsResourceNotFoundError);

    await expect(
      getRepositoryDetails(mockConnection, {
        projectId: 'test-project',
        repositoryId: 'non-existent-repo',
      }),
    ).rejects.toThrow(
      "Repository 'non-existent-repo' not found in project 'test-project'",
    );
  });

  test('should propagate custom errors when thrown internally', async () => {
    // Arrange
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => {
        throw new AzureDevOpsError('Custom error');
      }),
    };

    // Act & Assert
    await expect(
      getRepositoryDetails(mockConnection, {
        projectId: 'test-project',
        repositoryId: 'test-repo',
      }),
    ).rejects.toThrow(AzureDevOpsError);

    await expect(
      getRepositoryDetails(mockConnection, {
        projectId: 'test-project',
        repositoryId: 'test-repo',
      }),
    ).rejects.toThrow('Custom error');
  });

  test('should wrap unexpected errors in a friendly error message', async () => {
    // Arrange
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => {
        throw new Error('Unexpected error');
      }),
    };

    // Act & Assert
    await expect(
      getRepositoryDetails(mockConnection, {
        projectId: 'test-project',
        repositoryId: 'test-repo',
      }),
    ).rejects.toThrow('Failed to get repository details: Unexpected error');
  });

  test('should handle null refs gracefully', async () => {
    // Arrange
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => ({
        getRepository: jest.fn().mockResolvedValue(mockRepository),
        getRefs: jest.fn().mockResolvedValue(null), // Simulate null refs
      })),
    };

    // Act
    const result = await getRepositoryDetails(mockConnection, {
      projectId: 'test-project',
      repositoryId: 'test-repo',
      includeRefs: true,
    });

    // Assert
    expect(result).toBeDefined();
    expect(result.repository).toEqual(mockRepository);
    expect(result.refs).toBeDefined();
    expect(result.refs?.value).toEqual([]);
    expect(result.refs?.count).toBe(0);
  });
});

```

--------------------------------------------------------------------------------
/memory/tasks_memory_2025-05-26T16-18-03.json:
--------------------------------------------------------------------------------

```json
{
  "tasks": [
    {
      "id": "1c881b0f-7fd6-4184-89f5-1676a56e3719",
      "name": "Fix shared type definitions with explicit any warnings",
      "description": "Replace explicit 'any' types in shared type definitions with proper TypeScript types to resolve ESLint warnings. This includes RequestHandler return type and ToolDefinition inputSchema type.",
      "notes": "These are core type definitions used throughout the project, so changes must maintain backward compatibility. The CallToolResult type is the standard MCP SDK return type for tool responses.",
      "status": "completed",
      "dependencies": [],
      "createdAt": "2025-05-26T15:23:09.065Z",
      "updatedAt": "2025-05-26T15:33:17.167Z",
      "relatedFiles": [
        {
          "path": "src/shared/types/request-handler.ts",
          "type": "TO_MODIFY",
          "description": "Contains RequestHandler interface with 'any' return type",
          "lineStart": 14,
          "lineEnd": 16
        },
        {
          "path": "src/shared/types/tool-definition.ts",
          "type": "TO_MODIFY",
          "description": "Contains ToolDefinition interface with 'any' inputSchema type",
          "lineStart": 4,
          "lineEnd": 8
        }
      ],
      "implementationGuide": "1. Update src/shared/types/request-handler.ts line 15: Change 'any' to 'Promise<CallToolResult>' where CallToolResult is imported from '@modelcontextprotocol/sdk/types.js'\n2. Update src/shared/types/tool-definition.ts line 7: Change 'any' to 'JSONSchema7' where JSONSchema7 is imported from 'json-schema'\n3. Add necessary imports at the top of each file\n4. Ensure all existing functionality remains unchanged",
      "verificationCriteria": "1. ESLint warnings for these files should be resolved\n2. TypeScript compilation should succeed\n3. All existing tests should continue to pass\n4. Import statements should be properly added",
      "analysisResult": "Fix lint issues and get unit tests passing in the MCP Azure DevOps server project. The solution addresses 18 TypeScript 'any' type warnings by replacing them with proper types from Azure DevOps Node API and MCP SDK, and resolves 1 failing unit test by handling empty test files. All changes maintain backward compatibility and follow existing project patterns.",
      "summary": "Successfully replaced explicit 'any' types in shared type definitions with proper TypeScript types. Updated RequestHandler return type to use CallToolResult union type for backward compatibility, and ToolDefinition inputSchema to use JsonSchema7Type from zod-to-json-schema. ESLint warnings for these files are resolved, TypeScript compilation succeeds for the core types, and all existing functionality remains unchanged with proper imports added.",
      "completedAt": "2025-05-26T15:33:17.165Z"
    },
    {
      "id": "17aa94fe-24d4-4a8b-a127-ef27e121de38",
      "name": "Fix Azure DevOps client type warnings",
      "description": "Replace 'any' types in Azure DevOps client files with proper types from the azure-devops-node-api library to resolve 7 ESLint warnings in src/clients/azure-devops.ts.",
      "notes": "The azure-devops-node-api library provides comprehensive TypeScript interfaces. Prefer using existing library types over creating custom ones.",
      "status": "completed",
      "dependencies": [
        {
          "taskId": "1c881b0f-7fd6-4184-89f5-1676a56e3719"
        }
      ],
      "createdAt": "2025-05-26T15:23:09.065Z",
      "updatedAt": "2025-05-26T15:39:30.003Z",
      "relatedFiles": [
        {
          "path": "src/clients/azure-devops.ts",
          "type": "TO_MODIFY",
          "description": "Contains 7 'any' type warnings that need proper typing",
          "lineStart": 1,
          "lineEnd": 500
        }
      ],
      "implementationGuide": "1. Examine each 'any' usage in src/clients/azure-devops.ts at lines 78, 158, 244, 305, 345, 453, 484\n2. Replace with appropriate types from azure-devops-node-api interfaces\n3. Common patterns: Use TeamProject, GitRepository, WorkItem, BuildDefinition types\n4. For API responses, use the specific interface types provided by the library\n5. If no specific type exists, create a minimal interface with required properties",
      "verificationCriteria": "1. All 7 ESLint warnings in src/clients/azure-devops.ts should be resolved\n2. TypeScript compilation should succeed\n3. Existing functionality should remain unchanged\n4. Types should be imported from azure-devops-node-api where available",
      "analysisResult": "Fix lint issues and get unit tests passing in the MCP Azure DevOps server project. The solution addresses 18 TypeScript 'any' type warnings by replacing them with proper types from Azure DevOps Node API and MCP SDK, and resolves 1 failing unit test by handling empty test files. All changes maintain backward compatibility and follow existing project patterns.",
      "summary": "Successfully fixed all 7 ESLint warnings in src/clients/azure-devops.ts by replacing 'any' types with proper TypeScript interfaces. Created AzureDevOpsApiErrorResponse interface for Azure DevOps API error responses and replaced Record<string, any> with Record<string, string> for payload objects. All ESLint warnings are now resolved while maintaining existing functionality and backward compatibility.",
      "completedAt": "2025-05-26T15:39:30.002Z"
    },
    {
      "id": "d971e510-94cc-4f12-a1e8-a0ac35d57b7f",
      "name": "Fix feature-specific type warnings",
      "description": "Replace 'any' types in feature modules with proper Azure DevOps API types to resolve remaining ESLint warnings in projects, pull-requests, and repositories features.",
      "notes": "Each feature module should use the most specific Azure DevOps API type available. Check existing working features for type usage patterns.",
      "status": "completed",
      "dependencies": [
        {
          "taskId": "1c881b0f-7fd6-4184-89f5-1676a56e3719"
        }
      ],
      "createdAt": "2025-05-26T15:23:09.065Z",
      "updatedAt": "2025-05-26T15:51:24.788Z",
      "relatedFiles": [
        {
          "path": "src/features/projects/get-project-details/feature.ts",
          "type": "TO_MODIFY",
          "description": "Contains 'any' type warning at line 198"
        },
        {
          "path": "src/features/pull-requests/types.ts",
          "type": "TO_MODIFY",
          "description": "Contains 'any' type warnings at lines 20, 83"
        },
        {
          "path": "src/features/pull-requests/update-pull-request/feature.ts",
          "type": "TO_MODIFY",
          "description": "Contains 'any' type warnings at lines 33, 144, 213, 254"
        },
        {
          "path": "src/features/repositories/get-all-repositories-tree/feature.ts",
          "type": "TO_MODIFY",
          "description": "Contains 'any' type warning at line 231"
        },
        {
          "path": "src/shared/auth/client-factory.ts",
          "type": "TO_MODIFY",
          "description": "Contains 'any' type warning at line 282"
        }
      ],
      "implementationGuide": "1. Fix src/features/projects/get-project-details/feature.ts line 198: Use TeamProject or TeamProjectReference type\n2. Fix src/features/pull-requests/types.ts lines 20, 83: Use GitPullRequest related interfaces\n3. Fix src/features/pull-requests/update-pull-request/feature.ts lines 33, 144, 213, 254: Use GitPullRequest and JsonPatchOperation types\n4. Fix src/features/repositories/get-all-repositories-tree/feature.ts line 231: Use GitTreeRef or GitItem type\n5. Fix src/shared/auth/client-factory.ts line 282: Use proper authentication credential type\n6. Import types from azure-devops-node-api/interfaces/",
      "verificationCriteria": "1. All remaining ESLint 'any' type warnings should be resolved\n2. TypeScript compilation should succeed\n3. All existing tests should continue to pass\n4. Types should be consistent with Azure DevOps API documentation",
      "analysisResult": "Fix lint issues and get unit tests passing in the MCP Azure DevOps server project. The solution addresses 18 TypeScript 'any' type warnings by replacing them with proper types from Azure DevOps Node API and MCP SDK, and resolves 1 failing unit test by handling empty test files. All changes maintain backward compatibility and follow existing project patterns.",
      "summary": "Successfully fixed all feature-specific type warnings by replacing 'any' types with proper Azure DevOps API types. Fixed src/features/projects/get-project-details/feature.ts by using WorkItemTypeField interface, src/features/pull-requests/types.ts by replacing 'any' with specific union types, src/features/pull-requests/update-pull-request/feature.ts by using WebApi, AuthenticationMethod, and WorkItemRelation types, src/features/repositories/get-all-repositories-tree/feature.ts by using IGitApi type, and src/shared/auth/client-factory.ts by using IProfileApi type. All ESLint 'any' type warnings in the specified files have been resolved while maintaining type safety and consistency with Azure DevOps API documentation.",
      "completedAt": "2025-05-26T15:51:24.787Z"
    }
  ]
}
```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import { ICoreApi } from 'azure-devops-node-api/CoreApi';
import { IGitApi } from 'azure-devops-node-api/GitApi';
import { IWorkItemTrackingApi } from 'azure-devops-node-api/WorkItemTrackingApi';
import { IBuildApi } from 'azure-devops-node-api/BuildApi';
import { ITestApi } from 'azure-devops-node-api/TestApi';
import { IReleaseApi } from 'azure-devops-node-api/ReleaseApi';
import { ITaskAgentApi } from 'azure-devops-node-api/TaskAgentApi';
import { ITaskApi } from 'azure-devops-node-api/TaskApi';
import { IProfileApi } from 'azure-devops-node-api/ProfileApi';
import { AzureDevOpsError, AzureDevOpsAuthenticationError } from '../errors';
import { AuthConfig, createAuthClient } from './auth-factory';

/**
 * Azure DevOps Client
 *
 * Provides access to Azure DevOps APIs using the configured authentication method
 */
export class AzureDevOpsClient {
  private config: AuthConfig;
  private clientPromise: Promise<WebApi> | null = null;

  /**
   * Creates a new Azure DevOps client
   *
   * @param config Authentication configuration
   */
  constructor(config: AuthConfig) {
    this.config = config;
  }

  /**
   * Get the authenticated Azure DevOps client
   *
   * @returns The authenticated WebApi client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  private async getClient(): Promise<WebApi> {
    if (!this.clientPromise) {
      this.clientPromise = (async () => {
        try {
          return await createAuthClient(this.config);
        } catch (error) {
          // If it's already an AzureDevOpsError, rethrow it
          if (error instanceof AzureDevOpsError) {
            throw error;
          }
          // Otherwise, wrap it in an AzureDevOpsAuthenticationError
          throw new AzureDevOpsAuthenticationError(
            error instanceof Error
              ? `Authentication failed: ${error.message}`
              : 'Authentication failed: Unknown error',
          );
        }
      })();
    }
    return this.clientPromise;
  }

  /**
   * Get the underlying WebApi client
   *
   * @returns The authenticated WebApi client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getWebApiClient(): Promise<WebApi> {
    return this.getClient();
  }

  /**
   * Check if the client is authenticated
   *
   * @returns True if the client is authenticated
   */
  public async isAuthenticated(): Promise<boolean> {
    try {
      const client = await this.getClient();
      return !!client;
    } catch {
      // Any error means we're not authenticated
      return false;
    }
  }

  /**
   * Get the Core API
   *
   * @returns The Core API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getCoreApi(): Promise<ICoreApi> {
    try {
      const client = await this.getClient();
      return await client.getCoreApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Core API: ${error.message}`
          : 'Failed to get Core API: Unknown error',
      );
    }
  }

  /**
   * Get the Git API
   *
   * @returns The Git API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getGitApi(): Promise<IGitApi> {
    try {
      const client = await this.getClient();
      return await client.getGitApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Git API: ${error.message}`
          : 'Failed to get Git API: Unknown error',
      );
    }
  }

  /**
   * Get the Work Item Tracking API
   *
   * @returns The Work Item Tracking API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getWorkItemTrackingApi(): Promise<IWorkItemTrackingApi> {
    try {
      const client = await this.getClient();
      return await client.getWorkItemTrackingApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Work Item Tracking API: ${error.message}`
          : 'Failed to get Work Item Tracking API: Unknown error',
      );
    }
  }

  /**
   * Get the Build API
   *
   * @returns The Build API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getBuildApi(): Promise<IBuildApi> {
    try {
      const client = await this.getClient();
      return await client.getBuildApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Build API: ${error.message}`
          : 'Failed to get Build API: Unknown error',
      );
    }
  }

  /**
   * Get the Test API
   *
   * @returns The Test API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getTestApi(): Promise<ITestApi> {
    try {
      const client = await this.getClient();
      return await client.getTestApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Test API: ${error.message}`
          : 'Failed to get Test API: Unknown error',
      );
    }
  }

  /**
   * Get the Release API
   *
   * @returns The Release API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getReleaseApi(): Promise<IReleaseApi> {
    try {
      const client = await this.getClient();
      return await client.getReleaseApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Release API: ${error.message}`
          : 'Failed to get Release API: Unknown error',
      );
    }
  }

  /**
   * Get the Task Agent API
   *
   * @returns The Task Agent API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getTaskAgentApi(): Promise<ITaskAgentApi> {
    try {
      const client = await this.getClient();
      return await client.getTaskAgentApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Task Agent API: ${error.message}`
          : 'Failed to get Task Agent API: Unknown error',
      );
    }
  }

  /**
   * Get the Task API
   *
   * @returns The Task API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getTaskApi(): Promise<ITaskApi> {
    try {
      const client = await this.getClient();
      return await client.getTaskApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Task API: ${error.message}`
          : 'Failed to get Task API: Unknown error',
      );
    }
  }

  /**
   * Get the Profile API
   *
   * @returns The Profile API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getProfileApi(): Promise<IProfileApi> {
    try {
      const client = await this.getClient();
      return await client.getProfileApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Profile API: ${error.message}`
          : 'Failed to get Profile API: Unknown error',
      );
    }
  }
}

```

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

```typescript
import { GitPullRequest } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { WebApi } from 'azure-devops-node-api';
import {
  WorkItemRelation,
  WorkItemExpand,
} from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces';
import { AzureDevOpsClient } from '../../../shared/auth/client-factory';
import { AzureDevOpsError } from '../../../shared/errors';
import { UpdatePullRequestOptions } from '../types';
import { AuthenticationMethod } from '../../../shared/auth/auth-factory';
import { pullRequestStatusMapper } from '../../../shared/enums';

/**
 * Updates an existing pull request in Azure DevOps with the specified changes.
 *
 * @param options - The options for updating the pull request
 * @returns The updated pull request
 */
export const updatePullRequest = async (
  options: UpdatePullRequestOptions,
): Promise<GitPullRequest> => {
  const {
    projectId,
    repositoryId,
    pullRequestId,
    title,
    description,
    status,
    isDraft,
    addWorkItemIds,
    removeWorkItemIds,
    addReviewers,
    removeReviewers,
    additionalProperties,
  } = options;

  try {
    // Get connection to Azure DevOps
    const client = new AzureDevOpsClient({
      method:
        (process.env.AZURE_DEVOPS_AUTH_METHOD as AuthenticationMethod) ?? 'pat',
      organizationUrl: process.env.AZURE_DEVOPS_ORG_URL ?? '',
      personalAccessToken: process.env.AZURE_DEVOPS_PAT,
    });
    const connection = await client.getWebApiClient();

    // Get the Git API client
    const gitApi = await connection.getGitApi();

    // First, get the current pull request
    const pullRequest = await gitApi.getPullRequestById(
      pullRequestId,
      projectId,
    );

    if (!pullRequest) {
      throw new AzureDevOpsError(
        `Pull request ${pullRequestId} not found in repository ${repositoryId}`,
      );
    }

    // Store the artifactId for work item linking
    const artifactId = pullRequest.artifactId;

    // Create an object with the properties to update
    const updateObject: Partial<GitPullRequest> = {};

    if (title !== undefined) {
      updateObject.title = title;
    }

    if (description !== undefined) {
      updateObject.description = description;
    }

    if (isDraft !== undefined) {
      updateObject.isDraft = isDraft;
    }

    if (status) {
      const enumStatus = pullRequestStatusMapper.toEnum(status);
      if (enumStatus !== undefined) {
        updateObject.status = enumStatus;
      } else {
        throw new AzureDevOpsError(
          `Invalid status: ${status}. Valid values are: active, abandoned, completed`,
        );
      }
    }

    // Add any additional properties that were specified
    if (additionalProperties) {
      Object.assign(updateObject, additionalProperties);
    }

    // Update the pull request
    const updatedPullRequest = await gitApi.updatePullRequest(
      updateObject,
      repositoryId,
      pullRequestId,
      projectId,
    );

    // Handle work items separately if needed
    const addIds = addWorkItemIds ?? [];
    const removeIds = removeWorkItemIds ?? [];
    if (addIds.length > 0 || removeIds.length > 0) {
      await handleWorkItems({
        connection,
        pullRequestId,
        repositoryId,
        projectId,
        workItemIdsToAdd: addIds,
        workItemIdsToRemove: removeIds,
        artifactId,
      });
    }

    // Handle reviewers separately if needed
    const addReviewerIds = addReviewers ?? [];
    const removeReviewerIds = removeReviewers ?? [];
    if (addReviewerIds.length > 0 || removeReviewerIds.length > 0) {
      await handleReviewers({
        connection,
        pullRequestId,
        repositoryId,
        projectId,
        reviewersToAdd: addReviewerIds,
        reviewersToRemove: removeReviewerIds,
      });
    }

    return updatedPullRequest;
  } catch (error) {
    throw new AzureDevOpsError(
      `Failed to update pull request ${pullRequestId} in repository ${repositoryId}: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
};

/**
 * Handle adding or removing work items from a pull request
 */
interface WorkItemHandlingOptions {
  connection: WebApi;
  pullRequestId: number;
  repositoryId: string;
  projectId?: string;
  workItemIdsToAdd: number[];
  workItemIdsToRemove: number[];
  artifactId?: string;
}

async function handleWorkItems(
  options: WorkItemHandlingOptions,
): Promise<void> {
  const {
    connection,
    pullRequestId,
    repositoryId,
    projectId,
    workItemIdsToAdd,
    workItemIdsToRemove,
    artifactId,
  } = options;

  try {
    // For each work item to add, create a link
    if (workItemIdsToAdd.length > 0) {
      const workItemTrackingApi = await connection.getWorkItemTrackingApi();

      for (const workItemId of workItemIdsToAdd) {
        // Add the relationship between the work item and pull request
        await workItemTrackingApi.updateWorkItem(
          null,
          [
            {
              op: 'add',
              path: '/relations/-',
              value: {
                rel: 'ArtifactLink',
                // Use the artifactId if available, otherwise fall back to the old format
                url:
                  artifactId ||
                  `vstfs:///Git/PullRequestId/${projectId ?? ''}/${repositoryId}/${pullRequestId}`,
                attributes: {
                  name: 'Pull Request',
                },
              },
            },
          ],
          workItemId,
        );
      }
    }

    // For each work item to remove, remove the link
    if (workItemIdsToRemove.length > 0) {
      const workItemTrackingApi = await connection.getWorkItemTrackingApi();

      for (const workItemId of workItemIdsToRemove) {
        try {
          // First, get the work item with relations expanded
          const workItem = await workItemTrackingApi.getWorkItem(
            workItemId,
            undefined, // fields
            undefined, // asOf
            WorkItemExpand.Relations,
          );

          if (workItem.relations) {
            // Find the relationship to the pull request using the artifactId
            const prRelationIndex = workItem.relations.findIndex(
              (rel: WorkItemRelation) =>
                rel.rel === 'ArtifactLink' &&
                rel.attributes &&
                rel.attributes.name === 'Pull Request' &&
                rel.url === artifactId,
            );

            if (prRelationIndex !== -1) {
              // Remove the relationship
              await workItemTrackingApi.updateWorkItem(
                null,
                [
                  {
                    op: 'remove',
                    path: `/relations/${prRelationIndex}`,
                  },
                ],
                workItemId,
              );
            }
          }
        } catch (error) {
          console.log(
            `Error removing work item ${workItemId} from pull request ${pullRequestId}: ${
              error instanceof Error ? error.message : String(error)
            }`,
          );
        }
      }
    }
  } catch (error) {
    throw new AzureDevOpsError(
      `Failed to update work item links for pull request ${pullRequestId}: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

/**
 * Handle adding or removing reviewers from a pull request
 */
interface ReviewerHandlingOptions {
  connection: WebApi;
  pullRequestId: number;
  repositoryId: string;
  projectId?: string;
  reviewersToAdd: string[];
  reviewersToRemove: string[];
}

async function handleReviewers(
  options: ReviewerHandlingOptions,
): Promise<void> {
  const {
    connection,
    pullRequestId,
    repositoryId,
    projectId,
    reviewersToAdd,
    reviewersToRemove,
  } = options;

  try {
    const gitApi = await connection.getGitApi();

    // Add reviewers
    if (reviewersToAdd.length > 0) {
      for (const reviewer of reviewersToAdd) {
        try {
          // Create a reviewer object with the identifier
          await gitApi.createPullRequestReviewer(
            {
              id: reviewer, // This can be email or ID
              isRequired: false,
            },
            repositoryId,
            pullRequestId,
            reviewer,
            projectId,
          );
        } catch (error) {
          console.log(
            `Error adding reviewer ${reviewer} to pull request ${pullRequestId}: ${
              error instanceof Error ? error.message : String(error)
            }`,
          );
        }
      }
    }

    // Remove reviewers
    if (reviewersToRemove.length > 0) {
      for (const reviewer of reviewersToRemove) {
        try {
          await gitApi.deletePullRequestReviewer(
            repositoryId,
            pullRequestId,
            reviewer,
            projectId,
          );
        } catch (error) {
          console.log(
            `Error removing reviewer ${reviewer} from pull request ${pullRequestId}: ${
              error instanceof Error ? error.message : String(error)
            }`,
          );
        }
      }
    }
  } catch (error) {
    throw new AzureDevOpsError(
      `Failed to update reviewers for pull request ${pullRequestId}: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

```

--------------------------------------------------------------------------------
/src/features/pull-requests/get-pull-request-comments/feature.spec.int.ts:
--------------------------------------------------------------------------------

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

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

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

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

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

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

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

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

      pullRequestId = pullRequests.value[0].pullRequestId!;
      console.log(`Using existing pull request #${pullRequestId} for testing`);

      // Create a test comment thread that we can use for specific thread tests
      const result = await addPullRequestComment(
        connection,
        projectName,
        repositoryName,
        pullRequestId,
        {
          projectId: projectName,
          repositoryId: repositoryName,
          pullRequestId,
          content: `Test comment thread ${timestamp}-${randomSuffix}`,
          status: 'active',
        },
      );

      testThreadId = result.thread!.id!;
      console.log(`Created test comment thread #${testThreadId} for testing`);
    } catch (error) {
      console.error('Error in test setup:', error);
      throw error;
    }
  });

  test('should get all comment threads from pull request with file path and line number', async () => {
    // Skip if integration tests should be skipped
    if (shouldSkipIntegrationTest() || !connection) {
      console.log('Skipping test due to missing connection');
      return;
    }

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

    const threads = await getPullRequestComments(
      connection,
      projectName,
      repositoryName,
      pullRequestId,
      {
        projectId: projectName,
        repositoryId: repositoryName,
        pullRequestId,
      },
    );

    // Verify threads were returned
    expect(threads).toBeDefined();
    expect(Array.isArray(threads)).toBe(true);
    expect(threads.length).toBeGreaterThan(0);

    // Verify thread structure
    const firstThread = threads[0];
    expect(firstThread.id).toBeDefined();
    expect(firstThread.comments).toBeDefined();
    expect(Array.isArray(firstThread.comments)).toBe(true);
    expect(firstThread.comments!.length).toBeGreaterThan(0);

    // Verify comment structure including new fields
    const firstComment = firstThread.comments![0];
    expect(firstComment.content).toBeDefined();
    expect(firstComment.id).toBeDefined();
    expect(firstComment.publishedDate).toBeDefined();
    expect(firstComment.author).toBeDefined();

    // Verify new fields are present (may be undefined/null for general comments)
    expect(firstComment).toHaveProperty('filePath');
    expect(firstComment).toHaveProperty('rightFileStart');
    expect(firstComment).toHaveProperty('rightFileEnd');
    expect(firstComment).toHaveProperty('leftFileStart');
    expect(firstComment).toHaveProperty('leftFileEnd');
  }, 30000);

  test('should get a specific comment thread by ID with file path and line number', async () => {
    // Skip if integration tests should be skipped
    if (shouldSkipIntegrationTest() || !connection) {
      console.log('Skipping test due to missing connection');
      return;
    }

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

    const threads = await getPullRequestComments(
      connection,
      projectName,
      repositoryName,
      pullRequestId,
      {
        projectId: projectName,
        repositoryId: repositoryName,
        pullRequestId,
        threadId: testThreadId,
      },
    );

    // Verify only one thread was returned
    expect(threads).toBeDefined();
    expect(Array.isArray(threads)).toBe(true);
    expect(threads.length).toBe(1);

    // Verify it's the correct thread
    const thread = threads[0];
    expect(thread.id).toBe(testThreadId);
    expect(thread.comments).toBeDefined();
    expect(Array.isArray(thread.comments)).toBe(true);
    expect(thread.comments!.length).toBeGreaterThan(0);

    // Verify the comment content matches what we created
    const comment = thread.comments![0];
    expect(comment.content).toBe(
      `Test comment thread ${timestamp}-${randomSuffix}`,
    );

    // Verify new fields are present (may be undefined/null for general comments)
    expect(comment).toHaveProperty('filePath');
    expect(comment).toHaveProperty('rightFileStart');
    expect(comment).toHaveProperty('rightFileEnd');
    expect(comment).toHaveProperty('leftFileStart');
    expect(comment).toHaveProperty('leftFileEnd');
  }, 30000);

  test('should handle pagination with top parameter', async () => {
    // Skip if integration tests should be skipped
    if (shouldSkipIntegrationTest() || !connection) {
      console.log('Skipping test due to missing connection');
      return;
    }

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

    // Get all threads first to compare
    const allThreads = await getPullRequestComments(
      connection,
      projectName,
      repositoryName,
      pullRequestId,
      {
        projectId: projectName,
        repositoryId: repositoryName,
        pullRequestId,
      },
    );

    // Then get with pagination
    const paginatedThreads = await getPullRequestComments(
      connection,
      projectName,
      repositoryName,
      pullRequestId,
      {
        projectId: projectName,
        repositoryId: repositoryName,
        pullRequestId,
        top: 1,
      },
    );

    // Verify pagination
    expect(paginatedThreads).toBeDefined();
    expect(Array.isArray(paginatedThreads)).toBe(true);
    expect(paginatedThreads.length).toBe(1);
    expect(paginatedThreads.length).toBeLessThanOrEqual(allThreads.length);

    // Verify the thread structure is the same
    const thread = paginatedThreads[0];
    expect(thread.id).toBeDefined();
    expect(thread.comments).toBeDefined();
    expect(Array.isArray(thread.comments)).toBe(true);
    expect(thread.comments!.length).toBeGreaterThan(0);

    // Verify new fields are present in paginated results
    const comment = thread.comments![0];
    expect(comment).toHaveProperty('filePath');
    expect(comment).toHaveProperty('rightFileStart');
    expect(comment).toHaveProperty('rightFileEnd');
    expect(comment).toHaveProperty('leftFileStart');
    expect(comment).toHaveProperty('leftFileEnd');
  }, 30000);

  test('should handle includeDeleted parameter', async () => {
    // Skip if integration tests should be skipped
    if (shouldSkipIntegrationTest() || !connection) {
      console.log('Skipping test due to missing connection');
      return;
    }

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

    const threads = await getPullRequestComments(
      connection,
      projectName,
      repositoryName,
      pullRequestId,
      {
        projectId: projectName,
        repositoryId: repositoryName,
        pullRequestId,
        includeDeleted: true,
      },
    );

    // We can only verify the call succeeds, as we can't guarantee deleted comments exist
    expect(threads).toBeDefined();
    expect(Array.isArray(threads)).toBe(true);

    // If there are any threads, verify they have the new fields
    if (threads.length > 0) {
      const thread = threads[0];
      if (thread.comments && thread.comments.length > 0) {
        const comment = thread.comments[0];
        expect(comment).toHaveProperty('filePath');
        expect(comment).toHaveProperty('rightFileStart');
        expect(comment).toHaveProperty('rightFileEnd');
        expect(comment).toHaveProperty('leftFileStart');
        expect(comment).toHaveProperty('leftFileEnd');
      }
    }
  }, 30000); // 30 second timeout for integration test
});

```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import {
  AzureDevOpsResourceNotFoundError,
  AzureDevOpsError,
} from '../../../shared/errors';
import {
  TeamProject,
  WebApiTeam,
} from 'azure-devops-node-api/interfaces/CoreInterfaces';
import { WorkItemField } from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces';

// Type for work item type field with additional properties
interface WorkItemTypeField extends WorkItemField {
  isRequired?: boolean;
  isIdentity?: boolean;
  isPicklist?: boolean;
}

/**
 * Options for getting project details
 */
export interface GetProjectDetailsOptions {
  projectId: string;
  includeProcess?: boolean;
  includeWorkItemTypes?: boolean;
  includeFields?: boolean;
  includeTeams?: boolean;
  expandTeamIdentity?: boolean;
}

/**
 * Process information with work item types
 */
interface ProcessInfo {
  id: string;
  name: string;
  description?: string;
  isDefault: boolean;
  type: string;
  workItemTypes?: WorkItemTypeInfo[];
  hierarchyInfo?: {
    portfolioBacklogs?: {
      name: string;
      workItemTypes: string[];
    }[];
    requirementBacklog?: {
      name: string;
      workItemTypes: string[];
    };
    taskBacklog?: {
      name: string;
      workItemTypes: string[];
    };
  };
}

/**
 * Work item type information with states and fields
 */
interface WorkItemTypeInfo {
  name: string;
  referenceName: string;
  description?: string;
  isDisabled: boolean;
  states?: {
    name: string;
    color?: string;
    stateCategory: string;
  }[];
  fields?: {
    name: string;
    referenceName: string;
    type: string;
    required?: boolean;
    isIdentity?: boolean;
    isPicklist?: boolean;
    description?: string;
  }[];
}

/**
 * Project details response
 */
interface ProjectDetails extends TeamProject {
  process?: ProcessInfo;
  teams?: WebApiTeam[];
}

/**
 * Get detailed information about a project
 *
 * @param connection The Azure DevOps WebApi connection
 * @param options Options for getting project details
 * @returns The project details
 * @throws {AzureDevOpsResourceNotFoundError} If the project is not found
 */
export async function getProjectDetails(
  connection: WebApi,
  options: GetProjectDetailsOptions,
): Promise<ProjectDetails> {
  try {
    const {
      projectId,
      includeProcess = false,
      includeWorkItemTypes = false,
      includeFields = false,
      includeTeams = false,
      expandTeamIdentity = false,
    } = options;

    // Get the core API
    const coreApi = await connection.getCoreApi();

    // Get the basic project information
    const project = await coreApi.getProject(projectId);

    if (!project) {
      throw new AzureDevOpsResourceNotFoundError(
        `Project '${projectId}' not found`,
      );
    }

    // Initialize the result with the project information and ensure required properties
    const result: ProjectDetails = {
      ...project,
      // Ensure capabilities is always defined
      capabilities: project.capabilities || {
        versioncontrol: { sourceControlType: 'Git' },
        processTemplate: { templateName: 'Unknown', templateTypeId: 'unknown' },
      },
    };

    // If teams are requested, get them
    if (includeTeams) {
      const teams = await coreApi.getTeams(projectId, expandTeamIdentity);
      result.teams = teams;
    }

    // If process information is requested, get it
    if (includeProcess) {
      // Get the process template ID from the project capabilities
      const processTemplateId =
        project.capabilities?.processTemplate?.templateTypeId || 'unknown';

      // Always create a process object, even if we don't have a template ID
      // In a real implementation, we would use the Process API
      // Since it's not directly available in the WebApi type, we'll simulate it
      // This is a simplified version for the implementation
      // In a real implementation, you would need to use the appropriate API

      // Create the process info object directly
      const processInfo: ProcessInfo = {
        id: processTemplateId,
        name: project.capabilities?.processTemplate?.templateName || 'Unknown',
        description: 'Process template for the project',
        isDefault: true,
        type: 'system',
      };

      // If work item types are requested, get them
      if (includeWorkItemTypes) {
        // In a real implementation, we would get work item types from the API
        // For now, we'll use the work item tracking API to get basic types
        const workItemTrackingApi = await connection.getWorkItemTrackingApi();
        const workItemTypes =
          await workItemTrackingApi.getWorkItemTypes(projectId);

        // Map the work item types to our format
        const processWorkItemTypes: WorkItemTypeInfo[] = workItemTypes.map(
          (wit) => {
            // Create the work item type info object
            const workItemTypeInfo: WorkItemTypeInfo = {
              name: wit.name || 'Unknown',
              referenceName:
                wit.referenceName || `System.Unknown.${Date.now()}`,
              description: wit.description,
              isDisabled: false,
              states: [
                { name: 'New', stateCategory: 'Proposed' },
                { name: 'Active', stateCategory: 'InProgress' },
                { name: 'Resolved', stateCategory: 'InProgress' },
                { name: 'Closed', stateCategory: 'Completed' },
              ],
            };

            // If fields are requested, don't add fields here - we'll add them after fetching from API
            return workItemTypeInfo;
          },
        );

        // If fields are requested, get the field definitions from the API
        if (includeFields) {
          try {
            // Instead of getting all fields and applying them to all work item types,
            // let's get the fields specific to each work item type
            for (const wit of processWorkItemTypes) {
              try {
                // Get fields specific to this work item type using the specialized method
                const typeSpecificFields =
                  await workItemTrackingApi.getWorkItemTypeFieldsWithReferences(
                    projectId,
                    wit.name,
                  );

                // Map the fields to our format
                wit.fields = typeSpecificFields.map(
                  (field: WorkItemTypeField) => ({
                    name: field.name || 'Unknown',
                    referenceName: field.referenceName || 'Unknown',
                    type: field.type?.toString().toLowerCase() || 'string',
                    required: field.isRequired || false,
                    isIdentity: field.isIdentity || false,
                    isPicklist: field.isPicklist || false,
                    description: field.description,
                  }),
                );
              } catch (typeFieldError) {
                console.error(
                  `Error fetching fields for work item type ${wit.name}:`,
                  typeFieldError,
                );

                // Fallback to basic fields
                wit.fields = [
                  {
                    name: 'Title',
                    referenceName: 'System.Title',
                    type: 'string',
                    required: true,
                  },
                  {
                    name: 'Description',
                    referenceName: 'System.Description',
                    type: 'html',
                    required: false,
                  },
                ];
              }
            }
          } catch (fieldError) {
            console.error('Error in field processing:', fieldError);

            // Fallback to default fields if API call fails
            processWorkItemTypes.forEach((wit) => {
              wit.fields = [
                {
                  name: 'Title',
                  referenceName: 'System.Title',
                  type: 'string',
                  required: true,
                },
                {
                  name: 'Description',
                  referenceName: 'System.Description',
                  type: 'html',
                  required: false,
                },
              ];
            });
          }
        }

        processInfo.workItemTypes = processWorkItemTypes;

        // Add hierarchy information if available
        // This is a simplified version - in a real implementation, you would
        // need to get the backlog configuration and map it to the work item types
        processInfo.hierarchyInfo = {
          portfolioBacklogs: [
            {
              name: 'Epics',
              workItemTypes: processWorkItemTypes
                .filter(
                  (wit: WorkItemTypeInfo) => wit.name.toLowerCase() === 'epic',
                )
                .map((wit: WorkItemTypeInfo) => wit.name),
            },
            {
              name: 'Features',
              workItemTypes: processWorkItemTypes
                .filter(
                  (wit: WorkItemTypeInfo) =>
                    wit.name.toLowerCase() === 'feature',
                )
                .map((wit: WorkItemTypeInfo) => wit.name),
            },
          ],
          requirementBacklog: {
            name: 'Stories',
            workItemTypes: processWorkItemTypes
              .filter(
                (wit: WorkItemTypeInfo) =>
                  wit.name.toLowerCase() === 'user story' ||
                  wit.name.toLowerCase() === 'bug',
              )
              .map((wit: WorkItemTypeInfo) => wit.name),
          },
          taskBacklog: {
            name: 'Tasks',
            workItemTypes: processWorkItemTypes
              .filter(
                (wit: WorkItemTypeInfo) => wit.name.toLowerCase() === 'task',
              )
              .map((wit: WorkItemTypeInfo) => wit.name),
          },
        };
      }

      // Always set the process on the result
      result.process = processInfo;
    }

    return result;
  } catch (error) {
    if (error instanceof AzureDevOpsError) {
      throw error;
    }
    throw new Error(
      `Failed to get project details: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

```

--------------------------------------------------------------------------------
/docs/authentication.md:
--------------------------------------------------------------------------------

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

This guide provides detailed information about the authentication methods supported by the Azure DevOps MCP Server, including setup instructions, configuration examples, and troubleshooting tips.

## Supported Authentication Methods

The Azure DevOps MCP Server supports three authentication methods:

1. **Personal Access Token (PAT)** - Simple token-based authentication
2. **Azure Identity (DefaultAzureCredential)** - Flexible authentication using the Azure Identity SDK
3. **Azure CLI** - Authentication using your Azure CLI login

## Method 1: Personal Access Token (PAT) Authentication

PAT authentication is the simplest method and works well for personal use or testing.

### Setup Instructions

1. **Generate a PAT in Azure DevOps**:

   - Go to https://dev.azure.com/{your-organization}/_usersSettings/tokens
   - Or click on your profile picture > Personal access tokens
   - Select "+ New Token"
   - Name your token (e.g., "MCP Server Access")
   - Set an expiration date
   - Select the following scopes:
     - **Code**: Read & Write
     - **Work Items**: Read & Write
     - **Build**: Read & Execute
     - **Project and Team**: Read
     - **Graph**: Read
     - **Release**: Read & Execute
   - Click "Create" and copy the generated token

2. **Configure your `.env` file**:
   ```
   AZURE_DEVOPS_AUTH_METHOD=pat
   AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
   AZURE_DEVOPS_PAT=your-personal-access-token
   AZURE_DEVOPS_DEFAULT_PROJECT=your-default-project
   ```

### Security Considerations

- PATs have an expiration date and will need to be renewed
- Store your PAT securely and never commit it to source control
- Consider using environment variables or a secrets manager in production
- Scope your PAT to only the permissions needed for your use case

## Method 2: Azure Identity Authentication (DefaultAzureCredential)

Azure Identity authentication uses the `DefaultAzureCredential` class from the `@azure/identity` package, which provides a simplified authentication experience by trying multiple credential types in sequence.

### How DefaultAzureCredential Works

`DefaultAzureCredential` tries the following credential types in order:

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

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

### Setup Instructions

1. **Install the Azure Identity SDK**:
   The SDK is already included as a dependency in the Azure DevOps MCP Server.

2. **Configure your `.env` file**:

   ```
   AZURE_DEVOPS_AUTH_METHOD=azure-identity
   AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
   AZURE_DEVOPS_DEFAULT_PROJECT=your-default-project
   ```

3. **Set up credentials based on your environment**:

   a. **For service principals (client credentials)**:

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

   b. **For managed identities in Azure**:
   No additional configuration needed if running in Azure with a managed identity.

   c. **For local development**:

   - Log in with Azure CLI: `az login`
   - Or use Visual Studio Code Azure Account extension

### Security Considerations

- Use managed identities in Azure for improved security
- For service principals, rotate client secrets regularly
- Store credentials securely using Azure Key Vault or environment variables
- Apply the principle of least privilege when assigning roles

## Method 3: Azure CLI Authentication

Azure CLI authentication uses the `AzureCliCredential` class from the `@azure/identity` package, which authenticates using the Azure CLI's logged-in account.

### Setup Instructions

1. **Install the Azure CLI**:

   - Follow the instructions at https://docs.microsoft.com/cli/azure/install-azure-cli

2. **Log in to Azure**:

   ```bash
   az login
   ```

3. **Configure your `.env` file**:
   ```
   AZURE_DEVOPS_AUTH_METHOD=azure-cli
   AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
   AZURE_DEVOPS_DEFAULT_PROJECT=your-default-project
   ```

### Security Considerations

- Azure CLI authentication is best for local development
- Ensure your Azure CLI session is kept secure
- Log out when not in use: `az logout`

## Configuration Reference

| Variable                       | Description                                                                        | Required                     | Default          |
| ------------------------------ | ---------------------------------------------------------------------------------- | ---------------------------- | ---------------- |
| `AZURE_DEVOPS_AUTH_METHOD`     | Authentication method (`pat`, `azure-identity`, or `azure-cli`) - case-insensitive | No                           | `azure-identity` |
| `AZURE_DEVOPS_ORG_URL`         | Full URL to your Azure DevOps organization                                         | Yes                          | -                |
| `AZURE_DEVOPS_PAT`             | Personal Access Token (for PAT auth)                                               | Only with PAT auth           | -                |
| `AZURE_DEVOPS_DEFAULT_PROJECT` | Default project if none specified                                                  | No                           | -                |
| `AZURE_DEVOPS_API_VERSION`     | API version to use                                                                 | No                           | Latest           |
| `AZURE_TENANT_ID`              | Azure AD tenant ID (for service principals)                                        | Only with service principals | -                |
| `AZURE_CLIENT_ID`              | Azure AD application ID (for service principals)                                   | Only with service principals | -                |
| `AZURE_CLIENT_SECRET`          | Azure AD client secret (for service principals)                                    | Only with service principals | -                |
| `LOG_LEVEL`                    | Logging level (debug, info, warn, error)                                           | No                           | info             |

## Troubleshooting Authentication Issues

### PAT Authentication Issues

1. **Invalid PAT**: Ensure your PAT hasn't expired and has the required scopes

   - Error: `TF400813: The user 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' is not authorized to access this resource.`
   - Solution: Generate a new PAT with the correct scopes

2. **Scope issues**: If receiving 403 errors, check if your PAT has the necessary permissions

   - Error: `TF401027: You need the Git 'Read' permission to perform this action.`
   - Solution: Update your PAT with the required scopes

3. **Organization access**: Verify your PAT has access to the organization specified in the URL
   - Error: `TF400813: Resource not found for anonymous request.`
   - Solution: Ensure your PAT has access to the specified organization

### Azure Identity Authentication Issues

1. **Missing credentials**: Ensure you have the necessary credentials configured

   - Error: `CredentialUnavailableError: DefaultAzureCredential failed to retrieve a token`
   - Solution: Check that you're logged in with Azure CLI or have environment variables set

2. **Permission issues**: Verify your identity has the necessary permissions

   - Error: `AuthorizationFailed: The client does not have authorization to perform action`
   - Solution: Assign the appropriate roles to your identity

3. **Token acquisition errors**: Check network connectivity and Azure AD endpoint availability
   - Error: `ClientAuthError: Interaction required`
   - Solution: Check network connectivity or use a different credential type

### Azure CLI Authentication Issues

1. **CLI not installed**: Ensure Azure CLI is installed and in your PATH

   - Error: `AzureCliCredential authentication failed: Azure CLI not found`
   - Solution: Install Azure CLI

2. **Not logged in**: Verify you're logged in to Azure CLI

   - Error: `AzureCliCredential authentication failed: Please run 'az login'`
   - Solution: Run `az login`

3. **Permission issues**: Check if your Azure CLI account has access to Azure DevOps
   - Error: `TF400813: The user is not authorized to access this resource`
   - Solution: Log in with an account that has access to Azure DevOps

## Best Practices

1. **Choose the right authentication method for your environment**:

   - For local development: Azure CLI or PAT
   - For CI/CD pipelines: PAT or service principal
   - For Azure-hosted applications: Managed Identity

2. **Follow the principle of least privilege**:

   - Only grant the permissions needed for your use case
   - Regularly review and rotate credentials

3. **Secure your credentials**:

   - Use environment variables or a secrets manager
   - Never commit credentials to source control
   - Set appropriate expiration dates for PATs

4. **Monitor and audit authentication**:
   - Review Azure DevOps access logs
   - Set up alerts for suspicious activity

## Examples

### Example 1: Local Development with PAT

```bash
# .env file
AZURE_DEVOPS_AUTH_METHOD=pat
AZURE_DEVOPS_ORG_URL=https://dev.azure.com/mycompany
AZURE_DEVOPS_PAT=abcdefghijklmnopqrstuvwxyz0123456789
AZURE_DEVOPS_DEFAULT_PROJECT=MyProject
```

### Example 2: Azure-hosted Application with Managed Identity

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

### Example 3: CI/CD Pipeline with Service Principal

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

### Example 4: Local Development with Azure CLI

```bash
# .env file
AZURE_DEVOPS_AUTH_METHOD=azure-cli
AZURE_DEVOPS_ORG_URL=https://dev.azure.com/mycompany
AZURE_DEVOPS_DEFAULT_PROJECT=MyProject
```

```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import axios from 'axios';
import { searchWorkItems } from './feature';
import {
  AzureDevOpsError,
  AzureDevOpsResourceNotFoundError,
  AzureDevOpsValidationError,
  AzureDevOpsPermissionError,
} from '../../../shared/errors';
import { SearchWorkItemsOptions, WorkItemSearchResponse } from '../types';

// Mock axios
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;

// Mock @azure/identity
jest.mock('@azure/identity', () => ({
  DefaultAzureCredential: jest.fn().mockImplementation(() => ({
    getToken: jest
      .fn()
      .mockResolvedValue({ token: 'mock-azure-identity-token' }),
  })),
  AzureCliCredential: jest.fn(),
}));

// Mock WebApi
jest.mock('azure-devops-node-api');
const MockedWebApi = WebApi as jest.MockedClass<typeof WebApi>;

describe('searchWorkItems', () => {
  let connection: WebApi;
  let options: SearchWorkItemsOptions;
  let mockResponse: WorkItemSearchResponse;

  beforeEach(() => {
    // Reset mocks
    jest.clearAllMocks();

    // Mock environment variables
    process.env.AZURE_DEVOPS_AUTH_METHOD = 'pat';
    process.env.AZURE_DEVOPS_PAT = 'mock-pat';

    // Set up connection mock
    // Create a mock auth handler that implements IRequestHandler
    const mockAuthHandler = {
      prepareRequest: jest.fn(),
      canHandleAuthentication: jest.fn().mockReturnValue(true),
      handleAuthentication: jest.fn(),
    };
    connection = new MockedWebApi(
      'https://dev.azure.com/mock-org',
      mockAuthHandler,
    );
    (connection as any).serverUrl = 'https://dev.azure.com/mock-org';
    (connection.getCoreApi as jest.Mock).mockResolvedValue({
      getProjects: jest.fn().mockResolvedValue([]),
    });

    // Set up options
    options = {
      searchText: 'test query',
      projectId: 'mock-project',
      top: 50,
      skip: 0,
      includeFacets: true,
    };

    // Set up mock response
    mockResponse = {
      count: 2,
      results: [
        {
          project: {
            id: 'project-id-1',
            name: 'mock-project',
          },
          fields: {
            'system.id': '42',
            'system.workitemtype': 'Bug',
            'system.title': 'Test Bug',
            'system.state': 'Active',
            'system.assignedto': 'Test User',
          },
          hits: [
            {
              fieldReferenceName: 'system.title',
              highlights: ['Test <b>Bug</b>'],
            },
          ],
          url: 'https://dev.azure.com/mock-org/mock-project/_workitems/edit/42',
        },
        {
          project: {
            id: 'project-id-1',
            name: 'mock-project',
          },
          fields: {
            'system.id': '43',
            'system.workitemtype': 'Task',
            'system.title': 'Test Task',
            'system.state': 'New',
            'system.assignedto': 'Test User',
          },
          hits: [
            {
              fieldReferenceName: 'system.title',
              highlights: ['Test <b>Task</b>'],
            },
          ],
          url: 'https://dev.azure.com/mock-org/mock-project/_workitems/edit/43',
        },
      ],
      facets: {
        'System.WorkItemType': [
          {
            name: 'Bug',
            id: 'Bug',
            resultCount: 1,
          },
          {
            name: 'Task',
            id: 'Task',
            resultCount: 1,
          },
        ],
      },
    };

    // Mock axios response
    mockedAxios.post.mockResolvedValue({ data: mockResponse });
  });

  afterEach(() => {
    // Clean up environment variables
    delete process.env.AZURE_DEVOPS_AUTH_METHOD;
    delete process.env.AZURE_DEVOPS_PAT;
  });

  it('should search work items with the correct parameters', async () => {
    // Act
    const result = await searchWorkItems(connection, options);

    // Assert
    expect(mockedAxios.post).toHaveBeenCalledWith(
      'https://almsearch.dev.azure.com/mock-org/mock-project/_apis/search/workitemsearchresults?api-version=7.1',
      {
        searchText: 'test query',
        $skip: 0,
        $top: 50,
        filters: {
          'System.TeamProject': ['mock-project'],
        },
        includeFacets: true,
      },
      expect.objectContaining({
        headers: expect.objectContaining({
          Authorization: expect.stringContaining('Basic'),
          'Content-Type': 'application/json',
        }),
      }),
    );
    expect(result).toEqual(mockResponse);
  });

  it('should include filters when provided', async () => {
    // Arrange
    options.filters = {
      'System.WorkItemType': ['Bug', 'Task'],
      'System.State': ['Active'],
    };

    // Act
    await searchWorkItems(connection, options);

    // Assert
    expect(mockedAxios.post).toHaveBeenCalledWith(
      expect.any(String),
      expect.objectContaining({
        filters: {
          'System.TeamProject': ['mock-project'],
          'System.WorkItemType': ['Bug', 'Task'],
          'System.State': ['Active'],
        },
      }),
      expect.any(Object),
    );
  });

  it('should include orderBy when provided', async () => {
    // Arrange
    options.orderBy = [{ field: 'System.CreatedDate', sortOrder: 'ASC' }];

    // Act
    await searchWorkItems(connection, options);

    // Assert
    expect(mockedAxios.post).toHaveBeenCalledWith(
      expect.any(String),
      expect.objectContaining({
        $orderBy: [{ field: 'System.CreatedDate', sortOrder: 'ASC' }],
      }),
      expect.any(Object),
    );
  });

  it('should handle 404 errors correctly', async () => {
    // Arrange - Mock the implementation to throw the specific error
    mockedAxios.post.mockImplementation(() => {
      throw new AzureDevOpsResourceNotFoundError(
        'Resource not found: Project not found',
      );
    });

    // Act & Assert
    await expect(searchWorkItems(connection, options)).rejects.toThrow(
      AzureDevOpsResourceNotFoundError,
    );
  });

  it('should handle 400 errors correctly', async () => {
    // Arrange - Mock the implementation to throw the specific error
    mockedAxios.post.mockImplementation(() => {
      throw new AzureDevOpsValidationError('Invalid request: Invalid query');
    });

    // Act & Assert
    await expect(searchWorkItems(connection, options)).rejects.toThrow(
      AzureDevOpsValidationError,
    );
  });

  it('should handle 401/403 errors correctly', async () => {
    // Arrange - Mock the implementation to throw the specific error
    mockedAxios.post.mockImplementation(() => {
      throw new AzureDevOpsPermissionError(
        'Permission denied: Permission denied',
      );
    });

    // Act & Assert
    await expect(searchWorkItems(connection, options)).rejects.toThrow(
      AzureDevOpsPermissionError,
    );
  });

  it('should handle other axios errors correctly', async () => {
    // Arrange - Mock the implementation to throw the specific error
    mockedAxios.post.mockImplementation(() => {
      throw new AzureDevOpsError(
        'Azure DevOps API error: Internal server error',
      );
    });

    // Act & Assert
    await expect(searchWorkItems(connection, options)).rejects.toThrow(
      AzureDevOpsError,
    );
  });

  it('should handle non-axios errors correctly', async () => {
    // Arrange
    mockedAxios.post.mockRejectedValue(new Error('Network error'));

    // Act & Assert
    await expect(searchWorkItems(connection, options)).rejects.toThrow(
      AzureDevOpsError,
    );
  });

  it('should throw an error if organization cannot be extracted', async () => {
    // Arrange
    (connection as any).serverUrl = 'https://invalid-url';

    // Act & Assert
    await expect(searchWorkItems(connection, options)).rejects.toThrow(
      AzureDevOpsValidationError,
    );
  });

  it('should use Azure Identity authentication when AZURE_DEVOPS_AUTH_METHOD is azure-identity', async () => {
    // Mock environment variables
    const originalEnv = process.env.AZURE_DEVOPS_AUTH_METHOD;
    process.env.AZURE_DEVOPS_AUTH_METHOD = 'azure-identity';

    // Mock the WebApi connection
    const mockConnection = {
      serverUrl: 'https://dev.azure.com/testorg',
      getCoreApi: jest.fn().mockResolvedValue({
        getProjects: jest.fn().mockResolvedValue([]),
      }),
    };

    // Mock axios post
    const mockResponse = {
      data: {
        count: 0,
        results: [],
      },
    };
    (axios.post as jest.Mock).mockResolvedValueOnce(mockResponse);

    // Call the function
    await searchWorkItems(mockConnection as unknown as WebApi, {
      projectId: 'testproject',
      searchText: 'test query',
    });

    // Verify the axios post was called with a Bearer token
    expect(axios.post).toHaveBeenCalledWith(
      expect.any(String),
      expect.any(Object),
      {
        headers: {
          Authorization: 'Bearer mock-azure-identity-token',
          'Content-Type': 'application/json',
        },
      },
    );

    // Cleanup
    process.env.AZURE_DEVOPS_AUTH_METHOD = originalEnv;
  });

  test('should perform organization-wide work item search when projectId is not provided', async () => {
    // Arrange
    const mockSearchResponse = {
      data: {
        count: 2,
        results: [
          {
            id: 1,
            fields: {
              'System.Title': 'Test Bug 1',
              'System.State': 'Active',
              'System.WorkItemType': 'Bug',
              'System.TeamProject': 'Project1',
            },
            project: {
              name: 'Project1',
              id: 'project-id-1',
            },
          },
          {
            id: 2,
            fields: {
              'System.Title': 'Test Bug 2',
              'System.State': 'Active',
              'System.WorkItemType': 'Bug',
              'System.TeamProject': 'Project2',
            },
            project: {
              name: 'Project2',
              id: 'project-id-2',
            },
          },
        ],
      },
    };

    mockedAxios.post.mockResolvedValueOnce(mockSearchResponse);

    // Act
    const result = await searchWorkItems(connection, {
      searchText: 'bug',
    });

    // Assert
    expect(result).toBeDefined();
    expect(result.count).toBe(2);
    expect(result.results).toHaveLength(2);
    expect(result.results[0].fields['System.TeamProject']).toBe('Project1');
    expect(result.results[1].fields['System.TeamProject']).toBe('Project2');
    expect(mockedAxios.post).toHaveBeenCalledTimes(1);
    expect(mockedAxios.post).toHaveBeenCalledWith(
      expect.stringContaining(
        'https://almsearch.dev.azure.com/mock-org/_apis/search/workitemsearchresults',
      ),
      expect.not.objectContaining({
        filters: expect.objectContaining({
          'System.TeamProject': expect.anything(),
        }),
      }),
      expect.any(Object),
    );
  });
});

```

--------------------------------------------------------------------------------
/src/features/search/search-code/feature.spec.int.ts:
--------------------------------------------------------------------------------

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

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

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

  test('should search code in a project', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      console.log('Skipping test: No Azure DevOps connection available');
      return;
    }

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

    const options: SearchCodeOptions = {
      searchText: 'function',
      projectId: projectName,
      top: 10,
    };

    try {
      // Act - make an actual API call to Azure DevOps
      const result = await searchCode(connection, options);

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

      // Check structure of returned items (if any)
      if (result.results.length > 0) {
        const firstResult = result.results[0];
        expect(firstResult.fileName).toBeDefined();
        expect(firstResult.path).toBeDefined();
        expect(firstResult.project).toBeDefined();
        expect(firstResult.repository).toBeDefined();

        if (firstResult.project) {
          expect(firstResult.project.name).toBe(projectName);
        }
      }
    } catch (error) {
      // Skip test if the code search extension is not installed
      if (
        error instanceof Error &&
        (error.message.includes('ms.vss-code-search is not installed') ||
          error.message.includes('Resource not found') ||
          error.message.includes('Failed to search code'))
      ) {
        console.log(
          'Skipping test: Code Search extension is not installed or not available in this Azure DevOps organization',
        );
        return;
      }
      throw error;
    }
  });

  test('should include file content when requested', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      console.log('Skipping test: No Azure DevOps connection available');
      return;
    }

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

    const options: SearchCodeOptions = {
      searchText: 'function',
      projectId: projectName,
      top: 5,
      includeContent: true,
    };

    try {
      // Act - make an actual API call to Azure DevOps
      const result = await searchCode(connection, options);

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

      // Check if content is included (if any results)
      if (result.results.length > 0) {
        // At least some results should have content
        // Note: Some files might fail to fetch content, so we don't expect all to have it
        const hasContent = result.results.some((r) => r.content !== undefined);
        expect(hasContent).toBe(true);
      }
    } catch (error) {
      // Skip test if the code search extension is not installed
      if (
        error instanceof Error &&
        (error.message.includes('ms.vss-code-search is not installed') ||
          error.message.includes('Resource not found') ||
          error.message.includes('Failed to search code'))
      ) {
        console.log(
          'Skipping test: Code Search extension is not installed or not available in this Azure DevOps organization',
        );
        return;
      }
      throw error;
    }
  });

  test('should filter results when filters are provided', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      console.log('Skipping test: No Azure DevOps connection available');
      return;
    }

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

    try {
      // First get some results to find a repository name
      const initialOptions: SearchCodeOptions = {
        searchText: 'function',
        projectId: projectName,
        top: 1,
      };

      const initialResult = await searchCode(connection, initialOptions);

      // Skip if no results found
      if (initialResult.results.length === 0) {
        console.log('Skipping filter test: No initial results found');
        return;
      }

      // Use the repository from the first result for filtering
      const repoName = initialResult.results[0].repository.name;

      const filteredOptions: SearchCodeOptions = {
        searchText: 'function',
        projectId: projectName,
        filters: {
          Repository: [repoName],
        },
        top: 5,
      };

      // Act - make an actual API call to Azure DevOps with filters
      const result = await searchCode(connection, filteredOptions);

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

      // All results should be from the specified repository
      if (result.results.length > 0) {
        const allFromRepo = result.results.every(
          (r) => r.repository.name === repoName,
        );
        expect(allFromRepo).toBe(true);
      }
    } catch (error) {
      // Skip test if the code search extension is not installed
      if (
        error instanceof Error &&
        (error.message.includes('ms.vss-code-search is not installed') ||
          error.message.includes('Resource not found') ||
          error.message.includes('Failed to search code'))
      ) {
        console.log(
          'Skipping test: Code Search extension is not installed or not available in this Azure DevOps organization',
        );
        return;
      }
      throw error;
    }
  });

  test('should handle pagination', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      console.log('Skipping test: No Azure DevOps connection available');
      return;
    }

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

    try {
      // Get first page
      const firstPageOptions: SearchCodeOptions = {
        searchText: 'function',
        projectId: projectName,
        top: 2,
        skip: 0,
      };

      const firstPageResult = await searchCode(connection, firstPageOptions);

      // Skip if not enough results for pagination test
      if (firstPageResult.count <= 2) {
        console.log('Skipping pagination test: Not enough results');
        return;
      }

      // Get second page
      const secondPageOptions: SearchCodeOptions = {
        searchText: 'function',
        projectId: projectName,
        top: 2,
        skip: 2,
      };

      const secondPageResult = await searchCode(connection, secondPageOptions);

      // Assert on pagination
      expect(secondPageResult).toBeDefined();
      expect(secondPageResult.results.length).toBeGreaterThan(0);

      // First and second page should have different results
      if (
        firstPageResult.results.length > 0 &&
        secondPageResult.results.length > 0
      ) {
        const firstPagePaths = firstPageResult.results.map((r) => r.path);
        const secondPagePaths = secondPageResult.results.map((r) => r.path);

        // Check if there's any overlap between pages
        const hasOverlap = firstPagePaths.some((path) =>
          secondPagePaths.includes(path),
        );
        expect(hasOverlap).toBe(false);
      }
    } catch (error) {
      // Skip test if the code search extension is not installed
      if (
        error instanceof Error &&
        (error.message.includes('ms.vss-code-search is not installed') ||
          error.message.includes('Resource not found') ||
          error.message.includes('Failed to search code'))
      ) {
        console.log(
          'Skipping test: Code Search extension is not installed or not available in this Azure DevOps organization',
        );
        return;
      }
      throw error;
    }
  });

  test('should use default project when no projectId is provided', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      console.log('Skipping test: No Azure DevOps connection available');
      return;
    }

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

    // Store original environment variable
    const originalEnv = process.env.AZURE_DEVOPS_DEFAULT_PROJECT;

    try {
      // Set the default project to the current project name for testing
      process.env.AZURE_DEVOPS_DEFAULT_PROJECT = projectName;

      // Search without specifying a project ID
      const options: SearchCodeOptions = {
        searchText: 'function',
        top: 5,
      };

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

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

      // Check structure of returned items (if any)
      if (result.results.length > 0) {
        const firstResult = result.results[0];
        expect(firstResult.fileName).toBeDefined();
        expect(firstResult.path).toBeDefined();
        expect(firstResult.project).toBeDefined();
        expect(firstResult.repository).toBeDefined();

        if (firstResult.project) {
          expect(firstResult.project.name).toBe(projectName);
        }
      }
    } catch (error) {
      // Skip test if the code search extension is not installed
      if (
        error instanceof Error &&
        (error.message.includes('ms.vss-code-search is not installed') ||
          error.message.includes('Resource not found') ||
          error.message.includes('Failed to search code'))
      ) {
        console.log(
          'Skipping test: Code Search extension is not installed or not available in this Azure DevOps organization',
        );
        return;
      }
      throw error;
    } finally {
      // Restore original environment variable
      process.env.AZURE_DEVOPS_DEFAULT_PROJECT = originalEnv;
    }
  });
});

```

--------------------------------------------------------------------------------
/src/features/search/types.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Options for searching code in Azure DevOps repositories
 */
export interface SearchCodeOptions {
  searchText: string;
  projectId?: string;
  filters?: {
    Repository?: string[];
    Path?: string[];
    Branch?: string[];
    CodeElement?: string[];
  };
  top?: number;
  skip?: number;
  includeSnippet?: boolean;
  includeContent?: boolean;
}

/**
 * Request body for the Azure DevOps Search API
 */
export interface CodeSearchRequest {
  searchText: string;
  $skip?: number;
  $top?: number;
  filters?: {
    Project?: string[];
    Repository?: string[];
    Path?: string[];
    Branch?: string[];
    CodeElement?: string[];
  };
  includeFacets?: boolean;
  includeSnippet?: boolean;
}

/**
 * Match information for search results
 */
export interface CodeSearchMatch {
  charOffset: number;
  length: number;
}

/**
 * Collection information for search results
 */
export interface CodeSearchCollection {
  name: string;
}

/**
 * Project information for search results
 */
export interface CodeSearchProject {
  name: string;
  id: string;
}

/**
 * Repository information for search results
 */
export interface CodeSearchRepository {
  name: string;
  id: string;
  type: string;
}

/**
 * Version information for search results
 */
export interface CodeSearchVersion {
  branchName: string;
  changeId: string;
}

/**
 * Individual code search result
 */
export interface CodeSearchResult {
  fileName: string;
  path: string;
  content?: string; // Added to store full file content
  matches: {
    content?: CodeSearchMatch[];
    fileName?: CodeSearchMatch[];
  };
  collection: CodeSearchCollection;
  project: CodeSearchProject;
  repository: CodeSearchRepository;
  versions: CodeSearchVersion[];
  contentId: string;
}

/**
 * Facet information for search results
 */
export interface CodeSearchFacet {
  name: string;
  id: string;
  resultCount: number;
}

/**
 * Response from the Azure DevOps Search API
 */
export interface CodeSearchResponse {
  count: number;
  results: CodeSearchResult[];
  infoCode?: number;
  facets?: {
    Project?: CodeSearchFacet[];
    Repository?: CodeSearchFacet[];
    Path?: CodeSearchFacet[];
    Branch?: CodeSearchFacet[];
    CodeElement?: CodeSearchFacet[];
  };
}

/**
 * Options for searching wiki pages in Azure DevOps projects
 */
export interface SearchWikiOptions {
  /**
   * The text to search for within wiki pages
   */
  searchText: string;

  /**
   * The ID or name of the project to search in
   * If not provided, search will be performed across the entire organization
   */
  projectId?: string;

  /**
   * Optional filters to narrow search results
   */
  filters?: {
    /**
     * Filter by project names. Useful for cross-project searches.
     */
    Project?: string[];
  };

  /**
   * Number of results to return
   * @default 100
   * @minimum 1
   * @maximum 1000
   */
  top?: number;

  /**
   * Number of results to skip for pagination
   * @default 0
   * @minimum 0
   */
  skip?: number;

  /**
   * Whether to include faceting in results
   * @default true
   */
  includeFacets?: boolean;
}

/**
 * Request body for the Azure DevOps Wiki Search API
 */
export interface WikiSearchRequest {
  /**
   * The search text to find in wiki pages
   */
  searchText: string;

  /**
   * Number of results to skip for pagination
   */
  $skip?: number;

  /**
   * Number of results to return
   */
  $top?: number;

  /**
   * Filters to be applied. Set to null if no filters are needed.
   */
  filters?: {
    /**
     * Filter by project names
     */
    Project?: string[];
  };

  /**
   * Options for sorting search results
   * If null, results are sorted by relevance
   */
  $orderBy?: SortOption[];

  /**
   * Whether to include faceting in the result
   * @default false
   */
  includeFacets?: boolean;
}

/**
 * Sort option for search results
 */
export interface SortOption {
  /**
   * Field to sort by
   */
  field: string;

  /**
   * Sort direction
   */
  sortOrder: 'asc' | 'desc' | 'ASC' | 'DESC';
}

/**
 * Defines the matched terms in the field of the wiki result
 */
export interface WikiHit {
  /**
   * Reference name of the highlighted field
   */
  fieldReferenceName: string;

  /**
   * Matched/highlighted snippets of the field
   */
  highlights: string[];
}

/**
 * Defines the wiki result that matched a wiki search request
 */
export interface WikiResult {
  /**
   * Name of the result file
   */
  fileName: string;

  /**
   * Path at which result file is present
   */
  path: string;

  /**
   * Collection of the result file
   */
  collection: {
    /**
     * Name of the collection
     */
    name: string;
  };

  /**
   * Project details of the wiki document
   */
  project: {
    /**
     * ID of the project
     */
    id: string;

    /**
     * Name of the project
     */
    name: string;

    /**
     * Visibility of the project
     */
    visibility?: string;
  };

  /**
   * Wiki information for the result
   */
  wiki: {
    /**
     * ID of the wiki
     */
    id: string;

    /**
     * Mapped path for the wiki
     */
    mappedPath: string;

    /**
     * Name of the wiki
     */
    name: string;

    /**
     * Version for wiki
     */
    version: string;
  };

  /**
   * Content ID of the result file
   */
  contentId: string;

  /**
   * Highlighted snippets of fields that match the search request
   * The list is sorted by relevance of the snippets
   */
  hits: WikiHit[];
}

/**
 * Defines a wiki search response item
 */
export interface WikiSearchResponse {
  /**
   * Total number of matched wiki documents
   */
  count: number;

  /**
   * List of top matched wiki documents
   */
  results: WikiResult[];

  /**
   * Numeric code indicating additional information:
   * 0 - Ok
   * 1 - Account is being reindexed
   * 2 - Account indexing has not started
   * 3 - Invalid Request
   * ... and others as defined in the API
   */
  infoCode?: number;

  /**
   * A dictionary storing an array of Filter objects against each facet
   */
  facets?: {
    /**
     * Project facets for filtering
     */
    Project?: CodeSearchFacet[];
  };
}

/**
 * Options for searching work items in Azure DevOps projects
 */
export interface SearchWorkItemsOptions {
  /**
   * The text to search for within work items
   */
  searchText: string;

  /**
   * The ID or name of the project to search in
   * If not provided, search will be performed across the entire organization
   */
  projectId?: string;

  /**
   * Optional filters to narrow search results
   */
  filters?: {
    /**
     * Filter by project names. Useful for cross-project searches.
     */
    'System.TeamProject'?: string[];

    /**
     * Filter by work item types (Bug, Task, User Story, etc.)
     */
    'System.WorkItemType'?: string[];

    /**
     * Filter by work item states (New, Active, Closed, etc.)
     */
    'System.State'?: string[];

    /**
     * Filter by assigned users
     */
    'System.AssignedTo'?: string[];

    /**
     * Filter by area paths
     */
    'System.AreaPath'?: string[];
  };

  /**
   * Number of results to return
   * @default 100
   * @minimum 1
   * @maximum 1000
   */
  top?: number;

  /**
   * Number of results to skip for pagination
   * @default 0
   * @minimum 0
   */
  skip?: number;

  /**
   * Whether to include faceting in results
   * @default true
   */
  includeFacets?: boolean;

  /**
   * Options for sorting search results
   * If null, results are sorted by relevance
   */
  orderBy?: SortOption[];
}

/**
 * Request body for the Azure DevOps Work Item Search API
 */
export interface WorkItemSearchRequest {
  /**
   * The search text to find in work items
   */
  searchText: string;

  /**
   * Number of results to skip for pagination
   */
  $skip?: number;

  /**
   * Number of results to return
   */
  $top?: number;

  /**
   * Filters to be applied. Set to null if no filters are needed.
   */
  filters?: {
    'System.TeamProject'?: string[];
    'System.WorkItemType'?: string[];
    'System.State'?: string[];
    'System.AssignedTo'?: string[];
    'System.AreaPath'?: string[];
  };

  /**
   * Options for sorting search results
   * If null, results are sorted by relevance
   */
  $orderBy?: SortOption[];

  /**
   * Whether to include faceting in the result
   * @default false
   */
  includeFacets?: boolean;
}

/**
 * Defines the matched terms in the field of the work item result
 */
export interface WorkItemHit {
  /**
   * Reference name of the highlighted field
   */
  fieldReferenceName: string;

  /**
   * Matched/highlighted snippets of the field
   */
  highlights: string[];
}

/**
 * Defines the work item result that matched a work item search request
 */
export interface WorkItemResult {
  /**
   * Project details of the work item
   */
  project: {
    /**
     * ID of the project
     */
    id: string;

    /**
     * Name of the project
     */
    name: string;
  };

  /**
   * A standard set of work item fields and their values
   */
  fields: {
    /**
     * ID of the work item
     */
    'system.id': string;

    /**
     * Type of the work item (Bug, Task, User Story, etc.)
     */
    'system.workitemtype': string;

    /**
     * Title of the work item
     */
    'system.title': string;

    /**
     * User assigned to the work item
     */
    'system.assignedto'?: string;

    /**
     * Current state of the work item
     */
    'system.state'?: string;

    /**
     * Tags associated with the work item
     */
    'system.tags'?: string;

    /**
     * Revision number of the work item
     */
    'system.rev'?: string;

    /**
     * Creation date of the work item
     */
    'system.createddate'?: string;

    /**
     * Last modified date of the work item
     */
    'system.changeddate'?: string;

    /**
     * Other fields may be included based on the work item type
     */
    [key: string]: string | number | boolean | null | undefined;
  };

  /**
   * Highlighted snippets of fields that match the search request
   * The list is sorted by relevance of the snippets
   */
  hits: WorkItemHit[];

  /**
   * URL to the work item
   */
  url: string;
}

/**
 * Defines a work item search response item
 */
export interface WorkItemSearchResponse {
  /**
   * Total number of matched work items
   */
  count: number;

  /**
   * List of top matched work items
   */
  results: WorkItemResult[];

  /**
   * Numeric code indicating additional information:
   * 0 - Ok
   * 1 - Account is being reindexed
   * 2 - Account indexing has not started
   * 3 - Invalid Request
   * ... and others as defined in the API
   */
  infoCode?: number;

  /**
   * A dictionary storing an array of Filter objects against each facet
   */
  facets?: {
    'System.TeamProject'?: CodeSearchFacet[];
    'System.WorkItemType'?: CodeSearchFacet[];
    'System.State'?: CodeSearchFacet[];
    'System.AssignedTo'?: CodeSearchFacet[];
    'System.AreaPath'?: CodeSearchFacet[];
  };
}

```

--------------------------------------------------------------------------------
/setup_env.sh:
--------------------------------------------------------------------------------

```bash
#!/bin/bash

# Global variable to track if an error has occurred
ERROR_OCCURRED=0

# Function to handle errors without exiting the shell when sourced
handle_error() {
    local message=$1
    local reset_colors="\033[0m"
    echo -e "\033[0;31m$message$reset_colors"
    
    # Set the error flag
    ERROR_OCCURRED=1
    
    # If script is being sourced (. or source)
    if [[ "${BASH_SOURCE[0]}" != "${0}" ]] || [[ -n "$ZSH_VERSION" && "$ZSH_EVAL_CONTEXT" == *:file:* ]]; then
        echo "Script terminated with error. Returning to shell."
        # Reset colors to ensure shell isn't affected
        echo -e "$reset_colors"
        # The return will be caught by the caller
        return 1
    else
        # If script is being executed directly
        exit 1
    fi
}

# Function to check if we should continue after potential error points
should_continue() {
    if [ $ERROR_OCCURRED -eq 1 ]; then
        # Reset colors to ensure shell isn't affected
        echo -e "\033[0m"
        return 1
    fi
    return 0
}

# Ensure script is running with a compatible shell
if [ -z "$BASH_VERSION" ] && [ -z "$ZSH_VERSION" ]; then
    handle_error "This script requires bash or zsh to run. Please run it with: bash $(basename "$0") or zsh $(basename "$0")"
    return 1 2>/dev/null || exit 1
fi

# Set shell options for compatibility
if [ -n "$ZSH_VERSION" ]; then
    # ZSH specific settings
    setopt SH_WORD_SPLIT
    setopt KSH_ARRAYS
fi

# Colors for better output - ensure they're properly reset after use
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color

echo -e "${GREEN}Azure DevOps MCP Server - Environment Setup${NC}"
echo "This script will help you set up your .env file with Azure DevOps credentials."
echo

# Clean up any existing create_pat.json file
if [ -f "create_pat.json" ]; then
    echo -e "${YELLOW}Cleaning up existing create_pat.json file...${NC}"
    rm -f create_pat.json
fi

# Check if Azure CLI is installed
if ! command -v az &> /dev/null; then
    handle_error "Error: Azure CLI is not installed.\nPlease install Azure CLI first: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli"
    return 1 2>/dev/null || exit 1
fi
should_continue || return 1 2>/dev/null || exit 1

# Check if Azure DevOps extension is installed
echo -e "${YELLOW}Checking for Azure DevOps extension...${NC}"
az devops &> /dev/null
if [ $? -ne 0 ]; then
    echo "Azure DevOps extension not found. Installing..."
    az extension add --name azure-devops
    if [ $? -ne 0 ]; then
        handle_error "Failed to install Azure DevOps extension."
        return 1 2>/dev/null || exit 1
    else
        echo -e "${GREEN}Azure DevOps extension installed successfully.${NC}"
    fi
else
    echo "Azure DevOps extension is already installed."
fi
should_continue || return 1 2>/dev/null || exit 1

# Check if jq is installed
if ! command -v jq &> /dev/null; then
    handle_error "Error: jq is not installed.\nPlease install jq first. On Ubuntu/Debian: sudo apt-get install jq\nOn macOS: brew install jq"
    return 1 2>/dev/null || exit 1
fi
should_continue || return 1 2>/dev/null || exit 1

# Check if already logged in
echo -e "\n${YELLOW}Step 1: Checking Azure CLI authentication...${NC}"
if ! az account show &> /dev/null; then
    echo "Not logged in. Initiating login..."
    az login --allow-no-subscriptions
    if [ $? -ne 0 ]; then
        handle_error "Failed to login to Azure CLI."
        return 1 2>/dev/null || exit 1
    fi
else
    echo -e "${GREEN}Already logged in to Azure CLI.${NC}"
fi
should_continue || return 1 2>/dev/null || exit 1

# Get Azure DevOps Organizations using REST API
echo -e "\n${YELLOW}Step 2: Fetching your Azure DevOps organizations...${NC}"
echo "This may take a moment..."

# First get the user profile
echo "Getting user profile..."
profile_response=$(az rest --method get --uri "https://app.vssps.visualstudio.com/_apis/profile/profiles/me?api-version=6.0" --resource "499b84ac-1321-427f-aa17-267ca6975798" 2>&1)
profile_status=$?

if [ $profile_status -ne 0 ]; then
    echo -e "${RED}Error: Failed to get user profile${NC}"
    echo -e "${RED}Status code: $profile_status${NC}"
    echo -e "${RED}Error response:${NC}"
    echo "$profile_response"
    echo
    echo "Manually provide your organization name instead."
    read -p "Enter your Azure DevOps organization name: " org_name
else
    echo "Profile API response:"
    echo "$profile_response"
    echo
    public_alias=$(echo "$profile_response" | jq -r '.publicAlias')
    
    if [ "$public_alias" = "null" ] || [ -z "$public_alias" ]; then
        echo -e "${RED}Failed to extract publicAlias from response.${NC}"
        echo "Full response was:"
        echo "$profile_response"
        echo
        echo "Manually provide your organization name instead."
        read -p "Enter your Azure DevOps organization name: " org_name
    else
        # Get organizations using the publicAlias
        echo "Fetching organizations..."
        orgs_result=$(az rest --method get --uri "https://app.vssps.visualstudio.com/_apis/accounts?memberId=$public_alias&api-version=6.0" --resource "499b84ac-1321-427f-aa17-267ca6975798")
        
        # Extract organization names from the response using jq
        orgs=$(echo "$orgs_result" | jq -r '.value[].accountName')
        
        if [ -z "$orgs" ]; then
            echo -e "${RED}No organizations found.${NC}"
            echo "Manually provide your organization name instead."
            read -p "Enter your Azure DevOps organization name: " org_name
        else
            # Display organizations for selection
            echo -e "\nYour Azure DevOps organizations:"
            i=1
            OLDIFS=$IFS
            IFS=$'\n'
            # Create array in a shell-agnostic way
            orgs_array=()
            while IFS= read -r line; do
                [ -n "$line" ] && orgs_array+=("$line")
            done <<< "$orgs"
            IFS=$OLDIFS
            
            # Check if array is empty
            if [ ${#orgs_array[@]} -eq 0 ]; then
                echo -e "${RED}Failed to parse organizations list.${NC}"
                echo "Manually provide your organization name instead."
                read -p "Enter your Azure DevOps organization name: " org_name
            else
                # Display organizations with explicit indexing
                for ((idx=0; idx<${#orgs_array[@]}; idx++)); do
                    echo "$((idx+1)) ${orgs_array[$idx]}"
                done
                
                # Prompt for selection
                read -p "Select an organization (1-${#orgs_array[@]}): " org_selection
                
                if [[ "$org_selection" =~ ^[0-9]+$ ]] && [ "$org_selection" -ge 1 ] && [ "$org_selection" -le "${#orgs_array[@]}" ]; then
                    org_name=${orgs_array[$((org_selection-1))]}
                else
                    handle_error "Invalid selection. Please run the script again."
                    return 1 2>/dev/null || exit 1
                fi
            fi
        fi
    fi
fi
should_continue || return 1 2>/dev/null || exit 1

org_url="https://dev.azure.com/$org_name"
echo -e "${GREEN}Using organization URL: $org_url${NC}"

# Get Default Project (Optional)
echo -e "\n${YELLOW}Step 3: Would you like to set a default project? (y/n)${NC}"
read -p "Select option: " set_default_project

default_project=""
if [[ "$set_default_project" = "y" || "$set_default_project" = "Y" ]]; then
    # Configure az devops to use the selected organization
    az devops configure --defaults organization=$org_url
    
    # List projects
    echo "Fetching projects from $org_name..."
    projects=$(az devops project list --query "value[].name" -o tsv)
    
    if [ $? -ne 0 ] || [ -z "$projects" ]; then
        echo -e "${YELLOW}No projects found or unable to list projects.${NC}"
        read -p "Enter a default project name (leave blank to skip): " default_project
    else
        # Display projects for selection
        echo -e "\nAvailable projects in $org_name:"
        OLDIFS=$IFS
        IFS=$'\n'
        # Create array in a shell-agnostic way
        projects_array=()
        while IFS= read -r line; do
            [ -n "$line" ] && projects_array+=("$line")
        done <<< "$projects"
        IFS=$OLDIFS
        
        # Check if array is empty
        if [ ${#projects_array[@]} -eq 0 ]; then
            echo -e "${YELLOW}Failed to parse projects list.${NC}"
            read -p "Enter a default project name (leave blank to skip): " default_project
        else
            # Display projects with explicit indexing
            for ((idx=0; idx<${#projects_array[@]}; idx++)); do
                echo "$((idx+1)) ${projects_array[$idx]}"
            done
            
            echo "$((${#projects_array[@]}+1)) Skip setting a default project"
            
            # Prompt for selection
            read -p "Select a default project (1-$((${#projects_array[@]}+1))): " project_selection
            
            if [[ "$project_selection" =~ ^[0-9]+$ ]] && [ "$project_selection" -ge 1 ] && [ "$project_selection" -lt "$((${#projects_array[@]}+1))" ]; then
                default_project=${projects_array[$((project_selection-1))]}
                echo -e "${GREEN}Using default project: $default_project${NC}"
            else
                echo "No default project selected."
            fi
        fi
    fi
fi

# Create .env file
echo -e "\n${YELLOW}Step 5: Creating .env file...${NC}"

cat > .env << EOF
# Azure DevOps MCP Server - Environment Variables

# Azure DevOps Organization Name (selected from your available organizations)
AZURE_DEVOPS_ORG=$org_name

# Azure DevOps Organization URL (required)
AZURE_DEVOPS_ORG_URL=$org_url


AZURE_DEVOPS_AUTH_METHOD=azure-identity
EOF

# Add default project if specified
if [ ! -z "$default_project" ]; then
cat >> .env << EOF

# Default Project to use when not specified
AZURE_DEVOPS_DEFAULT_PROJECT=$default_project
EOF
else
cat >> .env << EOF

# Default Project to use when not specified (optional)
# AZURE_DEVOPS_DEFAULT_PROJECT=your-default-project
EOF
fi

# Add remaining configuration
cat >> .env << EOF

# API Version to use (optional, defaults to latest)
# AZURE_DEVOPS_API_VERSION=6.0

# Server Configuration
PORT=3000
HOST=localhost

# Logging Level (debug, info, warn, error)
LOG_LEVEL=info
EOF

echo -e "\n${GREEN}Environment setup completed successfully!${NC}"
echo "Your .env file has been created with the following configuration:"
echo "- Organization: $org_name"
echo "- Organization URL: $org_url"
if [ ! -z "$default_project" ]; then
    echo "- Default Project: $default_project"
fi
echo "- PAT: Created with expanded scopes for full integration"
echo
echo "You can now run your Azure DevOps MCP Server with:"
echo "  npm run dev"
echo
echo "You can also run integration tests with:"
echo "  npm run test:integration"

# At the end of the script, ensure colors are reset
echo -e "${NC}" 
```

--------------------------------------------------------------------------------
/src/features/repositories/get-all-repositories-tree/feature.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import { IGitApi } from 'azure-devops-node-api/GitApi';
import {
  GitVersionType,
  VersionControlRecursionType,
  GitItem,
  GitObjectType,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
import { minimatch } from 'minimatch';
import { AzureDevOpsError } from '../../../shared/errors';
import {
  GetAllRepositoriesTreeOptions,
  AllRepositoriesTreeResponse,
  RepositoryTreeResponse,
  RepositoryTreeItem,
  GitRepository,
} from '../types';

/**
 * Get tree view of files/directories across multiple repositories
 *
 * @param connection The Azure DevOps WebApi connection
 * @param options Options for getting repository tree
 * @returns Tree structure for each repository
 */
export async function getAllRepositoriesTree(
  connection: WebApi,
  options: GetAllRepositoriesTreeOptions,
): Promise<AllRepositoriesTreeResponse> {
  try {
    const gitApi = await connection.getGitApi();
    let repositories: GitRepository[] = [];

    // Get all repositories in the project
    repositories = await gitApi.getRepositories(options.projectId);

    // Filter repositories by name pattern if specified
    if (options.repositoryPattern) {
      repositories = repositories.filter((repo) =>
        minimatch(repo.name || '', options.repositoryPattern || '*'),
      );
    }

    // Initialize results array
    const results: RepositoryTreeResponse[] = [];

    // Process each repository
    for (const repo of repositories) {
      try {
        // Get default branch ref
        const defaultBranch = repo.defaultBranch;
        if (!defaultBranch) {
          // Skip repositories with no default branch
          results.push({
            name: repo.name || 'Unknown',
            tree: [],
            stats: { directories: 0, files: 0 },
            error: 'No default branch found',
          });
          continue;
        }

        // Clean the branch name (remove refs/heads/ prefix)
        const branchRef = defaultBranch.replace('refs/heads/', '');

        // Initialize tree items array and counters
        const treeItems: RepositoryTreeItem[] = [];
        const stats = { directories: 0, files: 0 };

        // Determine the recursion level and processing approach
        const depth = options.depth !== undefined ? options.depth : 0; // Default to 0 (max depth)

        if (depth === 0) {
          // For max depth (0), use server-side recursion for better performance
          const allItems = await gitApi.getItems(
            repo.id || '',
            options.projectId,
            '/',
            VersionControlRecursionType.Full, // Use full recursion
            true,
            false,
            false,
            false,
            {
              version: branchRef,
              versionType: GitVersionType.Branch,
            },
          );

          // Filter out the root item itself and bad items
          const itemsToProcess = allItems.filter(
            (item) =>
              item.path !== '/' && item.gitObjectType !== GitObjectType.Bad,
          );

          // Process all items at once (they're already retrieved recursively)
          processItemsNonRecursive(
            itemsToProcess,
            treeItems,
            stats,
            options.pattern,
          );
        } else {
          // For limited depth, use the regular recursive approach
          // Get items at the root level
          const rootItems = await gitApi.getItems(
            repo.id || '',
            options.projectId,
            '/',
            VersionControlRecursionType.OneLevel,
            true,
            false,
            false,
            false,
            {
              version: branchRef,
              versionType: GitVersionType.Branch,
            },
          );

          // Filter out the root item itself and bad items
          const itemsToProcess = rootItems.filter(
            (item) =>
              item.path !== '/' && item.gitObjectType !== GitObjectType.Bad,
          );

          // Process the root items and their children (up to specified depth)
          await processItems(
            gitApi,
            repo.id || '',
            options.projectId,
            itemsToProcess,
            branchRef,
            treeItems,
            stats,
            1,
            depth,
            options.pattern,
          );
        }

        // Add repository tree to results
        results.push({
          name: repo.name || 'Unknown',
          tree: treeItems,
          stats,
        });
      } catch (repoError) {
        // Handle errors for individual repositories
        results.push({
          name: repo.name || 'Unknown',
          tree: [],
          stats: { directories: 0, files: 0 },
          error: `Error processing repository: ${repoError instanceof Error ? repoError.message : String(repoError)}`,
        });
      }
    }

    return { repositories: results };
  } catch (error) {
    if (error instanceof AzureDevOpsError) {
      throw error;
    }
    throw new Error(
      `Failed to get repository tree: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

/**
 * Process items non-recursively when they're already retrieved with VersionControlRecursionType.Full
 */
function processItemsNonRecursive(
  items: GitItem[],
  result: RepositoryTreeItem[],
  stats: { directories: number; files: number },
  pattern?: string,
): void {
  // Sort items (folders first, then by path)
  const sortedItems = [...items].sort((a, b) => {
    if (a.isFolder === b.isFolder) {
      return (a.path || '').localeCompare(b.path || '');
    }
    return a.isFolder ? -1 : 1;
  });

  for (const item of sortedItems) {
    const name = item.path?.split('/').pop() || '';
    const path = item.path || '';
    const isFolder = !!item.isFolder;

    // Skip the root folder
    if (path === '/') {
      continue;
    }

    // Calculate level from path segments
    // Remove leading '/' then count segments
    // For paths like:
    // /README.md -> ["README.md"] -> length 1 -> level 1
    // /src/index.ts -> ["src", "index.ts"] -> length 2 -> level 2
    // /src/utils/helper.ts -> ["src", "utils", "helper.ts"] -> length 3 -> level 3
    const pathSegments = path.replace(/^\//, '').split('/');
    const level = pathSegments.length;

    // Filter files based on pattern (if specified)
    if (!isFolder && pattern && !minimatch(name, pattern)) {
      continue;
    }

    // Add item to results
    result.push({
      name,
      path,
      isFolder,
      level,
    });

    // Update counters
    if (isFolder) {
      stats.directories++;
    } else {
      stats.files++;
    }
  }
}

/**
 * Process items recursively up to the specified depth
 */
async function processItems(
  gitApi: IGitApi,
  repoId: string,
  projectId: string,
  items: GitItem[],
  branchRef: string,
  result: RepositoryTreeItem[],
  stats: { directories: number; files: number },
  currentDepth: number,
  maxDepth: number,
  pattern?: string,
): Promise<void> {
  // Sort items (directories first, then files)
  const sortedItems = [...items].sort((a, b) => {
    if (a.isFolder === b.isFolder) {
      return (a.path || '').localeCompare(b.path || '');
    }
    return a.isFolder ? -1 : 1;
  });

  for (const item of sortedItems) {
    const name = item.path?.split('/').pop() || '';
    const path = item.path || '';
    const isFolder = !!item.isFolder;

    // Filter files based on pattern (if specified)
    if (!isFolder && pattern && !minimatch(name, pattern)) {
      continue;
    }

    // Add item to results
    result.push({
      name,
      path,
      isFolder,
      level: currentDepth,
    });

    // Update counters
    if (isFolder) {
      stats.directories++;
    } else {
      stats.files++;
    }

    // Recursively process folders if not yet at max depth
    if (isFolder && currentDepth < maxDepth) {
      try {
        const childItems = await gitApi.getItems(
          repoId,
          projectId,
          path,
          VersionControlRecursionType.OneLevel,
          true,
          false,
          false,
          false,
          {
            version: branchRef,
            versionType: GitVersionType.Branch,
          },
        );

        // Filter out the parent folder itself and bad items
        const itemsToProcess = childItems.filter(
          (child: GitItem) =>
            child.path !== path && child.gitObjectType !== GitObjectType.Bad,
        );

        // Process child items
        await processItems(
          gitApi,
          repoId,
          projectId,
          itemsToProcess,
          branchRef,
          result,
          stats,
          currentDepth + 1,
          maxDepth,
          pattern,
        );
      } catch (error) {
        // Ignore errors in child items and continue with siblings
        console.error(`Error processing folder ${path}: ${error}`);
      }
    }
  }
}

/**
 * Convert the tree items to a formatted ASCII string representation
 *
 * @param repoName Repository name
 * @param items Tree items
 * @param stats Statistics about files and directories
 * @returns Formatted ASCII string
 */
export function formatRepositoryTree(
  repoName: string,
  items: RepositoryTreeItem[],
  stats: { directories: number; files: number },
  error?: string,
): string {
  let output = `${repoName}/\n`;

  if (error) {
    output += `  (${error})\n`;
  } else if (items.length === 0) {
    output += '  (Repository is empty or default branch not found)\n';
  } else {
    // Sort items by path to ensure proper sequence
    const sortedItems = [...items].sort((a, b) => {
      // Sort by level first
      if (a.level !== b.level) {
        return a.level - b.level;
      }
      // Then folders before files
      if (a.isFolder !== b.isFolder) {
        return a.isFolder ? -1 : 1;
      }
      // Then alphabetically
      return a.path.localeCompare(b.path);
    });

    // Create a structured tree representation
    const tree = createTreeStructure(sortedItems);

    // Format the tree starting from the root
    output += formatTree(tree, '  ');
  }

  // Add summary line
  output += `${stats.directories} directories, ${stats.files} files\n`;

  return output;
}

/**
 * Create a structured tree from the flat list of items
 */
function createTreeStructure(items: RepositoryTreeItem[]): TreeNode {
  const root: TreeNode = {
    name: '',
    path: '',
    isFolder: true,
    children: [],
  };

  // Map to track all nodes by path
  const nodeMap: Record<string, TreeNode> = { '': root };

  // First create all nodes
  for (const item of items) {
    nodeMap[item.path] = {
      name: item.name,
      path: item.path,
      isFolder: item.isFolder,
      children: [],
    };
  }

  // Then build the hierarchy
  for (const item of items) {
    if (item.path === '/') continue;

    const node = nodeMap[item.path];
    const lastSlashIndex = item.path.lastIndexOf('/');

    // For root level items, the parent path is empty
    const parentPath =
      lastSlashIndex <= 0 ? '' : item.path.substring(0, lastSlashIndex);

    // Get parent node (defaults to root if parent not found)
    const parent = nodeMap[parentPath] || root;

    // Add this node as a child of its parent
    parent.children.push(node);
  }

  return root;
}

/**
 * Format a tree structure into an ASCII tree representation
 */
function formatTree(node: TreeNode, indent: string): string {
  if (!node.children.length) return '';

  let output = '';

  // Sort the children: folders first, then alphabetically
  const children = [...node.children].sort((a, b) => {
    if (a.isFolder !== b.isFolder) {
      return a.isFolder ? -1 : 1;
    }
    return a.name.localeCompare(b.name);
  });

  // Format each child node
  for (let i = 0; i < children.length; i++) {
    const child = children[i];
    const isLast = i === children.length - 1;
    const connector = isLast ? '`-- ' : '|-- ';
    const childIndent = isLast ? '    ' : '|   ';

    // Add the node itself
    const suffix = child.isFolder ? '/' : '';
    output += `${indent}${connector}${child.name}${suffix}\n`;

    // Recursively add its children
    if (child.children.length > 0) {
      output += formatTree(child, indent + childIndent);
    }
  }

  return output;
}

/**
 * Tree node interface for hierarchical representation
 */
interface TreeNode {
  name: string;
  path: string;
  isFolder: boolean;
  children: TreeNode[];
}

```

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

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

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

  test('should return pull request comment threads with file path and line number', async () => {
    // Mock data for a comment thread
    const mockCommentThreads: GitPullRequestCommentThread[] = [
      {
        id: 1,
        status: 1, // Active
        threadContext: {
          filePath: '/src/app.ts',
          rightFileStart: {
            line: 10,
            offset: 5,
          },
          rightFileEnd: {
            line: 10,
            offset: 15,
          },
        },
        comments: [
          {
            id: 100,
            content: 'This code needs refactoring',
            commentType: 1, // CodeChange
            author: {
              displayName: 'Test User',
              id: 'test-user-id',
            },
            publishedDate: new Date(),
          },
          {
            id: 101,
            parentCommentId: 100,
            content: 'I agree, will update',
            commentType: 1, // CodeChange
            author: {
              displayName: 'Another User',
              id: 'another-user-id',
            },
            publishedDate: new Date(),
          },
        ],
      },
    ];

    // Setup mock connection
    const mockGitApi = {
      getThreads: jest.fn().mockResolvedValue(mockCommentThreads),
      getPullRequestThread: jest.fn(),
    };

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

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

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

    // Verify results
    expect(result).toHaveLength(1);
    expect(result[0].comments).toHaveLength(2);

    // Verify file path and line number are added to each comment
    result[0].comments?.forEach((comment) => {
      expect(comment).toHaveProperty('filePath', '/src/app.ts');
      expect(comment).toHaveProperty('rightFileStart', { line: 10, offset: 5 });
      expect(comment).toHaveProperty('rightFileEnd', { line: 10, offset: 15 });
      expect(comment).toHaveProperty('leftFileStart', undefined);
      expect(comment).toHaveProperty('leftFileEnd', undefined);
    });

    expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1);
    expect(mockGitApi.getThreads).toHaveBeenCalledTimes(1);
    expect(mockGitApi.getThreads).toHaveBeenCalledWith(
      repositoryId,
      pullRequestId,
      projectId,
      undefined,
      undefined,
    );
    expect(mockGitApi.getPullRequestThread).not.toHaveBeenCalled();
  });

  test('should handle comments without thread context', async () => {
    // Mock data for a comment thread without thread context
    const mockCommentThreads: GitPullRequestCommentThread[] = [
      {
        id: 1,
        status: 1, // Active
        comments: [
          {
            id: 100,
            content: 'General comment',
            commentType: 1,
            author: {
              displayName: 'Test User',
              id: 'test-user-id',
            },
            publishedDate: new Date(),
          },
        ],
      },
    ];

    // Setup mock connection
    const mockGitApi = {
      getThreads: jest.fn().mockResolvedValue(mockCommentThreads),
      getPullRequestThread: jest.fn(),
    };

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

    const result = await getPullRequestComments(
      mockConnection as WebApi,
      'test-project',
      'test-repo',
      123,
      {
        projectId: 'test-project',
        repositoryId: 'test-repo',
        pullRequestId: 123,
      },
    );

    // Verify results
    expect(result).toHaveLength(1);
    expect(result[0].comments).toHaveLength(1);
    expect(result[0].status).toBe('active');

    // Verify file path and line number are null for comments without thread context
    const comment = result[0].comments![0];
    expect(comment).toHaveProperty('filePath', undefined);
    expect(comment).toHaveProperty('rightFileStart', undefined);
    expect(comment).toHaveProperty('rightFileEnd', undefined);
    expect(comment).toHaveProperty('leftFileStart', undefined);
    expect(comment).toHaveProperty('leftFileEnd', undefined);
    expect(comment).toHaveProperty('commentType', 'text');
  });

  test('should use leftFileStart when rightFileStart is not available', async () => {
    // Mock data for a comment thread with only leftFileStart
    const mockCommentThreads: GitPullRequestCommentThread[] = [
      {
        id: 1,
        status: 1,
        threadContext: {
          filePath: '/src/app.ts',
          leftFileStart: {
            line: 5,
            offset: 1,
          },
        },
        comments: [
          {
            id: 100,
            content: 'Comment on deleted line',
            commentType: 1,
            author: {
              displayName: 'Test User',
              id: 'test-user-id',
            },
            publishedDate: new Date(),
          },
        ],
      },
    ];

    // Setup mock connection
    const mockGitApi = {
      getThreads: jest.fn().mockResolvedValue(mockCommentThreads),
      getPullRequestThread: jest.fn(),
    };

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

    const result = await getPullRequestComments(
      mockConnection as WebApi,
      'test-project',
      'test-repo',
      123,
      {
        projectId: 'test-project',
        repositoryId: 'test-repo',
        pullRequestId: 123,
      },
    );

    // Verify results
    expect(result).toHaveLength(1);
    expect(result[0].comments).toHaveLength(1);

    // Verify rightFileStart is undefined, leftFileStart is present
    const comment = result[0].comments![0];
    expect(comment).toHaveProperty('filePath', '/src/app.ts');
    expect(comment).toHaveProperty('leftFileStart', { line: 5, offset: 1 });
    expect(comment).toHaveProperty('rightFileStart', undefined);
    expect(comment).toHaveProperty('leftFileEnd', undefined);
    expect(comment).toHaveProperty('rightFileEnd', undefined);
  });

  test('should return a specific comment thread when threadId is provided', async () => {
    // Mock data for a specific comment thread
    const threadId = 42;
    const mockCommentThread: GitPullRequestCommentThread = {
      id: threadId,
      status: 1, // Active
      threadContext: {
        filePath: '/src/utils.ts',
        rightFileStart: {
          line: 15,
          offset: 1,
        },
      },
      comments: [
        {
          id: 100,
          content: 'Specific comment',
          commentType: 1, // CodeChange
          author: {
            displayName: 'Test User',
            id: 'test-user-id',
          },
          publishedDate: new Date(),
        },
      ],
    };

    // Setup mock connection
    const mockGitApi = {
      getThreads: jest.fn(),
      getPullRequestThread: jest.fn().mockResolvedValue(mockCommentThread),
    };

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

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

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

    // Verify results
    expect(result).toHaveLength(1);
    expect(result[0].id).toBe(threadId);
    expect(result[0].comments).toHaveLength(1);

    // Verify file path and line number are added
    const comment = result[0].comments![0];
    expect(comment).toHaveProperty('filePath', '/src/utils.ts');
    expect(comment).toHaveProperty('rightFileStart', { line: 15, offset: 1 });
    expect(comment).toHaveProperty('leftFileStart', undefined);
    expect(comment).toHaveProperty('leftFileEnd', undefined);
    expect(comment).toHaveProperty('rightFileEnd', undefined);

    expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1);
    expect(mockGitApi.getPullRequestThread).toHaveBeenCalledTimes(1);
    expect(mockGitApi.getPullRequestThread).toHaveBeenCalledWith(
      repositoryId,
      pullRequestId,
      threadId,
      projectId,
    );
    expect(mockGitApi.getThreads).not.toHaveBeenCalled();
  });

  test('should handle pagination when top parameter is provided', async () => {
    // Mock data for multiple comment threads
    const mockCommentThreads: GitPullRequestCommentThread[] = [
      {
        id: 1,
        status: 1,
        threadContext: {
          filePath: '/src/file1.ts',
          rightFileStart: { line: 1, offset: 1 },
        },
        comments: [{ id: 100, content: 'Comment 1' }],
      },
      {
        id: 2,
        status: 1,
        threadContext: {
          filePath: '/src/file2.ts',
          rightFileStart: { line: 2, offset: 1 },
        },
        comments: [{ id: 101, content: 'Comment 2' }],
      },
      {
        id: 3,
        status: 1,
        threadContext: {
          filePath: '/src/file3.ts',
          rightFileStart: { line: 3, offset: 1 },
        },
        comments: [{ id: 102, content: 'Comment 3' }],
      },
    ];

    // Setup mock connection
    const mockGitApi = {
      getThreads: jest.fn().mockResolvedValue(mockCommentThreads),
      getPullRequestThread: jest.fn(),
    };

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

    // Call the function with test parameters and top=2
    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const pullRequestId = 123;
    const options = {
      projectId,
      repositoryId,
      pullRequestId,
      top: 2,
    };

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

    // Verify results (should only include first 2 threads)
    expect(result).toHaveLength(2);
    expect(result).toEqual(
      mockCommentThreads.slice(0, 2).map((thread) => ({
        ...thread,
        status: 'active', // Transform enum to string
        comments: thread.comments?.map((comment) => ({
          ...comment,
          commentType: undefined, // Will be undefined since mock doesn't have commentType
          filePath: thread.threadContext?.filePath,
          rightFileStart: thread.threadContext?.rightFileStart ?? undefined,
          rightFileEnd: thread.threadContext?.rightFileEnd ?? undefined,
          leftFileStart: thread.threadContext?.leftFileStart ?? undefined,
          leftFileEnd: thread.threadContext?.leftFileEnd ?? undefined,
        })),
      })),
    );
    expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1);
    expect(mockGitApi.getThreads).toHaveBeenCalledTimes(1);
    expect(result[0].comments![0]).toHaveProperty('rightFileStart', {
      line: 1,
      offset: 1,
    });
    expect(result[1].comments![0]).toHaveProperty('rightFileStart', {
      line: 2,
      offset: 1,
    });
  });

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

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

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

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

```

--------------------------------------------------------------------------------
/src/features/repositories/get-all-repositories-tree/feature.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import {
  GitObjectType,
  VersionControlRecursionType,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
import { getAllRepositoriesTree, formatRepositoryTree } from './feature';
import { RepositoryTreeItem } from '../types';

// Mock the Azure DevOps API
jest.mock('azure-devops-node-api');

describe('getAllRepositoriesTree', () => {
  // Sample repositories
  const mockRepos = [
    {
      id: 'repo1-id',
      name: 'repo1',
      defaultBranch: 'refs/heads/main',
    },
    {
      id: 'repo2-id',
      name: 'repo2',
      defaultBranch: 'refs/heads/master',
    },
    {
      id: 'repo3-id',
      name: 'repo3-api',
      defaultBranch: null, // No default branch
    },
  ];

  // Sample files/folders for repo1 at root level
  const mockRepo1RootItems = [
    {
      path: '/',
      gitObjectType: GitObjectType.Tree,
    },
    {
      path: '/README.md',
      isFolder: false,
      gitObjectType: GitObjectType.Blob,
    },
    {
      path: '/src',
      isFolder: true,
      gitObjectType: GitObjectType.Tree,
    },
    {
      path: '/package.json',
      isFolder: false,
      gitObjectType: GitObjectType.Blob,
    },
  ];

  // Sample files/folders for repo1 - src folder
  const mockRepo1SrcItems = [
    {
      path: '/src',
      isFolder: true,
      gitObjectType: GitObjectType.Tree,
    },
    {
      path: '/src/index.ts',
      isFolder: false,
      gitObjectType: GitObjectType.Blob,
    },
    {
      path: '/src/utils',
      isFolder: true,
      gitObjectType: GitObjectType.Tree,
    },
  ];

  // Sample files/folders for repo1 with unlimited depth (what server would return for Full recursion)
  const mockRepo1FullRecursionItems = [
    {
      path: '/',
      gitObjectType: GitObjectType.Tree,
    },
    {
      path: '/README.md',
      isFolder: false,
      gitObjectType: GitObjectType.Blob,
    },
    {
      path: '/src',
      isFolder: true,
      gitObjectType: GitObjectType.Tree,
    },
    {
      path: '/package.json',
      isFolder: false,
      gitObjectType: GitObjectType.Blob,
    },
    {
      path: '/src/index.ts',
      isFolder: false,
      gitObjectType: GitObjectType.Blob,
    },
    {
      path: '/src/utils',
      isFolder: true,
      gitObjectType: GitObjectType.Tree,
    },
    {
      path: '/src/utils/helper.ts',
      isFolder: false,
      gitObjectType: GitObjectType.Blob,
    },
    {
      path: '/src/utils/constants.ts',
      isFolder: false,
      gitObjectType: GitObjectType.Blob,
    },
  ];

  // Sample files/folders for repo2
  const mockRepo2RootItems = [
    {
      path: '/',
      gitObjectType: GitObjectType.Tree,
    },
    {
      path: '/README.md',
      isFolder: false,
      gitObjectType: GitObjectType.Blob,
    },
    {
      path: '/data.json',
      isFolder: false,
      gitObjectType: GitObjectType.Blob,
    },
  ];

  let mockConnection: jest.Mocked<WebApi>;
  let mockGitApi: any;

  beforeEach(() => {
    // Clear mocks
    jest.clearAllMocks();

    // Create mock GitApi
    mockGitApi = {
      getRepositories: jest.fn().mockResolvedValue(mockRepos),
      getItems: jest
        .fn()
        .mockImplementation((repoId, _projectId, path, recursionLevel) => {
          if (repoId === 'repo1-id') {
            if (recursionLevel === VersionControlRecursionType.Full) {
              return Promise.resolve(mockRepo1FullRecursionItems);
            } else if (path === '/') {
              return Promise.resolve(mockRepo1RootItems);
            } else if (path === '/src') {
              return Promise.resolve(mockRepo1SrcItems);
            }
          } else if (repoId === 'repo2-id') {
            if (recursionLevel === VersionControlRecursionType.Full) {
              return Promise.resolve(mockRepo2RootItems);
            } else if (path === '/') {
              return Promise.resolve(mockRepo2RootItems);
            }
          }
          return Promise.resolve([]);
        }),
    };

    // Create mock connection
    mockConnection = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    } as unknown as jest.Mocked<WebApi>;
  });

  it('should return tree structures for multiple repositories with limited depth', async () => {
    // Arrange
    const options = {
      organizationId: 'testOrg',
      projectId: 'testProject',
      depth: 2, // Limited depth
    };

    // Act
    const result = await getAllRepositoriesTree(mockConnection, options);

    // Assert
    expect(mockGitApi.getRepositories).toHaveBeenCalledWith('testProject');
    expect(result.repositories.length).toBe(3);

    // Verify repo1 tree
    const repo1 = result.repositories.find((r) => r.name === 'repo1');
    expect(repo1).toBeDefined();
    expect(repo1?.tree.length).toBeGreaterThan(0);
    expect(repo1?.stats.directories).toBeGreaterThan(0);
    expect(repo1?.stats.files).toBeGreaterThan(0);

    // Verify repo2 tree
    const repo2 = result.repositories.find((r) => r.name === 'repo2');
    expect(repo2).toBeDefined();
    expect(repo2?.tree.length).toBeGreaterThan(0);

    // Verify repo3 has error (no default branch)
    const repo3 = result.repositories.find((r) => r.name === 'repo3-api');
    expect(repo3).toBeDefined();
    expect(repo3?.error).toContain('No default branch found');

    // Verify recursion level was set correctly
    expect(mockGitApi.getItems).toHaveBeenCalledWith(
      'repo1-id',
      'testProject',
      '/',
      VersionControlRecursionType.OneLevel,
      expect.anything(),
      expect.anything(),
      expect.anything(),
      expect.anything(),
      expect.anything(),
    );
  });

  it('should return tree structures with max depth using Full recursion', async () => {
    // Arrange
    const options = {
      organizationId: 'testOrg',
      projectId: 'testProject',
      depth: 0, // Max depth
    };

    // Act
    const result = await getAllRepositoriesTree(mockConnection, options);

    // Assert
    expect(mockGitApi.getRepositories).toHaveBeenCalledWith('testProject');
    expect(result.repositories.length).toBe(3);

    // Verify repo1 tree
    const repo1 = result.repositories.find((r) => r.name === 'repo1');
    expect(repo1).toBeDefined();
    expect(repo1?.tree.length).toBeGreaterThan(0);
    // Should include all items, including nested ones
    expect(repo1?.tree.length).toBe(mockRepo1FullRecursionItems.length - 1); // -1 for root folder

    // Verify recursion level was set correctly
    expect(mockGitApi.getItems).toHaveBeenCalledWith(
      'repo1-id',
      'testProject',
      '/',
      VersionControlRecursionType.Full,
      expect.anything(),
      expect.anything(),
      expect.anything(),
      expect.anything(),
      expect.anything(),
    );

    // Verify all levels are represented
    if (repo1) {
      const level1Items = repo1.tree.filter((item) => item.level === 1);
      const level2Items = repo1.tree.filter((item) => item.level === 2);
      const level3Items = repo1.tree.filter((item) => item.level === 3);

      // Verify we have items at level 1
      expect(level1Items.length).toBeGreaterThan(0);

      // Verify we have items at level 2 (src/something)
      expect(level2Items.length).toBeGreaterThan(0);

      // Check for level 3 items if they exist in our mock data
      if (
        mockRepo1FullRecursionItems.some((item) => {
          const pathSegments = item.path.split('/').filter(Boolean);
          return pathSegments.length >= 3;
        })
      ) {
        expect(level3Items.length).toBeGreaterThan(0);
      }
    }
  });

  it('should filter repositories by pattern', async () => {
    // Arrange
    const options = {
      organizationId: 'testOrg',
      projectId: 'testProject',
      repositoryPattern: '*api*',
      depth: 1,
    };

    // Act
    const result = await getAllRepositoriesTree(mockConnection, options);

    // Assert
    expect(mockGitApi.getRepositories).toHaveBeenCalledWith('testProject');
    expect(result.repositories.length).toBe(1);
    expect(result.repositories[0].name).toBe('repo3-api');
  });

  it('should format repository tree correctly', () => {
    // Arrange
    const treeItems: RepositoryTreeItem[] = [
      { name: 'src', path: '/src', isFolder: true, level: 1 },
      { name: 'index.ts', path: '/src/index.ts', isFolder: false, level: 2 },
      { name: 'README.md', path: '/README.md', isFolder: false, level: 1 },
    ];
    const stats = { directories: 1, files: 2 };

    // Act
    const formatted = formatRepositoryTree('test-repo', treeItems, stats);

    // Assert
    expect(formatted).toMatchSnapshot();
  });

  it('should format complex repository tree structures correctly', () => {
    // Arrange
    const treeItems: RepositoryTreeItem[] = [
      // Root level files
      { name: 'README.md', path: '/README.md', isFolder: false, level: 1 },
      {
        name: 'package.json',
        path: '/package.json',
        isFolder: false,
        level: 1,
      },
      { name: '.gitignore', path: '/.gitignore', isFolder: false, level: 1 },

      // Multiple folders at root level
      { name: 'src', path: '/src', isFolder: true, level: 1 },
      { name: 'tests', path: '/tests', isFolder: true, level: 1 },
      { name: 'docs', path: '/docs', isFolder: true, level: 1 },

      // Nested src folder structure
      { name: 'components', path: '/src/components', isFolder: true, level: 2 },
      { name: 'utils', path: '/src/utils', isFolder: true, level: 2 },
      { name: 'index.ts', path: '/src/index.ts', isFolder: false, level: 2 },

      // Deeply nested components
      {
        name: 'Button',
        path: '/src/components/Button',
        isFolder: true,
        level: 3,
      },
      { name: 'Card', path: '/src/components/Card', isFolder: true, level: 3 },
      {
        name: 'Button.tsx',
        path: '/src/components/Button/Button.tsx',
        isFolder: false,
        level: 4,
      },
      {
        name: 'Button.styles.ts',
        path: '/src/components/Button/Button.styles.ts',
        isFolder: false,
        level: 4,
      },
      {
        name: 'Button.test.tsx',
        path: '/src/components/Button/Button.test.tsx',
        isFolder: false,
        level: 4,
      },
      {
        name: 'index.ts',
        path: '/src/components/Button/index.ts',
        isFolder: false,
        level: 4,
      },
      {
        name: 'Card.tsx',
        path: '/src/components/Card/Card.tsx',
        isFolder: false,
        level: 4,
      },

      // Utils with files
      {
        name: 'helpers.ts',
        path: '/src/utils/helpers.ts',
        isFolder: false,
        level: 3,
      },
      {
        name: 'constants.ts',
        path: '/src/utils/constants.ts',
        isFolder: false,
        level: 3,
      },

      // Empty folder
      { name: 'assets', path: '/src/assets', isFolder: true, level: 2 },

      // Files with special characters
      {
        name: 'file-with-dashes.js',
        path: '/src/file-with-dashes.js',
        isFolder: false,
        level: 2,
      },
      {
        name: 'file_with_underscores.js',
        path: '/src/file_with_underscores.js',
        isFolder: false,
        level: 2,
      },

      // Folders in test directory
      { name: 'unit', path: '/tests/unit', isFolder: true, level: 2 },
      {
        name: 'integration',
        path: '/tests/integration',
        isFolder: true,
        level: 2,
      },

      // Files in test directories
      { name: 'setup.js', path: '/tests/setup.js', isFolder: false, level: 2 },
      {
        name: 'example.test.js',
        path: '/tests/unit/example.test.js',
        isFolder: false,
        level: 3,
      },

      // Files in docs
      { name: 'API.md', path: '/docs/API.md', isFolder: false, level: 2 },
      {
        name: 'CONTRIBUTING.md',
        path: '/docs/CONTRIBUTING.md',
        isFolder: false,
        level: 2,
      },
    ];

    const stats = { directories: 10, files: 18 };

    // Act
    const formatted = formatRepositoryTree('complex-repo', treeItems, stats);

    // Assert
    expect(formatted).toMatchSnapshot();
  });

  it('should handle repository errors gracefully', async () => {
    // Arrange
    mockGitApi.getItems = jest.fn().mockRejectedValue(new Error('API error'));

    const options = {
      organizationId: 'testOrg',
      projectId: 'testProject',
      depth: 1,
    };

    // Act
    const result = await getAllRepositoriesTree(mockConnection, options);

    // Assert
    expect(result.repositories.length).toBe(3);
    const repo1 = result.repositories.find((r) => r.name === 'repo1');
    expect(repo1?.error).toBeDefined();
  });
});

```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import { createWikiPage } from './feature';
import { CreateWikiPageSchema } from './schema';
import { getWikiPage } from '../get-wiki-page/feature';
import { getWikis } from '../get-wikis/feature';
import {
  getTestConnection,
  shouldSkipIntegrationTest,
} from '@/shared/test/test-helpers';
import { getOrgNameFromUrl } from '@/utils/environment';
import { AzureDevOpsError } from '@/shared/errors/azure-devops-errors';
import { z } from 'zod';

// Ensure environment variables are set for testing
process.env.AZURE_DEVOPS_DEFAULT_PROJECT =
  process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'default-project';

describe('createWikiPage Integration Tests', () => {
  let connection: WebApi | null = null;
  let projectName: string;
  let orgUrl: string;
  let organizationId: string;
  const testPagePath = '/IntegrationTestPage';
  const testPagePathSub = '/IntegrationTestPage/SubPage';
  const testPagePathDefault = '/DefaultPathPage';
  const testPagePathComment = '/CommentTestPage';

  beforeAll(async () => {
    // Mock the required environment variable for testing
    process.env.AZURE_DEVOPS_ORG_URL =
      process.env.AZURE_DEVOPS_ORG_URL || 'https://example.visualstudio.com';

    // Get and validate required environment variables
    const envProjectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT;
    if (!envProjectName) {
      throw new Error(
        'AZURE_DEVOPS_DEFAULT_PROJECT environment variable is required',
      );
    }
    projectName = envProjectName;

    const envOrgUrl = process.env.AZURE_DEVOPS_ORG_URL;
    if (!envOrgUrl) {
      throw new Error('AZURE_DEVOPS_ORG_URL environment variable is required');
    }
    orgUrl = envOrgUrl;
    organizationId = getOrgNameFromUrl(orgUrl);

    // Get a real connection using environment variables
    connection = await getTestConnection();
  });

  // Helper function to get a valid wiki ID
  async function getValidWikiId(): Promise<string | null> {
    if (!connection) return null;

    try {
      // Get available wikis
      const wikis = await getWikis(connection, { projectId: projectName });

      // Skip if no wikis are available
      if (wikis.length === 0) {
        console.log('No wikis available in the project');
        return null;
      }

      // Use the first available wiki
      const wiki = wikis[0];
      if (!wiki.name) {
        console.log('Wiki name is undefined');
        return null;
      }

      return wiki.name;
    } catch (error) {
      console.error('Error getting wikis:', error);
      return null;
    }
  }

  test('should create a new wiki page at the root', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      console.log('Skipping test due to missing connection');
      return;
    }

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

    // Get a valid wiki ID
    const wikiId = await getValidWikiId();
    if (!wikiId) {
      console.log('Skipping test: No valid wiki ID available');
      return;
    }

    const params: z.infer<typeof CreateWikiPageSchema> = {
      organizationId,
      projectId: projectName,
      wikiId,
      pagePath: testPagePath,
      content: 'This is content for the integration test page (root).',
    };

    try {
      // Create the wiki page
      const createdPage = await createWikiPage(params);

      // Verify the result
      expect(createdPage).toBeDefined();
      expect(createdPage.path).toBe(testPagePath);
      expect(createdPage.content).toBe(params.content);

      // Verify by fetching the page
      const fetchedPage = await getWikiPage({
        organizationId,
        projectId: projectName,
        wikiId,
        pagePath: testPagePath,
      });

      expect(fetchedPage).toBeDefined();
      expect(typeof fetchedPage).toBe('string');
      expect(fetchedPage).toContain(params.content);
    } catch (error) {
      console.error('Error in test:', error);
      throw error;
    }
  });

  test('should create a new wiki sub-page', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      console.log('Skipping test due to missing connection');
      return;
    }

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

    // Get a valid wiki ID
    const wikiId = await getValidWikiId();
    if (!wikiId) {
      console.log('Skipping test: No valid wiki ID available');
      return;
    }

    // First, ensure the parent page exists
    const parentParams: z.infer<typeof CreateWikiPageSchema> = {
      organizationId,
      projectId: projectName,
      wikiId,
      pagePath: testPagePath,
      content: 'This is the parent page for the sub-page test.',
    };

    try {
      // Create the parent page
      await createWikiPage(parentParams);

      // Now create the sub-page
      const subPageParams: z.infer<typeof CreateWikiPageSchema> = {
        organizationId,
        projectId: projectName,
        wikiId,
        pagePath: testPagePathSub,
        content: 'This is content for the integration test sub-page.',
      };

      const createdSubPage = await createWikiPage(subPageParams);

      // Verify the result
      expect(createdSubPage).toBeDefined();
      expect(createdSubPage.path).toBe(testPagePathSub);
      expect(createdSubPage.content).toBe(subPageParams.content);

      // Verify by fetching the sub-page
      const fetchedSubPage = await getWikiPage({
        organizationId,
        projectId: projectName,
        wikiId,
        pagePath: testPagePathSub,
      });

      expect(fetchedSubPage).toBeDefined();
      expect(typeof fetchedSubPage).toBe('string');
      expect(fetchedSubPage).toContain(subPageParams.content);
    } catch (error) {
      console.error('Error in test:', error);
      throw error;
    }
  });

  test('should update an existing wiki page if path already exists', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      console.log('Skipping test due to missing connection');
      return;
    }

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

    // Get a valid wiki ID
    const wikiId = await getValidWikiId();
    if (!wikiId) {
      console.log('Skipping test: No valid wiki ID available');
      return;
    }

    try {
      // First create a page with initial content
      const initialParams: z.infer<typeof CreateWikiPageSchema> = {
        organizationId,
        projectId: projectName,
        wikiId,
        pagePath: testPagePath,
        content: 'Initial content.',
      };

      await createWikiPage(initialParams);

      // Now update the page with new content
      const updatedParams: z.infer<typeof CreateWikiPageSchema> = {
        ...initialParams,
        content: 'Updated content for the page.',
      };

      const updatedPage = await createWikiPage(updatedParams);

      // Verify the result
      expect(updatedPage).toBeDefined();
      expect(updatedPage.path).toBe(testPagePath);
      expect(updatedPage.content).toBe(updatedParams.content);

      // Verify by fetching the page
      const fetchedPage = await getWikiPage({
        organizationId,
        projectId: projectName,
        wikiId,
        pagePath: testPagePath,
      });

      expect(fetchedPage).toBeDefined();
      expect(typeof fetchedPage).toBe('string');
      expect(fetchedPage).toContain(updatedParams.content);
    } catch (error) {
      console.error('Error in test:', error);
      throw error;
    }
  });

  test('should create a page with a default path if specified', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      console.log('Skipping test due to missing connection');
      return;
    }

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

    // Get a valid wiki ID
    const wikiId = await getValidWikiId();
    if (!wikiId) {
      console.log('Skipping test: No valid wiki ID available');
      return;
    }

    try {
      const params: z.infer<typeof CreateWikiPageSchema> = {
        organizationId,
        projectId: projectName,
        wikiId,
        pagePath: testPagePathDefault,
        content: 'Content for page created with default path.',
      };

      const createdPage = await createWikiPage(params);

      // Verify the result
      expect(createdPage).toBeDefined();
      expect(createdPage.path).toBe(testPagePathDefault);
      expect(createdPage.content).toBe(params.content);

      // Verify by fetching the page
      const fetchedPage = await getWikiPage({
        organizationId,
        projectId: projectName,
        wikiId,
        pagePath: testPagePathDefault,
      });

      expect(fetchedPage).toBeDefined();
      expect(typeof fetchedPage).toBe('string');
      expect(fetchedPage).toContain(params.content);
    } catch (error) {
      console.error('Error in test:', error);
      throw error;
    }
  });

  test('should include comment in the wiki page creation when provided', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      console.log('Skipping test due to missing connection');
      return;
    }

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

    // Get a valid wiki ID
    const wikiId = await getValidWikiId();
    if (!wikiId) {
      console.log('Skipping test: No valid wiki ID available');
      return;
    }

    try {
      const params: z.infer<typeof CreateWikiPageSchema> = {
        organizationId,
        projectId: projectName,
        wikiId,
        pagePath: testPagePathComment,
        content: 'Content with comment.',
        comment: 'This is a test comment for the wiki page creation',
      };

      const createdPage = await createWikiPage(params);

      // Verify the result
      expect(createdPage).toBeDefined();
      expect(createdPage.path).toBe(testPagePathComment);
      expect(createdPage.content).toBe(params.content);

      // Verify by fetching the page
      const fetchedPage = await getWikiPage({
        organizationId,
        projectId: projectName,
        wikiId,
        pagePath: testPagePathComment,
      });

      expect(fetchedPage).toBeDefined();
      expect(typeof fetchedPage).toBe('string');
      expect(fetchedPage).toContain(params.content);

      // Note: The API might not return the comment in the response
      // This test primarily verifies that including a comment doesn't break the API call
    } catch (error) {
      console.error('Error in test:', error);
      throw error;
    }
  });

  test('should handle error when wiki does not exist', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      console.log('Skipping test due to missing connection');
      return;
    }

    const nonExistentWikiId = 'non-existent-wiki-12345';

    const params: z.infer<typeof CreateWikiPageSchema> = {
      organizationId,
      projectId: projectName,
      wikiId: nonExistentWikiId,
      pagePath: '/test-page',
      content: 'This should fail.',
    };

    await expect(createWikiPage(params)).rejects.toThrow(AzureDevOpsError);
  });

  test('should handle error when project does not exist', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      console.log('Skipping test due to missing connection');
      return;
    }

    const nonExistentProjectId = 'non-existent-project-12345';

    const params: z.infer<typeof CreateWikiPageSchema> = {
      organizationId,
      projectId: nonExistentProjectId,
      wikiId: 'any-wiki',
      pagePath: '/test-page',
      content: 'This should fail.',
    };

    await expect(createWikiPage(params)).rejects.toThrow(AzureDevOpsError);
  });

  test('should handle error when organization does not exist', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      console.log('Skipping test due to missing connection');
      return;
    }

    const nonExistentOrgId = 'non-existent-org-12345';

    const params: z.infer<typeof CreateWikiPageSchema> = {
      organizationId: nonExistentOrgId,
      projectId: projectName,
      wikiId: 'any-wiki',
      pagePath: '/test-page',
      content: 'This should fail.',
    };

    await expect(createWikiPage(params)).rejects.toThrow(AzureDevOpsError);
  });
});

```

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

```typescript
import { getProjectDetails } from './feature';
import {
  AzureDevOpsError,
  AzureDevOpsResourceNotFoundError,
} from '../../../shared/errors';
import {
  TeamProject,
  WebApiTeam,
} from 'azure-devops-node-api/interfaces/CoreInterfaces';
import { WebApi } from 'azure-devops-node-api';
import { WorkItemType } from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces';

// Create mock interfaces for the APIs we'll use
interface MockCoreApi {
  getProject: jest.Mock<Promise<TeamProject | null>>;
  getTeams: jest.Mock<Promise<WebApiTeam[]>>;
}

interface MockWorkItemTrackingApi {
  getWorkItemTypes: jest.Mock<Promise<WorkItemType[]>>;
}

interface MockProcessApi {
  getProcesses: jest.Mock<Promise<any[]>>;
  getProcessWorkItemTypes: jest.Mock<Promise<any[]>>;
}

// Create a mock connection that resembles WebApi with minimal implementation
interface MockConnection {
  getCoreApi: jest.Mock<Promise<MockCoreApi>>;
  getWorkItemTrackingApi: jest.Mock<Promise<MockWorkItemTrackingApi>>;
  getProcessApi: jest.Mock<Promise<MockProcessApi>>;
  serverUrl?: string;
  authHandler?: unknown;
  rest?: unknown;
  vsoClient?: unknown;
}

// Sample data for tests
const mockProject = {
  id: 'project-id',
  name: 'Test Project',
  description: 'A test project',
  url: 'https://dev.azure.com/org/project',
  state: 1, // wellFormed
  revision: 123,
  visibility: 0, // private
  lastUpdateTime: new Date(),
  capabilities: {
    versioncontrol: {
      sourceControlType: 'Git',
    },
    processTemplate: {
      templateName: 'Agile',
      templateTypeId: 'template-guid',
    },
  },
} as unknown as TeamProject;

const mockTeams: WebApiTeam[] = [
  {
    id: 'team-guid-1',
    name: 'Team 1',
    description: 'First team',
    url: 'https://dev.azure.com/org/_apis/projects/project-guid/teams/team-guid-1',
    identityUrl: 'https://vssps.dev.azure.com/org/_apis/Identities/team-guid-1',
  } as WebApiTeam,
  {
    id: 'team-guid-2',
    name: 'Team 2',
    description: 'Second team',
    url: 'https://dev.azure.com/org/_apis/projects/project-guid/teams/team-guid-2',
    identityUrl: 'https://vssps.dev.azure.com/org/_apis/Identities/team-guid-2',
  } as WebApiTeam,
];

const mockWorkItemTypes: WorkItemType[] = [
  {
    name: 'User Story',
    description: 'Tracks user requirements',
    referenceName: 'Microsoft.VSTS.WorkItemTypes.UserStory',
    color: 'blue',
    icon: 'icon-user-story',
    isDisabled: false,
  } as WorkItemType,
  {
    name: 'Bug',
    description: 'Tracks defects in the product',
    referenceName: 'Microsoft.VSTS.WorkItemTypes.Bug',
    color: 'red',
    icon: 'icon-bug',
    isDisabled: false,
  } as WorkItemType,
];

const mockProcesses = [
  {
    id: 'process-guid',
    name: 'Agile',
    description: 'Agile process',
    isDefault: true,
    type: 'system',
  },
];

const mockProcessWorkItemTypes = [
  {
    name: 'User Story',
    referenceName: 'Microsoft.VSTS.WorkItemTypes.UserStory',
    description: 'Tracks user requirements',
    color: 'blue',
    icon: 'icon-user-story',
    isDisabled: false,
    states: [
      {
        name: 'New',
        color: 'blue',
        stateCategory: 'Proposed',
      },
      {
        name: 'Active',
        color: 'blue',
        stateCategory: 'InProgress',
      },
      {
        name: 'Resolved',
        color: 'blue',
        stateCategory: 'InProgress',
      },
      {
        name: 'Closed',
        color: 'blue',
        stateCategory: 'Completed',
      },
    ],
    fields: [
      {
        name: 'Title',
        referenceName: 'System.Title',
        type: 'string',
        required: true,
      },
      {
        name: 'Description',
        referenceName: 'System.Description',
        type: 'html',
      },
    ],
  },
  {
    name: 'Bug',
    referenceName: 'Microsoft.VSTS.WorkItemTypes.Bug',
    description: 'Tracks defects in the product',
    color: 'red',
    icon: 'icon-bug',
    isDisabled: false,
    states: [
      {
        name: 'New',
        color: 'red',
        stateCategory: 'Proposed',
      },
      {
        name: 'Active',
        color: 'red',
        stateCategory: 'InProgress',
      },
      {
        name: 'Resolved',
        color: 'red',
        stateCategory: 'InProgress',
      },
      {
        name: 'Closed',
        color: 'red',
        stateCategory: 'Completed',
      },
    ],
    fields: [
      {
        name: 'Title',
        referenceName: 'System.Title',
        type: 'string',
        required: true,
      },
      {
        name: 'Repro Steps',
        referenceName: 'Microsoft.VSTS.TCM.ReproSteps',
        type: 'html',
      },
    ],
  },
];

// Unit tests should only focus on isolated logic
describe('getProjectDetails unit', () => {
  test('should throw resource not found error when project is null', async () => {
    // Arrange
    const mockCoreApi: MockCoreApi = {
      getProject: jest.fn().mockResolvedValue(null), // Simulate project not found
      getTeams: jest.fn().mockResolvedValue([]),
    };

    const mockConnection: MockConnection = {
      getCoreApi: jest.fn().mockResolvedValue(mockCoreApi),
      getWorkItemTrackingApi: jest.fn().mockResolvedValue({
        getWorkItemTypes: jest.fn().mockResolvedValue([]),
      }),
      getProcessApi: jest.fn().mockResolvedValue({
        getProcesses: jest.fn().mockResolvedValue([]),
        getProcessWorkItemTypes: jest.fn().mockResolvedValue([]),
      }),
    };

    // Act & Assert
    await expect(
      getProjectDetails(mockConnection as unknown as WebApi, {
        projectId: 'non-existent-project',
      }),
    ).rejects.toThrow(AzureDevOpsResourceNotFoundError);

    await expect(
      getProjectDetails(mockConnection as unknown as WebApi, {
        projectId: 'non-existent-project',
      }),
    ).rejects.toThrow("Project 'non-existent-project' not found");
  });

  test('should return basic project details when no additional options are specified', async () => {
    // Arrange
    const mockCoreApi: MockCoreApi = {
      getProject: jest.fn().mockResolvedValue(mockProject),
      getTeams: jest.fn().mockResolvedValue([]),
    };

    const mockConnection: MockConnection = {
      getCoreApi: jest.fn().mockResolvedValue(mockCoreApi),
      getWorkItemTrackingApi: jest.fn().mockResolvedValue({
        getWorkItemTypes: jest.fn().mockResolvedValue([]),
      }),
      getProcessApi: jest.fn().mockResolvedValue({
        getProcesses: jest.fn().mockResolvedValue([]),
        getProcessWorkItemTypes: jest.fn().mockResolvedValue([]),
      }),
    };

    // Act
    const result = await getProjectDetails(
      mockConnection as unknown as WebApi,
      {
        projectId: 'test-project',
      },
    );

    // Assert
    expect(result).toBeDefined();
    expect(result.id).toBe(mockProject.id);
    expect(result.name).toBe(mockProject.name);
    expect(result.description).toBe(mockProject.description);
    expect(result.url).toBe(mockProject.url);
    expect(result.state).toBe(mockProject.state);
    expect(result.revision).toBe(mockProject.revision);
    expect(result.visibility).toBe(mockProject.visibility);
    expect(result.lastUpdateTime).toBe(mockProject.lastUpdateTime);
    expect(result.capabilities).toEqual(mockProject.capabilities);

    // Verify that additional details are not included
    expect(result.process).toBeUndefined();
    expect(result.teams).toBeUndefined();
  });

  test('should include teams when includeTeams is true', async () => {
    // Arrange
    const mockCoreApi: MockCoreApi = {
      getProject: jest.fn().mockResolvedValue(mockProject),
      getTeams: jest.fn().mockResolvedValue(mockTeams),
    };

    const mockConnection: MockConnection = {
      getCoreApi: jest.fn().mockResolvedValue(mockCoreApi),
      getWorkItemTrackingApi: jest.fn().mockResolvedValue({
        getWorkItemTypes: jest.fn().mockResolvedValue([]),
      }),
      getProcessApi: jest.fn().mockResolvedValue({
        getProcesses: jest.fn().mockResolvedValue([]),
        getProcessWorkItemTypes: jest.fn().mockResolvedValue([]),
      }),
    };

    // Act
    const result = await getProjectDetails(
      mockConnection as unknown as WebApi,
      {
        projectId: 'test-project',
        includeTeams: true,
      },
    );

    // Assert
    expect(result).toBeDefined();
    expect(result.teams).toBeDefined();
    expect(result.teams?.length).toBe(2);
    expect(result.teams?.[0].id).toBe(mockTeams[0].id);
    expect(result.teams?.[0].name).toBe(mockTeams[0].name);
    expect(result.teams?.[1].id).toBe(mockTeams[1].id);
    expect(result.teams?.[1].name).toBe(mockTeams[1].name);
  });

  test('should include process information when includeProcess is true', async () => {
    // Arrange
    const mockCoreApi: MockCoreApi = {
      getProject: jest.fn().mockResolvedValue(mockProject),
      getTeams: jest.fn().mockResolvedValue([]),
    };

    const mockWorkItemTrackingApi: MockWorkItemTrackingApi = {
      getWorkItemTypes: jest.fn().mockResolvedValue([]),
    };

    const mockConnection: MockConnection = {
      getCoreApi: jest.fn().mockResolvedValue(mockCoreApi),
      getWorkItemTrackingApi: jest
        .fn()
        .mockResolvedValue(mockWorkItemTrackingApi),
      getProcessApi: jest.fn(),
    };

    // Act
    const result = await getProjectDetails(
      mockConnection as unknown as WebApi,
      {
        projectId: 'test-project',
        includeProcess: true,
      },
    );

    // Assert
    expect(result).toBeDefined();
    expect(result.process).toBeDefined();
    expect(result.process?.name).toBe('Agile');
  });

  test('should include work item types when includeWorkItemTypes is true', async () => {
    // Arrange
    const mockCoreApi: MockCoreApi = {
      getProject: jest.fn().mockResolvedValue(mockProject),
      getTeams: jest.fn().mockResolvedValue([]),
    };

    const mockWorkItemTrackingApi: MockWorkItemTrackingApi = {
      getWorkItemTypes: jest.fn().mockResolvedValue(mockWorkItemTypes),
    };

    const mockProcessApi: MockProcessApi = {
      getProcesses: jest.fn().mockResolvedValue(mockProcesses),
      getProcessWorkItemTypes: jest
        .fn()
        .mockResolvedValue(mockProcessWorkItemTypes),
    };

    const mockConnection: MockConnection = {
      getCoreApi: jest.fn().mockResolvedValue(mockCoreApi),
      getWorkItemTrackingApi: jest
        .fn()
        .mockResolvedValue(mockWorkItemTrackingApi),
      getProcessApi: jest.fn().mockResolvedValue(mockProcessApi),
    };

    // Act
    const result = await getProjectDetails(
      mockConnection as unknown as WebApi,
      {
        projectId: 'test-project',
        includeWorkItemTypes: true,
        includeProcess: true,
      },
    );

    // Assert
    expect(result).toBeDefined();
    expect(result.process).toBeDefined();
    expect(result.process?.workItemTypes).toBeDefined();
    expect(result.process?.workItemTypes?.length).toBe(2);
    expect(result.process?.workItemTypes?.[0].name).toBe('User Story');
    expect(result.process?.workItemTypes?.[1].name).toBe('Bug');
  });

  test('should include fields when includeFields is true', async () => {
    // Arrange
    const mockCoreApi: MockCoreApi = {
      getProject: jest.fn().mockResolvedValue(mockProject),
      getTeams: jest.fn().mockResolvedValue([]),
    };

    const mockWorkItemTrackingApi: MockWorkItemTrackingApi = {
      getWorkItemTypes: jest.fn().mockResolvedValue(mockWorkItemTypes),
    };

    const mockProcessApi: MockProcessApi = {
      getProcesses: jest.fn().mockResolvedValue(mockProcesses),
      getProcessWorkItemTypes: jest
        .fn()
        .mockResolvedValue(mockProcessWorkItemTypes),
    };

    const mockConnection: MockConnection = {
      getCoreApi: jest.fn().mockResolvedValue(mockCoreApi),
      getWorkItemTrackingApi: jest
        .fn()
        .mockResolvedValue(mockWorkItemTrackingApi),
      getProcessApi: jest.fn().mockResolvedValue(mockProcessApi),
    };

    // Act
    const result = await getProjectDetails(
      mockConnection as unknown as WebApi,
      {
        projectId: 'test-project',
        includeWorkItemTypes: true,
        includeFields: true,
        includeProcess: true,
      },
    );

    // Assert
    expect(result).toBeDefined();
    expect(result.process).toBeDefined();
    expect(result.process?.workItemTypes).toBeDefined();
    expect(result.process?.workItemTypes?.[0].fields).toBeDefined();
    expect(result.process?.workItemTypes?.[0].fields?.length).toBe(2);
    expect(result.process?.workItemTypes?.[0].fields?.[0].name).toBe('Title');
    expect(result.process?.workItemTypes?.[0].fields?.[1].name).toBe(
      'Description',
    );
  });

  test('should propagate custom errors when thrown internally', async () => {
    // Arrange
    const mockConnection: MockConnection = {
      getCoreApi: jest.fn().mockImplementation(() => {
        throw new AzureDevOpsError('Custom error');
      }),
      getWorkItemTrackingApi: jest.fn(),
      getProcessApi: jest.fn(),
    };

    // Act & Assert
    await expect(
      getProjectDetails(mockConnection as unknown as WebApi, {
        projectId: 'test-project',
      }),
    ).rejects.toThrow(AzureDevOpsError);

    await expect(
      getProjectDetails(mockConnection as unknown as WebApi, {
        projectId: 'test-project',
      }),
    ).rejects.toThrow('Custom error');
  });

  test('should wrap unexpected errors in a friendly error message', async () => {
    // Arrange
    const mockConnection: MockConnection = {
      getCoreApi: jest.fn().mockImplementation(() => {
        throw new Error('Unexpected error');
      }),
      getWorkItemTrackingApi: jest.fn(),
      getProcessApi: jest.fn(),
    };

    // Act & Assert
    await expect(
      getProjectDetails(mockConnection as unknown as WebApi, {
        projectId: 'test-project',
      }),
    ).rejects.toThrow('Failed to get project details: Unexpected error');
  });
});

```
Page 4/6FirstPrevNextLast