#
tokens: 48403/50000 16/281 files (page 5/8)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 5 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

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

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

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

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

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
  3 | import { isRepositoriesRequest, handleRepositoriesRequest } from './index';
  4 | import { getRepository } from './get-repository';
  5 | import { getRepositoryDetails } from './get-repository-details';
  6 | import { listRepositories } from './list-repositories';
  7 | import { getFileContent } from './get-file-content';
  8 | import {
  9 |   getAllRepositoriesTree,
 10 |   formatRepositoryTree,
 11 | } from './get-all-repositories-tree';
 12 | import { GitVersionType } from 'azure-devops-node-api/interfaces/GitInterfaces';
 13 | 
 14 | // Mock the imported modules
 15 | jest.mock('./get-repository', () => ({
 16 |   getRepository: jest.fn(),
 17 | }));
 18 | 
 19 | jest.mock('./get-repository-details', () => ({
 20 |   getRepositoryDetails: jest.fn(),
 21 | }));
 22 | 
 23 | jest.mock('./list-repositories', () => ({
 24 |   listRepositories: jest.fn(),
 25 | }));
 26 | 
 27 | jest.mock('./get-file-content', () => ({
 28 |   getFileContent: jest.fn(),
 29 | }));
 30 | 
 31 | jest.mock('./get-all-repositories-tree', () => ({
 32 |   getAllRepositoriesTree: jest.fn(),
 33 |   formatRepositoryTree: jest.fn(),
 34 | }));
 35 | 
 36 | describe('Repositories Request Handlers', () => {
 37 |   const mockConnection = {} as WebApi;
 38 | 
 39 |   describe('isRepositoriesRequest', () => {
 40 |     it('should return true for repositories requests', () => {
 41 |       const validTools = [
 42 |         'get_repository',
 43 |         'get_repository_details',
 44 |         'list_repositories',
 45 |         'get_file_content',
 46 |         'get_all_repositories_tree',
 47 |       ];
 48 |       validTools.forEach((tool) => {
 49 |         const request = {
 50 |           params: { name: tool, arguments: {} },
 51 |           method: 'tools/call',
 52 |         } as CallToolRequest;
 53 |         expect(isRepositoriesRequest(request)).toBe(true);
 54 |       });
 55 |     });
 56 | 
 57 |     it('should return false for non-repositories requests', () => {
 58 |       const request = {
 59 |         params: { name: 'list_projects', arguments: {} },
 60 |         method: 'tools/call',
 61 |       } as CallToolRequest;
 62 |       expect(isRepositoriesRequest(request)).toBe(false);
 63 |     });
 64 |   });
 65 | 
 66 |   describe('handleRepositoriesRequest', () => {
 67 |     it('should handle get_repository request', async () => {
 68 |       const mockRepository = { id: 'repo1', name: 'Repository 1' };
 69 |       (getRepository as jest.Mock).mockResolvedValue(mockRepository);
 70 | 
 71 |       const request = {
 72 |         params: {
 73 |           name: 'get_repository',
 74 |           arguments: {
 75 |             repositoryId: 'repo1',
 76 |           },
 77 |         },
 78 |         method: 'tools/call',
 79 |       } as CallToolRequest;
 80 | 
 81 |       const response = await handleRepositoriesRequest(mockConnection, request);
 82 |       expect(response.content).toHaveLength(1);
 83 |       expect(JSON.parse(response.content[0].text as string)).toEqual(
 84 |         mockRepository,
 85 |       );
 86 |       expect(getRepository).toHaveBeenCalledWith(
 87 |         mockConnection,
 88 |         expect.any(String),
 89 |         'repo1',
 90 |       );
 91 |     });
 92 | 
 93 |     it('should handle get_repository_details request', async () => {
 94 |       const mockRepositoryDetails = {
 95 |         repository: { id: 'repo1', name: 'Repository 1' },
 96 |         statistics: { branches: [] },
 97 |         refs: { value: [], count: 0 },
 98 |       };
 99 |       (getRepositoryDetails as jest.Mock).mockResolvedValue(
100 |         mockRepositoryDetails,
101 |       );
102 | 
103 |       const request = {
104 |         params: {
105 |           name: 'get_repository_details',
106 |           arguments: {
107 |             repositoryId: 'repo1',
108 |             includeStatistics: true,
109 |             includeRefs: true,
110 |           },
111 |         },
112 |         method: 'tools/call',
113 |       } as CallToolRequest;
114 | 
115 |       const response = await handleRepositoriesRequest(mockConnection, request);
116 |       expect(response.content).toHaveLength(1);
117 |       expect(JSON.parse(response.content[0].text as string)).toEqual(
118 |         mockRepositoryDetails,
119 |       );
120 |       expect(getRepositoryDetails).toHaveBeenCalledWith(
121 |         mockConnection,
122 |         expect.objectContaining({
123 |           repositoryId: 'repo1',
124 |           includeStatistics: true,
125 |           includeRefs: true,
126 |         }),
127 |       );
128 |     });
129 | 
130 |     it('should handle list_repositories request', async () => {
131 |       const mockRepositories = [
132 |         { id: 'repo1', name: 'Repository 1' },
133 |         { id: 'repo2', name: 'Repository 2' },
134 |       ];
135 |       (listRepositories as jest.Mock).mockResolvedValue(mockRepositories);
136 | 
137 |       const request = {
138 |         params: {
139 |           name: 'list_repositories',
140 |           arguments: {
141 |             projectId: 'project1',
142 |             includeLinks: true,
143 |           },
144 |         },
145 |         method: 'tools/call',
146 |       } as CallToolRequest;
147 | 
148 |       const response = await handleRepositoriesRequest(mockConnection, request);
149 |       expect(response.content).toHaveLength(1);
150 |       expect(JSON.parse(response.content[0].text as string)).toEqual(
151 |         mockRepositories,
152 |       );
153 |       expect(listRepositories).toHaveBeenCalledWith(
154 |         mockConnection,
155 |         expect.objectContaining({
156 |           projectId: 'project1',
157 |           includeLinks: true,
158 |         }),
159 |       );
160 |     });
161 | 
162 |     it('should handle get_file_content request', async () => {
163 |       const mockFileContent = { content: 'file content', isFolder: false };
164 |       (getFileContent as jest.Mock).mockResolvedValue(mockFileContent);
165 | 
166 |       const request = {
167 |         params: {
168 |           name: 'get_file_content',
169 |           arguments: {
170 |             repositoryId: 'repo1',
171 |             path: '/path/to/file',
172 |             version: 'main',
173 |             versionType: 'branch',
174 |           },
175 |         },
176 |         method: 'tools/call',
177 |       } as CallToolRequest;
178 | 
179 |       const response = await handleRepositoriesRequest(mockConnection, request);
180 |       expect(response.content).toHaveLength(1);
181 |       expect(JSON.parse(response.content[0].text as string)).toEqual(
182 |         mockFileContent,
183 |       );
184 |       expect(getFileContent).toHaveBeenCalledWith(
185 |         mockConnection,
186 |         expect.any(String),
187 |         'repo1',
188 |         '/path/to/file',
189 |         { versionType: GitVersionType.Branch, version: 'main' },
190 |       );
191 |     });
192 | 
193 |     it('should handle get_all_repositories_tree request', async () => {
194 |       const mockTreeResponse = {
195 |         repositories: [
196 |           {
197 |             name: 'repo1',
198 |             tree: [
199 |               { name: 'file1', path: '/file1', isFolder: false, level: 0 },
200 |             ],
201 |             stats: { directories: 0, files: 1 },
202 |           },
203 |         ],
204 |       };
205 |       (getAllRepositoriesTree as jest.Mock).mockResolvedValue(mockTreeResponse);
206 |       (formatRepositoryTree as jest.Mock).mockReturnValue('repo1\n  file1\n');
207 | 
208 |       const request = {
209 |         params: {
210 |           name: 'get_all_repositories_tree',
211 |           arguments: {
212 |             projectId: 'project1',
213 |             depth: 2,
214 |           },
215 |         },
216 |         method: 'tools/call',
217 |       } as CallToolRequest;
218 | 
219 |       const response = await handleRepositoriesRequest(mockConnection, request);
220 |       expect(response.content).toHaveLength(1);
221 |       expect(response.content[0].text as string).toContain('repo1');
222 |       expect(getAllRepositoriesTree).toHaveBeenCalledWith(
223 |         mockConnection,
224 |         expect.objectContaining({
225 |           projectId: 'project1',
226 |           depth: 2,
227 |         }),
228 |       );
229 |       expect(formatRepositoryTree).toHaveBeenCalledWith(
230 |         'repo1',
231 |         expect.any(Array),
232 |         expect.any(Object),
233 |         undefined,
234 |       );
235 |     });
236 | 
237 |     it('should throw error for unknown tool', async () => {
238 |       const request = {
239 |         params: {
240 |           name: 'unknown_tool',
241 |           arguments: {},
242 |         },
243 |         method: 'tools/call',
244 |       } as CallToolRequest;
245 | 
246 |       await expect(
247 |         handleRepositoriesRequest(mockConnection, request),
248 |       ).rejects.toThrow('Unknown repositories tool');
249 |     });
250 | 
251 |     it('should propagate errors from repository functions', async () => {
252 |       const mockError = new Error('Test error');
253 |       (listRepositories as jest.Mock).mockRejectedValue(mockError);
254 | 
255 |       const request = {
256 |         params: {
257 |           name: 'list_repositories',
258 |           arguments: {
259 |             projectId: 'project1',
260 |           },
261 |         },
262 |         method: 'tools/call',
263 |       } as CallToolRequest;
264 | 
265 |       await expect(
266 |         handleRepositoriesRequest(mockConnection, request),
267 |       ).rejects.toThrow(mockError);
268 |     });
269 |   });
270 | });
271 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import axios from 'axios';
  3 | import { DefaultAzureCredential, AzureCliCredential } from '@azure/identity';
  4 | import {
  5 |   AzureDevOpsError,
  6 |   AzureDevOpsResourceNotFoundError,
  7 |   AzureDevOpsValidationError,
  8 |   AzureDevOpsPermissionError,
  9 |   AzureDevOpsAuthenticationError,
 10 | } from '../../../shared/errors';
 11 | import {
 12 |   SearchCodeOptions,
 13 |   CodeSearchRequest,
 14 |   CodeSearchResponse,
 15 |   CodeSearchResult,
 16 | } from '../types';
 17 | import { GitVersionType } from 'azure-devops-node-api/interfaces/GitInterfaces';
 18 | 
 19 | /**
 20 |  * Search for code in Azure DevOps repositories
 21 |  *
 22 |  * @param connection The Azure DevOps WebApi connection
 23 |  * @param options Parameters for searching code
 24 |  * @returns Search results with optional file content
 25 |  */
 26 | export async function searchCode(
 27 |   connection: WebApi,
 28 |   options: SearchCodeOptions,
 29 | ): Promise<CodeSearchResponse> {
 30 |   try {
 31 |     // When includeContent is true, limit results to prevent timeouts
 32 |     const top = options.includeContent
 33 |       ? Math.min(options.top || 10, 10)
 34 |       : options.top;
 35 | 
 36 |     // Get the project ID (either provided or default)
 37 |     const projectId =
 38 |       options.projectId || process.env.AZURE_DEVOPS_DEFAULT_PROJECT;
 39 | 
 40 |     if (!projectId) {
 41 |       throw new AzureDevOpsValidationError(
 42 |         'Project ID is required. Either provide a projectId or set the AZURE_DEVOPS_DEFAULT_PROJECT environment variable.',
 43 |       );
 44 |     }
 45 | 
 46 |     // Prepare the search request
 47 |     const searchRequest: CodeSearchRequest = {
 48 |       searchText: options.searchText,
 49 |       $skip: options.skip,
 50 |       $top: top, // Use limited top value when includeContent is true
 51 |       filters: {
 52 |         Project: [projectId],
 53 |         ...(options.filters || {}),
 54 |       },
 55 |       includeFacets: true,
 56 |       includeSnippet: options.includeSnippet,
 57 |     };
 58 | 
 59 |     // Get the authorization header from the connection
 60 |     const authHeader = await getAuthorizationHeader();
 61 | 
 62 |     // Extract organization from the connection URL
 63 |     const { organization } = extractOrgFromUrl(connection);
 64 | 
 65 |     // Make the search API request with the project ID
 66 |     const searchUrl = `https://almsearch.dev.azure.com/${organization}/${projectId}/_apis/search/codesearchresults?api-version=7.1`;
 67 | 
 68 |     const searchResponse = await axios.post<CodeSearchResponse>(
 69 |       searchUrl,
 70 |       searchRequest,
 71 |       {
 72 |         headers: {
 73 |           Authorization: authHeader,
 74 |           'Content-Type': 'application/json',
 75 |         },
 76 |       },
 77 |     );
 78 | 
 79 |     const results = searchResponse.data;
 80 | 
 81 |     // If includeContent is true, fetch the content for each result
 82 |     if (options.includeContent && results.results.length > 0) {
 83 |       await enrichResultsWithContent(connection, results.results);
 84 |     }
 85 | 
 86 |     return results;
 87 |   } catch (error) {
 88 |     if (error instanceof AzureDevOpsError) {
 89 |       throw error;
 90 |     }
 91 | 
 92 |     if (axios.isAxiosError(error)) {
 93 |       const status = error.response?.status;
 94 |       if (status === 404) {
 95 |         throw new AzureDevOpsResourceNotFoundError(
 96 |           'Repository or project not found',
 97 |           { cause: error },
 98 |         );
 99 |       }
100 |       if (status === 400) {
101 |         throw new AzureDevOpsValidationError(
102 |           'Invalid search parameters',
103 |           error.response?.data,
104 |           { cause: error },
105 |         );
106 |       }
107 |       if (status === 401) {
108 |         throw new AzureDevOpsAuthenticationError('Authentication failed', {
109 |           cause: error,
110 |         });
111 |       }
112 |       if (status === 403) {
113 |         throw new AzureDevOpsPermissionError(
114 |           'Permission denied to access repository',
115 |           { cause: error },
116 |         );
117 |       }
118 |     }
119 | 
120 |     throw new AzureDevOpsError('Failed to search code', { cause: error });
121 |   }
122 | }
123 | 
124 | /**
125 |  * Extract organization from the connection URL
126 |  *
127 |  * @param connection The Azure DevOps WebApi connection
128 |  * @returns The organization
129 |  */
130 | function extractOrgFromUrl(connection: WebApi): { organization: string } {
131 |   // Extract organization from the connection URL
132 |   const url = connection.serverUrl;
133 |   const match = url.match(/https?:\/\/dev\.azure\.com\/([^/]+)/);
134 |   const organization = match ? match[1] : '';
135 | 
136 |   if (!organization) {
137 |     throw new AzureDevOpsValidationError(
138 |       'Could not extract organization from connection URL',
139 |     );
140 |   }
141 | 
142 |   return {
143 |     organization,
144 |   };
145 | }
146 | 
147 | /**
148 |  * Get the authorization header from the connection
149 |  *
150 |  * @returns The authorization header
151 |  */
152 | async function getAuthorizationHeader(): Promise<string> {
153 |   try {
154 |     // For PAT authentication, we can construct the header directly
155 |     if (
156 |       process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'pat' &&
157 |       process.env.AZURE_DEVOPS_PAT
158 |     ) {
159 |       // For PAT auth, we can construct the Basic auth header directly
160 |       const token = process.env.AZURE_DEVOPS_PAT;
161 |       const base64Token = Buffer.from(`:${token}`).toString('base64');
162 |       return `Basic ${base64Token}`;
163 |     }
164 | 
165 |     // For Azure Identity / Azure CLI auth, we need to get a token
166 |     // using the Azure DevOps resource ID
167 |     // Choose the appropriate credential based on auth method
168 |     const credential =
169 |       process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'azure-cli'
170 |         ? new AzureCliCredential()
171 |         : new DefaultAzureCredential();
172 | 
173 |     // Azure DevOps resource ID for token acquisition
174 |     const AZURE_DEVOPS_RESOURCE_ID = '499b84ac-1321-427f-aa17-267ca6975798';
175 | 
176 |     // Get token for Azure DevOps
177 |     const token = await credential.getToken(
178 |       `${AZURE_DEVOPS_RESOURCE_ID}/.default`,
179 |     );
180 | 
181 |     if (!token || !token.token) {
182 |       throw new Error('Failed to acquire token for Azure DevOps');
183 |     }
184 | 
185 |     return `Bearer ${token.token}`;
186 |   } catch (error) {
187 |     throw new AzureDevOpsValidationError(
188 |       `Failed to get authorization header: ${error instanceof Error ? error.message : String(error)}`,
189 |     );
190 |   }
191 | }
192 | 
193 | /**
194 |  * Enrich search results with file content
195 |  *
196 |  * @param connection The Azure DevOps WebApi connection
197 |  * @param results The search results to enrich
198 |  */
199 | async function enrichResultsWithContent(
200 |   connection: WebApi,
201 |   results: CodeSearchResult[],
202 | ): Promise<void> {
203 |   try {
204 |     const gitApi = await connection.getGitApi();
205 | 
206 |     // Process each result in parallel
207 |     await Promise.all(
208 |       results.map(async (result) => {
209 |         try {
210 |           // Get the file content using the Git API
211 |           // Pass only the required parameters to avoid the "path" and "scopePath" conflict
212 |           const contentStream = await gitApi.getItemContent(
213 |             result.repository.id,
214 |             result.path,
215 |             result.project.name,
216 |             undefined, // No version descriptor object
217 |             undefined, // No recursion level
218 |             undefined, // Don't include content metadata
219 |             undefined, // No latest processed change
220 |             false, // Don't download
221 |             {
222 |               version: result.versions[0]?.changeId,
223 |               versionType: GitVersionType.Commit,
224 |             }, // Version descriptor
225 |             true, // Include content
226 |           );
227 | 
228 |           // Convert the stream to a string and store it in the result
229 |           if (contentStream) {
230 |             // Since getItemContent always returns NodeJS.ReadableStream, we need to read the stream
231 |             const chunks: Buffer[] = [];
232 | 
233 |             // Listen for data events to collect chunks
234 |             contentStream.on('data', (chunk) => {
235 |               chunks.push(Buffer.from(chunk));
236 |             });
237 | 
238 |             // Use a promise to wait for the stream to finish
239 |             result.content = await new Promise<string>((resolve, reject) => {
240 |               contentStream.on('end', () => {
241 |                 // Concatenate all chunks and convert to string
242 |                 const buffer = Buffer.concat(chunks);
243 |                 resolve(buffer.toString('utf8'));
244 |               });
245 | 
246 |               contentStream.on('error', (err) => {
247 |                 reject(err);
248 |               });
249 |             });
250 |           }
251 |         } catch (error) {
252 |           // Log the error but don't fail the entire operation
253 |           console.error(
254 |             `Failed to fetch content for ${result.path}: ${error instanceof Error ? error.message : String(error)}`,
255 |           );
256 |         }
257 |       }),
258 |     );
259 |   } catch (error) {
260 |     // Log the error but don't fail the entire operation
261 |     console.error(
262 |       `Failed to enrich results with content: ${error instanceof Error ? error.message : String(error)}`,
263 |     );
264 |   }
265 | }
266 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { ICoreApi } from 'azure-devops-node-api/CoreApi';
  3 | import { IGitApi } from 'azure-devops-node-api/GitApi';
  4 | import { IWorkItemTrackingApi } from 'azure-devops-node-api/WorkItemTrackingApi';
  5 | import { IBuildApi } from 'azure-devops-node-api/BuildApi';
  6 | import { ITestApi } from 'azure-devops-node-api/TestApi';
  7 | import { IReleaseApi } from 'azure-devops-node-api/ReleaseApi';
  8 | import { ITaskAgentApi } from 'azure-devops-node-api/TaskAgentApi';
  9 | import { ITaskApi } from 'azure-devops-node-api/TaskApi';
 10 | import { AzureDevOpsError, AzureDevOpsAuthenticationError } from '../errors';
 11 | import { AuthenticationMethod } from '../auth';
 12 | import { AzureDevOpsClient as SharedClient } from '../auth/client-factory';
 13 | 
 14 | export interface AzureDevOpsClientConfig {
 15 |   orgUrl: string;
 16 |   pat: string;
 17 | }
 18 | 
 19 | /**
 20 |  * Azure DevOps Client
 21 |  *
 22 |  * Provides access to Azure DevOps APIs
 23 |  */
 24 | export class AzureDevOpsClient {
 25 |   private config: AzureDevOpsClientConfig;
 26 |   private clientPromise: Promise<WebApi> | null = null;
 27 | 
 28 |   constructor(config: AzureDevOpsClientConfig) {
 29 |     this.config = config;
 30 |   }
 31 | 
 32 |   /**
 33 |    * Get the authenticated Azure DevOps client
 34 |    *
 35 |    * @returns The authenticated WebApi client
 36 |    * @throws {AzureDevOpsAuthenticationError} If authentication fails
 37 |    */
 38 |   private async getClient(): Promise<WebApi> {
 39 |     if (!this.clientPromise) {
 40 |       this.clientPromise = (async () => {
 41 |         try {
 42 |           const sharedClient = new SharedClient({
 43 |             method: AuthenticationMethod.PersonalAccessToken,
 44 |             organizationUrl: this.config.orgUrl,
 45 |             personalAccessToken: this.config.pat,
 46 |           });
 47 |           return await sharedClient.getWebApiClient();
 48 |         } catch (error) {
 49 |           // If it's already an AzureDevOpsError, rethrow it
 50 |           if (error instanceof AzureDevOpsError) {
 51 |             throw error;
 52 |           }
 53 |           // Otherwise, wrap it in an AzureDevOpsAuthenticationError
 54 |           throw new AzureDevOpsAuthenticationError(
 55 |             error instanceof Error
 56 |               ? `Authentication failed: ${error.message}`
 57 |               : 'Authentication failed: Unknown error',
 58 |           );
 59 |         }
 60 |       })();
 61 |     }
 62 |     return this.clientPromise;
 63 |   }
 64 | 
 65 |   /**
 66 |    * Check if the client is authenticated
 67 |    *
 68 |    * @returns True if the client is authenticated
 69 |    */
 70 |   public async isAuthenticated(): Promise<boolean> {
 71 |     try {
 72 |       const client = await this.getClient();
 73 |       return !!client;
 74 |     } catch {
 75 |       // Any error means we're not authenticated
 76 |       return false;
 77 |     }
 78 |   }
 79 | 
 80 |   /**
 81 |    * Get the Core API
 82 |    *
 83 |    * @returns The Core API client
 84 |    * @throws {AzureDevOpsAuthenticationError} If authentication fails
 85 |    */
 86 |   public async getCoreApi(): Promise<ICoreApi> {
 87 |     try {
 88 |       const client = await this.getClient();
 89 |       return await client.getCoreApi();
 90 |     } catch (error) {
 91 |       // If it's already an AzureDevOpsError, rethrow it
 92 |       if (error instanceof AzureDevOpsError) {
 93 |         throw error;
 94 |       }
 95 |       // Otherwise, wrap it in an AzureDevOpsAuthenticationError
 96 |       throw new AzureDevOpsAuthenticationError(
 97 |         error instanceof Error
 98 |           ? `Failed to get Core API: ${error.message}`
 99 |           : 'Failed to get Core API: Unknown error',
100 |       );
101 |     }
102 |   }
103 | 
104 |   /**
105 |    * Get the Git API
106 |    *
107 |    * @returns The Git API client
108 |    * @throws {AzureDevOpsAuthenticationError} If authentication fails
109 |    */
110 |   public async getGitApi(): Promise<IGitApi> {
111 |     try {
112 |       const client = await this.getClient();
113 |       return await client.getGitApi();
114 |     } catch (error) {
115 |       // If it's already an AzureDevOpsError, rethrow it
116 |       if (error instanceof AzureDevOpsError) {
117 |         throw error;
118 |       }
119 |       // Otherwise, wrap it in an AzureDevOpsAuthenticationError
120 |       throw new AzureDevOpsAuthenticationError(
121 |         error instanceof Error
122 |           ? `Failed to get Git API: ${error.message}`
123 |           : 'Failed to get Git API: Unknown error',
124 |       );
125 |     }
126 |   }
127 | 
128 |   /**
129 |    * Get the Work Item Tracking API
130 |    *
131 |    * @returns The Work Item Tracking API client
132 |    * @throws {AzureDevOpsAuthenticationError} If authentication fails
133 |    */
134 |   public async getWorkItemTrackingApi(): Promise<IWorkItemTrackingApi> {
135 |     try {
136 |       const client = await this.getClient();
137 |       return await client.getWorkItemTrackingApi();
138 |     } catch (error) {
139 |       // If it's already an AzureDevOpsError, rethrow it
140 |       if (error instanceof AzureDevOpsError) {
141 |         throw error;
142 |       }
143 |       // Otherwise, wrap it in an AzureDevOpsAuthenticationError
144 |       throw new AzureDevOpsAuthenticationError(
145 |         error instanceof Error
146 |           ? `Failed to get Work Item Tracking API: ${error.message}`
147 |           : 'Failed to get Work Item Tracking API: Unknown error',
148 |       );
149 |     }
150 |   }
151 | 
152 |   /**
153 |    * Get the Build API
154 |    *
155 |    * @returns The Build API client
156 |    * @throws {AzureDevOpsAuthenticationError} If authentication fails
157 |    */
158 |   public async getBuildApi(): Promise<IBuildApi> {
159 |     try {
160 |       const client = await this.getClient();
161 |       return await client.getBuildApi();
162 |     } catch (error) {
163 |       // If it's already an AzureDevOpsError, rethrow it
164 |       if (error instanceof AzureDevOpsError) {
165 |         throw error;
166 |       }
167 |       // Otherwise, wrap it in an AzureDevOpsAuthenticationError
168 |       throw new AzureDevOpsAuthenticationError(
169 |         error instanceof Error
170 |           ? `Failed to get Build API: ${error.message}`
171 |           : 'Failed to get Build API: Unknown error',
172 |       );
173 |     }
174 |   }
175 | 
176 |   /**
177 |    * Get the Test API
178 |    *
179 |    * @returns The Test API client
180 |    * @throws {AzureDevOpsAuthenticationError} If authentication fails
181 |    */
182 |   public async getTestApi(): Promise<ITestApi> {
183 |     try {
184 |       const client = await this.getClient();
185 |       return await client.getTestApi();
186 |     } catch (error) {
187 |       // If it's already an AzureDevOpsError, rethrow it
188 |       if (error instanceof AzureDevOpsError) {
189 |         throw error;
190 |       }
191 |       // Otherwise, wrap it in an AzureDevOpsAuthenticationError
192 |       throw new AzureDevOpsAuthenticationError(
193 |         error instanceof Error
194 |           ? `Failed to get Test API: ${error.message}`
195 |           : 'Failed to get Test API: Unknown error',
196 |       );
197 |     }
198 |   }
199 | 
200 |   /**
201 |    * Get the Release API
202 |    *
203 |    * @returns The Release API client
204 |    * @throws {AzureDevOpsAuthenticationError} If authentication fails
205 |    */
206 |   public async getReleaseApi(): Promise<IReleaseApi> {
207 |     try {
208 |       const client = await this.getClient();
209 |       return await client.getReleaseApi();
210 |     } catch (error) {
211 |       // If it's already an AzureDevOpsError, rethrow it
212 |       if (error instanceof AzureDevOpsError) {
213 |         throw error;
214 |       }
215 |       // Otherwise, wrap it in an AzureDevOpsAuthenticationError
216 |       throw new AzureDevOpsAuthenticationError(
217 |         error instanceof Error
218 |           ? `Failed to get Release API: ${error.message}`
219 |           : 'Failed to get Release API: Unknown error',
220 |       );
221 |     }
222 |   }
223 | 
224 |   /**
225 |    * Get the Task Agent API
226 |    *
227 |    * @returns The Task Agent API client
228 |    * @throws {AzureDevOpsAuthenticationError} If authentication fails
229 |    */
230 |   public async getTaskAgentApi(): Promise<ITaskAgentApi> {
231 |     try {
232 |       const client = await this.getClient();
233 |       return await client.getTaskAgentApi();
234 |     } catch (error) {
235 |       // If it's already an AzureDevOpsError, rethrow it
236 |       if (error instanceof AzureDevOpsError) {
237 |         throw error;
238 |       }
239 |       // Otherwise, wrap it in an AzureDevOpsAuthenticationError
240 |       throw new AzureDevOpsAuthenticationError(
241 |         error instanceof Error
242 |           ? `Failed to get Task Agent API: ${error.message}`
243 |           : 'Failed to get Task Agent API: Unknown error',
244 |       );
245 |     }
246 |   }
247 | 
248 |   /**
249 |    * Get the Task API
250 |    *
251 |    * @returns The Task API client
252 |    * @throws {AzureDevOpsAuthenticationError} If authentication fails
253 |    */
254 |   public async getTaskApi(): Promise<ITaskApi> {
255 |     try {
256 |       const client = await this.getClient();
257 |       return await client.getTaskApi();
258 |     } catch (error) {
259 |       // If it's already an AzureDevOpsError, rethrow it
260 |       if (error instanceof AzureDevOpsError) {
261 |         throw error;
262 |       }
263 |       // Otherwise, wrap it in an AzureDevOpsAuthenticationError
264 |       throw new AzureDevOpsAuthenticationError(
265 |         error instanceof Error
266 |           ? `Failed to get Task API: ${error.message}`
267 |           : 'Failed to get Task API: Unknown error',
268 |       );
269 |     }
270 |   }
271 | }
272 | 
```

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

```typescript
  1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
  2 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
  3 | import { spawn } from 'child_process';
  4 | import { join } from 'path';
  5 | import dotenv from 'dotenv';
  6 | import { Organization } from './features/organizations/types';
  7 | import fs from 'fs';
  8 | 
  9 | // Load environment variables from .env file
 10 | dotenv.config();
 11 | 
 12 | describe('Azure DevOps MCP Server E2E Tests', () => {
 13 |   let client: Client;
 14 |   let serverProcess: ReturnType<typeof spawn>;
 15 |   let transport: StdioClientTransport;
 16 |   let tempEnvFile: string | null = null;
 17 | 
 18 |   beforeAll(async () => {
 19 |     // Debug: Log environment variables
 20 |     console.error('E2E TEST ENVIRONMENT VARIABLES:');
 21 |     console.error(
 22 |       `AZURE_DEVOPS_ORG_URL: ${process.env.AZURE_DEVOPS_ORG_URL || 'NOT SET'}`,
 23 |     );
 24 |     console.error(
 25 |       `AZURE_DEVOPS_PAT: ${process.env.AZURE_DEVOPS_PAT ? 'SET (hidden value)' : 'NOT SET'}`,
 26 |     );
 27 |     console.error(
 28 |       `AZURE_DEVOPS_DEFAULT_PROJECT: ${process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'NOT SET'}`,
 29 |     );
 30 |     console.error(
 31 |       `AZURE_DEVOPS_AUTH_METHOD: ${process.env.AZURE_DEVOPS_AUTH_METHOD || 'NOT SET'}`,
 32 |     );
 33 | 
 34 |     // Start the MCP server process
 35 |     const serverPath = join(process.cwd(), 'dist', 'index.js');
 36 | 
 37 |     // Create a temporary .env file for testing if needed
 38 |     const orgUrl = process.env.AZURE_DEVOPS_ORG_URL || '';
 39 |     const pat = process.env.AZURE_DEVOPS_PAT || '';
 40 |     const defaultProject = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || '';
 41 |     const authMethod = process.env.AZURE_DEVOPS_AUTH_METHOD || 'pat';
 42 | 
 43 |     if (orgUrl) {
 44 |       // Create a temporary .env file for the test
 45 |       tempEnvFile = join(process.cwd(), '.env.e2e-test');
 46 | 
 47 |       const envFileContent = `
 48 | AZURE_DEVOPS_ORG_URL=${orgUrl}
 49 | AZURE_DEVOPS_PAT=${pat}
 50 | AZURE_DEVOPS_DEFAULT_PROJECT=${defaultProject}
 51 | AZURE_DEVOPS_AUTH_METHOD=${authMethod}
 52 | `;
 53 | 
 54 |       fs.writeFileSync(tempEnvFile, envFileContent);
 55 |       console.error(`Created temporary .env file at ${tempEnvFile}`);
 56 | 
 57 |       // Start server with explicit file path to the temp .env file
 58 |       serverProcess = spawn('node', ['-r', 'dotenv/config', serverPath], {
 59 |         env: {
 60 |           ...process.env,
 61 |           NODE_ENV: 'test',
 62 |           DOTENV_CONFIG_PATH: tempEnvFile,
 63 |         },
 64 |       });
 65 |     } else {
 66 |       throw new Error(
 67 |         'Cannot start server: AZURE_DEVOPS_ORG_URL is not set in the environment',
 68 |       );
 69 |     }
 70 | 
 71 |     // Capture server output for debugging
 72 |     if (serverProcess && serverProcess.stderr) {
 73 |       serverProcess.stderr.on('data', (data) => {
 74 |         console.error(`Server error: ${data.toString()}`);
 75 |       });
 76 |     }
 77 | 
 78 |     // Give the server a moment to start
 79 |     await new Promise((resolve) => setTimeout(resolve, 1000));
 80 | 
 81 |     // Connect the MCP client to the server
 82 |     transport = new StdioClientTransport({
 83 |       command: 'node',
 84 |       args: ['-r', 'dotenv/config', serverPath],
 85 |       env: {
 86 |         ...process.env,
 87 |         NODE_ENV: 'test',
 88 |         DOTENV_CONFIG_PATH: tempEnvFile,
 89 |       },
 90 |     });
 91 | 
 92 |     client = new Client(
 93 |       {
 94 |         name: 'e2e-test-client',
 95 |         version: '1.0.0',
 96 |       },
 97 |       {
 98 |         capabilities: {
 99 |           tools: {},
100 |         },
101 |       },
102 |     );
103 | 
104 |     await client.connect(transport);
105 |   });
106 | 
107 |   afterAll(async () => {
108 |     // Clean up the client transport
109 |     if (transport) {
110 |       await transport.close();
111 |     }
112 | 
113 |     // Clean up the client
114 |     if (client) {
115 |       await client.close();
116 |     }
117 | 
118 |     // Clean up the server process
119 |     if (serverProcess) {
120 |       serverProcess.kill();
121 |     }
122 | 
123 |     // Clean up temporary env file
124 |     if (tempEnvFile && fs.existsSync(tempEnvFile)) {
125 |       fs.unlinkSync(tempEnvFile);
126 |       console.error(`Deleted temporary .env file at ${tempEnvFile}`);
127 |     }
128 | 
129 |     // Force exit to clean up any remaining handles
130 |     await new Promise<void>((resolve) => {
131 |       setTimeout(() => {
132 |         resolve();
133 |       }, 500);
134 |     });
135 |   });
136 | 
137 |   describe('Organizations', () => {
138 |     test('should list organizations', async () => {
139 |       // Arrange
140 |       // No specific arrangement needed for this test as we're just listing organizations
141 | 
142 |       // Act
143 |       const result = await client.callTool({
144 |         name: 'list_organizations',
145 |         arguments: {},
146 |       });
147 | 
148 |       // Assert
149 |       expect(result).toBeDefined();
150 | 
151 |       // Access the content safely
152 |       const content = result.content as Array<{ type: string; text: string }>;
153 |       expect(content).toBeDefined();
154 |       expect(content.length).toBeGreaterThan(0);
155 | 
156 |       // Parse the result content
157 |       const resultText = content[0].text;
158 |       const organizations: Organization[] = JSON.parse(resultText);
159 | 
160 |       // Verify the response structure
161 |       expect(Array.isArray(organizations)).toBe(true);
162 |       if (organizations.length > 0) {
163 |         const firstOrg = organizations[0];
164 |         expect(firstOrg).toHaveProperty('id');
165 |         expect(firstOrg).toHaveProperty('name');
166 |         expect(firstOrg).toHaveProperty('url');
167 |       }
168 |     });
169 |   });
170 | 
171 |   describe('Parameterless Tools', () => {
172 |     test('should call list_organizations without arguments', async () => {
173 |       // Act - call the tool without providing arguments
174 |       const result = await client.callTool({
175 |         name: 'list_organizations',
176 |         // No arguments provided
177 |         arguments: {},
178 |       });
179 | 
180 |       // Assert
181 |       expect(result).toBeDefined();
182 |       const content = result.content as Array<{ type: string; text: string }>;
183 |       expect(content).toBeDefined();
184 |       expect(content.length).toBeGreaterThan(0);
185 | 
186 |       // Verify we got a valid JSON response
187 |       const resultText = content[0].text;
188 |       const organizations = JSON.parse(resultText);
189 |       expect(Array.isArray(organizations)).toBe(true);
190 |     });
191 | 
192 |     test('should call get_me without arguments', async () => {
193 |       // Act - call the tool without providing arguments
194 |       const result = await client.callTool({
195 |         name: 'get_me',
196 |         // No arguments provided
197 |         arguments: {},
198 |       });
199 | 
200 |       // Assert
201 |       expect(result).toBeDefined();
202 |       const content = result.content as Array<{ type: string; text: string }>;
203 |       expect(content).toBeDefined();
204 |       expect(content.length).toBeGreaterThan(0);
205 | 
206 |       // Verify we got a valid JSON response with user info
207 |       const resultText = content[0].text;
208 |       const userInfo = JSON.parse(resultText);
209 |       expect(userInfo).toHaveProperty('id');
210 |       expect(userInfo).toHaveProperty('displayName');
211 |     });
212 |   });
213 | 
214 |   describe('Tools with Optional Parameters', () => {
215 |     test('should call list_projects without arguments', async () => {
216 |       // Act - call the tool without providing arguments
217 |       const result = await client.callTool({
218 |         name: 'list_projects',
219 |         // No arguments provided
220 |         arguments: {},
221 |       });
222 | 
223 |       // Assert
224 |       expect(result).toBeDefined();
225 |       const content = result.content as Array<{ type: string; text: string }>;
226 |       expect(content).toBeDefined();
227 |       expect(content.length).toBeGreaterThan(0);
228 | 
229 |       // Verify we got a valid JSON response
230 |       const resultText = content[0].text;
231 |       const projects = JSON.parse(resultText);
232 |       expect(Array.isArray(projects)).toBe(true);
233 |     });
234 | 
235 |     test('should call get_project without arguments', async () => {
236 |       // Act - call the tool without providing arguments
237 |       const result = await client.callTool({
238 |         name: 'get_project',
239 |         // No arguments provided
240 |         arguments: {},
241 |       });
242 | 
243 |       // Assert
244 |       expect(result).toBeDefined();
245 |       const content = result.content as Array<{ type: string; text: string }>;
246 |       expect(content).toBeDefined();
247 |       expect(content.length).toBeGreaterThan(0);
248 | 
249 |       // Verify we got a valid JSON response with project info
250 |       const resultText = content[0].text;
251 |       const project = JSON.parse(resultText);
252 |       expect(project).toHaveProperty('id');
253 |       expect(project).toHaveProperty('name');
254 |     });
255 | 
256 |     test('should call list_repositories without arguments', async () => {
257 |       // Act - call the tool without providing arguments
258 |       const result = await client.callTool({
259 |         name: 'list_repositories',
260 |         // No arguments provided
261 |         arguments: {},
262 |       });
263 | 
264 |       // Assert
265 |       expect(result).toBeDefined();
266 |       const content = result.content as Array<{ type: string; text: string }>;
267 |       expect(content).toBeDefined();
268 |       expect(content.length).toBeGreaterThan(0);
269 | 
270 |       // Verify we got a valid JSON response
271 |       const resultText = content[0].text;
272 |       const repositories = JSON.parse(resultText);
273 |       expect(Array.isArray(repositories)).toBe(true);
274 |     });
275 |   });
276 | });
277 | 
```

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

```typescript
  1 | import { getRepositoryDetails } from './feature';
  2 | import { GitVersionType } from 'azure-devops-node-api/interfaces/GitInterfaces';
  3 | import {
  4 |   AzureDevOpsError,
  5 |   AzureDevOpsResourceNotFoundError,
  6 | } from '../../../shared/errors';
  7 | import { GitRepository, GitBranchStats, GitRef } from '../types';
  8 | 
  9 | // Unit tests should only focus on isolated logic
 10 | // No real connections, HTTP requests, or dependencies
 11 | describe('getRepositoryDetails unit', () => {
 12 |   // Mock repository data
 13 |   const mockRepository: GitRepository = {
 14 |     id: 'repo-id',
 15 |     name: 'test-repo',
 16 |     url: 'https://dev.azure.com/org/project/_apis/git/repositories/repo-id',
 17 |     project: {
 18 |       id: 'project-id',
 19 |       name: 'test-project',
 20 |     },
 21 |     defaultBranch: 'refs/heads/main',
 22 |     size: 1024,
 23 |     remoteUrl: 'https://dev.azure.com/org/project/_git/test-repo',
 24 |     sshUrl: '[email protected]:v3/org/project/test-repo',
 25 |     webUrl: 'https://dev.azure.com/org/project/_git/test-repo',
 26 |   };
 27 | 
 28 |   // Mock branch stats data
 29 |   const mockBranchStats: GitBranchStats[] = [
 30 |     {
 31 |       name: 'refs/heads/main',
 32 |       aheadCount: 0,
 33 |       behindCount: 0,
 34 |       isBaseVersion: true,
 35 |       commit: {
 36 |         commitId: 'commit-id',
 37 |         author: {
 38 |           name: 'Test User',
 39 |           email: '[email protected]',
 40 |           date: new Date(),
 41 |         },
 42 |         committer: {
 43 |           name: 'Test User',
 44 |           email: '[email protected]',
 45 |           date: new Date(),
 46 |         },
 47 |         comment: 'Test commit',
 48 |       },
 49 |     },
 50 |   ];
 51 | 
 52 |   // Mock refs data
 53 |   const mockRefs: GitRef[] = [
 54 |     {
 55 |       name: 'refs/heads/main',
 56 |       objectId: 'commit-id',
 57 |       creator: {
 58 |         displayName: 'Test User',
 59 |         id: 'user-id',
 60 |       },
 61 |       url: 'https://dev.azure.com/org/project/_apis/git/repositories/repo-id/refs/heads/main',
 62 |     },
 63 |   ];
 64 | 
 65 |   test('should return basic repository information when no additional options are specified', async () => {
 66 |     // Arrange
 67 |     const mockConnection: any = {
 68 |       getGitApi: jest.fn().mockImplementation(() => ({
 69 |         getRepository: jest.fn().mockResolvedValue(mockRepository),
 70 |       })),
 71 |     };
 72 | 
 73 |     // Act
 74 |     const result = await getRepositoryDetails(mockConnection, {
 75 |       projectId: 'test-project',
 76 |       repositoryId: 'test-repo',
 77 |     });
 78 | 
 79 |     // Assert
 80 |     expect(result).toBeDefined();
 81 |     expect(result.repository).toEqual(mockRepository);
 82 |     expect(result.statistics).toBeUndefined();
 83 |     expect(result.refs).toBeUndefined();
 84 |   });
 85 | 
 86 |   test('should include branch statistics when includeStatistics is true', async () => {
 87 |     // Arrange
 88 |     const mockConnection: any = {
 89 |       getGitApi: jest.fn().mockImplementation(() => ({
 90 |         getRepository: jest.fn().mockResolvedValue(mockRepository),
 91 |         getBranches: jest.fn().mockResolvedValue(mockBranchStats),
 92 |       })),
 93 |     };
 94 | 
 95 |     // Act
 96 |     const result = await getRepositoryDetails(mockConnection, {
 97 |       projectId: 'test-project',
 98 |       repositoryId: 'test-repo',
 99 |       includeStatistics: true,
100 |     });
101 | 
102 |     // Assert
103 |     expect(result).toBeDefined();
104 |     expect(result.repository).toEqual(mockRepository);
105 |     expect(result.statistics).toBeDefined();
106 |     expect(result.statistics?.branches).toEqual(mockBranchStats);
107 |     expect(result.refs).toBeUndefined();
108 |   });
109 | 
110 |   test('should include refs when includeRefs is true', async () => {
111 |     // Arrange
112 |     const mockConnection: any = {
113 |       getGitApi: jest.fn().mockImplementation(() => ({
114 |         getRepository: jest.fn().mockResolvedValue(mockRepository),
115 |         getRefs: jest.fn().mockResolvedValue(mockRefs),
116 |       })),
117 |     };
118 | 
119 |     // Act
120 |     const result = await getRepositoryDetails(mockConnection, {
121 |       projectId: 'test-project',
122 |       repositoryId: 'test-repo',
123 |       includeRefs: true,
124 |     });
125 | 
126 |     // Assert
127 |     expect(result).toBeDefined();
128 |     expect(result.repository).toEqual(mockRepository);
129 |     expect(result.statistics).toBeUndefined();
130 |     expect(result.refs).toBeDefined();
131 |     expect(result.refs?.value).toEqual(mockRefs);
132 |     expect(result.refs?.count).toBe(mockRefs.length);
133 |   });
134 | 
135 |   test('should include both statistics and refs when both options are true', async () => {
136 |     // Arrange
137 |     const mockConnection: any = {
138 |       getGitApi: jest.fn().mockImplementation(() => ({
139 |         getRepository: jest.fn().mockResolvedValue(mockRepository),
140 |         getBranches: jest.fn().mockResolvedValue(mockBranchStats),
141 |         getRefs: jest.fn().mockResolvedValue(mockRefs),
142 |       })),
143 |     };
144 | 
145 |     // Act
146 |     const result = await getRepositoryDetails(mockConnection, {
147 |       projectId: 'test-project',
148 |       repositoryId: 'test-repo',
149 |       includeStatistics: true,
150 |       includeRefs: true,
151 |     });
152 | 
153 |     // Assert
154 |     expect(result).toBeDefined();
155 |     expect(result.repository).toEqual(mockRepository);
156 |     expect(result.statistics).toBeDefined();
157 |     expect(result.statistics?.branches).toEqual(mockBranchStats);
158 |     expect(result.refs).toBeDefined();
159 |     expect(result.refs?.value).toEqual(mockRefs);
160 |     expect(result.refs?.count).toBe(mockRefs.length);
161 |   });
162 | 
163 |   test('should pass refFilter to getRefs when provided', async () => {
164 |     // Arrange
165 |     const getRefs = jest.fn().mockResolvedValue(mockRefs);
166 |     const mockConnection: any = {
167 |       getGitApi: jest.fn().mockImplementation(() => ({
168 |         getRepository: jest.fn().mockResolvedValue(mockRepository),
169 |         getRefs,
170 |       })),
171 |     };
172 | 
173 |     // Act
174 |     await getRepositoryDetails(mockConnection, {
175 |       projectId: 'test-project',
176 |       repositoryId: 'test-repo',
177 |       includeRefs: true,
178 |       refFilter: 'heads/',
179 |     });
180 | 
181 |     // Assert
182 |     expect(getRefs).toHaveBeenCalledWith(
183 |       mockRepository.id,
184 |       'test-project',
185 |       'heads/',
186 |     );
187 |   });
188 | 
189 |   test('should pass branchName to getBranches when provided', async () => {
190 |     // Arrange
191 |     const getBranches = jest.fn().mockResolvedValue(mockBranchStats);
192 |     const mockConnection: any = {
193 |       getGitApi: jest.fn().mockImplementation(() => ({
194 |         getRepository: jest.fn().mockResolvedValue(mockRepository),
195 |         getBranches,
196 |       })),
197 |     };
198 | 
199 |     // Act
200 |     await getRepositoryDetails(mockConnection, {
201 |       projectId: 'test-project',
202 |       repositoryId: 'test-repo',
203 |       includeStatistics: true,
204 |       branchName: 'main',
205 |     });
206 | 
207 |     // Assert
208 |     expect(getBranches).toHaveBeenCalledWith(
209 |       mockRepository.id,
210 |       'test-project',
211 |       {
212 |         version: 'main',
213 |         versionType: GitVersionType.Branch,
214 |       },
215 |     );
216 |   });
217 | 
218 |   test('should propagate resource not found errors', async () => {
219 |     // Arrange
220 |     const mockConnection: any = {
221 |       getGitApi: jest.fn().mockImplementation(() => ({
222 |         getRepository: jest.fn().mockResolvedValue(null), // Simulate repository not found
223 |       })),
224 |     };
225 | 
226 |     // Act & Assert
227 |     await expect(
228 |       getRepositoryDetails(mockConnection, {
229 |         projectId: 'test-project',
230 |         repositoryId: 'non-existent-repo',
231 |       }),
232 |     ).rejects.toThrow(AzureDevOpsResourceNotFoundError);
233 | 
234 |     await expect(
235 |       getRepositoryDetails(mockConnection, {
236 |         projectId: 'test-project',
237 |         repositoryId: 'non-existent-repo',
238 |       }),
239 |     ).rejects.toThrow(
240 |       "Repository 'non-existent-repo' not found in project 'test-project'",
241 |     );
242 |   });
243 | 
244 |   test('should propagate custom errors when thrown internally', async () => {
245 |     // Arrange
246 |     const mockConnection: any = {
247 |       getGitApi: jest.fn().mockImplementation(() => {
248 |         throw new AzureDevOpsError('Custom error');
249 |       }),
250 |     };
251 | 
252 |     // Act & Assert
253 |     await expect(
254 |       getRepositoryDetails(mockConnection, {
255 |         projectId: 'test-project',
256 |         repositoryId: 'test-repo',
257 |       }),
258 |     ).rejects.toThrow(AzureDevOpsError);
259 | 
260 |     await expect(
261 |       getRepositoryDetails(mockConnection, {
262 |         projectId: 'test-project',
263 |         repositoryId: 'test-repo',
264 |       }),
265 |     ).rejects.toThrow('Custom error');
266 |   });
267 | 
268 |   test('should wrap unexpected errors in a friendly error message', async () => {
269 |     // Arrange
270 |     const mockConnection: any = {
271 |       getGitApi: jest.fn().mockImplementation(() => {
272 |         throw new Error('Unexpected error');
273 |       }),
274 |     };
275 | 
276 |     // Act & Assert
277 |     await expect(
278 |       getRepositoryDetails(mockConnection, {
279 |         projectId: 'test-project',
280 |         repositoryId: 'test-repo',
281 |       }),
282 |     ).rejects.toThrow('Failed to get repository details: Unexpected error');
283 |   });
284 | 
285 |   test('should handle null refs gracefully', async () => {
286 |     // Arrange
287 |     const mockConnection: any = {
288 |       getGitApi: jest.fn().mockImplementation(() => ({
289 |         getRepository: jest.fn().mockResolvedValue(mockRepository),
290 |         getRefs: jest.fn().mockResolvedValue(null), // Simulate null refs
291 |       })),
292 |     };
293 | 
294 |     // Act
295 |     const result = await getRepositoryDetails(mockConnection, {
296 |       projectId: 'test-project',
297 |       repositoryId: 'test-repo',
298 |       includeRefs: true,
299 |     });
300 | 
301 |     // Assert
302 |     expect(result).toBeDefined();
303 |     expect(result.repository).toEqual(mockRepository);
304 |     expect(result.refs).toBeDefined();
305 |     expect(result.refs?.value).toEqual([]);
306 |     expect(result.refs?.count).toBe(0);
307 |   });
308 | });
309 | 
```

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

```json
  1 | {
  2 |   "tasks": [
  3 |     {
  4 |       "id": "1c881b0f-7fd6-4184-89f5-1676a56e3719",
  5 |       "name": "Fix shared type definitions with explicit any warnings",
  6 |       "description": "Replace explicit 'any' types in shared type definitions with proper TypeScript types to resolve ESLint warnings. This includes RequestHandler return type and ToolDefinition inputSchema type.",
  7 |       "notes": "These are core type definitions used throughout the project, so changes must maintain backward compatibility. The CallToolResult type is the standard MCP SDK return type for tool responses.",
  8 |       "status": "completed",
  9 |       "dependencies": [],
 10 |       "createdAt": "2025-05-26T15:23:09.065Z",
 11 |       "updatedAt": "2025-05-26T15:33:17.167Z",
 12 |       "relatedFiles": [
 13 |         {
 14 |           "path": "src/shared/types/request-handler.ts",
 15 |           "type": "TO_MODIFY",
 16 |           "description": "Contains RequestHandler interface with 'any' return type",
 17 |           "lineStart": 14,
 18 |           "lineEnd": 16
 19 |         },
 20 |         {
 21 |           "path": "src/shared/types/tool-definition.ts",
 22 |           "type": "TO_MODIFY",
 23 |           "description": "Contains ToolDefinition interface with 'any' inputSchema type",
 24 |           "lineStart": 4,
 25 |           "lineEnd": 8
 26 |         }
 27 |       ],
 28 |       "implementationGuide": "1. Update src/shared/types/request-handler.ts line 15: Change 'any' to 'Promise<CallToolResult>' where CallToolResult is imported from '@modelcontextprotocol/sdk/types.js'\n2. Update src/shared/types/tool-definition.ts line 7: Change 'any' to 'JSONSchema7' where JSONSchema7 is imported from 'json-schema'\n3. Add necessary imports at the top of each file\n4. Ensure all existing functionality remains unchanged",
 29 |       "verificationCriteria": "1. ESLint warnings for these files should be resolved\n2. TypeScript compilation should succeed\n3. All existing tests should continue to pass\n4. Import statements should be properly added",
 30 |       "analysisResult": "Fix lint issues and get unit tests passing in the MCP Azure DevOps server project. The solution addresses 18 TypeScript 'any' type warnings by replacing them with proper types from Azure DevOps Node API and MCP SDK, and resolves 1 failing unit test by handling empty test files. All changes maintain backward compatibility and follow existing project patterns.",
 31 |       "summary": "Successfully replaced explicit 'any' types in shared type definitions with proper TypeScript types. Updated RequestHandler return type to use CallToolResult union type for backward compatibility, and ToolDefinition inputSchema to use JsonSchema7Type from zod-to-json-schema. ESLint warnings for these files are resolved, TypeScript compilation succeeds for the core types, and all existing functionality remains unchanged with proper imports added.",
 32 |       "completedAt": "2025-05-26T15:33:17.165Z"
 33 |     },
 34 |     {
 35 |       "id": "17aa94fe-24d4-4a8b-a127-ef27e121de38",
 36 |       "name": "Fix Azure DevOps client type warnings",
 37 |       "description": "Replace 'any' types in Azure DevOps client files with proper types from the azure-devops-node-api library to resolve 7 ESLint warnings in src/clients/azure-devops.ts.",
 38 |       "notes": "The azure-devops-node-api library provides comprehensive TypeScript interfaces. Prefer using existing library types over creating custom ones.",
 39 |       "status": "completed",
 40 |       "dependencies": [
 41 |         {
 42 |           "taskId": "1c881b0f-7fd6-4184-89f5-1676a56e3719"
 43 |         }
 44 |       ],
 45 |       "createdAt": "2025-05-26T15:23:09.065Z",
 46 |       "updatedAt": "2025-05-26T15:39:30.003Z",
 47 |       "relatedFiles": [
 48 |         {
 49 |           "path": "src/clients/azure-devops.ts",
 50 |           "type": "TO_MODIFY",
 51 |           "description": "Contains 7 'any' type warnings that need proper typing",
 52 |           "lineStart": 1,
 53 |           "lineEnd": 500
 54 |         }
 55 |       ],
 56 |       "implementationGuide": "1. Examine each 'any' usage in src/clients/azure-devops.ts at lines 78, 158, 244, 305, 345, 453, 484\n2. Replace with appropriate types from azure-devops-node-api interfaces\n3. Common patterns: Use TeamProject, GitRepository, WorkItem, BuildDefinition types\n4. For API responses, use the specific interface types provided by the library\n5. If no specific type exists, create a minimal interface with required properties",
 57 |       "verificationCriteria": "1. All 7 ESLint warnings in src/clients/azure-devops.ts should be resolved\n2. TypeScript compilation should succeed\n3. Existing functionality should remain unchanged\n4. Types should be imported from azure-devops-node-api where available",
 58 |       "analysisResult": "Fix lint issues and get unit tests passing in the MCP Azure DevOps server project. The solution addresses 18 TypeScript 'any' type warnings by replacing them with proper types from Azure DevOps Node API and MCP SDK, and resolves 1 failing unit test by handling empty test files. All changes maintain backward compatibility and follow existing project patterns.",
 59 |       "summary": "Successfully fixed all 7 ESLint warnings in src/clients/azure-devops.ts by replacing 'any' types with proper TypeScript interfaces. Created AzureDevOpsApiErrorResponse interface for Azure DevOps API error responses and replaced Record<string, any> with Record<string, string> for payload objects. All ESLint warnings are now resolved while maintaining existing functionality and backward compatibility.",
 60 |       "completedAt": "2025-05-26T15:39:30.002Z"
 61 |     },
 62 |     {
 63 |       "id": "d971e510-94cc-4f12-a1e8-a0ac35d57b7f",
 64 |       "name": "Fix feature-specific type warnings",
 65 |       "description": "Replace 'any' types in feature modules with proper Azure DevOps API types to resolve remaining ESLint warnings in projects, pull-requests, and repositories features.",
 66 |       "notes": "Each feature module should use the most specific Azure DevOps API type available. Check existing working features for type usage patterns.",
 67 |       "status": "completed",
 68 |       "dependencies": [
 69 |         {
 70 |           "taskId": "1c881b0f-7fd6-4184-89f5-1676a56e3719"
 71 |         }
 72 |       ],
 73 |       "createdAt": "2025-05-26T15:23:09.065Z",
 74 |       "updatedAt": "2025-05-26T15:51:24.788Z",
 75 |       "relatedFiles": [
 76 |         {
 77 |           "path": "src/features/projects/get-project-details/feature.ts",
 78 |           "type": "TO_MODIFY",
 79 |           "description": "Contains 'any' type warning at line 198"
 80 |         },
 81 |         {
 82 |           "path": "src/features/pull-requests/types.ts",
 83 |           "type": "TO_MODIFY",
 84 |           "description": "Contains 'any' type warnings at lines 20, 83"
 85 |         },
 86 |         {
 87 |           "path": "src/features/pull-requests/update-pull-request/feature.ts",
 88 |           "type": "TO_MODIFY",
 89 |           "description": "Contains 'any' type warnings at lines 33, 144, 213, 254"
 90 |         },
 91 |         {
 92 |           "path": "src/features/repositories/get-all-repositories-tree/feature.ts",
 93 |           "type": "TO_MODIFY",
 94 |           "description": "Contains 'any' type warning at line 231"
 95 |         },
 96 |         {
 97 |           "path": "src/shared/auth/client-factory.ts",
 98 |           "type": "TO_MODIFY",
 99 |           "description": "Contains 'any' type warning at line 282"
100 |         }
101 |       ],
102 |       "implementationGuide": "1. Fix src/features/projects/get-project-details/feature.ts line 198: Use TeamProject or TeamProjectReference type\n2. Fix src/features/pull-requests/types.ts lines 20, 83: Use GitPullRequest related interfaces\n3. Fix src/features/pull-requests/update-pull-request/feature.ts lines 33, 144, 213, 254: Use GitPullRequest and JsonPatchOperation types\n4. Fix src/features/repositories/get-all-repositories-tree/feature.ts line 231: Use GitTreeRef or GitItem type\n5. Fix src/shared/auth/client-factory.ts line 282: Use proper authentication credential type\n6. Import types from azure-devops-node-api/interfaces/",
103 |       "verificationCriteria": "1. All remaining ESLint 'any' type warnings should be resolved\n2. TypeScript compilation should succeed\n3. All existing tests should continue to pass\n4. Types should be consistent with Azure DevOps API documentation",
104 |       "analysisResult": "Fix lint issues and get unit tests passing in the MCP Azure DevOps server project. The solution addresses 18 TypeScript 'any' type warnings by replacing them with proper types from Azure DevOps Node API and MCP SDK, and resolves 1 failing unit test by handling empty test files. All changes maintain backward compatibility and follow existing project patterns.",
105 |       "summary": "Successfully fixed all feature-specific type warnings by replacing 'any' types with proper Azure DevOps API types. Fixed src/features/projects/get-project-details/feature.ts by using WorkItemTypeField interface, src/features/pull-requests/types.ts by replacing 'any' with specific union types, src/features/pull-requests/update-pull-request/feature.ts by using WebApi, AuthenticationMethod, and WorkItemRelation types, src/features/repositories/get-all-repositories-tree/feature.ts by using IGitApi type, and src/shared/auth/client-factory.ts by using IProfileApi type. All ESLint 'any' type warnings in the specified files have been resolved while maintaining type safety and consistency with Azure DevOps API documentation.",
106 |       "completedAt": "2025-05-26T15:51:24.787Z"
107 |     }
108 |   ]
109 | }
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { ICoreApi } from 'azure-devops-node-api/CoreApi';
  3 | import { IGitApi } from 'azure-devops-node-api/GitApi';
  4 | import { IWorkItemTrackingApi } from 'azure-devops-node-api/WorkItemTrackingApi';
  5 | import { IBuildApi } from 'azure-devops-node-api/BuildApi';
  6 | import { ITestApi } from 'azure-devops-node-api/TestApi';
  7 | import { IReleaseApi } from 'azure-devops-node-api/ReleaseApi';
  8 | import { ITaskAgentApi } from 'azure-devops-node-api/TaskAgentApi';
  9 | import { ITaskApi } from 'azure-devops-node-api/TaskApi';
 10 | import { IProfileApi } from 'azure-devops-node-api/ProfileApi';
 11 | import { AzureDevOpsError, AzureDevOpsAuthenticationError } from '../errors';
 12 | import { AuthConfig, createAuthClient } from './auth-factory';
 13 | 
 14 | /**
 15 |  * Azure DevOps Client
 16 |  *
 17 |  * Provides access to Azure DevOps APIs using the configured authentication method
 18 |  */
 19 | export class AzureDevOpsClient {
 20 |   private config: AuthConfig;
 21 |   private clientPromise: Promise<WebApi> | null = null;
 22 | 
 23 |   /**
 24 |    * Creates a new Azure DevOps client
 25 |    *
 26 |    * @param config Authentication configuration
 27 |    */
 28 |   constructor(config: AuthConfig) {
 29 |     this.config = config;
 30 |   }
 31 | 
 32 |   /**
 33 |    * Get the authenticated Azure DevOps client
 34 |    *
 35 |    * @returns The authenticated WebApi client
 36 |    * @throws {AzureDevOpsAuthenticationError} If authentication fails
 37 |    */
 38 |   private async getClient(): Promise<WebApi> {
 39 |     if (!this.clientPromise) {
 40 |       this.clientPromise = (async () => {
 41 |         try {
 42 |           return await createAuthClient(this.config);
 43 |         } catch (error) {
 44 |           // If it's already an AzureDevOpsError, rethrow it
 45 |           if (error instanceof AzureDevOpsError) {
 46 |             throw error;
 47 |           }
 48 |           // Otherwise, wrap it in an AzureDevOpsAuthenticationError
 49 |           throw new AzureDevOpsAuthenticationError(
 50 |             error instanceof Error
 51 |               ? `Authentication failed: ${error.message}`
 52 |               : 'Authentication failed: Unknown error',
 53 |           );
 54 |         }
 55 |       })();
 56 |     }
 57 |     return this.clientPromise;
 58 |   }
 59 | 
 60 |   /**
 61 |    * Get the underlying WebApi client
 62 |    *
 63 |    * @returns The authenticated WebApi client
 64 |    * @throws {AzureDevOpsAuthenticationError} If authentication fails
 65 |    */
 66 |   public async getWebApiClient(): Promise<WebApi> {
 67 |     return this.getClient();
 68 |   }
 69 | 
 70 |   /**
 71 |    * Check if the client is authenticated
 72 |    *
 73 |    * @returns True if the client is authenticated
 74 |    */
 75 |   public async isAuthenticated(): Promise<boolean> {
 76 |     try {
 77 |       const client = await this.getClient();
 78 |       return !!client;
 79 |     } catch {
 80 |       // Any error means we're not authenticated
 81 |       return false;
 82 |     }
 83 |   }
 84 | 
 85 |   /**
 86 |    * Get the Core API
 87 |    *
 88 |    * @returns The Core API client
 89 |    * @throws {AzureDevOpsAuthenticationError} If authentication fails
 90 |    */
 91 |   public async getCoreApi(): Promise<ICoreApi> {
 92 |     try {
 93 |       const client = await this.getClient();
 94 |       return await client.getCoreApi();
 95 |     } catch (error) {
 96 |       // If it's already an AzureDevOpsError, rethrow it
 97 |       if (error instanceof AzureDevOpsError) {
 98 |         throw error;
 99 |       }
100 |       // Otherwise, wrap it in an AzureDevOpsAuthenticationError
101 |       throw new AzureDevOpsAuthenticationError(
102 |         error instanceof Error
103 |           ? `Failed to get Core API: ${error.message}`
104 |           : 'Failed to get Core API: Unknown error',
105 |       );
106 |     }
107 |   }
108 | 
109 |   /**
110 |    * Get the Git API
111 |    *
112 |    * @returns The Git API client
113 |    * @throws {AzureDevOpsAuthenticationError} If authentication fails
114 |    */
115 |   public async getGitApi(): Promise<IGitApi> {
116 |     try {
117 |       const client = await this.getClient();
118 |       return await client.getGitApi();
119 |     } catch (error) {
120 |       // If it's already an AzureDevOpsError, rethrow it
121 |       if (error instanceof AzureDevOpsError) {
122 |         throw error;
123 |       }
124 |       // Otherwise, wrap it in an AzureDevOpsAuthenticationError
125 |       throw new AzureDevOpsAuthenticationError(
126 |         error instanceof Error
127 |           ? `Failed to get Git API: ${error.message}`
128 |           : 'Failed to get Git API: Unknown error',
129 |       );
130 |     }
131 |   }
132 | 
133 |   /**
134 |    * Get the Work Item Tracking API
135 |    *
136 |    * @returns The Work Item Tracking API client
137 |    * @throws {AzureDevOpsAuthenticationError} If authentication fails
138 |    */
139 |   public async getWorkItemTrackingApi(): Promise<IWorkItemTrackingApi> {
140 |     try {
141 |       const client = await this.getClient();
142 |       return await client.getWorkItemTrackingApi();
143 |     } catch (error) {
144 |       // If it's already an AzureDevOpsError, rethrow it
145 |       if (error instanceof AzureDevOpsError) {
146 |         throw error;
147 |       }
148 |       // Otherwise, wrap it in an AzureDevOpsAuthenticationError
149 |       throw new AzureDevOpsAuthenticationError(
150 |         error instanceof Error
151 |           ? `Failed to get Work Item Tracking API: ${error.message}`
152 |           : 'Failed to get Work Item Tracking API: Unknown error',
153 |       );
154 |     }
155 |   }
156 | 
157 |   /**
158 |    * Get the Build API
159 |    *
160 |    * @returns The Build API client
161 |    * @throws {AzureDevOpsAuthenticationError} If authentication fails
162 |    */
163 |   public async getBuildApi(): Promise<IBuildApi> {
164 |     try {
165 |       const client = await this.getClient();
166 |       return await client.getBuildApi();
167 |     } catch (error) {
168 |       // If it's already an AzureDevOpsError, rethrow it
169 |       if (error instanceof AzureDevOpsError) {
170 |         throw error;
171 |       }
172 |       // Otherwise, wrap it in an AzureDevOpsAuthenticationError
173 |       throw new AzureDevOpsAuthenticationError(
174 |         error instanceof Error
175 |           ? `Failed to get Build API: ${error.message}`
176 |           : 'Failed to get Build API: Unknown error',
177 |       );
178 |     }
179 |   }
180 | 
181 |   /**
182 |    * Get the Test API
183 |    *
184 |    * @returns The Test API client
185 |    * @throws {AzureDevOpsAuthenticationError} If authentication fails
186 |    */
187 |   public async getTestApi(): Promise<ITestApi> {
188 |     try {
189 |       const client = await this.getClient();
190 |       return await client.getTestApi();
191 |     } catch (error) {
192 |       // If it's already an AzureDevOpsError, rethrow it
193 |       if (error instanceof AzureDevOpsError) {
194 |         throw error;
195 |       }
196 |       // Otherwise, wrap it in an AzureDevOpsAuthenticationError
197 |       throw new AzureDevOpsAuthenticationError(
198 |         error instanceof Error
199 |           ? `Failed to get Test API: ${error.message}`
200 |           : 'Failed to get Test API: Unknown error',
201 |       );
202 |     }
203 |   }
204 | 
205 |   /**
206 |    * Get the Release API
207 |    *
208 |    * @returns The Release API client
209 |    * @throws {AzureDevOpsAuthenticationError} If authentication fails
210 |    */
211 |   public async getReleaseApi(): Promise<IReleaseApi> {
212 |     try {
213 |       const client = await this.getClient();
214 |       return await client.getReleaseApi();
215 |     } catch (error) {
216 |       // If it's already an AzureDevOpsError, rethrow it
217 |       if (error instanceof AzureDevOpsError) {
218 |         throw error;
219 |       }
220 |       // Otherwise, wrap it in an AzureDevOpsAuthenticationError
221 |       throw new AzureDevOpsAuthenticationError(
222 |         error instanceof Error
223 |           ? `Failed to get Release API: ${error.message}`
224 |           : 'Failed to get Release API: Unknown error',
225 |       );
226 |     }
227 |   }
228 | 
229 |   /**
230 |    * Get the Task Agent API
231 |    *
232 |    * @returns The Task Agent API client
233 |    * @throws {AzureDevOpsAuthenticationError} If authentication fails
234 |    */
235 |   public async getTaskAgentApi(): Promise<ITaskAgentApi> {
236 |     try {
237 |       const client = await this.getClient();
238 |       return await client.getTaskAgentApi();
239 |     } catch (error) {
240 |       // If it's already an AzureDevOpsError, rethrow it
241 |       if (error instanceof AzureDevOpsError) {
242 |         throw error;
243 |       }
244 |       // Otherwise, wrap it in an AzureDevOpsAuthenticationError
245 |       throw new AzureDevOpsAuthenticationError(
246 |         error instanceof Error
247 |           ? `Failed to get Task Agent API: ${error.message}`
248 |           : 'Failed to get Task Agent API: Unknown error',
249 |       );
250 |     }
251 |   }
252 | 
253 |   /**
254 |    * Get the Task API
255 |    *
256 |    * @returns The Task API client
257 |    * @throws {AzureDevOpsAuthenticationError} If authentication fails
258 |    */
259 |   public async getTaskApi(): Promise<ITaskApi> {
260 |     try {
261 |       const client = await this.getClient();
262 |       return await client.getTaskApi();
263 |     } catch (error) {
264 |       // If it's already an AzureDevOpsError, rethrow it
265 |       if (error instanceof AzureDevOpsError) {
266 |         throw error;
267 |       }
268 |       // Otherwise, wrap it in an AzureDevOpsAuthenticationError
269 |       throw new AzureDevOpsAuthenticationError(
270 |         error instanceof Error
271 |           ? `Failed to get Task API: ${error.message}`
272 |           : 'Failed to get Task API: Unknown error',
273 |       );
274 |     }
275 |   }
276 | 
277 |   /**
278 |    * Get the Profile API
279 |    *
280 |    * @returns The Profile API client
281 |    * @throws {AzureDevOpsAuthenticationError} If authentication fails
282 |    */
283 |   public async getProfileApi(): Promise<IProfileApi> {
284 |     try {
285 |       const client = await this.getClient();
286 |       return await client.getProfileApi();
287 |     } catch (error) {
288 |       // If it's already an AzureDevOpsError, rethrow it
289 |       if (error instanceof AzureDevOpsError) {
290 |         throw error;
291 |       }
292 |       // Otherwise, wrap it in an AzureDevOpsAuthenticationError
293 |       throw new AzureDevOpsAuthenticationError(
294 |         error instanceof Error
295 |           ? `Failed to get Profile API: ${error.message}`
296 |           : 'Failed to get Profile API: Unknown error',
297 |       );
298 |     }
299 |   }
300 | }
301 | 
```

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

```typescript
  1 | import { GitPullRequest } from 'azure-devops-node-api/interfaces/GitInterfaces';
  2 | import { WebApi } from 'azure-devops-node-api';
  3 | import {
  4 |   WorkItemRelation,
  5 |   WorkItemExpand,
  6 | } from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces';
  7 | import { AzureDevOpsClient } from '../../../shared/auth/client-factory';
  8 | import { AzureDevOpsError } from '../../../shared/errors';
  9 | import { UpdatePullRequestOptions } from '../types';
 10 | import { AuthenticationMethod } from '../../../shared/auth/auth-factory';
 11 | import { pullRequestStatusMapper } from '../../../shared/enums';
 12 | 
 13 | /**
 14 |  * Updates an existing pull request in Azure DevOps with the specified changes.
 15 |  *
 16 |  * @param options - The options for updating the pull request
 17 |  * @returns The updated pull request
 18 |  */
 19 | export const updatePullRequest = async (
 20 |   options: UpdatePullRequestOptions,
 21 | ): Promise<GitPullRequest> => {
 22 |   const {
 23 |     projectId,
 24 |     repositoryId,
 25 |     pullRequestId,
 26 |     title,
 27 |     description,
 28 |     status,
 29 |     isDraft,
 30 |     addWorkItemIds,
 31 |     removeWorkItemIds,
 32 |     addReviewers,
 33 |     removeReviewers,
 34 |     additionalProperties,
 35 |   } = options;
 36 | 
 37 |   try {
 38 |     // Get connection to Azure DevOps
 39 |     const client = new AzureDevOpsClient({
 40 |       method:
 41 |         (process.env.AZURE_DEVOPS_AUTH_METHOD as AuthenticationMethod) ?? 'pat',
 42 |       organizationUrl: process.env.AZURE_DEVOPS_ORG_URL ?? '',
 43 |       personalAccessToken: process.env.AZURE_DEVOPS_PAT,
 44 |     });
 45 |     const connection = await client.getWebApiClient();
 46 | 
 47 |     // Get the Git API client
 48 |     const gitApi = await connection.getGitApi();
 49 | 
 50 |     // First, get the current pull request
 51 |     const pullRequest = await gitApi.getPullRequestById(
 52 |       pullRequestId,
 53 |       projectId,
 54 |     );
 55 | 
 56 |     if (!pullRequest) {
 57 |       throw new AzureDevOpsError(
 58 |         `Pull request ${pullRequestId} not found in repository ${repositoryId}`,
 59 |       );
 60 |     }
 61 | 
 62 |     // Store the artifactId for work item linking
 63 |     const artifactId = pullRequest.artifactId;
 64 | 
 65 |     // Create an object with the properties to update
 66 |     const updateObject: Partial<GitPullRequest> = {};
 67 | 
 68 |     if (title !== undefined) {
 69 |       updateObject.title = title;
 70 |     }
 71 | 
 72 |     if (description !== undefined) {
 73 |       updateObject.description = description;
 74 |     }
 75 | 
 76 |     if (isDraft !== undefined) {
 77 |       updateObject.isDraft = isDraft;
 78 |     }
 79 | 
 80 |     if (status) {
 81 |       const enumStatus = pullRequestStatusMapper.toEnum(status);
 82 |       if (enumStatus !== undefined) {
 83 |         updateObject.status = enumStatus;
 84 |       } else {
 85 |         throw new AzureDevOpsError(
 86 |           `Invalid status: ${status}. Valid values are: active, abandoned, completed`,
 87 |         );
 88 |       }
 89 |     }
 90 | 
 91 |     // Add any additional properties that were specified
 92 |     if (additionalProperties) {
 93 |       Object.assign(updateObject, additionalProperties);
 94 |     }
 95 | 
 96 |     // Update the pull request
 97 |     const updatedPullRequest = await gitApi.updatePullRequest(
 98 |       updateObject,
 99 |       repositoryId,
100 |       pullRequestId,
101 |       projectId,
102 |     );
103 | 
104 |     // Handle work items separately if needed
105 |     const addIds = addWorkItemIds ?? [];
106 |     const removeIds = removeWorkItemIds ?? [];
107 |     if (addIds.length > 0 || removeIds.length > 0) {
108 |       await handleWorkItems({
109 |         connection,
110 |         pullRequestId,
111 |         repositoryId,
112 |         projectId,
113 |         workItemIdsToAdd: addIds,
114 |         workItemIdsToRemove: removeIds,
115 |         artifactId,
116 |       });
117 |     }
118 | 
119 |     // Handle reviewers separately if needed
120 |     const addReviewerIds = addReviewers ?? [];
121 |     const removeReviewerIds = removeReviewers ?? [];
122 |     if (addReviewerIds.length > 0 || removeReviewerIds.length > 0) {
123 |       await handleReviewers({
124 |         connection,
125 |         pullRequestId,
126 |         repositoryId,
127 |         projectId,
128 |         reviewersToAdd: addReviewerIds,
129 |         reviewersToRemove: removeReviewerIds,
130 |       });
131 |     }
132 | 
133 |     return updatedPullRequest;
134 |   } catch (error) {
135 |     throw new AzureDevOpsError(
136 |       `Failed to update pull request ${pullRequestId} in repository ${repositoryId}: ${error instanceof Error ? error.message : String(error)}`,
137 |     );
138 |   }
139 | };
140 | 
141 | /**
142 |  * Handle adding or removing work items from a pull request
143 |  */
144 | interface WorkItemHandlingOptions {
145 |   connection: WebApi;
146 |   pullRequestId: number;
147 |   repositoryId: string;
148 |   projectId?: string;
149 |   workItemIdsToAdd: number[];
150 |   workItemIdsToRemove: number[];
151 |   artifactId?: string;
152 | }
153 | 
154 | async function handleWorkItems(
155 |   options: WorkItemHandlingOptions,
156 | ): Promise<void> {
157 |   const {
158 |     connection,
159 |     pullRequestId,
160 |     repositoryId,
161 |     projectId,
162 |     workItemIdsToAdd,
163 |     workItemIdsToRemove,
164 |     artifactId,
165 |   } = options;
166 | 
167 |   try {
168 |     // For each work item to add, create a link
169 |     if (workItemIdsToAdd.length > 0) {
170 |       const workItemTrackingApi = await connection.getWorkItemTrackingApi();
171 | 
172 |       for (const workItemId of workItemIdsToAdd) {
173 |         // Add the relationship between the work item and pull request
174 |         await workItemTrackingApi.updateWorkItem(
175 |           null,
176 |           [
177 |             {
178 |               op: 'add',
179 |               path: '/relations/-',
180 |               value: {
181 |                 rel: 'ArtifactLink',
182 |                 // Use the artifactId if available, otherwise fall back to the old format
183 |                 url:
184 |                   artifactId ||
185 |                   `vstfs:///Git/PullRequestId/${projectId ?? ''}/${repositoryId}/${pullRequestId}`,
186 |                 attributes: {
187 |                   name: 'Pull Request',
188 |                 },
189 |               },
190 |             },
191 |           ],
192 |           workItemId,
193 |         );
194 |       }
195 |     }
196 | 
197 |     // For each work item to remove, remove the link
198 |     if (workItemIdsToRemove.length > 0) {
199 |       const workItemTrackingApi = await connection.getWorkItemTrackingApi();
200 | 
201 |       for (const workItemId of workItemIdsToRemove) {
202 |         try {
203 |           // First, get the work item with relations expanded
204 |           const workItem = await workItemTrackingApi.getWorkItem(
205 |             workItemId,
206 |             undefined, // fields
207 |             undefined, // asOf
208 |             WorkItemExpand.Relations,
209 |           );
210 | 
211 |           if (workItem.relations) {
212 |             // Find the relationship to the pull request using the artifactId
213 |             const prRelationIndex = workItem.relations.findIndex(
214 |               (rel: WorkItemRelation) =>
215 |                 rel.rel === 'ArtifactLink' &&
216 |                 rel.attributes &&
217 |                 rel.attributes.name === 'Pull Request' &&
218 |                 rel.url === artifactId,
219 |             );
220 | 
221 |             if (prRelationIndex !== -1) {
222 |               // Remove the relationship
223 |               await workItemTrackingApi.updateWorkItem(
224 |                 null,
225 |                 [
226 |                   {
227 |                     op: 'remove',
228 |                     path: `/relations/${prRelationIndex}`,
229 |                   },
230 |                 ],
231 |                 workItemId,
232 |               );
233 |             }
234 |           }
235 |         } catch (error) {
236 |           console.log(
237 |             `Error removing work item ${workItemId} from pull request ${pullRequestId}: ${
238 |               error instanceof Error ? error.message : String(error)
239 |             }`,
240 |           );
241 |         }
242 |       }
243 |     }
244 |   } catch (error) {
245 |     throw new AzureDevOpsError(
246 |       `Failed to update work item links for pull request ${pullRequestId}: ${error instanceof Error ? error.message : String(error)}`,
247 |     );
248 |   }
249 | }
250 | 
251 | /**
252 |  * Handle adding or removing reviewers from a pull request
253 |  */
254 | interface ReviewerHandlingOptions {
255 |   connection: WebApi;
256 |   pullRequestId: number;
257 |   repositoryId: string;
258 |   projectId?: string;
259 |   reviewersToAdd: string[];
260 |   reviewersToRemove: string[];
261 | }
262 | 
263 | async function handleReviewers(
264 |   options: ReviewerHandlingOptions,
265 | ): Promise<void> {
266 |   const {
267 |     connection,
268 |     pullRequestId,
269 |     repositoryId,
270 |     projectId,
271 |     reviewersToAdd,
272 |     reviewersToRemove,
273 |   } = options;
274 | 
275 |   try {
276 |     const gitApi = await connection.getGitApi();
277 | 
278 |     // Add reviewers
279 |     if (reviewersToAdd.length > 0) {
280 |       for (const reviewer of reviewersToAdd) {
281 |         try {
282 |           // Create a reviewer object with the identifier
283 |           await gitApi.createPullRequestReviewer(
284 |             {
285 |               id: reviewer, // This can be email or ID
286 |               isRequired: false,
287 |             },
288 |             repositoryId,
289 |             pullRequestId,
290 |             reviewer,
291 |             projectId,
292 |           );
293 |         } catch (error) {
294 |           console.log(
295 |             `Error adding reviewer ${reviewer} to pull request ${pullRequestId}: ${
296 |               error instanceof Error ? error.message : String(error)
297 |             }`,
298 |           );
299 |         }
300 |       }
301 |     }
302 | 
303 |     // Remove reviewers
304 |     if (reviewersToRemove.length > 0) {
305 |       for (const reviewer of reviewersToRemove) {
306 |         try {
307 |           await gitApi.deletePullRequestReviewer(
308 |             repositoryId,
309 |             pullRequestId,
310 |             reviewer,
311 |             projectId,
312 |           );
313 |         } catch (error) {
314 |           console.log(
315 |             `Error removing reviewer ${reviewer} from pull request ${pullRequestId}: ${
316 |               error instanceof Error ? error.message : String(error)
317 |             }`,
318 |           );
319 |         }
320 |       }
321 |     }
322 |   } catch (error) {
323 |     throw new AzureDevOpsError(
324 |       `Failed to update reviewers for pull request ${pullRequestId}: ${error instanceof Error ? error.message : String(error)}`,
325 |     );
326 |   }
327 | }
328 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { getPullRequestComments } from './feature';
  3 | import { listPullRequests } from '../list-pull-requests/feature';
  4 | import { addPullRequestComment } from '../add-pull-request-comment/feature';
  5 | import {
  6 |   getTestConnection,
  7 |   shouldSkipIntegrationTest,
  8 | } from '@/shared/test/test-helpers';
  9 | 
 10 | describe('getPullRequestComments integration', () => {
 11 |   let connection: WebApi | null = null;
 12 |   let projectName: string;
 13 |   let repositoryName: string;
 14 |   let pullRequestId: number;
 15 |   let testThreadId: number;
 16 | 
 17 |   // Generate unique identifiers using timestamp for comment content
 18 |   const timestamp = Date.now();
 19 |   const randomSuffix = Math.floor(Math.random() * 1000);
 20 | 
 21 |   beforeAll(async () => {
 22 |     // Get a real connection using environment variables
 23 |     connection = await getTestConnection();
 24 | 
 25 |     // Set up project and repository names from environment
 26 |     projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
 27 |     repositoryName = process.env.AZURE_DEVOPS_DEFAULT_REPOSITORY || '';
 28 | 
 29 |     // Skip setup if integration tests should be skipped
 30 |     if (shouldSkipIntegrationTest() || !connection) {
 31 |       return;
 32 |     }
 33 | 
 34 |     try {
 35 |       // Find an active pull request to use for testing
 36 |       const pullRequests = await listPullRequests(
 37 |         connection,
 38 |         projectName,
 39 |         repositoryName,
 40 |         {
 41 |           projectId: projectName,
 42 |           repositoryId: repositoryName,
 43 |           status: 'active',
 44 |           top: 1,
 45 |         },
 46 |       );
 47 | 
 48 |       if (!pullRequests || pullRequests.value.length === 0) {
 49 |         throw new Error('No active pull requests found for testing');
 50 |       }
 51 | 
 52 |       pullRequestId = pullRequests.value[0].pullRequestId!;
 53 |       console.log(`Using existing pull request #${pullRequestId} for testing`);
 54 | 
 55 |       // Create a test comment thread that we can use for specific thread tests
 56 |       const result = await addPullRequestComment(
 57 |         connection,
 58 |         projectName,
 59 |         repositoryName,
 60 |         pullRequestId,
 61 |         {
 62 |           projectId: projectName,
 63 |           repositoryId: repositoryName,
 64 |           pullRequestId,
 65 |           content: `Test comment thread ${timestamp}-${randomSuffix}`,
 66 |           status: 'active',
 67 |         },
 68 |       );
 69 | 
 70 |       testThreadId = result.thread!.id!;
 71 |       console.log(`Created test comment thread #${testThreadId} for testing`);
 72 |     } catch (error) {
 73 |       console.error('Error in test setup:', error);
 74 |       throw error;
 75 |     }
 76 |   });
 77 | 
 78 |   test('should get all comment threads from pull request with file path and line number', async () => {
 79 |     // Skip if integration tests should be skipped
 80 |     if (shouldSkipIntegrationTest() || !connection) {
 81 |       console.log('Skipping test due to missing connection');
 82 |       return;
 83 |     }
 84 | 
 85 |     // Skip if repository name is not defined
 86 |     if (!repositoryName) {
 87 |       console.log('Skipping test due to missing repository name');
 88 |       return;
 89 |     }
 90 | 
 91 |     const threads = await getPullRequestComments(
 92 |       connection,
 93 |       projectName,
 94 |       repositoryName,
 95 |       pullRequestId,
 96 |       {
 97 |         projectId: projectName,
 98 |         repositoryId: repositoryName,
 99 |         pullRequestId,
100 |       },
101 |     );
102 | 
103 |     // Verify threads were returned
104 |     expect(threads).toBeDefined();
105 |     expect(Array.isArray(threads)).toBe(true);
106 |     expect(threads.length).toBeGreaterThan(0);
107 | 
108 |     // Verify thread structure
109 |     const firstThread = threads[0];
110 |     expect(firstThread.id).toBeDefined();
111 |     expect(firstThread.comments).toBeDefined();
112 |     expect(Array.isArray(firstThread.comments)).toBe(true);
113 |     expect(firstThread.comments!.length).toBeGreaterThan(0);
114 | 
115 |     // Verify comment structure including new fields
116 |     const firstComment = firstThread.comments![0];
117 |     expect(firstComment.content).toBeDefined();
118 |     expect(firstComment.id).toBeDefined();
119 |     expect(firstComment.publishedDate).toBeDefined();
120 |     expect(firstComment.author).toBeDefined();
121 | 
122 |     // Verify new fields are present (may be undefined/null for general comments)
123 |     expect(firstComment).toHaveProperty('filePath');
124 |     expect(firstComment).toHaveProperty('rightFileStart');
125 |     expect(firstComment).toHaveProperty('rightFileEnd');
126 |     expect(firstComment).toHaveProperty('leftFileStart');
127 |     expect(firstComment).toHaveProperty('leftFileEnd');
128 |   }, 30000);
129 | 
130 |   test('should get a specific comment thread by ID with file path and line number', async () => {
131 |     // Skip if integration tests should be skipped
132 |     if (shouldSkipIntegrationTest() || !connection) {
133 |       console.log('Skipping test due to missing connection');
134 |       return;
135 |     }
136 | 
137 |     // Skip if repository name is not defined
138 |     if (!repositoryName) {
139 |       console.log('Skipping test due to missing repository name');
140 |       return;
141 |     }
142 | 
143 |     const threads = await getPullRequestComments(
144 |       connection,
145 |       projectName,
146 |       repositoryName,
147 |       pullRequestId,
148 |       {
149 |         projectId: projectName,
150 |         repositoryId: repositoryName,
151 |         pullRequestId,
152 |         threadId: testThreadId,
153 |       },
154 |     );
155 | 
156 |     // Verify only one thread was returned
157 |     expect(threads).toBeDefined();
158 |     expect(Array.isArray(threads)).toBe(true);
159 |     expect(threads.length).toBe(1);
160 | 
161 |     // Verify it's the correct thread
162 |     const thread = threads[0];
163 |     expect(thread.id).toBe(testThreadId);
164 |     expect(thread.comments).toBeDefined();
165 |     expect(Array.isArray(thread.comments)).toBe(true);
166 |     expect(thread.comments!.length).toBeGreaterThan(0);
167 | 
168 |     // Verify the comment content matches what we created
169 |     const comment = thread.comments![0];
170 |     expect(comment.content).toBe(
171 |       `Test comment thread ${timestamp}-${randomSuffix}`,
172 |     );
173 | 
174 |     // Verify new fields are present (may be undefined/null for general comments)
175 |     expect(comment).toHaveProperty('filePath');
176 |     expect(comment).toHaveProperty('rightFileStart');
177 |     expect(comment).toHaveProperty('rightFileEnd');
178 |     expect(comment).toHaveProperty('leftFileStart');
179 |     expect(comment).toHaveProperty('leftFileEnd');
180 |   }, 30000);
181 | 
182 |   test('should handle pagination with top parameter', async () => {
183 |     // Skip if integration tests should be skipped
184 |     if (shouldSkipIntegrationTest() || !connection) {
185 |       console.log('Skipping test due to missing connection');
186 |       return;
187 |     }
188 | 
189 |     // Skip if repository name is not defined
190 |     if (!repositoryName) {
191 |       console.log('Skipping test due to missing repository name');
192 |       return;
193 |     }
194 | 
195 |     // Get all threads first to compare
196 |     const allThreads = await getPullRequestComments(
197 |       connection,
198 |       projectName,
199 |       repositoryName,
200 |       pullRequestId,
201 |       {
202 |         projectId: projectName,
203 |         repositoryId: repositoryName,
204 |         pullRequestId,
205 |       },
206 |     );
207 | 
208 |     // Then get with pagination
209 |     const paginatedThreads = await getPullRequestComments(
210 |       connection,
211 |       projectName,
212 |       repositoryName,
213 |       pullRequestId,
214 |       {
215 |         projectId: projectName,
216 |         repositoryId: repositoryName,
217 |         pullRequestId,
218 |         top: 1,
219 |       },
220 |     );
221 | 
222 |     // Verify pagination
223 |     expect(paginatedThreads).toBeDefined();
224 |     expect(Array.isArray(paginatedThreads)).toBe(true);
225 |     expect(paginatedThreads.length).toBe(1);
226 |     expect(paginatedThreads.length).toBeLessThanOrEqual(allThreads.length);
227 | 
228 |     // Verify the thread structure is the same
229 |     const thread = paginatedThreads[0];
230 |     expect(thread.id).toBeDefined();
231 |     expect(thread.comments).toBeDefined();
232 |     expect(Array.isArray(thread.comments)).toBe(true);
233 |     expect(thread.comments!.length).toBeGreaterThan(0);
234 | 
235 |     // Verify new fields are present in paginated results
236 |     const comment = thread.comments![0];
237 |     expect(comment).toHaveProperty('filePath');
238 |     expect(comment).toHaveProperty('rightFileStart');
239 |     expect(comment).toHaveProperty('rightFileEnd');
240 |     expect(comment).toHaveProperty('leftFileStart');
241 |     expect(comment).toHaveProperty('leftFileEnd');
242 |   }, 30000);
243 | 
244 |   test('should handle includeDeleted parameter', async () => {
245 |     // Skip if integration tests should be skipped
246 |     if (shouldSkipIntegrationTest() || !connection) {
247 |       console.log('Skipping test due to missing connection');
248 |       return;
249 |     }
250 | 
251 |     // Skip if repository name is not defined
252 |     if (!repositoryName) {
253 |       console.log('Skipping test due to missing repository name');
254 |       return;
255 |     }
256 | 
257 |     const threads = await getPullRequestComments(
258 |       connection,
259 |       projectName,
260 |       repositoryName,
261 |       pullRequestId,
262 |       {
263 |         projectId: projectName,
264 |         repositoryId: repositoryName,
265 |         pullRequestId,
266 |         includeDeleted: true,
267 |       },
268 |     );
269 | 
270 |     // We can only verify the call succeeds, as we can't guarantee deleted comments exist
271 |     expect(threads).toBeDefined();
272 |     expect(Array.isArray(threads)).toBe(true);
273 | 
274 |     // If there are any threads, verify they have the new fields
275 |     if (threads.length > 0) {
276 |       const thread = threads[0];
277 |       if (thread.comments && thread.comments.length > 0) {
278 |         const comment = thread.comments[0];
279 |         expect(comment).toHaveProperty('filePath');
280 |         expect(comment).toHaveProperty('rightFileStart');
281 |         expect(comment).toHaveProperty('rightFileEnd');
282 |         expect(comment).toHaveProperty('leftFileStart');
283 |         expect(comment).toHaveProperty('leftFileEnd');
284 |       }
285 |     }
286 |   }, 30000); // 30 second timeout for integration test
287 | });
288 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import {
  3 |   AzureDevOpsResourceNotFoundError,
  4 |   AzureDevOpsError,
  5 | } from '../../../shared/errors';
  6 | import {
  7 |   TeamProject,
  8 |   WebApiTeam,
  9 | } from 'azure-devops-node-api/interfaces/CoreInterfaces';
 10 | import { WorkItemField } from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces';
 11 | 
 12 | // Type for work item type field with additional properties
 13 | interface WorkItemTypeField extends WorkItemField {
 14 |   isRequired?: boolean;
 15 |   isIdentity?: boolean;
 16 |   isPicklist?: boolean;
 17 | }
 18 | 
 19 | /**
 20 |  * Options for getting project details
 21 |  */
 22 | export interface GetProjectDetailsOptions {
 23 |   projectId: string;
 24 |   includeProcess?: boolean;
 25 |   includeWorkItemTypes?: boolean;
 26 |   includeFields?: boolean;
 27 |   includeTeams?: boolean;
 28 |   expandTeamIdentity?: boolean;
 29 | }
 30 | 
 31 | /**
 32 |  * Process information with work item types
 33 |  */
 34 | interface ProcessInfo {
 35 |   id: string;
 36 |   name: string;
 37 |   description?: string;
 38 |   isDefault: boolean;
 39 |   type: string;
 40 |   workItemTypes?: WorkItemTypeInfo[];
 41 |   hierarchyInfo?: {
 42 |     portfolioBacklogs?: {
 43 |       name: string;
 44 |       workItemTypes: string[];
 45 |     }[];
 46 |     requirementBacklog?: {
 47 |       name: string;
 48 |       workItemTypes: string[];
 49 |     };
 50 |     taskBacklog?: {
 51 |       name: string;
 52 |       workItemTypes: string[];
 53 |     };
 54 |   };
 55 | }
 56 | 
 57 | /**
 58 |  * Work item type information with states and fields
 59 |  */
 60 | interface WorkItemTypeInfo {
 61 |   name: string;
 62 |   referenceName: string;
 63 |   description?: string;
 64 |   isDisabled: boolean;
 65 |   states?: {
 66 |     name: string;
 67 |     color?: string;
 68 |     stateCategory: string;
 69 |   }[];
 70 |   fields?: {
 71 |     name: string;
 72 |     referenceName: string;
 73 |     type: string;
 74 |     required?: boolean;
 75 |     isIdentity?: boolean;
 76 |     isPicklist?: boolean;
 77 |     description?: string;
 78 |   }[];
 79 | }
 80 | 
 81 | /**
 82 |  * Project details response
 83 |  */
 84 | interface ProjectDetails extends TeamProject {
 85 |   process?: ProcessInfo;
 86 |   teams?: WebApiTeam[];
 87 | }
 88 | 
 89 | /**
 90 |  * Get detailed information about a project
 91 |  *
 92 |  * @param connection The Azure DevOps WebApi connection
 93 |  * @param options Options for getting project details
 94 |  * @returns The project details
 95 |  * @throws {AzureDevOpsResourceNotFoundError} If the project is not found
 96 |  */
 97 | export async function getProjectDetails(
 98 |   connection: WebApi,
 99 |   options: GetProjectDetailsOptions,
100 | ): Promise<ProjectDetails> {
101 |   try {
102 |     const {
103 |       projectId,
104 |       includeProcess = false,
105 |       includeWorkItemTypes = false,
106 |       includeFields = false,
107 |       includeTeams = false,
108 |       expandTeamIdentity = false,
109 |     } = options;
110 | 
111 |     // Get the core API
112 |     const coreApi = await connection.getCoreApi();
113 | 
114 |     // Get the basic project information
115 |     const project = await coreApi.getProject(projectId);
116 | 
117 |     if (!project) {
118 |       throw new AzureDevOpsResourceNotFoundError(
119 |         `Project '${projectId}' not found`,
120 |       );
121 |     }
122 | 
123 |     // Initialize the result with the project information and ensure required properties
124 |     const result: ProjectDetails = {
125 |       ...project,
126 |       // Ensure capabilities is always defined
127 |       capabilities: project.capabilities || {
128 |         versioncontrol: { sourceControlType: 'Git' },
129 |         processTemplate: { templateName: 'Unknown', templateTypeId: 'unknown' },
130 |       },
131 |     };
132 | 
133 |     // If teams are requested, get them
134 |     if (includeTeams) {
135 |       const teams = await coreApi.getTeams(projectId, expandTeamIdentity);
136 |       result.teams = teams;
137 |     }
138 | 
139 |     // If process information is requested, get it
140 |     if (includeProcess) {
141 |       // Get the process template ID from the project capabilities
142 |       const processTemplateId =
143 |         project.capabilities?.processTemplate?.templateTypeId || 'unknown';
144 | 
145 |       // Always create a process object, even if we don't have a template ID
146 |       // In a real implementation, we would use the Process API
147 |       // Since it's not directly available in the WebApi type, we'll simulate it
148 |       // This is a simplified version for the implementation
149 |       // In a real implementation, you would need to use the appropriate API
150 | 
151 |       // Create the process info object directly
152 |       const processInfo: ProcessInfo = {
153 |         id: processTemplateId,
154 |         name: project.capabilities?.processTemplate?.templateName || 'Unknown',
155 |         description: 'Process template for the project',
156 |         isDefault: true,
157 |         type: 'system',
158 |       };
159 | 
160 |       // If work item types are requested, get them
161 |       if (includeWorkItemTypes) {
162 |         // In a real implementation, we would get work item types from the API
163 |         // For now, we'll use the work item tracking API to get basic types
164 |         const workItemTrackingApi = await connection.getWorkItemTrackingApi();
165 |         const workItemTypes =
166 |           await workItemTrackingApi.getWorkItemTypes(projectId);
167 | 
168 |         // Map the work item types to our format
169 |         const processWorkItemTypes: WorkItemTypeInfo[] = workItemTypes.map(
170 |           (wit) => {
171 |             // Create the work item type info object
172 |             const workItemTypeInfo: WorkItemTypeInfo = {
173 |               name: wit.name || 'Unknown',
174 |               referenceName:
175 |                 wit.referenceName || `System.Unknown.${Date.now()}`,
176 |               description: wit.description,
177 |               isDisabled: false,
178 |               states: [
179 |                 { name: 'New', stateCategory: 'Proposed' },
180 |                 { name: 'Active', stateCategory: 'InProgress' },
181 |                 { name: 'Resolved', stateCategory: 'InProgress' },
182 |                 { name: 'Closed', stateCategory: 'Completed' },
183 |               ],
184 |             };
185 | 
186 |             // If fields are requested, don't add fields here - we'll add them after fetching from API
187 |             return workItemTypeInfo;
188 |           },
189 |         );
190 | 
191 |         // If fields are requested, get the field definitions from the API
192 |         if (includeFields) {
193 |           try {
194 |             // Instead of getting all fields and applying them to all work item types,
195 |             // let's get the fields specific to each work item type
196 |             for (const wit of processWorkItemTypes) {
197 |               try {
198 |                 // Get fields specific to this work item type using the specialized method
199 |                 const typeSpecificFields =
200 |                   await workItemTrackingApi.getWorkItemTypeFieldsWithReferences(
201 |                     projectId,
202 |                     wit.name,
203 |                   );
204 | 
205 |                 // Map the fields to our format
206 |                 wit.fields = typeSpecificFields.map(
207 |                   (field: WorkItemTypeField) => ({
208 |                     name: field.name || 'Unknown',
209 |                     referenceName: field.referenceName || 'Unknown',
210 |                     type: field.type?.toString().toLowerCase() || 'string',
211 |                     required: field.isRequired || false,
212 |                     isIdentity: field.isIdentity || false,
213 |                     isPicklist: field.isPicklist || false,
214 |                     description: field.description,
215 |                   }),
216 |                 );
217 |               } catch (typeFieldError) {
218 |                 console.error(
219 |                   `Error fetching fields for work item type ${wit.name}:`,
220 |                   typeFieldError,
221 |                 );
222 | 
223 |                 // Fallback to basic fields
224 |                 wit.fields = [
225 |                   {
226 |                     name: 'Title',
227 |                     referenceName: 'System.Title',
228 |                     type: 'string',
229 |                     required: true,
230 |                   },
231 |                   {
232 |                     name: 'Description',
233 |                     referenceName: 'System.Description',
234 |                     type: 'html',
235 |                     required: false,
236 |                   },
237 |                 ];
238 |               }
239 |             }
240 |           } catch (fieldError) {
241 |             console.error('Error in field processing:', fieldError);
242 | 
243 |             // Fallback to default fields if API call fails
244 |             processWorkItemTypes.forEach((wit) => {
245 |               wit.fields = [
246 |                 {
247 |                   name: 'Title',
248 |                   referenceName: 'System.Title',
249 |                   type: 'string',
250 |                   required: true,
251 |                 },
252 |                 {
253 |                   name: 'Description',
254 |                   referenceName: 'System.Description',
255 |                   type: 'html',
256 |                   required: false,
257 |                 },
258 |               ];
259 |             });
260 |           }
261 |         }
262 | 
263 |         processInfo.workItemTypes = processWorkItemTypes;
264 | 
265 |         // Add hierarchy information if available
266 |         // This is a simplified version - in a real implementation, you would
267 |         // need to get the backlog configuration and map it to the work item types
268 |         processInfo.hierarchyInfo = {
269 |           portfolioBacklogs: [
270 |             {
271 |               name: 'Epics',
272 |               workItemTypes: processWorkItemTypes
273 |                 .filter(
274 |                   (wit: WorkItemTypeInfo) => wit.name.toLowerCase() === 'epic',
275 |                 )
276 |                 .map((wit: WorkItemTypeInfo) => wit.name),
277 |             },
278 |             {
279 |               name: 'Features',
280 |               workItemTypes: processWorkItemTypes
281 |                 .filter(
282 |                   (wit: WorkItemTypeInfo) =>
283 |                     wit.name.toLowerCase() === 'feature',
284 |                 )
285 |                 .map((wit: WorkItemTypeInfo) => wit.name),
286 |             },
287 |           ],
288 |           requirementBacklog: {
289 |             name: 'Stories',
290 |             workItemTypes: processWorkItemTypes
291 |               .filter(
292 |                 (wit: WorkItemTypeInfo) =>
293 |                   wit.name.toLowerCase() === 'user story' ||
294 |                   wit.name.toLowerCase() === 'bug',
295 |               )
296 |               .map((wit: WorkItemTypeInfo) => wit.name),
297 |           },
298 |           taskBacklog: {
299 |             name: 'Tasks',
300 |             workItemTypes: processWorkItemTypes
301 |               .filter(
302 |                 (wit: WorkItemTypeInfo) => wit.name.toLowerCase() === 'task',
303 |               )
304 |               .map((wit: WorkItemTypeInfo) => wit.name),
305 |           },
306 |         };
307 |       }
308 | 
309 |       // Always set the process on the result
310 |       result.process = processInfo;
311 |     }
312 | 
313 |     return result;
314 |   } catch (error) {
315 |     if (error instanceof AzureDevOpsError) {
316 |       throw error;
317 |     }
318 |     throw new Error(
319 |       `Failed to get project details: ${error instanceof Error ? error.message : String(error)}`,
320 |     );
321 |   }
322 | }
323 | 
```

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

```markdown
  1 | # Authentication Guide for Azure DevOps MCP Server
  2 | 
  3 | This guide provides detailed information about the authentication methods supported by the Azure DevOps MCP Server, including setup instructions, configuration examples, and troubleshooting tips.
  4 | 
  5 | ## Supported Authentication Methods
  6 | 
  7 | The Azure DevOps MCP Server supports three authentication methods:
  8 | 
  9 | 1. **Personal Access Token (PAT)** - Simple token-based authentication
 10 | 2. **Azure Identity (DefaultAzureCredential)** - Flexible authentication using the Azure Identity SDK
 11 | 3. **Azure CLI** - Authentication using your Azure CLI login
 12 | 
 13 | ## Method 1: Personal Access Token (PAT) Authentication
 14 | 
 15 | PAT authentication is the simplest method and works well for personal use or testing.
 16 | 
 17 | ### Setup Instructions
 18 | 
 19 | 1. **Generate a PAT in Azure DevOps**:
 20 | 
 21 |    - Go to https://dev.azure.com/{your-organization}/_usersSettings/tokens
 22 |    - Or click on your profile picture > Personal access tokens
 23 |    - Select "+ New Token"
 24 |    - Name your token (e.g., "MCP Server Access")
 25 |    - Set an expiration date
 26 |    - Select the following scopes:
 27 |      - **Code**: Read & Write
 28 |      - **Work Items**: Read & Write
 29 |      - **Build**: Read & Execute
 30 |      - **Project and Team**: Read
 31 |      - **Graph**: Read
 32 |      - **Release**: Read & Execute
 33 |    - Click "Create" and copy the generated token
 34 | 
 35 | 2. **Configure your `.env` file**:
 36 |    ```
 37 |    AZURE_DEVOPS_AUTH_METHOD=pat
 38 |    AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
 39 |    AZURE_DEVOPS_PAT=your-personal-access-token
 40 |    AZURE_DEVOPS_DEFAULT_PROJECT=your-default-project
 41 |    ```
 42 | 
 43 | ### Security Considerations
 44 | 
 45 | - PATs have an expiration date and will need to be renewed
 46 | - Store your PAT securely and never commit it to source control
 47 | - Consider using environment variables or a secrets manager in production
 48 | - Scope your PAT to only the permissions needed for your use case
 49 | 
 50 | ## Method 2: Azure Identity Authentication (DefaultAzureCredential)
 51 | 
 52 | Azure Identity authentication uses the `DefaultAzureCredential` class from the `@azure/identity` package, which provides a simplified authentication experience by trying multiple credential types in sequence.
 53 | 
 54 | ### How DefaultAzureCredential Works
 55 | 
 56 | `DefaultAzureCredential` tries the following credential types in order:
 57 | 
 58 | 1. Environment variables (EnvironmentCredential)
 59 | 2. Managed Identity (ManagedIdentityCredential)
 60 | 3. Azure CLI (AzureCliCredential)
 61 | 4. Visual Studio Code (VisualStudioCodeCredential)
 62 | 5. Azure PowerShell (AzurePowerShellCredential)
 63 | 6. Interactive Browser (InteractiveBrowserCredential) - optional, disabled by default
 64 | 
 65 | This makes it ideal for applications that need to work in different environments (local development, Azure-hosted) without code changes.
 66 | 
 67 | ### Setup Instructions
 68 | 
 69 | 1. **Install the Azure Identity SDK**:
 70 |    The SDK is already included as a dependency in the Azure DevOps MCP Server.
 71 | 
 72 | 2. **Configure your `.env` file**:
 73 | 
 74 |    ```
 75 |    AZURE_DEVOPS_AUTH_METHOD=azure-identity
 76 |    AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
 77 |    AZURE_DEVOPS_DEFAULT_PROJECT=your-default-project
 78 |    ```
 79 | 
 80 | 3. **Set up credentials based on your environment**:
 81 | 
 82 |    a. **For service principals (client credentials)**:
 83 | 
 84 |    ```
 85 |    AZURE_TENANT_ID=your-tenant-id
 86 |    AZURE_CLIENT_ID=your-client-id
 87 |    AZURE_CLIENT_SECRET=your-client-secret
 88 |    ```
 89 | 
 90 |    b. **For managed identities in Azure**:
 91 |    No additional configuration needed if running in Azure with a managed identity.
 92 | 
 93 |    c. **For local development**:
 94 | 
 95 |    - Log in with Azure CLI: `az login`
 96 |    - Or use Visual Studio Code Azure Account extension
 97 | 
 98 | ### Security Considerations
 99 | 
