Skip to main content
Glama
repositories.test.ts157 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { AccessToken } from "@azure/identity"; import { WebApi } from "azure-devops-node-api"; import { configureRepoTools, REPO_TOOLS } from "../../../src/tools/repositories"; import { PullRequestStatus, GitVersionType, GitPullRequestQueryType, CommentThreadStatus } from "azure-devops-node-api/interfaces/GitInterfaces.js"; import { getCurrentUserDetails, getUserIdFromEmail } from "../../../src/tools/auth"; // Mock the auth module jest.mock("../../../src/tools/auth", () => ({ getCurrentUserDetails: jest.fn(), getUserIdFromEmail: jest.fn(), })); const mockGetCurrentUserDetails = getCurrentUserDetails as jest.MockedFunction<typeof getCurrentUserDetails>; const mockGetUserIdFromEmail = getUserIdFromEmail as jest.MockedFunction<typeof getUserIdFromEmail>; describe("repos tools", () => { let server: McpServer; let tokenProvider: jest.MockedFunction<() => Promise<string>>; let connectionProvider: jest.MockedFunction<() => Promise<WebApi>>; let userAgentProvider: () => string; let mockGitApi: { updatePullRequest: jest.MockedFunction<(...args: unknown[]) => Promise<unknown>>; createPullRequest: jest.MockedFunction<(...args: unknown[]) => Promise<unknown>>; createPullRequestReviewers: jest.MockedFunction<(...args: unknown[]) => Promise<unknown>>; deletePullRequestReviewer: jest.MockedFunction<(...args: unknown[]) => Promise<unknown>>; getRepositories: jest.MockedFunction<(...args: unknown[]) => Promise<unknown>>; getPullRequests: jest.MockedFunction<(...args: unknown[]) => Promise<unknown>>; getPullRequestsByProject: jest.MockedFunction<(...args: unknown[]) => Promise<unknown>>; getThreads: jest.MockedFunction<(...args: unknown[]) => Promise<unknown>>; getComments: jest.MockedFunction<(...args: unknown[]) => Promise<unknown>>; getRefs: jest.MockedFunction<(...args: unknown[]) => Promise<unknown>>; getPullRequest: jest.MockedFunction<(...args: unknown[]) => Promise<unknown>>; createComment: jest.MockedFunction<(...args: unknown[]) => Promise<unknown>>; createThread: jest.MockedFunction<(...args: unknown[]) => Promise<unknown>>; updateThread: jest.MockedFunction<(...args: unknown[]) => Promise<unknown>>; getCommits: jest.MockedFunction<(...args: unknown[]) => Promise<unknown>>; getPullRequestQuery: jest.MockedFunction<(...args: unknown[]) => Promise<unknown>>; updateRefs: jest.MockedFunction<(...args: unknown[]) => Promise<unknown>>; }; beforeEach(() => { server = { tool: jest.fn(), } as unknown as McpServer; tokenProvider = jest.fn(); mockGitApi = { updatePullRequest: jest.fn(), createPullRequest: jest.fn(), createPullRequestReviewers: jest.fn(), deletePullRequestReviewer: jest.fn(), getRepositories: jest.fn(), getPullRequests: jest.fn(), getPullRequestsByProject: jest.fn(), getThreads: jest.fn(), getComments: jest.fn(), getRefs: jest.fn(), getPullRequest: jest.fn(), createComment: jest.fn(), createThread: jest.fn(), updateThread: jest.fn(), getCommits: jest.fn(), getPullRequestQuery: jest.fn(), updateRefs: jest.fn(), }; connectionProvider = jest.fn().mockResolvedValue({ getGitApi: jest.fn().mockResolvedValue(mockGitApi), }); userAgentProvider = () => "Jest"; mockGetCurrentUserDetails.mockResolvedValue({ authenticatedUser: { id: "user123", uniqueName: "testuser@example.com", displayName: "Test User" }, } as any); }); describe("repo_update_pull_request", () => { it("should update pull request with all provided fields", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.update_pull_request); if (!call) throw new Error("repo_update_pull_request tool not registered"); const [, , , handler] = call; const mockUpdatedPR = { pullRequestId: 123, codeReviewId: 123, repository: { name: "test-repo" }, status: 1, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com", }, creationDate: "2023-01-01T00:00:00Z", title: "Updated Title", description: "Updated Description", isDraft: true, sourceRefName: "refs/heads/feature", targetRefName: "refs/heads/main", }; mockGitApi.updatePullRequest.mockResolvedValue(mockUpdatedPR); const params = { repositoryId: "repo123", pullRequestId: 123, title: "Updated Title", description: "Updated Description", isDraft: true, targetRefName: "refs/heads/main", }; const result = await handler(params); expect(mockGitApi.updatePullRequest).toHaveBeenCalledWith( { title: "Updated Title", description: "Updated Description", isDraft: true, targetRefName: "refs/heads/main", }, "repo123", 123 ); const expectedTrimmedPR = { pullRequestId: 123, codeReviewId: 123, repository: "test-repo", status: 1, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com", }, creationDate: "2023-01-01T00:00:00Z", title: "Updated Title", description: "Updated Description", isDraft: true, sourceRefName: "refs/heads/feature", targetRefName: "refs/heads/main", }; expect(result.content[0].text).toBe(JSON.stringify(expectedTrimmedPR, null, 2)); }); it("should update pull request with only title", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.update_pull_request); if (!call) throw new Error("repo_update_pull_request tool not registered"); const [, , , handler] = call; const mockUpdatedPR = { pullRequestId: 123, codeReviewId: 123, repository: { name: "test-repo" }, status: 1, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com", }, creationDate: "2023-01-01T00:00:00Z", title: "New Title", isDraft: false, sourceRefName: "refs/heads/feature", targetRefName: "refs/heads/main", }; mockGitApi.updatePullRequest.mockResolvedValue(mockUpdatedPR); const params = { repositoryId: "repo123", pullRequestId: 123, title: "New Title", }; const result = await handler(params); expect(mockGitApi.updatePullRequest).toHaveBeenCalledWith( { title: "New Title", }, "repo123", 123 ); const expectedTrimmedPR = { pullRequestId: 123, codeReviewId: 123, repository: "test-repo", status: 1, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com", }, creationDate: "2023-01-01T00:00:00Z", title: "New Title", description: "", isDraft: false, sourceRefName: "refs/heads/feature", targetRefName: "refs/heads/main", }; expect(result.content[0].text).toBe(JSON.stringify(expectedTrimmedPR, null, 2)); }); it("should update pull request status to Active", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.update_pull_request); if (!call) throw new Error("repo_update_pull_request tool not registered"); const [, , , handler] = call; const mockUpdatedPR = { pullRequestId: 123, codeReviewId: 123, repository: { name: "test-repo" }, status: PullRequestStatus.Active, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com", }, creationDate: "2023-01-01T00:00:00Z", title: "Test PR", isDraft: false, sourceRefName: "refs/heads/feature", targetRefName: "refs/heads/main", }; mockGitApi.updatePullRequest.mockResolvedValue(mockUpdatedPR); const params = { repositoryId: "repo123", pullRequestId: 123, status: "Active" as const, }; const result = await handler(params); expect(mockGitApi.updatePullRequest).toHaveBeenCalledWith( { status: PullRequestStatus.Active, }, "repo123", 123 ); const expectedTrimmedPR = { pullRequestId: 123, codeReviewId: 123, repository: "test-repo", status: PullRequestStatus.Active, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com", }, creationDate: "2023-01-01T00:00:00Z", title: "Test PR", description: "", isDraft: false, sourceRefName: "refs/heads/feature", targetRefName: "refs/heads/main", }; expect(result.content[0].text).toBe(JSON.stringify(expectedTrimmedPR, null, 2)); }); it("should update pull request status to Abandoned", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.update_pull_request); if (!call) throw new Error("repo_update_pull_request tool not registered"); const [, , , handler] = call; const mockUpdatedPR = { pullRequestId: 123, codeReviewId: 123, repository: { name: "test-repo" }, status: PullRequestStatus.Abandoned, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com", }, creationDate: "2023-01-01T00:00:00Z", title: "Test PR", isDraft: false, sourceRefName: "refs/heads/feature", targetRefName: "refs/heads/main", }; mockGitApi.updatePullRequest.mockResolvedValue(mockUpdatedPR); const params = { repositoryId: "repo123", pullRequestId: 123, status: "Abandoned" as const, }; const result = await handler(params); expect(mockGitApi.updatePullRequest).toHaveBeenCalledWith( { status: PullRequestStatus.Abandoned, }, "repo123", 123 ); const expectedTrimmedPR = { pullRequestId: 123, codeReviewId: 123, repository: "test-repo", status: PullRequestStatus.Abandoned, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com", }, creationDate: "2023-01-01T00:00:00Z", title: "Test PR", description: "", isDraft: false, sourceRefName: "refs/heads/feature", targetRefName: "refs/heads/main", }; expect(result.content[0].text).toBe(JSON.stringify(expectedTrimmedPR, null, 2)); }); it("should update pull request with status and other fields", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.update_pull_request); if (!call) throw new Error("repo_update_pull_request tool not registered"); const [, , , handler] = call; const mockUpdatedPR = { pullRequestId: 123, codeReviewId: 123, repository: { name: "test-repo" }, status: PullRequestStatus.Active, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com", }, creationDate: "2023-01-01T00:00:00Z", title: "Updated Title", isDraft: false, sourceRefName: "refs/heads/feature", targetRefName: "refs/heads/main", }; mockGitApi.updatePullRequest.mockResolvedValue(mockUpdatedPR); const params = { repositoryId: "repo123", pullRequestId: 123, title: "Updated Title", status: "Active" as const, }; const result = await handler(params); expect(mockGitApi.updatePullRequest).toHaveBeenCalledWith( { title: "Updated Title", status: PullRequestStatus.Active, }, "repo123", 123 ); const expectedTrimmedPR = { pullRequestId: 123, codeReviewId: 123, repository: "test-repo", status: PullRequestStatus.Active, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com", }, creationDate: "2023-01-01T00:00:00Z", title: "Updated Title", description: "", isDraft: false, sourceRefName: "refs/heads/feature", targetRefName: "refs/heads/main", }; expect(result.content[0].text).toBe(JSON.stringify(expectedTrimmedPR, null, 2)); }); it("should return error when no fields provided", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.update_pull_request); if (!call) throw new Error("repo_update_pull_request tool not registered"); const [, , , handler] = call; const params = { repositoryId: "repo123", pullRequestId: 123, }; const result = await handler(params); expect(mockGitApi.updatePullRequest).not.toHaveBeenCalled(); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("At least one field (title, description, isDraft, targetRefName, status, or autoComplete options) must be provided for update."); }); it("should update pull request with autocomplete enabled", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.update_pull_request); if (!call) throw new Error("repo_update_pull_request tool not registered"); const [, , , handler] = call; const mockUpdatedPR = { pullRequestId: 123, title: "Updated PR", autoCompleteSetBy: { id: "user-id" }, completionOptions: { mergeStrategy: 2, // Squash deleteSourceBranch: true, transitionWorkItems: true, bypassPolicy: false, }, }; mockGitApi.updatePullRequest.mockResolvedValue(mockUpdatedPR); mockGetCurrentUserDetails.mockResolvedValue({ authenticatedUser: { id: "current-user-id" }, authorizedUser: { id: "current-user-id" }, }); const params = { repositoryId: "test-repo-id", pullRequestId: 123, autoComplete: true, mergeStrategy: "Squash", deleteSourceBranch: true, transitionWorkItems: true, }; const result = await handler(params); expect(mockGitApi.updatePullRequest).toHaveBeenCalledWith( expect.objectContaining({ autoCompleteSetBy: { id: "current-user-id" }, completionOptions: expect.objectContaining({ mergeStrategy: 2, // GitPullRequestMergeStrategy.Squash deleteSourceBranch: true, transitionWorkItems: true, bypassPolicy: false, }), }), "test-repo-id", 123 ); expect(result.isError).toBeFalsy(); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult.pullRequestId).toBe(123); }); it("should disable autocomplete when autoComplete is false", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.update_pull_request); if (!call) throw new Error("repo_update_pull_request tool not registered"); const [, , , handler] = call; const mockUpdatedPR = { pullRequestId: 123, title: "Updated PR", autoCompleteSetBy: null, completionOptions: null, }; mockGitApi.updatePullRequest.mockResolvedValue(mockUpdatedPR); const params = { repositoryId: "test-repo-id", pullRequestId: 123, autoComplete: false, }; const result = await handler(params); expect(mockGitApi.updatePullRequest).toHaveBeenCalledWith( expect.objectContaining({ autoCompleteSetBy: null, completionOptions: null, }), "test-repo-id", 123 ); expect(result.isError).toBeFalsy(); }); it("should not bypass policies when bypassReason is not provided", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.update_pull_request); if (!call) throw new Error("repo_update_pull_request tool not registered"); const [, , , handler] = call; const mockUpdatedPR = { pullRequestId: 123, codeReviewId: 123, repository: { name: "test-repo" }, status: 1, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com", }, creationDate: "2023-01-01T00:00:00Z", title: "Test PR", isDraft: false, sourceRefName: "refs/heads/feature", targetRefName: "refs/heads/main", }; mockGitApi.updatePullRequest.mockResolvedValue(mockUpdatedPR); const params = { repositoryId: "test-repo-id", pullRequestId: 123, autoComplete: true, }; const result = await handler(params); expect(mockGitApi.updatePullRequest).toHaveBeenCalledWith( expect.objectContaining({ autoCompleteSetBy: { id: "user123" }, completionOptions: expect.objectContaining({ bypassPolicy: false, }), }), "test-repo-id", 123 ); expect(result.isError).toBeFalsy(); }); it("should automatically bypass policies when bypassReason is provided", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.update_pull_request); if (!call) throw new Error("repo_update_pull_request tool not registered"); const [, , , handler] = call; const mockUpdatedPR = { pullRequestId: 123, codeReviewId: 123, repository: { name: "test-repo" }, status: 1, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com", }, creationDate: "2023-01-01T00:00:00Z", title: "Test PR", isDraft: false, sourceRefName: "refs/heads/feature", targetRefName: "refs/heads/main", }; mockGitApi.updatePullRequest.mockResolvedValue(mockUpdatedPR); const params = { repositoryId: "test-repo-id", pullRequestId: 123, autoComplete: true, bypassReason: "Emergency fix needed", }; const result = await handler(params); expect(mockGitApi.updatePullRequest).toHaveBeenCalledWith( expect.objectContaining({ autoCompleteSetBy: { id: "user123" }, completionOptions: expect.objectContaining({ bypassPolicy: true, bypassReason: "Emergency fix needed", }), }), "test-repo-id", 123 ); expect(result.isError).toBeFalsy(); }); }); describe("repo_create_pull_request", () => { it("should create pull request with basic fields", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request); if (!call) throw new Error("repo_create_pull_request tool not registered"); const [, , , handler] = call; const mockCreatedPR = { pullRequestId: 456, codeReviewId: 456, repository: { name: "test-repo" }, status: 1, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com", }, creationDate: "2023-01-01T00:00:00Z", title: "New Feature", isDraft: false, sourceRefName: "refs/heads/feature-branch", targetRefName: "refs/heads/main", }; mockGitApi.createPullRequest.mockResolvedValue(mockCreatedPR); const params = { repositoryId: "repo123", sourceRefName: "refs/heads/feature-branch", targetRefName: "refs/heads/main", title: "New Feature", }; const result = await handler(params); expect(mockGitApi.createPullRequest).toHaveBeenCalledWith( { sourceRefName: "refs/heads/feature-branch", targetRefName: "refs/heads/main", title: "New Feature", description: undefined, isDraft: undefined, workItemRefs: [], forkSource: undefined, }, "repo123" ); const expectedTrimmedPR = { pullRequestId: 456, codeReviewId: 456, repository: "test-repo", status: 1, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com", }, creationDate: "2023-01-01T00:00:00Z", title: "New Feature", description: "", isDraft: false, sourceRefName: "refs/heads/feature-branch", targetRefName: "refs/heads/main", }; expect(result.content[0].text).toBe(JSON.stringify(expectedTrimmedPR, null, 2)); }); it("should create pull request with all optional fields", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request); if (!call) throw new Error("repo_create_pull_request tool not registered"); const [, , , handler] = call; const mockCreatedPR = { pullRequestId: 456, codeReviewId: 456, repository: { name: "test-repo" }, status: PullRequestStatus.Active, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com", }, creationDate: "2023-01-01T00:00:00Z", title: "New Feature", description: "This is a new feature", isDraft: true, sourceRefName: "refs/heads/feature-branch", targetRefName: "refs/heads/main", }; mockGitApi.createPullRequest.mockResolvedValue(mockCreatedPR); const params = { repositoryId: "repo123", sourceRefName: "refs/heads/feature-branch", targetRefName: "refs/heads/main", title: "New Feature", description: "This is a new feature", isDraft: true, workItems: "1234 5678", forkSourceRepositoryId: "fork-repo-123", }; const result = await handler(params); expect(mockGitApi.createPullRequest).toHaveBeenCalledWith( { sourceRefName: "refs/heads/feature-branch", targetRefName: "refs/heads/main", title: "New Feature", description: "This is a new feature", isDraft: true, workItemRefs: [{ id: "1234" }, { id: "5678" }], forkSource: { repository: { id: "fork-repo-123", }, }, }, "repo123" ); const expectedTrimmedPR = { pullRequestId: 456, codeReviewId: 456, repository: "test-repo", status: PullRequestStatus.Active, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com", }, creationDate: "2023-01-01T00:00:00Z", title: "New Feature", description: "This is a new feature", isDraft: true, sourceRefName: "refs/heads/feature-branch", targetRefName: "refs/heads/main", }; expect(result.content[0].text).toBe(JSON.stringify(expectedTrimmedPR, null, 2)); }); }); describe("repo_create_branch", () => { it("should create branch with default source branch (main)", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_branch); if (!call) throw new Error("repo_create_branch tool not registered"); const [, , , handler] = call; const mockSourceBranch = [ { name: "refs/heads/main", objectId: "abc123def456", }, ]; const mockUpdateResult = [ { success: true, updateStatus: 0, }, ]; mockGitApi.getRefs.mockResolvedValue(mockSourceBranch); mockGitApi.updateRefs.mockResolvedValue(mockUpdateResult); const params = { repositoryId: "repo123", branchName: "feature-branch", sourceBranchName: "main", }; const result = await handler(params); expect(mockGitApi.getRefs).toHaveBeenCalledWith("repo123", undefined, "heads/", false, false, undefined, false, undefined, "main"); expect(mockGitApi.updateRefs).toHaveBeenCalledWith( [ { name: "refs/heads/feature-branch", newObjectId: "abc123def456", oldObjectId: "0000000000000000000000000000000000000000", }, ], "repo123" ); expect(result.content[0].text).toBe("Branch 'feature-branch' created successfully from 'main' (abc123def456)"); }); it("should create branch with custom source branch", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_branch); if (!call) throw new Error("repo_create_branch tool not registered"); const [, , , handler] = call; const mockSourceBranch = [ { name: "refs/heads/develop", objectId: "def456ghi789", }, ]; const mockUpdateResult = [ { success: true, updateStatus: 0, }, ]; mockGitApi.getRefs.mockResolvedValue(mockSourceBranch); mockGitApi.updateRefs.mockResolvedValue(mockUpdateResult); const params = { repositoryId: "repo123", branchName: "feature-branch", sourceBranchName: "develop", }; const result = await handler(params); expect(mockGitApi.getRefs).toHaveBeenCalledWith("repo123", undefined, "heads/", false, false, undefined, false, undefined, "develop"); expect(mockGitApi.updateRefs).toHaveBeenCalledWith( [ { name: "refs/heads/feature-branch", newObjectId: "def456ghi789", oldObjectId: "0000000000000000000000000000000000000000", }, ], "repo123" ); expect(result.content[0].text).toBe("Branch 'feature-branch' created successfully from 'develop' (def456ghi789)"); }); it("should create branch with specific commit ID", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_branch); if (!call) throw new Error("repo_create_branch tool not registered"); const [, , , handler] = call; const mockUpdateResult = [ { success: true, updateStatus: 0, }, ]; mockGitApi.updateRefs.mockResolvedValue(mockUpdateResult); const params = { repositoryId: "repo123", branchName: "feature-branch", sourceBranchName: "main", sourceCommitId: "xyz789abc123", }; const result = await handler(params); // Should not call getRefs when sourceCommitId is provided expect(mockGitApi.getRefs).not.toHaveBeenCalled(); expect(mockGitApi.updateRefs).toHaveBeenCalledWith( [ { name: "refs/heads/feature-branch", newObjectId: "xyz789abc123", oldObjectId: "0000000000000000000000000000000000000000", }, ], "repo123" ); expect(result.content[0].text).toBe("Branch 'feature-branch' created successfully from 'main' (xyz789abc123)"); }); it("should handle source branch not found error", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_branch); if (!call) throw new Error("repo_create_branch tool not registered"); const [, , , handler] = call; mockGitApi.getRefs.mockResolvedValue([]); const params = { repositoryId: "repo123", branchName: "feature-branch", sourceBranchName: "nonexistent", }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("Error: Source branch 'nonexistent' not found in repository repo123"); }); it("should handle getRefs API error", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_branch); if (!call) throw new Error("repo_create_branch tool not registered"); const [, , , handler] = call; const mockError = new Error("API Error"); mockGitApi.getRefs.mockRejectedValue(mockError); const params = { repositoryId: "repo123", branchName: "feature-branch", sourceBranchName: "main", }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("Error retrieving source branch 'main': API Error"); }); it("should handle updateRefs failure", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_branch); if (!call) throw new Error("repo_create_branch tool not registered"); const [, , , handler] = call; const mockSourceBranch = [ { name: "refs/heads/main", objectId: "abc123def456", }, ]; const mockUpdateResult = [ { success: false, customMessage: "Branch already exists", }, ]; mockGitApi.getRefs.mockResolvedValue(mockSourceBranch); mockGitApi.updateRefs.mockResolvedValue(mockUpdateResult); const params = { repositoryId: "repo123", branchName: "existing-branch", sourceBranchName: "main", }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("Error creating branch 'existing-branch': Branch already exists"); }); it("should handle updateRefs failure without custom message", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_branch); if (!call) throw new Error("repo_create_branch tool not registered"); const [, , , handler] = call; const mockSourceBranch = [ { name: "refs/heads/main", objectId: "abc123def456", }, ]; const mockUpdateResult = [ { success: false, }, ]; mockGitApi.getRefs.mockResolvedValue(mockSourceBranch); mockGitApi.updateRefs.mockResolvedValue(mockUpdateResult); const params = { repositoryId: "repo123", branchName: "failing-branch", sourceBranchName: "main", }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("Error creating branch 'failing-branch': Unknown error occurred during branch creation"); }); it("should handle updateRefs API error", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_branch); if (!call) throw new Error("repo_create_branch tool not registered"); const [, , , handler] = call; const mockSourceBranch = [ { name: "refs/heads/main", objectId: "abc123def456", }, ]; const mockError = new Error("Update API Error"); mockGitApi.getRefs.mockResolvedValue(mockSourceBranch); mockGitApi.updateRefs.mockRejectedValue(mockError); const params = { repositoryId: "repo123", branchName: "feature-branch", sourceBranchName: "main", }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("Error creating branch 'feature-branch': Update API Error"); }); it("should handle source branch with missing objectId", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_branch); if (!call) throw new Error("repo_create_branch tool not registered"); const [, , , handler] = call; const mockSourceBranch = [ { name: "refs/heads/main", // objectId is missing }, ]; mockGitApi.getRefs.mockResolvedValue(mockSourceBranch); const params = { repositoryId: "repo123", branchName: "feature-branch", sourceBranchName: "main", }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("Error: Source branch 'main' not found in repository repo123"); }); }); describe("repo_update_pull_request_reviewers", () => { it("should add reviewers to pull request", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.update_pull_request_reviewers); if (!call) throw new Error("repo_update_pull_request_reviewers tool not registered"); const [, , , handler] = call; const mockReviewers = [{ id: "reviewer1" }, { id: "reviewer2" }]; mockGitApi.createPullRequestReviewers.mockResolvedValue(mockReviewers); const params = { repositoryId: "repo123", pullRequestId: 456, reviewerIds: ["reviewer1", "reviewer2"], action: "add" as const, }; const result = await handler(params); expect(mockGitApi.createPullRequestReviewers).toHaveBeenCalledWith([{ id: "reviewer1" }, { id: "reviewer2" }], "repo123", 456); expect(result.content[0].text).toBe(JSON.stringify(mockReviewers, null, 2)); }); it("should remove reviewers from pull request", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.update_pull_request_reviewers); if (!call) throw new Error("repo_update_pull_request_reviewers tool not registered"); const [, , , handler] = call; mockGitApi.deletePullRequestReviewer.mockResolvedValue({}); const params = { repositoryId: "repo123", pullRequestId: 456, reviewerIds: ["reviewer1", "reviewer2"], action: "remove" as const, }; const result = await handler(params); expect(mockGitApi.deletePullRequestReviewer).toHaveBeenCalledTimes(2); expect(mockGitApi.deletePullRequestReviewer).toHaveBeenCalledWith("repo123", 456, "reviewer1"); expect(mockGitApi.deletePullRequestReviewer).toHaveBeenCalledWith("repo123", 456, "reviewer2"); expect(result.content[0].text).toBe("Reviewers with IDs reviewer1, reviewer2 removed from pull request 456."); }); }); describe("repo_list_repos_by_project", () => { it("should list repositories by project", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_repos_by_project); if (!call) throw new Error("repo_list_repos_by_project tool not registered"); const [, , , handler] = call; const mockRepos = [ { id: "repo1", name: "Repository 1", isDisabled: false, isFork: false, isInMaintenance: false, webUrl: "https://dev.azure.com/org/project/_git/repo1", size: 1024, }, { id: "repo2", name: "Repository 2", isDisabled: false, isFork: true, isInMaintenance: false, webUrl: "https://dev.azure.com/org/project/_git/repo2", size: 2048, }, ]; mockGitApi.getRepositories.mockResolvedValue(mockRepos); const params = { project: "test-project", top: 100, skip: 0, }; const result = await handler(params); expect(mockGitApi.getRepositories).toHaveBeenCalledWith("test-project", false, false, false); const expectedTrimmedRepos = mockRepos.map((repo) => ({ id: repo.id, name: repo.name, isDisabled: repo.isDisabled, isFork: repo.isFork, isInMaintenance: repo.isInMaintenance, webUrl: repo.webUrl, size: repo.size, })); expect(result.content[0].text).toBe(JSON.stringify(expectedTrimmedRepos, null, 2)); }); it("should filter repositories by name", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_repos_by_project); if (!call) throw new Error("repo_list_repos_by_project tool not registered"); const [, , , handler] = call; const mockRepos = [ { id: "repo1", name: "frontend-app", isDisabled: false, isFork: false, isInMaintenance: false, webUrl: "url1", size: 1024 }, { id: "repo2", name: "backend-api", isDisabled: false, isFork: false, isInMaintenance: false, webUrl: "url2", size: 2048 }, { id: "repo3", name: "frontend-web", isDisabled: false, isFork: false, isInMaintenance: false, webUrl: "url3", size: 3072 }, ]; mockGitApi.getRepositories.mockResolvedValue(mockRepos); const params = { project: "test-project", repoNameFilter: "frontend", top: 100, skip: 0, }; const result = await handler(params); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult).toHaveLength(2); expect(parsedResult.map((r: { name: string }) => r.name).sort()).toEqual(["frontend-app", "frontend-web"]); }); }); describe("repo_list_pull_requests_by_repo_or_project - repository tests", () => { it("should list pull requests by repository", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; const mockPRs = [ { pullRequestId: 123, codeReviewId: 456, status: PullRequestStatus.Active, createdBy: { displayName: "John Doe", uniqueName: "john@example.com" }, creationDate: "2023-01-01T00:00:00Z", title: "Feature PR", isDraft: false, sourceRefName: "refs/heads/feature-branch", targetRefName: "refs/heads/main", }, ]; mockGitApi.getPullRequests.mockResolvedValue(mockPRs); const params = { repositoryId: "repo123", top: 100, skip: 0, created_by_me: false, i_am_reviewer: false, status: "Active", }; const result = await handler(params); expect(mockGitApi.getPullRequests).toHaveBeenCalledWith("repo123", { status: PullRequestStatus.Active, repositoryId: "repo123" }, undefined, undefined, 0, 100); expect(result.content[0].text).toBe(JSON.stringify(mockPRs, null, 2)); }); it("should filter pull requests created by me", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; mockGitApi.getPullRequests.mockResolvedValue([]); const params = { repositoryId: "repo123", created_by_me: true, status: "Active", top: 100, skip: 0, }; await handler(params); expect(mockGetCurrentUserDetails).toHaveBeenCalled(); expect(mockGitApi.getPullRequests).toHaveBeenCalledWith("repo123", { status: PullRequestStatus.Active, repositoryId: "repo123", creatorId: "user123" }, undefined, undefined, 0, 100); }); it("should filter pull requests where I am a reviewer", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; mockGitApi.getPullRequests.mockResolvedValue([]); const params = { repositoryId: "repo123", i_am_reviewer: true, status: "Active", top: 100, skip: 0, }; await handler(params); expect(mockGetCurrentUserDetails).toHaveBeenCalled(); expect(mockGitApi.getPullRequests).toHaveBeenCalledWith("repo123", { status: PullRequestStatus.Active, repositoryId: "repo123", reviewerId: "user123" }, undefined, undefined, 0, 100); }); it("should filter pull requests created by me and where I am a reviewer", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; mockGitApi.getPullRequests.mockResolvedValue([]); const params = { repositoryId: "repo123", created_by_me: true, i_am_reviewer: true, status: "Active", top: 100, skip: 0, }; await handler(params); expect(mockGetCurrentUserDetails).toHaveBeenCalled(); expect(mockGitApi.getPullRequests).toHaveBeenCalledWith( "repo123", { status: PullRequestStatus.Active, repositoryId: "repo123", creatorId: "user123", reviewerId: "user123" }, undefined, undefined, 0, 100 ); }); it("should filter pull requests created by specific user successfully", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; // Mock successful user lookup mockGetUserIdFromEmail.mockResolvedValue("specific-user-123"); mockGitApi.getPullRequests.mockResolvedValue([]); const params = { repositoryId: "repo123", created_by_user: "john@example.com", status: "Active", top: 100, skip: 0, }; await handler(params); expect(mockGetUserIdFromEmail).toHaveBeenCalledWith("john@example.com", tokenProvider, connectionProvider, userAgentProvider); expect(mockGitApi.getPullRequests).toHaveBeenCalledWith("repo123", { status: PullRequestStatus.Active, repositoryId: "repo123", creatorId: "specific-user-123" }, undefined, undefined, 0, 100); }); it("should filter pull requests by source branch", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; mockGitApi.getPullRequests.mockResolvedValue([]); const params = { repositoryId: "repo123", sourceRefName: "refs/heads/feature-branch", status: "Active", top: 100, skip: 0, }; await handler(params); expect(mockGitApi.getPullRequests).toHaveBeenCalledWith( "repo123", { status: PullRequestStatus.Active, repositoryId: "repo123", sourceRefName: "refs/heads/feature-branch", }, undefined, undefined, 0, 100 ); }); it("should filter pull requests by target branch", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; mockGitApi.getPullRequests.mockResolvedValue([]); const params = { repositoryId: "repo123", targetRefName: "refs/heads/main", status: "Active", top: 100, skip: 0, }; await handler(params); expect(mockGitApi.getPullRequests).toHaveBeenCalledWith( "repo123", { status: PullRequestStatus.Active, repositoryId: "repo123", targetRefName: "refs/heads/main", }, undefined, undefined, 0, 100 ); }); it("should filter pull requests by both source and target branches", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; mockGitApi.getPullRequests.mockResolvedValue([]); const params = { repositoryId: "repo123", sourceRefName: "refs/heads/feature-branch", targetRefName: "refs/heads/main", status: "Active", top: 100, skip: 0, }; await handler(params); expect(mockGitApi.getPullRequests).toHaveBeenCalledWith( "repo123", { status: PullRequestStatus.Active, repositoryId: "repo123", sourceRefName: "refs/heads/feature-branch", targetRefName: "refs/heads/main", }, undefined, undefined, 0, 100 ); }); it("should combine branch filters with user filters", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; mockGitApi.getPullRequests.mockResolvedValue([]); const params = { repositoryId: "repo123", sourceRefName: "refs/heads/feature-branch", targetRefName: "refs/heads/main", created_by_me: true, i_am_reviewer: true, status: "Active", top: 100, skip: 0, }; await handler(params); expect(mockGetCurrentUserDetails).toHaveBeenCalled(); expect(mockGitApi.getPullRequests).toHaveBeenCalledWith( "repo123", { status: PullRequestStatus.Active, repositoryId: "repo123", sourceRefName: "refs/heads/feature-branch", targetRefName: "refs/heads/main", creatorId: "user123", reviewerId: "user123", }, undefined, undefined, 0, 100 ); }); it("should filter pull requests by specific reviewer successfully", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; // Mock successful user lookup mockGetUserIdFromEmail.mockResolvedValue("reviewer-user-123"); mockGitApi.getPullRequests.mockResolvedValue([]); const params = { repositoryId: "repo123", user_is_reviewer: "reviewer@example.com", status: "Active", top: 100, skip: 0, }; await handler(params); expect(mockGetUserIdFromEmail).toHaveBeenCalledWith("reviewer@example.com", tokenProvider, connectionProvider, userAgentProvider); expect(mockGitApi.getPullRequests).toHaveBeenCalledWith("repo123", { status: PullRequestStatus.Active, repositoryId: "repo123", reviewerId: "reviewer-user-123" }, undefined, undefined, 0, 100); }); it("should prioritize user_is_reviewer over i_am_reviewer flag", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; // Mock successful user lookup mockGetUserIdFromEmail.mockResolvedValue("specific-reviewer-123"); mockGitApi.getPullRequests.mockResolvedValue([]); const params = { repositoryId: "repo123", user_is_reviewer: "specific-reviewer@example.com", i_am_reviewer: true, // This should be ignored status: "Active", top: 100, skip: 0, }; await handler(params); expect(mockGetUserIdFromEmail).toHaveBeenCalledWith("specific-reviewer@example.com", tokenProvider, connectionProvider, userAgentProvider); expect(mockGetCurrentUserDetails).not.toHaveBeenCalled(); // Should not be called since user_is_reviewer takes precedence expect(mockGitApi.getPullRequests).toHaveBeenCalledWith( "repo123", { status: PullRequestStatus.Active, repositoryId: "repo123", reviewerId: "specific-reviewer-123" }, undefined, undefined, 0, 100 ); }); it("should handle error when user_is_reviewer user not found", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; // Mock user lookup failure mockGetUserIdFromEmail.mockRejectedValue(new Error("User not found")); const params = { repositoryId: "repo123", user_is_reviewer: "nonexistent@example.com", status: "Active", top: 100, skip: 0, }; const result = await handler(params); expect(mockGetUserIdFromEmail).toHaveBeenCalledWith("nonexistent@example.com", tokenProvider, connectionProvider, userAgentProvider); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("Error finding reviewer with email nonexistent@example.com: User not found"); expect(mockGitApi.getPullRequests).not.toHaveBeenCalled(); }); }); describe("repo_list_pull_requests_by_repo_or_project - project tests", () => { it("should list pull requests by project", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; const mockPRs = [ { pullRequestId: 123, codeReviewId: 456, repository: { name: "test-repo" }, status: PullRequestStatus.Active, createdBy: { displayName: "John Doe", uniqueName: "john@example.com" }, creationDate: "2023-01-01T00:00:00Z", title: "Feature PR", isDraft: false, sourceRefName: "refs/heads/feature-branch", targetRefName: "refs/heads/main", }, ]; mockGitApi.getPullRequestsByProject.mockResolvedValue(mockPRs); const params = { project: "test-project", status: "Active", top: 100, skip: 0, }; const result = await handler(params); expect(mockGitApi.getPullRequestsByProject).toHaveBeenCalledWith("test-project", { status: PullRequestStatus.Active }, undefined, 0, 100); const expectedResult = [ { pullRequestId: 123, codeReviewId: 456, repository: "test-repo", status: PullRequestStatus.Active, createdBy: { displayName: "John Doe", uniqueName: "john@example.com" }, creationDate: "2023-01-01T00:00:00Z", title: "Feature PR", isDraft: false, sourceRefName: "refs/heads/feature-branch", targetRefName: "refs/heads/main", }, ]; expect(result.content[0].text).toBe(JSON.stringify(expectedResult, null, 2)); }); it("should filter by current user when created_by_me is true", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; const mockPRs = [ { pullRequestId: 123, codeReviewId: 456, repository: { name: "test-repo" }, status: PullRequestStatus.Active, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com" }, creationDate: "2023-01-01T00:00:00Z", title: "My Feature PR", isDraft: false, sourceRefName: "refs/heads/my-feature-branch", targetRefName: "refs/heads/main", }, ]; mockGitApi.getPullRequestsByProject.mockResolvedValue(mockPRs); const params = { project: "test-project", created_by_me: true, status: "Active", top: 100, skip: 0, }; const result = await handler(params); expect(mockGetCurrentUserDetails).toHaveBeenCalledWith(tokenProvider, connectionProvider, userAgentProvider); expect(mockGitApi.getPullRequestsByProject).toHaveBeenCalledWith("test-project", { status: PullRequestStatus.Active, creatorId: "user123" }, undefined, 0, 100); const expectedResult = [ { pullRequestId: 123, codeReviewId: 456, repository: "test-repo", status: PullRequestStatus.Active, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com" }, creationDate: "2023-01-01T00:00:00Z", title: "My Feature PR", isDraft: false, sourceRefName: "refs/heads/my-feature-branch", targetRefName: "refs/heads/main", }, ]; expect(result.content[0].text).toBe(JSON.stringify(expectedResult, null, 2)); }); it("should filter by current user as reviewer when i_am_reviewer is true", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; const mockPRs = [ { pullRequestId: 456, codeReviewId: 789, repository: { name: "test-repo" }, status: PullRequestStatus.Active, createdBy: { displayName: "Other User", uniqueName: "other@example.com" }, creationDate: "2023-01-02T00:00:00Z", title: "Review Me PR", isDraft: false, sourceRefName: "refs/heads/review-branch", targetRefName: "refs/heads/main", }, ]; mockGitApi.getPullRequestsByProject.mockResolvedValue(mockPRs); const params = { project: "test-project", i_am_reviewer: true, status: "Active", top: 100, skip: 0, }; const result = await handler(params); expect(mockGetCurrentUserDetails).toHaveBeenCalledWith(tokenProvider, connectionProvider, userAgentProvider); expect(mockGitApi.getPullRequestsByProject).toHaveBeenCalledWith("test-project", { status: PullRequestStatus.Active, reviewerId: "user123" }, undefined, 0, 100); const expectedResult = [ { pullRequestId: 456, codeReviewId: 789, repository: "test-repo", status: PullRequestStatus.Active, createdBy: { displayName: "Other User", uniqueName: "other@example.com" }, creationDate: "2023-01-02T00:00:00Z", title: "Review Me PR", isDraft: false, sourceRefName: "refs/heads/review-branch", targetRefName: "refs/heads/main", }, ]; expect(result.content[0].text).toBe(JSON.stringify(expectedResult, null, 2)); }); it("should filter by both creator and reviewer when both created_by_me and i_am_reviewer are true", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; const mockPRs = [ { pullRequestId: 789, codeReviewId: 101112, repository: { name: "test-repo" }, status: PullRequestStatus.Active, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com" }, creationDate: "2023-01-03T00:00:00Z", title: "Both Creator and Reviewer PR", isDraft: false, sourceRefName: "refs/heads/both-branch", targetRefName: "refs/heads/main", }, ]; mockGitApi.getPullRequestsByProject.mockResolvedValue(mockPRs); const params = { project: "test-project", created_by_me: true, i_am_reviewer: true, status: "Active", top: 100, skip: 0, }; const result = await handler(params); expect(mockGetCurrentUserDetails).toHaveBeenCalledWith(tokenProvider, connectionProvider, userAgentProvider); expect(mockGitApi.getPullRequestsByProject).toHaveBeenCalledWith("test-project", { status: PullRequestStatus.Active, creatorId: "user123", reviewerId: "user123" }, undefined, 0, 100); const expectedResult = [ { pullRequestId: 789, codeReviewId: 101112, repository: "test-repo", status: PullRequestStatus.Active, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com" }, creationDate: "2023-01-03T00:00:00Z", title: "Both Creator and Reviewer PR", isDraft: false, sourceRefName: "refs/heads/both-branch", targetRefName: "refs/heads/main", }, ]; expect(result.content[0].text).toBe(JSON.stringify(expectedResult, null, 2)); }); it("should prioritize created_by_user over created_by_me flag", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; // Mock getUserIdFromEmail to return a specific user ID mockGetUserIdFromEmail.mockResolvedValue("specific-user-123"); const mockPRs = [ { pullRequestId: 999, codeReviewId: 888, repository: { name: "test-repo" }, status: PullRequestStatus.Active, createdBy: { displayName: "Specific User", uniqueName: "specific@example.com" }, creationDate: "2023-01-04T00:00:00Z", title: "Specific User PR", isDraft: false, sourceRefName: "refs/heads/specific-branch", targetRefName: "refs/heads/main", }, ]; mockGitApi.getPullRequestsByProject.mockResolvedValue(mockPRs); const params = { project: "test-project", created_by_user: "specific@example.com", created_by_me: true, // This should be ignored since created_by_user takes precedence status: "Active", top: 100, skip: 0, }; const result = await handler(params); expect(mockGetUserIdFromEmail).toHaveBeenCalledWith("specific@example.com", tokenProvider, connectionProvider, userAgentProvider); expect(mockGetCurrentUserDetails).not.toHaveBeenCalled(); // Should not be called when created_by_user is provided expect(mockGitApi.getPullRequestsByProject).toHaveBeenCalledWith("test-project", { status: PullRequestStatus.Active, creatorId: "specific-user-123" }, undefined, 0, 100); const expectedResult = [ { pullRequestId: 999, codeReviewId: 888, repository: "test-repo", status: PullRequestStatus.Active, createdBy: { displayName: "Specific User", uniqueName: "specific@example.com" }, creationDate: "2023-01-04T00:00:00Z", title: "Specific User PR", isDraft: false, sourceRefName: "refs/heads/specific-branch", targetRefName: "refs/heads/main", }, ]; expect(result.content[0].text).toBe(JSON.stringify(expectedResult, null, 2)); }); it("should filter pull requests by source branch", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; mockGitApi.getPullRequestsByProject.mockResolvedValue([]); const params = { project: "test-project", sourceRefName: "refs/heads/feature-branch", status: "Active", top: 100, skip: 0, }; await handler(params); expect(mockGitApi.getPullRequestsByProject).toHaveBeenCalledWith( "test-project", { status: PullRequestStatus.Active, sourceRefName: "refs/heads/feature-branch", }, undefined, 0, 100 ); }); it("should filter pull requests by target branch", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; mockGitApi.getPullRequestsByProject.mockResolvedValue([]); const params = { project: "test-project", targetRefName: "refs/heads/main", status: "Active", top: 100, skip: 0, }; await handler(params); expect(mockGitApi.getPullRequestsByProject).toHaveBeenCalledWith( "test-project", { status: PullRequestStatus.Active, targetRefName: "refs/heads/main", }, undefined, 0, 100 ); }); it("should filter pull requests by both source and target branches", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; mockGitApi.getPullRequestsByProject.mockResolvedValue([]); const params = { project: "test-project", sourceRefName: "refs/heads/feature-branch", targetRefName: "refs/heads/main", status: "Active", top: 100, skip: 0, }; await handler(params); expect(mockGitApi.getPullRequestsByProject).toHaveBeenCalledWith( "test-project", { status: PullRequestStatus.Active, sourceRefName: "refs/heads/feature-branch", targetRefName: "refs/heads/main", }, undefined, 0, 100 ); }); it("should combine branch filters with user filters", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; mockGitApi.getPullRequestsByProject.mockResolvedValue([]); const params = { project: "test-project", sourceRefName: "refs/heads/feature-branch", targetRefName: "refs/heads/main", created_by_me: true, status: "Active", top: 100, skip: 0, }; await handler(params); expect(mockGetCurrentUserDetails).toHaveBeenCalled(); expect(mockGitApi.getPullRequestsByProject).toHaveBeenCalledWith( "test-project", { status: PullRequestStatus.Active, sourceRefName: "refs/heads/feature-branch", targetRefName: "refs/heads/main", creatorId: "user123", }, undefined, 0, 100 ); }); it("should filter pull requests by specific reviewer successfully", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; // Mock successful user lookup mockGetUserIdFromEmail.mockResolvedValue("reviewer-user-123"); const mockPRs = [ { pullRequestId: 555, codeReviewId: 666, repository: { name: "test-repo" }, status: PullRequestStatus.Active, createdBy: { displayName: "Another User", uniqueName: "another@example.com" }, creationDate: "2023-01-05T00:00:00Z", title: "PR Reviewed by Specific User", isDraft: false, sourceRefName: "refs/heads/reviewed-branch", targetRefName: "refs/heads/main", }, ]; mockGitApi.getPullRequestsByProject.mockResolvedValue(mockPRs); const params = { project: "test-project", user_is_reviewer: "reviewer@example.com", status: "Active", top: 100, skip: 0, }; const result = await handler(params); expect(mockGetUserIdFromEmail).toHaveBeenCalledWith("reviewer@example.com", tokenProvider, connectionProvider, userAgentProvider); expect(mockGitApi.getPullRequestsByProject).toHaveBeenCalledWith("test-project", { status: PullRequestStatus.Active, reviewerId: "reviewer-user-123" }, undefined, 0, 100); const expectedResult = [ { pullRequestId: 555, codeReviewId: 666, repository: "test-repo", status: PullRequestStatus.Active, createdBy: { displayName: "Another User", uniqueName: "another@example.com" }, creationDate: "2023-01-05T00:00:00Z", title: "PR Reviewed by Specific User", isDraft: false, sourceRefName: "refs/heads/reviewed-branch", targetRefName: "refs/heads/main", }, ]; expect(result.content[0].text).toBe(JSON.stringify(expectedResult, null, 2)); }); it("should prioritize user_is_reviewer over i_am_reviewer flag", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; // Mock successful user lookup mockGetUserIdFromEmail.mockResolvedValue("specific-reviewer-123"); mockGitApi.getPullRequestsByProject.mockResolvedValue([]); const params = { project: "test-project", user_is_reviewer: "specific-reviewer@example.com", i_am_reviewer: true, // This should be ignored status: "Active", top: 100, skip: 0, }; await handler(params); expect(mockGetUserIdFromEmail).toHaveBeenCalledWith("specific-reviewer@example.com", tokenProvider, connectionProvider, userAgentProvider); expect(mockGetCurrentUserDetails).not.toHaveBeenCalled(); // Should not be called since user_is_reviewer takes precedence expect(mockGitApi.getPullRequestsByProject).toHaveBeenCalledWith("test-project", { status: PullRequestStatus.Active, reviewerId: "specific-reviewer-123" }, undefined, 0, 100); }); it("should handle error when user_is_reviewer user not found", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; // Mock user lookup failure mockGetUserIdFromEmail.mockRejectedValue(new Error("User not found")); const params = { project: "test-project", user_is_reviewer: "nonexistent@example.com", status: "Active", top: 100, skip: 0, }; const result = await handler(params); expect(mockGetUserIdFromEmail).toHaveBeenCalledWith("nonexistent@example.com", tokenProvider, connectionProvider, userAgentProvider); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("Error finding reviewer with email nonexistent@example.com: User not found"); expect(mockGitApi.getPullRequestsByProject).not.toHaveBeenCalled(); }); it("should support both created_by_user and user_is_reviewer filters simultaneously", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; // Mock both user lookups mockGetUserIdFromEmail .mockResolvedValueOnce("creator-user-123") // First call for created_by_user .mockResolvedValueOnce("reviewer-user-123"); // Second call for user_is_reviewer mockGitApi.getPullRequestsByProject.mockResolvedValue([]); const params = { project: "test-project", created_by_user: "creator@example.com", user_is_reviewer: "reviewer@example.com", status: "Active", top: 100, skip: 0, }; await handler(params); expect(mockGetUserIdFromEmail).toHaveBeenCalledWith("creator@example.com", tokenProvider, connectionProvider, userAgentProvider); expect(mockGetUserIdFromEmail).toHaveBeenCalledWith("reviewer@example.com", tokenProvider, connectionProvider, userAgentProvider); expect(mockGitApi.getPullRequestsByProject).toHaveBeenCalledWith( "test-project", { status: PullRequestStatus.Active, creatorId: "creator-user-123", reviewerId: "reviewer-user-123", }, undefined, 0, 100 ); }); }); describe("repo_list_pull_request_threads", () => { it("should list pull request threads", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_request_threads); if (!call) throw new Error("repo_list_pull_request_threads tool not registered"); const [, , , handler] = call; const mockThreads = [ { id: 1, publishedDate: "2023-01-01T00:00:00Z", lastUpdatedDate: "2023-01-01T01:00:00Z", status: CommentThreadStatus.Active, comments: [ { id: 1, author: { displayName: "John Doe", uniqueName: "john@example.com" }, content: "This looks good", publishedDate: "2023-01-01T00:00:00Z", isDeleted: false, lastUpdatedDate: "2023-01-01T00:30:00Z", lastContentUpdatedDate: "2023-01-01T00:15:00Z", }, ], }, ]; mockGitApi.getThreads.mockResolvedValue(mockThreads); const params = { repositoryId: "repo123", pullRequestId: 456, top: 100, skip: 0, }; const result = await handler(params); expect(mockGitApi.getThreads).toHaveBeenCalledWith("repo123", 456, undefined, undefined, undefined); const expectedResult = [ { id: 1, publishedDate: "2023-01-01T00:00:00Z", lastUpdatedDate: "2023-01-01T01:00:00Z", status: CommentThreadStatus.Active, comments: [ { id: 1, author: { displayName: "John Doe", uniqueName: "john@example.com" }, content: "This looks good", publishedDate: "2023-01-01T00:00:00Z", lastUpdatedDate: "2023-01-01T00:30:00Z", lastContentUpdatedDate: "2023-01-01T00:15:00Z", }, ], }, ]; expect(result.content[0].text).toBe(JSON.stringify(expectedResult, null, 2)); }); it("should return full response when requested", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_request_threads); if (!call) throw new Error("repo_list_pull_request_threads tool not registered"); const [, , , handler] = call; const mockThreads = [{ id: 1, fullData: "complete" }]; mockGitApi.getThreads.mockResolvedValue(mockThreads); const params = { repositoryId: "repo123", pullRequestId: 456, fullResponse: true, top: 100, skip: 0, }; const result = await handler(params); expect(result.content[0].text).toBe(JSON.stringify(mockThreads, null, 2)); }); }); describe("repo_list_pull_request_thread_comments", () => { it("should list pull request thread comments", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_request_thread_comments); if (!call) throw new Error("repo_list_pull_request_thread_comments tool not registered"); const [, , , handler] = call; const mockComments = [ { id: 1, author: { displayName: "John Doe", uniqueName: "john@example.com" }, content: "This looks good", publishedDate: "2023-01-01T00:00:00Z", lastUpdatedDate: "2023-01-01T00:30:00Z", lastContentUpdatedDate: "2023-01-01T00:15:00Z", isDeleted: false, }, { id: 2, author: { displayName: "Jane Doe", uniqueName: "jane@example.com" }, content: "Deleted comment", publishedDate: "2023-01-01T01:00:00Z", isDeleted: true, }, ]; mockGitApi.getComments.mockResolvedValue(mockComments); const params = { repositoryId: "repo123", pullRequestId: 456, threadId: 789, top: 100, skip: 0, }; const result = await handler(params); expect(mockGitApi.getComments).toHaveBeenCalledWith("repo123", 456, 789, undefined); const expectedResult = [ { id: 1, author: { displayName: "John Doe", uniqueName: "john@example.com" }, content: "This looks good", publishedDate: "2023-01-01T00:00:00Z", lastUpdatedDate: "2023-01-01T00:30:00Z", lastContentUpdatedDate: "2023-01-01T00:15:00Z", }, ]; expect(result.content[0].text).toBe(JSON.stringify(expectedResult, null, 2)); }); it("should list pull request thread comments with full response", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_request_thread_comments); if (!call) throw new Error("repo_list_pull_request_thread_comments tool not registered"); const [, , , handler] = call; const mockComments = [ { id: 1, author: { displayName: "John Doe", uniqueName: "john@example.com" }, content: "This looks good", publishedDate: "2023-01-01T00:00:00Z", lastUpdatedDate: "2023-01-01T00:30:00Z", lastContentUpdatedDate: "2023-01-01T00:15:00Z", isDeleted: false, // Additional properties that would be in full response commentType: 1, usersLiked: [], parentCommentId: 0, }, { id: 2, author: { displayName: "Jane Doe", uniqueName: "jane@example.com" }, content: "Deleted comment", publishedDate: "2023-01-01T01:00:00Z", isDeleted: true, commentType: 1, usersLiked: [], parentCommentId: 0, }, ]; mockGitApi.getComments.mockResolvedValue(mockComments); const params = { repositoryId: "repo123", pullRequestId: 456, threadId: 789, top: 100, skip: 0, fullResponse: true, }; const result = await handler(params); expect(mockGitApi.getComments).toHaveBeenCalledWith("repo123", 456, 789, undefined); // When fullResponse is true, it should return the full comment objects without trimming expect(result.content[0].text).toBe(JSON.stringify(mockComments, null, 2)); }); }); describe("repo_list_branches_by_repo", () => { it("should list branches by repository", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_branches_by_repo); if (!call) throw new Error("repo_list_branches_by_repo tool not registered"); const [, , , handler] = call; const mockBranches = [ { name: "refs/heads/main" }, { name: "refs/heads/feature-1" }, { name: "refs/heads/feature-2" }, { name: "refs/tags/v1.0" }, // Should be filtered out ]; mockGitApi.getRefs.mockResolvedValue(mockBranches); const params = { repositoryId: "repo123", top: 100, }; const result = await handler(params); expect(mockGitApi.getRefs).toHaveBeenCalledWith("repo123", undefined, "heads/", undefined, undefined, undefined, undefined, undefined, undefined); const expectedResult = ["main", "feature-2", "feature-1"]; // Sorted reverse alphabetically expect(result.content[0].text).toBe(JSON.stringify(expectedResult, null, 2)); }); }); describe("repo_list_my_branches_by_repo", () => { it("should list my branches by repository", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_my_branches_by_repo); if (!call) throw new Error("repo_list_my_branches_by_repo tool not registered"); const [, , , handler] = call; const mockBranches = [{ name: "refs/heads/main" }, { name: "refs/heads/my-feature" }]; mockGitApi.getRefs.mockResolvedValue(mockBranches); const params = { repositoryId: "repo123", top: 100, }; const result = await handler(params); expect(mockGitApi.getRefs).toHaveBeenCalledWith("repo123", undefined, "heads/", undefined, undefined, true, undefined, undefined, undefined); const expectedResult = ["my-feature", "main"]; expect(result.content[0].text).toBe(JSON.stringify(expectedResult, null, 2)); }); }); describe("repo_get_repo_by_name_or_id", () => { it("should get repository by name", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.get_repo_by_name_or_id); if (!call) throw new Error("repo_get_repo_by_name_or_id tool not registered"); const [, , , handler] = call; const mockRepos = [ { id: "repo1", name: "test-repo" }, { id: "repo2", name: "other-repo" }, ]; mockGitApi.getRepositories.mockResolvedValue(mockRepos); const params = { project: "test-project", repositoryNameOrId: "test-repo", }; const result = await handler(params); expect(mockGitApi.getRepositories).toHaveBeenCalledWith("test-project"); expect(result.content[0].text).toBe(JSON.stringify(mockRepos[0], null, 2)); }); it("should get repository by ID", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.get_repo_by_name_or_id); if (!call) throw new Error("repo_get_repo_by_name_or_id tool not registered"); const [, , , handler] = call; const mockRepos = [ { id: "repo1", name: "test-repo" }, { id: "repo2", name: "other-repo" }, ]; mockGitApi.getRepositories.mockResolvedValue(mockRepos); const params = { project: "test-project", repositoryNameOrId: "repo2", }; const result = await handler(params); expect(result.content[0].text).toBe(JSON.stringify(mockRepos[1], null, 2)); }); it("should throw error when repository not found", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.get_repo_by_name_or_id); if (!call) throw new Error("repo_get_repo_by_name_or_id tool not registered"); const [, , , handler] = call; mockGitApi.getRepositories.mockResolvedValue([]); const params = { project: "test-project", repositoryNameOrId: "nonexistent-repo", }; await expect(handler(params)).rejects.toThrow("Repository nonexistent-repo not found in project test-project"); }); }); describe("repo_get_branch_by_name", () => { it("should get branch by name", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.get_branch_by_name); if (!call) throw new Error("repo_get_branch_by_name tool not registered"); const [, , , handler] = call; const mockBranches = [ { name: "refs/heads/main", objectId: "abc123" }, { name: "refs/heads/feature", objectId: "def456" }, ]; mockGitApi.getRefs.mockResolvedValue(mockBranches); const params = { repositoryId: "repo123", branchName: "main", }; const result = await handler(params); expect(mockGitApi.getRefs).toHaveBeenCalledWith("repo123", undefined, "heads/", false, false, undefined, false, undefined, "main"); expect(result.content[0].text).toBe(JSON.stringify(mockBranches[0], null, 2)); }); it("should return error message when branch not found", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.get_branch_by_name); if (!call) throw new Error("repo_get_branch_by_name tool not registered"); const [, , , handler] = call; mockGitApi.getRefs.mockResolvedValue([]); const params = { repositoryId: "repo123", branchName: "nonexistent", }; const result = await handler(params); expect(result.content[0].text).toBe("Branch nonexistent not found in repository repo123"); }); }); describe("repo_get_pull_request_by_id", () => { it("should get pull request by ID", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.get_pull_request_by_id); if (!call) throw new Error("repo_get_pull_request_by_id tool not registered"); const [, , , handler] = call; const mockPR = { pullRequestId: 123, title: "Test PR", status: 1, }; mockGitApi.getPullRequest.mockResolvedValue(mockPR); const params = { repositoryId: "repo123", pullRequestId: 123, includeWorkItemRefs: false, }; const result = await handler(params); expect(mockGitApi.getPullRequest).toHaveBeenCalledWith("repo123", 123, undefined, undefined, undefined, undefined, undefined, false); expect(result.content[0].text).toBe(JSON.stringify(mockPR, null, 2)); }); it("should include work item refs when requested", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.get_pull_request_by_id); if (!call) throw new Error("repo_get_pull_request_by_id tool not registered"); const [, , , handler] = call; mockGitApi.getPullRequest.mockResolvedValue({}); const params = { repositoryId: "repo123", pullRequestId: 123, includeWorkItemRefs: true, }; await handler(params); expect(mockGitApi.getPullRequest).toHaveBeenCalledWith("repo123", 123, undefined, undefined, undefined, undefined, undefined, true); }); }); describe("repo_reply_to_comment", () => { it("should reply to comment successfully", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.reply_to_comment); if (!call) throw new Error("repo_reply_to_comment tool not registered"); const [, , , handler] = call; const mockComment = { id: 789, content: "Reply content" }; mockGitApi.createComment.mockResolvedValue(mockComment); const params = { repositoryId: "repo123", pullRequestId: 456, threadId: 789, content: "Reply content", }; const result = await handler(params); expect(mockGitApi.createComment).toHaveBeenCalledWith({ content: "Reply content" }, "repo123", 456, 789, undefined); expect(result.content[0].text).toBe("Comment successfully added to thread 789."); }); it("should return full response when requested", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.reply_to_comment); if (!call) throw new Error("repo_reply_to_comment tool not registered"); const [, , , handler] = call; const mockComment = { id: 789, content: "Reply content" }; mockGitApi.createComment.mockResolvedValue(mockComment); const params = { repositoryId: "repo123", pullRequestId: 456, threadId: 789, content: "Reply content", fullResponse: true, }; const result = await handler(params); expect(result.content[0].text).toBe(JSON.stringify(mockComment, null, 2)); }); it("should return error when comment creation fails", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.reply_to_comment); if (!call) throw new Error("repo_reply_to_comment tool not registered"); const [, , , handler] = call; mockGitApi.createComment.mockResolvedValue(null); const params = { repositoryId: "repo123", pullRequestId: 456, threadId: 789, content: "Reply content", }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("Error: Failed to add comment to thread 789"); }); }); describe("repo_create_pull_request_thread", () => { it("should create pull request thread with basic content", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request_thread); if (!call) throw new Error("repo_create_pull_request_thread tool not registered"); const [, , , handler] = call; const mockThread = { id: 123, status: 1 }; mockGitApi.createThread.mockResolvedValue(mockThread); const params = { repositoryId: "repo123", pullRequestId: 456, content: "New thread content", }; const result = await handler(params); expect(mockGitApi.createThread).toHaveBeenCalledWith( { comments: [{ content: "New thread content" }], threadContext: { filePath: undefined }, status: undefined, // Default status would be handled by CommentThreadStatus enum lookup }, "repo123", 456, undefined ); expect(result.content[0].text).toBe(JSON.stringify(mockThread, null, 2)); }); it("should create pull request thread with file context and position", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request_thread); if (!call) throw new Error("repo_create_pull_request_thread tool not registered"); const [, , , handler] = call; const mockThread = { id: 123 }; mockGitApi.createThread.mockResolvedValue(mockThread); const params = { repositoryId: "repo123", pullRequestId: 456, content: "Thread with position", filePath: "src/test.ts", rightFileStartLine: 10, rightFileStartOffset: 5, rightFileEndLine: 12, rightFileEndOffset: 15, }; const result = await handler(params); expect(mockGitApi.createThread).toHaveBeenCalledWith( { comments: [{ content: "Thread with position" }], threadContext: { filePath: "/src/test.ts", rightFileStart: { line: 10, offset: 5 }, rightFileEnd: { line: 12, offset: 15 }, }, status: undefined, }, "repo123", 456, undefined ); expect(result.content[0].text).toBe(JSON.stringify(mockThread, null, 2)); }); it("should normalize file path by adding leading slash if missing", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request_thread); if (!call) throw new Error("repo_create_pull_request_thread tool not registered"); const [, , , handler] = call; const mockThread = { id: 123 }; mockGitApi.createThread.mockResolvedValue(mockThread); const params = { repositoryId: "repo123", pullRequestId: 456, content: "Thread with normalized path", filePath: "src/file-without-slash.ts", // Path without leading slash }; const result = await handler(params); expect(mockGitApi.createThread).toHaveBeenCalledWith( { comments: [{ content: "Thread with normalized path" }], threadContext: { filePath: "/src/file-without-slash.ts", // Should have leading slash added }, status: undefined, }, "repo123", 456, undefined ); expect(result.content[0].text).toBe(JSON.stringify(mockThread, null, 2)); }); it("should preserve file path if it already starts with slash", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request_thread); if (!call) throw new Error("repo_create_pull_request_thread tool not registered"); const [, , , handler] = call; const mockThread = { id: 123 }; mockGitApi.createThread.mockResolvedValue(mockThread); const params = { repositoryId: "repo123", pullRequestId: 456, content: "Thread with existing slash", filePath: "/src/file-with-slash.ts", // Path already has leading slash }; const result = await handler(params); expect(mockGitApi.createThread).toHaveBeenCalledWith( { comments: [{ content: "Thread with existing slash" }], threadContext: { filePath: "/src/file-with-slash.ts", // Should remain unchanged }, status: undefined, }, "repo123", 456, undefined ); expect(result.content[0].text).toBe(JSON.stringify(mockThread, null, 2)); }); it("should throw error for invalid line numbers", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request_thread); if (!call) throw new Error("repo_create_pull_request_thread tool not registered"); const [, , , handler] = call; const params = { repositoryId: "repo123", pullRequestId: 456, content: "Thread content", rightFileStartLine: 0, // Invalid line number }; await expect(handler(params)).rejects.toThrow("rightFileStartLine must be greater than or equal to 1."); }); }); describe("repo_resolve_comment", () => { it("should resolve comment thread successfully", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.resolve_comment); if (!call) throw new Error("repo_resolve_comment tool not registered"); const [, , , handler] = call; const mockThread = { id: 123, status: CommentThreadStatus.Fixed }; mockGitApi.updateThread.mockResolvedValue(mockThread); const params = { repositoryId: "repo123", pullRequestId: 456, threadId: 789, }; const result = await handler(params); expect(mockGitApi.updateThread).toHaveBeenCalledWith({ status: CommentThreadStatus.Fixed }, "repo123", 456, 789); expect(result.content[0].text).toBe("Thread 789 was successfully resolved."); }); it("should return full response when requested", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.resolve_comment); if (!call) throw new Error("repo_resolve_comment tool not registered"); const [, , , handler] = call; const mockThread = { id: 123, status: CommentThreadStatus.Fixed }; mockGitApi.updateThread.mockResolvedValue(mockThread); const params = { repositoryId: "repo123", pullRequestId: 456, threadId: 789, fullResponse: true, }; const result = await handler(params); expect(result.content[0].text).toBe(JSON.stringify(mockThread, null, 2)); }); it("should return error when thread resolution fails", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.resolve_comment); if (!call) throw new Error("repo_resolve_comment tool not registered"); const [, , , handler] = call; mockGitApi.updateThread.mockResolvedValue(null); const params = { repositoryId: "repo123", pullRequestId: 456, threadId: 789, }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("Error: Failed to resolve thread 789"); }); }); describe("repo_search_commits", () => { it("should search commits successfully", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.search_commits); if (!call) throw new Error("repo_search_commits tool not registered"); const [, , , handler] = call; const mockCommits = [ { commitId: "abc123", comment: "Initial commit" }, { commitId: "def456", comment: "Add feature" }, ]; mockGitApi.getCommits.mockResolvedValue(mockCommits); const params = { project: "test-project", repository: "test-repo", version: "main", versionType: "Branch", skip: 0, top: 10, }; const result = await handler(params); expect(mockGitApi.getCommits).toHaveBeenCalledWith( "test-repo", { fromCommitId: undefined, toCommitId: undefined, includeLinks: undefined, includeWorkItems: undefined, itemVersion: { version: "main", versionType: GitVersionType.Branch, }, }, "test-project", 0, 10 ); expect(result.content[0].text).toBe(JSON.stringify(mockCommits, null, 2)); }); it("should handle commit search errors", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.search_commits); if (!call) throw new Error("repo_search_commits tool not registered"); const [, , , handler] = call; mockGitApi.getCommits.mockRejectedValue(new Error("API Error")); const params = { project: "test-project", repository: "test-repo", }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("Error searching commits: API Error"); }); }); describe("repo_list_pull_requests_by_commits", () => { it("should list pull requests by commits successfully", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_commits); if (!call) throw new Error("repo_list_pull_requests_by_commits tool not registered"); const [, , , handler] = call; const mockQueryResult = { results: [ { pullRequestId: 123, commit: "abc123", }, ], }; mockGitApi.getPullRequestQuery.mockResolvedValue(mockQueryResult); const params = { project: "test-project", repository: "test-repo", commits: ["abc123", "def456"], queryType: "LastMergeCommit", }; const result = await handler(params); expect(mockGitApi.getPullRequestQuery).toHaveBeenCalledWith( { queries: [ { items: ["abc123", "def456"], type: GitPullRequestQueryType.LastMergeCommit, }, ], }, "test-repo", "test-project" ); expect(result.content[0].text).toBe(JSON.stringify(mockQueryResult, null, 2)); }); it("should handle pull request query errors", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_commits); if (!call) throw new Error("repo_list_pull_requests_by_commits tool not registered"); const [, , , handler] = call; mockGitApi.getPullRequestQuery.mockRejectedValue(new Error("Query Error")); const params = { project: "test-project", repository: "test-repo", commits: ["abc123"], }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("Error querying pull requests by commits: Query Error"); }); }); describe("pullRequestStatusStringToInt function coverage", () => { it("should handle Completed status", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; mockGetCurrentUserDetails.mockResolvedValue({ authenticatedUser: { id: "user123" }, }); mockGitApi.getPullRequests.mockResolvedValue([]); const params = { repositoryId: "repo123", status: "Completed", top: 100, skip: 0, }; await handler(params); expect(mockGitApi.getPullRequests).toHaveBeenCalledWith("repo123", { status: PullRequestStatus.Completed, repositoryId: "repo123" }, undefined, undefined, 0, 100); }); it("should handle All status", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; mockGitApi.getPullRequests.mockResolvedValue([]); const params = { repositoryId: "repo123", status: "All", top: 100, skip: 0, }; await handler(params); expect(mockGitApi.getPullRequests).toHaveBeenCalledWith("repo123", { status: PullRequestStatus.All, repositoryId: "repo123" }, undefined, undefined, 0, 100); }); it("should handle NotSet status", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; mockGitApi.getPullRequests.mockResolvedValue([]); const params = { repositoryId: "repo123", status: "NotSet", top: 100, skip: 0, }; await handler(params); expect(mockGitApi.getPullRequests).toHaveBeenCalledWith("repo123", { status: PullRequestStatus.NotSet, repositoryId: "repo123" }, undefined, undefined, 0, 100); }); it("should handle Abandoned status", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; mockGitApi.getPullRequests.mockResolvedValue([]); const params = { repositoryId: "repo123", status: "Abandoned", top: 100, skip: 0, }; await handler(params); expect(mockGitApi.getPullRequests).toHaveBeenCalledWith("repo123", { status: PullRequestStatus.Abandoned, repositoryId: "repo123" }, undefined, undefined, 0, 100); }); it("should throw error for unknown status", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; const params = { repositoryId: "repo123", status: "InvalidStatus", top: 100, skip: 0, }; await expect(handler(params)).rejects.toThrow("Unknown pull request status: InvalidStatus"); }); }); describe("error handling coverage", () => { it("should handle getUserIdFromEmail error in list_pull_requests_by_repo", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; // Mock getUserIdFromEmail to throw an error mockGetUserIdFromEmail.mockRejectedValue(new Error("User not found")); const params = { repositoryId: "repo123", created_by_user: "nonexistent@example.com", status: "Active", top: 100, skip: 0, }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("Error finding user with email nonexistent@example.com: User not found"); }); it("should handle getUserIdFromEmail error in list_pull_requests_by_project", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; // Mock getUserIdFromEmail to throw an error mockGetUserIdFromEmail.mockRejectedValue(new Error("User not found")); const params = { project: "test-project", created_by_user: "nonexistent@example.com", status: "Active", top: 100, skip: 0, }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("Error finding user with email nonexistent@example.com: User not found"); }); it("should handle commit search error in search_commits", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.search_commits); if (!call) throw new Error("repo_search_commits tool not registered"); const [, , , handler] = call; mockGitApi.getCommits.mockRejectedValue(new Error("API Error")); const params = { project: "test-project", repository: "test-repo", }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("Error searching commits: API Error"); }); it("should handle thread creation error", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request_thread); if (!call) throw new Error("repo_create_pull_request_thread tool not registered"); const [, , , handler] = call; mockGitApi.createThread.mockRejectedValue(new Error("Thread creation failed")); const params = { repositoryId: "repo123", pullRequestId: 456, content: "Test comment", }; await expect(handler(params)).rejects.toThrow("Thread creation failed"); }); it("should handle thread resolution error", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.resolve_comment); if (!call) throw new Error("repo_resolve_comment tool not registered"); const [, , , handler] = call; mockGitApi.updateThread.mockRejectedValue(new Error("Thread resolution failed")); const params = { repositoryId: "repo123", pullRequestId: 456, threadId: 789, }; await expect(handler(params)).rejects.toThrow("Thread resolution failed"); }); it("should handle comment reply error", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.reply_to_comment); if (!call) throw new Error("repo_reply_to_comment tool not registered"); const [, , , handler] = call; mockGitApi.createComment.mockRejectedValue(new Error("Comment creation failed")); const params = { repositoryId: "repo123", pullRequestId: 456, threadId: 789, content: "Test reply", }; await expect(handler(params)).rejects.toThrow("Comment creation failed"); }); }); describe("edge cases and validation", () => { it("should handle invalid line numbers in create_pull_request_thread", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request_thread); if (!call) throw new Error("repo_create_pull_request_thread tool not registered"); const [, , , handler] = call; const params = { repositoryId: "repo123", pullRequestId: 456, content: "Test comment", filePath: "/test/file.js", rightFileStartLine: 0, // Invalid line number (should be >= 1) }; await expect(handler(params)).rejects.toThrow("rightFileStartLine must be greater than or equal to 1."); }); it("should handle create_pull_request with undefined forkSourceRepositoryId", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request); if (!call) throw new Error("repo_create_pull_request tool not registered"); const [, , , handler] = call; const mockPR = { pullRequestId: 123, codeReviewId: 123, repository: { name: "test-repo" }, status: PullRequestStatus.Active, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com", }, creationDate: "2023-01-01T00:00:00Z", title: "Test PR", description: "", isDraft: false, sourceRefName: "refs/heads/feature", targetRefName: "refs/heads/main", }; mockGitApi.createPullRequest.mockResolvedValue(mockPR); const params = { repositoryId: "repo123", sourceRefName: "refs/heads/feature", targetRefName: "refs/heads/main", title: "Test PR", description: undefined, isDraft: undefined, // forkSourceRepositoryId is undefined - should test the branch where it's undefined }; const result = await handler(params); expect(mockGitApi.createPullRequest).toHaveBeenCalledWith( { sourceRefName: "refs/heads/feature", targetRefName: "refs/heads/main", title: "Test PR", description: undefined, isDraft: undefined, // This is what actually gets passed when isDraft is not provided workItemRefs: [], forkSource: undefined, // This should be undefined when forkSourceRepositoryId is not provided }, "repo123" ); const expectedTrimmedPR = { pullRequestId: 123, codeReviewId: 123, repository: "test-repo", status: PullRequestStatus.Active, createdBy: { displayName: "Test User", uniqueName: "testuser@example.com", }, creationDate: "2023-01-01T00:00:00Z", title: "Test PR", description: "", isDraft: false, sourceRefName: "refs/heads/feature", targetRefName: "refs/heads/main", }; expect(result.content[0].text).toBe(JSON.stringify(expectedTrimmedPR, null, 2)); }); it("should handle trimComments with undefined comments", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_request_threads); if (!call) throw new Error("repo_list_pull_request_threads tool not registered"); const [, , , handler] = call; // Mock threads with undefined comments to test the trimComments function const mockThreads = [ { id: 1, publishedDate: "2023-01-01T00:00:00Z", lastUpdatedDate: "2023-01-01T00:00:00Z", status: 1, comments: undefined, // undefined comments }, { id: 2, publishedDate: "2023-01-02T00:00:00Z", lastUpdatedDate: "2023-01-02T00:00:00Z", status: 1, comments: null, // null comments }, ]; mockGitApi.getThreads.mockResolvedValue(mockThreads); const params = { repositoryId: "repo123", pullRequestId: 456, top: 10, skip: 0, }; const result = await handler(params); const resultData = JSON.parse(result.content[0].text); expect(resultData).toHaveLength(2); expect(resultData[0].comments).toBeUndefined(); expect(resultData[1].comments).toBeUndefined(); }); it("should handle trimComments with deleted comments", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_request_threads); if (!call) throw new Error("repo_list_pull_request_threads tool not registered"); const [, , , handler] = call; // Mock threads with deleted comments to test the trimComments function const mockThreads = [ { id: 1, publishedDate: "2023-01-01T00:00:00Z", lastUpdatedDate: "2023-01-01T00:00:00Z", status: 1, comments: [ { id: 1, content: "This is a normal comment", isDeleted: false, author: { displayName: "User 1", uniqueName: "user1@example.com" }, }, { id: 2, content: "This comment was deleted", isDeleted: true, // This should be filtered out author: { displayName: "User 2", uniqueName: "user2@example.com" }, }, ], }, ]; mockGitApi.getThreads.mockResolvedValue(mockThreads); const params = { repositoryId: "repo123", pullRequestId: 456, top: 10, skip: 0, }; const result = await handler(params); const resultData = JSON.parse(result.content[0].text); expect(resultData).toHaveLength(1); expect(resultData[0].comments).toHaveLength(1); // Only non-deleted comment should remain expect(resultData[0].comments[0].id).toBe(1); }); it("should handle list_repos_by_project without repoNameFilter", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_repos_by_project); if (!call) throw new Error("repo_list_repos_by_project tool not registered"); const [, , , handler] = call; const mockRepos = [ { id: "1", name: "repo1", isDisabled: false, isFork: false, isInMaintenance: false, webUrl: "http://example.com/repo1", size: 100 }, { id: "2", name: "repo2", isDisabled: false, isFork: false, isInMaintenance: false, webUrl: "http://example.com/repo2", size: 200 }, ]; mockGitApi.getRepositories.mockResolvedValue(mockRepos); const params = { project: "test-project", top: 100, skip: 0, // repoNameFilter is undefined - should test the branch where it's not provided }; const result = await handler(params); const resultData = JSON.parse(result.content[0].text); expect(resultData).toHaveLength(2); // All repos should be returned when no filter is applied expect(resultData[0].name).toBe("repo1"); expect(resultData[1].name).toBe("repo2"); }); it("should handle branches.find returning undefined (branch name mismatch)", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.get_branch_by_name); if (!call) throw new Error("repo_get_branch_by_name tool not registered"); const [, , , handler] = call; // Mock branches that don't match the requested branch name const mockBranches = [ { name: "refs/heads/other-branch", objectId: "abc123" }, { name: "refs/heads/another-branch", objectId: "def456" }, ]; mockGitApi.getRefs.mockResolvedValue(mockBranches); const params = { repositoryId: "repo123", branchName: "nonexistent-branch", // This branch doesn't exist in the mock data }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toBe("Branch nonexistent-branch not found in repository repo123"); }); it("should handle branch.name with exact branchName match", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.get_branch_by_name); if (!call) throw new Error("repo_get_branch_by_name tool not registered"); const [, , , handler] = call; // Mock branches where one matches exactly with the branchName (second condition in the find) const mockBranches = [ { name: "refs/heads/other-branch", objectId: "abc123" }, { name: "main", objectId: "def456" }, // This matches the branchName directly ]; mockGitApi.getRefs.mockResolvedValue(mockBranches); const params = { repositoryId: "repo123", branchName: "main", }; const result = await handler(params); expect(result.isError).toBeUndefined(); expect(JSON.parse(result.content[0].text).name).toBe("main"); }); it("should handle list_pull_requests_by_repo with created_by_user and i_am_reviewer both false", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; mockGitApi.getPullRequests.mockResolvedValue([]); const params = { repositoryId: "repo123", status: "Active", // Provide explicit status to avoid undefined created_by_me: false, i_am_reviewer: false, top: 100, // Explicit defaults skip: 0, // Explicit defaults // created_by_user is undefined - should test the case where we don't call getCurrentUserDetails }; await handler(params); // getCurrentUserDetails should not be called when both flags are false and created_by_user is undefined expect(mockGetCurrentUserDetails).not.toHaveBeenCalled(); expect(mockGitApi.getPullRequests).toHaveBeenCalledWith( "repo123", { status: PullRequestStatus.Active, repositoryId: "repo123" }, undefined, undefined, 0, // skip 100 // top ); }); it("should handle list_pull_requests_by_project with created_by_user and i_am_reviewer both false", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; mockGitApi.getPullRequestsByProject.mockResolvedValue([]); const params = { project: "test-project", status: "Active", // Provide explicit status to avoid undefined created_by_me: false, i_am_reviewer: false, top: 100, // Explicit defaults skip: 0, // Explicit defaults // created_by_user is undefined - should test the case where we don't call getCurrentUserDetails }; await handler(params); // getCurrentUserDetails should not be called when both flags are false and created_by_user is undefined expect(mockGetCurrentUserDetails).not.toHaveBeenCalled(); expect(mockGitApi.getPullRequestsByProject).toHaveBeenCalledWith( "test-project", { status: PullRequestStatus.Active }, undefined, 0, // skip 100 // top ); }); it("should handle comments?.flatMap with null/undefined branch in branchesFilterOutIrrelevantProperties", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_branches_by_repo); if (!call) throw new Error("repo_list_branches_by_repo tool not registered"); const [, , , handler] = call; // Mock branches with some having null/undefined names to test the flatMap filter const mockBranches = [ { name: "refs/heads/main", objectId: "abc123" }, { name: null, objectId: "def456" }, // null name should be filtered out { name: undefined, objectId: "ghi789" }, // undefined name should be filtered out { name: "refs/heads/feature", objectId: "jkl012" }, { name: "refs/tags/v1.0", objectId: "mno345" }, // not a heads/ ref, should be filtered out ]; mockGitApi.getRefs.mockResolvedValue(mockBranches); const params = { repositoryId: "repo123", }; const result = await handler(params); const resultData = JSON.parse(result.content[0].text); // Should only include valid heads/ refs with names expect(resultData).toEqual(["main", "feature"]); }); it("should handle rightFileStartOffset without validation error", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request_thread); if (!call) throw new Error("repo_create_pull_request_thread tool not registered"); const [, , , handler] = call; const mockThread = { id: 123, status: 1, comments: [] }; mockGitApi.createThread.mockResolvedValue(mockThread); const params = { repositoryId: "repo123", pullRequestId: 456, content: "Test comment", filePath: "/test/file.js", status: "Active", // Provide explicit status rightFileStartLine: 5, rightFileStartOffset: 10, // Valid offset }; const result = await handler(params); expect(mockGitApi.createThread).toHaveBeenCalledWith( { comments: [{ content: "Test comment" }], threadContext: { filePath: "/test/file.js", rightFileStart: { line: 5, offset: 10 }, }, status: CommentThreadStatus.Active, }, "repo123", 456, undefined ); expect(result.content[0].text).toBe(JSON.stringify(mockThread, null, 2)); }); it("should handle rightFileEndOffset without validation error", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request_thread); if (!call) throw new Error("repo_create_pull_request_thread tool not registered"); const [, , , handler] = call; const mockThread = { id: 123, status: 1, comments: [] }; mockGitApi.createThread.mockResolvedValue(mockThread); const params = { repositoryId: "repo123", pullRequestId: 456, content: "Test comment", filePath: "/test/file.js", status: "Active", // Provide explicit status rightFileStartLine: 5, rightFileEndLine: 10, rightFileEndOffset: 15, // Valid end offset }; const result = await handler(params); expect(mockGitApi.createThread).toHaveBeenCalledWith( { comments: [{ content: "Test comment" }], threadContext: { filePath: "/test/file.js", rightFileStart: { line: 5 }, rightFileEnd: { line: 10, offset: 15 }, }, status: CommentThreadStatus.Active, }, "repo123", 456, undefined ); expect(result.content[0].text).toBe(JSON.stringify(mockThread, null, 2)); }); it("should handle search_commits with version parameter", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.search_commits); if (!call) throw new Error("repo_search_commits tool not registered"); const [, , , handler] = call; const mockCommits = [{ commitId: "abc123", comment: "Test commit" }]; mockGitApi.getCommits.mockResolvedValue(mockCommits); const params = { project: "test-project", repository: "test-repo", version: "main", // This should trigger the version branch versionType: "Branch", skip: 0, // Provide explicit values top: 10, includeLinks: false, includeWorkItems: false, }; const result = await handler(params); expect(mockGitApi.getCommits).toHaveBeenCalledWith( "test-repo", { fromCommitId: undefined, toCommitId: undefined, includeLinks: false, includeWorkItems: false, itemVersion: { version: "main", versionType: GitVersionType.Branch, }, }, "test-project", 0, 10 ); expect(result.content[0].text).toBe(JSON.stringify(mockCommits, null, 2)); }); it("should handle search_commits without version parameter", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.search_commits); if (!call) throw new Error("repo_search_commits tool not registered"); const [, , , handler] = call; const mockCommits = [{ commitId: "abc123", comment: "Test commit" }]; mockGitApi.getCommits.mockResolvedValue(mockCommits); const params = { project: "test-project", repository: "test-repo", skip: 0, // Provide explicit values top: 10, includeLinks: false, includeWorkItems: false, // version is undefined - should test the branch where itemVersion is not set }; const result = await handler(params); expect(mockGitApi.getCommits).toHaveBeenCalledWith( "test-repo", { fromCommitId: undefined, toCommitId: undefined, includeLinks: false, includeWorkItems: false, // itemVersion should not be set when version is undefined }, "test-project", 0, 10 ); expect(result.content[0].text).toBe(JSON.stringify(mockCommits, null, 2)); }); it("should handle rightFileEndLine without rightFileStartLine", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request_thread); if (!call) throw new Error("repo_create_pull_request_thread tool not registered"); const [, , , handler] = call; const params = { repositoryId: "repo123", pullRequestId: 456, content: "Test comment", filePath: "/test/file.js", rightFileEndLine: 10, // End line specified without start line }; await expect(handler(params)).rejects.toThrow("rightFileEndLine must only be specified if rightFileStartLine is also specified."); }); it("should handle invalid rightFileEndLine value", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request_thread); if (!call) throw new Error("repo_create_pull_request_thread tool not registered"); const [, , , handler] = call; const params = { repositoryId: "repo123", pullRequestId: 456, content: "Test comment", filePath: "/test/file.js", rightFileStartLine: 5, rightFileEndLine: 0, // Invalid end line }; await expect(handler(params)).rejects.toThrow("rightFileEndLine must be greater than or equal to 1."); }); it("should handle invalid rightFileStartOffset value", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request_thread); if (!call) throw new Error("repo_create_pull_request_thread tool not registered"); const [, , , handler] = call; const params = { repositoryId: "repo123", pullRequestId: 456, content: "Test comment", filePath: "/test/file.js", rightFileStartLine: 5, rightFileStartOffset: 0, // Invalid offset }; await expect(handler(params)).rejects.toThrow("rightFileStartOffset must be greater than or equal to 1."); }); it("should handle invalid rightFileEndOffset value", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request_thread); if (!call) throw new Error("repo_create_pull_request_thread tool not registered"); const [, , , handler] = call; const params = { repositoryId: "repo123", pullRequestId: 456, content: "Test comment", filePath: "/test/file.js", rightFileStartLine: 5, rightFileEndLine: 10, rightFileEndOffset: 0, // Invalid offset }; await expect(handler(params)).rejects.toThrow("rightFileEndOffset must be greater than or equal to 1."); }); it("should test pullRequestStatusStringToInt with unknown status", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; const params = { repositoryId: "repo123", status: "UnknownStatus" as "Active", // Invalid status that should trigger the default case created_by_me: false, i_am_reviewer: false, }; await expect(handler(params)).rejects.toThrow("Unknown pull request status: UnknownStatus"); }); it("should handle threads?.sort with undefined id values", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_request_threads); if (!call) throw new Error("repo_list_pull_request_threads tool not registered"); const [, , , handler] = call; // Mock threads with undefined/null id values to test the sort function const mockThreads = [ { id: undefined, // undefined id publishedDate: "2023-01-03T00:00:00Z", lastUpdatedDate: "2023-01-03T00:00:00Z", status: 1, comments: [], }, { id: 2, publishedDate: "2023-01-02T00:00:00Z", lastUpdatedDate: "2023-01-02T00:00:00Z", status: 1, comments: [], }, { id: null, // null id publishedDate: "2023-01-01T00:00:00Z", lastUpdatedDate: "2023-01-01T00:00:00Z", status: 1, comments: [], }, ]; mockGitApi.getThreads.mockResolvedValue(mockThreads); const params = { repositoryId: "repo123", pullRequestId: 456, top: 10, skip: 0, }; const result = await handler(params); const resultData = JSON.parse(result.content[0].text); expect(resultData).toHaveLength(3); // All threads should be returned even with undefined/null ids }); it("should handle comments?.sort with undefined id values", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_request_thread_comments); if (!call) throw new Error("repo_list_pull_request_thread_comments tool not registered"); const [, , , handler] = call; // Mock comments with undefined/null id values to test the sort function const mockComments = [ { id: undefined, // undefined id content: "Comment with undefined id", isDeleted: false, author: { displayName: "User 1", uniqueName: "user1@example.com" }, }, { id: 2, content: "Comment with id 2", isDeleted: false, author: { displayName: "User 2", uniqueName: "user2@example.com" }, }, { id: null, // null id content: "Comment with null id", isDeleted: false, author: { displayName: "User 3", uniqueName: "user3@example.com" }, }, ]; mockGitApi.getComments.mockResolvedValue(mockComments); const params = { repositoryId: "repo123", pullRequestId: 456, threadId: 789, top: 10, skip: 0, }; const result = await handler(params); const resultData = JSON.parse(result.content[0].text); expect(resultData).toHaveLength(3); // All comments should be returned even with undefined/null ids }); it("should handle workItemRefs when workItems is undefined", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request); if (!call) throw new Error("repo_create_pull_request tool not registered"); const [, , , handler] = call; const mockPR = { pullRequestId: 123, title: "Test PR" }; mockGitApi.createPullRequest.mockResolvedValue(mockPR); const params = { repositoryId: "repo123", sourceRefName: "refs/heads/feature", targetRefName: "refs/heads/main", title: "Test PR", // workItems is undefined - should test the ternary operator }; await handler(params); expect(mockGitApi.createPullRequest).toHaveBeenCalledWith( expect.objectContaining({ workItemRefs: [], // Should be empty array when workItems is undefined }), "repo123" ); }); it("should handle workItemRefs when workItems is provided", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request); if (!call) throw new Error("repo_create_pull_request tool not registered"); const [, , , handler] = call; const mockPR = { pullRequestId: 123, title: "Test PR" }; mockGitApi.createPullRequest.mockResolvedValue(mockPR); const params = { repositoryId: "repo123", sourceRefName: "refs/heads/feature", targetRefName: "refs/heads/main", title: "Test PR", workItems: "123 456", // workItems provided - should be split and mapped }; await handler(params); expect(mockGitApi.createPullRequest).toHaveBeenCalledWith( expect.objectContaining({ workItemRefs: [{ id: "123" }, { id: "456" }], // Should be split and mapped }), "repo123" ); }); it("should handle empty repoNameFilter in list_repos_by_project", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_repos_by_project); if (!call) throw new Error("repo_list_repos_by_project tool not registered"); const [, , , handler] = call; const mockRepos = [{ id: "repo1", name: "Repository 1", isDisabled: false, isFork: false, isInMaintenance: false, webUrl: "url1", size: 1024 }]; mockGitApi.getRepositories.mockResolvedValue(mockRepos); const params = { project: "test-project", repoNameFilter: "", // Empty string - should use all repositories top: 100, skip: 0, }; const result = await handler(params); // Should return all repositories since empty string is falsy const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult).toHaveLength(1); expect(parsedResult[0].name).toBe("Repository 1"); }); it("should handle getUserIdFromEmail error with created_by_user in list_pull_requests_by_repo", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; mockGetUserIdFromEmail.mockRejectedValue(new Error("User not found")); const params = { repositoryId: "repo123", created_by_user: "nonexistent@example.com", status: "Active", top: 100, skip: 0, }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("Error finding user with email nonexistent@example.com: User not found"); }); it("should handle getUserIdFromEmail error with created_by_user in list_pull_requests_by_project", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; mockGetUserIdFromEmail.mockRejectedValue(new Error("User not found")); const params = { project: "test-project", created_by_user: "nonexistent@example.com", status: "Active", top: 100, skip: 0, }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("Error finding user with email nonexistent@example.com: User not found"); }); it("should handle rightFileEndOffset set without rightFileEndLine in create_pull_request_thread", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request_thread); if (!call) throw new Error("repo_create_pull_request_thread tool not registered"); const [, , , handler] = call; const mockThread = { id: 1, status: CommentThreadStatus.Active }; mockGitApi.createThread.mockResolvedValue(mockThread); const params = { repositoryId: "repo123", pullRequestId: 456, content: "Test comment", filePath: "/test/file.js", rightFileStartLine: 5, rightFileStartOffset: 10, rightFileEndOffset: 20, // End offset without end line - should still work }; const result = await handler(params); expect(mockGitApi.createThread).toHaveBeenCalled(); expect(result.content[0].text).toBe(JSON.stringify(mockThread, null, 2)); }); it("should handle error in list_pull_requests_by_commits", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_commits); if (!call) throw new Error("repo_list_pull_requests_by_commits tool not registered"); const [, , , handler] = call; mockGitApi.getPullRequestQuery.mockRejectedValue(new Error("API error")); const params = { project: "test-project", repository: "test-repo", commits: ["abc123", "def456"], queryType: "LastMergeCommit", }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("Error querying pull requests by commits: API error"); }); it("should handle different queryType values in list_pull_requests_by_commits", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_commits); if (!call) throw new Error("repo_list_pull_requests_by_commits tool not registered"); const [, , , handler] = call; const mockQueryResult = { results: [] }; mockGitApi.getPullRequestQuery.mockResolvedValue(mockQueryResult); const params = { project: "test-project", repository: "test-repo", commits: ["abc123"], queryType: "Commit", }; const result = await handler(params); expect(mockGitApi.getPullRequestQuery).toHaveBeenCalledWith( expect.objectContaining({ queries: [ expect.objectContaining({ items: ["abc123"], type: expect.any(Number), // Should be the enum value for Commit }), ], }), "test-repo", "test-project" ); expect(result.content[0].text).toBe(JSON.stringify(mockQueryResult, null, 2)); }); it("should handle repositories with null/undefined names in sorting", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_repos_by_project); if (!call) throw new Error("repo_list_repos_by_project tool not registered"); const [, , , handler] = call; const mockRepos = [ { id: "repo1", name: undefined, isDisabled: false, isFork: false, isInMaintenance: false, webUrl: "url1", size: 1024 }, { id: "repo2", name: "Repository B", isDisabled: false, isFork: false, isInMaintenance: false, webUrl: "url2", size: 2048 }, { id: "repo3", name: null, isDisabled: false, isFork: false, isInMaintenance: false, webUrl: "url3", size: 3072 }, ]; mockGitApi.getRepositories.mockResolvedValue(mockRepos); const params = { project: "test-project", top: 100, skip: 0, }; const result = await handler(params); // Should handle sorting even with null/undefined names const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult).toHaveLength(3); }); it("should handle non-Error exceptions in list_pull_requests_by_repo", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; mockGetUserIdFromEmail.mockRejectedValue("String error"); // Non-Error exception const params = { repositoryId: "repo123", created_by_user: "nonexistent@example.com", status: "Active", top: 100, skip: 0, }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("Error finding user with email nonexistent@example.com: String error"); }); it("should handle non-Error exceptions in list_pull_requests_by_project", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_repo_or_project); if (!call) throw new Error("repo_list_pull_requests_by_repo_or_project tool not registered"); const [, , , handler] = call; mockGetUserIdFromEmail.mockRejectedValue("String error"); // Non-Error exception const params = { project: "test-project", created_by_user: "nonexistent@example.com", status: "Active", top: 100, skip: 0, }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("Error finding user with email nonexistent@example.com: String error"); }); it("should handle non-Error exceptions in list_pull_requests_by_commits", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.list_pull_requests_by_commits); if (!call) throw new Error("repo_list_pull_requests_by_commits tool not registered"); const [, , , handler] = call; mockGitApi.getPullRequestQuery.mockRejectedValue("String error"); // Non-Error exception const params = { project: "test-project", repository: "test-repo", commits: ["abc123", "def456"], queryType: "LastMergeCommit", }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("Error querying pull requests by commits: String error"); }); it("should handle invalid rightFileEndOffset with rightFileEndLine in create_pull_request_thread", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request_thread); if (!call) throw new Error("repo_create_pull_request_thread tool not registered"); const [, , , handler] = call; const params = { repositoryId: "repo123", pullRequestId: 456, content: "Test comment", filePath: "/test/file.js", rightFileStartLine: 5, rightFileEndLine: 10, rightFileEndOffset: 0, // Invalid end offset when end line is specified }; await expect(handler(params)).rejects.toThrow("rightFileEndOffset must be greater than or equal to 1."); }); it("should handle non-Error exceptions in search_commits", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.search_commits); if (!call) throw new Error("repo_search_commits tool not registered"); const [, , , handler] = call; mockGitApi.getCommits.mockRejectedValue("String error"); // Non-Error exception const params = { project: "test-project", repository: "test-repo", top: 10, skip: 0, }; const result = await handler(params); expect(result.isError).toBe(true); expect(result.content[0].text).toContain("Error searching commits: String error"); }); it("should handle valid rightFileEndOffset with rightFileEndLine in create_pull_request_thread", async () => { configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider); const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === REPO_TOOLS.create_pull_request_thread); if (!call) throw new Error("repo_create_pull_request_thread tool not registered"); const [, , , handler] = call; const mockThread = { id: 1, status: CommentThreadStatus.Active }; mockGitApi.createThread.mockResolvedValue(mockThread); const params = { repositoryId: "repo123", pullRequestId: 456, content: "Test comment", filePath: "/test/file.js", rightFileStartLine: 5, rightFileEndLine: 10, rightFileEndOffset: 20, // Valid end offset with end line }; const result = await handler(params); expect(mockGitApi.createThread).toHaveBeenCalledWith( expect.objectContaining({ threadContext: expect.objectContaining({ rightFileEnd: expect.objectContaining({ line: 10, offset: 20, }), }), }), "repo123", 456, undefined ); expect(result.content[0].text).toBe(JSON.stringify(mockThread, null, 2)); }); }); });

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