/** * Unit tests for BitbucketRouter */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { BitbucketRouter, ToolResult } from '../../src/router.js'; // Mock domain clients - use simple object mocks instead of nested vi.fn vi.mock('../../src/clients/pull-request-client.js', () => { const mockPRClient = { listPullRequests: async () => [ { id: 1, title: 'Test PR 1', state: 'OPEN' }, { id: 2, title: 'Test PR 2', state: 'MERGED' } ], getPullRequest: async () => ({ id: 1, title: 'Test PR', state: 'OPEN', source: { branch: { name: 'feature' } }, destination: { branch: { name: 'main' } } }), getPullRequestStatus: async () => ({ id: 1, title: 'Test PR', state: 'OPEN', status: 'NORMAL' }), getPullRequestActivities: async () => ({ values: [{ action: 'OPEN' }] }), getPullRequestChanges: async () => ({ values: [{ type: 'modified', path: 'src/test.ts' }] }), getPullRequestCommits: async () => ({ values: [{ hash: 'abc123' }] }), getPullRequestDiff: async () => ({ diff: '--- test.ts\n+++ test.ts\n@@ -1 +1 @@' }), getPullRequestPatch: async () => ({ patch: '--- original\n+++ modified' }), getPullRequestParticipants: async () => ({ values: [{ user: { display_name: 'Test User' } }] }), getPullRequestReviewers: async () => [ { user: { display_name: 'Reviewer 1' } } ], getPullRequestTasks: async () => ({ values: [] }), getPullRequestTaskCount: async () => ({ count: 0 }), getFullPullRequest: async () => ({ id: 1, title: 'Test PR', description: 'Test description' }), createPullRequest: async () => ({ id: 10, title: 'New PR', state: 'OPEN' }), updatePullRequest: async () => ({ id: 1, title: 'Updated PR', state: 'OPEN' }), mergePullRequest: async () => ({ id: 1, state: 'MERGED' }), declinePullRequest: async () => ({ id: 1, state: 'DECLINED' }), approvePullRequest: async () => ({ approved: true }), unapprovePullRequest: async () => ({ success: true }), requestChangesPullRequest: async () => ({ state: 'changes_requested' }), removeRequestChangesPullRequest: async () => ({ success: true }) }; return { PullRequestClient: class { constructor() { Object.assign(this, mockPRClient); } } }; }); vi.mock('../../src/clients/repository-client.js', () => { const mockRepoClient = { listWorkspaces: async () => ({ values: [{ slug: 'my-workspace' }] }), listRepositories: async () => ({ values: [{ slug: 'my-repo', name: 'My Repo' }] }), getRepository: async () => ({ slug: 'my-repo', name: 'My Repo', full_name: 'ws/my-repo' }), listBranches: async () => ({ values: [{ name: 'main' }, { name: 'develop' }] }) }; return { RepositoryClient: class { constructor() { Object.assign(this, mockRepoClient); } } }; }); vi.mock('../../src/clients/comment-client.js', () => { const mockCommentClient = { getPullRequestComments: async () => ({ values: [{ id: 1, content: { raw: 'Test comment' } }] }), getPullRequestComment: async () => ({ id: 1, content: { raw: 'Test comment' } }), addPullRequestComment: async () => ({ id: 100, content: { raw: 'New comment' } }), updatePullRequestComment: async () => ({ id: 100, content: { raw: 'Updated' } }), deletePullRequestComment: async () => ({ success: true }), createPullRequestTask: async () => ({ id: 200, content: { raw: 'Task' }, state: 'UNRESOLVED' }), updatePullRequestTask: async () => ({ id: 200, state: 'RESOLVED' }), deletePullRequestTask: async () => ({ success: true }) }; return { CommentClient: class { constructor() { Object.assign(this, mockCommentClient); } } }; }); describe('BitbucketRouter', () => { let router: BitbucketRouter; const originalEnv = { ...process.env }; beforeEach(() => { process.env = { ...originalEnv }; delete process.env.DEFAULT_WORKSPACE; delete process.env.DEFAULT_REPO; router = new BitbucketRouter(); }); afterEach(() => { process.env = originalEnv; }); describe('list_pull_requests', () => { it('should return pull requests for valid parameters', async () => { const result = await router.executeTool('list_pull_requests', { workspace: 'test-workspace', repository: 'test-repo' }) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('pull_requests'); expect(result.data.pull_requests).toHaveLength(2); }); it('should fail without workspace', async () => { const result = await router.executeTool('list_pull_requests', { repository: 'test-repo' }) as ToolResult; expect(result.success).toBe(false); expect(result.error).toContain('Missing required parameters'); }); it('should fail without repository', async () => { const result = await router.executeTool('list_pull_requests', { workspace: 'test-workspace' }) as ToolResult; expect(result.success).toBe(false); expect(result.error).toContain('Missing required parameters'); }); it('should accept "repo" as alias for "repository"', async () => { const result = await router.executeTool('list_pull_requests', { workspace: 'test-workspace', repo: 'test-repo' }) as ToolResult; expect(result.success).toBe(true); }); it('should use DEFAULT_WORKSPACE when workspace is not provided', async () => { process.env.DEFAULT_WORKSPACE = 'default-workspace'; process.env.DEFAULT_REPO = 'default-repo'; const result = await router.executeTool('list_pull_requests', {}) as ToolResult; expect(result.success).toBe(true); }); it('should use DEFAULT_REPO when repository is not provided', async () => { process.env.DEFAULT_REPO = 'default-repo'; const result = await router.executeTool('list_pull_requests', { workspace: 'test-workspace' }) as ToolResult; expect(result.success).toBe(true); }); it('should use both DEFAULT_WORKSPACE and DEFAULT_REPO when neither is provided', async () => { process.env.DEFAULT_WORKSPACE = 'default-workspace'; process.env.DEFAULT_REPO = 'default-repo'; const result = await router.executeTool('list_pull_requests', {}) as ToolResult; expect(result.success).toBe(true); }); it('should prefer explicit params over defaults', async () => { process.env.DEFAULT_WORKSPACE = 'default-workspace'; process.env.DEFAULT_REPO = 'default-repo'; const result = await router.executeTool('list_pull_requests', { workspace: 'explicit-workspace', repository: 'explicit-repo' }) as ToolResult; expect(result.success).toBe(true); }); it('should still fail when no workspace and no default available', async () => { const result = await router.executeTool('list_pull_requests', { repository: 'test-repo' }) as ToolResult; expect(result.success).toBe(false); expect(result.error).toContain('Missing required parameters'); }); it('should still fail when no repository and no default available', async () => { const result = await router.executeTool('list_pull_requests', { workspace: 'test-workspace' }) as ToolResult; expect(result.success).toBe(false); expect(result.error).toContain('Missing required parameters'); }); }); describe('get_pull_request', () => { it('should return pull request for valid parameters', async () => { const result = await router.executeTool('get_pull_request', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1 }) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('id', 1); }); it('should accept pullRequestId as pr_id', async () => { const result = await router.executeTool('get_pull_request', { workspace: 'test-workspace', repository: 'test-repo', pr_id: 1 }) as ToolResult; expect(result.success).toBe(true); }); it('should fail with invalid pullRequestId', async () => { const result = await router.executeTool('get_pull_request', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 'invalid' }) as ToolResult; expect(result.success).toBe(false); }); }); describe('get_pull_request_activities', () => { it('should return activities for valid PR', async () => { const result = await router.executeTool('get_pull_request_activities', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1 }) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('values'); }); it('should accept pagination parameters', async () => { const result = await router.executeTool('get_pull_request_activities', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1, limit: 10, start: 0 }) as ToolResult; expect(result.success).toBe(true); }); }); describe('get_pull_request_changes', () => { it('should return changes for valid PR', async () => { const result = await router.executeTool('get_pull_request_changes', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1 }) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('values'); }); }); describe('get_pull_request_comments', () => { it('should return comments for valid PR', async () => { const result = await router.executeTool('get_pull_request_comments', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1 }) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('values'); }); }); describe('get_pull_request_comment', () => { it('should return specific comment', async () => { const result = await router.executeTool('get_pull_request_comment', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1, commentId: 1 }) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('id', 1); }); it('should fail without commentId', async () => { const result = await router.executeTool('get_pull_request_comment', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1 }) as ToolResult; expect(result.success).toBe(false); }); }); describe('get_pull_request_commits', () => { it('should return commits for valid PR', async () => { const result = await router.executeTool('get_pull_request_commits', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1 }) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('values'); }); }); describe('get_pull_request_diff', () => { it('should return diff for valid PR', async () => { const result = await router.executeTool('get_pull_request_diff', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1 }) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('diff'); }); it('should accept path and context parameters', async () => { const result = await router.executeTool('get_pull_request_diff', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1, path: 'src/test.ts', context: 3 }) as ToolResult; expect(result.success).toBe(true); }); }); describe('get_pull_request_patch', () => { it('should return patch for valid PR', async () => { const result = await router.executeTool('get_pull_request_patch', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1 }) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('patch'); }); }); describe('get_pull_request_participants', () => { it('should return participants for valid PR', async () => { const result = await router.executeTool('get_pull_request_participants', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1 }) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('values'); }); }); describe('get_pull_request_reviewers', () => { it('should return reviewers for valid PR', async () => { const result = await router.executeTool('get_pull_request_reviewers', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1 }) as ToolResult; expect(result.success).toBe(true); }); }); describe('get_pull_request_status', () => { it('should return status for valid PR', async () => { const result = await router.executeTool('get_pull_request_status', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1 }) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('state'); }); }); describe('get_pull_request_tasks', () => { it('should return tasks for valid PR', async () => { const result = await router.executeTool('get_pull_request_tasks', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1 }) as ToolResult; expect(result.success).toBe(true); }); }); describe('get_pull_request_task_count', () => { it('should return task count for valid PR', async () => { const result = await router.executeTool('get_pull_request_task_count', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1 }) as ToolResult; expect(result.success).toBe(true); }); }); describe('get_full_pull_request', () => { it('should return full PR details', async () => { const result = await router.executeTool('get_full_pull_request', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1 }) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('id'); expect(result.data).toHaveProperty('description'); }); }); describe('list_workspaces', () => { it('should return workspaces', async () => { const result = await router.executeTool('list_workspaces', {}) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('values'); }); }); describe('list_repositories', () => { it('should return repositories for valid workspace', async () => { const result = await router.executeTool('list_repositories', { workspace: 'test-workspace', }) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('values'); }); it('should fail without workspace', async () => { const result = await router.executeTool('list_repositories', {}) as ToolResult; expect(result.success).toBe(false); expect(result.error).toContain('Missing required parameters'); }); }); describe('get_repository', () => { it('should return repository metadata', async () => { const result = await router.executeTool('get_repository', { workspace: 'test-workspace', repository: 'test-repo', }) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('slug'); }); }); describe('list_branches', () => { it('should return branches', async () => { const result = await router.executeTool('list_branches', { workspace: 'test-workspace', repository: 'test-repo', }) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('values'); }); }); describe('create_pull_request', () => { it('should create a pull request', async () => { const result = await router.executeTool('create_pull_request', { workspace: 'test-workspace', repository: 'test-repo', title: 'My PR', source_branch: 'feature/x', destination_branch: 'main', }) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('id'); }); it('should fail without title', async () => { const result = await router.executeTool('create_pull_request', { workspace: 'test-workspace', repository: 'test-repo', source_branch: 'feature/x', destination_branch: 'main', }) as ToolResult; expect(result.success).toBe(false); expect(result.error).toContain('Missing required parameters'); }); it('should fail without source_branch', async () => { const result = await router.executeTool('create_pull_request', { workspace: 'test-workspace', repository: 'test-repo', title: 'My PR', destination_branch: 'main', }) as ToolResult; expect(result.success).toBe(false); expect(result.error).toContain('Missing required parameters'); }); }); describe('update_pull_request', () => { it('should update a pull request', async () => { const result = await router.executeTool('update_pull_request', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1, title: 'Updated', }) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('id'); }); }); describe('merge_pull_request', () => { it('should merge a pull request', async () => { const result = await router.executeTool('merge_pull_request', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1, }) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('state', 'MERGED'); }); }); describe('decline_pull_request', () => { it('should decline a pull request', async () => { const result = await router.executeTool('decline_pull_request', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1, }) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('state', 'DECLINED'); }); }); describe('approve_pull_request', () => { it('should approve a pull request', async () => { const result = await router.executeTool('approve_pull_request', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1, }) as ToolResult; expect(result.success).toBe(true); }); }); describe('unapprove_pull_request', () => { it('should remove approval', async () => { const result = await router.executeTool('unapprove_pull_request', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1, }) as ToolResult; expect(result.success).toBe(true); }); }); describe('request_changes_pull_request', () => { it('should request changes', async () => { const result = await router.executeTool('request_changes_pull_request', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1, }) as ToolResult; expect(result.success).toBe(true); }); }); describe('remove_request_changes_pull_request', () => { it('should remove request-changes', async () => { const result = await router.executeTool('remove_request_changes_pull_request', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1, }) as ToolResult; expect(result.success).toBe(true); }); }); describe('add_pull_request_comment', () => { it('should add a comment', async () => { const result = await router.executeTool('add_pull_request_comment', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1, content: 'Great work!', }) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('id'); }); it('should fail without content', async () => { const result = await router.executeTool('add_pull_request_comment', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1, }) as ToolResult; expect(result.success).toBe(false); expect(result.error).toContain('Missing required parameters'); }); }); describe('update_pull_request_comment', () => { it('should update a comment', async () => { const result = await router.executeTool('update_pull_request_comment', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1, commentId: 100, content: 'Edited', }) as ToolResult; expect(result.success).toBe(true); }); }); describe('delete_pull_request_comment', () => { it('should delete a comment', async () => { const result = await router.executeTool('delete_pull_request_comment', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1, commentId: 100, }) as ToolResult; expect(result.success).toBe(true); }); }); describe('create_pull_request_task', () => { it('should create a task', async () => { const result = await router.executeTool('create_pull_request_task', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1, content: 'Review section 3', }) as ToolResult; expect(result.success).toBe(true); expect(result.data).toHaveProperty('id'); }); it('should fail without content', async () => { const result = await router.executeTool('create_pull_request_task', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1, }) as ToolResult; expect(result.success).toBe(false); expect(result.error).toContain('Missing required parameters'); }); }); describe('update_pull_request_task', () => { it('should update a task', async () => { const result = await router.executeTool('update_pull_request_task', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1, taskId: 200, state: 'RESOLVED', }) as ToolResult; expect(result.success).toBe(true); }); }); describe('delete_pull_request_task', () => { it('should delete a task', async () => { const result = await router.executeTool('delete_pull_request_task', { workspace: 'test-workspace', repository: 'test-repo', pullRequestId: 1, taskId: 200, }) as ToolResult; expect(result.success).toBe(true); }); }); describe('unknown tool', () => { it('should return error for unknown tool', async () => { const result = await router.executeTool('unknown_tool', {}) as ToolResult; expect(result.success).toBe(false); expect(result.error).toContain('Unknown tool'); }); }); describe('error handling', () => { it('should handle API errors gracefully', async () => { // Create router with a mock that throws const errorRouter = new BitbucketRouter(); const result = await errorRouter.executeTool('list_pull_requests', { workspace: 'test-workspace', repository: 'test-repo' }) as ToolResult; // With our mock, this should succeed, but error cases should return proper format if (!result.success) { expect(result.error).toBeDefined(); expect(typeof result.error).toBe('string'); } }); }); });