#
tokens: 45763/50000 10/281 files (page 6/8)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 6 of 8. Use http://codebase.md/tiberriver256/azure-devops-mcp?lines=true&page={x} to view the full context.

# Directory Structure

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

# Files

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

```bash
  1 | #!/bin/bash
  2 | 
  3 | # Global variable to track if an error has occurred
  4 | ERROR_OCCURRED=0
  5 | 
  6 | # Function to handle errors without exiting the shell when sourced
  7 | handle_error() {
  8 |     local message=$1
  9 |     local reset_colors="\033[0m"
 10 |     echo -e "\033[0;31m$message$reset_colors"
 11 |     
 12 |     # Set the error flag
 13 |     ERROR_OCCURRED=1
 14 |     
 15 |     # If script is being sourced (. or source)
 16 |     if [[ "${BASH_SOURCE[0]}" != "${0}" ]] || [[ -n "$ZSH_VERSION" && "$ZSH_EVAL_CONTEXT" == *:file:* ]]; then
 17 |         echo "Script terminated with error. Returning to shell."
 18 |         # Reset colors to ensure shell isn't affected
 19 |         echo -e "$reset_colors"
 20 |         # The return will be caught by the caller
 21 |         return 1
 22 |     else
 23 |         # If script is being executed directly
 24 |         exit 1
 25 |     fi
 26 | }
 27 | 
 28 | # Function to check if we should continue after potential error points
 29 | should_continue() {
 30 |     if [ $ERROR_OCCURRED -eq 1 ]; then
 31 |         # Reset colors to ensure shell isn't affected
 32 |         echo -e "\033[0m"
 33 |         return 1
 34 |     fi
 35 |     return 0
 36 | }
 37 | 
 38 | # Ensure script is running with a compatible shell
 39 | if [ -z "$BASH_VERSION" ] && [ -z "$ZSH_VERSION" ]; then
 40 |     handle_error "This script requires bash or zsh to run. Please run it with: bash $(basename "$0") or zsh $(basename "$0")"
 41 |     return 1 2>/dev/null || exit 1
 42 | fi
 43 | 
 44 | # Set shell options for compatibility
 45 | if [ -n "$ZSH_VERSION" ]; then
 46 |     # ZSH specific settings
 47 |     setopt SH_WORD_SPLIT
 48 |     setopt KSH_ARRAYS
 49 | fi
 50 | 
 51 | # Colors for better output - ensure they're properly reset after use
 52 | GREEN='\033[0;32m'
 53 | YELLOW='\033[0;33m'
 54 | RED='\033[0;31m'
 55 | NC='\033[0m' # No Color
 56 | 
 57 | echo -e "${GREEN}Azure DevOps MCP Server - Environment Setup${NC}"
 58 | echo "This script will help you set up your .env file with Azure DevOps credentials."
 59 | echo
 60 | 
 61 | # Clean up any existing create_pat.json file
 62 | if [ -f "create_pat.json" ]; then
 63 |     echo -e "${YELLOW}Cleaning up existing create_pat.json file...${NC}"
 64 |     rm -f create_pat.json
 65 | fi
 66 | 
 67 | # Check if Azure CLI is installed
 68 | if ! command -v az &> /dev/null; then
 69 |     handle_error "Error: Azure CLI is not installed.\nPlease install Azure CLI first: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli"
 70 |     return 1 2>/dev/null || exit 1
 71 | fi
 72 | should_continue || return 1 2>/dev/null || exit 1
 73 | 
 74 | # Check if Azure DevOps extension is installed
 75 | echo -e "${YELLOW}Checking for Azure DevOps extension...${NC}"
 76 | az devops &> /dev/null
 77 | if [ $? -ne 0 ]; then
 78 |     echo "Azure DevOps extension not found. Installing..."
 79 |     az extension add --name azure-devops
 80 |     if [ $? -ne 0 ]; then
 81 |         handle_error "Failed to install Azure DevOps extension."
 82 |         return 1 2>/dev/null || exit 1
 83 |     else
 84 |         echo -e "${GREEN}Azure DevOps extension installed successfully.${NC}"
 85 |     fi
 86 | else
 87 |     echo "Azure DevOps extension is already installed."
 88 | fi
 89 | should_continue || return 1 2>/dev/null || exit 1
 90 | 
 91 | # Check if jq is installed
 92 | if ! command -v jq &> /dev/null; then
 93 |     handle_error "Error: jq is not installed.\nPlease install jq first. On Ubuntu/Debian: sudo apt-get install jq\nOn macOS: brew install jq"
 94 |     return 1 2>/dev/null || exit 1
 95 | fi
 96 | should_continue || return 1 2>/dev/null || exit 1
 97 | 
 98 | # Check if already logged in
 99 | echo -e "\n${YELLOW}Step 1: Checking Azure CLI authentication...${NC}"
100 | if ! az account show &> /dev/null; then
101 |     echo "Not logged in. Initiating login..."
102 |     az login --allow-no-subscriptions
103 |     if [ $? -ne 0 ]; then
104 |         handle_error "Failed to login to Azure CLI."
105 |         return 1 2>/dev/null || exit 1
106 |     fi
107 | else
108 |     echo -e "${GREEN}Already logged in to Azure CLI.${NC}"
109 | fi
110 | should_continue || return 1 2>/dev/null || exit 1
111 | 
112 | # Get Azure DevOps Organizations using REST API
113 | echo -e "\n${YELLOW}Step 2: Fetching your Azure DevOps organizations...${NC}"
114 | echo "This may take a moment..."
115 | 
116 | # First get the user profile
117 | echo "Getting user profile..."
118 | 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)
119 | profile_status=$?
120 | 
121 | if [ $profile_status -ne 0 ]; then
122 |     echo -e "${RED}Error: Failed to get user profile${NC}"
123 |     echo -e "${RED}Status code: $profile_status${NC}"
124 |     echo -e "${RED}Error response:${NC}"
125 |     echo "$profile_response"
126 |     echo
127 |     echo "Manually provide your organization name instead."
128 |     read -p "Enter your Azure DevOps organization name: " org_name
129 | else
130 |     echo "Profile API response:"
131 |     echo "$profile_response"
132 |     echo
133 |     public_alias=$(echo "$profile_response" | jq -r '.publicAlias')
134 |     
135 |     if [ "$public_alias" = "null" ] || [ -z "$public_alias" ]; then
136 |         echo -e "${RED}Failed to extract publicAlias from response.${NC}"
137 |         echo "Full response was:"
138 |         echo "$profile_response"
139 |         echo
140 |         echo "Manually provide your organization name instead."
141 |         read -p "Enter your Azure DevOps organization name: " org_name
142 |     else
143 |         # Get organizations using the publicAlias
144 |         echo "Fetching organizations..."
145 |         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")
146 |         
147 |         # Extract organization names from the response using jq
148 |         orgs=$(echo "$orgs_result" | jq -r '.value[].accountName')
149 |         
150 |         if [ -z "$orgs" ]; then
151 |             echo -e "${RED}No organizations found.${NC}"
152 |             echo "Manually provide your organization name instead."
153 |             read -p "Enter your Azure DevOps organization name: " org_name
154 |         else
155 |             # Display organizations for selection
156 |             echo -e "\nYour Azure DevOps organizations:"
157 |             i=1
158 |             OLDIFS=$IFS
159 |             IFS=$'\n'
160 |             # Create array in a shell-agnostic way
161 |             orgs_array=()
162 |             while IFS= read -r line; do
163 |                 [ -n "$line" ] && orgs_array+=("$line")
164 |             done <<< "$orgs"
165 |             IFS=$OLDIFS
166 |             
167 |             # Check if array is empty
168 |             if [ ${#orgs_array[@]} -eq 0 ]; then
169 |                 echo -e "${RED}Failed to parse organizations list.${NC}"
170 |                 echo "Manually provide your organization name instead."
171 |                 read -p "Enter your Azure DevOps organization name: " org_name
172 |             else
173 |                 # Display organizations with explicit indexing
174 |                 for ((idx=0; idx<${#orgs_array[@]}; idx++)); do
175 |                     echo "$((idx+1)) ${orgs_array[$idx]}"
176 |                 done
177 |                 
178 |                 # Prompt for selection
179 |                 read -p "Select an organization (1-${#orgs_array[@]}): " org_selection
180 |                 
181 |                 if [[ "$org_selection" =~ ^[0-9]+$ ]] && [ "$org_selection" -ge 1 ] && [ "$org_selection" -le "${#orgs_array[@]}" ]; then
182 |                     org_name=${orgs_array[$((org_selection-1))]}
183 |                 else
184 |                     handle_error "Invalid selection. Please run the script again."
185 |                     return 1 2>/dev/null || exit 1
186 |                 fi
187 |             fi
188 |         fi
189 |     fi
190 | fi
191 | should_continue || return 1 2>/dev/null || exit 1
192 | 
193 | org_url="https://dev.azure.com/$org_name"
194 | echo -e "${GREEN}Using organization URL: $org_url${NC}"
195 | 
196 | # Get Default Project (Optional)
197 | echo -e "\n${YELLOW}Step 3: Would you like to set a default project? (y/n)${NC}"
198 | read -p "Select option: " set_default_project
199 | 
200 | default_project=""
201 | if [[ "$set_default_project" = "y" || "$set_default_project" = "Y" ]]; then
202 |     # Configure az devops to use the selected organization
203 |     az devops configure --defaults organization=$org_url
204 |     
205 |     # List projects
206 |     echo "Fetching projects from $org_name..."
207 |     projects=$(az devops project list --query "value[].name" -o tsv)
208 |     
209 |     if [ $? -ne 0 ] || [ -z "$projects" ]; then
210 |         echo -e "${YELLOW}No projects found or unable to list projects.${NC}"
211 |         read -p "Enter a default project name (leave blank to skip): " default_project
212 |     else
213 |         # Display projects for selection
214 |         echo -e "\nAvailable projects in $org_name:"
215 |         OLDIFS=$IFS
216 |         IFS=$'\n'
217 |         # Create array in a shell-agnostic way
218 |         projects_array=()
219 |         while IFS= read -r line; do
220 |             [ -n "$line" ] && projects_array+=("$line")
221 |         done <<< "$projects"
222 |         IFS=$OLDIFS
223 |         
224 |         # Check if array is empty
225 |         if [ ${#projects_array[@]} -eq 0 ]; then
226 |             echo -e "${YELLOW}Failed to parse projects list.${NC}"
227 |             read -p "Enter a default project name (leave blank to skip): " default_project
228 |         else
229 |             # Display projects with explicit indexing
230 |             for ((idx=0; idx<${#projects_array[@]}; idx++)); do
231 |                 echo "$((idx+1)) ${projects_array[$idx]}"
232 |             done
233 |             
234 |             echo "$((${#projects_array[@]}+1)) Skip setting a default project"
235 |             
236 |             # Prompt for selection
237 |             read -p "Select a default project (1-$((${#projects_array[@]}+1))): " project_selection
238 |             
239 |             if [[ "$project_selection" =~ ^[0-9]+$ ]] && [ "$project_selection" -ge 1 ] && [ "$project_selection" -lt "$((${#projects_array[@]}+1))" ]; then
240 |                 default_project=${projects_array[$((project_selection-1))]}
241 |                 echo -e "${GREEN}Using default project: $default_project${NC}"
242 |             else
243 |                 echo "No default project selected."
244 |             fi
245 |         fi
246 |     fi
247 | fi
248 | 
249 | # Create .env file
250 | echo -e "\n${YELLOW}Step 5: Creating .env file...${NC}"
251 | 
252 | cat > .env << EOF
253 | # Azure DevOps MCP Server - Environment Variables
254 | 
255 | # Azure DevOps Organization Name (selected from your available organizations)
256 | AZURE_DEVOPS_ORG=$org_name
257 | 
258 | # Azure DevOps Organization URL (required)
259 | AZURE_DEVOPS_ORG_URL=$org_url
260 | 
261 | 
262 | AZURE_DEVOPS_AUTH_METHOD=azure-identity
263 | EOF
264 | 
265 | # Add default project if specified
266 | if [ ! -z "$default_project" ]; then
267 | cat >> .env << EOF
268 | 
269 | # Default Project to use when not specified
270 | AZURE_DEVOPS_DEFAULT_PROJECT=$default_project
271 | EOF
272 | else
273 | cat >> .env << EOF
274 | 
275 | # Default Project to use when not specified (optional)
276 | # AZURE_DEVOPS_DEFAULT_PROJECT=your-default-project
277 | EOF
278 | fi
279 | 
280 | # Add remaining configuration
281 | cat >> .env << EOF
282 | 
283 | # API Version to use (optional, defaults to latest)
284 | # AZURE_DEVOPS_API_VERSION=6.0
285 | 
286 | # Server Configuration
287 | PORT=3000
288 | HOST=localhost
289 | 
290 | # Logging Level (debug, info, warn, error)
291 | LOG_LEVEL=info
292 | EOF
293 | 
294 | echo -e "\n${GREEN}Environment setup completed successfully!${NC}"
295 | echo "Your .env file has been created with the following configuration:"
296 | echo "- Organization: $org_name"
297 | echo "- Organization URL: $org_url"
298 | if [ ! -z "$default_project" ]; then
299 |     echo "- Default Project: $default_project"
300 | fi
301 | echo "- PAT: Created with expanded scopes for full integration"
302 | echo
303 | echo "You can now run your Azure DevOps MCP Server with:"
304 | echo "  npm run dev"
305 | echo
306 | echo "You can also run integration tests with:"
307 | echo "  npm run test:integration"
308 | 
309 | # At the end of the script, ensure colors are reset
310 | echo -e "${NC}" 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { IGitApi } from 'azure-devops-node-api/GitApi';
  3 | import {
  4 |   GitVersionType,
  5 |   VersionControlRecursionType,
  6 |   GitItem,
  7 |   GitObjectType,
  8 | } from 'azure-devops-node-api/interfaces/GitInterfaces';
  9 | import { minimatch } from 'minimatch';
 10 | import { AzureDevOpsError } from '../../../shared/errors';
 11 | import {
 12 |   GetAllRepositoriesTreeOptions,
 13 |   AllRepositoriesTreeResponse,
 14 |   RepositoryTreeResponse,
 15 |   RepositoryTreeItem,
 16 |   GitRepository,
 17 | } from '../types';
 18 | 
 19 | /**
 20 |  * Get tree view of files/directories across multiple repositories
 21 |  *
 22 |  * @param connection The Azure DevOps WebApi connection
 23 |  * @param options Options for getting repository tree
 24 |  * @returns Tree structure for each repository
 25 |  */
 26 | export async function getAllRepositoriesTree(
 27 |   connection: WebApi,
 28 |   options: GetAllRepositoriesTreeOptions,
 29 | ): Promise<AllRepositoriesTreeResponse> {
 30 |   try {
 31 |     const gitApi = await connection.getGitApi();
 32 |     let repositories: GitRepository[] = [];
 33 | 
 34 |     // Get all repositories in the project
 35 |     repositories = await gitApi.getRepositories(options.projectId);
 36 | 
 37 |     // Filter repositories by name pattern if specified
 38 |     if (options.repositoryPattern) {
 39 |       repositories = repositories.filter((repo) =>
 40 |         minimatch(repo.name || '', options.repositoryPattern || '*'),
 41 |       );
 42 |     }
 43 | 
 44 |     // Initialize results array
 45 |     const results: RepositoryTreeResponse[] = [];
 46 | 
 47 |     // Process each repository
 48 |     for (const repo of repositories) {
 49 |       try {
 50 |         // Get default branch ref
 51 |         const defaultBranch = repo.defaultBranch;
 52 |         if (!defaultBranch) {
 53 |           // Skip repositories with no default branch
 54 |           results.push({
 55 |             name: repo.name || 'Unknown',
 56 |             tree: [],
 57 |             stats: { directories: 0, files: 0 },
 58 |             error: 'No default branch found',
 59 |           });
 60 |           continue;
 61 |         }
 62 | 
 63 |         // Clean the branch name (remove refs/heads/ prefix)
 64 |         const branchRef = defaultBranch.replace('refs/heads/', '');
 65 | 
 66 |         // Initialize tree items array and counters
 67 |         const treeItems: RepositoryTreeItem[] = [];
 68 |         const stats = { directories: 0, files: 0 };
 69 | 
 70 |         // Determine the recursion level and processing approach
 71 |         const depth = options.depth !== undefined ? options.depth : 0; // Default to 0 (max depth)
 72 | 
 73 |         if (depth === 0) {
 74 |           // For max depth (0), use server-side recursion for better performance
 75 |           const allItems = await gitApi.getItems(
 76 |             repo.id || '',
 77 |             options.projectId,
 78 |             '/',
 79 |             VersionControlRecursionType.Full, // Use full recursion
 80 |             true,
 81 |             false,
 82 |             false,
 83 |             false,
 84 |             {
 85 |               version: branchRef,
 86 |               versionType: GitVersionType.Branch,
 87 |             },
 88 |           );
 89 | 
 90 |           // Filter out the root item itself and bad items
 91 |           const itemsToProcess = allItems.filter(
 92 |             (item) =>
 93 |               item.path !== '/' && item.gitObjectType !== GitObjectType.Bad,
 94 |           );
 95 | 
 96 |           // Process all items at once (they're already retrieved recursively)
 97 |           processItemsNonRecursive(
 98 |             itemsToProcess,
 99 |             treeItems,
100 |             stats,
101 |             options.pattern,
102 |           );
103 |         } else {
104 |           // For limited depth, use the regular recursive approach
105 |           // Get items at the root level
106 |           const rootItems = await gitApi.getItems(
107 |             repo.id || '',
108 |             options.projectId,
109 |             '/',
110 |             VersionControlRecursionType.OneLevel,
111 |             true,
112 |             false,
113 |             false,
114 |             false,
115 |             {
116 |               version: branchRef,
117 |               versionType: GitVersionType.Branch,
118 |             },
119 |           );
120 | 
121 |           // Filter out the root item itself and bad items
122 |           const itemsToProcess = rootItems.filter(
123 |             (item) =>
124 |               item.path !== '/' && item.gitObjectType !== GitObjectType.Bad,
125 |           );
126 | 
127 |           // Process the root items and their children (up to specified depth)
128 |           await processItems(
129 |             gitApi,
130 |             repo.id || '',
131 |             options.projectId,
132 |             itemsToProcess,
133 |             branchRef,
134 |             treeItems,
135 |             stats,
136 |             1,
137 |             depth,
138 |             options.pattern,
139 |           );
140 |         }
141 | 
142 |         // Add repository tree to results
143 |         results.push({
144 |           name: repo.name || 'Unknown',
145 |           tree: treeItems,
146 |           stats,
147 |         });
148 |       } catch (repoError) {
149 |         // Handle errors for individual repositories
150 |         results.push({
151 |           name: repo.name || 'Unknown',
152 |           tree: [],
153 |           stats: { directories: 0, files: 0 },
154 |           error: `Error processing repository: ${repoError instanceof Error ? repoError.message : String(repoError)}`,
155 |         });
156 |       }
157 |     }
158 | 
159 |     return { repositories: results };
160 |   } catch (error) {
161 |     if (error instanceof AzureDevOpsError) {
162 |       throw error;
163 |     }
164 |     throw new Error(
165 |       `Failed to get repository tree: ${error instanceof Error ? error.message : String(error)}`,
166 |     );
167 |   }
168 | }
169 | 
170 | /**
171 |  * Process items non-recursively when they're already retrieved with VersionControlRecursionType.Full
172 |  */
173 | function processItemsNonRecursive(
174 |   items: GitItem[],
175 |   result: RepositoryTreeItem[],
176 |   stats: { directories: number; files: number },
177 |   pattern?: string,
178 | ): void {
179 |   // Sort items (folders first, then by path)
180 |   const sortedItems = [...items].sort((a, b) => {
181 |     if (a.isFolder === b.isFolder) {
182 |       return (a.path || '').localeCompare(b.path || '');
183 |     }
184 |     return a.isFolder ? -1 : 1;
185 |   });
186 | 
187 |   for (const item of sortedItems) {
188 |     const name = item.path?.split('/').pop() || '';
189 |     const path = item.path || '';
190 |     const isFolder = !!item.isFolder;
191 | 
192 |     // Skip the root folder
193 |     if (path === '/') {
194 |       continue;
195 |     }
196 | 
197 |     // Calculate level from path segments
198 |     // Remove leading '/' then count segments
199 |     // For paths like:
200 |     // /README.md -> ["README.md"] -> length 1 -> level 1
201 |     // /src/index.ts -> ["src", "index.ts"] -> length 2 -> level 2
202 |     // /src/utils/helper.ts -> ["src", "utils", "helper.ts"] -> length 3 -> level 3
203 |     const pathSegments = path.replace(/^\//, '').split('/');
204 |     const level = pathSegments.length;
205 | 
206 |     // Filter files based on pattern (if specified)
207 |     if (!isFolder && pattern && !minimatch(name, pattern)) {
208 |       continue;
209 |     }
210 | 
211 |     // Add item to results
212 |     result.push({
213 |       name,
214 |       path,
215 |       isFolder,
216 |       level,
217 |     });
218 | 
219 |     // Update counters
220 |     if (isFolder) {
221 |       stats.directories++;
222 |     } else {
223 |       stats.files++;
224 |     }
225 |   }
226 | }
227 | 
228 | /**
229 |  * Process items recursively up to the specified depth
230 |  */
231 | async function processItems(
232 |   gitApi: IGitApi,
233 |   repoId: string,
234 |   projectId: string,
235 |   items: GitItem[],
236 |   branchRef: string,
237 |   result: RepositoryTreeItem[],
238 |   stats: { directories: number; files: number },
239 |   currentDepth: number,
240 |   maxDepth: number,
241 |   pattern?: string,
242 | ): Promise<void> {
243 |   // Sort items (directories first, then files)
244 |   const sortedItems = [...items].sort((a, b) => {
245 |     if (a.isFolder === b.isFolder) {
246 |       return (a.path || '').localeCompare(b.path || '');
247 |     }
248 |     return a.isFolder ? -1 : 1;
249 |   });
250 | 
251 |   for (const item of sortedItems) {
252 |     const name = item.path?.split('/').pop() || '';
253 |     const path = item.path || '';
254 |     const isFolder = !!item.isFolder;
255 | 
256 |     // Filter files based on pattern (if specified)
257 |     if (!isFolder && pattern && !minimatch(name, pattern)) {
258 |       continue;
259 |     }
260 | 
261 |     // Add item to results
262 |     result.push({
263 |       name,
264 |       path,
265 |       isFolder,
266 |       level: currentDepth,
267 |     });
268 | 
269 |     // Update counters
270 |     if (isFolder) {
271 |       stats.directories++;
272 |     } else {
273 |       stats.files++;
274 |     }
275 | 
276 |     // Recursively process folders if not yet at max depth
277 |     if (isFolder && currentDepth < maxDepth) {
278 |       try {
279 |         const childItems = await gitApi.getItems(
280 |           repoId,
281 |           projectId,
282 |           path,
283 |           VersionControlRecursionType.OneLevel,
284 |           true,
285 |           false,
286 |           false,
287 |           false,
288 |           {
289 |             version: branchRef,
290 |             versionType: GitVersionType.Branch,
291 |           },
292 |         );
293 | 
294 |         // Filter out the parent folder itself and bad items
295 |         const itemsToProcess = childItems.filter(
296 |           (child: GitItem) =>
297 |             child.path !== path && child.gitObjectType !== GitObjectType.Bad,
298 |         );
299 | 
300 |         // Process child items
301 |         await processItems(
302 |           gitApi,
303 |           repoId,
304 |           projectId,
305 |           itemsToProcess,
306 |           branchRef,
307 |           result,
308 |           stats,
309 |           currentDepth + 1,
310 |           maxDepth,
311 |           pattern,
312 |         );
313 |       } catch (error) {
314 |         // Ignore errors in child items and continue with siblings
315 |         console.error(`Error processing folder ${path}: ${error}`);
316 |       }
317 |     }
318 |   }
319 | }
320 | 
321 | /**
322 |  * Convert the tree items to a formatted ASCII string representation
323 |  *
324 |  * @param repoName Repository name
325 |  * @param items Tree items
326 |  * @param stats Statistics about files and directories
327 |  * @returns Formatted ASCII string
328 |  */
329 | export function formatRepositoryTree(
330 |   repoName: string,
331 |   items: RepositoryTreeItem[],
332 |   stats: { directories: number; files: number },
333 |   error?: string,
334 | ): string {
335 |   let output = `${repoName}/\n`;
336 | 
337 |   if (error) {
338 |     output += `  (${error})\n`;
339 |   } else if (items.length === 0) {
340 |     output += '  (Repository is empty or default branch not found)\n';
341 |   } else {
342 |     // Sort items by path to ensure proper sequence
343 |     const sortedItems = [...items].sort((a, b) => {
344 |       // Sort by level first
345 |       if (a.level !== b.level) {
346 |         return a.level - b.level;
347 |       }
348 |       // Then folders before files
349 |       if (a.isFolder !== b.isFolder) {
350 |         return a.isFolder ? -1 : 1;
351 |       }
352 |       // Then alphabetically
353 |       return a.path.localeCompare(b.path);
354 |     });
355 | 
356 |     // Create a structured tree representation
357 |     const tree = createTreeStructure(sortedItems);
358 | 
359 |     // Format the tree starting from the root
360 |     output += formatTree(tree, '  ');
361 |   }
362 | 
363 |   // Add summary line
364 |   output += `${stats.directories} directories, ${stats.files} files\n`;
365 | 
366 |   return output;
367 | }
368 | 
369 | /**
370 |  * Create a structured tree from the flat list of items
371 |  */
372 | function createTreeStructure(items: RepositoryTreeItem[]): TreeNode {
373 |   const root: TreeNode = {
374 |     name: '',
375 |     path: '',
376 |     isFolder: true,
377 |     children: [],
378 |   };
379 | 
380 |   // Map to track all nodes by path
381 |   const nodeMap: Record<string, TreeNode> = { '': root };
382 | 
383 |   // First create all nodes
384 |   for (const item of items) {
385 |     nodeMap[item.path] = {
386 |       name: item.name,
387 |       path: item.path,
388 |       isFolder: item.isFolder,
389 |       children: [],
390 |     };
391 |   }
392 | 
393 |   // Then build the hierarchy
394 |   for (const item of items) {
395 |     if (item.path === '/') continue;
396 | 
397 |     const node = nodeMap[item.path];
398 |     const lastSlashIndex = item.path.lastIndexOf('/');
399 | 
400 |     // For root level items, the parent path is empty
401 |     const parentPath =
402 |       lastSlashIndex <= 0 ? '' : item.path.substring(0, lastSlashIndex);
403 | 
404 |     // Get parent node (defaults to root if parent not found)
405 |     const parent = nodeMap[parentPath] || root;
406 | 
407 |     // Add this node as a child of its parent
408 |     parent.children.push(node);
409 |   }
410 | 
411 |   return root;
412 | }
413 | 
414 | /**
415 |  * Format a tree structure into an ASCII tree representation
416 |  */
417 | function formatTree(node: TreeNode, indent: string): string {
418 |   if (!node.children.length) return '';
419 | 
420 |   let output = '';
421 | 
422 |   // Sort the children: folders first, then alphabetically
423 |   const children = [...node.children].sort((a, b) => {
424 |     if (a.isFolder !== b.isFolder) {
425 |       return a.isFolder ? -1 : 1;
426 |     }
427 |     return a.name.localeCompare(b.name);
428 |   });
429 | 
430 |   // Format each child node
431 |   for (let i = 0; i < children.length; i++) {
432 |     const child = children[i];
433 |     const isLast = i === children.length - 1;
434 |     const connector = isLast ? '`-- ' : '|-- ';
435 |     const childIndent = isLast ? '    ' : '|   ';
436 | 
437 |     // Add the node itself
438 |     const suffix = child.isFolder ? '/' : '';
439 |     output += `${indent}${connector}${child.name}${suffix}\n`;
440 | 
441 |     // Recursively add its children
442 |     if (child.children.length > 0) {
443 |       output += formatTree(child, indent + childIndent);
444 |     }
445 |   }
446 | 
447 |   return output;
448 | }
449 | 
450 | /**
451 |  * Tree node interface for hierarchical representation
452 |  */
453 | interface TreeNode {
454 |   name: string;
455 |   path: string;
456 |   isFolder: boolean;
457 |   children: TreeNode[];
458 | }
459 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { getPullRequestComments } from './feature';
  3 | import { GitPullRequestCommentThread } from 'azure-devops-node-api/interfaces/GitInterfaces';
  4 | 
  5 | describe('getPullRequestComments', () => {
  6 |   afterEach(() => {
  7 |     jest.resetAllMocks();
  8 |   });
  9 | 
 10 |   test('should return pull request comment threads with file path and line number', async () => {
 11 |     // Mock data for a comment thread
 12 |     const mockCommentThreads: GitPullRequestCommentThread[] = [
 13 |       {
 14 |         id: 1,
 15 |         status: 1, // Active
 16 |         threadContext: {
 17 |           filePath: '/src/app.ts',
 18 |           rightFileStart: {
 19 |             line: 10,
 20 |             offset: 5,
 21 |           },
 22 |           rightFileEnd: {
 23 |             line: 10,
 24 |             offset: 15,
 25 |           },
 26 |         },
 27 |         comments: [
 28 |           {
 29 |             id: 100,
 30 |             content: 'This code needs refactoring',
 31 |             commentType: 1, // CodeChange
 32 |             author: {
 33 |               displayName: 'Test User',
 34 |               id: 'test-user-id',
 35 |             },
 36 |             publishedDate: new Date(),
 37 |           },
 38 |           {
 39 |             id: 101,
 40 |             parentCommentId: 100,
 41 |             content: 'I agree, will update',
 42 |             commentType: 1, // CodeChange
 43 |             author: {
 44 |               displayName: 'Another User',
 45 |               id: 'another-user-id',
 46 |             },
 47 |             publishedDate: new Date(),
 48 |           },
 49 |         ],
 50 |       },
 51 |     ];
 52 | 
 53 |     // Setup mock connection
 54 |     const mockGitApi = {
 55 |       getThreads: jest.fn().mockResolvedValue(mockCommentThreads),
 56 |       getPullRequestThread: jest.fn(),
 57 |     };
 58 | 
 59 |     const mockConnection: any = {
 60 |       getGitApi: jest.fn().mockResolvedValue(mockGitApi),
 61 |     };
 62 | 
 63 |     // Call the function with test parameters
 64 |     const projectId = 'test-project';
 65 |     const repositoryId = 'test-repo';
 66 |     const pullRequestId = 123;
 67 |     const options = {
 68 |       projectId,
 69 |       repositoryId,
 70 |       pullRequestId,
 71 |     };
 72 | 
 73 |     const result = await getPullRequestComments(
 74 |       mockConnection as WebApi,
 75 |       projectId,
 76 |       repositoryId,
 77 |       pullRequestId,
 78 |       options,
 79 |     );
 80 | 
 81 |     // Verify results
 82 |     expect(result).toHaveLength(1);
 83 |     expect(result[0].comments).toHaveLength(2);
 84 | 
 85 |     // Verify file path and line number are added to each comment
 86 |     result[0].comments?.forEach((comment) => {
 87 |       expect(comment).toHaveProperty('filePath', '/src/app.ts');
 88 |       expect(comment).toHaveProperty('rightFileStart', { line: 10, offset: 5 });
 89 |       expect(comment).toHaveProperty('rightFileEnd', { line: 10, offset: 15 });
 90 |       expect(comment).toHaveProperty('leftFileStart', undefined);
 91 |       expect(comment).toHaveProperty('leftFileEnd', undefined);
 92 |     });
 93 | 
 94 |     expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1);
 95 |     expect(mockGitApi.getThreads).toHaveBeenCalledTimes(1);
 96 |     expect(mockGitApi.getThreads).toHaveBeenCalledWith(
 97 |       repositoryId,
 98 |       pullRequestId,
 99 |       projectId,
100 |       undefined,
101 |       undefined,
102 |     );
103 |     expect(mockGitApi.getPullRequestThread).not.toHaveBeenCalled();
104 |   });
105 | 
106 |   test('should handle comments without thread context', async () => {
107 |     // Mock data for a comment thread without thread context
108 |     const mockCommentThreads: GitPullRequestCommentThread[] = [
109 |       {
110 |         id: 1,
111 |         status: 1, // Active
112 |         comments: [
113 |           {
114 |             id: 100,
115 |             content: 'General comment',
116 |             commentType: 1,
117 |             author: {
118 |               displayName: 'Test User',
119 |               id: 'test-user-id',
120 |             },
121 |             publishedDate: new Date(),
122 |           },
123 |         ],
124 |       },
125 |     ];
126 | 
127 |     // Setup mock connection
128 |     const mockGitApi = {
129 |       getThreads: jest.fn().mockResolvedValue(mockCommentThreads),
130 |       getPullRequestThread: jest.fn(),
131 |     };
132 | 
133 |     const mockConnection: any = {
134 |       getGitApi: jest.fn().mockResolvedValue(mockGitApi),
135 |     };
136 | 
137 |     const result = await getPullRequestComments(
138 |       mockConnection as WebApi,
139 |       'test-project',
140 |       'test-repo',
141 |       123,
142 |       {
143 |         projectId: 'test-project',
144 |         repositoryId: 'test-repo',
145 |         pullRequestId: 123,
146 |       },
147 |     );
148 | 
149 |     // Verify results
150 |     expect(result).toHaveLength(1);
151 |     expect(result[0].comments).toHaveLength(1);
152 |     expect(result[0].status).toBe('active');
153 | 
154 |     // Verify file path and line number are null for comments without thread context
155 |     const comment = result[0].comments![0];
156 |     expect(comment).toHaveProperty('filePath', undefined);
157 |     expect(comment).toHaveProperty('rightFileStart', undefined);
158 |     expect(comment).toHaveProperty('rightFileEnd', undefined);
159 |     expect(comment).toHaveProperty('leftFileStart', undefined);
160 |     expect(comment).toHaveProperty('leftFileEnd', undefined);
161 |     expect(comment).toHaveProperty('commentType', 'text');
162 |   });
163 | 
164 |   test('should use leftFileStart when rightFileStart is not available', async () => {
165 |     // Mock data for a comment thread with only leftFileStart
166 |     const mockCommentThreads: GitPullRequestCommentThread[] = [
167 |       {
168 |         id: 1,
169 |         status: 1,
170 |         threadContext: {
171 |           filePath: '/src/app.ts',
172 |           leftFileStart: {
173 |             line: 5,
174 |             offset: 1,
175 |           },
176 |         },
177 |         comments: [
178 |           {
179 |             id: 100,
180 |             content: 'Comment on deleted line',
181 |             commentType: 1,
182 |             author: {
183 |               displayName: 'Test User',
184 |               id: 'test-user-id',
185 |             },
186 |             publishedDate: new Date(),
187 |           },
188 |         ],
189 |       },
190 |     ];
191 | 
192 |     // Setup mock connection
193 |     const mockGitApi = {
194 |       getThreads: jest.fn().mockResolvedValue(mockCommentThreads),
195 |       getPullRequestThread: jest.fn(),
196 |     };
197 | 
198 |     const mockConnection: any = {
199 |       getGitApi: jest.fn().mockResolvedValue(mockGitApi),
200 |     };
201 | 
202 |     const result = await getPullRequestComments(
203 |       mockConnection as WebApi,
204 |       'test-project',
205 |       'test-repo',
206 |       123,
207 |       {
208 |         projectId: 'test-project',
209 |         repositoryId: 'test-repo',
210 |         pullRequestId: 123,
211 |       },
212 |     );
213 | 
214 |     // Verify results
215 |     expect(result).toHaveLength(1);
216 |     expect(result[0].comments).toHaveLength(1);
217 | 
218 |     // Verify rightFileStart is undefined, leftFileStart is present
219 |     const comment = result[0].comments![0];
220 |     expect(comment).toHaveProperty('filePath', '/src/app.ts');
221 |     expect(comment).toHaveProperty('leftFileStart', { line: 5, offset: 1 });
222 |     expect(comment).toHaveProperty('rightFileStart', undefined);
223 |     expect(comment).toHaveProperty('leftFileEnd', undefined);
224 |     expect(comment).toHaveProperty('rightFileEnd', undefined);
225 |   });
226 | 
227 |   test('should return a specific comment thread when threadId is provided', async () => {
228 |     // Mock data for a specific comment thread
229 |     const threadId = 42;
230 |     const mockCommentThread: GitPullRequestCommentThread = {
231 |       id: threadId,
232 |       status: 1, // Active
233 |       threadContext: {
234 |         filePath: '/src/utils.ts',
235 |         rightFileStart: {
236 |           line: 15,
237 |           offset: 1,
238 |         },
239 |       },
240 |       comments: [
241 |         {
242 |           id: 100,
243 |           content: 'Specific comment',
244 |           commentType: 1, // CodeChange
245 |           author: {
246 |             displayName: 'Test User',
247 |             id: 'test-user-id',
248 |           },
249 |           publishedDate: new Date(),
250 |         },
251 |       ],
252 |     };
253 | 
254 |     // Setup mock connection
255 |     const mockGitApi = {
256 |       getThreads: jest.fn(),
257 |       getPullRequestThread: jest.fn().mockResolvedValue(mockCommentThread),
258 |     };
259 | 
260 |     const mockConnection: any = {
261 |       getGitApi: jest.fn().mockResolvedValue(mockGitApi),
262 |     };
263 | 
264 |     // Call the function with test parameters
265 |     const projectId = 'test-project';
266 |     const repositoryId = 'test-repo';
267 |     const pullRequestId = 123;
268 |     const options = {
269 |       projectId,
270 |       repositoryId,
271 |       pullRequestId,
272 |       threadId,
273 |     };
274 | 
275 |     const result = await getPullRequestComments(
276 |       mockConnection as WebApi,
277 |       projectId,
278 |       repositoryId,
279 |       pullRequestId,
280 |       options,
281 |     );
282 | 
283 |     // Verify results
284 |     expect(result).toHaveLength(1);
285 |     expect(result[0].id).toBe(threadId);
286 |     expect(result[0].comments).toHaveLength(1);
287 | 
288 |     // Verify file path and line number are added
289 |     const comment = result[0].comments![0];
290 |     expect(comment).toHaveProperty('filePath', '/src/utils.ts');
291 |     expect(comment).toHaveProperty('rightFileStart', { line: 15, offset: 1 });
292 |     expect(comment).toHaveProperty('leftFileStart', undefined);
293 |     expect(comment).toHaveProperty('leftFileEnd', undefined);
294 |     expect(comment).toHaveProperty('rightFileEnd', undefined);
295 | 
296 |     expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1);
297 |     expect(mockGitApi.getPullRequestThread).toHaveBeenCalledTimes(1);
298 |     expect(mockGitApi.getPullRequestThread).toHaveBeenCalledWith(
299 |       repositoryId,
300 |       pullRequestId,
301 |       threadId,
302 |       projectId,
303 |     );
304 |     expect(mockGitApi.getThreads).not.toHaveBeenCalled();
305 |   });
306 | 
307 |   test('should handle pagination when top parameter is provided', async () => {
308 |     // Mock data for multiple comment threads
309 |     const mockCommentThreads: GitPullRequestCommentThread[] = [
310 |       {
311 |         id: 1,
312 |         status: 1,
313 |         threadContext: {
314 |           filePath: '/src/file1.ts',
315 |           rightFileStart: { line: 1, offset: 1 },
316 |         },
317 |         comments: [{ id: 100, content: 'Comment 1' }],
318 |       },
319 |       {
320 |         id: 2,
321 |         status: 1,
322 |         threadContext: {
323 |           filePath: '/src/file2.ts',
324 |           rightFileStart: { line: 2, offset: 1 },
325 |         },
326 |         comments: [{ id: 101, content: 'Comment 2' }],
327 |       },
328 |       {
329 |         id: 3,
330 |         status: 1,
331 |         threadContext: {
332 |           filePath: '/src/file3.ts',
333 |           rightFileStart: { line: 3, offset: 1 },
334 |         },
335 |         comments: [{ id: 102, content: 'Comment 3' }],
336 |       },
337 |     ];
338 | 
339 |     // Setup mock connection
340 |     const mockGitApi = {
341 |       getThreads: jest.fn().mockResolvedValue(mockCommentThreads),
342 |       getPullRequestThread: jest.fn(),
343 |     };
344 | 
345 |     const mockConnection: any = {
346 |       getGitApi: jest.fn().mockResolvedValue(mockGitApi),
347 |     };
348 | 
349 |     // Call the function with test parameters and top=2
350 |     const projectId = 'test-project';
351 |     const repositoryId = 'test-repo';
352 |     const pullRequestId = 123;
353 |     const options = {
354 |       projectId,
355 |       repositoryId,
356 |       pullRequestId,
357 |       top: 2,
358 |     };
359 | 
360 |     const result = await getPullRequestComments(
361 |       mockConnection as WebApi,
362 |       projectId,
363 |       repositoryId,
364 |       pullRequestId,
365 |       options,
366 |     );
367 | 
368 |     // Verify results (should only include first 2 threads)
369 |     expect(result).toHaveLength(2);
370 |     expect(result).toEqual(
371 |       mockCommentThreads.slice(0, 2).map((thread) => ({
372 |         ...thread,
373 |         status: 'active', // Transform enum to string
374 |         comments: thread.comments?.map((comment) => ({
375 |           ...comment,
376 |           commentType: undefined, // Will be undefined since mock doesn't have commentType
377 |           filePath: thread.threadContext?.filePath,
378 |           rightFileStart: thread.threadContext?.rightFileStart ?? undefined,
379 |           rightFileEnd: thread.threadContext?.rightFileEnd ?? undefined,
380 |           leftFileStart: thread.threadContext?.leftFileStart ?? undefined,
381 |           leftFileEnd: thread.threadContext?.leftFileEnd ?? undefined,
382 |         })),
383 |       })),
384 |     );
385 |     expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1);
386 |     expect(mockGitApi.getThreads).toHaveBeenCalledTimes(1);
387 |     expect(result[0].comments![0]).toHaveProperty('rightFileStart', {
388 |       line: 1,
389 |       offset: 1,
390 |     });
391 |     expect(result[1].comments![0]).toHaveProperty('rightFileStart', {
392 |       line: 2,
393 |       offset: 1,
394 |     });
395 |   });
396 | 
397 |   test('should handle error when API call fails', async () => {
398 |     // Setup mock connection with error
399 |     const errorMessage = 'API error';
400 |     const mockGitApi = {
401 |       getThreads: jest.fn().mockRejectedValue(new Error(errorMessage)),
402 |     };
403 | 
404 |     const mockConnection: any = {
405 |       getGitApi: jest.fn().mockResolvedValue(mockGitApi),
406 |     };
407 | 
408 |     // Call the function with test parameters
409 |     const projectId = 'test-project';
410 |     const repositoryId = 'test-repo';
411 |     const pullRequestId = 123;
412 |     const options = {
413 |       projectId,
414 |       repositoryId,
415 |       pullRequestId,
416 |     };
417 | 
418 |     // Verify error handling
419 |     await expect(
420 |       getPullRequestComments(
421 |         mockConnection as WebApi,
422 |         projectId,
423 |         repositoryId,
424 |         pullRequestId,
425 |         options,
426 |       ),
427 |     ).rejects.toThrow(`Failed to get pull request comments: ${errorMessage}`);
428 |   });
429 | });
430 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import {
  3 |   GitObjectType,
  4 |   VersionControlRecursionType,
  5 | } from 'azure-devops-node-api/interfaces/GitInterfaces';
  6 | import { getAllRepositoriesTree, formatRepositoryTree } from './feature';
  7 | import { RepositoryTreeItem } from '../types';
  8 | 
  9 | // Mock the Azure DevOps API
 10 | jest.mock('azure-devops-node-api');
 11 | 
 12 | describe('getAllRepositoriesTree', () => {
 13 |   // Sample repositories
 14 |   const mockRepos = [
 15 |     {
 16 |       id: 'repo1-id',
 17 |       name: 'repo1',
 18 |       defaultBranch: 'refs/heads/main',
 19 |     },
 20 |     {
 21 |       id: 'repo2-id',
 22 |       name: 'repo2',
 23 |       defaultBranch: 'refs/heads/master',
 24 |     },
 25 |     {
 26 |       id: 'repo3-id',
 27 |       name: 'repo3-api',
 28 |       defaultBranch: null, // No default branch
 29 |     },
 30 |   ];
 31 | 
 32 |   // Sample files/folders for repo1 at root level
 33 |   const mockRepo1RootItems = [
 34 |     {
 35 |       path: '/',
 36 |       gitObjectType: GitObjectType.Tree,
 37 |     },
 38 |     {
 39 |       path: '/README.md',
 40 |       isFolder: false,
 41 |       gitObjectType: GitObjectType.Blob,
 42 |     },
 43 |     {
 44 |       path: '/src',
 45 |       isFolder: true,
 46 |       gitObjectType: GitObjectType.Tree,
 47 |     },
 48 |     {
 49 |       path: '/package.json',
 50 |       isFolder: false,
 51 |       gitObjectType: GitObjectType.Blob,
 52 |     },
 53 |   ];
 54 | 
 55 |   // Sample files/folders for repo1 - src folder
 56 |   const mockRepo1SrcItems = [
 57 |     {
 58 |       path: '/src',
 59 |       isFolder: true,
 60 |       gitObjectType: GitObjectType.Tree,
 61 |     },
 62 |     {
 63 |       path: '/src/index.ts',
 64 |       isFolder: false,
 65 |       gitObjectType: GitObjectType.Blob,
 66 |     },
 67 |     {
 68 |       path: '/src/utils',
 69 |       isFolder: true,
 70 |       gitObjectType: GitObjectType.Tree,
 71 |     },
 72 |   ];
 73 | 
 74 |   // Sample files/folders for repo1 with unlimited depth (what server would return for Full recursion)
 75 |   const mockRepo1FullRecursionItems = [
 76 |     {
 77 |       path: '/',
 78 |       gitObjectType: GitObjectType.Tree,
 79 |     },
 80 |     {
 81 |       path: '/README.md',
 82 |       isFolder: false,
 83 |       gitObjectType: GitObjectType.Blob,
 84 |     },
 85 |     {
 86 |       path: '/src',
 87 |       isFolder: true,
 88 |       gitObjectType: GitObjectType.Tree,
 89 |     },
 90 |     {
 91 |       path: '/package.json',
 92 |       isFolder: false,
 93 |       gitObjectType: GitObjectType.Blob,
 94 |     },
 95 |     {
 96 |       path: '/src/index.ts',
 97 |       isFolder: false,
 98 |       gitObjectType: GitObjectType.Blob,
 99 |     },
100 |     {
101 |       path: '/src/utils',
102 |       isFolder: true,
103 |       gitObjectType: GitObjectType.Tree,
104 |     },
105 |     {
106 |       path: '/src/utils/helper.ts',
107 |       isFolder: false,
108 |       gitObjectType: GitObjectType.Blob,
109 |     },
110 |     {
111 |       path: '/src/utils/constants.ts',
112 |       isFolder: false,
113 |       gitObjectType: GitObjectType.Blob,
114 |     },
115 |   ];
116 | 
117 |   // Sample files/folders for repo2
118 |   const mockRepo2RootItems = [
119 |     {
120 |       path: '/',
121 |       gitObjectType: GitObjectType.Tree,
122 |     },
123 |     {
124 |       path: '/README.md',
125 |       isFolder: false,
126 |       gitObjectType: GitObjectType.Blob,
127 |     },
128 |     {
129 |       path: '/data.json',
130 |       isFolder: false,
131 |       gitObjectType: GitObjectType.Blob,
132 |     },
133 |   ];
134 | 
135 |   let mockConnection: jest.Mocked<WebApi>;
136 |   let mockGitApi: any;
137 | 
138 |   beforeEach(() => {
139 |     // Clear mocks
140 |     jest.clearAllMocks();
141 | 
142 |     // Create mock GitApi
143 |     mockGitApi = {
144 |       getRepositories: jest.fn().mockResolvedValue(mockRepos),
145 |       getItems: jest
146 |         .fn()
147 |         .mockImplementation((repoId, _projectId, path, recursionLevel) => {
148 |           if (repoId === 'repo1-id') {
149 |             if (recursionLevel === VersionControlRecursionType.Full) {
150 |               return Promise.resolve(mockRepo1FullRecursionItems);
151 |             } else if (path === '/') {
152 |               return Promise.resolve(mockRepo1RootItems);
153 |             } else if (path === '/src') {
154 |               return Promise.resolve(mockRepo1SrcItems);
155 |             }
156 |           } else if (repoId === 'repo2-id') {
157 |             if (recursionLevel === VersionControlRecursionType.Full) {
158 |               return Promise.resolve(mockRepo2RootItems);
159 |             } else if (path === '/') {
160 |               return Promise.resolve(mockRepo2RootItems);
161 |             }
162 |           }
163 |           return Promise.resolve([]);
164 |         }),
165 |     };
166 | 
167 |     // Create mock connection
168 |     mockConnection = {
169 |       getGitApi: jest.fn().mockResolvedValue(mockGitApi),
170 |     } as unknown as jest.Mocked<WebApi>;
171 |   });
172 | 
173 |   it('should return tree structures for multiple repositories with limited depth', async () => {
174 |     // Arrange
175 |     const options = {
176 |       organizationId: 'testOrg',
177 |       projectId: 'testProject',
178 |       depth: 2, // Limited depth
179 |     };
180 | 
181 |     // Act
182 |     const result = await getAllRepositoriesTree(mockConnection, options);
183 | 
184 |     // Assert
185 |     expect(mockGitApi.getRepositories).toHaveBeenCalledWith('testProject');
186 |     expect(result.repositories.length).toBe(3);
187 | 
188 |     // Verify repo1 tree
189 |     const repo1 = result.repositories.find((r) => r.name === 'repo1');
190 |     expect(repo1).toBeDefined();
191 |     expect(repo1?.tree.length).toBeGreaterThan(0);
192 |     expect(repo1?.stats.directories).toBeGreaterThan(0);
193 |     expect(repo1?.stats.files).toBeGreaterThan(0);
194 | 
195 |     // Verify repo2 tree
196 |     const repo2 = result.repositories.find((r) => r.name === 'repo2');
197 |     expect(repo2).toBeDefined();
198 |     expect(repo2?.tree.length).toBeGreaterThan(0);
199 | 
200 |     // Verify repo3 has error (no default branch)
201 |     const repo3 = result.repositories.find((r) => r.name === 'repo3-api');
202 |     expect(repo3).toBeDefined();
203 |     expect(repo3?.error).toContain('No default branch found');
204 | 
205 |     // Verify recursion level was set correctly
206 |     expect(mockGitApi.getItems).toHaveBeenCalledWith(
207 |       'repo1-id',
208 |       'testProject',
209 |       '/',
210 |       VersionControlRecursionType.OneLevel,
211 |       expect.anything(),
212 |       expect.anything(),
213 |       expect.anything(),
214 |       expect.anything(),
215 |       expect.anything(),
216 |     );
217 |   });
218 | 
219 |   it('should return tree structures with max depth using Full recursion', async () => {
220 |     // Arrange
221 |     const options = {
222 |       organizationId: 'testOrg',
223 |       projectId: 'testProject',
224 |       depth: 0, // Max depth
225 |     };
226 | 
227 |     // Act
228 |     const result = await getAllRepositoriesTree(mockConnection, options);
229 | 
230 |     // Assert
231 |     expect(mockGitApi.getRepositories).toHaveBeenCalledWith('testProject');
232 |     expect(result.repositories.length).toBe(3);
233 | 
234 |     // Verify repo1 tree
235 |     const repo1 = result.repositories.find((r) => r.name === 'repo1');
236 |     expect(repo1).toBeDefined();
237 |     expect(repo1?.tree.length).toBeGreaterThan(0);
238 |     // Should include all items, including nested ones
239 |     expect(repo1?.tree.length).toBe(mockRepo1FullRecursionItems.length - 1); // -1 for root folder
240 | 
241 |     // Verify recursion level was set correctly
242 |     expect(mockGitApi.getItems).toHaveBeenCalledWith(
243 |       'repo1-id',
244 |       'testProject',
245 |       '/',
246 |       VersionControlRecursionType.Full,
247 |       expect.anything(),
248 |       expect.anything(),
249 |       expect.anything(),
250 |       expect.anything(),
251 |       expect.anything(),
252 |     );
253 | 
254 |     // Verify all levels are represented
255 |     if (repo1) {
256 |       const level1Items = repo1.tree.filter((item) => item.level === 1);
257 |       const level2Items = repo1.tree.filter((item) => item.level === 2);
258 |       const level3Items = repo1.tree.filter((item) => item.level === 3);
259 | 
260 |       // Verify we have items at level 1
261 |       expect(level1Items.length).toBeGreaterThan(0);
262 | 
263 |       // Verify we have items at level 2 (src/something)
264 |       expect(level2Items.length).toBeGreaterThan(0);
265 | 
266 |       // Check for level 3 items if they exist in our mock data
267 |       if (
268 |         mockRepo1FullRecursionItems.some((item) => {
269 |           const pathSegments = item.path.split('/').filter(Boolean);
270 |           return pathSegments.length >= 3;
271 |         })
272 |       ) {
273 |         expect(level3Items.length).toBeGreaterThan(0);
274 |       }
275 |     }
276 |   });
277 | 
278 |   it('should filter repositories by pattern', async () => {
279 |     // Arrange
280 |     const options = {
281 |       organizationId: 'testOrg',
282 |       projectId: 'testProject',
283 |       repositoryPattern: '*api*',
284 |       depth: 1,
285 |     };
286 | 
287 |     // Act
288 |     const result = await getAllRepositoriesTree(mockConnection, options);
289 | 
290 |     // Assert
291 |     expect(mockGitApi.getRepositories).toHaveBeenCalledWith('testProject');
292 |     expect(result.repositories.length).toBe(1);
293 |     expect(result.repositories[0].name).toBe('repo3-api');
294 |   });
295 | 
296 |   it('should format repository tree correctly', () => {
297 |     // Arrange
298 |     const treeItems: RepositoryTreeItem[] = [
299 |       { name: 'src', path: '/src', isFolder: true, level: 1 },
300 |       { name: 'index.ts', path: '/src/index.ts', isFolder: false, level: 2 },
301 |       { name: 'README.md', path: '/README.md', isFolder: false, level: 1 },
302 |     ];
303 |     const stats = { directories: 1, files: 2 };
304 | 
305 |     // Act
306 |     const formatted = formatRepositoryTree('test-repo', treeItems, stats);
307 | 
308 |     // Assert
309 |     expect(formatted).toMatchSnapshot();
310 |   });
311 | 
312 |   it('should format complex repository tree structures correctly', () => {
313 |     // Arrange
314 |     const treeItems: RepositoryTreeItem[] = [
315 |       // Root level files
316 |       { name: 'README.md', path: '/README.md', isFolder: false, level: 1 },
317 |       {
318 |         name: 'package.json',
319 |         path: '/package.json',
320 |         isFolder: false,
321 |         level: 1,
322 |       },
323 |       { name: '.gitignore', path: '/.gitignore', isFolder: false, level: 1 },
324 | 
325 |       // Multiple folders at root level
326 |       { name: 'src', path: '/src', isFolder: true, level: 1 },
327 |       { name: 'tests', path: '/tests', isFolder: true, level: 1 },
328 |       { name: 'docs', path: '/docs', isFolder: true, level: 1 },
329 | 
330 |       // Nested src folder structure
331 |       { name: 'components', path: '/src/components', isFolder: true, level: 2 },
332 |       { name: 'utils', path: '/src/utils', isFolder: true, level: 2 },
333 |       { name: 'index.ts', path: '/src/index.ts', isFolder: false, level: 2 },
334 | 
335 |       // Deeply nested components
336 |       {
337 |         name: 'Button',
338 |         path: '/src/components/Button',
339 |         isFolder: true,
340 |         level: 3,
341 |       },
342 |       { name: 'Card', path: '/src/components/Card', isFolder: true, level: 3 },
343 |       {
344 |         name: 'Button.tsx',
345 |         path: '/src/components/Button/Button.tsx',
346 |         isFolder: false,
347 |         level: 4,
348 |       },
349 |       {
350 |         name: 'Button.styles.ts',
351 |         path: '/src/components/Button/Button.styles.ts',
352 |         isFolder: false,
353 |         level: 4,
354 |       },
355 |       {
356 |         name: 'Button.test.tsx',
357 |         path: '/src/components/Button/Button.test.tsx',
358 |         isFolder: false,
359 |         level: 4,
360 |       },
361 |       {
362 |         name: 'index.ts',
363 |         path: '/src/components/Button/index.ts',
364 |         isFolder: false,
365 |         level: 4,
366 |       },
367 |       {
368 |         name: 'Card.tsx',
369 |         path: '/src/components/Card/Card.tsx',
370 |         isFolder: false,
371 |         level: 4,
372 |       },
373 | 
374 |       // Utils with files
375 |       {
376 |         name: 'helpers.ts',
377 |         path: '/src/utils/helpers.ts',
378 |         isFolder: false,
379 |         level: 3,
380 |       },
381 |       {
382 |         name: 'constants.ts',
383 |         path: '/src/utils/constants.ts',
384 |         isFolder: false,
385 |         level: 3,
386 |       },
387 | 
388 |       // Empty folder
389 |       { name: 'assets', path: '/src/assets', isFolder: true, level: 2 },
390 | 
391 |       // Files with special characters
392 |       {
393 |         name: 'file-with-dashes.js',
394 |         path: '/src/file-with-dashes.js',
395 |         isFolder: false,
396 |         level: 2,
397 |       },
398 |       {
399 |         name: 'file_with_underscores.js',
400 |         path: '/src/file_with_underscores.js',
401 |         isFolder: false,
402 |         level: 2,
403 |       },
404 | 
405 |       // Folders in test directory
406 |       { name: 'unit', path: '/tests/unit', isFolder: true, level: 2 },
407 |       {
408 |         name: 'integration',
409 |         path: '/tests/integration',
410 |         isFolder: true,
411 |         level: 2,
412 |       },
413 | 
414 |       // Files in test directories
415 |       { name: 'setup.js', path: '/tests/setup.js', isFolder: false, level: 2 },
416 |       {
417 |         name: 'example.test.js',
418 |         path: '/tests/unit/example.test.js',
419 |         isFolder: false,
420 |         level: 3,
421 |       },
422 | 
423 |       // Files in docs
424 |       { name: 'API.md', path: '/docs/API.md', isFolder: false, level: 2 },
425 |       {
426 |         name: 'CONTRIBUTING.md',
427 |         path: '/docs/CONTRIBUTING.md',
428 |         isFolder: false,
429 |         level: 2,
430 |       },
431 |     ];
432 | 
433 |     const stats = { directories: 10, files: 18 };
434 | 
435 |     // Act
436 |     const formatted = formatRepositoryTree('complex-repo', treeItems, stats);
437 | 
438 |     // Assert
439 |     expect(formatted).toMatchSnapshot();
440 |   });
441 | 
442 |   it('should handle repository errors gracefully', async () => {
443 |     // Arrange
444 |     mockGitApi.getItems = jest.fn().mockRejectedValue(new Error('API error'));
445 | 
446 |     const options = {
447 |       organizationId: 'testOrg',
448 |       projectId: 'testProject',
449 |       depth: 1,
450 |     };
451 | 
452 |     // Act
453 |     const result = await getAllRepositoriesTree(mockConnection, options);
454 | 
455 |     // Assert
456 |     expect(result.repositories.length).toBe(3);
457 |     const repo1 = result.repositories.find((r) => r.name === 'repo1');
458 |     expect(repo1?.error).toBeDefined();
459 |   });
460 | });
461 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { createWikiPage } from './feature';
  3 | import { CreateWikiPageSchema } from './schema';
  4 | import { getWikiPage } from '../get-wiki-page/feature';
  5 | import { getWikis } from '../get-wikis/feature';
  6 | import {
  7 |   getTestConnection,
  8 |   shouldSkipIntegrationTest,
  9 | } from '@/shared/test/test-helpers';
 10 | import { getOrgNameFromUrl } from '@/utils/environment';
 11 | import { AzureDevOpsError } from '@/shared/errors/azure-devops-errors';
 12 | import { z } from 'zod';
 13 | 
 14 | // Ensure environment variables are set for testing
 15 | process.env.AZURE_DEVOPS_DEFAULT_PROJECT =
 16 |   process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'default-project';
 17 | 
 18 | describe('createWikiPage Integration Tests', () => {
 19 |   let connection: WebApi | null = null;
 20 |   let projectName: string;
 21 |   let orgUrl: string;
 22 |   let organizationId: string;
 23 |   const testPagePath = '/IntegrationTestPage';
 24 |   const testPagePathSub = '/IntegrationTestPage/SubPage';
 25 |   const testPagePathDefault = '/DefaultPathPage';
 26 |   const testPagePathComment = '/CommentTestPage';
 27 | 
 28 |   beforeAll(async () => {
 29 |     // Mock the required environment variable for testing
 30 |     process.env.AZURE_DEVOPS_ORG_URL =
 31 |       process.env.AZURE_DEVOPS_ORG_URL || 'https://example.visualstudio.com';
 32 | 
 33 |     // Get and validate required environment variables
 34 |     const envProjectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT;
 35 |     if (!envProjectName) {
 36 |       throw new Error(
 37 |         'AZURE_DEVOPS_DEFAULT_PROJECT environment variable is required',
 38 |       );
 39 |     }
 40 |     projectName = envProjectName;
 41 | 
 42 |     const envOrgUrl = process.env.AZURE_DEVOPS_ORG_URL;
 43 |     if (!envOrgUrl) {
 44 |       throw new Error('AZURE_DEVOPS_ORG_URL environment variable is required');
 45 |     }
 46 |     orgUrl = envOrgUrl;
 47 |     organizationId = getOrgNameFromUrl(orgUrl);
 48 | 
 49 |     // Get a real connection using environment variables
 50 |     connection = await getTestConnection();
 51 |   });
 52 | 
 53 |   // Helper function to get a valid wiki ID
 54 |   async function getValidWikiId(): Promise<string | null> {
 55 |     if (!connection) return null;
 56 | 
 57 |     try {
 58 |       // Get available wikis
 59 |       const wikis = await getWikis(connection, { projectId: projectName });
 60 | 
 61 |       // Skip if no wikis are available
 62 |       if (wikis.length === 0) {
 63 |         console.log('No wikis available in the project');
 64 |         return null;
 65 |       }
 66 | 
 67 |       // Use the first available wiki
 68 |       const wiki = wikis[0];
 69 |       if (!wiki.name) {
 70 |         console.log('Wiki name is undefined');
 71 |         return null;
 72 |       }
 73 | 
 74 |       return wiki.name;
 75 |     } catch (error) {
 76 |       console.error('Error getting wikis:', error);
 77 |       return null;
 78 |     }
 79 |   }
 80 | 
 81 |   test('should create a new wiki page at the root', async () => {
 82 |     // Skip if no connection is available
 83 |     if (shouldSkipIntegrationTest()) {
 84 |       console.log('Skipping test due to missing connection');
 85 |       return;
 86 |     }
 87 | 
 88 |     // This connection must be available if we didn't skip
 89 |     if (!connection) {
 90 |       throw new Error(
 91 |         'Connection should be available when test is not skipped',
 92 |       );
 93 |     }
 94 | 
 95 |     // Get a valid wiki ID
 96 |     const wikiId = await getValidWikiId();
 97 |     if (!wikiId) {
 98 |       console.log('Skipping test: No valid wiki ID available');
 99 |       return;
100 |     }
101 | 
102 |     const params: z.infer<typeof CreateWikiPageSchema> = {
103 |       organizationId,
104 |       projectId: projectName,
105 |       wikiId,
106 |       pagePath: testPagePath,
107 |       content: 'This is content for the integration test page (root).',
108 |     };
109 | 
110 |     try {
111 |       // Create the wiki page
112 |       const createdPage = await createWikiPage(params);
113 | 
114 |       // Verify the result
115 |       expect(createdPage).toBeDefined();
116 |       expect(createdPage.path).toBe(testPagePath);
117 |       expect(createdPage.content).toBe(params.content);
118 | 
119 |       // Verify by fetching the page
120 |       const fetchedPage = await getWikiPage({
121 |         organizationId,
122 |         projectId: projectName,
123 |         wikiId,
124 |         pagePath: testPagePath,
125 |       });
126 | 
127 |       expect(fetchedPage).toBeDefined();
128 |       expect(typeof fetchedPage).toBe('string');
129 |       expect(fetchedPage).toContain(params.content);
130 |     } catch (error) {
131 |       console.error('Error in test:', error);
132 |       throw error;
133 |     }
134 |   });
135 | 
136 |   test('should create a new wiki sub-page', async () => {
137 |     // Skip if no connection is available
138 |     if (shouldSkipIntegrationTest()) {
139 |       console.log('Skipping test due to missing connection');
140 |       return;
141 |     }
142 | 
143 |     // This connection must be available if we didn't skip
144 |     if (!connection) {
145 |       throw new Error(
146 |         'Connection should be available when test is not skipped',
147 |       );
148 |     }
149 | 
150 |     // Get a valid wiki ID
151 |     const wikiId = await getValidWikiId();
152 |     if (!wikiId) {
153 |       console.log('Skipping test: No valid wiki ID available');
154 |       return;
155 |     }
156 | 
157 |     // First, ensure the parent page exists
158 |     const parentParams: z.infer<typeof CreateWikiPageSchema> = {
159 |       organizationId,
160 |       projectId: projectName,
161 |       wikiId,
162 |       pagePath: testPagePath,
163 |       content: 'This is the parent page for the sub-page test.',
164 |     };
165 | 
166 |     try {
167 |       // Create the parent page
168 |       await createWikiPage(parentParams);
169 | 
170 |       // Now create the sub-page
171 |       const subPageParams: z.infer<typeof CreateWikiPageSchema> = {
172 |         organizationId,
173 |         projectId: projectName,
174 |         wikiId,
175 |         pagePath: testPagePathSub,
176 |         content: 'This is content for the integration test sub-page.',
177 |       };
178 | 
179 |       const createdSubPage = await createWikiPage(subPageParams);
180 | 
181 |       // Verify the result
182 |       expect(createdSubPage).toBeDefined();
183 |       expect(createdSubPage.path).toBe(testPagePathSub);
184 |       expect(createdSubPage.content).toBe(subPageParams.content);
185 | 
186 |       // Verify by fetching the sub-page
187 |       const fetchedSubPage = await getWikiPage({
188 |         organizationId,
189 |         projectId: projectName,
190 |         wikiId,
191 |         pagePath: testPagePathSub,
192 |       });
193 | 
194 |       expect(fetchedSubPage).toBeDefined();
195 |       expect(typeof fetchedSubPage).toBe('string');
196 |       expect(fetchedSubPage).toContain(subPageParams.content);
197 |     } catch (error) {
198 |       console.error('Error in test:', error);
199 |       throw error;
200 |     }
201 |   });
202 | 
203 |   test('should update an existing wiki page if path already exists', async () => {
204 |     // Skip if no connection is available
205 |     if (shouldSkipIntegrationTest()) {
206 |       console.log('Skipping test due to missing connection');
207 |       return;
208 |     }
209 | 
210 |     // This connection must be available if we didn't skip
211 |     if (!connection) {
212 |       throw new Error(
213 |         'Connection should be available when test is not skipped',
214 |       );
215 |     }
216 | 
217 |     // Get a valid wiki ID
218 |     const wikiId = await getValidWikiId();
219 |     if (!wikiId) {
220 |       console.log('Skipping test: No valid wiki ID available');
221 |       return;
222 |     }
223 | 
224 |     try {
225 |       // First create a page with initial content
226 |       const initialParams: z.infer<typeof CreateWikiPageSchema> = {
227 |         organizationId,
228 |         projectId: projectName,
229 |         wikiId,
230 |         pagePath: testPagePath,
231 |         content: 'Initial content.',
232 |       };
233 | 
234 |       await createWikiPage(initialParams);
235 | 
236 |       // Now update the page with new content
237 |       const updatedParams: z.infer<typeof CreateWikiPageSchema> = {
238 |         ...initialParams,
239 |         content: 'Updated content for the page.',
240 |       };
241 | 
242 |       const updatedPage = await createWikiPage(updatedParams);
243 | 
244 |       // Verify the result
245 |       expect(updatedPage).toBeDefined();
246 |       expect(updatedPage.path).toBe(testPagePath);
247 |       expect(updatedPage.content).toBe(updatedParams.content);
248 | 
249 |       // Verify by fetching the page
250 |       const fetchedPage = await getWikiPage({
251 |         organizationId,
252 |         projectId: projectName,
253 |         wikiId,
254 |         pagePath: testPagePath,
255 |       });
256 | 
257 |       expect(fetchedPage).toBeDefined();
258 |       expect(typeof fetchedPage).toBe('string');
259 |       expect(fetchedPage).toContain(updatedParams.content);
260 |     } catch (error) {
261 |       console.error('Error in test:', error);
262 |       throw error;
263 |     }
264 |   });
265 | 
266 |   test('should create a page with a default path if specified', async () => {
267 |     // Skip if no connection is available
268 |     if (shouldSkipIntegrationTest()) {
269 |       console.log('Skipping test due to missing connection');
270 |       return;
271 |     }
272 | 
273 |     // This connection must be available if we didn't skip
274 |     if (!connection) {
275 |       throw new Error(
276 |         'Connection should be available when test is not skipped',
277 |       );
278 |     }
279 | 
280 |     // Get a valid wiki ID
281 |     const wikiId = await getValidWikiId();
282 |     if (!wikiId) {
283 |       console.log('Skipping test: No valid wiki ID available');
284 |       return;
285 |     }
286 | 
287 |     try {
288 |       const params: z.infer<typeof CreateWikiPageSchema> = {
289 |         organizationId,
290 |         projectId: projectName,
291 |         wikiId,
292 |         pagePath: testPagePathDefault,
293 |         content: 'Content for page created with default path.',
294 |       };
295 | 
296 |       const createdPage = await createWikiPage(params);
297 | 
298 |       // Verify the result
299 |       expect(createdPage).toBeDefined();
300 |       expect(createdPage.path).toBe(testPagePathDefault);
301 |       expect(createdPage.content).toBe(params.content);
302 | 
303 |       // Verify by fetching the page
304 |       const fetchedPage = await getWikiPage({
305 |         organizationId,
306 |         projectId: projectName,
307 |         wikiId,
308 |         pagePath: testPagePathDefault,
309 |       });
310 | 
311 |       expect(fetchedPage).toBeDefined();
312 |       expect(typeof fetchedPage).toBe('string');
313 |       expect(fetchedPage).toContain(params.content);
314 |     } catch (error) {
315 |       console.error('Error in test:', error);
316 |       throw error;
317 |     }
318 |   });
319 | 
320 |   test('should include comment in the wiki page creation when provided', async () => {
321 |     // Skip if no connection is available
322 |     if (shouldSkipIntegrationTest()) {
323 |       console.log('Skipping test due to missing connection');
324 |       return;
325 |     }
326 | 
327 |     // This connection must be available if we didn't skip
328 |     if (!connection) {
329 |       throw new Error(
330 |         'Connection should be available when test is not skipped',
331 |       );
332 |     }
333 | 
334 |     // Get a valid wiki ID
335 |     const wikiId = await getValidWikiId();
336 |     if (!wikiId) {
337 |       console.log('Skipping test: No valid wiki ID available');
338 |       return;
339 |     }
340 | 
341 |     try {
342 |       const params: z.infer<typeof CreateWikiPageSchema> = {
343 |         organizationId,
344 |         projectId: projectName,
345 |         wikiId,
346 |         pagePath: testPagePathComment,
347 |         content: 'Content with comment.',
348 |         comment: 'This is a test comment for the wiki page creation',
349 |       };
350 | 
351 |       const createdPage = await createWikiPage(params);
352 | 
353 |       // Verify the result
354 |       expect(createdPage).toBeDefined();
355 |       expect(createdPage.path).toBe(testPagePathComment);
356 |       expect(createdPage.content).toBe(params.content);
357 | 
358 |       // Verify by fetching the page
359 |       const fetchedPage = await getWikiPage({
360 |         organizationId,
361 |         projectId: projectName,
362 |         wikiId,
363 |         pagePath: testPagePathComment,
364 |       });
365 | 
366 |       expect(fetchedPage).toBeDefined();
367 |       expect(typeof fetchedPage).toBe('string');
368 |       expect(fetchedPage).toContain(params.content);
369 | 
370 |       // Note: The API might not return the comment in the response
371 |       // This test primarily verifies that including a comment doesn't break the API call
372 |     } catch (error) {
373 |       console.error('Error in test:', error);
374 |       throw error;
375 |     }
376 |   });
377 | 
378 |   test('should handle error when wiki does not exist', async () => {
379 |     // Skip if no connection is available
380 |     if (shouldSkipIntegrationTest()) {
381 |       console.log('Skipping test due to missing connection');
382 |       return;
383 |     }
384 | 
385 |     const nonExistentWikiId = 'non-existent-wiki-12345';
386 | 
387 |     const params: z.infer<typeof CreateWikiPageSchema> = {
388 |       organizationId,
389 |       projectId: projectName,
390 |       wikiId: nonExistentWikiId,
391 |       pagePath: '/test-page',
392 |       content: 'This should fail.',
393 |     };
394 | 
395 |     await expect(createWikiPage(params)).rejects.toThrow(AzureDevOpsError);
396 |   });
397 | 
398 |   test('should handle error when project does not exist', async () => {
399 |     // Skip if no connection is available
400 |     if (shouldSkipIntegrationTest()) {
401 |       console.log('Skipping test due to missing connection');
402 |       return;
403 |     }
404 | 
405 |     const nonExistentProjectId = 'non-existent-project-12345';
406 | 
407 |     const params: z.infer<typeof CreateWikiPageSchema> = {
408 |       organizationId,
409 |       projectId: nonExistentProjectId,
410 |       wikiId: 'any-wiki',
411 |       pagePath: '/test-page',
412 |       content: 'This should fail.',
413 |     };
414 | 
415 |     await expect(createWikiPage(params)).rejects.toThrow(AzureDevOpsError);
416 |   });
417 | 
418 |   test('should handle error when organization does not exist', async () => {
419 |     // Skip if no connection is available
420 |     if (shouldSkipIntegrationTest()) {
421 |       console.log('Skipping test due to missing connection');
422 |       return;
423 |     }
424 | 
425 |     const nonExistentOrgId = 'non-existent-org-12345';
426 | 
427 |     const params: z.infer<typeof CreateWikiPageSchema> = {
428 |       organizationId: nonExistentOrgId,
429 |       projectId: projectName,
430 |       wikiId: 'any-wiki',
431 |       pagePath: '/test-page',
432 |       content: 'This should fail.',
433 |     };
434 | 
435 |     await expect(createWikiPage(params)).rejects.toThrow(AzureDevOpsError);
436 |   });
437 | });
438 | 
```

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

```typescript
  1 | import { getProjectDetails } from './feature';
  2 | import {
  3 |   AzureDevOpsError,
  4 |   AzureDevOpsResourceNotFoundError,
  5 | } from '../../../shared/errors';
  6 | import {
  7 |   TeamProject,
  8 |   WebApiTeam,
  9 | } from 'azure-devops-node-api/interfaces/CoreInterfaces';
 10 | import { WebApi } from 'azure-devops-node-api';
 11 | import { WorkItemType } from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces';
 12 | 
 13 | // Create mock interfaces for the APIs we'll use
 14 | interface MockCoreApi {
 15 |   getProject: jest.Mock<Promise<TeamProject | null>>;
 16 |   getTeams: jest.Mock<Promise<WebApiTeam[]>>;
 17 | }
 18 | 
 19 | interface MockWorkItemTrackingApi {
 20 |   getWorkItemTypes: jest.Mock<Promise<WorkItemType[]>>;
 21 | }
 22 | 
 23 | interface MockProcessApi {
 24 |   getProcesses: jest.Mock<Promise<any[]>>;
 25 |   getProcessWorkItemTypes: jest.Mock<Promise<any[]>>;
 26 | }
 27 | 
 28 | // Create a mock connection that resembles WebApi with minimal implementation
 29 | interface MockConnection {
 30 |   getCoreApi: jest.Mock<Promise<MockCoreApi>>;
 31 |   getWorkItemTrackingApi: jest.Mock<Promise<MockWorkItemTrackingApi>>;
 32 |   getProcessApi: jest.Mock<Promise<MockProcessApi>>;
 33 |   serverUrl?: string;
 34 |   authHandler?: unknown;
 35 |   rest?: unknown;
 36 |   vsoClient?: unknown;
 37 | }
 38 | 
 39 | // Sample data for tests
 40 | const mockProject = {
 41 |   id: 'project-id',
 42 |   name: 'Test Project',
 43 |   description: 'A test project',
 44 |   url: 'https://dev.azure.com/org/project',
 45 |   state: 1, // wellFormed
 46 |   revision: 123,
 47 |   visibility: 0, // private
 48 |   lastUpdateTime: new Date(),
 49 |   capabilities: {
 50 |     versioncontrol: {
 51 |       sourceControlType: 'Git',
 52 |     },
 53 |     processTemplate: {
 54 |       templateName: 'Agile',
 55 |       templateTypeId: 'template-guid',
 56 |     },
 57 |   },
 58 | } as unknown as TeamProject;
 59 | 
 60 | const mockTeams: WebApiTeam[] = [
 61 |   {
 62 |     id: 'team-guid-1',
 63 |     name: 'Team 1',
 64 |     description: 'First team',
 65 |     url: 'https://dev.azure.com/org/_apis/projects/project-guid/teams/team-guid-1',
 66 |     identityUrl: 'https://vssps.dev.azure.com/org/_apis/Identities/team-guid-1',
 67 |   } as WebApiTeam,
 68 |   {
 69 |     id: 'team-guid-2',
 70 |     name: 'Team 2',
 71 |     description: 'Second team',
 72 |     url: 'https://dev.azure.com/org/_apis/projects/project-guid/teams/team-guid-2',
 73 |     identityUrl: 'https://vssps.dev.azure.com/org/_apis/Identities/team-guid-2',
 74 |   } as WebApiTeam,
 75 | ];
 76 | 
 77 | const mockWorkItemTypes: WorkItemType[] = [
 78 |   {
 79 |     name: 'User Story',
 80 |     description: 'Tracks user requirements',
 81 |     referenceName: 'Microsoft.VSTS.WorkItemTypes.UserStory',
 82 |     color: 'blue',
 83 |     icon: 'icon-user-story',
 84 |     isDisabled: false,
 85 |   } as WorkItemType,
 86 |   {
 87 |     name: 'Bug',
 88 |     description: 'Tracks defects in the product',
 89 |     referenceName: 'Microsoft.VSTS.WorkItemTypes.Bug',
 90 |     color: 'red',
 91 |     icon: 'icon-bug',
 92 |     isDisabled: false,
 93 |   } as WorkItemType,
 94 | ];
 95 | 
 96 | const mockProcesses = [
 97 |   {
 98 |     id: 'process-guid',
 99 |     name: 'Agile',
100 |     description: 'Agile process',
101 |     isDefault: true,
102 |     type: 'system',
103 |   },
104 | ];
105 | 
106 | const mockProcessWorkItemTypes = [
107 |   {
108 |     name: 'User Story',
109 |     referenceName: 'Microsoft.VSTS.WorkItemTypes.UserStory',
110 |     description: 'Tracks user requirements',
111 |     color: 'blue',
112 |     icon: 'icon-user-story',
113 |     isDisabled: false,
114 |     states: [
115 |       {
116 |         name: 'New',
117 |         color: 'blue',
118 |         stateCategory: 'Proposed',
119 |       },
120 |       {
121 |         name: 'Active',
122 |         color: 'blue',
123 |         stateCategory: 'InProgress',
124 |       },
125 |       {
126 |         name: 'Resolved',
127 |         color: 'blue',
128 |         stateCategory: 'InProgress',
129 |       },
130 |       {
131 |         name: 'Closed',
132 |         color: 'blue',
133 |         stateCategory: 'Completed',
134 |       },
135 |     ],
136 |     fields: [
137 |       {
138 |         name: 'Title',
139 |         referenceName: 'System.Title',
140 |         type: 'string',
141 |         required: true,
142 |       },
143 |       {
144 |         name: 'Description',
145 |         referenceName: 'System.Description',
146 |         type: 'html',
147 |       },
148 |     ],
149 |   },
150 |   {
151 |     name: 'Bug',
152 |     referenceName: 'Microsoft.VSTS.WorkItemTypes.Bug',
153 |     description: 'Tracks defects in the product',
154 |     color: 'red',
155 |     icon: 'icon-bug',
156 |     isDisabled: false,
157 |     states: [
158 |       {
159 |         name: 'New',
160 |         color: 'red',
161 |         stateCategory: 'Proposed',
162 |       },
163 |       {
164 |         name: 'Active',
165 |         color: 'red',
166 |         stateCategory: 'InProgress',
167 |       },
168 |       {
169 |         name: 'Resolved',
170 |         color: 'red',
171 |         stateCategory: 'InProgress',
172 |       },
173 |       {
174 |         name: 'Closed',
175 |         color: 'red',
176 |         stateCategory: 'Completed',
177 |       },
178 |     ],
179 |     fields: [
180 |       {
181 |         name: 'Title',
182 |         referenceName: 'System.Title',
183 |         type: 'string',
184 |         required: true,
185 |       },
186 |       {
187 |         name: 'Repro Steps',
188 |         referenceName: 'Microsoft.VSTS.TCM.ReproSteps',
189 |         type: 'html',
190 |       },
191 |     ],
192 |   },
193 | ];
194 | 
195 | // Unit tests should only focus on isolated logic
196 | describe('getProjectDetails unit', () => {
197 |   test('should throw resource not found error when project is null', async () => {
198 |     // Arrange
199 |     const mockCoreApi: MockCoreApi = {
200 |       getProject: jest.fn().mockResolvedValue(null), // Simulate project not found
201 |       getTeams: jest.fn().mockResolvedValue([]),
202 |     };
203 | 
204 |     const mockConnection: MockConnection = {
205 |       getCoreApi: jest.fn().mockResolvedValue(mockCoreApi),
206 |       getWorkItemTrackingApi: jest.fn().mockResolvedValue({
207 |         getWorkItemTypes: jest.fn().mockResolvedValue([]),
208 |       }),
209 |       getProcessApi: jest.fn().mockResolvedValue({
210 |         getProcesses: jest.fn().mockResolvedValue([]),
211 |         getProcessWorkItemTypes: jest.fn().mockResolvedValue([]),
212 |       }),
213 |     };
214 | 
215 |     // Act & Assert
216 |     await expect(
217 |       getProjectDetails(mockConnection as unknown as WebApi, {
218 |         projectId: 'non-existent-project',
219 |       }),
220 |     ).rejects.toThrow(AzureDevOpsResourceNotFoundError);
221 | 
222 |     await expect(
223 |       getProjectDetails(mockConnection as unknown as WebApi, {
224 |         projectId: 'non-existent-project',
225 |       }),
226 |     ).rejects.toThrow("Project 'non-existent-project' not found");
227 |   });
228 | 
229 |   test('should return basic project details when no additional options are specified', async () => {
230 |     // Arrange
231 |     const mockCoreApi: MockCoreApi = {
232 |       getProject: jest.fn().mockResolvedValue(mockProject),
233 |       getTeams: jest.fn().mockResolvedValue([]),
234 |     };
235 | 
236 |     const mockConnection: MockConnection = {
237 |       getCoreApi: jest.fn().mockResolvedValue(mockCoreApi),
238 |       getWorkItemTrackingApi: jest.fn().mockResolvedValue({
239 |         getWorkItemTypes: jest.fn().mockResolvedValue([]),
240 |       }),
241 |       getProcessApi: jest.fn().mockResolvedValue({
242 |         getProcesses: jest.fn().mockResolvedValue([]),
243 |         getProcessWorkItemTypes: jest.fn().mockResolvedValue([]),
244 |       }),
245 |     };
246 | 
247 |     // Act
248 |     const result = await getProjectDetails(
249 |       mockConnection as unknown as WebApi,
250 |       {
251 |         projectId: 'test-project',
252 |       },
253 |     );
254 | 
255 |     // Assert
256 |     expect(result).toBeDefined();
257 |     expect(result.id).toBe(mockProject.id);
258 |     expect(result.name).toBe(mockProject.name);
259 |     expect(result.description).toBe(mockProject.description);
260 |     expect(result.url).toBe(mockProject.url);
261 |     expect(result.state).toBe(mockProject.state);
262 |     expect(result.revision).toBe(mockProject.revision);
263 |     expect(result.visibility).toBe(mockProject.visibility);
264 |     expect(result.lastUpdateTime).toBe(mockProject.lastUpdateTime);
265 |     expect(result.capabilities).toEqual(mockProject.capabilities);
266 | 
267 |     // Verify that additional details are not included
268 |     expect(result.process).toBeUndefined();
269 |     expect(result.teams).toBeUndefined();
270 |   });
271 | 
272 |   test('should include teams when includeTeams is true', async () => {
273 |     // Arrange
274 |     const mockCoreApi: MockCoreApi = {
275 |       getProject: jest.fn().mockResolvedValue(mockProject),
276 |       getTeams: jest.fn().mockResolvedValue(mockTeams),
277 |     };
278 | 
279 |     const mockConnection: MockConnection = {
280 |       getCoreApi: jest.fn().mockResolvedValue(mockCoreApi),
281 |       getWorkItemTrackingApi: jest.fn().mockResolvedValue({
282 |         getWorkItemTypes: jest.fn().mockResolvedValue([]),
283 |       }),
284 |       getProcessApi: jest.fn().mockResolvedValue({
285 |         getProcesses: jest.fn().mockResolvedValue([]),
286 |         getProcessWorkItemTypes: jest.fn().mockResolvedValue([]),
287 |       }),
288 |     };
289 | 
290 |     // Act
291 |     const result = await getProjectDetails(
292 |       mockConnection as unknown as WebApi,
293 |       {
294 |         projectId: 'test-project',
295 |         includeTeams: true,
296 |       },
297 |     );
298 | 
299 |     // Assert
300 |     expect(result).toBeDefined();
301 |     expect(result.teams).toBeDefined();
302 |     expect(result.teams?.length).toBe(2);
303 |     expect(result.teams?.[0].id).toBe(mockTeams[0].id);
304 |     expect(result.teams?.[0].name).toBe(mockTeams[0].name);
305 |     expect(result.teams?.[1].id).toBe(mockTeams[1].id);
306 |     expect(result.teams?.[1].name).toBe(mockTeams[1].name);
307 |   });
308 | 
309 |   test('should include process information when includeProcess is true', async () => {
310 |     // Arrange
311 |     const mockCoreApi: MockCoreApi = {
312 |       getProject: jest.fn().mockResolvedValue(mockProject),
313 |       getTeams: jest.fn().mockResolvedValue([]),
314 |     };
315 | 
316 |     const mockWorkItemTrackingApi: MockWorkItemTrackingApi = {
317 |       getWorkItemTypes: jest.fn().mockResolvedValue([]),
318 |     };
319 | 
320 |     const mockConnection: MockConnection = {
321 |       getCoreApi: jest.fn().mockResolvedValue(mockCoreApi),
322 |       getWorkItemTrackingApi: jest
323 |         .fn()
324 |         .mockResolvedValue(mockWorkItemTrackingApi),
325 |       getProcessApi: jest.fn(),
326 |     };
327 | 
328 |     // Act
329 |     const result = await getProjectDetails(
330 |       mockConnection as unknown as WebApi,
331 |       {
332 |         projectId: 'test-project',
333 |         includeProcess: true,
334 |       },
335 |     );
336 | 
337 |     // Assert
338 |     expect(result).toBeDefined();
339 |     expect(result.process).toBeDefined();
340 |     expect(result.process?.name).toBe('Agile');
341 |   });
342 | 
343 |   test('should include work item types when includeWorkItemTypes is true', async () => {
344 |     // Arrange
345 |     const mockCoreApi: MockCoreApi = {
346 |       getProject: jest.fn().mockResolvedValue(mockProject),
347 |       getTeams: jest.fn().mockResolvedValue([]),
348 |     };
349 | 
350 |     const mockWorkItemTrackingApi: MockWorkItemTrackingApi = {
351 |       getWorkItemTypes: jest.fn().mockResolvedValue(mockWorkItemTypes),
352 |     };
353 | 
354 |     const mockProcessApi: MockProcessApi = {
355 |       getProcesses: jest.fn().mockResolvedValue(mockProcesses),
356 |       getProcessWorkItemTypes: jest
357 |         .fn()
358 |         .mockResolvedValue(mockProcessWorkItemTypes),
359 |     };
360 | 
361 |     const mockConnection: MockConnection = {
362 |       getCoreApi: jest.fn().mockResolvedValue(mockCoreApi),
363 |       getWorkItemTrackingApi: jest
364 |         .fn()
365 |         .mockResolvedValue(mockWorkItemTrackingApi),
366 |       getProcessApi: jest.fn().mockResolvedValue(mockProcessApi),
367 |     };
368 | 
369 |     // Act
370 |     const result = await getProjectDetails(
371 |       mockConnection as unknown as WebApi,
372 |       {
373 |         projectId: 'test-project',
374 |         includeWorkItemTypes: true,
375 |         includeProcess: true,
376 |       },
377 |     );
378 | 
379 |     // Assert
380 |     expect(result).toBeDefined();
381 |     expect(result.process).toBeDefined();
382 |     expect(result.process?.workItemTypes).toBeDefined();
383 |     expect(result.process?.workItemTypes?.length).toBe(2);
384 |     expect(result.process?.workItemTypes?.[0].name).toBe('User Story');
385 |     expect(result.process?.workItemTypes?.[1].name).toBe('Bug');
386 |   });
387 | 
388 |   test('should include fields when includeFields is true', async () => {
389 |     // Arrange
390 |     const mockCoreApi: MockCoreApi = {
391 |       getProject: jest.fn().mockResolvedValue(mockProject),
392 |       getTeams: jest.fn().mockResolvedValue([]),
393 |     };
394 | 
395 |     const mockWorkItemTrackingApi: MockWorkItemTrackingApi = {
396 |       getWorkItemTypes: jest.fn().mockResolvedValue(mockWorkItemTypes),
397 |     };
398 | 
399 |     const mockProcessApi: MockProcessApi = {
400 |       getProcesses: jest.fn().mockResolvedValue(mockProcesses),
401 |       getProcessWorkItemTypes: jest
402 |         .fn()
403 |         .mockResolvedValue(mockProcessWorkItemTypes),
404 |     };
405 | 
406 |     const mockConnection: MockConnection = {
407 |       getCoreApi: jest.fn().mockResolvedValue(mockCoreApi),
408 |       getWorkItemTrackingApi: jest
409 |         .fn()
410 |         .mockResolvedValue(mockWorkItemTrackingApi),
411 |       getProcessApi: jest.fn().mockResolvedValue(mockProcessApi),
412 |     };
413 | 
414 |     // Act
415 |     const result = await getProjectDetails(
416 |       mockConnection as unknown as WebApi,
417 |       {
418 |         projectId: 'test-project',
419 |         includeWorkItemTypes: true,
420 |         includeFields: true,
421 |         includeProcess: true,
422 |       },
423 |     );
424 | 
425 |     // Assert
426 |     expect(result).toBeDefined();
427 |     expect(result.process).toBeDefined();
428 |     expect(result.process?.workItemTypes).toBeDefined();
429 |     expect(result.process?.workItemTypes?.[0].fields).toBeDefined();
430 |     expect(result.process?.workItemTypes?.[0].fields?.length).toBe(2);
431 |     expect(result.process?.workItemTypes?.[0].fields?.[0].name).toBe('Title');
432 |     expect(result.process?.workItemTypes?.[0].fields?.[1].name).toBe(
433 |       'Description',
434 |     );
435 |   });
436 | 
437 |   test('should propagate custom errors when thrown internally', async () => {
438 |     // Arrange
439 |     const mockConnection: MockConnection = {
440 |       getCoreApi: jest.fn().mockImplementation(() => {
441 |         throw new AzureDevOpsError('Custom error');
442 |       }),
443 |       getWorkItemTrackingApi: jest.fn(),
444 |       getProcessApi: jest.fn(),
445 |     };
446 | 
447 |     // Act & Assert
448 |     await expect(
449 |       getProjectDetails(mockConnection as unknown as WebApi, {
450 |         projectId: 'test-project',
451 |       }),
452 |     ).rejects.toThrow(AzureDevOpsError);
453 | 
454 |     await expect(
455 |       getProjectDetails(mockConnection as unknown as WebApi, {
456 |         projectId: 'test-project',
457 |       }),
458 |     ).rejects.toThrow('Custom error');
459 |   });
460 | 
461 |   test('should wrap unexpected errors in a friendly error message', async () => {
462 |     // Arrange
463 |     const mockConnection: MockConnection = {
464 |       getCoreApi: jest.fn().mockImplementation(() => {
465 |         throw new Error('Unexpected error');
466 |       }),
467 |       getWorkItemTrackingApi: jest.fn(),
468 |       getProcessApi: jest.fn(),
469 |     };
470 | 
471 |     // Act & Assert
472 |     await expect(
473 |       getProjectDetails(mockConnection as unknown as WebApi, {
474 |         projectId: 'test-project',
475 |       }),
476 |     ).rejects.toThrow('Failed to get project details: Unexpected error');
477 |   });
478 | });
479 | 
```

--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
  2 | import {
  3 |   CallToolRequestSchema,
  4 |   ListToolsRequestSchema,
  5 |   ListResourcesRequestSchema,
  6 |   ReadResourceRequestSchema,
  7 | } from '@modelcontextprotocol/sdk/types.js';
  8 | import { WebApi } from 'azure-devops-node-api';
  9 | import { GitVersionType } from 'azure-devops-node-api/interfaces/GitInterfaces';
 10 | import { VERSION } from './shared/config';
 11 | import { AzureDevOpsConfig } from './shared/types';
 12 | import {
 13 |   AzureDevOpsAuthenticationError,
 14 |   AzureDevOpsError,
 15 |   AzureDevOpsResourceNotFoundError,
 16 |   AzureDevOpsValidationError,
 17 | } from './shared/errors';
 18 | import { handleResponseError } from './shared/errors/handle-request-error';
 19 | import { AuthenticationMethod, AzureDevOpsClient } from './shared/auth';
 20 | // Import environment defaults when needed in feature handlers
 21 | 
 22 | // Import feature modules with request handlers and tool definitions
 23 | import {
 24 |   workItemsTools,
 25 |   isWorkItemsRequest,
 26 |   handleWorkItemsRequest,
 27 | } from './features/work-items';
 28 | 
 29 | import {
 30 |   projectsTools,
 31 |   isProjectsRequest,
 32 |   handleProjectsRequest,
 33 | } from './features/projects';
 34 | 
 35 | import {
 36 |   repositoriesTools,
 37 |   isRepositoriesRequest,
 38 |   handleRepositoriesRequest,
 39 | } from './features/repositories';
 40 | 
 41 | import {
 42 |   organizationsTools,
 43 |   isOrganizationsRequest,
 44 |   handleOrganizationsRequest,
 45 | } from './features/organizations';
 46 | 
 47 | import {
 48 |   searchTools,
 49 |   isSearchRequest,
 50 |   handleSearchRequest,
 51 | } from './features/search';
 52 | 
 53 | import {
 54 |   usersTools,
 55 |   isUsersRequest,
 56 |   handleUsersRequest,
 57 | } from './features/users';
 58 | 
 59 | import {
 60 |   pullRequestsTools,
 61 |   isPullRequestsRequest,
 62 |   handlePullRequestsRequest,
 63 | } from './features/pull-requests';
 64 | 
 65 | import {
 66 |   pipelinesTools,
 67 |   isPipelinesRequest,
 68 |   handlePipelinesRequest,
 69 | } from './features/pipelines';
 70 | 
 71 | import {
 72 |   wikisTools,
 73 |   isWikisRequest,
 74 |   handleWikisRequest,
 75 | } from './features/wikis';
 76 | 
 77 | // Create a safe console logging function that won't interfere with MCP protocol
 78 | function safeLog(message: string) {
 79 |   process.stderr.write(`${message}\n`);
 80 | }
 81 | 
 82 | /**
 83 |  * Type definition for the Azure DevOps MCP Server
 84 |  */
 85 | export type AzureDevOpsServer = Server;
 86 | 
 87 | /**
 88 |  * Create an Azure DevOps MCP Server
 89 |  *
 90 |  * @param config The Azure DevOps configuration
 91 |  * @returns A configured MCP server instance
 92 |  */
 93 | export function createAzureDevOpsServer(config: AzureDevOpsConfig): Server {
 94 |   // Validate the configuration
 95 |   validateConfig(config);
 96 | 
 97 |   // Initialize the MCP server
 98 |   const server = new Server(
 99 |     {
100 |       name: 'azure-devops-mcp',
101 |       version: VERSION,
102 |     },
103 |     {
104 |       capabilities: {
105 |         tools: {},
106 |         resources: {},
107 |       },
108 |     },
109 |   );
110 | 
111 |   // Register the ListTools request handler
112 |   server.setRequestHandler(ListToolsRequestSchema, () => {
113 |     // Combine tools from all features
114 |     const tools = [
115 |       ...usersTools,
116 |       ...organizationsTools,
117 |       ...projectsTools,
118 |       ...repositoriesTools,
119 |       ...workItemsTools,
120 |       ...searchTools,
121 |       ...pullRequestsTools,
122 |       ...pipelinesTools,
123 |       ...wikisTools,
124 |     ];
125 | 
126 |     return { tools };
127 |   });
128 | 
129 |   // Register the resource handlers
130 |   // ListResources - register available resource templates
131 |   server.setRequestHandler(ListResourcesRequestSchema, async () => {
132 |     // Create resource templates for repository content
133 |     const templates = [
134 |       // Default branch content
135 |       {
136 |         uriTemplate: 'ado://{organization}/{project}/{repo}/contents{/path*}',
137 |         name: 'Repository Content',
138 |         description: 'Content from the default branch of a repository',
139 |       },
140 |       // Branch specific content
141 |       {
142 |         uriTemplate:
143 |           'ado://{organization}/{project}/{repo}/branches/{branch}/contents{/path*}',
144 |         name: 'Branch Content',
145 |         description: 'Content from a specific branch of a repository',
146 |       },
147 |       // Commit specific content
148 |       {
149 |         uriTemplate:
150 |           'ado://{organization}/{project}/{repo}/commits/{commit}/contents{/path*}',
151 |         name: 'Commit Content',
152 |         description: 'Content from a specific commit in a repository',
153 |       },
154 |       // Tag specific content
155 |       {
156 |         uriTemplate:
157 |           'ado://{organization}/{project}/{repo}/tags/{tag}/contents{/path*}',
158 |         name: 'Tag Content',
159 |         description: 'Content from a specific tag in a repository',
160 |       },
161 |       // Pull request specific content
162 |       {
163 |         uriTemplate:
164 |           'ado://{organization}/{project}/{repo}/pullrequests/{prId}/contents{/path*}',
165 |         name: 'Pull Request Content',
166 |         description: 'Content from a specific pull request in a repository',
167 |       },
168 |     ];
169 | 
170 |     return {
171 |       resources: [],
172 |       templates,
173 |     };
174 |   });
175 | 
176 |   // ReadResource - handle reading content from the templates
177 |   server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
178 |     try {
179 |       const uri = new URL(request.params.uri);
180 | 
181 |       // Parse the URI to extract components
182 |       const segments = uri.pathname.split('/').filter(Boolean);
183 | 
184 |       // Check if it's an Azure DevOps resource URI
185 |       if (uri.protocol !== 'ado:') {
186 |         throw new AzureDevOpsResourceNotFoundError(
187 |           `Unsupported protocol: ${uri.protocol}`,
188 |         );
189 |       }
190 | 
191 |       // Extract organization, project, and repo
192 |       // const organization = segments[0]; // Currently unused but kept for future use
193 |       const project = segments[1];
194 |       const repo = segments[2];
195 | 
196 |       // Get a connection to Azure DevOps
197 |       const connection = await getConnection(config);
198 | 
199 |       // Default path is root if not specified
200 |       let path = '/';
201 |       // Extract path from the remaining segments, if there are at least 5 segments (org/project/repo/contents/path)
202 |       if (segments.length >= 5 && segments[3] === 'contents') {
203 |         path = '/' + segments.slice(4).join('/');
204 |       }
205 | 
206 |       // Determine version control parameters based on URI pattern
207 |       let versionType: number | undefined;
208 |       let version: string | undefined;
209 | 
210 |       if (segments[3] === 'branches' && segments.length >= 5) {
211 |         versionType = GitVersionType.Branch;
212 |         version = segments[4];
213 | 
214 |         // Extract path if present
215 |         if (segments.length >= 7 && segments[5] === 'contents') {
216 |           path = '/' + segments.slice(6).join('/');
217 |         }
218 |       } else if (segments[3] === 'commits' && segments.length >= 5) {
219 |         versionType = GitVersionType.Commit;
220 |         version = segments[4];
221 | 
222 |         // Extract path if present
223 |         if (segments.length >= 7 && segments[5] === 'contents') {
224 |           path = '/' + segments.slice(6).join('/');
225 |         }
226 |       } else if (segments[3] === 'tags' && segments.length >= 5) {
227 |         versionType = GitVersionType.Tag;
228 |         version = segments[4];
229 | 
230 |         // Extract path if present
231 |         if (segments.length >= 7 && segments[5] === 'contents') {
232 |           path = '/' + segments.slice(6).join('/');
233 |         }
234 |       } else if (segments[3] === 'pullrequests' && segments.length >= 5) {
235 |         // TODO: For PR head, we need to get the source branch or commit
236 |         // Currently just use the default branch as a fallback
237 |         // versionType = GitVersionType.Branch;
238 |         // version = 'PR-' + segments[4];
239 | 
240 |         // Extract path if present
241 |         if (segments.length >= 7 && segments[5] === 'contents') {
242 |           path = '/' + segments.slice(6).join('/');
243 |         }
244 |       }
245 | 
246 |       // Get the content
247 |       const versionDescriptor =
248 |         versionType && version ? { versionType, version } : undefined;
249 | 
250 |       // Import the getFileContent function from repositories feature
251 |       const { getFileContent } = await import(
252 |         './features/repositories/get-file-content/index.js'
253 |       );
254 | 
255 |       const fileContent = await getFileContent(
256 |         connection,
257 |         project,
258 |         repo,
259 |         path,
260 |         versionDescriptor,
261 |       );
262 | 
263 |       // Return the content based on whether it's a file or directory
264 |       return {
265 |         contents: [
266 |           {
267 |             uri: request.params.uri,
268 |             mimeType: fileContent.isDirectory
269 |               ? 'application/json'
270 |               : getMimeType(path),
271 |             text: fileContent.content,
272 |           },
273 |         ],
274 |       };
275 |     } catch (error) {
276 |       safeLog(`Error reading resource: ${error}`);
277 |       if (error instanceof AzureDevOpsError) {
278 |         throw error;
279 |       }
280 |       throw new AzureDevOpsResourceNotFoundError(
281 |         `Failed to read resource: ${error instanceof Error ? error.message : String(error)}`,
282 |       );
283 |     }
284 |   });
285 | 
286 |   // Register the CallTool request handler
287 |   server.setRequestHandler(CallToolRequestSchema, async (request) => {
288 |     try {
289 |       // Note: We don't need to validate the presence of arguments here because:
290 |       // 1. The schema validations (via zod.parse) will check for required parameters
291 |       // 2. Default values from environment.ts are applied for optional parameters (projectId, organizationId)
292 |       // 3. Arguments can be omitted entirely for tools with no required parameters
293 | 
294 |       // Get a connection to Azure DevOps
295 |       const connection = await getConnection(config);
296 | 
297 |       // Route the request to the appropriate feature handler
298 |       if (isWorkItemsRequest(request)) {
299 |         return await handleWorkItemsRequest(connection, request);
300 |       }
301 | 
302 |       if (isProjectsRequest(request)) {
303 |         return await handleProjectsRequest(connection, request);
304 |       }
305 | 
306 |       if (isRepositoriesRequest(request)) {
307 |         return await handleRepositoriesRequest(connection, request);
308 |       }
309 | 
310 |       if (isOrganizationsRequest(request)) {
311 |         // Organizations feature doesn't need the config object anymore
312 |         return await handleOrganizationsRequest(connection, request);
313 |       }
314 | 
315 |       if (isSearchRequest(request)) {
316 |         return await handleSearchRequest(connection, request);
317 |       }
318 | 
319 |       if (isUsersRequest(request)) {
320 |         return await handleUsersRequest(connection, request);
321 |       }
322 | 
323 |       if (isPullRequestsRequest(request)) {
324 |         return await handlePullRequestsRequest(connection, request);
325 |       }
326 | 
327 |       if (isPipelinesRequest(request)) {
328 |         return await handlePipelinesRequest(connection, request);
329 |       }
330 | 
331 |       if (isWikisRequest(request)) {
332 |         return await handleWikisRequest(connection, request);
333 |       }
334 | 
335 |       // If we get here, the tool is not recognized by any feature handler
336 |       throw new Error(`Unknown tool: ${request.params.name}`);
337 |     } catch (error) {
338 |       return handleResponseError(error);
339 |     }
340 |   });
341 | 
342 |   return server;
343 | }
344 | 
345 | /**
346 |  * Get a mime type based on file extension
347 |  *
348 |  * @param path File path
349 |  * @returns Mime type string
350 |  */
351 | function getMimeType(path: string): string {
352 |   const extension = path.split('.').pop()?.toLowerCase();
353 | 
354 |   switch (extension) {
355 |     case 'txt':
356 |       return 'text/plain';
357 |     case 'html':
358 |     case 'htm':
359 |       return 'text/html';
360 |     case 'css':
361 |       return 'text/css';
362 |     case 'js':
363 |       return 'application/javascript';
364 |     case 'json':
365 |       return 'application/json';
366 |     case 'xml':
367 |       return 'application/xml';
368 |     case 'md':
369 |       return 'text/markdown';
370 |     case 'png':
371 |       return 'image/png';
372 |     case 'jpg':
373 |     case 'jpeg':
374 |       return 'image/jpeg';
375 |     case 'gif':
376 |       return 'image/gif';
377 |     case 'webp':
378 |       return 'image/webp';
379 |     case 'svg':
380 |       return 'image/svg+xml';
381 |     case 'pdf':
382 |       return 'application/pdf';
383 |     case 'ts':
384 |     case 'tsx':
385 |       return 'application/typescript';
386 |     case 'py':
387 |       return 'text/x-python';
388 |     case 'cs':
389 |       return 'text/x-csharp';
390 |     case 'java':
391 |       return 'text/x-java';
392 |     case 'c':
393 |       return 'text/x-c';
394 |     case 'cpp':
395 |     case 'cc':
396 |       return 'text/x-c++';
397 |     case 'go':
398 |       return 'text/x-go';
399 |     case 'rs':
400 |       return 'text/x-rust';
401 |     case 'rb':
402 |       return 'text/x-ruby';
403 |     case 'sh':
404 |       return 'text/x-sh';
405 |     case 'yaml':
406 |     case 'yml':
407 |       return 'text/yaml';
408 |     default:
409 |       return 'text/plain';
410 |   }
411 | }
412 | 
413 | /**
414 |  * Validate the Azure DevOps configuration
415 |  *
416 |  * @param config The configuration to validate
417 |  * @throws {AzureDevOpsValidationError} If the configuration is invalid
418 |  */
419 | function validateConfig(config: AzureDevOpsConfig): void {
420 |   if (!config.organizationUrl) {
421 |     process.stderr.write(
422 |       'ERROR: Organization URL is required but was not provided.\n',
423 |     );
424 |     process.stderr.write(
425 |       `Config: ${JSON.stringify(
426 |         {
427 |           organizationUrl: config.organizationUrl,
428 |           authMethod: config.authMethod,
429 |           defaultProject: config.defaultProject,
430 |           // Hide PAT for security
431 |           personalAccessToken: config.personalAccessToken
432 |             ? 'REDACTED'
433 |             : undefined,
434 |           apiVersion: config.apiVersion,
435 |         },
436 |         null,
437 |         2,
438 |       )}\n`,
439 |     );
440 |     throw new AzureDevOpsValidationError('Organization URL is required');
441 |   }
442 | 
443 |   // Set default authentication method if not specified
444 |   if (!config.authMethod) {
445 |     config.authMethod = AuthenticationMethod.AzureIdentity;
446 |   }
447 | 
448 |   // Validate PAT if using PAT authentication
449 |   if (
450 |     config.authMethod === AuthenticationMethod.PersonalAccessToken &&
451 |     !config.personalAccessToken
452 |   ) {
453 |     throw new AzureDevOpsValidationError(
454 |       'Personal access token is required when using PAT authentication',
455 |     );
456 |   }
457 | }
458 | 
459 | /**
460 |  * Create a connection to Azure DevOps
461 |  *
462 |  * @param config The configuration to use
463 |  * @returns A WebApi connection
464 |  */
465 | export async function getConnection(
466 |   config: AzureDevOpsConfig,
467 | ): Promise<WebApi> {
468 |   try {
469 |     // Create a client with the appropriate authentication method
470 |     const client = new AzureDevOpsClient({
471 |       method: config.authMethod || AuthenticationMethod.AzureIdentity,
472 |       organizationUrl: config.organizationUrl,
473 |       personalAccessToken: config.personalAccessToken,
474 |     });
475 | 
476 |     // Test the connection by getting the Core API
477 |     await client.getCoreApi();
478 | 
479 |     // Return the underlying WebApi client
480 |     return await client.getWebApiClient();
481 |   } catch (error) {
482 |     throw new AzureDevOpsAuthenticationError(
483 |       `Failed to connect to Azure DevOps: ${error instanceof Error ? error.message : String(error)}`,
484 |     );
485 |   }
486 | }
487 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { updatePullRequest } from './feature';
  3 | import { createPullRequest } from '../create-pull-request/feature';
  4 | import { listWorkItems } from '../../work-items/list-work-items/feature';
  5 | import {
  6 |   getTestConnection,
  7 |   shouldSkipIntegrationTest,
  8 | } from '@/shared/test/test-helpers';
  9 | 
 10 | describe('updatePullRequest integration', () => {
 11 |   let connection: WebApi | null = null;
 12 |   let projectName: string;
 13 |   let repositoryName: string;
 14 |   let pullRequestId: number;
 15 |   let workItemId: number | null = null;
 16 | 
 17 |   // Generate unique identifiers using timestamp
 18 |   const timestamp = Date.now();
 19 |   const randomSuffix = Math.floor(Math.random() * 1000);
 20 |   const uniqueBranchName = `test-branch-${timestamp}-${randomSuffix}`;
 21 |   const uniqueTitle = `Test PR ${timestamp}-${randomSuffix}`;
 22 |   const updatedTitle = `Updated PR ${timestamp}-${randomSuffix}`;
 23 | 
 24 |   beforeAll(async () => {
 25 |     // Skip if integration tests should be skipped
 26 |     if (shouldSkipIntegrationTest()) {
 27 |       return;
 28 |     }
 29 | 
 30 |     // Get a real connection using environment variables
 31 |     connection = await getTestConnection();
 32 | 
 33 |     // Get project and repository names from environment variables
 34 |     projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
 35 |     repositoryName =
 36 |       process.env.AZURE_DEVOPS_DEFAULT_REPOSITORY || 'DefaultRepo';
 37 | 
 38 |     // Find an existing work item to use in tests
 39 |     if (!connection) {
 40 |       throw new Error('Connection is null');
 41 |     }
 42 |     const workItems = await listWorkItems(connection, {
 43 |       projectId: projectName,
 44 |       top: 1, // Just need one work item
 45 |     });
 46 | 
 47 |     if (workItems && workItems.length > 0 && workItems[0].id) {
 48 |       workItemId = workItems[0].id;
 49 |     }
 50 | 
 51 |     // Create a test pull request or find an existing one
 52 |     const gitApi = await connection.getGitApi();
 53 | 
 54 |     // Get the default branch's object ID
 55 |     const repository = await gitApi.getRepository(repositoryName, projectName);
 56 |     const defaultBranch =
 57 |       repository.defaultBranch?.replace('refs/heads/', '') || 'main';
 58 | 
 59 |     // Get the latest commit on the default branch
 60 |     const commits = await gitApi.getCommits(
 61 |       repositoryName,
 62 |       {
 63 |         $top: 1,
 64 |         itemVersion: {
 65 |           version: defaultBranch,
 66 |           versionType: 0, // 0 = branch
 67 |         },
 68 |       },
 69 |       projectName,
 70 |     );
 71 | 
 72 |     if (!commits || commits.length === 0) {
 73 |       throw new Error('No commits found in repository');
 74 |     }
 75 | 
 76 |     // Create a new branch
 77 |     const refUpdate = {
 78 |       name: `refs/heads/${uniqueBranchName}`,
 79 |       oldObjectId: '0000000000000000000000000000000000000000',
 80 |       newObjectId: commits[0].commitId,
 81 |     };
 82 | 
 83 |     const updateResult = await gitApi.updateRefs(
 84 |       [refUpdate],
 85 |       repositoryName,
 86 |       projectName,
 87 |     );
 88 | 
 89 |     if (
 90 |       !updateResult ||
 91 |       updateResult.length === 0 ||
 92 |       !updateResult[0].success
 93 |     ) {
 94 |       throw new Error('Failed to create new branch');
 95 |     }
 96 | 
 97 |     // Create a test pull request
 98 |     const testPullRequest = await createPullRequest(
 99 |       connection,
100 |       projectName,
101 |       repositoryName,
102 |       {
103 |         title: uniqueTitle,
104 |         description: 'Test pull request for integration testing',
105 |         sourceRefName: `refs/heads/${uniqueBranchName}`,
106 |         targetRefName: repository.defaultBranch || 'refs/heads/main',
107 |         isDraft: true,
108 |       },
109 |     );
110 | 
111 |     pullRequestId = testPullRequest.pullRequestId!;
112 |   });
113 | 
114 |   afterAll(async () => {
115 |     // Clean up created resources
116 |     if (!shouldSkipIntegrationTest() && connection && pullRequestId) {
117 |       try {
118 |         // Check the current state of the pull request
119 |         const gitApi = await connection.getGitApi();
120 |         const pullRequest = await gitApi.getPullRequestById(
121 |           pullRequestId,
122 |           projectName,
123 |         );
124 | 
125 |         // Only try to abandon if it's still active (status 1)
126 |         if (pullRequest && pullRequest.status === 1) {
127 |           await gitApi.updatePullRequest(
128 |             {
129 |               status: 2, // 2 = Abandoned
130 |             },
131 |             repositoryName,
132 |             pullRequestId,
133 |             projectName,
134 |           );
135 |         }
136 |         // eslint-disable-next-line @typescript-eslint/no-unused-vars
137 |       } catch (_) {
138 |         // Ignore cleanup errors
139 |       }
140 |     }
141 |   });
142 | 
143 |   test('should update pull request title and description', async () => {
144 |     // Skip if integration tests should be skipped
145 |     if (shouldSkipIntegrationTest() || !connection) {
146 |       console.log('Skipping test due to missing connection');
147 |       return;
148 |     }
149 | 
150 |     const updatedDescription = 'Updated description for integration testing';
151 | 
152 |     const result = await updatePullRequest({
153 |       projectId: projectName,
154 |       repositoryId: repositoryName,
155 |       pullRequestId,
156 |       title: updatedTitle,
157 |       description: updatedDescription,
158 |     });
159 | 
160 |     // Verify the update was successful
161 |     expect(result).toBeDefined();
162 |     expect(result.pullRequestId).toBe(pullRequestId);
163 |     expect(result.title).toBe(updatedTitle);
164 |     expect(result.description).toBe(updatedDescription);
165 |   }, 30000); // 30 second timeout for integration test
166 | 
167 |   test('should update pull request draft status', async () => {
168 |     // Skip if integration tests should be skipped
169 |     if (shouldSkipIntegrationTest() || !connection) {
170 |       console.log('Skipping test due to missing connection');
171 |       return;
172 |     }
173 | 
174 |     // Mark as not a draft
175 |     const result = await updatePullRequest({
176 |       projectId: projectName,
177 |       repositoryId: repositoryName,
178 |       pullRequestId,
179 |       isDraft: false,
180 |     });
181 | 
182 |     // Verify the update was successful
183 |     expect(result).toBeDefined();
184 |     expect(result.pullRequestId).toBe(pullRequestId);
185 |     expect(result.isDraft).toBe(false);
186 |   }, 30000); // 30 second timeout for integration test
187 | 
188 |   test('should add work item links to pull request', async () => {
189 |     // Skip if no work items were found
190 |     if (shouldSkipIntegrationTest()) {
191 |       console.log('Skipping test due to missing connection or work item');
192 |       return;
193 |     }
194 | 
195 |     // Add the work item link
196 |     const result = await updatePullRequest({
197 |       projectId: projectName,
198 |       repositoryId: repositoryName,
199 |       pullRequestId,
200 |       addWorkItemIds: [workItemId!],
201 |     });
202 | 
203 |     // Verify the update was successful
204 |     expect(result).toBeDefined();
205 |     expect(result.pullRequestId).toBe(pullRequestId);
206 | 
207 |     // Get the pull request work items using the proper API
208 |     const gitApi = await connection!.getGitApi();
209 | 
210 |     // Add a delay to allow Azure DevOps to process the work item link
211 |     await new Promise((resolve) => setTimeout(resolve, 5000));
212 | 
213 |     // Use the getPullRequestWorkItemRefs method to get the work items
214 |     const workItemRefs = await gitApi.getPullRequestWorkItemRefs(
215 |       repositoryName,
216 |       pullRequestId,
217 |       projectName,
218 |     );
219 | 
220 |     // Verify that work items are linked
221 |     expect(workItemRefs).toBeDefined();
222 |     expect(Array.isArray(workItemRefs)).toBe(true);
223 | 
224 |     // Check if our work item is in the list
225 |     const hasWorkItem = workItemRefs.some(
226 |       (ref) => ref.id !== undefined && Number(ref.id) === workItemId,
227 |     );
228 |     expect(hasWorkItem).toBe(true);
229 |   }, 60000); // 60 second timeout for integration test
230 | 
231 |   test('should remove work item links from pull request', async () => {
232 |     // Skip if no work items were found
233 |     if (shouldSkipIntegrationTest()) {
234 |       console.log('Skipping test due to missing connection or work item');
235 |       return;
236 |     }
237 | 
238 |     // First ensure the work item is linked
239 |     try {
240 |       await updatePullRequest({
241 |         projectId: projectName,
242 |         repositoryId: repositoryName,
243 |         pullRequestId,
244 |         addWorkItemIds: [workItemId!],
245 |       });
246 | 
247 |       // Add a delay to allow Azure DevOps to process the work item link
248 |       await new Promise((resolve) => setTimeout(resolve, 3000));
249 |     } catch (error) {
250 |       // If there's an error adding the link, that's okay
251 |       console.log(
252 |         "Error adding work item (already be linked so that's 👍):",
253 |         error instanceof Error ? error.message : String(error),
254 |       );
255 |     }
256 | 
257 |     // Then remove the work item link
258 |     const result = await updatePullRequest({
259 |       projectId: projectName,
260 |       repositoryId: repositoryName,
261 |       pullRequestId,
262 |       removeWorkItemIds: [workItemId!],
263 |     });
264 | 
265 |     // Verify the update was successful
266 |     expect(result).toBeDefined();
267 |     expect(result.pullRequestId).toBe(pullRequestId);
268 | 
269 |     // Get the pull request work items using the proper API
270 |     const gitApi = await connection!.getGitApi();
271 | 
272 |     // Add a delay to allow Azure DevOps to process the work item unlink
273 |     await new Promise((resolve) => setTimeout(resolve, 5000));
274 | 
275 |     // Use the getPullRequestWorkItemRefs method to get the work items
276 |     const workItemRefs = await gitApi.getPullRequestWorkItemRefs(
277 |       repositoryName,
278 |       pullRequestId,
279 |       projectName,
280 |     );
281 | 
282 |     // Verify that work items are properly unlinked
283 |     expect(workItemRefs).toBeDefined();
284 |     expect(Array.isArray(workItemRefs)).toBe(true);
285 | 
286 |     // Check if our work item is not in the list
287 |     const hasWorkItem = workItemRefs.some(
288 |       (ref) => ref.id !== undefined && Number(ref.id) === workItemId,
289 |     );
290 |     expect(hasWorkItem).toBe(false);
291 |   }, 60000); // 60 second timeout for integration test
292 | 
293 |   test('should add reviewers to pull request', async () => {
294 |     // Skip if integration tests should be skipped
295 |     if (shouldSkipIntegrationTest() || !connection) {
296 |       console.log('Skipping test due to missing connection');
297 |       return;
298 |     }
299 | 
300 |     // Find an actual user in the organization to use as a reviewer
301 |     const gitApi = await connection.getGitApi();
302 | 
303 |     // Get the pull request creator as a reviewer (they always exist)
304 |     const pullRequest = await gitApi.getPullRequestById(
305 |       pullRequestId,
306 |       projectName,
307 |     )!;
308 | 
309 |     // Use the pull request creator's ID as the reviewer
310 |     const reviewer = pullRequest.createdBy!.id!;
311 | 
312 |     // Add the reviewer
313 |     const result = await updatePullRequest({
314 |       projectId: projectName,
315 |       repositoryId: repositoryName,
316 |       pullRequestId,
317 |       addReviewers: [reviewer],
318 |     });
319 | 
320 |     // Verify the update was successful
321 |     expect(result).toBeDefined();
322 |     expect(result.pullRequestId).toBe(pullRequestId);
323 | 
324 |     // Add a delay to allow Azure DevOps to process the reviewer addition
325 |     await new Promise((resolve) => setTimeout(resolve, 1000));
326 | 
327 |     const reviewers = await gitApi.getPullRequestReviewers(
328 |       repositoryName,
329 |       pullRequestId,
330 |       projectName,
331 |     );
332 | 
333 |     // Verify that the reviewer was added
334 |     expect(reviewers).toBeDefined();
335 |     expect(Array.isArray(reviewers)).toBe(true);
336 | 
337 |     // Check if our reviewer is in the list by ID
338 |     const hasReviewer = reviewers.some((r) => r.id === reviewer);
339 |     expect(hasReviewer).toBe(true);
340 |   }, 60000); // 60 second timeout for integration test
341 | 
342 |   test('should remove reviewers from pull request', async () => {
343 |     // Skip if integration tests should be skipped
344 |     if (shouldSkipIntegrationTest() || !connection) {
345 |       console.log('Skipping test due to missing connection');
346 |       return;
347 |     }
348 | 
349 |     // Find an actual user in the organization to use as a reviewer
350 |     const gitApi = await connection.getGitApi();
351 | 
352 |     // Get the pull request creator as a reviewer (they always exist)
353 |     const pullRequest = await gitApi.getPullRequestById(
354 |       pullRequestId,
355 |       projectName,
356 |     );
357 | 
358 |     if (!pullRequest || !pullRequest.createdBy || !pullRequest.createdBy.id) {
359 |       throw new Error('Could not determine pull request creator');
360 |     }
361 | 
362 |     // Use the pull request creator's ID as the reviewer
363 |     const reviewer = pullRequest.createdBy.id;
364 | 
365 |     // First ensure the reviewer is added
366 |     try {
367 |       await updatePullRequest({
368 |         projectId: projectName,
369 |         repositoryId: repositoryName,
370 |         pullRequestId,
371 |         addReviewers: [reviewer],
372 |       });
373 | 
374 |       // Add a delay to allow Azure DevOps to process the reviewer addition
375 |       await new Promise((resolve) => setTimeout(resolve, 3000));
376 |     } catch (error) {
377 |       // If there's an error adding the reviewer, that's okay
378 |       console.log(
379 |         'Error adding reviewer (might already be added):',
380 |         error instanceof Error ? error.message : String(error),
381 |       );
382 |     }
383 | 
384 |     // Then remove the reviewer
385 |     const result = await updatePullRequest({
386 |       projectId: projectName,
387 |       repositoryId: repositoryName,
388 |       pullRequestId,
389 |       removeReviewers: [reviewer],
390 |     });
391 | 
392 |     // Verify the update was successful
393 |     expect(result).toBeDefined();
394 |     expect(result.pullRequestId).toBe(pullRequestId);
395 | 
396 |     // Add a delay to allow Azure DevOps to process the reviewer removal
397 |     await new Promise((resolve) => setTimeout(resolve, 3000));
398 | 
399 |     const reviewers = await gitApi.getPullRequestReviewers(
400 |       repositoryName,
401 |       pullRequestId,
402 |       projectName,
403 |     );
404 | 
405 |     // Verify that the reviewer was removed
406 |     expect(reviewers).toBeDefined();
407 |     expect(Array.isArray(reviewers)).toBe(true);
408 | 
409 |     // Check if our reviewer is not in the list
410 |     const hasReviewer = reviewers.some((r) => r.id === reviewer);
411 |     expect(hasReviewer).toBe(false);
412 |   }, 60000); // 60 second timeout for integration test
413 | 
414 |   test('should update pull request with additional properties', async () => {
415 |     // Skip if integration tests should be skipped
416 |     if (shouldSkipIntegrationTest() || !connection) {
417 |       console.log('Skipping test due to missing connection');
418 |       return;
419 |     }
420 | 
421 |     // Use a custom property that Azure DevOps supports
422 |     const customProperty = 'autoComplete';
423 |     const customValue = true;
424 | 
425 |     const result = await updatePullRequest({
426 |       projectId: projectName,
427 |       repositoryId: repositoryName,
428 |       pullRequestId,
429 |       additionalProperties: {
430 |         [customProperty]: customValue,
431 |       },
432 |     });
433 | 
434 |     // Verify the update was successful
435 |     expect(result).toBeDefined();
436 |     expect(result.pullRequestId).toBe(pullRequestId);
437 | 
438 |     // For autoComplete specifically, we can check if it's in the response
439 |     if (customProperty in result) {
440 |       expect(result[customProperty]).toBe(customValue);
441 |     }
442 |   }, 30000); // 30 second timeout for integration test
443 | 
444 |   test('should update pull request status to abandoned', async () => {
445 |     // Skip if integration tests should be skipped
446 |     if (shouldSkipIntegrationTest() || !connection) {
447 |       console.log('Skipping test due to missing connection');
448 |       return;
449 |     }
450 | 
451 |     // Abandon the pull request instead of completing it
452 |     // Completing requires additional setup that's complex for integration tests
453 |     const result = await updatePullRequest({
454 |       projectId: projectName,
455 |       repositoryId: repositoryName,
456 |       pullRequestId,
457 |       status: 'abandoned',
458 |     });
459 | 
460 |     // Verify the update was successful
461 |     expect(result).toBeDefined();
462 |     expect(result.pullRequestId).toBe(pullRequestId);
463 |     expect(result.status).toBe(2); // 2 = Abandoned
464 |   }, 30000); // 30 second timeout for integration test
465 | });
466 | 
```

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

