diff --git a/src/azure-devops/.eslintrc.js b/src/azure-devops/.eslintrc.js new file mode 100644 index 00000000..45fe9487 --- /dev/null +++ b/src/azure-devops/.eslintrc.js @@ -0,0 +1,19 @@ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended' + ], + rules: { + '@typescript-eslint/explicit-function-return-type': 'error', + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + }, + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +} \ No newline at end of file diff --git a/src/azure-devops/.github/workflows/ci.yml b/src/azure-devops/.github/workflows/ci.yml new file mode 100644 index 00000000..54f1912d --- /dev/null +++ b/src/azure-devops/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + push: + paths: + - 'src/azure-devops/**' + pull_request: + paths: + - 'src/azure-devops/**' + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16.x, 18.x] + + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + working-directory: ./src/azure-devops + run: npm ci + + - name: Lint + working-directory: ./src/azure-devops + run: npm run lint + + - name: Build + working-directory: ./src/azure-devops + run: npm run build + + - name: Test + working-directory: ./src/azure-devops + run: npm test + + - name: Upload coverage + uses: codecov/codecov-action@v3 + with: + directory: ./src/azure-devops/coverage \ No newline at end of file diff --git a/src/azure-devops/CHANGELOG.md b/src/azure-devops/CHANGELOG.md new file mode 100644 index 00000000..739ae66e --- /dev/null +++ b/src/azure-devops/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +All notable changes to the Azure DevOps MCP Server will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0] - 2024-12-27 + +### Added +- Initial release +- Work Item Management API + - Create work items + - Get work item by ID + - Update work items +- Project Management API + - List projects + - Get project details + - Create/delete projects +- Repository Management API + - List repositories + - Create/delete repositories + - Get repository branches + +[0.1.0]: https://github.com/modelcontextprotocol/servers/releases/tag/azure-devops-v0.1.0 \ No newline at end of file diff --git a/src/azure-devops/README.md b/src/azure-devops/README.md new file mode 100644 index 00000000..bbbbfeeb --- /dev/null +++ b/src/azure-devops/README.md @@ -0,0 +1,181 @@ +# Azure DevOps MCP Server + +A Model Context Protocol (MCP) server implementation for Azure DevOps, enabling AI language models to interact with Azure DevOps services through a standardized interface. + +## Features + +### Work Item Management +- Create, read, and update work items +- Track bugs, tasks, and user stories +- Manage work item states + +### Project Management +- List and view projects +- Create new projects +- Delete existing projects + +### Repository Management +- List repositories in projects +- Create and delete repositories +- Manage repository branches + +## Installation + +```bash +npm install @modelcontextprotocol/server-azure-devops +``` + +## Configuration + +Set the following environment variables: +- `AZURE_DEVOPS_ORG_URL`: Your Azure DevOps organization URL (e.g., https://dev.azure.com/yourorg) +- `AZURE_PERSONAL_ACCESS_TOKEN`: Your Azure DevOps Personal Access Token + +## Using with MCP Client + +Add this to your MCP client configuration (e.g. Claude Desktop): + +```json +{ + "mcpServers": { + "azure": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-azure-devops"], + "env": { + "AZURE_DEVOPS_ORG_URL": "https://dev.azure.com/yourorg", + "AZURE_PERSONAL_ACCESS_TOKEN": "" + } + } + } +} +``` + +## API Documentation + +### Work Items + +#### Get Work Item +```typescript +getWorkItem({ id: number }): Promise +``` +Retrieves a work item by its ID. + +#### Create Work Item +```typescript +createWorkItem({ + project: string, + type: string, + title: string, + description?: string +}): Promise +``` +Creates a new work item in the specified project. + +#### Update Work Item +```typescript +updateWorkItem({ + id: number, + title?: string, + state?: string, + description?: string +}): Promise +``` +Updates an existing work item. + +### Projects + +#### List Projects +```typescript +listProjects(): Promise +``` +Lists all accessible projects. + +#### Get Project +```typescript +getProject({ name: string }): Promise +``` +Gets details of a specific project. + +### Repositories + +#### List Repositories +```typescript +listRepositories({ project: string }): Promise +``` +Lists all repositories in a project. + +#### Create Repository +```typescript +createRepository({ + project: string, + name: string +}): Promise +``` +Creates a new repository in the specified project. + +## Examples + +### Managing Work Items +```typescript +// Get a work item +const workItem = await azure.getWorkItem({ id: 123 }); + +// Create a new task +const newTask = await azure.createWorkItem({ + project: 'MyProject', + type: 'Task', + title: 'Implement feature X', + description: 'Implementation details...' +}); + +// Update work item state +const updatedItem = await azure.updateWorkItem({ + id: 123, + state: 'Active' +}); +``` + +### Managing Projects +```typescript +// List all projects +const projects = await azure.listProjects(); + +// Get specific project +const project = await azure.getProject({ name: 'MyProject' }); +``` + +### Managing Repositories +```typescript +// List repositories +const repos = await azure.listRepositories({ project: 'MyProject' }); + +// Create new repository +const newRepo = await azure.createRepository({ + project: 'MyProject', + name: 'new-service' +}); +``` + +## Development + +```bash +# Install dependencies +npm install + +# Run tests +npm test + +# Build +npm run build + +# Lint +npm run lint +``` + +## Contributing + +See [CONTRIBUTING.md](../../CONTRIBUTING.md) for information about contributing to this repository. + +## License + +This project is licensed under the MIT License - see the [LICENSE](../../LICENSE) file for details. \ No newline at end of file diff --git a/src/azure-devops/examples/basic-usage.ts b/src/azure-devops/examples/basic-usage.ts new file mode 100644 index 00000000..290f1747 --- /dev/null +++ b/src/azure-devops/examples/basic-usage.ts @@ -0,0 +1,35 @@ +import { AzureDevOpsMCPServer } from '../index'; + +async function main() { + // Initialize the server + const server = new AzureDevOpsMCPServer(); + + try { + // List all projects + const projects = await server.projects.listProjects(); + console.log('Projects:', projects); + + // Create a work item + const workItem = await server.workItems.createWorkItem({ + project: 'Your Project Name', + type: 'Task', + title: 'Example Task', + description: 'This is an example task created through the MCP server' + }); + console.log('Created Work Item:', workItem); + + // List repositories + const repos = await server.repositories.listRepositories({ + project: 'Your Project Name' + }); + console.log('Repositories:', repos); + + } catch (error) { + console.error('Error:', error.message); + } +} + +// Run the example if this script is executed directly +if (require.main === module) { + main().catch(console.error); +} \ No newline at end of file diff --git a/src/azure-devops/functions/workItems.ts b/src/azure-devops/functions/workItems.ts index 6d8e48e3..dde8c2f6 100644 --- a/src/azure-devops/functions/workItems.ts +++ b/src/azure-devops/functions/workItems.ts @@ -2,11 +2,20 @@ import { MCPFunction, MCPFunctionGroup } from '@modelcontextprotocol/typescript- import * as azdev from 'azure-devops-node-api'; import { WorkItem } from '../types'; +/** + * WorkItemManagement class handles all work item related operations in Azure DevOps + * through the Model Context Protocol interface. + * + * @implements {MCPFunctionGroup} + */ export class WorkItemManagement implements MCPFunctionGroup { private connection: azdev.WebApi; + /** + * Initializes the WorkItemManagement with Azure DevOps credentials + * @throws {Error} If required environment variables are not set + */ constructor() { - // Initialize Azure DevOps connection const orgUrl = process.env.AZURE_DEVOPS_ORG_URL; const token = process.env.AZURE_PERSONAL_ACCESS_TOKEN; @@ -18,6 +27,14 @@ export class WorkItemManagement implements MCPFunctionGroup { this.connection = new azdev.WebApi(orgUrl, authHandler); } + /** + * Retrieves a work item by its ID + * + * @param {Object} params - Parameters for the function + * @param {number} params.id - The ID of the work item to retrieve + * @returns {Promise} The requested work item + * @throws {Error} If the work item is not found or access is denied + */ @MCPFunction({ description: 'Get work item by ID', parameters: { @@ -29,18 +46,37 @@ export class WorkItemManagement implements MCPFunctionGroup { } }) async getWorkItem({ id }: { id: number }): Promise { - const client = await this.connection.getWorkItemTrackingApi(); - const item = await client.getWorkItem(id); - - return { - id: item.id, - title: item.fields['System.Title'], - state: item.fields['System.State'], - type: item.fields['System.WorkItemType'], - description: item.fields['System.Description'] - }; + try { + const client = await this.connection.getWorkItemTrackingApi(); + const item = await client.getWorkItem(id); + + if (!item) { + throw new Error(`Work item ${id} not found`); + } + + return { + id: item.id, + title: item.fields['System.Title'], + state: item.fields['System.State'], + type: item.fields['System.WorkItemType'], + description: item.fields['System.Description'] + }; + } catch (error) { + throw new Error(`Failed to get work item ${id}: ${error.message}`); + } } + /** + * Creates a new work item + * + * @param {Object} params - Parameters for creating the work item + * @param {string} params.project - The project where the work item will be created + * @param {string} params.type - The type of work item (e.g., Bug, Task, User Story) + * @param {string} params.title - The title of the work item + * @param {string} [params.description] - Optional description for the work item + * @returns {Promise} The created work item + * @throws {Error} If creation fails or parameters are invalid + */ @MCPFunction({ description: 'Create new work item', parameters: { @@ -60,29 +96,47 @@ export class WorkItemManagement implements MCPFunctionGroup { title: string; description?: string; }): Promise { - const client = await this.connection.getWorkItemTrackingApi(); - - const patchDocument = [ - { op: 'add', path: '/fields/System.Title', value: title }, - { op: 'add', path: '/fields/System.Description', value: description } - ]; + try { + const client = await this.connection.getWorkItemTrackingApi(); + + const patchDocument = [ + { op: 'add', path: '/fields/System.Title', value: title } + ]; + + if (description) { + patchDocument.push({ op: 'add', path: '/fields/System.Description', value: description }); + } - const item = await client.createWorkItem( - null, - patchDocument, - project, - type - ); + const item = await client.createWorkItem( + null, + patchDocument, + project, + type + ); - return { - id: item.id, - title: item.fields['System.Title'], - state: item.fields['System.State'], - type: item.fields['System.WorkItemType'], - description: item.fields['System.Description'] - }; + return { + id: item.id, + title: item.fields['System.Title'], + state: item.fields['System.State'], + type: item.fields['System.WorkItemType'], + description: item.fields['System.Description'] + }; + } catch (error) { + throw new Error(`Failed to create work item: ${error.message}`); + } } + /** + * Updates an existing work item + * + * @param {Object} params - Parameters for updating the work item + * @param {number} params.id - The ID of the work item to update + * @param {string} [params.title] - New title for the work item + * @param {string} [params.state] - New state for the work item + * @param {string} [params.description] - New description for the work item + * @returns {Promise} The updated work item + * @throws {Error} If update fails or work item is not found + */ @MCPFunction({ description: 'Update work item', parameters: { @@ -102,25 +156,33 @@ export class WorkItemManagement implements MCPFunctionGroup { state?: string; description?: string; }): Promise { - const client = await this.connection.getWorkItemTrackingApi(); - - const patchDocument = []; - if (title) patchDocument.push({ op: 'add', path: '/fields/System.Title', value: title }); - if (state) patchDocument.push({ op: 'add', path: '/fields/System.State', value: state }); - if (description) patchDocument.push({ op: 'add', path: '/fields/System.Description', value: description }); + try { + const client = await this.connection.getWorkItemTrackingApi(); + + const patchDocument = []; + if (title) patchDocument.push({ op: 'add', path: '/fields/System.Title', value: title }); + if (state) patchDocument.push({ op: 'add', path: '/fields/System.State', value: state }); + if (description) patchDocument.push({ op: 'add', path: '/fields/System.Description', value: description }); - const item = await client.updateWorkItem( - null, - patchDocument, - id - ); + if (patchDocument.length === 0) { + throw new Error('No updates specified'); + } - return { - id: item.id, - title: item.fields['System.Title'], - state: item.fields['System.State'], - type: item.fields['System.WorkItemType'], - description: item.fields['System.Description'] - }; + const item = await client.updateWorkItem( + null, + patchDocument, + id + ); + + return { + id: item.id, + title: item.fields['System.Title'], + state: item.fields['System.State'], + type: item.fields['System.WorkItemType'], + description: item.fields['System.Description'] + }; + } catch (error) { + throw new Error(`Failed to update work item ${id}: ${error.message}`); + } } } \ No newline at end of file diff --git a/src/azure-devops/jest.config.js b/src/azure-devops/jest.config.js new file mode 100644 index 00000000..b3d946e6 --- /dev/null +++ b/src/azure-devops/jest.config.js @@ -0,0 +1,23 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/tests'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest' + }, + coverageThreshold: { + global: { + branches: 80, + functions: 80, + lines: 80, + statements: 80 + } + }, + collectCoverageFrom: [ + 'functions/**/*.ts', + 'types/**/*.ts', + 'index.ts', + '!tests/**/*' + ] +}; \ No newline at end of file diff --git a/src/azure-devops/package.json b/src/azure-devops/package.json new file mode 100644 index 00000000..a849572a --- /dev/null +++ b/src/azure-devops/package.json @@ -0,0 +1,47 @@ +{ + "name": "@modelcontextprotocol/server-azure-devops", + "version": "0.1.0", + "description": "Azure DevOps MCP Server Implementation", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "test": "jest --coverage", + "lint": "eslint . --ext .ts", + "format": "prettier --write \"**/*.ts\"", + "prepare": "npm run build", + "start": "node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/typescript-sdk": "^0.1.0", + "azure-devops-node-api": "^12.0.0" + }, + "devDependencies": { + "@types/jest": "^29.5.0", + "@types/node": "^18.0.0", + "@typescript-eslint/eslint-plugin": "^5.0.0", + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^8.0.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.5.0", + "prettier": "^2.8.0", + "ts-jest": "^29.1.0", + "typescript": "^5.0.0" + }, + "publishConfig": { + "access": "public" + }, + "keywords": [ + "mcp", + "azure-devops", + "modelcontextprotocol" + ], + "author": "Zubeid Hendricks", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/modelcontextprotocol/servers.git", + "directory": "src/azure-devops" + } +} \ No newline at end of file diff --git a/src/azure-devops/tests/integration/basic.test.ts b/src/azure-devops/tests/integration/basic.test.ts new file mode 100644 index 00000000..2ee8c8a0 --- /dev/null +++ b/src/azure-devops/tests/integration/basic.test.ts @@ -0,0 +1,67 @@ +import { AzureDevOpsMCPServer } from '../../index'; + +describe('Azure DevOps MCP Server Integration', () => { + let server: AzureDevOpsMCPServer; + + beforeAll(() => { + // These would typically be set in the environment or a .env file + process.env.AZURE_DEVOPS_ORG_URL = process.env.TEST_AZURE_DEVOPS_ORG_URL; + process.env.AZURE_PERSONAL_ACCESS_TOKEN = process.env.TEST_AZURE_PERSONAL_ACCESS_TOKEN; + }); + + beforeEach(() => { + server = new AzureDevOpsMCPServer(); + }); + + it('should initialize without errors', () => { + expect(server).toBeInstanceOf(AzureDevOpsMCPServer); + }); + + describe('when credentials are not provided', () => { + beforeEach(() => { + delete process.env.AZURE_DEVOPS_ORG_URL; + delete process.env.AZURE_PERSONAL_ACCESS_TOKEN; + }); + + it('should throw an error', () => { + expect(() => new AzureDevOpsMCPServer()).toThrow(); + }); + }); + + describe('Work Items', () => { + it('should create and retrieve work items', async () => { + const workItemTitle = `Test Item ${Date.now()}`; + + // Create work item + const created = await server.workItems.createWorkItem({ + project: process.env.TEST_PROJECT_NAME || 'Test Project', + type: 'Task', + title: workItemTitle, + description: 'Test description' + }); + + expect(created.title).toBe(workItemTitle); + + // Retrieve work item + const retrieved = await server.workItems.getWorkItem({ id: created.id }); + expect(retrieved.id).toBe(created.id); + expect(retrieved.title).toBe(workItemTitle); + }); + }); + + describe('Projects', () => { + it('should list projects', async () => { + const projects = await server.projects.listProjects(); + expect(Array.isArray(projects)).toBe(true); + }); + }); + + describe('Repositories', () => { + it('should list repositories in a project', async () => { + const repos = await server.repositories.listRepositories({ + project: process.env.TEST_PROJECT_NAME || 'Test Project' + }); + expect(Array.isArray(repos)).toBe(true); + }); + }); +}); \ No newline at end of file diff --git a/src/azure-devops/tests/projects.test.ts b/src/azure-devops/tests/projects.test.ts new file mode 100644 index 00000000..dd5894af --- /dev/null +++ b/src/azure-devops/tests/projects.test.ts @@ -0,0 +1,88 @@ +import { ProjectManagement } from '../functions/projects'; +import * as azdev from 'azure-devops-node-api'; + +jest.mock('azure-devops-node-api'); + +describe('ProjectManagement', () => { + let projectManager: ProjectManagement; + const mockCoreApi = { + getProjects: jest.fn(), + getProject: jest.fn(), + createProject: jest.fn(), + deleteProject: jest.fn() + }; + + beforeEach(() => { + process.env.AZURE_DEVOPS_ORG_URL = 'https://dev.azure.com/test'; + process.env.AZURE_PERSONAL_ACCESS_TOKEN = 'test-token'; + + (azdev.WebApi as jest.Mock).mockImplementation(() => ({ + getCoreApi: () => mockCoreApi + })); + + projectManager = new ProjectManagement(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('listProjects', () => { + it('should list all projects', async () => { + const mockProjects = [ + { id: '1', name: 'Project 1', description: 'Description 1' }, + { id: '2', name: 'Project 2', description: 'Description 2' } + ]; + + mockCoreApi.getProjects.mockResolvedValue(mockProjects); + + const result = await projectManager.listProjects(); + + expect(result).toEqual(mockProjects); + expect(mockCoreApi.getProjects).toHaveBeenCalled(); + }); + + it('should handle errors when listing projects', async () => { + mockCoreApi.getProjects.mockRejectedValue(new Error('API Error')); + + await expect(projectManager.listProjects()).rejects.toThrow('Failed to list projects'); + }); + }); + + describe('getProject', () => { + it('should get project by name', async () => { + const mockProject = { + id: '1', + name: 'Test Project', + description: 'Test Description' + }; + + mockCoreApi.getProject.mockResolvedValue(mockProject); + + const result = await projectManager.getProject({ name: 'Test Project' }); + + expect(result).toEqual(mockProject); + expect(mockCoreApi.getProject).toHaveBeenCalledWith('Test Project'); + }); + }); + + describe('createProject', () => { + it('should create a new project', async () => { + const mockProject = { + id: '1', + name: 'New Project', + description: 'New Description' + }; + + mockCoreApi.createProject.mockResolvedValue(mockProject); + + const result = await projectManager.createProject({ + name: 'New Project', + description: 'New Description' + }); + + expect(result).toEqual(mockProject); + expect(mockCoreApi.createProject).toHaveBeenCalled(); + }); + }); +}); \ No newline at end of file diff --git a/src/azure-devops/tests/repositories.test.ts b/src/azure-devops/tests/repositories.test.ts new file mode 100644 index 00000000..6de5cd0d --- /dev/null +++ b/src/azure-devops/tests/repositories.test.ts @@ -0,0 +1,103 @@ +import { RepositoryManagement } from '../functions/repositories'; +import * as azdev from 'azure-devops-node-api'; + +jest.mock('azure-devops-node-api'); + +describe('RepositoryManagement', () => { + let repoManager: RepositoryManagement; + const mockGitApi = { + getRepositories: jest.fn(), + createRepository: jest.fn(), + deleteRepository: jest.fn(), + getBranches: jest.fn() + }; + + beforeEach(() => { + process.env.AZURE_DEVOPS_ORG_URL = 'https://dev.azure.com/test'; + process.env.AZURE_PERSONAL_ACCESS_TOKEN = 'test-token'; + + (azdev.WebApi as jest.Mock).mockImplementation(() => ({ + getGitApi: () => mockGitApi + })); + + repoManager = new RepositoryManagement(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('listRepositories', () => { + it('should list repositories in a project', async () => { + const mockRepos = [ + { + id: '1', + name: 'Repo 1', + defaultBranch: 'main', + url: 'https://example.com/repo1' + }, + { + id: '2', + name: 'Repo 2', + defaultBranch: 'main', + url: 'https://example.com/repo2' + } + ]; + + mockGitApi.getRepositories.mockResolvedValue(mockRepos); + + const result = await repoManager.listRepositories({ project: 'Test Project' }); + + expect(result).toEqual(mockRepos); + expect(mockGitApi.getRepositories).toHaveBeenCalledWith('Test Project'); + }); + }); + + describe('createRepository', () => { + it('should create a new repository', async () => { + const mockRepo = { + id: '1', + name: 'New Repo', + defaultBranch: 'main', + url: 'https://example.com/new-repo' + }; + + mockGitApi.createRepository.mockResolvedValue(mockRepo); + + const result = await repoManager.createRepository({ + project: 'Test Project', + name: 'New Repo' + }); + + expect(result).toEqual(mockRepo); + expect(mockGitApi.createRepository).toHaveBeenCalledWith({ + name: 'New Repo', + project: { name: 'Test Project' } + }); + }); + }); + + describe('getBranches', () => { + it('should get repository branches', async () => { + const mockRepos = [{ + id: '1', + name: 'Test Repo' + }]; + const mockBranches = [ + { name: 'main' }, + { name: 'develop' } + ]; + + mockGitApi.getRepositories.mockResolvedValue(mockRepos); + mockGitApi.getBranches.mockResolvedValue(mockBranches); + + const result = await repoManager.getBranches({ + project: 'Test Project', + repository: 'Test Repo' + }); + + expect(result).toEqual(['main', 'develop']); + expect(mockGitApi.getBranches).toHaveBeenCalledWith('1'); + }); + }); +}); \ No newline at end of file diff --git a/src/azure-devops/tests/workItems.test.ts b/src/azure-devops/tests/workItems.test.ts new file mode 100644 index 00000000..180ef92b --- /dev/null +++ b/src/azure-devops/tests/workItems.test.ts @@ -0,0 +1,50 @@ +import { WorkItemManagement } from '../functions/workItems'; +import * as azdev from 'azure-devops-node-api'; + +jest.mock('azure-devops-node-api'); + +describe('WorkItemManagement', () => { + let workItemManager: WorkItemManagement; + + beforeEach(() => { + process.env.AZURE_DEVOPS_ORG_URL = 'https://dev.azure.com/test'; + process.env.AZURE_PERSONAL_ACCESS_TOKEN = 'test-token'; + workItemManager = new WorkItemManagement(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('getWorkItem', () => { + it('should retrieve work item by ID', async () => { + const mockWorkItem = { + id: 1, + fields: { + 'System.Title': 'Test Work Item', + 'System.State': 'Active', + 'System.WorkItemType': 'Task', + 'System.Description': 'Test Description' + } + }; + + const mockClient = { + getWorkItem: jest.fn().mockResolvedValue(mockWorkItem) + }; + + (azdev.WebApi as jest.Mock).mockImplementation(() => ({ + getWorkItemTrackingApi: () => mockClient + })); + + const result = await workItemManager.getWorkItem({ id: 1 }); + + expect(result).toEqual({ + id: 1, + title: 'Test Work Item', + state: 'Active', + type: 'Task', + description: 'Test Description' + }); + }); + }); +}); \ No newline at end of file diff --git a/src/azure-devops/tsconfig.json b/src/azure-devops/tsconfig.json new file mode 100644 index 00000000..552f598b --- /dev/null +++ b/src/azure-devops/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "declaration": true, + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "rootDir": "./", + "baseUrl": "./", + "paths": { + "*": ["node_modules/*", "src/types/*"] + } + }, + "include": [ + "**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts" + ] +} \ No newline at end of file