Skip to main content
Glama
work-items.test.ts105 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { describe, expect, it } from "@jest/globals"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { configureWorkItemTools } from "../../../src/tools/work-items"; import { WebApi } from "azure-devops-node-api"; import { QueryExpand } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js"; import { _mockBacklogs, _mockQuery, _mockQueryResults, _mockWorkItem, _mockWorkItemComment, _mockWorkItemComments, _mockWorkItems, _mockWorkItemsForIteration, _mockWorkItemType, } from "../../mocks/work-items"; type TokenProviderMock = () => Promise<string>; type ConnectionProviderMock = () => Promise<WebApi>; interface WorkApiMock { getBacklogs: jest.Mock; getBacklogLevelWorkItems: jest.Mock; getPredefinedQueryResults: jest.Mock; getTeamIterations: jest.Mock; getIterationWorkItems: jest.Mock; } interface WorkItemTrackingApiMock { getWorkItemsBatch: jest.Mock; getWorkItem: jest.Mock; getComments: jest.Mock; addComment: jest.Mock; updateWorkItem: jest.Mock; createWorkItem: jest.Mock; getWorkItemType: jest.Mock; getQuery: jest.Mock; queryById: jest.Mock; } interface MockConnection { getWorkApi: jest.Mock; getWorkItemTrackingApi: jest.Mock; serverUrl?: string; } describe("configureWorkItemTools", () => { let server: McpServer; let tokenProvider: TokenProviderMock; let connectionProvider: ConnectionProviderMock; let userAgentProvider: () => string; let mockConnection: MockConnection; let mockWorkApi: WorkApiMock; let mockWorkItemTrackingApi: WorkItemTrackingApiMock; beforeEach(() => { server = { tool: jest.fn() } as unknown as McpServer; tokenProvider = jest.fn(); mockWorkApi = { getBacklogs: jest.fn(), getBacklogLevelWorkItems: jest.fn(), getPredefinedQueryResults: jest.fn(), getTeamIterations: jest.fn(), getIterationWorkItems: jest.fn(), }; mockWorkItemTrackingApi = { getWorkItemsBatch: jest.fn(), getWorkItem: jest.fn(), getComments: jest.fn(), addComment: jest.fn(), updateWorkItem: jest.fn(), createWorkItem: jest.fn(), getWorkItemType: jest.fn(), getQuery: jest.fn(), queryById: jest.fn(), }; mockConnection = { getWorkApi: jest.fn().mockResolvedValue(mockWorkApi), getWorkItemTrackingApi: jest.fn().mockResolvedValue(mockWorkItemTrackingApi), }; connectionProvider = jest.fn().mockResolvedValue(mockConnection); userAgentProvider = () => "Jest"; }); describe("tool registration", () => { it("registers core tools on the server", () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); expect(server.tool as jest.Mock).toHaveBeenCalled(); }); }); describe("list_backlogs tool", () => { it("should call getBacklogs API with the correct parameters and return the expected result", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_list_backlogs"); if (!call) throw new Error("wit_list_backlogs tool not registered"); const [, , , handler] = call; (mockWorkApi.getBacklogs as jest.Mock).mockResolvedValue([_mockBacklogs]); const params = { project: "Contoso", team: "Fabrikam", }; const result = await handler(params); expect(mockWorkApi.getBacklogs).toHaveBeenCalledWith({ project: params.project, team: params.team, }); expect(result.content[0].text).toBe(JSON.stringify([_mockBacklogs], null, 2)); }); }); describe("list_backlog_work_items tool", () => { it("should call getBacklogLevelWorkItems API with the correct parameters and return the expected result", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_list_backlog_work_items"); if (!call) throw new Error("wit_list_backlog_work_items tool not registered"); const [, , , handler] = call; (mockWorkApi.getBacklogLevelWorkItems as jest.Mock).mockResolvedValue([ { workItems: [ { rel: null, source: null, target: { id: 50, }, }, { rel: null, source: null, target: { id: 49, }, }, ], }, ]); const params = { project: "Contoso", team: "Fabrikam", backlogId: "Microsoft.FeatureCategory", }; const result = await handler(params); expect(mockWorkApi.getBacklogLevelWorkItems).toHaveBeenCalledWith({ project: params.project, team: params.team }, params.backlogId); expect(result.content[0].text).toBe( JSON.stringify( [ { workItems: [ { rel: null, source: null, target: { id: 50, }, }, { rel: null, source: null, target: { id: 49, }, }, ], }, ], null, 2 ) ); }); }); describe("my_work_items tool", () => { it("should call getPredefinedQueryResults API with the correct parameters and return the expected result", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_my_work_items"); if (!call) throw new Error("wit_my_work_items tool not registered"); const [, , , handler] = call; (mockWorkApi.getPredefinedQueryResults as jest.Mock).mockResolvedValue([ { id: "assignedtome", name: "Assigned to me", url: "https://dev.azure.com/org/project/_apis/work/predefinedQueries/assignedtome", webUrl: "https://dev.azure.com/org/project/project/_workitems/assignedtome", hasMore: false, results: [ { id: 115784, url: "https://dev.azure.com/org/_apis/wit/workItems/115784", }, { id: 115794, url: "https://dev.azure.com/org/_apis/wit/workItems/115794", }, { id: 115792, url: "https://dev.azure.com/org/_apis/wit/workItems/115792", }, ], }, ]); const params = { project: "Contoso", type: "assignedtome", top: 10, includeCompleted: false, }; const result = await handler(params); expect(mockWorkApi.getPredefinedQueryResults).toHaveBeenCalledWith(params.project, params.type, params.top, params.includeCompleted); expect(result.content[0].text).toBe( JSON.stringify( [ { id: "assignedtome", name: "Assigned to me", url: "https://dev.azure.com/org/project/_apis/work/predefinedQueries/assignedtome", webUrl: "https://dev.azure.com/org/project/project/_workitems/assignedtome", hasMore: false, results: [ { id: 115784, url: "https://dev.azure.com/org/_apis/wit/workItems/115784", }, { id: 115794, url: "https://dev.azure.com/org/_apis/wit/workItems/115794", }, { id: 115792, url: "https://dev.azure.com/org/_apis/wit/workItems/115792", }, ], }, ], null, 2 ) ); }); }); describe("getWorkItemsBatch tool", () => { it("should call workItemApi.getWorkItemsBatch API with the correct parameters and return the expected result", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids"); if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue([_mockWorkItems]); const params = { ids: [297, 299, 300], project: "Contoso", }; const result = await handler(params); expect(mockWorkItemTrackingApi.getWorkItemsBatch).toHaveBeenCalledWith( { ids: params.ids, fields: ["System.Id", "System.WorkItemType", "System.Title", "System.State", "System.Parent", "System.Tags", "Microsoft.VSTS.Common.StackRank", "System.AssignedTo"], }, params.project ); expect(result.content[0].text).toBe(JSON.stringify([_mockWorkItems], null, 2)); }); it("should call workItemApi.getWorkItemsBatch API with custom fields when fields parameter is provided", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids"); if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered"); const [, , , handler] = call; const mockWorkItemsWithCustomFields = [ { id: 297, fields: { "System.Id": 297, "System.Title": "Test Work Item", }, }, ]; (mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue(mockWorkItemsWithCustomFields); const params = { ids: [297, 299, 300], project: "Contoso", fields: ["System.Id", "System.Title"], }; const result = await handler(params); expect(mockWorkItemTrackingApi.getWorkItemsBatch).toHaveBeenCalledWith( { ids: params.ids, fields: params.fields, }, params.project ); expect(result.content[0].text).toBe(JSON.stringify(mockWorkItemsWithCustomFields, null, 2)); }); it("should use default fields when an empty fields array is provided", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids"); if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue([_mockWorkItems]); const params = { ids: [297, 299, 300], project: "Contoso", fields: [], // Empty array should trigger default fields }; const result = await handler(params); expect(mockWorkItemTrackingApi.getWorkItemsBatch).toHaveBeenCalledWith( { ids: params.ids, fields: ["System.Id", "System.WorkItemType", "System.Title", "System.State", "System.Parent", "System.Tags", "Microsoft.VSTS.Common.StackRank", "System.AssignedTo"], }, params.project ); expect(result.content[0].text).toBe(JSON.stringify([_mockWorkItems], null, 2)); }); it("should transform System.AssignedTo object to formatted string", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids"); if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered"); const [, , , handler] = call; // Mock work items with System.AssignedTo as objects const mockWorkItemsWithAssignedTo = [ { id: 297, fields: { "System.Id": 297, "System.WorkItemType": "Bug", "System.Title": "Test Bug", "System.AssignedTo": { displayName: "John Doe", uniqueName: "john.doe@example.com", id: "12345", }, }, }, { id: 298, fields: { "System.Id": 298, "System.WorkItemType": "User Story", "System.Title": "Test Story", "System.AssignedTo": { displayName: "Jane Smith", uniqueName: "jane.smith@example.com", id: "67890", }, }, }, ]; (mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue(mockWorkItemsWithAssignedTo); const params = { ids: [297, 298], project: "Contoso", }; const result = await handler(params); // Parse the returned JSON to verify transformation const resultData = JSON.parse(result.content[0].text); expect(resultData[0].fields["System.AssignedTo"]).toBe("John Doe <john.doe@example.com>"); expect(resultData[1].fields["System.AssignedTo"]).toBe("Jane Smith <jane.smith@example.com>"); }); it("should handle System.AssignedTo with only displayName", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids"); if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered"); const [, , , handler] = call; const mockWorkItemsWithPartialAssignedTo = [ { id: 297, fields: { "System.Id": 297, "System.WorkItemType": "Bug", "System.Title": "Test Bug", "System.AssignedTo": { displayName: "John Doe", id: "12345", }, }, }, ]; (mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue(mockWorkItemsWithPartialAssignedTo); const params = { ids: [297], project: "Contoso", }; const result = await handler(params); const resultData = JSON.parse(result.content[0].text); expect(resultData[0].fields["System.AssignedTo"]).toBe("John Doe <>"); }); it("should handle System.AssignedTo with only uniqueName", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids"); if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered"); const [, , , handler] = call; const mockWorkItemsWithPartialAssignedTo = [ { id: 297, fields: { "System.Id": 297, "System.WorkItemType": "Bug", "System.Title": "Test Bug", "System.AssignedTo": { uniqueName: "john.doe@example.com", id: "12345", }, }, }, ]; (mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue(mockWorkItemsWithPartialAssignedTo); const params = { ids: [297], project: "Contoso", }; const result = await handler(params); const resultData = JSON.parse(result.content[0].text); expect(resultData[0].fields["System.AssignedTo"]).toBe("<john.doe@example.com>"); }); it("should not transform System.AssignedTo if it's not an object", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids"); if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered"); const [, , , handler] = call; const mockWorkItemsWithStringAssignedTo = [ { id: 297, fields: { "System.Id": 297, "System.WorkItemType": "Bug", "System.Title": "Test Bug", "System.AssignedTo": "Already a string", }, }, ]; (mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue(mockWorkItemsWithStringAssignedTo); const params = { ids: [297], project: "Contoso", }; const result = await handler(params); const resultData = JSON.parse(result.content[0].text); expect(resultData[0].fields["System.AssignedTo"]).toBe("Already a string"); }); it("should handle work items without System.AssignedTo field", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids"); if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered"); const [, , , handler] = call; const mockWorkItemsWithoutAssignedTo = [ { id: 297, fields: { "System.Id": 297, "System.WorkItemType": "Bug", "System.Title": "Test Bug", }, }, ]; (mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue(mockWorkItemsWithoutAssignedTo); const params = { ids: [297], project: "Contoso", }; const result = await handler(params); const resultData = JSON.parse(result.content[0].text); expect(resultData[0].fields["System.AssignedTo"]).toBeUndefined(); }); it("should handle null or undefined workitems response", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids"); if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue(null); const params = { ids: [297], project: "Contoso", }; const result = await handler(params); expect(result.content[0].text).toBe(JSON.stringify(null, null, 2)); }); it("should transform all user fields to formatted strings", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_batch_by_ids"); if (!call) throw new Error("wit_get_work_items_batch_by_ids tool not registered"); const [, , , handler] = call; // Mock work items with all user fields as objects const mockWorkItemsWithUserFields = [ { id: 297, fields: { "System.Id": 297, "System.WorkItemType": "Bug", "System.Title": "Test Bug", "System.AssignedTo": { displayName: "John Doe", uniqueName: "john.doe@example.com", id: "12345", }, "System.CreatedBy": { displayName: "Jane Smith", uniqueName: "jane.smith@example.com", id: "67890", }, "System.ChangedBy": { displayName: "Bob Johnson", uniqueName: "bob.johnson@example.com", id: "11111", }, "System.AuthorizedAs": { displayName: "Alice Brown", uniqueName: "alice.brown@example.com", id: "22222", }, "Microsoft.VSTS.Common.ActivatedBy": { displayName: "Charlie Wilson", uniqueName: "charlie.wilson@example.com", id: "33333", }, "Microsoft.VSTS.Common.ResolvedBy": { displayName: "Diana Clark", uniqueName: "diana.clark@example.com", id: "44444", }, "Microsoft.VSTS.Common.ClosedBy": { displayName: "Edward Davis", uniqueName: "edward.davis@example.com", id: "55555", }, }, }, ]; (mockWorkItemTrackingApi.getWorkItemsBatch as jest.Mock).mockResolvedValue(mockWorkItemsWithUserFields); const params = { ids: [297], project: "Contoso", }; const result = await handler(params); // Parse the returned JSON to verify transformation const resultData = JSON.parse(result.content[0].text); // Verify that all user fields are transformed to formatted strings expect(resultData[0].fields["System.AssignedTo"]).toBe("John Doe <john.doe@example.com>"); expect(resultData[0].fields["System.CreatedBy"]).toBe("Jane Smith <jane.smith@example.com>"); expect(resultData[0].fields["System.ChangedBy"]).toBe("Bob Johnson <bob.johnson@example.com>"); expect(resultData[0].fields["System.AuthorizedAs"]).toBe("Alice Brown <alice.brown@example.com>"); expect(resultData[0].fields["Microsoft.VSTS.Common.ActivatedBy"]).toBe("Charlie Wilson <charlie.wilson@example.com>"); expect(resultData[0].fields["Microsoft.VSTS.Common.ResolvedBy"]).toBe("Diana Clark <diana.clark@example.com>"); expect(resultData[0].fields["Microsoft.VSTS.Common.ClosedBy"]).toBe("Edward Davis <edward.davis@example.com>"); }); }); describe("get_work_item tool", () => { it("should call workItemApi.getWorkItem API with the correct parameters and return the expected result", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_item"); if (!call) throw new Error("wit_get_work_item tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.getWorkItem as jest.Mock).mockResolvedValue([_mockWorkItem]); const params = { id: 12, fields: undefined, asOf: undefined, expand: "none", project: "Contoso", }; const result = await handler(params); expect(mockWorkItemTrackingApi.getWorkItem).toHaveBeenCalledWith(params.id, params.fields, params.asOf, params.expand, params.project); expect(result.content[0].text).toBe(JSON.stringify([_mockWorkItem], null, 2)); }); }); describe("list_work_item_comments tool", () => { it("should call workItemApi.getComments API with the correct parameters and return the expected result", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_list_work_item_comments"); if (!call) throw new Error("wit_list_work_item_comments tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.getComments as jest.Mock).mockResolvedValue([_mockWorkItemComments]); const params = { project: "Contoso", workItemId: 299, top: 10, }; const result = await handler(params); expect(mockWorkItemTrackingApi.getComments).toHaveBeenCalledWith(params.project, params.workItemId, params.top); expect(result.content[0].text).toBe(JSON.stringify([_mockWorkItemComments], null, 2)); }); }); describe("add_work_item_comment tool", () => { it("should call Add Work Item Comments API with the correct parameters and return the expected result with no format specified", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_work_item_comment"); if (!call) throw new Error("wit_add_work_item_comment tool not registered"); const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); // Mock fetch for the API call const mockFetch = jest.fn().mockResolvedValue({ ok: true, text: () => Promise.resolve(JSON.stringify(_mockWorkItemComment)), }); global.fetch = mockFetch; const params = { comment: "hello world!", project: "Contoso", workItemId: 299, }; const result = await handler(params); expect(mockFetch).toHaveBeenCalledWith( "https://dev.azure.com/contoso/Contoso/_apis/wit/workItems/299/comments?format=1&api-version=7.2-preview.4", expect.objectContaining({ method: "POST", headers: expect.objectContaining({ "Authorization": "Bearer fake-token", "Content-Type": "application/json", }), }) ); expect(result.content[0].text).toBe(JSON.stringify(_mockWorkItemComment)); }); it("should call Add Work Item Comments API with the correct parameters and return the expected result with markdown format", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_work_item_comment"); if (!call) throw new Error("wit_add_work_item_comment tool not registered"); const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); // Mock fetch for the API call const mockFetch = jest.fn().mockResolvedValue({ ok: true, text: () => Promise.resolve(JSON.stringify(_mockWorkItemComment)), }); global.fetch = mockFetch; const params = { comment: "hello world!", project: "Contoso", workItemId: 299, format: "markdown", }; const result = await handler(params); expect(mockFetch).toHaveBeenCalledWith( "https://dev.azure.com/contoso/Contoso/_apis/wit/workItems/299/comments?format=0&api-version=7.2-preview.4", expect.objectContaining({ method: "POST", headers: expect.objectContaining({ "Authorization": "Bearer fake-token", "Content-Type": "application/json", }), }) ); expect(result.content[0].text).toBe(JSON.stringify(_mockWorkItemComment)); }); it("should handle fetch failure response", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_work_item_comment"); if (!call) throw new Error("wit_add_work_item_comment tool not registered"); const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); // Mock fetch for the API call const mockFetch = jest.fn().mockResolvedValue({ ok: false, statusText: "Not Found", }); global.fetch = mockFetch; const params = { comment: "hello world!", project: "Contoso", workItemId: 299, }; await expect(handler(params)).rejects.toThrow("Failed to add a work item comment: Not Found"); }); }); describe("link_work_item_to_pull_request tool", () => { it("should call workItemApi.updateWorkItem API with the correct parameters and return the expected result", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_link_work_item_to_pull_request"); if (!call) throw new Error("wit_link_work_item_to_pull_request tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.updateWorkItem as jest.Mock).mockResolvedValue([_mockWorkItem]); const params = { projectId: "6bfde89e-b22e-422e-814a-e8db432f5a58", repositoryId: 12345, pullRequestId: 67890, workItemId: 131489, }; const artifactPathValue = `${params.projectId}/${params.repositoryId}/${params.pullRequestId}`; const vstfsUrl = `vstfs:///Git/PullRequestId/${encodeURIComponent(artifactPathValue)}`; const document = [ { op: "add", path: "/relations/-", value: { rel: "ArtifactLink", url: vstfsUrl, attributes: { name: "Pull Request", }, }, }, ]; const result = await handler(params); expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith({}, document, params.workItemId, params.projectId); expect(result.content[0].text).toBe( JSON.stringify( { workItemId: 131489, pullRequestId: 67890, success: true, }, null, 2 ) ); }); it("should handle errors from updateWorkItem and return a descriptive error", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_link_work_item_to_pull_request"); if (!call) throw new Error("wit_link_work_item_to_pull_request tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.updateWorkItem as jest.Mock).mockRejectedValue(new Error("API failure")); const params = { projectId: "6bfde89e-b22e-422e-814a-e8db432f5a58", repositoryId: 12345, pullRequestId: 67890, workItemId: 131489, }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("API failure"); }); it("should encode special characters in projectId and repositoryId for vstfsUrl", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_link_work_item_to_pull_request"); if (!call) throw new Error("wit_link_work_item_to_pull_request tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.updateWorkItem as jest.Mock).mockResolvedValue([_mockWorkItem]); const params = { projectId: "6bfde89e-b22e-422e-814a-e8db432f5a58", repositoryId: "repo/with/slash", pullRequestId: 67890, workItemId: 131489, }; const artifactPathValue = `${params.projectId}/${params.repositoryId}/${params.pullRequestId}`; const vstfsUrl = `vstfs:///Git/PullRequestId/${encodeURIComponent(artifactPathValue)}`; const document = [ { op: "add", path: "/relations/-", value: { rel: "ArtifactLink", url: vstfsUrl, attributes: { name: "Pull Request", }, }, }, ]; await handler(params); expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith({}, document, params.workItemId, params.projectId); }); it("should use pullRequestProjectId instead of projectId when provided", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_link_work_item_to_pull_request"); if (!call) throw new Error("wit_link_work_item_to_pull_request tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.updateWorkItem as jest.Mock).mockResolvedValue([_mockWorkItem]); const params = { projectId: "work-item-project-id", repositoryId: "repo-123", pullRequestId: 67890, workItemId: 131489, pullRequestProjectId: "different-project-id", }; // Should use pullRequestProjectId instead of projectId const artifactPathValue = `${params.pullRequestProjectId}/${params.repositoryId}/${params.pullRequestId}`; const vstfsUrl = `vstfs:///Git/PullRequestId/${encodeURIComponent(artifactPathValue)}`; const document = [ { op: "add", path: "/relations/-", value: { rel: "ArtifactLink", url: vstfsUrl, attributes: { name: "Pull Request", }, }, }, ]; await handler(params); // Note: Work item should still be updated in the original project expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith({}, document, params.workItemId, params.projectId); }); it("should fall back to projectId when pullRequestProjectId is empty", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_link_work_item_to_pull_request"); if (!call) throw new Error("wit_link_work_item_to_pull_request tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.updateWorkItem as jest.Mock).mockResolvedValue([_mockWorkItem]); // Testing with empty string for pullRequestProjectId const params = { projectId: "work-item-project-id", repositoryId: "repo-123", pullRequestId: 67890, workItemId: 131489, pullRequestProjectId: "", }; // Should use projectId since pullRequestProjectId is empty const artifactPathValue = `${params.projectId}/${params.repositoryId}/${params.pullRequestId}`; const vstfsUrl = `vstfs:///Git/PullRequestId/${encodeURIComponent(artifactPathValue)}`; const document = [ { op: "add", path: "/relations/-", value: { rel: "ArtifactLink", url: vstfsUrl, attributes: { name: "Pull Request", }, }, }, ]; await handler(params); expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith({}, document, params.workItemId, params.projectId); }); it("should handle link_work_item_to_pull_request unknown error type", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_link_work_item_to_pull_request"); if (!call) throw new Error("wit_link_work_item_to_pull_request tool not registered"); const [, , , handler] = call; // Simulate an unknown error type (not an Error instance) (mockWorkItemTrackingApi.updateWorkItem as jest.Mock).mockRejectedValue("String error"); const params = { projectId: "6bfde89e-b22e-422e-814a-e8db432f5a58", repositoryId: "repo-123", pullRequestId: 42, workItemId: 1, pullRequestProjectId: "other-project", }; const result = await handler(params); expect(result.content[0].text).toBe("Error linking work item to pull request: Unknown error occurred"); expect(result.isError).toBe(true); }); }); describe("get_work_items_for_iteration tool", () => { it("should call workApi.getIterationWorkItems API with the correct parameters and return the expected result", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_items_for_iteration"); if (!call) throw new Error("wit_get_work_items_for_iterationt tool not registered"); const [, , , handler] = call; (mockWorkApi.getIterationWorkItems as jest.Mock).mockResolvedValue([_mockWorkItemsForIteration]); const params = { project: "Contoso", team: "Fabrikam", iterationId: "6bfde89e-b22e-422e-814a-e8db432f5a58", }; const result = await handler(params); expect(mockWorkApi.getIterationWorkItems).toHaveBeenCalledWith( { project: params.project, team: params.team, }, params.iterationId ); expect(result.content[0].text).toBe(JSON.stringify([_mockWorkItemsForIteration], null, 2)); }); }); describe("update_work_item tool", () => { it("should call workItemApi.updateWorkItem API with the correct parameters and return the expected result", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_update_work_item"); if (!call) throw new Error("wit_update_work_item tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.updateWorkItem as jest.Mock).mockResolvedValue([_mockWorkItem]); const params = { id: 131489, updates: [ { op: "Add", path: "/fields/System.Title", value: "Updated Sample Task", }, { op: "Replace", path: "/fields/System.Description", value: "Updated Description", }, ], }; const result = await handler(params); // In line 456-471, the operation is actually not transformed to lowercase // despite the comment saying otherwise, so we use the original value const expectedUpdates = params.updates; expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith(null, expectedUpdates, params.id); expect(result.content[0].text).toBe(JSON.stringify([_mockWorkItem], null, 2)); }); }); describe("get_work_item_type tool", () => { it("should call workItemApi.getWorkItemType API with the correct parameters and return the expected result", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_work_item_type"); if (!call) throw new Error("wit_get_work_item_type tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.getWorkItemType as jest.Mock).mockResolvedValue([_mockWorkItemType]); const params = { project: "Contoso", workItemType: "Bug", }; const result = await handler(params); expect(mockWorkItemTrackingApi.getWorkItemType).toHaveBeenCalledWith(params.project, params.workItemType); expect(result.content[0].text).toBe(JSON.stringify([_mockWorkItemType], null, 2)); }); }); describe("create_work_item tool", () => { it("should call workItemApi.createWorkItem API with the correct parameters and return the expected result", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_create_work_item"); if (!call) throw new Error("wit_create_work_item tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.createWorkItem as jest.Mock).mockResolvedValue(_mockWorkItem); const params = { project: "Contoso", workItemType: "Task", fields: [ { name: "System.Title", value: "Hello World!" }, { name: "System.Description", value: "This is a sample task" }, { name: "System.AreaPath", value: "Contoso\\Development" }, ], }; const expectedDocument = [ { op: "add", path: "/fields/System.Title", value: "Hello World!" }, { op: "add", path: "/fields/System.Description", value: "This is a sample task" }, { op: "add", path: "/fields/System.AreaPath", value: "Contoso\\Development" }, ]; const result = await handler(params); expect(mockWorkItemTrackingApi.createWorkItem).toHaveBeenCalledWith(null, expectedDocument, params.project, params.workItemType); expect(result.content[0].text).toBe(JSON.stringify(_mockWorkItem, null, 2)); }); it("should handle Markdown format for long fields", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_create_work_item"); if (!call) throw new Error("wit_create_work_item tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.createWorkItem as jest.Mock).mockResolvedValue(_mockWorkItem); const longDescription = "This is a very long description that is definitely more than 50 characters long and should trigger Markdown formatting"; const params = { project: "Contoso", workItemType: "Task", fields: [ { name: "System.Title", value: "Hello World!" }, { name: "System.Description", value: longDescription, format: "Markdown" }, ], }; const expectedDocument = [ { op: "add", path: "/fields/System.Title", value: "Hello World!" }, { op: "add", path: "/fields/System.Description", value: longDescription }, { op: "add", path: "/multilineFieldsFormat/System.Description", value: "Markdown" }, ]; const result = await handler(params); expect(mockWorkItemTrackingApi.createWorkItem).toHaveBeenCalledWith(null, expectedDocument, params.project, params.workItemType); expect(result.content[0].text).toBe(JSON.stringify(_mockWorkItem, null, 2)); }); it("should handle null response from createWorkItem", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_create_work_item"); if (!call) throw new Error("wit_create_work_item tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.createWorkItem as jest.Mock).mockResolvedValue(null); const params = { project: "Contoso", workItemType: "Task", fields: [{ name: "System.Title", value: "Test" }], }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("Work item was not created"); }); it("should handle errors from createWorkItem", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_create_work_item"); if (!call) throw new Error("wit_create_work_item tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.createWorkItem as jest.Mock).mockRejectedValue(new Error("API failure")); const params = { project: "Contoso", workItemType: "Task", fields: [{ name: "System.Title", value: "Test" }], }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("Error creating work item: API failure"); }); it("should handle unknown error types", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_create_work_item"); if (!call) throw new Error("wit_create_work_item tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.createWorkItem as jest.Mock).mockRejectedValue("String error"); const params = { project: "Contoso", workItemType: "Task", fields: [{ name: "System.Title", value: "Test" }], }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("Error creating work item: Unknown error occurred"); }); }); describe("get_query tool", () => { it("should call workItemApi.getQuery API with the correct parameters and return the expected result", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_query"); if (!call) throw new Error("wit_get_query tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.getQuery as jest.Mock).mockResolvedValue([_mockQuery]); const params = { project: "Contoso", query: "342f0f44-4069-46b1-a940-3d0468979ceb", expand: "None", depth: 1, includeDeleted: false, useIsoDateFormat: false, }; const result = await handler(params); expect(mockWorkItemTrackingApi.getQuery).toHaveBeenCalledWith(params.project, params.query, QueryExpand.None, params.depth, params.includeDeleted, params.useIsoDateFormat); expect(result.content[0].text).toBe(JSON.stringify([_mockQuery], null, 2)); }); }); describe("get_query_results_by_id tool", () => { it("should call workItemApi.getQueryById API with the correct parameters and return the expected result", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_get_query_results_by_id"); if (!call) throw new Error("wit_get_query_results_by_id tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.queryById as jest.Mock).mockResolvedValue([_mockQueryResults]); const params = { id: "342f0f44-4069-46b1-a940-3d0468979ceb", project: "Contoso", team: "Fabrikam", timePrecision: false, top: 50, }; const result = await handler(params); expect(mockWorkItemTrackingApi.queryById).toHaveBeenCalledWith(params.id, { project: params.project, team: params.team }, params.timePrecision, params.top); expect(result.content[0].text).toBe(JSON.stringify([_mockQueryResults], null, 2)); }); }); describe("getLinkTypeFromName function coverage", () => { it("should handle all link types through work_items_link tool", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_work_items_link"); if (!call) throw new Error("wit_work_items_link tool not registered"); const [, , , handler] = call; // Mock the connection and serverUrl mockConnection.serverUrl = "https://dev.azure.com/contoso"; // Mock tokenProvider for this test (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); // Mock fetch for successful response global.fetch = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({ success: true }), }); // Test different link types to cover all branches in getLinkTypeFromName const linkTypes = ["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by"]; for (const linkType of linkTypes) { const params = { project: "TestProject", updates: [ { id: 1, linkToId: 2, type: linkType as "parent" | "child" | "duplicate" | "duplicate of" | "related" | "successor" | "predecessor" | "tested by" | "tests" | "affects" | "affected by", comment: "Test comment", }, ], }; await handler(params); } expect(fetch).toHaveBeenCalled(); }); it("should throw error for unknown link type", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_work_items_link"); if (!call) throw new Error("wit_work_items_link tool not registered"); const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; const params = { project: "TestProject", updates: [ { id: 1, linkToId: 2, type: "unknown_type", comment: "Test comment", }, ], }; await expect(handler(params)).rejects.toThrow("Unknown link type: unknown_type"); }); }); describe("update_work_items_batch tool", () => { it("should update work items in batch successfully", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_update_work_items_batch"); if (!call) throw new Error("wit_update_work_items_batch tool not registered"); const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); global.fetch = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue([{ id: 1, success: true }]), }); const params = { updates: [ { op: "replace", id: 1, path: "/fields/System.Title", value: "Updated Title", }, { op: "add", id: 2, path: "/fields/System.Description", value: "New Description", }, ], }; const result = await handler(params); // This verifies that the updates are grouped by work item ID as implemented in line 643 const expectedBody = [ { method: "PATCH", uri: "/_apis/wit/workitems/1?api-version=5.0", headers: { "Content-Type": "application/json-patch+json" }, body: [{ op: "replace", path: "/fields/System.Title", value: "Updated Title" }], }, { method: "PATCH", uri: "/_apis/wit/workitems/2?api-version=5.0", headers: { "Content-Type": "application/json-patch+json" }, body: [{ op: "add", path: "/fields/System.Description", value: "New Description" }], }, ]; expect(fetch).toHaveBeenCalledWith( "https://dev.azure.com/contoso/_apis/wit/$batch?api-version=5.0", expect.objectContaining({ method: "PATCH", headers: expect.objectContaining({ "Authorization": "Bearer fake-token", "Content-Type": "application/json", }), body: JSON.stringify(expectedBody), }) ); expect(result.content[0].text).toBe(JSON.stringify([{ id: 1, success: true }], null, 2)); }); it("should handle Markdown format for large text fields", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_update_work_items_batch"); if (!call) throw new Error("wit_update_work_items_batch tool not registered"); const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); global.fetch = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue([{ id: 1, success: true }]), }); const longDescription = "This is a very long description that is definitely more than 50 characters long and should trigger Markdown formatting"; const params = { updates: [ { op: "Add", // Match the capitalization in the implementation id: 1, path: "/fields/System.Description", value: longDescription, format: "Markdown", }, { op: "Add", // Match the capitalization in the implementation id: 1, path: "/fields/System.Title", value: "Simple Title", }, ], }; const result = await handler(params); // This verifies that the Markdown format is applied for the long text field as implemented in line 643 const expectedBody = [ { method: "PATCH", uri: "/_apis/wit/workitems/1?api-version=5.0", headers: { "Content-Type": "application/json-patch+json" }, body: [ { op: "Add", path: "/fields/System.Description", value: longDescription }, { op: "Add", path: "/fields/System.Title", value: "Simple Title" }, { op: "Add", path: "/multilineFieldsFormat/System.Description", value: "Markdown", }, ], }, ]; expect(fetch).toHaveBeenCalledWith( "https://dev.azure.com/contoso/_apis/wit/$batch?api-version=5.0", expect.objectContaining({ method: "PATCH", headers: expect.objectContaining({ "Authorization": "Bearer fake-token", "Content-Type": "application/json", }), body: JSON.stringify(expectedBody), }) ); expect(result.content[0].text).toBe(JSON.stringify([{ id: 1, success: true }], null, 2)); }); it("should handle batch update failure", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_update_work_items_batch"); if (!call) throw new Error("wit_update_work_items_batch tool not registered"); const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); global.fetch = jest.fn().mockResolvedValue({ ok: false, statusText: "Bad Request", }); const params = { updates: [ { op: "replace", id: 1, path: "/fields/System.Title", value: "Updated Title", }, ], }; await expect(handler(params)).rejects.toThrow("Failed to update work items in batch: Bad Request"); }); }); describe("work_items_link tool", () => { it("should link work items successfully", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_work_items_link"); if (!call) throw new Error("wit_work_items_link tool not registered"); const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); global.fetch = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue([{ id: 1, success: true }]), }); const params = { project: "TestProject", updates: [ { id: 1, linkToId: 2, type: "related", comment: "Related work item", }, ], }; const result = await handler(params); expect(fetch).toHaveBeenCalledWith( "https://dev.azure.com/contoso/_apis/wit/$batch?api-version=5.0", expect.objectContaining({ method: "PATCH", headers: expect.objectContaining({ "Authorization": "Bearer fake-token", "Content-Type": "application/json", }), }) ); expect(result.content[0].text).toBe(JSON.stringify([{ id: 1, success: true }], null, 2)); }); it("should handle linking failure", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_work_items_link"); if (!call) throw new Error("wit_work_items_link tool not registered"); const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); global.fetch = jest.fn().mockResolvedValue({ ok: false, statusText: "Unauthorized", }); const params = { project: "TestProject", updates: [ { id: 1, linkToId: 2, type: "related", comment: "Related work item", }, ], }; await expect(handler(params)).rejects.toThrow("Failed to update work items in batch: Unauthorized"); }); }); describe("work_item_unlink tool", () => { it("should unlink work items successfully", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_work_item_unlink"); if (!call) throw new Error("wit_work_item_unlink tool not registered"); const [, , , handler] = call; // Mock work item with relations const mockWorkItemWithRelations = { id: 1, relations: [ { rel: "System.LinkTypes.Related", url: "https://dev.azure.com/contoso/_apis/wit/workItems/2", attributes: { isLocked: false, name: "Related" }, }, { rel: "System.LinkTypes.Related", url: "https://dev.azure.com/contoso/_apis/wit/workItems/3", attributes: { isLocked: false, name: "Related" }, }, ], }; const mockUpdatedWorkItem = { id: 1, rev: 5, relations: [ { rel: "System.LinkTypes.Related", url: "https://dev.azure.com/contoso/_apis/wit/workItems/3", attributes: { isLocked: false, name: "Related" }, }, ], }; (mockWorkItemTrackingApi.getWorkItem as jest.Mock).mockResolvedValue(mockWorkItemWithRelations); (mockWorkItemTrackingApi.updateWorkItem as jest.Mock).mockResolvedValue(mockUpdatedWorkItem); const params = { project: "TestProject", id: 1, type: "related", url: "https://dev.azure.com/contoso/_apis/wit/workItems/2", }; const result = await handler(params); expect(mockWorkItemTrackingApi.getWorkItem).toHaveBeenCalledWith(1, undefined, undefined, 1, "TestProject"); expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith(null, [{ op: "remove", path: "/relations/0" }], 1, "TestProject"); expect(result.content[0].text).toContain("Removed 1 link(s) of type 'related':"); expect(result.content[0].text).toContain("System.LinkTypes.Related"); expect(result.content[0].text).toContain("Updated work item result:"); }); it("should unlink all links of a specific type when no URL is provided", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_work_item_unlink"); if (!call) throw new Error("wit_work_item_unlink tool not registered"); const [, , , handler] = call; // Mock work item with multiple related links const mockWorkItemWithRelations = { id: 1, relations: [ { rel: "System.LinkTypes.Related", url: "https://dev.azure.com/contoso/_apis/wit/workItems/2", attributes: { isLocked: false, name: "Related" }, }, { rel: "System.LinkTypes.Related", url: "https://dev.azure.com/contoso/_apis/wit/workItems/3", attributes: { isLocked: false, name: "Related" }, }, { rel: "System.LinkTypes.Hierarchy-Forward", url: "https://dev.azure.com/contoso/_apis/wit/workItems/4", attributes: { isLocked: false, name: "Child" }, }, ], }; const mockUpdatedWorkItem = { id: 1, rev: 6, relations: [ { rel: "System.LinkTypes.Hierarchy-Forward", url: "https://dev.azure.com/contoso/_apis/wit/workItems/4", attributes: { isLocked: false, name: "Child" }, }, ], }; (mockWorkItemTrackingApi.getWorkItem as jest.Mock).mockResolvedValue(mockWorkItemWithRelations); (mockWorkItemTrackingApi.updateWorkItem as jest.Mock).mockResolvedValue(mockUpdatedWorkItem); const params = { project: "TestProject", id: 1, type: "related", }; const result = await handler(params); expect(mockWorkItemTrackingApi.getWorkItem).toHaveBeenCalledWith(1, undefined, undefined, 1, "TestProject"); expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith( null, [ { op: "remove", path: "/relations/1" }, { op: "remove", path: "/relations/0" }, ], 1, "TestProject" ); expect(result.content[0].text).toContain("Removed 2 link(s) of type 'related':"); expect(result.content[0].text).toContain("System.LinkTypes.Related"); }); it("should handle artifact link removal", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_work_item_unlink"); if (!call) throw new Error("wit_work_item_unlink tool not registered"); const [, , , handler] = call; const mockWorkItemWithRelations = { id: 1, relations: [ { rel: "ArtifactLink", url: "vstfs:///Git/Ref/project%2Frepo%2Fbranch", attributes: { name: "Branch" }, }, { rel: "System.LinkTypes.Related", url: "https://dev.azure.com/contoso/_apis/wit/workItems/2", attributes: { isLocked: false, name: "Related" }, }, ], }; const mockUpdatedWorkItem = { id: 1, rev: 7, relations: [ { rel: "System.LinkTypes.Related", url: "https://dev.azure.com/contoso/_apis/wit/workItems/2", attributes: { isLocked: false, name: "Related" }, }, ], }; (mockWorkItemTrackingApi.getWorkItem as jest.Mock).mockResolvedValue(mockWorkItemWithRelations); (mockWorkItemTrackingApi.updateWorkItem as jest.Mock).mockResolvedValue(mockUpdatedWorkItem); const params = { project: "TestProject", id: 1, type: "artifact", url: "vstfs:///Git/Ref/project%2Frepo%2Fbranch", }; const result = await handler(params); expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith(null, [{ op: "remove", path: "/relations/0" }], 1, "TestProject"); expect(result.content[0].text).toContain("Removed 1 link(s) of type 'artifact':"); expect(result.content[0].text).toContain("ArtifactLink"); }); it("should handle when no matching relations are found", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_work_item_unlink"); if (!call) throw new Error("wit_work_item_unlink tool not registered"); const [, , , handler] = call; const mockWorkItemWithRelations = { id: 1, relations: [ { rel: "System.LinkTypes.Hierarchy-Forward", url: "https://dev.azure.com/contoso/_apis/wit/workItems/4", attributes: { isLocked: false, name: "Child" }, }, ], }; (mockWorkItemTrackingApi.getWorkItem as jest.Mock).mockResolvedValue(mockWorkItemWithRelations); const params = { project: "TestProject", id: 1, type: "related", url: "https://dev.azure.com/contoso/_apis/wit/workItems/999", }; const result = await handler(params); expect(mockWorkItemTrackingApi.updateWorkItem).not.toHaveBeenCalled(); expect(result.content[0].text).toContain("No matching relations found for link type 'related' and URL 'https://dev.azure.com/contoso/_apis/wit/workItems/999'"); expect(result.isError).toBe(true); }); it("should handle updateWorkItem API failure", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_work_item_unlink"); if (!call) throw new Error("wit_work_item_unlink tool not registered"); const [, , , handler] = call; const mockWorkItemWithRelations = { id: 1, relations: [ { rel: "System.LinkTypes.Related", url: "https://dev.azure.com/contoso/_apis/wit/workItems/2", attributes: { isLocked: false, name: "Related" }, }, ], }; (mockWorkItemTrackingApi.getWorkItem as jest.Mock).mockResolvedValue(mockWorkItemWithRelations); (mockWorkItemTrackingApi.updateWorkItem as jest.Mock).mockRejectedValue(new Error("Update failed")); const params = { project: "TestProject", id: 1, type: "related", url: "https://dev.azure.com/contoso/_apis/wit/workItems/2", }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("Error unlinking work item: Update failed"); }); it("should handle getWorkItem API failure", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_work_item_unlink"); if (!call) throw new Error("wit_work_item_unlink tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.getWorkItem as jest.Mock).mockRejectedValue(new Error("Work item not found")); const params = { project: "TestProject", id: 999, type: "related", }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("Error unlinking work item: Work item not found"); }); it("should handle work items with no relations", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_work_item_unlink"); if (!call) throw new Error("wit_work_item_unlink tool not registered"); const [, , , handler] = call; const mockWorkItemWithNoRelations = { id: 1, relations: null, }; (mockWorkItemTrackingApi.getWorkItem as jest.Mock).mockResolvedValue(mockWorkItemWithNoRelations); const params = { project: "TestProject", id: 1, type: "related", }; const result = await handler(params); expect(result.content[0].text).toContain("No matching relations found for link type 'related'"); expect(result.isError).toBe(true); }); it("should handle specific URL matching correctly", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_work_item_unlink"); if (!call) throw new Error("wit_work_item_unlink tool not registered"); const [, , , handler] = call; const mockWorkItemWithRelations = { id: 1, relations: [ { rel: "System.LinkTypes.Related", url: "https://dev.azure.com/contoso/_apis/wit/workItems/2", attributes: { isLocked: false, name: "Related" }, }, { rel: "System.LinkTypes.Hierarchy-Forward", url: "https://dev.azure.com/contoso/_apis/wit/workItems/3", attributes: { isLocked: false, name: "Child" }, }, { rel: "ArtifactLink", url: "vstfs:///Git/Ref/project%2Frepo%2Fbranch", attributes: { name: "Branch" }, }, ], }; const mockUpdatedWorkItem = { id: 1, rev: 8, }; (mockWorkItemTrackingApi.getWorkItem as jest.Mock).mockResolvedValue(mockWorkItemWithRelations); (mockWorkItemTrackingApi.updateWorkItem as jest.Mock).mockResolvedValue(mockUpdatedWorkItem); const params = { project: "TestProject", id: 1, type: "related", url: "https://dev.azure.com/contoso/_apis/wit/workItems/2", }; const result = await handler(params); // Should remove only the matching relation at index 0 expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith(null, [{ op: "remove", path: "/relations/0" }], 1, "TestProject"); expect(result.content[0].text).toContain("Removed 1 link(s) of type 'related':"); expect(result.content[0].text).toContain("System.LinkTypes.Related"); }); it("should throw error for unknown link type in work_item_unlink", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_work_item_unlink"); if (!call) throw new Error("wit_work_item_unlink tool not registered"); const [, , , handler] = call; // Mock a work item with some relations (this won't matter since we'll hit the error before processing them) const mockWorkItemWithRelations = { id: 1, relations: [], }; (mockWorkItemTrackingApi.getWorkItem as jest.Mock).mockResolvedValue(mockWorkItemWithRelations); const params = { project: "TestProject", id: 1, type: "unknown_type", }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("Error unlinking work item: Unknown link type: unknown_type"); }); it("should handle unknown error types in work_item_unlink", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_work_item_unlink"); if (!call) throw new Error("wit_work_item_unlink tool not registered"); const [, , , handler] = call; // Simulate an unknown error type (not an Error instance) (mockWorkItemTrackingApi.getWorkItem as jest.Mock).mockRejectedValue("String error"); const params = { project: "TestProject", id: 1, type: "related", }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("Error unlinking work item: Unknown error occurred"); }); }); // Add error handling tests for existing tools describe("error handling coverage", () => { it("should handle create_work_item errors", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_create_work_item"); if (!call) throw new Error("wit_create_work_item tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.createWorkItem as jest.Mock).mockRejectedValue(new Error("API Error")); const params = { project: "TestProject", workItemType: "Task", fields: [ { name: "System.Title", value: "Test Task" }, { name: "System.Description", value: "Test Description" }, ], }; const result = await handler(params); expect(result.content[0].text).toBe("Error creating work item: API Error"); expect(result.isError).toBe(true); }); it("should handle create_work_item null response", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_create_work_item"); if (!call) throw new Error("wit_create_work_item tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.createWorkItem as jest.Mock).mockResolvedValue(null); const params = { project: "TestProject", workItemType: "Task", fields: [{ name: "System.Title", value: "Test Task" }], }; const result = await handler(params); expect(result.content[0].text).toBe("Work item was not created"); expect(result.isError).toBe(true); }); it("should handle link_work_item_to_pull_request errors", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_link_work_item_to_pull_request"); if (!call) throw new Error("wit_link_work_item_to_pull_request tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.updateWorkItem as jest.Mock).mockRejectedValue(new Error("Linking failed")); const params = { projectId: "TestProject", repositoryId: "repo-123", pullRequestId: 42, workItemId: 1, pullRequestProjectId: "OtherProject", }; const result = await handler(params); expect(result.content[0].text).toBe("Error linking work item to pull request: Linking failed"); expect(result.isError).toBe(true); }); it("should handle link_work_item_to_pull_request null response", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_link_work_item_to_pull_request"); if (!call) throw new Error("wit_link_work_item_to_pull_request tool not registered"); const [, , , handler] = call; (mockWorkItemTrackingApi.updateWorkItem as jest.Mock).mockResolvedValue(null); const params = { projectId: "TestProject", repositoryId: "repo-123", pullRequestId: 42, workItemId: 1, pullRequestProjectId: "OtherProject", }; const result = await handler(params); expect(result.content[0].text).toBe("Work item update failed"); expect(result.isError).toBe(true); }); it("should handle create_work_item unknown error type", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_create_work_item"); if (!call) throw new Error("wit_create_work_item tool not registered"); const [, , , handler] = call; // Simulate an unknown error type (not an Error instance) (mockWorkItemTrackingApi.createWorkItem as jest.Mock).mockRejectedValue({ message: "Complex error object" }); const params = { project: "TestProject", workItemType: "Task", fields: [{ name: "System.Title", value: "Test Task" }], }; const result = await handler(params); expect(result.content[0].text).toBe("Error creating work item: Unknown error occurred"); expect(result.isError).toBe(true); }); it("should handle work_items_link with empty comment", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_work_items_link"); if (!call) throw new Error("wit_work_items_link tool not registered"); const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); global.fetch = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue([{ id: 1, success: true }]), }); const params = { project: "TestProject", updates: [ { id: 1, linkToId: 2, type: "related", // No comment provided, should default to empty string }, ], }; const result = await handler(params); expect(fetch).toHaveBeenCalled(); expect(result.content[0].text).toBe(JSON.stringify([{ id: 1, success: true }], null, 2)); }); }); // Add tests for optional parameters and edge cases describe("optional parameters coverage", () => { it("should handle add_child_work_item with optional parameters", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_child_work_items"); if (!call) throw new Error("wit_add_child_work_items tool not registered"); const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); // Mock fetch for the batch API call const mockFetch = jest.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ responses: [{ body: { id: 123 } }] }), }); global.fetch = mockFetch; const params = { parentId: 1, project: "TestProject", workItemType: "Task", items: [ { title: "Child Task", description: "Child Description", areaPath: "TestProject\\Area1", iterationPath: "TestProject\\Sprint1", }, ], }; await handler(params); expect(mockFetch).toHaveBeenCalledWith( "https://dev.azure.com/contoso/_apis/wit/$batch?api-version=5.0", expect.objectContaining({ method: "PATCH", headers: expect.objectContaining({ "Authorization": "Bearer fake-token", "Content-Type": "application/json", }), body: expect.stringContaining("TestProject\\\\Area1"), }) ); }); it("should handle add_child_work_item with empty optional parameters", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_child_work_items"); if (!call) throw new Error("wit_add_child_work_items tool not registered"); const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); // Mock fetch for the batch API call const mockFetch = jest.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ responses: [{ body: { id: 123 } }] }), }); global.fetch = mockFetch; const params = { parentId: 1, project: "TestProject", workItemType: "Task", items: [ { title: "Child Task", description: "Child Description", areaPath: "", iterationPath: " ", // whitespace only }, ], }; await handler(params); // Should not include area or iteration path since they're empty/whitespace expect(mockFetch).toHaveBeenCalledWith( "https://dev.azure.com/contoso/_apis/wit/$batch?api-version=5.0", expect.objectContaining({ body: expect.not.stringContaining("System.AreaPath"), }) ); expect(mockFetch).toHaveBeenCalledWith( "https://dev.azure.com/contoso/_apis/wit/$batch?api-version=5.0", expect.objectContaining({ body: expect.not.stringContaining("System.IterationPath"), }) ); }); it("should reject when more than 50 items are provided", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_child_work_items"); if (!call) throw new Error("wit_add_child_work_items tool not registered"); const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); // Create 51 items to exceed the limit const items = Array.from({ length: 51 }, (_, i) => ({ title: `Child Task ${i + 1}`, description: `Description ${i + 1}`, })); const params = { parentId: 1, project: "TestProject", workItemType: "Task", items, }; const result = await handler(params); expect(result.content[0].text).toBe("A maximum of 50 child work items can be created in a single call."); expect(result.isError).toBe(true); }); it("should handle Markdown format correctly", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_child_work_items"); if (!call) throw new Error("wit_add_child_work_items tool not registered"); const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); // Mock fetch for the batch API call const mockFetch = jest.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ responses: [{ body: { id: 123 } }] }), }); global.fetch = mockFetch; const params = { parentId: 1, project: "TestProject", workItemType: "Task", items: [ { title: "Child Task", description: "Child Description in **Markdown**", format: "Markdown" as "Markdown" | "Html", }, ], }; await handler(params); // Should include Markdown format fields expect(mockFetch).toHaveBeenCalledWith( "https://dev.azure.com/contoso/_apis/wit/$batch?api-version=5.0", expect.objectContaining({ body: expect.stringContaining("multilineFieldsFormat/System.Description"), }) ); expect(mockFetch).toHaveBeenCalledWith( "https://dev.azure.com/contoso/_apis/wit/$batch?api-version=5.0", expect.objectContaining({ body: expect.stringContaining("multilineFieldsFormat/Microsoft.VSTS.TCM.ReproSteps"), }) ); }); it("should handle fetch failure response", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_child_work_items"); if (!call) throw new Error("wit_add_child_work_items tool not registered"); const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; (tokenProvider as jest.Mock).mockResolvedValue("fake-token"); // Mock fetch for a failed response const mockFetch = jest.fn().mockResolvedValue({ ok: false, statusText: "Internal Server Error", }); global.fetch = mockFetch; const params = { parentId: 1, project: "TestProject", workItemType: "Task", items: [ { title: "Child Task", description: "Child Description", }, ], }; const result = await handler(params); expect(result.content[0].text).toBe("Error creating child work items: Failed to update work items in batch: Internal Server Error"); expect(result.isError).toBe(true); }); it("should handle unknown error types in add_child_work_items", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_child_work_items"); if (!call) throw new Error("wit_add_child_work_items tool not registered"); const [, , , handler] = call; mockConnection.serverUrl = "https://dev.azure.com/contoso"; // Mock tokenProvider to throw a non-Error object (tokenProvider as jest.Mock).mockRejectedValue("String error"); const params = { parentId: 1, project: "TestProject", workItemType: "Task", items: [ { title: "Child Task", description: "Child Description", }, ], }; const result = await handler(params); expect(result.content[0].text).toBe("Error creating child work items: Unknown error occurred"); expect(result.isError).toBe(true); }); }); describe("artifact link tools", () => { describe("wit_add_artifact_link", () => { it("should add artifact link to work item successfully", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_artifact_link"); if (!call) throw new Error("wit_add_artifact_link tool not registered"); const [, , , handler] = call; const mockWorkItem = { id: 1234, fields: { "System.Title": "Test Item" } }; mockWorkItemTrackingApi.updateWorkItem.mockResolvedValue(mockWorkItem); const params = { workItemId: 1234, project: "TestProject", artifactUri: "vstfs:///Git/Ref/12341234-1234-1234-1234-123412341234%2F12341234-1234-1234-1234-123412341234%2FGBmain", linkType: "Branch", comment: "Linked to main branch", }; const result = await handler(params); expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith( {}, [ { op: "add", path: "/relations/-", value: { rel: "ArtifactLink", url: "vstfs:///Git/Ref/12341234-1234-1234-1234-123412341234%2F12341234-1234-1234-1234-123412341234%2FGBmain", attributes: { name: "Branch", comment: "Linked to main branch", }, }, }, ], 1234, "TestProject" ); const response = JSON.parse(result.content[0].text); expect(response.workItemId).toBe(1234); expect(response.artifactUri).toBe("vstfs:///Git/Ref/12341234-1234-1234-1234-123412341234%2F12341234-1234-1234-1234-123412341234%2FGBmain"); expect(response.linkType).toBe("Branch"); expect(response.comment).toBe("Linked to main branch"); expect(response.success).toBe(true); }); it("should add artifact link without comment", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_artifact_link"); if (!call) throw new Error("wit_add_artifact_link tool not registered"); const [, , , handler] = call; const mockWorkItem = { id: 1234, fields: { "System.Title": "Test Item" } }; mockWorkItemTrackingApi.updateWorkItem.mockResolvedValue(mockWorkItem); const params = { workItemId: 1234, project: "TestProject", artifactUri: "vstfs:///Git/Commit/12341234-1234-1234-1234-123412341234%2F12341234-1234-1234-1234-123412341234%2Fabc123", linkType: "Commit", }; const result = await handler(params); expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith( {}, [ { op: "add", path: "/relations/-", value: { rel: "ArtifactLink", url: "vstfs:///Git/Commit/12341234-1234-1234-1234-123412341234%2F12341234-1234-1234-1234-123412341234%2Fabc123", attributes: { name: "Commit", }, }, }, ], 1234, "TestProject" ); const response = JSON.parse(result.content[0].text); expect(response.comment).toBe(null); }); it("should handle errors when adding artifact link", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_artifact_link"); if (!call) throw new Error("wit_add_artifact_link tool not registered"); const [, , , handler] = call; mockWorkItemTrackingApi.updateWorkItem.mockRejectedValue(new Error("API Error")); const params = { workItemId: 1234, project: "TestProject", artifactUri: "vstfs:///Git/Ref/invalid", linkType: "Branch", }; const result = await handler(params); expect(result.content[0].text).toBe("Error adding artifact link to work item: API Error"); expect(result.isError).toBe(true); }); // Tests to cover lines 929-973: URI building switch statement logic it("should build Branch URI from components", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_artifact_link"); if (!call) throw new Error("wit_add_artifact_link tool not registered"); const [, , , handler] = call; const mockWorkItem = { id: 1234, fields: { "System.Title": "Test Item" } }; mockWorkItemTrackingApi.updateWorkItem.mockResolvedValue(mockWorkItem); const params = { workItemId: 1234, project: "TestProject", linkType: "Branch", projectId: "project-guid", repositoryId: "repo-guid", branchName: "feature/test-branch", }; const result = await handler(params); expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith( {}, [ { op: "add", path: "/relations/-", value: { rel: "ArtifactLink", url: "vstfs:///Git/Ref/project-guid%2Frepo-guid%2FGBfeature%2Ftest-branch", attributes: { name: "Branch", }, }, }, ], 1234, "TestProject" ); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(true); }); it("should return error for Branch link missing required parameters", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_artifact_link"); if (!call) throw new Error("wit_add_artifact_link tool not registered"); const [, , , handler] = call; const params = { workItemId: 1234, project: "TestProject", linkType: "Branch", projectId: "project-guid", // Missing repositoryId and branchName }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("For 'Branch' links, 'projectId', 'repositoryId', and 'branchName' are required."); }); it("should build Fixed in Commit URI from components", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_artifact_link"); if (!call) throw new Error("wit_add_artifact_link tool not registered"); const [, , , handler] = call; const mockWorkItem = { id: 1234, fields: { "System.Title": "Test Item" } }; mockWorkItemTrackingApi.updateWorkItem.mockResolvedValue(mockWorkItem); const params = { workItemId: 1234, project: "TestProject", linkType: "Fixed in Commit", projectId: "project-guid", repositoryId: "repo-guid", commitId: "abc123def456", comment: "Fixed in this commit", }; const result = await handler(params); expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith( {}, [ { op: "add", path: "/relations/-", value: { rel: "ArtifactLink", url: "vstfs:///Git/Commit/project-guid%2Frepo-guid%2Fabc123def456", attributes: { name: "Fixed in Commit", comment: "Fixed in this commit", }, }, }, ], 1234, "TestProject" ); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(true); }); it("should return error for Fixed in Commit link missing required parameters", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_artifact_link"); if (!call) throw new Error("wit_add_artifact_link tool not registered"); const [, , , handler] = call; const params = { workItemId: 1234, project: "TestProject", linkType: "Fixed in Commit", projectId: "project-guid", // Missing repositoryId and commitId }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("For 'Fixed in Commit' links, 'projectId', 'repositoryId', and 'commitId' are required."); }); it("should build Pull Request URI from components", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_artifact_link"); if (!call) throw new Error("wit_add_artifact_link tool not registered"); const [, , , handler] = call; const mockWorkItem = { id: 1234, fields: { "System.Title": "Test Item" } }; mockWorkItemTrackingApi.updateWorkItem.mockResolvedValue(mockWorkItem); const params = { workItemId: 1234, project: "TestProject", linkType: "Pull Request", projectId: "project-guid", repositoryId: "repo-guid", pullRequestId: 42, }; const result = await handler(params); expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith( {}, [ { op: "add", path: "/relations/-", value: { rel: "ArtifactLink", url: "vstfs:///Git/PullRequestId/project-guid%2Frepo-guid%2F42", attributes: { name: "Pull Request", }, }, }, ], 1234, "TestProject" ); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(true); }); it("should return error for Pull Request link missing required parameters", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_artifact_link"); if (!call) throw new Error("wit_add_artifact_link tool not registered"); const [, , , handler] = call; const params = { workItemId: 1234, project: "TestProject", linkType: "Pull Request", projectId: "project-guid", repositoryId: "repo-guid", // Missing pullRequestId }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("For 'Pull Request' links, 'projectId', 'repositoryId', and 'pullRequestId' are required."); }); it("should build Build URI from components", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_artifact_link"); if (!call) throw new Error("wit_add_artifact_link tool not registered"); const [, , , handler] = call; const mockWorkItem = { id: 1234, fields: { "System.Title": "Test Item" } }; mockWorkItemTrackingApi.updateWorkItem.mockResolvedValue(mockWorkItem); const params = { workItemId: 1234, project: "TestProject", linkType: "Build", buildId: 123, }; const result = await handler(params); expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith( {}, [ { op: "add", path: "/relations/-", value: { rel: "ArtifactLink", url: "vstfs:///Build/Build/123", attributes: { name: "Build", }, }, }, ], 1234, "TestProject" ); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(true); }); it("should build Found in build URI from components", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_artifact_link"); if (!call) throw new Error("wit_add_artifact_link tool not registered"); const [, , , handler] = call; const mockWorkItem = { id: 1234, fields: { "System.Title": "Test Item" } }; mockWorkItemTrackingApi.updateWorkItem.mockResolvedValue(mockWorkItem); const params = { workItemId: 1234, project: "TestProject", linkType: "Found in build", buildId: 456, }; const result = await handler(params); expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith( {}, [ { op: "add", path: "/relations/-", value: { rel: "ArtifactLink", url: "vstfs:///Build/Build/456", attributes: { name: "Found in build", }, }, }, ], 1234, "TestProject" ); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(true); }); it("should build Integrated in build URI from components", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_artifact_link"); if (!call) throw new Error("wit_add_artifact_link tool not registered"); const [, , , handler] = call; const mockWorkItem = { id: 1234, fields: { "System.Title": "Test Item" } }; mockWorkItemTrackingApi.updateWorkItem.mockResolvedValue(mockWorkItem); const params = { workItemId: 1234, project: "TestProject", linkType: "Integrated in build", buildId: 789, }; const result = await handler(params); expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith( {}, [ { op: "add", path: "/relations/-", value: { rel: "ArtifactLink", url: "vstfs:///Build/Build/789", attributes: { name: "Integrated in build", }, }, }, ], 1234, "TestProject" ); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(true); }); it("should return error for build link types missing buildId", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_artifact_link"); if (!call) throw new Error("wit_add_artifact_link tool not registered"); const [, , , handler] = call; const params = { workItemId: 1234, project: "TestProject", linkType: "Build", // Missing buildId }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("For 'Build' links, 'buildId' is required."); }); it("should return error for unsupported link type in URI building", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_artifact_link"); if (!call) throw new Error("wit_add_artifact_link tool not registered"); const [, , , handler] = call; const params = { workItemId: 1234, project: "TestProject", linkType: "Model Link", // Unsupported link type for URI building }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("URI building from components is not supported for link type 'Model Link'. Please provide the full 'artifactUri' instead."); }); it("should handle null response from updateWorkItem (line 1000 coverage)", async () => { configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_artifact_link"); if (!call) throw new Error("wit_add_artifact_link tool not registered"); const [, , , handler] = call; // Mock updateWorkItem to return null mockWorkItemTrackingApi.updateWorkItem.mockResolvedValue(null); const params = { workItemId: 1234, project: "TestProject", artifactUri: "vstfs:///Git/Ref/12341234-1234-1234-1234-123412341234%2F12341234-1234-1234-1234-123412341234%2FGBmain", linkType: "Branch", }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("Work item update failed"); }); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/magemaclean/azure-devops-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server