```typescript
  1 | import { listWikiPages, WikiPageSummary } from './feature';
  2 | import * as azureDevOpsClient from '../../../clients/azure-devops';
  3 | import {
  4 |   AzureDevOpsError,
  5 |   AzureDevOpsResourceNotFoundError,
  6 |   AzureDevOpsPermissionError,
  7 | } from '../../../shared/errors/azure-devops-errors';
  8 | 
  9 | // Mock the Azure DevOps client
 10 | jest.mock('../../../clients/azure-devops');
 11 | 
 12 | // Mock the environment utilities to avoid dependency on environment variables
 13 | jest.mock('../../../utils/environment', () => ({
 14 |   defaultOrg: 'azure-devops-mcp-testing',
 15 |   defaultProject: 'eShopOnWeb',
 16 | }));
 17 | 
 18 | describe('listWikiPages unit', () => {
 19 |   // Mock WikiClient
 20 |   const mockWikiClient = {
 21 |     listWikiPages: jest.fn(),
 22 |   };
 23 | 
 24 |   // Mock getWikiClient function
 25 |   const mockGetWikiClient =
 26 |     azureDevOpsClient.getWikiClient as jest.MockedFunction<
 27 |       typeof azureDevOpsClient.getWikiClient
 28 |     >;
 29 | 
 30 |   beforeEach(() => {
 31 |     // Clear mock calls between tests
 32 |     jest.clearAllMocks();
 33 | 
 34 |     // Setup default mock implementation
 35 |     mockGetWikiClient.mockResolvedValue(mockWikiClient as any);
 36 |   });
 37 | 
 38 |   describe('Happy Path Scenarios', () => {
 39 |     test('should return wiki pages successfully', async () => {
 40 |       // Mock data
 41 |       const mockPages: WikiPageSummary[] = [
 42 |         {
 43 |           id: 1,
 44 |           path: '/Home',
 45 |           url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/1',
 46 |           order: 1,
 47 |         },
 48 |         {
 49 |           id: 2,
 50 |           path: '/Getting-Started',
 51 |           url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/2',
 52 |           order: 2,
 53 |         },
 54 |       ];
 55 | 
 56 |       // Setup mock responses
 57 |       mockWikiClient.listWikiPages.mockResolvedValue(mockPages);
 58 | 
 59 |       // Call the function
 60 |       const result = await listWikiPages({
 61 |         organizationId: 'test-org',
 62 |         projectId: 'test-project',
 63 |         wikiId: 'test-wiki',
 64 |       });
 65 | 
 66 |       // Assertions
 67 |       expect(mockGetWikiClient).toHaveBeenCalledWith({
 68 |         organizationId: 'test-org',
 69 |       });
 70 |       expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith(
 71 |         'test-project',
 72 |         'test-wiki',
 73 |       );
 74 |       expect(result).toEqual(mockPages);
 75 |       expect(result.length).toBe(2);
 76 |     });
 77 | 
 78 |     test('should handle basic listing without parameters', async () => {
 79 |       const mockPages: WikiPageSummary[] = [
 80 |         {
 81 |           id: 3,
 82 |           path: '/docs/api',
 83 |           url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/3',
 84 |           order: 1,
 85 |         },
 86 |       ];
 87 | 
 88 |       mockWikiClient.listWikiPages.mockResolvedValue(mockPages);
 89 | 
 90 |       const result = await listWikiPages({
 91 |         organizationId: 'test-org',
 92 |         projectId: 'test-project',
 93 |         wikiId: 'test-wiki',
 94 |       });
 95 | 
 96 |       expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith(
 97 |         'test-project',
 98 |         'test-wiki',
 99 |       );
100 |       expect(result).toEqual(mockPages);
101 |     });
102 | 
103 |     test('should handle nested pages correctly', async () => {
104 |       const mockPages: WikiPageSummary[] = [
105 |         {
106 |           id: 4,
107 |           path: '/deep/nested/page',
108 |           url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/4',
109 |           order: 1,
110 |         },
111 |       ];
112 | 
113 |       mockWikiClient.listWikiPages.mockResolvedValue(mockPages);
114 | 
115 |       const result = await listWikiPages({
116 |         organizationId: 'test-org',
117 |         projectId: 'test-project',
118 |         wikiId: 'test-wiki',
119 |       });
120 | 
121 |       expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith(
122 |         'test-project',
123 |         'test-wiki',
124 |       );
125 |       expect(result).toEqual(mockPages);
126 |     });
127 | 
128 |     test('should handle empty wiki correctly', async () => {
129 |       const mockPages: WikiPageSummary[] = [];
130 | 
131 |       mockWikiClient.listWikiPages.mockResolvedValue(mockPages);
132 | 
133 |       const result = await listWikiPages({
134 |         organizationId: 'test-org',
135 |         projectId: 'test-project',
136 |         wikiId: 'test-wiki',
137 |       });
138 | 
139 |       expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith(
140 |         'test-project',
141 |         'test-wiki',
142 |       );
143 |       expect(result).toEqual(mockPages);
144 |     });
145 | 
146 |     test('should return empty array when no pages found', async () => {
147 |       mockWikiClient.listWikiPages.mockResolvedValue([]);
148 | 
149 |       const result = await listWikiPages({
150 |         organizationId: 'test-org',
151 |         projectId: 'test-project',
152 |         wikiId: 'empty-wiki',
153 |       });
154 | 
155 |       expect(result).toEqual([]);
156 |       expect(Array.isArray(result)).toBe(true);
157 |     });
158 | 
159 |     test('should use default organization and project when not provided', async () => {
160 |       const mockPages: WikiPageSummary[] = [
161 |         {
162 |           id: 5,
163 |           path: '/default-page',
164 |           url: 'https://dev.azure.com/default-org/default-project/_wiki/wikis/wiki1/5',
165 |           order: 1,
166 |         },
167 |       ];
168 | 
169 |       mockWikiClient.listWikiPages.mockResolvedValue(mockPages);
170 | 
171 |       const result = await listWikiPages({
172 |         wikiId: 'test-wiki',
173 |       });
174 | 
175 |       expect(mockGetWikiClient).toHaveBeenCalledWith({
176 |         organizationId: 'azure-devops-mcp-testing', // Uses default from environment
177 |       });
178 |       expect(result).toEqual(mockPages);
179 |     });
180 |   });
181 | 
182 |   describe('Error Scenarios', () => {
183 |     test('should handle network timeout errors', async () => {
184 |       const timeoutError = new Error('Network timeout');
185 |       timeoutError.name = 'ETIMEDOUT';
186 |       mockWikiClient.listWikiPages.mockRejectedValue(timeoutError);
187 | 
188 |       await expect(
189 |         listWikiPages({
190 |           organizationId: 'test-org',
191 |           projectId: 'test-project',
192 |           wikiId: 'test-wiki',
193 |         }),
194 |       ).rejects.toThrow(AzureDevOpsError);
195 |     });
196 | 
197 |     test('should handle connection refused errors', async () => {
198 |       const connectionError = new Error('Connection refused');
199 |       connectionError.name = 'ECONNREFUSED';
200 |       mockWikiClient.listWikiPages.mockRejectedValue(connectionError);
201 | 
202 |       await expect(
203 |         listWikiPages({
204 |           organizationId: 'test-org',
205 |           projectId: 'test-project',
206 |           wikiId: 'test-wiki',
207 |         }),
208 |       ).rejects.toThrow(AzureDevOpsError);
209 |     });
210 | 
211 |     test('should propagate AzureDevOpsResourceNotFoundError from client', async () => {
212 |       const notFoundError = new AzureDevOpsResourceNotFoundError(
213 |         'Wiki not found: test-wiki',
214 |       );
215 |       mockWikiClient.listWikiPages.mockRejectedValue(notFoundError);
216 | 
217 |       await expect(
218 |         listWikiPages({
219 |           organizationId: 'test-org',
220 |           projectId: 'test-project',
221 |           wikiId: 'non-existent-wiki',
222 |         }),
223 |       ).rejects.toThrow(AzureDevOpsResourceNotFoundError);
224 |     });
225 | 
226 |     test('should propagate AzureDevOpsPermissionError from client', async () => {
227 |       const permissionError = new AzureDevOpsPermissionError(
228 |         'Permission denied to access wiki',
229 |       );
230 |       mockWikiClient.listWikiPages.mockRejectedValue(permissionError);
231 | 
232 |       await expect(
233 |         listWikiPages({
234 |           organizationId: 'test-org',
235 |           projectId: 'test-project',
236 |           wikiId: 'restricted-wiki',
237 |         }),
238 |       ).rejects.toThrow(AzureDevOpsPermissionError);
239 |     });
240 | 
241 |     test('should wrap unknown errors in AzureDevOpsError', async () => {
242 |       const unknownError = new Error('Unknown error occurred');
243 |       mockWikiClient.listWikiPages.mockRejectedValue(unknownError);
244 | 
245 |       await expect(
246 |         listWikiPages({
247 |           organizationId: 'test-org',
248 |           projectId: 'test-project',
249 |           wikiId: 'test-wiki',
250 |         }),
251 |       ).rejects.toThrow(AzureDevOpsError);
252 | 
253 |       try {
254 |         await listWikiPages({
255 |           organizationId: 'test-org',
256 |           projectId: 'test-project',
257 |           wikiId: 'test-wiki',
258 |         });
259 |       } catch (error) {
260 |         expect(error).toBeInstanceOf(AzureDevOpsError);
261 |         expect((error as AzureDevOpsError).message).toBe(
262 |           'Failed to list wiki pages',
263 |         );
264 |       }
265 |     });
266 | 
267 |     test('should handle client creation failure', async () => {
268 |       const clientError = new Error('Failed to create client');
269 |       mockGetWikiClient.mockRejectedValue(clientError);
270 | 
271 |       await expect(
272 |         listWikiPages({
273 |           organizationId: 'invalid-org',
274 |           projectId: 'test-project',
275 |           wikiId: 'test-wiki',
276 |         }),
277 |       ).rejects.toThrow(AzureDevOpsError);
278 |     });
279 |   });
280 | 
281 |   describe('Edge Cases and Input Validation', () => {
282 |     test('should handle malformed API response gracefully', async () => {
283 |       // Mock malformed response (missing required fields)
284 |       const malformedPages = [
285 |         {
286 |           id: 'invalid-id', // Should be number
287 |           path: null, // Should be string
288 |           url: undefined, // Should be string
289 |         },
290 |       ];
291 | 
292 |       mockWikiClient.listWikiPages.mockResolvedValue(malformedPages as any);
293 | 
294 |       const result = await listWikiPages({
295 |         organizationId: 'test-org',
296 |         projectId: 'test-project',
297 |         wikiId: 'test-wiki',
298 |       });
299 | 
300 |       // Should still return the data as-is (transformation happens in client)
301 |       expect(result).toEqual(malformedPages);
302 |     });
303 | 
304 |     test('should handle null/undefined response from client', async () => {
305 |       mockWikiClient.listWikiPages.mockResolvedValue(null as any);
306 | 
307 |       await expect(
308 |         listWikiPages({
309 |           organizationId: 'test-org',
310 |           projectId: 'test-project',
311 |           wikiId: 'test-wiki',
312 |         }),
313 |       ).rejects.toThrow(AzureDevOpsError);
314 |     });
315 | 
316 |     test('should handle very large page collections', async () => {
317 |       // Create a large mock dataset
318 |       const largeMockPages: WikiPageSummary[] = Array.from(
319 |         { length: 10000 },
320 |         (_, i) => ({
321 |           id: i + 1,
322 |           path: `/page-${i + 1}`,
323 |           url: `https://dev.azure.com/org/project/_wiki/wikis/wiki1/${i + 1}`,
324 |           order: i + 1,
325 |         }),
326 |       );
327 | 
328 |       mockWikiClient.listWikiPages.mockResolvedValue(largeMockPages);
329 | 
330 |       const result = await listWikiPages({
331 |         organizationId: 'test-org',
332 |         projectId: 'test-project',
333 |         wikiId: 'large-wiki',
334 |       });
335 | 
336 |       expect(result).toEqual(largeMockPages);
337 |       expect(result.length).toBe(10000);
338 |     });
339 | 
340 |     test('should handle pages with special characters in paths', async () => {
341 |       const specialCharPages: WikiPageSummary[] = [
342 |         {
343 |           id: 1,
344 |           path: '/页面-中文',
345 |           url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/1',
346 |           order: 1,
347 |         },
348 |         {
349 |           id: 2,
350 |           path: '/página-español',
351 |           url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/2',
352 |           order: 2,
353 |         },
354 |         {
355 |           id: 3,
356 |           path: '/page with spaces & symbols!@#$%',
357 |           url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/3',
358 |           order: 3,
359 |         },
360 |       ];
361 | 
362 |       mockWikiClient.listWikiPages.mockResolvedValue(specialCharPages);
363 | 
364 |       const result = await listWikiPages({
365 |         organizationId: 'test-org',
366 |         projectId: 'test-project',
367 |         wikiId: 'special-wiki',
368 |       });
369 | 
370 |       expect(result).toEqual(specialCharPages);
371 |     });
372 | 
373 |     test('should handle pages with missing optional order field', async () => {
374 |       const pagesWithoutOrder: WikiPageSummary[] = [
375 |         {
376 |           id: 1,
377 |           path: '/page-1',
378 |           url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/1',
379 |           // order field is optional and missing
380 |         } as WikiPageSummary,
381 |         {
382 |           id: 2,
383 |           path: '/page-2',
384 |           url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/2',
385 |           order: 5,
386 |         },
387 |       ];
388 | 
389 |       mockWikiClient.listWikiPages.mockResolvedValue(pagesWithoutOrder);
390 | 
391 |       const result = await listWikiPages({
392 |         organizationId: 'test-org',
393 |         projectId: 'test-project',
394 |         wikiId: 'test-wiki',
395 |       });
396 | 
397 |       expect(result).toEqual(pagesWithoutOrder);
398 |       expect(result[0].order).toBeUndefined();
399 |       expect(result[1].order).toBe(5);
400 |     });
401 |   });
402 | 
403 |   describe('Parameter Validation Edge Cases', () => {
404 |     test('should handle basic parameter validation', async () => {
405 |       const mockPages: WikiPageSummary[] = [];
406 |       mockWikiClient.listWikiPages.mockResolvedValue(mockPages);
407 | 
408 |       await listWikiPages({
409 |         organizationId: 'test-org',
410 |         projectId: 'test-project',
411 |         wikiId: 'test-wiki',
412 |       });
413 | 
414 |       expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith(
415 |         'test-project',
416 |         'test-wiki',
417 |       );
418 | 
419 |       await listWikiPages({
420 |         organizationId: 'test-org',
421 |         projectId: 'test-project',
422 |         wikiId: 'test-wiki',
423 |       });
424 | 
425 |       expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith(
426 |         'test-project',
427 |         'test-wiki',
428 |       );
429 |     });
430 | 
431 |     test('should handle empty string parameters', async () => {
432 |       const mockPages: WikiPageSummary[] = [];
433 |       mockWikiClient.listWikiPages.mockResolvedValue(mockPages);
434 | 
435 |       await listWikiPages({
436 |         organizationId: '',
437 |         projectId: '',
438 |         wikiId: 'test-wiki',
439 |       });
440 | 
441 |       expect(mockGetWikiClient).toHaveBeenCalledWith({
442 |         organizationId: 'azure-devops-mcp-testing', // Empty string gets overridden by default
443 |       });
444 |       expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith(
445 |         'eShopOnWeb', // Empty string gets overridden by default project
446 |         'test-wiki',
447 |       );
448 |     });
449 |   });
450 | 
451 |   describe('Data Transformation and Mapping', () => {
452 |     test('should preserve all WikiPageSummary fields correctly', async () => {
453 |       const mockPages: WikiPageSummary[] = [
454 |         {
455 |           id: 42,
456 |           path: '/test-page',
457 |           url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/42',
458 |           order: 10,
459 |         },
460 |       ];
461 | 
462 |       mockWikiClient.listWikiPages.mockResolvedValue(mockPages);
463 | 
464 |       const result = await listWikiPages({
465 |         organizationId: 'test-org',
466 |         projectId: 'test-project',
467 |         wikiId: 'test-wiki',
468 |       });
469 | 
470 |       expect(result[0]).toEqual({
471 |         id: 42,
472 |         path: '/test-page',
473 |         url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/42',
474 |         order: 10,
475 |       });
476 |     });
477 | 
478 |     test('should handle mixed data types in response', async () => {
479 |       const mixedPages = [
480 |         {
481 |           id: 1,
482 |           path: '/normal-page',
483 |           url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/1',
484 |           order: 1,
485 |         },
486 |         {
487 |           id: 2,
488 |           path: '/page-without-order',
489 |           url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/2',
490 |           // order is undefined
491 |         },
492 |         {
493 |           id: 3,
494 |           path: '/page-with-zero-order',
495 |           url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/3',
496 |           order: 0,
497 |         },
498 |       ];
499 | 
500 |       mockWikiClient.listWikiPages.mockResolvedValue(
501 |         mixedPages as WikiPageSummary[],
502 |       );
503 | 
504 |       const result = await listWikiPages({
505 |         organizationId: 'test-org',
506 |         projectId: 'test-project',
507 |         wikiId: 'test-wiki',
508 |       });
509 | 
510 |       expect(result).toEqual(mixedPages);
511 |       expect(result[1].order).toBeUndefined();
512 |       expect(result[2].order).toBe(0);
513 |     });
514 |   });
515 | });
516 | 
```

--------------------------------------------------------------------------------
/src/features/wikis/list-wiki-pages/feature.spec.int.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { listWikiPages, WikiPageSummary } from './feature';
  2 | import { getWikis } from '../get-wikis/feature';
  3 | import {
  4 |   getTestConnection,
  5 |   shouldSkipIntegrationTest,
  6 | } from '@/shared/test/test-helpers';
  7 | import { getOrgNameFromUrl } from '@/utils/environment';
  8 | import { AzureDevOpsError } from '@/shared/errors/azure-devops-errors';
  9 | 
 10 | // Ensure environment variables are set for testing
 11 | process.env.AZURE_DEVOPS_DEFAULT_PROJECT =
 12 |   process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'default-project';
 13 | 
 14 | describe('listWikiPages integration', () => {
 15 |   let projectName: string;
 16 |   let orgUrl: string;
 17 |   let organizationId: string;
 18 | 
 19 |   beforeAll(async () => {
 20 |     // Mock the required environment variable for testing
 21 |     process.env.AZURE_DEVOPS_ORG_URL =
 22 |       process.env.AZURE_DEVOPS_ORG_URL || 'https://example.visualstudio.com';
 23 | 
 24 |     // Get and validate required environment variables
 25 |     const envProjectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT;
 26 |     if (!envProjectName) {
 27 |       throw new Error(
 28 |         'AZURE_DEVOPS_DEFAULT_PROJECT environment variable is required',
 29 |       );
 30 |     }
 31 |     projectName = envProjectName;
 32 | 
 33 |     const envOrgUrl = process.env.AZURE_DEVOPS_ORG_URL;
 34 |     if (!envOrgUrl) {
 35 |       throw new Error('AZURE_DEVOPS_ORG_URL environment variable is required');
 36 |     }
 37 |     orgUrl = envOrgUrl;
 38 |     organizationId = getOrgNameFromUrl(orgUrl);
 39 |   });
 40 | 
 41 |   describe('Happy Path Tests', () => {
 42 |     test('should list pages in real test wiki', async () => {
 43 |       // Skip if no connection available
 44 |       if (shouldSkipIntegrationTest()) {
 45 |         return;
 46 |       }
 47 | 
 48 |       // Get a real connection using environment variables
 49 |       const connection = await getTestConnection();
 50 |       if (!connection) {
 51 |         throw new Error(
 52 |           'Connection should be available when test is not skipped',
 53 |         );
 54 |       }
 55 | 
 56 |       // First get available wikis
 57 |       const wikis = await getWikis(connection, { projectId: projectName });
 58 | 
 59 |       // Skip if no wikis are available
 60 |       if (wikis.length === 0) {
 61 |         console.log('Skipping test: No wikis available in the project');
 62 |         return;
 63 |       }
 64 | 
 65 |       // Use the first available wiki
 66 |       const wiki = wikis[0];
 67 |       if (!wiki.name) {
 68 |         throw new Error('Wiki name is undefined');
 69 |       }
 70 | 
 71 |       // List wiki pages
 72 |       const result = await listWikiPages({
 73 |         organizationId,
 74 |         projectId: projectName,
 75 |         wikiId: wiki.name,
 76 |       });
 77 | 
 78 |       // Verify the result structure
 79 |       expect(result).toBeDefined();
 80 |       expect(Array.isArray(result)).toBe(true);
 81 | 
 82 |       // If pages exist, verify their structure matches WikiPageSummary interface
 83 |       if (result.length > 0) {
 84 |         const page = result[0];
 85 |         expect(page).toHaveProperty('id');
 86 |         expect(page).toHaveProperty('path');
 87 |         expect(page).toHaveProperty('url');
 88 |         expect(typeof page.id).toBe('number');
 89 |         expect(typeof page.path).toBe('string');
 90 |         // url and order are optional
 91 |         if (page.url !== undefined) {
 92 |           expect(typeof page.url).toBe('string');
 93 |         }
 94 |         if (page.order !== undefined) {
 95 |           expect(typeof page.order).toBe('number');
 96 |         }
 97 |       }
 98 |     });
 99 | 
100 |     test('should handle wiki listing for different wiki structures', async () => {
101 |       // Skip if integration tests are disabled or no connection available
102 |       if (shouldSkipIntegrationTest()) {
103 |         return;
104 |       }
105 | 
106 |       // Get a real connection using environment variables
107 |       const connection = await getTestConnection();
108 |       if (!connection) {
109 |         throw new Error(
110 |           'Connection should be available when test is not skipped',
111 |         );
112 |       }
113 | 
114 |       // First get available wikis
115 |       const wikis = await getWikis(connection, { projectId: projectName });
116 | 
117 |       // Skip if no wikis are available
118 |       if (wikis.length === 0) {
119 |         console.log('Skipping test: No wikis available in the project');
120 |         return;
121 |       }
122 | 
123 |       // Use the first available wiki
124 |       const wiki = wikis[0];
125 |       if (!wiki.name) {
126 |         throw new Error('Wiki name is undefined');
127 |       }
128 | 
129 |       // Get all pages for different wiki structures
130 |       const allPages = await listWikiPages({
131 |         organizationId,
132 |         projectId: projectName,
133 |         wikiId: wiki.name,
134 |       });
135 | 
136 |       expect(Array.isArray(allPages)).toBe(true);
137 | 
138 |       // If we have pages, verify they have expected structure
139 |       if (allPages.length > 0) {
140 |         const firstPage = allPages[0];
141 |         expect(firstPage).toHaveProperty('id');
142 |         expect(firstPage).toHaveProperty('path');
143 |         expect(firstPage).toHaveProperty('url');
144 | 
145 |         // Verify nested pages if they exist
146 |         const nestedPages = allPages.filter(
147 |           (page) => page.path.includes('/') && page.path !== '/',
148 |         );
149 |         console.log(
150 |           `Found ${nestedPages.length} nested pages out of ${allPages.length} total pages`,
151 |         );
152 |       }
153 |     });
154 | 
155 |     test('should handle basic wiki page listing consistently', async () => {
156 |       // Skip if integration tests are disabled or no connection available
157 |       if (shouldSkipIntegrationTest()) {
158 |         return;
159 |       }
160 | 
161 |       // Get a real connection using environment variables
162 |       const connection = await getTestConnection();
163 |       if (!connection) {
164 |         throw new Error(
165 |           'Connection should be available when test is not skipped',
166 |         );
167 |       }
168 | 
169 |       // First get available wikis
170 |       const wikis = await getWikis(connection, { projectId: projectName });
171 | 
172 |       // Skip if no wikis are available
173 |       if (wikis.length === 0) {
174 |         console.log('Skipping test: No wikis available in the project');
175 |         return;
176 |       }
177 | 
178 |       // Use the first available wiki
179 |       const wiki = wikis[0];
180 |       if (!wiki.name) {
181 |         throw new Error('Wiki name is undefined');
182 |       }
183 | 
184 |       // Test basic page listing
185 |       const firstResult = await listWikiPages({
186 |         organizationId,
187 |         projectId: projectName,
188 |         wikiId: wiki.name,
189 |       });
190 | 
191 |       expect(Array.isArray(firstResult)).toBe(true);
192 | 
193 |       // Test again to ensure consistency
194 |       const secondResult = await listWikiPages({
195 |         organizationId,
196 |         projectId: projectName,
197 |         wikiId: wiki.name,
198 |       });
199 | 
200 |       expect(Array.isArray(secondResult)).toBe(true);
201 | 
202 |       // Results should be consistent
203 |       expect(secondResult.length).toBe(firstResult.length);
204 |     });
205 |   });
206 | 
207 |   describe('Error Scenarios', () => {
208 |     test('should handle invalid wikiId (expect 404 error)', async () => {
209 |       // Skip if integration tests are disabled or no connection available
210 |       if (shouldSkipIntegrationTest()) {
211 |         return;
212 |       }
213 | 
214 |       const invalidWikiId = 'non-existent-wiki-id-12345';
215 | 
216 |       await expect(
217 |         listWikiPages({
218 |           organizationId,
219 |           projectId: projectName,
220 |           wikiId: invalidWikiId,
221 |         }),
222 |       ).rejects.toThrow(AzureDevOpsError);
223 |     });
224 | 
225 |     test('should handle invalid projectId', async () => {
226 |       // Skip if integration tests are disabled or no connection available
227 |       if (shouldSkipIntegrationTest()) {
228 |         return;
229 |       }
230 | 
231 |       const invalidProjectId = 'non-existent-project-12345';
232 | 
233 |       await expect(
234 |         listWikiPages({
235 |           organizationId,
236 |           projectId: invalidProjectId,
237 |           wikiId: 'any-wiki',
238 |         }),
239 |       ).rejects.toThrow(AzureDevOpsError);
240 |     });
241 | 
242 |     test('should handle invalid organizationId', async () => {
243 |       // Skip if integration tests are disabled or no connection available
244 |       if (shouldSkipIntegrationTest()) {
245 |         return;
246 |       }
247 | 
248 |       const invalidOrgId = 'non-existent-org-12345';
249 | 
250 |       await expect(
251 |         listWikiPages({
252 |           organizationId: invalidOrgId,
253 |           projectId: projectName,
254 |           wikiId: 'any-wiki',
255 |         }),
256 |       ).rejects.toThrow(AzureDevOpsError);
257 |     });
258 |   });
259 | 
260 |   describe('Edge Cases', () => {
261 |     test('should handle empty wikis gracefully', async () => {
262 |       // Skip if integration tests are disabled or no connection available
263 |       if (shouldSkipIntegrationTest()) {
264 |         return;
265 |       }
266 | 
267 |       // Get a real connection using environment variables
268 |       const connection = await getTestConnection();
269 |       if (!connection) {
270 |         throw new Error(
271 |           'Connection should be available when test is not skipped',
272 |         );
273 |       }
274 | 
275 |       // First get available wikis
276 |       const wikis = await getWikis(connection, { projectId: projectName });
277 | 
278 |       // Skip if no wikis are available
279 |       if (wikis.length === 0) {
280 |         console.log('Skipping test: No wikis available in the project');
281 |         return;
282 |       }
283 | 
284 |       // Use the first available wiki
285 |       const wiki = wikis[0];
286 |       if (!wiki.name) {
287 |         throw new Error('Wiki name is undefined');
288 |       }
289 | 
290 |       // Test with a path that likely doesn't exist
291 |       const result = await listWikiPages({
292 |         organizationId,
293 |         projectId: projectName,
294 |         wikiId: wiki.name,
295 |       });
296 | 
297 |       // Should return an array (may be empty or contain all pages depending on API behavior)
298 |       expect(Array.isArray(result)).toBe(true);
299 |       // Note: Azure DevOps API may return all pages when path doesn't match
300 |       console.log(`Path filter test returned ${result.length} pages`);
301 |     });
302 | 
303 |     test('should handle deeply nested paths', async () => {
304 |       // Skip if integration tests are disabled or no connection available
305 |       if (shouldSkipIntegrationTest()) {
306 |         return;
307 |       }
308 | 
309 |       // Get a real connection using environment variables
310 |       const connection = await getTestConnection();
311 |       if (!connection) {
312 |         throw new Error(
313 |           'Connection should be available when test is not skipped',
314 |         );
315 |       }
316 | 
317 |       // First get available wikis
318 |       const wikis = await getWikis(connection, { projectId: projectName });
319 | 
320 |       // Skip if no wikis are available
321 |       if (wikis.length === 0) {
322 |         console.log('Skipping test: No wikis available in the project');
323 |         return;
324 |       }
325 | 
326 |       // Use the first available wiki
327 |       const wiki = wikis[0];
328 |       if (!wiki.name) {
329 |         throw new Error('Wiki name is undefined');
330 |       }
331 | 
332 |       // Test with default parameters
333 |       const result = await listWikiPages({
334 |         organizationId,
335 |         projectId: projectName,
336 |         wikiId: wiki.name,
337 |       });
338 | 
339 |       expect(Array.isArray(result)).toBe(true);
340 |       // Should not throw error with basic parameters
341 |     });
342 | 
343 |     test('should handle boundary recursionLevel values', async () => {
344 |       // Skip if integration tests are disabled or no connection available
345 |       if (shouldSkipIntegrationTest()) {
346 |         return;
347 |       }
348 | 
349 |       // Get a real connection using environment variables
350 |       const connection = await getTestConnection();
351 |       if (!connection) {
352 |         throw new Error(
353 |           'Connection should be available when test is not skipped',
354 |         );
355 |       }
356 | 
357 |       // First get available wikis
358 |       const wikis = await getWikis(connection, { projectId: projectName });
359 | 
360 |       // Skip if no wikis are available
361 |       if (wikis.length === 0) {
362 |         console.log('Skipping test: No wikis available in the project');
363 |         return;
364 |       }
365 | 
366 |       // Use the first available wiki
367 |       const wiki = wikis[0];
368 |       if (!wiki.name) {
369 |         throw new Error('Wiki name is undefined');
370 |       }
371 | 
372 |       // Test basic page listing
373 |       const firstResult = await listWikiPages({
374 |         organizationId,
375 |         projectId: projectName,
376 |         wikiId: wiki.name,
377 |       });
378 | 
379 |       expect(Array.isArray(firstResult)).toBe(true);
380 | 
381 |       // Test again for consistency
382 |       const secondResult = await listWikiPages({
383 |         organizationId,
384 |         projectId: projectName,
385 |         wikiId: wiki.name,
386 |       });
387 | 
388 |       expect(Array.isArray(secondResult)).toBe(true);
389 |     });
390 |   });
391 | 
392 |   describe('Data Structure Validation', () => {
393 |     test('should verify returned data structure matches WikiPageSummary interface', async () => {
394 |       // Skip if integration tests are disabled or no connection available
395 |       if (shouldSkipIntegrationTest()) {
396 |         return;
397 |       }
398 | 
399 |       // Get a real connection using environment variables
400 |       const connection = await getTestConnection();
401 |       if (!connection) {
402 |         throw new Error(
403 |           'Connection should be available when test is not skipped',
404 |         );
405 |       }
406 | 
407 |       // First get available wikis
408 |       const wikis = await getWikis(connection, { projectId: projectName });
409 | 
410 |       // Skip if no wikis are available
411 |       if (wikis.length === 0) {
412 |         console.log('Skipping test: No wikis available in the project');
413 |         return;
414 |       }
415 | 
416 |       // Use the first available wiki
417 |       const wiki = wikis[0];
418 |       if (!wiki.name) {
419 |         throw new Error('Wiki name is undefined');
420 |       }
421 | 
422 |       const result = await listWikiPages({
423 |         organizationId,
424 |         projectId: projectName,
425 |         wikiId: wiki.name,
426 |       });
427 | 
428 |       expect(Array.isArray(result)).toBe(true);
429 | 
430 |       // Validate each page in the result
431 |       result.forEach((page: WikiPageSummary) => {
432 |         // Required fields
433 |         expect(page).toHaveProperty('id');
434 |         expect(page).toHaveProperty('path');
435 |         expect(page).toHaveProperty('url');
436 | 
437 |         expect(typeof page.id).toBe('number');
438 |         expect(typeof page.path).toBe('string');
439 | 
440 |         // Optional fields
441 |         if (page.url !== undefined) {
442 |           expect(typeof page.url).toBe('string');
443 |         }
444 |         if (page.order !== undefined) {
445 |           expect(typeof page.order).toBe('number');
446 |         }
447 | 
448 |         // Validate URL format (if present)
449 |         if (page.url !== undefined) {
450 |           expect(page.url).toMatch(/^https?:\/\//);
451 |         }
452 | 
453 |         // Validate path format (should start with /)
454 |         expect(page.path).toMatch(/^\//);
455 |       });
456 |     });
457 |   });
458 | 
459 |   describe('Performance and Pagination', () => {
460 |     test('should handle large wiki structures efficiently', async () => {
461 |       // Skip if integration tests are disabled or no connection available
462 |       if (shouldSkipIntegrationTest()) {
463 |         return;
464 |       }
465 | 
466 |       // Get a real connection using environment variables
467 |       const connection = await getTestConnection();
468 |       if (!connection) {
469 |         throw new Error(
470 |           'Connection should be available when test is not skipped',
471 |         );
472 |       }
473 | 
474 |       // First get available wikis
475 |       const wikis = await getWikis(connection, { projectId: projectName });
476 | 
477 |       // Skip if no wikis are available
478 |       if (wikis.length === 0) {
479 |         console.log('Skipping test: No wikis available in the project');
480 |         return;
481 |       }
482 | 
483 |       // Use the first available wiki
484 |       const wiki = wikis[0];
485 |       if (!wiki.name) {
486 |         throw new Error('Wiki name is undefined');
487 |       }
488 | 
489 |       const startTime = Date.now();
490 | 
491 |       const result = await listWikiPages({
492 |         organizationId,
493 |         projectId: projectName,
494 |         wikiId: wiki.name,
495 |       });
496 | 
497 |       const endTime = Date.now();
498 |       const duration = endTime - startTime;
499 | 
500 |       expect(Array.isArray(result)).toBe(true);
501 | 
502 |       // Performance check - should complete within reasonable time (30 seconds)
503 |       expect(duration).toBeLessThan(30000);
504 | 
505 |       console.log(`Retrieved ${result.length} pages in ${duration}ms`);
506 |     });
507 |   });
508 | });
509 | 
```
Page 6/8FirstPrevNextLast