100 | - Use managed identities in Azure for improved security
101 | - For service principals, rotate client secrets regularly
102 | - Store credentials securely using Azure Key Vault or environment variables
103 | - Apply the principle of least privilege when assigning roles
104 | 
105 | ## Method 3: Azure CLI Authentication
106 | 
107 | Azure CLI authentication uses the `AzureCliCredential` class from the `@azure/identity` package, which authenticates using the Azure CLI's logged-in account.
108 | 
109 | ### Setup Instructions
110 | 
111 | 1. **Install the Azure CLI**:
112 | 
113 |    - Follow the instructions at https://docs.microsoft.com/cli/azure/install-azure-cli
114 | 
115 | 2. **Log in to Azure**:
116 | 
117 |    ```bash
118 |    az login
119 |    ```
120 | 
121 | 3. **Configure your `.env` file**:
122 |    ```
123 |    AZURE_DEVOPS_AUTH_METHOD=azure-cli
124 |    AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
125 |    AZURE_DEVOPS_DEFAULT_PROJECT=your-default-project
126 |    ```
127 | 
128 | ### Security Considerations
129 | 
130 | - Azure CLI authentication is best for local development
131 | - Ensure your Azure CLI session is kept secure
132 | - Log out when not in use: `az logout`
133 | 
134 | ## Configuration Reference
135 | 
136 | | Variable                       | Description                                                                        | Required                     | Default          |
137 | | ------------------------------ | ---------------------------------------------------------------------------------- | ---------------------------- | ---------------- |
138 | | `AZURE_DEVOPS_AUTH_METHOD`     | Authentication method (`pat`, `azure-identity`, or `azure-cli`) - case-insensitive | No                           | `azure-identity` |
139 | | `AZURE_DEVOPS_ORG_URL`         | Full URL to your Azure DevOps organization                                         | Yes                          | -                |
140 | | `AZURE_DEVOPS_PAT`             | Personal Access Token (for PAT auth)                                               | Only with PAT auth           | -                |
141 | | `AZURE_DEVOPS_DEFAULT_PROJECT` | Default project if none specified                                                  | No                           | -                |
142 | | `AZURE_DEVOPS_API_VERSION`     | API version to use                                                                 | No                           | Latest           |
143 | | `AZURE_TENANT_ID`              | Azure AD tenant ID (for service principals)                                        | Only with service principals | -                |
144 | | `AZURE_CLIENT_ID`              | Azure AD application ID (for service principals)                                   | Only with service principals | -                |
145 | | `AZURE_CLIENT_SECRET`          | Azure AD client secret (for service principals)                                    | Only with service principals | -                |
146 | | `LOG_LEVEL`                    | Logging level (debug, info, warn, error)                                           | No                           | info             |
147 | 
148 | ## Troubleshooting Authentication Issues
149 | 
150 | ### PAT Authentication Issues
151 | 
152 | 1. **Invalid PAT**: Ensure your PAT hasn't expired and has the required scopes
153 | 
154 |    - Error: `TF400813: The user 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' is not authorized to access this resource.`
155 |    - Solution: Generate a new PAT with the correct scopes
156 | 
157 | 2. **Scope issues**: If receiving 403 errors, check if your PAT has the necessary permissions
158 | 
159 |    - Error: `TF401027: You need the Git 'Read' permission to perform this action.`
160 |    - Solution: Update your PAT with the required scopes
161 | 
162 | 3. **Organization access**: Verify your PAT has access to the organization specified in the URL
163 |    - Error: `TF400813: Resource not found for anonymous request.`
164 |    - Solution: Ensure your PAT has access to the specified organization
165 | 
166 | ### Azure Identity Authentication Issues
167 | 
168 | 1. **Missing credentials**: Ensure you have the necessary credentials configured
169 | 
170 |    - Error: `CredentialUnavailableError: DefaultAzureCredential failed to retrieve a token`
171 |    - Solution: Check that you're logged in with Azure CLI or have environment variables set
172 | 
173 | 2. **Permission issues**: Verify your identity has the necessary permissions
174 | 
175 |    - Error: `AuthorizationFailed: The client does not have authorization to perform action`
176 |    - Solution: Assign the appropriate roles to your identity
177 | 
178 | 3. **Token acquisition errors**: Check network connectivity and Azure AD endpoint availability
179 |    - Error: `ClientAuthError: Interaction required`
180 |    - Solution: Check network connectivity or use a different credential type
181 | 
182 | ### Azure CLI Authentication Issues
183 | 
184 | 1. **CLI not installed**: Ensure Azure CLI is installed and in your PATH
185 | 
186 |    - Error: `AzureCliCredential authentication failed: Azure CLI not found`
187 |    - Solution: Install Azure CLI
188 | 
189 | 2. **Not logged in**: Verify you're logged in to Azure CLI
190 | 
191 |    - Error: `AzureCliCredential authentication failed: Please run 'az login'`
192 |    - Solution: Run `az login`
193 | 
194 | 3. **Permission issues**: Check if your Azure CLI account has access to Azure DevOps
195 |    - Error: `TF400813: The user is not authorized to access this resource`
196 |    - Solution: Log in with an account that has access to Azure DevOps
197 | 
198 | ## Best Practices
199 | 
200 | 1. **Choose the right authentication method for your environment**:
201 | 
202 |    - For local development: Azure CLI or PAT
203 |    - For CI/CD pipelines: PAT or service principal
204 |    - For Azure-hosted applications: Managed Identity
205 | 
206 | 2. **Follow the principle of least privilege**:
207 | 
208 |    - Only grant the permissions needed for your use case
209 |    - Regularly review and rotate credentials
210 | 
211 | 3. **Secure your credentials**:
212 | 
213 |    - Use environment variables or a secrets manager
214 |    - Never commit credentials to source control
215 |    - Set appropriate expiration dates for PATs
216 | 
217 | 4. **Monitor and audit authentication**:
218 |    - Review Azure DevOps access logs
219 |    - Set up alerts for suspicious activity
220 | 
221 | ## Examples
222 | 
223 | ### Example 1: Local Development with PAT
224 | 
225 | ```bash
226 | # .env file
227 | AZURE_DEVOPS_AUTH_METHOD=pat
228 | AZURE_DEVOPS_ORG_URL=https://dev.azure.com/mycompany
229 | AZURE_DEVOPS_PAT=abcdefghijklmnopqrstuvwxyz0123456789
230 | AZURE_DEVOPS_DEFAULT_PROJECT=MyProject
231 | ```
232 | 
233 | ### Example 2: Azure-hosted Application with Managed Identity
234 | 
235 | ```bash
236 | # .env file
237 | AZURE_DEVOPS_AUTH_METHOD=azure-identity
238 | AZURE_DEVOPS_ORG_URL=https://dev.azure.com/mycompany
239 | AZURE_DEVOPS_DEFAULT_PROJECT=MyProject
240 | ```
241 | 
242 | ### Example 3: CI/CD Pipeline with Service Principal
243 | 
244 | ```bash
245 | # .env file
246 | AZURE_DEVOPS_AUTH_METHOD=azure-identity
247 | AZURE_DEVOPS_ORG_URL=https://dev.azure.com/mycompany
248 | AZURE_DEVOPS_DEFAULT_PROJECT=MyProject
249 | AZURE_TENANT_ID=00000000-0000-0000-0000-000000000000
250 | AZURE_CLIENT_ID=11111111-1111-1111-1111-111111111111
251 | AZURE_CLIENT_SECRET=your-client-secret
252 | ```
253 | 
254 | ### Example 4: Local Development with Azure CLI
255 | 
256 | ```bash
257 | # .env file
258 | AZURE_DEVOPS_AUTH_METHOD=azure-cli
259 | AZURE_DEVOPS_ORG_URL=https://dev.azure.com/mycompany
260 | AZURE_DEVOPS_DEFAULT_PROJECT=MyProject
261 | ```
262 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import axios from 'axios';
  3 | import { searchWorkItems } from './feature';
  4 | import {
  5 |   AzureDevOpsError,
  6 |   AzureDevOpsResourceNotFoundError,
  7 |   AzureDevOpsValidationError,
  8 |   AzureDevOpsPermissionError,
  9 | } from '../../../shared/errors';
 10 | import { SearchWorkItemsOptions, WorkItemSearchResponse } from '../types';
 11 | 
 12 | // Mock axios
 13 | jest.mock('axios');
 14 | const mockedAxios = axios as jest.Mocked<typeof axios>;
 15 | 
 16 | // Mock @azure/identity
 17 | jest.mock('@azure/identity', () => ({
 18 |   DefaultAzureCredential: jest.fn().mockImplementation(() => ({
 19 |     getToken: jest
 20 |       .fn()
 21 |       .mockResolvedValue({ token: 'mock-azure-identity-token' }),
 22 |   })),
 23 |   AzureCliCredential: jest.fn(),
 24 | }));
 25 | 
 26 | // Mock WebApi
 27 | jest.mock('azure-devops-node-api');
 28 | const MockedWebApi = WebApi as jest.MockedClass<typeof WebApi>;
 29 | 
 30 | describe('searchWorkItems', () => {
 31 |   let connection: WebApi;
 32 |   let options: SearchWorkItemsOptions;
 33 |   let mockResponse: WorkItemSearchResponse;
 34 | 
 35 |   beforeEach(() => {
 36 |     // Reset mocks
 37 |     jest.clearAllMocks();
 38 | 
 39 |     // Mock environment variables
 40 |     process.env.AZURE_DEVOPS_AUTH_METHOD = 'pat';
 41 |     process.env.AZURE_DEVOPS_PAT = 'mock-pat';
 42 | 
 43 |     // Set up connection mock
 44 |     // Create a mock auth handler that implements IRequestHandler
 45 |     const mockAuthHandler = {
 46 |       prepareRequest: jest.fn(),
 47 |       canHandleAuthentication: jest.fn().mockReturnValue(true),
 48 |       handleAuthentication: jest.fn(),
 49 |     };
 50 |     connection = new MockedWebApi(
 51 |       'https://dev.azure.com/mock-org',
 52 |       mockAuthHandler,
 53 |     );
 54 |     (connection as any).serverUrl = 'https://dev.azure.com/mock-org';
 55 |     (connection.getCoreApi as jest.Mock).mockResolvedValue({
 56 |       getProjects: jest.fn().mockResolvedValue([]),
 57 |     });
 58 | 
 59 |     // Set up options
 60 |     options = {
 61 |       searchText: 'test query',
 62 |       projectId: 'mock-project',
 63 |       top: 50,
 64 |       skip: 0,
 65 |       includeFacets: true,
 66 |     };
 67 | 
 68 |     // Set up mock response
 69 |     mockResponse = {
 70 |       count: 2,
 71 |       results: [
 72 |         {
 73 |           project: {
 74 |             id: 'project-id-1',
 75 |             name: 'mock-project',
 76 |           },
 77 |           fields: {
 78 |             'system.id': '42',
 79 |             'system.workitemtype': 'Bug',
 80 |             'system.title': 'Test Bug',
 81 |             'system.state': 'Active',
 82 |             'system.assignedto': 'Test User',
 83 |           },
 84 |           hits: [
 85 |             {
 86 |               fieldReferenceName: 'system.title',
 87 |               highlights: ['Test <b>Bug</b>'],
 88 |             },
 89 |           ],
 90 |           url: 'https://dev.azure.com/mock-org/mock-project/_workitems/edit/42',
 91 |         },
 92 |         {
 93 |           project: {
 94 |             id: 'project-id-1',
 95 |             name: 'mock-project',
 96 |           },
 97 |           fields: {
 98 |             'system.id': '43',
 99 |             'system.workitemtype': 'Task',
100 |             'system.title': 'Test Task',
101 |             'system.state': 'New',
102 |             'system.assignedto': 'Test User',
103 |           },
104 |           hits: [
105 |             {
106 |               fieldReferenceName: 'system.title',
107 |               highlights: ['Test <b>Task</b>'],
108 |             },
109 |           ],
110 |           url: 'https://dev.azure.com/mock-org/mock-project/_workitems/edit/43',
111 |         },
112 |       ],
113 |       facets: {
114 |         'System.WorkItemType': [
115 |           {
116 |             name: 'Bug',
117 |             id: 'Bug',
118 |             resultCount: 1,
119 |           },
120 |           {
121 |             name: 'Task',
122 |             id: 'Task',
123 |             resultCount: 1,
124 |           },
125 |         ],
126 |       },
127 |     };
128 | 
129 |     // Mock axios response
130 |     mockedAxios.post.mockResolvedValue({ data: mockResponse });
131 |   });
132 | 
133 |   afterEach(() => {
134 |     // Clean up environment variables
135 |     delete process.env.AZURE_DEVOPS_AUTH_METHOD;
136 |     delete process.env.AZURE_DEVOPS_PAT;
137 |   });
138 | 
139 |   it('should search work items with the correct parameters', async () => {
140 |     // Act
141 |     const result = await searchWorkItems(connection, options);
142 | 
143 |     // Assert
144 |     expect(mockedAxios.post).toHaveBeenCalledWith(
145 |       'https://almsearch.dev.azure.com/mock-org/mock-project/_apis/search/workitemsearchresults?api-version=7.1',
146 |       {
147 |         searchText: 'test query',
148 |         $skip: 0,
149 |         $top: 50,
150 |         filters: {
151 |           'System.TeamProject': ['mock-project'],
152 |         },
153 |         includeFacets: true,
154 |       },
155 |       expect.objectContaining({
156 |         headers: expect.objectContaining({
157 |           Authorization: expect.stringContaining('Basic'),
158 |           'Content-Type': 'application/json',
159 |         }),
160 |       }),
161 |     );
162 |     expect(result).toEqual(mockResponse);
163 |   });
164 | 
165 |   it('should include filters when provided', async () => {
166 |     // Arrange
167 |     options.filters = {
168 |       'System.WorkItemType': ['Bug', 'Task'],
169 |       'System.State': ['Active'],
170 |     };
171 | 
172 |     // Act
173 |     await searchWorkItems(connection, options);
174 | 
175 |     // Assert
176 |     expect(mockedAxios.post).toHaveBeenCalledWith(
177 |       expect.any(String),
178 |       expect.objectContaining({
179 |         filters: {
180 |           'System.TeamProject': ['mock-project'],
181 |           'System.WorkItemType': ['Bug', 'Task'],
182 |           'System.State': ['Active'],
183 |         },
184 |       }),
185 |       expect.any(Object),
186 |     );
187 |   });
188 | 
189 |   it('should include orderBy when provided', async () => {
190 |     // Arrange
191 |     options.orderBy = [{ field: 'System.CreatedDate', sortOrder: 'ASC' }];
192 | 
193 |     // Act
194 |     await searchWorkItems(connection, options);
195 | 
196 |     // Assert
197 |     expect(mockedAxios.post).toHaveBeenCalledWith(
198 |       expect.any(String),
199 |       expect.objectContaining({
200 |         $orderBy: [{ field: 'System.CreatedDate', sortOrder: 'ASC' }],
201 |       }),
202 |       expect.any(Object),
203 |     );
204 |   });
205 | 
206 |   it('should handle 404 errors correctly', async () => {
207 |     // Arrange - Mock the implementation to throw the specific error
208 |     mockedAxios.post.mockImplementation(() => {
209 |       throw new AzureDevOpsResourceNotFoundError(
210 |         'Resource not found: Project not found',
211 |       );
212 |     });
213 | 
214 |     // Act & Assert
215 |     await expect(searchWorkItems(connection, options)).rejects.toThrow(
216 |       AzureDevOpsResourceNotFoundError,
217 |     );
218 |   });
219 | 
220 |   it('should handle 400 errors correctly', async () => {
221 |     // Arrange - Mock the implementation to throw the specific error
222 |     mockedAxios.post.mockImplementation(() => {
223 |       throw new AzureDevOpsValidationError('Invalid request: Invalid query');
224 |     });
225 | 
226 |     // Act & Assert
227 |     await expect(searchWorkItems(connection, options)).rejects.toThrow(
228 |       AzureDevOpsValidationError,
229 |     );
230 |   });
231 | 
232 |   it('should handle 401/403 errors correctly', async () => {
233 |     // Arrange - Mock the implementation to throw the specific error
234 |     mockedAxios.post.mockImplementation(() => {
235 |       throw new AzureDevOpsPermissionError(
236 |         'Permission denied: Permission denied',
237 |       );
238 |     });
239 | 
240 |     // Act & Assert
241 |     await expect(searchWorkItems(connection, options)).rejects.toThrow(
242 |       AzureDevOpsPermissionError,
243 |     );
244 |   });
245 | 
246 |   it('should handle other axios errors correctly', async () => {
247 |     // Arrange - Mock the implementation to throw the specific error
248 |     mockedAxios.post.mockImplementation(() => {
249 |       throw new AzureDevOpsError(
250 |         'Azure DevOps API error: Internal server error',
251 |       );
252 |     });
253 | 
254 |     // Act & Assert
255 |     await expect(searchWorkItems(connection, options)).rejects.toThrow(
256 |       AzureDevOpsError,
257 |     );
258 |   });
259 | 
260 |   it('should handle non-axios errors correctly', async () => {
261 |     // Arrange
262 |     mockedAxios.post.mockRejectedValue(new Error('Network error'));
263 | 
264 |     // Act & Assert
265 |     await expect(searchWorkItems(connection, options)).rejects.toThrow(
266 |       AzureDevOpsError,
267 |     );
268 |   });
269 | 
270 |   it('should throw an error if organization cannot be extracted', async () => {
271 |     // Arrange
272 |     (connection as any).serverUrl = 'https://invalid-url';
273 | 
274 |     // Act & Assert
275 |     await expect(searchWorkItems(connection, options)).rejects.toThrow(
276 |       AzureDevOpsValidationError,
277 |     );
278 |   });
279 | 
280 |   it('should use Azure Identity authentication when AZURE_DEVOPS_AUTH_METHOD is azure-identity', async () => {
281 |     // Mock environment variables
282 |     const originalEnv = process.env.AZURE_DEVOPS_AUTH_METHOD;
283 |     process.env.AZURE_DEVOPS_AUTH_METHOD = 'azure-identity';
284 | 
285 |     // Mock the WebApi connection
286 |     const mockConnection = {
287 |       serverUrl: 'https://dev.azure.com/testorg',
288 |       getCoreApi: jest.fn().mockResolvedValue({
289 |         getProjects: jest.fn().mockResolvedValue([]),
290 |       }),
291 |     };
292 | 
293 |     // Mock axios post
294 |     const mockResponse = {
295 |       data: {
296 |         count: 0,
297 |         results: [],
298 |       },
299 |     };
300 |     (axios.post as jest.Mock).mockResolvedValueOnce(mockResponse);
301 | 
302 |     // Call the function
303 |     await searchWorkItems(mockConnection as unknown as WebApi, {
304 |       projectId: 'testproject',
305 |       searchText: 'test query',
306 |     });
307 | 
308 |     // Verify the axios post was called with a Bearer token
309 |     expect(axios.post).toHaveBeenCalledWith(
310 |       expect.any(String),
311 |       expect.any(Object),
312 |       {
313 |         headers: {
314 |           Authorization: 'Bearer mock-azure-identity-token',
315 |           'Content-Type': 'application/json',
316 |         },
317 |       },
318 |     );
319 | 
320 |     // Cleanup
321 |     process.env.AZURE_DEVOPS_AUTH_METHOD = originalEnv;
322 |   });
323 | 
324 |   test('should perform organization-wide work item search when projectId is not provided', async () => {
325 |     // Arrange
326 |     const mockSearchResponse = {
327 |       data: {
328 |         count: 2,
329 |         results: [
330 |           {
331 |             id: 1,
332 |             fields: {
333 |               'System.Title': 'Test Bug 1',
334 |               'System.State': 'Active',
335 |               'System.WorkItemType': 'Bug',
336 |               'System.TeamProject': 'Project1',
337 |             },
338 |             project: {
339 |               name: 'Project1',
340 |               id: 'project-id-1',
341 |             },
342 |           },
343 |           {
344 |             id: 2,
345 |             fields: {
346 |               'System.Title': 'Test Bug 2',
347 |               'System.State': 'Active',
348 |               'System.WorkItemType': 'Bug',
349 |               'System.TeamProject': 'Project2',
350 |             },
351 |             project: {
352 |               name: 'Project2',
353 |               id: 'project-id-2',
354 |             },
355 |           },
356 |         ],
357 |       },
358 |     };
359 | 
360 |     mockedAxios.post.mockResolvedValueOnce(mockSearchResponse);
361 | 
362 |     // Act
363 |     const result = await searchWorkItems(connection, {
364 |       searchText: 'bug',
365 |     });
366 | 
367 |     // Assert
368 |     expect(result).toBeDefined();
369 |     expect(result.count).toBe(2);
370 |     expect(result.results).toHaveLength(2);
371 |     expect(result.results[0].fields['System.TeamProject']).toBe('Project1');
372 |     expect(result.results[1].fields['System.TeamProject']).toBe('Project2');
373 |     expect(mockedAxios.post).toHaveBeenCalledTimes(1);
374 |     expect(mockedAxios.post).toHaveBeenCalledWith(
375 |       expect.stringContaining(
376 |         'https://almsearch.dev.azure.com/mock-org/_apis/search/workitemsearchresults',
377 |       ),
378 |       expect.not.objectContaining({
379 |         filters: expect.objectContaining({
380 |           'System.TeamProject': expect.anything(),
381 |         }),
382 |       }),
383 |       expect.any(Object),
384 |     );
385 |   });
386 | });
387 | 
```

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

```typescript
  1 | import { WebApi } from 'azure-devops-node-api';
  2 | import { searchCode } from './feature';
  3 | import {
  4 |   getTestConnection,
  5 |   shouldSkipIntegrationTest,
  6 | } from '@/shared/test/test-helpers';
  7 | import { SearchCodeOptions } from '../types';
  8 | 
  9 | describe('searchCode integration', () => {
 10 |   let connection: WebApi | null = null;
 11 |   let projectName: string;
 12 | 
 13 |   beforeAll(async () => {
 14 |     // Get a real connection using environment variables
 15 |     connection = await getTestConnection();
 16 |     projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
 17 |   });
 18 | 
 19 |   test('should search code in a project', async () => {
 20 |     // Skip if no connection is available
 21 |     if (shouldSkipIntegrationTest()) {
 22 |       console.log('Skipping test: No Azure DevOps connection available');
 23 |       return;
 24 |     }
 25 | 
 26 |     // This connection must be available if we didn't skip
 27 |     if (!connection) {
 28 |       throw new Error(
 29 |         'Connection should be available when test is not skipped',
 30 |       );
 31 |     }
 32 | 
 33 |     const options: SearchCodeOptions = {
 34 |       searchText: 'function',
 35 |       projectId: projectName,
 36 |       top: 10,
 37 |     };
 38 | 
 39 |     try {
 40 |       // Act - make an actual API call to Azure DevOps
 41 |       const result = await searchCode(connection, options);
 42 | 
 43 |       // Assert on the actual response
 44 |       expect(result).toBeDefined();
 45 |       expect(typeof result.count).toBe('number');
 46 |       expect(Array.isArray(result.results)).toBe(true);
 47 | 
 48 |       // Check structure of returned items (if any)
 49 |       if (result.results.length > 0) {
 50 |         const firstResult = result.results[0];
 51 |         expect(firstResult.fileName).toBeDefined();
 52 |         expect(firstResult.path).toBeDefined();
 53 |         expect(firstResult.project).toBeDefined();
 54 |         expect(firstResult.repository).toBeDefined();
 55 | 
 56 |         if (firstResult.project) {
 57 |           expect(firstResult.project.name).toBe(projectName);
 58 |         }
 59 |       }
 60 |     } catch (error) {
 61 |       // Skip test if the code search extension is not installed
 62 |       if (
 63 |         error instanceof Error &&
 64 |         (error.message.includes('ms.vss-code-search is not installed') ||
 65 |           error.message.includes('Resource not found') ||
 66 |           error.message.includes('Failed to search code'))
 67 |       ) {
 68 |         console.log(
 69 |           'Skipping test: Code Search extension is not installed or not available in this Azure DevOps organization',
 70 |         );
 71 |         return;
 72 |       }
 73 |       throw error;
 74 |     }
 75 |   });
 76 | 
 77 |   test('should include file content when requested', async () => {
 78 |     // Skip if no connection is available
 79 |     if (shouldSkipIntegrationTest()) {
 80 |       console.log('Skipping test: No Azure DevOps connection available');
 81 |       return;
 82 |     }
 83 | 
 84 |     // This connection must be available if we didn't skip
 85 |     if (!connection) {
 86 |       throw new Error(
 87 |         'Connection should be available when test is not skipped',
 88 |       );
 89 |     }
 90 | 
 91 |     const options: SearchCodeOptions = {
 92 |       searchText: 'function',
 93 |       projectId: projectName,
 94 |       top: 5,
 95 |       includeContent: true,
 96 |     };
 97 | 
 98 |     try {
 99 |       // Act - make an actual API call to Azure DevOps
100 |       const result = await searchCode(connection, options);
101 | 
102 |       // Assert on the actual response
103 |       expect(result).toBeDefined();
104 | 
105 |       // Check if content is included (if any results)
106 |       if (result.results.length > 0) {
107 |         // At least some results should have content
108 |         // Note: Some files might fail to fetch content, so we don't expect all to have it
109 |         const hasContent = result.results.some((r) => r.content !== undefined);
110 |         expect(hasContent).toBe(true);
111 |       }
112 |     } catch (error) {
113 |       // Skip test if the code search extension is not installed
114 |       if (
115 |         error instanceof Error &&
116 |         (error.message.includes('ms.vss-code-search is not installed') ||
117 |           error.message.includes('Resource not found') ||
118 |           error.message.includes('Failed to search code'))
119 |       ) {
120 |         console.log(
121 |           'Skipping test: Code Search extension is not installed or not available in this Azure DevOps organization',
122 |         );
123 |         return;
124 |       }
125 |       throw error;
126 |     }
127 |   });
128 | 
129 |   test('should filter results when filters are provided', async () => {
130 |     // Skip if no connection is available
131 |     if (shouldSkipIntegrationTest()) {
132 |       console.log('Skipping test: No Azure DevOps connection available');
133 |       return;
134 |     }
135 | 
136 |     // This connection must be available if we didn't skip
137 |     if (!connection) {
138 |       throw new Error(
139 |         'Connection should be available when test is not skipped',
140 |       );
141 |     }
142 | 
143 |     try {
144 |       // First get some results to find a repository name
145 |       const initialOptions: SearchCodeOptions = {
146 |         searchText: 'function',
147 |         projectId: projectName,
148 |         top: 1,
149 |       };
150 | 
151 |       const initialResult = await searchCode(connection, initialOptions);
152 | 
153 |       // Skip if no results found
154 |       if (initialResult.results.length === 0) {
155 |         console.log('Skipping filter test: No initial results found');
156 |         return;
157 |       }
158 | 
159 |       // Use the repository from the first result for filtering
160 |       const repoName = initialResult.results[0].repository.name;
161 | 
162 |       const filteredOptions: SearchCodeOptions = {
163 |         searchText: 'function',
164 |         projectId: projectName,
165 |         filters: {
166 |           Repository: [repoName],
167 |         },
168 |         top: 5,
169 |       };
170 | 
171 |       // Act - make an actual API call to Azure DevOps with filters
172 |       const result = await searchCode(connection, filteredOptions);
173 | 
174 |       // Assert on the actual response
175 |       expect(result).toBeDefined();
176 | 
177 |       // All results should be from the specified repository
178 |       if (result.results.length > 0) {
179 |         const allFromRepo = result.results.every(
180 |           (r) => r.repository.name === repoName,
181 |         );
182 |         expect(allFromRepo).toBe(true);
183 |       }
184 |     } catch (error) {
185 |       // Skip test if the code search extension is not installed
186 |       if (
187 |         error instanceof Error &&
188 |         (error.message.includes('ms.vss-code-search is not installed') ||
189 |           error.message.includes('Resource not found') ||
190 |           error.message.includes('Failed to search code'))
191 |       ) {
192 |         console.log(
193 |           'Skipping test: Code Search extension is not installed or not available in this Azure DevOps organization',
194 |         );
195 |         return;
196 |       }
197 |       throw error;
198 |     }
199 |   });
200 | 
201 |   test('should handle pagination', async () => {
202 |     // Skip if no connection is available
203 |     if (shouldSkipIntegrationTest()) {
204 |       console.log('Skipping test: No Azure DevOps connection available');
205 |       return;
206 |     }
207 | 
208 |     // This connection must be available if we didn't skip
209 |     if (!connection) {
210 |       throw new Error(
211 |         'Connection should be available when test is not skipped',
212 |       );
213 |     }
214 | 
215 |     try {
216 |       // Get first page
217 |       const firstPageOptions: SearchCodeOptions = {
218 |         searchText: 'function',
219 |         projectId: projectName,
220 |         top: 2,
221 |         skip: 0,
222 |       };
223 | 
224 |       const firstPageResult = await searchCode(connection, firstPageOptions);
225 | 
226 |       // Skip if not enough results for pagination test
227 |       if (firstPageResult.count <= 2) {
228 |         console.log('Skipping pagination test: Not enough results');
229 |         return;
230 |       }
231 | 
232 |       // Get second page
233 |       const secondPageOptions: SearchCodeOptions = {
234 |         searchText: 'function',
235 |         projectId: projectName,
236 |         top: 2,
237 |         skip: 2,
238 |       };
239 | 
240 |       const secondPageResult = await searchCode(connection, secondPageOptions);
241 | 
242 |       // Assert on pagination
243 |       expect(secondPageResult).toBeDefined();
244 |       expect(secondPageResult.results.length).toBeGreaterThan(0);
245 | 
246 |       // First and second page should have different results
247 |       if (
248 |         firstPageResult.results.length > 0 &&
249 |         secondPageResult.results.length > 0
250 |       ) {
251 |         const firstPagePaths = firstPageResult.results.map((r) => r.path);
252 |         const secondPagePaths = secondPageResult.results.map((r) => r.path);
253 | 
254 |         // Check if there's any overlap between pages
255 |         const hasOverlap = firstPagePaths.some((path) =>
256 |           secondPagePaths.includes(path),
257 |         );
258 |         expect(hasOverlap).toBe(false);
259 |       }
260 |     } catch (error) {
261 |       // Skip test if the code search extension is not installed
262 |       if (
263 |         error instanceof Error &&
264 |         (error.message.includes('ms.vss-code-search is not installed') ||
265 |           error.message.includes('Resource not found') ||
266 |           error.message.includes('Failed to search code'))
267 |       ) {
268 |         console.log(
269 |           'Skipping test: Code Search extension is not installed or not available in this Azure DevOps organization',
270 |         );
271 |         return;
272 |       }
273 |       throw error;
274 |     }
275 |   });
276 | 
277 |   test('should use default project when no projectId is provided', async () => {
278 |     // Skip if no connection is available
279 |     if (shouldSkipIntegrationTest()) {
280 |       console.log('Skipping test: No Azure DevOps connection available');
281 |       return;
282 |     }
283 | 
284 |     // This connection must be available if we didn't skip
285 |     if (!connection) {
286 |       throw new Error(
287 |         'Connection should be available when test is not skipped',
288 |       );
289 |     }
290 | 
291 |     // Store original environment variable
292 |     const originalEnv = process.env.AZURE_DEVOPS_DEFAULT_PROJECT;
293 | 
294 |     try {
295 |       // Set the default project to the current project name for testing
296 |       process.env.AZURE_DEVOPS_DEFAULT_PROJECT = projectName;
297 | 
298 |       // Search without specifying a project ID
299 |       const options: SearchCodeOptions = {
300 |         searchText: 'function',
301 |         top: 5,
302 |       };
303 | 
304 |       // Act - make an actual API call to Azure DevOps
305 |       const result = await searchCode(connection, options);
306 | 
307 |       // Assert on the actual response
308 |       expect(result).toBeDefined();
309 |       expect(typeof result.count).toBe('number');
310 |       expect(Array.isArray(result.results)).toBe(true);
311 | 
312 |       // Check structure of returned items (if any)
313 |       if (result.results.length > 0) {
314 |         const firstResult = result.results[0];
315 |         expect(firstResult.fileName).toBeDefined();
316 |         expect(firstResult.path).toBeDefined();
317 |         expect(firstResult.project).toBeDefined();
318 |         expect(firstResult.repository).toBeDefined();
319 | 
320 |         if (firstResult.project) {
321 |           expect(firstResult.project.name).toBe(projectName);
322 |         }
323 |       }
324 |     } catch (error) {
325 |       // Skip test if the code search extension is not installed
326 |       if (
327 |         error instanceof Error &&
328 |         (error.message.includes('ms.vss-code-search is not installed') ||
329 |           error.message.includes('Resource not found') ||
330 |           error.message.includes('Failed to search code'))
331 |       ) {
332 |         console.log(
333 |           'Skipping test: Code Search extension is not installed or not available in this Azure DevOps organization',
334 |         );
335 |         return;
336 |       }
337 |       throw error;
338 |     } finally {
339 |       // Restore original environment variable
340 |       process.env.AZURE_DEVOPS_DEFAULT_PROJECT = originalEnv;
341 |     }
342 |   });
343 | });
344 | 
```

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

```typescript
  1 | /**
  2 |  * Options for searching code in Azure DevOps repositories
  3 |  */
  4 | export interface SearchCodeOptions {
  5 |   searchText: string;
  6 |   projectId?: string;
  7 |   filters?: {
  8 |     Repository?: string[];
  9 |     Path?: string[];
 10 |     Branch?: string[];
 11 |     CodeElement?: string[];
 12 |   };
 13 |   top?: number;
 14 |   skip?: number;
 15 |   includeSnippet?: boolean;
 16 |   includeContent?: boolean;
 17 | }
 18 | 
 19 | /**
 20 |  * Request body for the Azure DevOps Search API
 21 |  */
 22 | export interface CodeSearchRequest {
 23 |   searchText: string;
 24 |   $skip?: number;
 25 |   $top?: number;
 26 |   filters?: {
 27 |     Project?: string[];
 28 |     Repository?: string[];
 29 |     Path?: string[];
 30 |     Branch?: string[];
 31 |     CodeElement?: string[];
 32 |   };
 33 |   includeFacets?: boolean;
 34 |   includeSnippet?: boolean;
 35 | }
 36 | 
 37 | /**
 38 |  * Match information for search results
 39 |  */
 40 | export interface CodeSearchMatch {
 41 |   charOffset: number;
 42 |   length: number;
 43 | }
 44 | 
 45 | /**
 46 |  * Collection information for search results
 47 |  */
 48 | export interface CodeSearchCollection {
 49 |   name: string;
 50 | }
 51 | 
 52 | /**
 53 |  * Project information for search results
 54 |  */
 55 | export interface CodeSearchProject {
 56 |   name: string;
 57 |   id: string;
 58 | }
 59 | 
 60 | /**
 61 |  * Repository information for search results
 62 |  */
 63 | export interface CodeSearchRepository {
 64 |   name: string;
 65 |   id: string;
 66 |   type: string;
 67 | }
 68 | 
 69 | /**
 70 |  * Version information for search results
 71 |  */
 72 | export interface CodeSearchVersion {
 73 |   branchName: string;
 74 |   changeId: string;
 75 | }
 76 | 
 77 | /**
 78 |  * Individual code search result
 79 |  */
 80 | export interface CodeSearchResult {
 81 |   fileName: string;
 82 |   path: string;
 83 |   content?: string; // Added to store full file content
 84 |   matches: {
 85 |     content?: CodeSearchMatch[];
 86 |     fileName?: CodeSearchMatch[];
 87 |   };
 88 |   collection: CodeSearchCollection;
 89 |   project: CodeSearchProject;
 90 |   repository: CodeSearchRepository;
 91 |   versions: CodeSearchVersion[];
 92 |   contentId: string;
 93 | }
 94 | 
 95 | /**
 96 |  * Facet information for search results
 97 |  */
 98 | export interface CodeSearchFacet {
 99 |   name: string;
100 |   id: string;
101 |   resultCount: number;
102 | }
103 | 
104 | /**
105 |  * Response from the Azure DevOps Search API
106 |  */
107 | export interface CodeSearchResponse {
108 |   count: number;
109 |   results: CodeSearchResult[];
110 |   infoCode?: number;
111 |   facets?: {
112 |     Project?: CodeSearchFacet[];
113 |     Repository?: CodeSearchFacet[];
114 |     Path?: CodeSearchFacet[];
115 |     Branch?: CodeSearchFacet[];
116 |     CodeElement?: CodeSearchFacet[];
117 |   };
118 | }
119 | 
120 | /**
121 |  * Options for searching wiki pages in Azure DevOps projects
122 |  */
123 | export interface SearchWikiOptions {
124 |   /**
125 |    * The text to search for within wiki pages
126 |    */
127 |   searchText: string;
128 | 
129 |   /**
130 |    * The ID or name of the project to search in
131 |    * If not provided, search will be performed across the entire organization
132 |    */
133 |   projectId?: string;
134 | 
135 |   /**
136 |    * Optional filters to narrow search results
137 |    */
138 |   filters?: {
139 |     /**
140 |      * Filter by project names. Useful for cross-project searches.
141 |      */
142 |     Project?: string[];
143 |   };
144 | 
145 |   /**
146 |    * Number of results to return
147 |    * @default 100
148 |    * @minimum 1
149 |    * @maximum 1000
150 |    */
151 |   top?: number;
152 | 
153 |   /**
154 |    * Number of results to skip for pagination
155 |    * @default 0
156 |    * @minimum 0
157 |    */
158 |   skip?: number;
159 | 
160 |   /**
161 |    * Whether to include faceting in results
162 |    * @default true
163 |    */
164 |   includeFacets?: boolean;
165 | }
166 | 
167 | /**
168 |  * Request body for the Azure DevOps Wiki Search API
169 |  */
170 | export interface WikiSearchRequest {
171 |   /**
172 |    * The search text to find in wiki pages
173 |    */
174 |   searchText: string;
175 | 
176 |   /**
177 |    * Number of results to skip for pagination
178 |    */
179 |   $skip?: number;
180 | 
181 |   /**
182 |    * Number of results to return
183 |    */
184 |   $top?: number;
185 | 
186 |   /**
187 |    * Filters to be applied. Set to null if no filters are needed.
188 |    */
189 |   filters?: {
190 |     /**
191 |      * Filter by project names
192 |      */
193 |     Project?: string[];
194 |   };
195 | 
196 |   /**
197 |    * Options for sorting search results
198 |    * If null, results are sorted by relevance
199 |    */
200 |   $orderBy?: SortOption[];
201 | 
202 |   /**
203 |    * Whether to include faceting in the result
204 |    * @default false
205 |    */
206 |   includeFacets?: boolean;
207 | }
208 | 
209 | /**
210 |  * Sort option for search results
211 |  */
212 | export interface SortOption {
213 |   /**
214 |    * Field to sort by
215 |    */
216 |   field: string;
217 | 
218 |   /**
219 |    * Sort direction
220 |    */
221 |   sortOrder: 'asc' | 'desc' | 'ASC' | 'DESC';
222 | }
223 | 
224 | /**
225 |  * Defines the matched terms in the field of the wiki result
226 |  */
227 | export interface WikiHit {
228 |   /**
229 |    * Reference name of the highlighted field
230 |    */
231 |   fieldReferenceName: string;
232 | 
233 |   /**
234 |    * Matched/highlighted snippets of the field
235 |    */
236 |   highlights: string[];
237 | }
238 | 
239 | /**
240 |  * Defines the wiki result that matched a wiki search request
241 |  */
242 | export interface WikiResult {
243 |   /**
244 |    * Name of the result file
245 |    */
246 |   fileName: string;
247 | 
248 |   /**
249 |    * Path at which result file is present
250 |    */
251 |   path: string;
252 | 
253 |   /**
254 |    * Collection of the result file
255 |    */
256 |   collection: {
257 |     /**
258 |      * Name of the collection
259 |      */
260 |     name: string;
261 |   };
262 | 
263 |   /**
264 |    * Project details of the wiki document
265 |    */
266 |   project: {
267 |     /**
268 |      * ID of the project
269 |      */
270 |     id: string;
271 | 
272 |     /**
273 |      * Name of the project
274 |      */
275 |     name: string;
276 | 
277 |     /**
278 |      * Visibility of the project
279 |      */
280 |     visibility?: string;
281 |   };
282 | 
283 |   /**
284 |    * Wiki information for the result
285 |    */
286 |   wiki: {
287 |     /**
288 |      * ID of the wiki
289 |      */
290 |     id: string;
291 | 
292 |     /**
293 |      * Mapped path for the wiki
294 |      */
295 |     mappedPath: string;
296 | 
297 |     /**
298 |      * Name of the wiki
299 |      */
300 |     name: string;
301 | 
302 |     /**
303 |      * Version for wiki
304 |      */
305 |     version: string;
306 |   };
307 | 
308 |   /**
309 |    * Content ID of the result file
310 |    */
311 |   contentId: string;
312 | 
313 |   /**
314 |    * Highlighted snippets of fields that match the search request
315 |    * The list is sorted by relevance of the snippets
316 |    */
317 |   hits: WikiHit[];
318 | }
319 | 
320 | /**
321 |  * Defines a wiki search response item
322 |  */
323 | export interface WikiSearchResponse {
324 |   /**
325 |    * Total number of matched wiki documents
326 |    */
327 |   count: number;
328 | 
329 |   /**
330 |    * List of top matched wiki documents
331 |    */
332 |   results: WikiResult[];
333 | 
334 |   /**
335 |    * Numeric code indicating additional information:
336 |    * 0 - Ok
337 |    * 1 - Account is being reindexed
338 |    * 2 - Account indexing has not started
339 |    * 3 - Invalid Request
340 |    * ... and others as defined in the API
341 |    */
342 |   infoCode?: number;
343 | 
344 |   /**
345 |    * A dictionary storing an array of Filter objects against each facet
346 |    */
347 |   facets?: {
348 |     /**
349 |      * Project facets for filtering
350 |      */
351 |     Project?: CodeSearchFacet[];
352 |   };
353 | }
354 | 
355 | /**
356 |  * Options for searching work items in Azure DevOps projects
357 |  */
358 | export interface SearchWorkItemsOptions {
359 |   /**
360 |    * The text to search for within work items
361 |    */
362 |   searchText: string;
363 | 
364 |   /**
365 |    * The ID or name of the project to search in
366 |    * If not provided, search will be performed across the entire organization
367 |    */
368 |   projectId?: string;
369 | 
370 |   /**
371 |    * Optional filters to narrow search results
372 |    */
373 |   filters?: {
374 |     /**
375 |      * Filter by project names. Useful for cross-project searches.
376 |      */
377 |     'System.TeamProject'?: string[];
378 | 
379 |     /**
380 |      * Filter by work item types (Bug, Task, User Story, etc.)
381 |      */
382 |     'System.WorkItemType'?: string[];
383 | 
384 |     /**
385 |      * Filter by work item states (New, Active, Closed, etc.)
386 |      */
387 |     'System.State'?: string[];
388 | 
389 |     /**
390 |      * Filter by assigned users
391 |      */
392 |     'System.AssignedTo'?: string[];
393 | 
394 |     /**
395 |      * Filter by area paths
396 |      */
397 |     'System.AreaPath'?: string[];
398 |   };
399 | 
400 |   /**
401 |    * Number of results to return
402 |    * @default 100
403 |    * @minimum 1
404 |    * @maximum 1000
405 |    */
406 |   top?: number;
407 | 
408 |   /**
409 |    * Number of results to skip for pagination
410 |    * @default 0
411 |    * @minimum 0
412 |    */
413 |   skip?: number;
414 | 
415 |   /**
416 |    * Whether to include faceting in results
417 |    * @default true
418 |    */
419 |   includeFacets?: boolean;
420 | 
421 |   /**
422 |    * Options for sorting search results
423 |    * If null, results are sorted by relevance
424 |    */
425 |   orderBy?: SortOption[];
426 | }
427 | 
428 | /**
429 |  * Request body for the Azure DevOps Work Item Search API
430 |  */
431 | export interface WorkItemSearchRequest {
432 |   /**
433 |    * The search text to find in work items
434 |    */
435 |   searchText: string;
436 | 
437 |   /**
438 |    * Number of results to skip for pagination
439 |    */
440 |   $skip?: number;
441 | 
442 |   /**
443 |    * Number of results to return
444 |    */
445 |   $top?: number;
446 | 
447 |   /**
448 |    * Filters to be applied. Set to null if no filters are needed.
449 |    */
450 |   filters?: {
451 |     'System.TeamProject'?: string[];
452 |     'System.WorkItemType'?: string[];
453 |     'System.State'?: string[];
454 |     'System.AssignedTo'?: string[];
455 |     'System.AreaPath'?: string[];
456 |   };
457 | 
458 |   /**
459 |    * Options for sorting search results
460 |    * If null, results are sorted by relevance
461 |    */
462 |   $orderBy?: SortOption[];
463 | 
464 |   /**
465 |    * Whether to include faceting in the result
466 |    * @default false
467 |    */
468 |   includeFacets?: boolean;
469 | }
470 | 
471 | /**
472 |  * Defines the matched terms in the field of the work item result
473 |  */
474 | export interface WorkItemHit {
475 |   /**
476 |    * Reference name of the highlighted field
477 |    */
478 |   fieldReferenceName: string;
479 | 
480 |   /**
481 |    * Matched/highlighted snippets of the field
482 |    */
483 |   highlights: string[];
484 | }
485 | 
486 | /**
487 |  * Defines the work item result that matched a work item search request
488 |  */
489 | export interface WorkItemResult {
490 |   /**
491 |    * Project details of the work item
492 |    */
493 |   project: {
494 |     /**
495 |      * ID of the project
496 |      */
497 |     id: string;
498 | 
499 |     /**
500 |      * Name of the project
501 |      */
502 |     name: string;
503 |   };
504 | 
505 |   /**
506 |    * A standard set of work item fields and their values
507 |    */
508 |   fields: {
509 |     /**
510 |      * ID of the work item
511 |      */
512 |     'system.id': string;
513 | 
514 |     /**
515 |      * Type of the work item (Bug, Task, User Story, etc.)
516 |      */
517 |     'system.workitemtype': string;
518 | 
519 |     /**
520 |      * Title of the work item
521 |      */
522 |     'system.title': string;
523 | 
524 |     /**
525 |      * User assigned to the work item
526 |      */
527 |     'system.assignedto'?: string;
528 | 
529 |     /**
530 |      * Current state of the work item
531 |      */
532 |     'system.state'?: string;
533 | 
534 |     /**
535 |      * Tags associated with the work item
536 |      */
537 |     'system.tags'?: string;
538 | 
539 |     /**
540 |      * Revision number of the work item
541 |      */
542 |     'system.rev'?: string;
543 | 
544 |     /**
545 |      * Creation date of the work item
546 |      */
547 |     'system.createddate'?: string;
548 | 
549 |     /**
550 |      * Last modified date of the work item
551 |      */
552 |     'system.changeddate'?: string;
553 | 
554 |     /**
555 |      * Other fields may be included based on the work item type
556 |      */
557 |     [key: string]: string | number | boolean | null | undefined;
558 |   };
559 | 
560 |   /**
561 |    * Highlighted snippets of fields that match the search request
562 |    * The list is sorted by relevance of the snippets
563 |    */
564 |   hits: WorkItemHit[];
565 | 
566 |   /**
567 |    * URL to the work item
568 |    */
569 |   url: string;
570 | }
571 | 
572 | /**
573 |  * Defines a work item search response item
574 |  */
575 | export interface WorkItemSearchResponse {
576 |   /**
577 |    * Total number of matched work items
578 |    */
579 |   count: number;
580 | 
581 |   /**
582 |    * List of top matched work items
583 |    */
584 |   results: WorkItemResult[];
585 | 
586 |   /**
587 |    * Numeric code indicating additional information:
588 |    * 0 - Ok
589 |    * 1 - Account is being reindexed
590 |    * 2 - Account indexing has not started
591 |    * 3 - Invalid Request
592 |    * ... and others as defined in the API
593 |    */
594 |   infoCode?: number;
595 | 
596 |   /**
597 |    * A dictionary storing an array of Filter objects against each facet
598 |    */
599 |   facets?: {
600 |     'System.TeamProject'?: CodeSearchFacet[];
601 |     'System.WorkItemType'?: CodeSearchFacet[];
602 |     'System.State'?: CodeSearchFacet[];
603 |     'System.AssignedTo'?: CodeSearchFacet[];
604 |     'System.AreaPath'?: CodeSearchFacet[];
605 |   };
606 | }
607 | 
```
Page 5/8FirstPrevNextLast