diff --git a/src/bitbucket-client.ts b/src/bitbucket-client.ts index fdfa20a..b4709b2 100644 --- a/src/bitbucket-client.ts +++ b/src/bitbucket-client.ts @@ -1,612 +1,118 @@ -/** - * Bitbucket API client with token configuration integration. - */ +import { ClientOptions } from './clients/base-client.js'; +import { PullRequestClient, CreatePROptions, UpdatePROptions, MergePROptions } from './clients/pull-request-client.js'; +import { RepositoryClient } from './clients/repository-client.js'; +import { CommentClient, AddCommentOptions, CreateTaskOptions, UpdateTaskOptions } from './clients/comment-client.js'; -import axios, { AxiosInstance, AxiosError } from 'axios'; -import { TokenConfigLoader, TokenConfig } from './config.js'; +export type { CreatePROptions, UpdatePROptions, MergePROptions }; +export type { AddCommentOptions, CreateTaskOptions, UpdateTaskOptions }; +export type { ClientOptions as BitbucketClientOptions }; -export interface BitbucketClientOptions { - timeout?: number; - retryCount?: number; -} - -/** - * Bitbucket API client wrapper with authentication and error handling. - */ export class BitbucketClient { - private client!: AxiosInstance; - private tokenSource: string | null = null; - private initialized: boolean = false; + private pr!: PullRequestClient; + private repo!: RepositoryClient; + private comment!: CommentClient; - constructor(options: BitbucketClientOptions = {}) { - // Initialize synchronously to avoid race conditions - this.initializeClient(options); + // ── Pull Request read ───────────────────────────────────────────────────── + listPullRequests!: (workspace: string, repo: string, options?: any) => Promise; + getPullRequest!: (workspace: string, repo: string, prId: number) => Promise; + getPullRequestActivities!: (workspace: string, repo: string, prId: number, options?: any) => Promise; + getPullRequestChanges!: (workspace: string, repo: string, prId: number, options?: any) => Promise; + getPullRequestCommits!: (workspace: string, repo: string, prId: number, options?: any) => Promise; + getPullRequestDiff!: (workspace: string, repo: string, prId: number, options?: any) => Promise; + getPullRequestPatch!: (workspace: string, repo: string, prId: number) => Promise; + getPullRequestParticipants!: (workspace: string, repo: string, prId: number) => Promise; + getPullRequestReviewers!: (workspace: string, repo: string, prId: number) => Promise; + getPullRequestStatus!: (workspace: string, repo: string, prId: number) => Promise; + getPullRequestTasks!: (workspace: string, repo: string, prId: number) => Promise; + getPullRequestTaskCount!: (workspace: string, repo: string, prId: number) => Promise; + getFullPullRequest!: (workspace: string, repo: string, prId: number) => Promise; + + // ── Pull Request write ──────────────────────────────────────────────────── + createPullRequest!: (workspace: string, repo: string, options: CreatePROptions) => Promise; + updatePullRequest!: (workspace: string, repo: string, prId: number, options: UpdatePROptions) => Promise; + mergePullRequest!: (workspace: string, repo: string, prId: number, options: MergePROptions) => Promise; + declinePullRequest!: (workspace: string, repo: string, prId: number) => Promise; + approvePullRequest!: (workspace: string, repo: string, prId: number) => Promise; + unapprovePullRequest!: (workspace: string, repo: string, prId: number) => Promise; + requestChangesPullRequest!: (workspace: string, repo: string, prId: number) => Promise; + removeRequestChangesPullRequest!: (workspace: string, repo: string, prId: number) => Promise; + + // ── Repository / workspace ──────────────────────────────────────────────── + listWorkspaces!: (options?: any) => Promise; + listRepositories!: (workspace: string, options?: any) => Promise; + getRepository!: (workspace: string, repo: string) => Promise; + listBranches!: (workspace: string, repo: string, options?: any) => Promise; + + // ── Comment read ────────────────────────────────────────────────────────── + getPullRequestComments!: (workspace: string, repo: string, prId: number, options?: any) => Promise; + getPullRequestComment!: (workspace: string, repo: string, prId: number, commentId: number) => Promise; + + // ── Comment / task write ────────────────────────────────────────────────── + addPullRequestComment!: (workspace: string, repo: string, prId: number, options: AddCommentOptions) => Promise; + updatePullRequestComment!: (workspace: string, repo: string, prId: number, commentId: number, options: AddCommentOptions) => Promise; + deletePullRequestComment!: (workspace: string, repo: string, prId: number, commentId: number) => Promise; + createPullRequestTask!: (workspace: string, repo: string, prId: number, options: CreateTaskOptions) => Promise; + updatePullRequestTask!: (workspace: string, repo: string, prId: number, taskId: number, options: UpdateTaskOptions) => Promise; + deletePullRequestTask!: (workspace: string, repo: string, prId: number, taskId: number) => Promise; + + constructor(options: ClientOptions = {}) { + this.pr = new PullRequestClient(options); + this.repo = new RepositoryClient(options); + this.comment = new CommentClient(options); + + // Bind all PR methods + this.listPullRequests = this.pr.listPullRequests.bind(this.pr); + this.getPullRequest = this.pr.getPullRequest.bind(this.pr); + this.getPullRequestActivities = this.pr.getPullRequestActivities.bind(this.pr); + this.getPullRequestChanges = this.pr.getPullRequestChanges.bind(this.pr); + this.getPullRequestCommits = this.pr.getPullRequestCommits.bind(this.pr); + this.getPullRequestDiff = this.pr.getPullRequestDiff.bind(this.pr); + this.getPullRequestPatch = this.pr.getPullRequestPatch.bind(this.pr); + this.getPullRequestParticipants = this.pr.getPullRequestParticipants.bind(this.pr); + this.getPullRequestReviewers = this.pr.getPullRequestReviewers.bind(this.pr); + this.getPullRequestStatus = this.pr.getPullRequestStatus.bind(this.pr); + this.getPullRequestTasks = this.pr.getPullRequestTasks.bind(this.pr); + this.getPullRequestTaskCount = this.pr.getPullRequestTaskCount.bind(this.pr); + this.getFullPullRequest = this.pr.getFullPullRequest.bind(this.pr); + + this.createPullRequest = this.pr.createPullRequest.bind(this.pr); + this.updatePullRequest = this.pr.updatePullRequest.bind(this.pr); + this.mergePullRequest = this.pr.mergePullRequest.bind(this.pr); + this.declinePullRequest = this.pr.declinePullRequest.bind(this.pr); + this.approvePullRequest = this.pr.approvePullRequest.bind(this.pr); + this.unapprovePullRequest = this.pr.unapprovePullRequest.bind(this.pr); + this.requestChangesPullRequest = this.pr.requestChangesPullRequest.bind(this.pr); + this.removeRequestChangesPullRequest = this.pr.removeRequestChangesPullRequest.bind(this.pr); + + // Bind all repo methods + this.listWorkspaces = this.repo.listWorkspaces.bind(this.repo); + this.listRepositories = this.repo.listRepositories.bind(this.repo); + this.getRepository = this.repo.getRepository.bind(this.repo); + this.listBranches = this.repo.listBranches.bind(this.repo); + + // Bind all comment methods + this.getPullRequestComments = this.comment.getPullRequestComments.bind(this.comment); + this.getPullRequestComment = this.comment.getPullRequestComment.bind(this.comment); + + this.addPullRequestComment = this.comment.addPullRequestComment.bind(this.comment); + this.updatePullRequestComment = this.comment.updatePullRequestComment.bind(this.comment); + this.deletePullRequestComment = this.comment.deletePullRequestComment.bind(this.comment); + this.createPullRequestTask = this.comment.createPullRequestTask.bind(this.comment); + this.updatePullRequestTask = this.comment.updatePullRequestTask.bind(this.comment); + this.deletePullRequestTask = this.comment.deletePullRequestTask.bind(this.comment); } - private async initializeClient(options: BitbucketClientOptions): Promise { - try { - const config = await this.loadTokenConfig(); - this.tokenSource = config.source; - - this.client = axios.create({ - baseURL: 'https://api.bitbucket.org/2.0', - headers: { - 'Content-Type': 'application/json' - }, - auth: { - username: config.email, - password: config.token - }, - timeout: options.timeout || 30000, - maxRedirects: 5 - }); - - // Request interceptor for token refresh and logging - this.client.interceptors.request.use(config => { - config.headers['User-Agent'] = `Bitbucket-MCP-Server/${this.getVersion()}`; - return config; - }); - - // Response interceptor for rate limiting and errors - this.client.interceptors.response.use( - response => response, - async (error: AxiosError) => { - await this.handleResponseError(error); - } - ); - - this.initialized = true; - } catch (error) { - console.error('Failed to load token configuration:', error); - throw new Error('Unable to initialize Bitbucket client - check token configuration'); - } - } - - private async ensureInitialized(): Promise { - if (!this.initialized) { - // Wait for initialization to complete - await new Promise(resolve => { - const checkInit = () => { - if (this.initialized) { - resolve(void 0); - } else { - setTimeout(checkInit, 10); - } - }; - checkInit(); - }); - } - } - - private async loadTokenConfig(): Promise { - return TokenConfigLoader.load(); - } - - private getVersion(): string { - try { - // Use synchronous fs for version lookup - no async needed - const fs = require('fs'); - const path = require('path'); - const pkgPath = path.join(process.cwd(), 'package.json'); - if (fs.existsSync(pkgPath)) { - const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); - return pkg.version || '1.0.0'; - } - return '1.0.0'; - } catch { - return '1.0.0'; - } - } - - private async handleResponseError(error: AxiosError): Promise { - const status = error.response?.status; - - if (status === 401) { - // Authentication error - provide detailed logging - const data = error.response?.data; - console.error('🔐 Bitbucket API Authentication Error (401):'); - console.error(` Token source: ${this.tokenSource || 'unknown'}`); - console.error(` Request URL: ${error.config?.url}`); - console.error(` Request method: ${error.config?.method?.toUpperCase()}`); - - if (typeof data === 'object' && data !== null) { - console.error(' Response data:', JSON.stringify(data, null, 2)); - } else if (data) { - console.error(` Response data: ${data}`); - } - - // Check if token might be expired or malformed - const authHeader = error.config?.headers?.Authorization; - if (typeof authHeader === 'string') { - const token = authHeader.replace('Bearer ', ''); - const tokenLength = token.length; - const isJWT = token.includes('.'); - console.error(` Token info: length=${tokenLength}, appears to be JWT=${isJWT}`); - } - - console.error(' Possible causes:'); - console.error(' - Token expired'); - console.error(' - Token lacks required permissions (needs repository read access)'); - console.error(' - Token is malformed or corrupted'); - console.error(' - Repository/workspace access denied'); - } else if (status === 403) { - // Forbidden - similar to auth but different permissions - console.error('🚫 Bitbucket API Forbidden Error (403):'); - console.error(` Token source: ${this.tokenSource || 'unknown'}`); - console.error(` Request URL: ${error.config?.url}`); - console.error(' Possible causes:'); - console.error(' - Token lacks permission to access this repository'); - console.error(' - Repository is private and token has insufficient scope'); - } else if (status === 429) { - // Rate limited - wait and retry - const retryAfter = error.response?.headers['retry-after']; - const delay = retryAfter ? parseInt(retryAfter, 10) * 1000 : 5000; - - console.log(`Rate limited. Retrying in ${delay}ms...`); - await this.sleep(delay); - } else if (status && status >= 400 && status < 500) { - // Other client error - log but don't retry - const data = error.response?.data; - let message: string; - if (typeof data === 'object' && data !== null && 'message' in data) { - message = String(data.message); - } else { - message = `Client error ${status}`; - } - console.error(`Bitbucket API ${status}: ${message}`); - } else if (status && status >= 500) { - // Server error - could implement exponential backoff retry - console.warn(`Server error ${status}, will retry...`); - } - - throw error; - } - - private sleep(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); - } - - /** - * Validate the authentication token by making a test API call. - */ async validateToken(): Promise<{ valid: boolean; message: string }> { - await this.ensureInitialized(); - try { - // Make a simple API call to test authentication - const response = await this.client.get('/user'); + await this.repo.listWorkspaces({ pagelen: 1 }); return { valid: true, message: 'Token is valid' }; } catch (error: any) { - const status = error.response?.status; - if (status === 401) { - return { valid: false, message: 'Token is invalid or expired' }; - } else if (status === 403) { - return { valid: false, message: 'Token lacks required permissions' }; - } else { - return { valid: false, message: `Token validation failed: ${this.formatError(error)}` }; - } + const status = error?.response?.status ?? error?.cause?.response?.status; + if (status === 401) return { valid: false, message: 'Token is invalid or expired' }; + if (status === 403) return { valid: false, message: 'Token lacks required permissions' }; + return { valid: false, message: `Token validation failed: ${error?.message || error}` }; } } - - /** - * List pull requests in a repository. - */ - async listPullRequests( - workspace: string, - repoSlug: string, - options?: { - state?: 'open' | 'closed' | 'all'; - author?: string; - reviewer?: string; - since?: string; - } - ): Promise { - await this.ensureInitialized(); - - const params: Record = {}; - - if (options?.state) params.state = options.state; - if (options?.author) params.author = options.author; - if (options?.reviewer) params.reviewer = options.reviewer; - if (options?.since) params.since = options.since; - - try { - const response = await this.client.get( - `/repositories/${workspace}/${repoSlug}/pullrequests`, - { params } - ); - - return response.data.values || []; - } catch (error) { - throw new Error(`Failed to list pull requests: ${this.formatError(error)}`); - } - } - - /** - * Get a specific pull request. - */ - async getPullRequest( - workspace: string, - repoSlug: string, - prId: number - ): Promise { - await this.ensureInitialized(); - - try { - const response = await this.client.get( - `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}` - ); - return response.data; - } catch (error) { - throw new Error(`Failed to get pull request: ${this.formatError(error)}`); - } - } - - /** - * Get pull request activities (events/actions on the PR). - */ - async getPullRequestActivities( - workspace: string, - repoSlug: string, - prId: number, - options?: { - limit?: number; - start?: number; - } - ): Promise { - await this.ensureInitialized(); - - try { - const params: Record = {}; - if (options?.limit) params.limit = options.limit; - if (options?.start) params.start = options.start; - - const response = await this.client.get( - `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/activity`, - { params } - ); - return response.data; - } catch (error) { - throw new Error(`Failed to get pull request activities: ${this.formatError(error)}`); - } - } - - /** - * Get pull request changes (files modified in the PR). - */ - async getPullRequestChanges( - workspace: string, - repoSlug: string, - prId: number, - options?: { - limit?: number; - start?: number; - } - ): Promise { - await this.ensureInitialized(); - - try { - const params: Record = {}; - if (options?.limit) params.limit = options.limit; - if (options?.start) params.start = options.start; - - const prResponse = await this.client.get( - `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}` - ); - const sourceHash = prResponse.data.source?.commit?.hash; - const destHash = prResponse.data.destination?.commit?.hash; - - if (sourceHash && destHash) { - const changesPath = `/repositories/${workspace}/${repoSlug}/diffstat/${sourceHash}..${destHash}`; - const response = await this.client.get(changesPath, { params }); - return response.data; - } - - throw new Error('Could not determine source and destination commits for PR'); - } catch (error) { - throw new Error(`Failed to get pull request changes: ${this.formatError(error)}`); - } - } - - /** - * Get pull request comments. - */ - async getPullRequestComments( - workspace: string, - repoSlug: string, - prId: number, - options?: { - limit?: number; - start?: number; - } - ): Promise { - await this.ensureInitialized(); - - try { - const params: Record = {}; - if (options?.limit) params.limit = options.limit; - if (options?.start) params.start = options.start; - - const response = await this.client.get( - `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/comments`, - { params } - ); - return response.data; - } catch (error) { - throw new Error(`Failed to get pull request comments: ${this.formatError(error)}`); - } - } - - /** - * Get a specific pull request comment. - */ - async getPullRequestComment( - workspace: string, - repoSlug: string, - prId: number, - commentId: number - ): Promise { - await this.ensureInitialized(); - - try { - const response = await this.client.get( - `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/comments/${commentId}` - ); - return response.data; - } catch (error) { - throw new Error(`Failed to get pull request comment: ${this.formatError(error)}`); - } - } - - /** - * Get commits included in a pull request. - */ - async getPullRequestCommits( - workspace: string, - repoSlug: string, - prId: number, - options?: { - limit?: number; - start?: number; - } - ): Promise { - await this.ensureInitialized(); - - try { - const params: Record = {}; - if (options?.limit) params.limit = options.limit; - if (options?.start) params.start = options.start; - - const response = await this.client.get( - `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/commits`, - { params } - ); - return response.data; - } catch (error) { - throw new Error(`Failed to get pull request commits: ${this.formatError(error)}`); - } - } - - /** - * Get diff for a pull request. - */ - async getPullRequestDiff( - workspace: string, - repoSlug: string, - prId: number, - options?: { - context?: number; - path?: string; - whitespace?: 'ignore-all' | 'ignore-changing' | 'ignore-eol' | 'show-all'; - } - ): Promise { - await this.ensureInitialized(); - - try { - const params: Record = {}; - if (options?.context) params.context = options.context; - if (options?.whitespace) params.whitespace = options.whitespace; - - if (options?.path) { - const response = await this.client.get( - `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/diff/${options.path}`, - { params, responseType: 'text' } - ); - return response.data; - } - - const prResponse = await this.client.get( - `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}` - ); - const sourceHash = prResponse.data.source?.commit?.hash; - const destHash = prResponse.data.destination?.commit?.hash; - - if (sourceHash && destHash) { - const diffPath = `/repositories/${workspace}/${repoSlug}/diff/${sourceHash}..${destHash}`; - const response = await this.client.get(diffPath, { params, responseType: 'text' }); - return response.data; - } - - throw new Error('Could not determine source and destination commits for PR'); - } catch (error) { - throw new Error(`Failed to get pull request diff: ${this.formatError(error)}`); - } - } - - /** - * Get raw patch for a pull request. - */ - async getPullRequestPatch( - workspace: string, - repoSlug: string, - prId: number - ): Promise { - await this.ensureInitialized(); - - try { - const prResponse = await this.client.get( - `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}` - ); - const sourceHash = prResponse.data.source?.commit?.hash; - const destHash = prResponse.data.destination?.commit?.hash; - - if (sourceHash && destHash) { - const patchPath = `/repositories/${workspace}/${repoSlug}/diff/${sourceHash}..${destHash}`; - const response = await this.client.get(patchPath, { responseType: 'text' }); - return { patch: response.data }; - } - - throw new Error('Could not determine source and destination commits for PR'); - } catch (error) { - throw new Error(`Failed to get pull request patch: ${this.formatError(error)}`); - } - } - - /** - * Get participants of a pull request. - */ - async getPullRequestParticipants( - workspace: string, - repoSlug: string, - prId: number - ): Promise { - await this.ensureInitialized(); - - try { - const response = await this.client.get( - `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}` - ); - return response.data.participants || []; - } catch (error) { - throw new Error(`Failed to get pull request participants: ${this.formatError(error)}`); - } - } - - /** - * Get reviewers of a pull request. - */ - async getPullRequestReviewers( - workspace: string, - repoSlug: string, - prId: number - ): Promise { - await this.ensureInitialized(); - - try { - const response = await this.client.get( - `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}` - ); - return response.data.reviewers || []; - } catch (error) { - throw new Error(`Failed to get pull request reviewers: ${this.formatError(error)}`); - } - } - - /** - * Get status of a pull request (merged, open, declined). - */ - async getPullRequestStatus( - workspace: string, - repoSlug: string, - prId: number - ): Promise { - await this.ensureInitialized(); - - try { - const response = await this.client.get( - `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}` - ); - const pr = response.data; - return { - id: pr.id, - title: pr.title, - state: pr.state, - status: pr.status, - author: pr.author, - source_branch: pr.source?.branch?.name, - destination_branch: pr.destination?.branch?.name, - created_on: pr.created_on, - updated_on: pr.updated_on, - closed_on: pr.closed_on, - merge_commit: pr.merge_commit - }; - } catch (error) { - throw new Error(`Failed to get pull request status: ${this.formatError(error)}`); - } - } - - /** - * Get tasks associated with a pull request. - */ - async getPullRequestTasks( - workspace: string, - repoSlug: string, - prId: number - ): Promise { - await this.ensureInitialized(); - - try { - const response = await this.client.get( - `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/tasks` - ); - return response.data; - } catch (error) { - throw new Error(`Failed to get pull request tasks: ${this.formatError(error)}`); - } - } - - /** - * Get task count for a pull request. - */ - async getPullRequestTaskCount( - workspace: string, - repoSlug: string, - prId: number - ): Promise { - await this.ensureInitialized(); - - try { - const response = await this.client.get( - `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/tasks` - ); - const tasks = response.data; - return { count: tasks.values?.length || 0 }; - } catch (error) { - throw new Error(`Failed to get pull request task count: ${this.formatError(error)}`); - } - } - - /** - * Get the full raw pull request with all details. - */ - async getFullPullRequest( - workspace: string, - repoSlug: string, - prId: number - ): Promise { - await this.ensureInitialized(); - - try { - const response = await this.client.get( - `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`, - { - params: { - fields: '+*' - } - } - ); - return response.data; - } catch (error) { - throw new Error(`Failed to get full pull request: ${this.formatError(error)}`); - } - } - - private formatError(error: any): string { - if (error?.response?.status) { - return `HTTP ${error.response.status}: ${error.message}`; - } - return error?.message || 'Unknown error'; - } } -export default BitbucketClient; \ No newline at end of file +export default BitbucketClient; diff --git a/tests/unit/router.test.ts b/tests/unit/router.test.ts index 922b5a5..3673a9d 100644 --- a/tests/unit/router.test.ts +++ b/tests/unit/router.test.ts @@ -2,68 +2,118 @@ * Unit tests for BitbucketRouter */ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { BitbucketRouter, ToolResult } from '../../src/router.js'; -// Mock the BitbucketClient -vi.mock('../../src/bitbucket-client.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: 1 }), + updatePullRequest: async () => ({ id: 1 }), + mergePullRequest: async () => ({ id: 1 }), + declinePullRequest: async () => ({ id: 1 }), + approvePullRequest: async () => ({ id: 1 }), + unapprovePullRequest: async () => ({ id: 1 }), + requestChangesPullRequest: async () => ({ id: 1 }), + removeRequestChangesPullRequest: async () => ({ id: 1 }) + }; + return { - BitbucketClient: vi.fn().mockImplementation(() => ({ - listPullRequests: vi.fn().mockResolvedValue([ - { id: 1, title: 'Test PR 1', state: 'OPEN' }, - { id: 2, title: 'Test PR 2', state: 'MERGED' } - ]), - getPullRequest: vi.fn().mockResolvedValue({ - id: 1, - title: 'Test PR', - state: 'OPEN', - source: { branch: { name: 'feature' } }, - destination: { branch: { name: 'main' } } - }), - getPullRequestStatus: vi.fn().mockResolvedValue({ - id: 1, - title: 'Test PR', - state: 'OPEN', - status: 'NORMAL' - }), - getPullRequestActivities: vi.fn().mockResolvedValue({ - values: [{ action: 'OPEN' }] - }), - getPullRequestChanges: vi.fn().mockResolvedValue({ - values: [{ type: 'modified', path: 'src/test.ts' }] - }), - getPullRequestComments: vi.fn().mockResolvedValue({ - values: [{ id: 1, content: { raw: 'Test comment' } }] - }), - getPullRequestComment: vi.fn().mockResolvedValue({ - id: 1, - content: { raw: 'Test comment' } - }), - getPullRequestCommits: vi.fn().mockResolvedValue({ - values: [{ hash: 'abc123' }] - }), - getPullRequestDiff: vi.fn().mockResolvedValue({ - diff: '--- test.ts\n+++ test.ts\n@@ -1 +1 @@' - }), - getPullRequestPatch: vi.fn().mockResolvedValue({ - patch: '--- original\n+++ modified' - }), - getPullRequestParticipants: vi.fn().mockResolvedValue({ - values: [{ user: { display_name: 'Test User' } }] - }), - getPullRequestReviewers: vi.fn().mockResolvedValue([ - { user: { display_name: 'Reviewer 1' } } - ]), - getPullRequestTasks: vi.fn().mockResolvedValue({ - values: [] - }), - getPullRequestTaskCount: vi.fn().mockResolvedValue({ count: 0 }), - getFullPullRequest: vi.fn().mockResolvedValue({ - id: 1, - title: 'Test PR', - description: 'Test description' - }) - })) + PullRequestClient: class { + constructor() { + Object.assign(this, mockPRClient); + } + } + }; +}); + +vi.mock('../../src/clients/repository-client.js', () => { + const mockRepoClient = { + listWorkspaces: async () => ({ values: [] }), + listRepositories: async () => ({ values: [] }), + getRepository: async () => ({ slug: 'test' }), + listBranches: async () => ({ values: [] }) + }; + + 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: 1 }), + updatePullRequestComment: async () => ({ id: 1 }), + deletePullRequestComment: async () => ({ success: true }), + createPullRequestTask: async () => ({ id: 1 }), + updatePullRequestTask: async () => ({ id: 1 }), + deletePullRequestTask: async () => ({ success: true }) + }; + + return { + CommentClient: class { + constructor() { + Object.assign(this, mockCommentClient); + } + } }; });