@atlassian-dc-mcp/bitbucket 0.34.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +55 -2
  3. package/build/__tests__/bitbucket-service.test.js +41 -2
  4. package/build/__tests__/bitbucket-service.test.js.map +1 -1
  5. package/build/__tests__/config.test.js +2 -1
  6. package/build/__tests__/config.test.js.map +1 -1
  7. package/build/__tests__/pr-builds.test.d.ts +2 -0
  8. package/build/__tests__/pr-builds.test.d.ts.map +1 -0
  9. package/build/__tests__/pr-builds.test.js +74 -0
  10. package/build/__tests__/pr-builds.test.js.map +1 -0
  11. package/build/__tests__/pr-merge.test.js +5 -4
  12. package/build/__tests__/pr-merge.test.js.map +1 -1
  13. package/build/__tests__/pr-state.test.d.ts +2 -0
  14. package/build/__tests__/pr-state.test.d.ts.map +1 -0
  15. package/build/__tests__/pr-state.test.js +118 -0
  16. package/build/__tests__/pr-state.test.js.map +1 -0
  17. package/build/__tests__/repo-gateway.test.d.ts +2 -0
  18. package/build/__tests__/repo-gateway.test.d.ts.map +1 -0
  19. package/build/__tests__/{merge-gateway.test.js → repo-gateway.test.js} +70 -10
  20. package/build/__tests__/repo-gateway.test.js.map +1 -0
  21. package/build/bitbucket-service.d.ts +44 -0
  22. package/build/bitbucket-service.d.ts.map +1 -1
  23. package/build/bitbucket-service.js +59 -0
  24. package/build/bitbucket-service.js.map +1 -1
  25. package/build/index.js +33 -1
  26. package/build/index.js.map +1 -1
  27. package/build/pr-builds.d.ts +36 -0
  28. package/build/pr-builds.d.ts.map +1 -0
  29. package/build/pr-builds.js +40 -0
  30. package/build/pr-builds.js.map +1 -0
  31. package/build/pr-merge.d.ts +2 -2
  32. package/build/pr-merge.d.ts.map +1 -1
  33. package/build/pr-merge.js +2 -2
  34. package/build/pr-merge.js.map +1 -1
  35. package/build/pr-state.d.ts +33 -0
  36. package/build/pr-state.d.ts.map +1 -0
  37. package/build/pr-state.js +37 -0
  38. package/build/pr-state.js.map +1 -0
  39. package/build/repo-gateway.d.ts +57 -0
  40. package/build/repo-gateway.d.ts.map +1 -0
  41. package/build/repo-gateway.js +122 -0
  42. package/build/repo-gateway.js.map +1 -0
  43. package/package.json +3 -3
  44. package/src/__tests__/bitbucket-service.test.ts +50 -2
  45. package/src/__tests__/config.test.ts +2 -1
  46. package/src/__tests__/pr-builds.test.ts +93 -0
  47. package/src/__tests__/pr-merge.test.ts +5 -5
  48. package/src/__tests__/pr-state.test.ts +150 -0
  49. package/src/__tests__/{merge-gateway.test.ts → repo-gateway.test.ts} +83 -11
  50. package/src/bitbucket-service.ts +67 -0
  51. package/src/index.ts +52 -1
  52. package/src/pr-builds.ts +74 -0
  53. package/src/pr-merge.ts +3 -3
  54. package/src/pr-state.ts +68 -0
  55. package/src/repo-gateway.ts +183 -0
  56. package/tsconfig.tsbuildinfo +1 -1
  57. package/build/__tests__/merge-gateway.test.d.ts +0 -2
  58. package/build/__tests__/merge-gateway.test.d.ts.map +0 -1
  59. package/build/__tests__/merge-gateway.test.js.map +0 -1
  60. package/build/merge-gateway.d.ts +0 -34
  61. package/build/merge-gateway.d.ts.map +0 -1
  62. package/build/merge-gateway.js +0 -86
  63. package/build/merge-gateway.js.map +0 -1
  64. package/src/merge-gateway.ts +0 -132
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Operator-controlled gate for pull request operations that change state on a shared branch
3
+ * or in front of other people.
4
+ *
5
+ * A merge cannot be undone through the API, and a decline is visible to every reviewer the
6
+ * moment it lands, so both are disabled by default: the tool is not even registered unless
7
+ * the operator enables it and names the repositories it may act on. Each operation reads its
8
+ * own environment variables, so allowing declines never implies allowing merges. The scope is
9
+ * read once at startup, so the model can never select or widen it. Read-only checks are not
10
+ * gated.
11
+ */
12
+ /** Names an operation in refusal messages and points the operator at the variables that open it. */
13
+ export interface GatewayPolicy {
14
+ /** Short operation name, used to tag warnings. */
15
+ name: string;
16
+ /** Gerund that starts a refusal message, e.g. "Merging". */
17
+ gerund: string;
18
+ flagVar: string;
19
+ reposVar: string;
20
+ /** Omitted for operations that have no meaningful target-branch restriction. */
21
+ targetRefsVar?: string;
22
+ }
23
+ export declare const MERGE_POLICY: GatewayPolicy;
24
+ /**
25
+ * Declining has no target-ref restriction: which branch a pull request would have landed on
26
+ * says nothing about whether abandoning it is safe.
27
+ */
28
+ export declare const DECLINE_POLICY: GatewayPolicy;
29
+ export interface RepoGateway {
30
+ /** Whether the operation is enabled and at least one valid repository pattern resolved. */
31
+ enabled: boolean;
32
+ /** Allowed `PROJECT/repository-slug` targets; a `PROJECT/*` entry allows the whole project. */
33
+ repos: string[];
34
+ /** Allowed target refs; a trailing `*` matches a prefix. Empty means any ref in an allowed repository. */
35
+ targetRefs: string[];
36
+ policy: GatewayPolicy;
37
+ }
38
+ type Env = Record<string, string | undefined>;
39
+ type Warn = (message: string) => void;
40
+ export interface ResolveGatewayOptions {
41
+ env?: Env;
42
+ warn?: Warn;
43
+ }
44
+ /**
45
+ * Reads one operation's gateway configuration from the environment. The operation only
46
+ * activates when its flag is set and at least one repository entry is valid; otherwise a
47
+ * warning is logged and the gateway stays disabled.
48
+ */
49
+ export declare function resolveGateway(policy: GatewayPolicy, options?: ResolveGatewayOptions): RepoGateway;
50
+ export declare function resolveMergeGateway(options?: ResolveGatewayOptions): RepoGateway;
51
+ export declare function resolveDeclineGateway(options?: ResolveGatewayOptions): RepoGateway;
52
+ /** Throws unless the gateway allows its operation in this repository. No network call. */
53
+ export declare function assertRepoOperationAllowed(gateway: RepoGateway, projectKey: string, repositorySlug: string): void;
54
+ /** Throws unless the gateway allows merging into this target ref. A gateway with no ref restriction allows all. */
55
+ export declare function assertTargetRefMergeAllowed(gateway: RepoGateway, targetRefId: string | undefined): void;
56
+ export {};
57
+ //# sourceMappingURL=repo-gateway.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo-gateway.d.ts","sourceRoot":"","sources":["../src/repo-gateway.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,oGAAoG;AACpG,MAAM,WAAW,aAAa;IAC5B,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,gFAAgF;IAChF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,YAAY,EAAE,aAM1B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,aAK5B,CAAC;AAEF,MAAM,WAAW,WAAW;IAC1B,2FAA2F;IAC3F,OAAO,EAAE,OAAO,CAAC;IACjB,+FAA+F;IAC/F,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,0GAA0G;IAC1G,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAC9C,KAAK,IAAI,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAEtC,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,EAAE,GAAG,CAAC;IACV,IAAI,CAAC,EAAE,IAAI,CAAC;CACb;AA+CD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,WAAW,CA2BlG;AAED,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,WAAW,CAEhF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,WAAW,CAElF;AAED,0FAA0F;AAC1F,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,CAcjH;AAED,mHAAmH;AACnH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAgBvG"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Operator-controlled gate for pull request operations that change state on a shared branch
3
+ * or in front of other people.
4
+ *
5
+ * A merge cannot be undone through the API, and a decline is visible to every reviewer the
6
+ * moment it lands, so both are disabled by default: the tool is not even registered unless
7
+ * the operator enables it and names the repositories it may act on. Each operation reads its
8
+ * own environment variables, so allowing declines never implies allowing merges. The scope is
9
+ * read once at startup, so the model can never select or widen it. Read-only checks are not
10
+ * gated.
11
+ */
12
+ export const MERGE_POLICY = {
13
+ name: 'merge',
14
+ gerund: 'Merging',
15
+ flagVar: 'BITBUCKET_MERGE_ENABLED',
16
+ reposVar: 'BITBUCKET_MERGE_ALLOWED_REPOS',
17
+ targetRefsVar: 'BITBUCKET_MERGE_ALLOWED_TARGET_REFS',
18
+ };
19
+ /**
20
+ * Declining has no target-ref restriction: which branch a pull request would have landed on
21
+ * says nothing about whether abandoning it is safe.
22
+ */
23
+ export const DECLINE_POLICY = {
24
+ name: 'decline',
25
+ gerund: 'Declining',
26
+ flagVar: 'BITBUCKET_DECLINE_ENABLED',
27
+ reposVar: 'BITBUCKET_DECLINE_ALLOWED_REPOS',
28
+ };
29
+ const REPO_ENTRY_RE = /^[^\s/]+\/[^\s/]+$/;
30
+ function readBool(env, name) {
31
+ const value = env[name]?.trim().toLowerCase();
32
+ return value === 'true' || value === '1' || value === 'yes';
33
+ }
34
+ function parseList(raw) {
35
+ return (raw ?? '').split(/[,;\s]+/).map(entry => entry.trim()).filter(Boolean);
36
+ }
37
+ function dedupe(values) {
38
+ return [...new Set(values)];
39
+ }
40
+ /**
41
+ * Accepts `PROJECT/repository-slug` or `PROJECT/*`. The project key is upper-cased and
42
+ * the slug lower-cased to match the casing the REST API uses, so comparisons are exact.
43
+ */
44
+ function normalizeRepoEntry(entry, warn) {
45
+ if (!REPO_ENTRY_RE.test(entry)) {
46
+ warn(`Ignoring repository entry that is not "PROJECT/repository-slug" or "PROJECT/*": "${entry}"`);
47
+ return undefined;
48
+ }
49
+ const [projectKey, slug] = entry.split('/');
50
+ if (projectKey === '*') {
51
+ warn(`Ignoring repository entry that would allow every project: "${entry}"`);
52
+ return undefined;
53
+ }
54
+ return `${projectKey.toUpperCase()}/${slug.toLowerCase()}`;
55
+ }
56
+ /** Bare branch names are expanded so operators can write `develop` instead of `refs/heads/develop`. */
57
+ function normalizeRefEntry(entry) {
58
+ return entry.startsWith('refs/') ? entry : `refs/heads/${entry}`;
59
+ }
60
+ function matchesPattern(value, pattern) {
61
+ return pattern.endsWith('*') ? value.startsWith(pattern.slice(0, -1)) : value === pattern;
62
+ }
63
+ function disabledGateway(policy) {
64
+ return { enabled: false, repos: [], targetRefs: [], policy };
65
+ }
66
+ /**
67
+ * Reads one operation's gateway configuration from the environment. The operation only
68
+ * activates when its flag is set and at least one repository entry is valid; otherwise a
69
+ * warning is logged and the gateway stays disabled.
70
+ */
71
+ export function resolveGateway(policy, options) {
72
+ const env = options?.env ?? process.env;
73
+ const warn = options?.warn ?? ((message) => console.error(`[${policy.name}-gateway] ${message}`));
74
+ if (!readBool(env, policy.flagVar)) {
75
+ return disabledGateway(policy);
76
+ }
77
+ const repos = dedupe(parseList(env[policy.reposVar])
78
+ .map(entry => normalizeRepoEntry(entry, warn))
79
+ .filter((entry) => Boolean(entry)));
80
+ if (repos.length === 0) {
81
+ warn(`${policy.gerund} was enabled but no valid repository is configured (set ${policy.reposVar} ` +
82
+ 'to a list of "PROJECT/repository-slug" or "PROJECT/*" entries); the tool will stay disabled.');
83
+ return disabledGateway(policy);
84
+ }
85
+ const targetRefs = policy.targetRefsVar
86
+ ? dedupe(parseList(env[policy.targetRefsVar]).map(normalizeRefEntry))
87
+ : [];
88
+ return { enabled: true, repos, targetRefs, policy };
89
+ }
90
+ export function resolveMergeGateway(options) {
91
+ return resolveGateway(MERGE_POLICY, options);
92
+ }
93
+ export function resolveDeclineGateway(options) {
94
+ return resolveGateway(DECLINE_POLICY, options);
95
+ }
96
+ /** Throws unless the gateway allows its operation in this repository. No network call. */
97
+ export function assertRepoOperationAllowed(gateway, projectKey, repositorySlug) {
98
+ const { policy } = gateway;
99
+ if (!gateway.enabled) {
100
+ throw new Error(`${policy.gerund} pull requests is disabled on this server. Enable it with ${policy.flagVar} ` +
101
+ `and list the allowed repositories in ${policy.reposVar}.`);
102
+ }
103
+ const target = `${projectKey.toUpperCase()}/${repositorySlug.toLowerCase()}`;
104
+ if (!gateway.repos.some(pattern => matchesPattern(target, pattern))) {
105
+ throw new Error(`${policy.gerund} is not allowed in ${target} on this server. Allowed: ${gateway.repos.join(', ')}.`);
106
+ }
107
+ }
108
+ /** Throws unless the gateway allows merging into this target ref. A gateway with no ref restriction allows all. */
109
+ export function assertTargetRefMergeAllowed(gateway, targetRefId) {
110
+ if (gateway.targetRefs.length === 0) {
111
+ return;
112
+ }
113
+ if (!targetRefId) {
114
+ throw new Error('Could not determine the target branch of the pull request, and this server restricts which ' +
115
+ `branches may be merged into (${gateway.policy.targetRefsVar}); refusing to merge.`);
116
+ }
117
+ if (!gateway.targetRefs.some(pattern => matchesPattern(targetRefId, pattern))) {
118
+ throw new Error(`Merging into ${targetRefId} is not allowed on this server. ` +
119
+ `Allowed target refs: ${gateway.targetRefs.join(', ')}.`);
120
+ }
121
+ }
122
+ //# sourceMappingURL=repo-gateway.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo-gateway.js","sourceRoot":"","sources":["../src/repo-gateway.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlassian-dc-mcp/bitbucket",
3
- "version": "0.34.0",
3
+ "version": "0.36.0",
4
4
  "main": "build/index.js",
5
5
  "type": "module",
6
6
  "bin": "./bin/run.js",
@@ -31,7 +31,7 @@
31
31
  "test": "jest"
32
32
  },
33
33
  "dependencies": {
34
- "@atlassian-dc-mcp/common": "^0.34.0",
34
+ "@atlassian-dc-mcp/common": "^0.36.0",
35
35
  "@modelcontextprotocol/sdk": "^1.27.1",
36
36
  "dotenv": "^16.4.7",
37
37
  "zod": "^3.24.2"
@@ -48,5 +48,5 @@
48
48
  "publishConfig": {
49
49
  "access": "public"
50
50
  },
51
- "gitHead": "ab2b534bafefa4666feca463e79824dadb797ff3"
51
+ "gitHead": "f2ef2f49cce893cbf1ff2cad072cdecaae7ad28d"
52
52
  }
@@ -3,7 +3,8 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { initializeRuntimeConfig } from '@atlassian-dc-mcp/common';
5
5
  import { BitbucketService } from '../bitbucket-service.js';
6
- import { PullRequestsService, RepositoryService } from '../bitbucket-client/index.js';
6
+ import { DeprecatedService, PullRequestsService, RepositoryService } from '../bitbucket-client/index.js';
7
+ import { DECLINE_POLICY, type RepoGateway } from '../repo-gateway.js';
7
8
  import { request as mockRequest } from '../bitbucket-client/core/request.js';
8
9
 
9
10
  // Mock the request function
@@ -23,12 +24,17 @@ jest.mock('../bitbucket-client/index.js', () => ({
23
24
  updateStatus: jest.fn(),
24
25
  getPage: jest.fn(),
25
26
  getReviewers: jest.fn(),
26
- get3: jest.fn()
27
+ get3: jest.fn(),
28
+ decline: jest.fn(),
29
+ reopen: jest.fn()
27
30
  },
28
31
  RepositoryService: {
29
32
  streamRaw: jest.fn(),
30
33
  createBranch: jest.fn()
31
34
  },
35
+ DeprecatedService: {
36
+ getBuildStatus: jest.fn()
37
+ },
32
38
  OpenAPI: {
33
39
  BASE: '',
34
40
  TOKEN: '',
@@ -2971,6 +2977,48 @@ describe('BitbucketService', () => {
2971
2977
  'refs/heads/feature', 'refs/heads/main'
2972
2978
  );
2973
2979
  });
2980
+
2981
+ it('should uppercase projectKey and lowercase repositorySlug for declinePullRequest', async () => {
2982
+ const gateway: RepoGateway = {
2983
+ enabled: true,
2984
+ repos: ['TEST/test-repo'],
2985
+ targetRefs: [],
2986
+ policy: DECLINE_POLICY,
2987
+ };
2988
+ (PullRequestsService.decline as jest.Mock).mockResolvedValue({ id: 1, state: 'DECLINED' });
2989
+
2990
+ await bitbucketService.declinePullRequest({
2991
+ projectKey: 'test',
2992
+ repositorySlug: 'Test-Repo',
2993
+ pullRequestId: '1',
2994
+ version: 2,
2995
+ gateway,
2996
+ });
2997
+
2998
+ expect(PullRequestsService.decline).toHaveBeenCalledWith('TEST', '1', 'test-repo', '2', { version: 2 });
2999
+ });
3000
+
3001
+ it('should uppercase projectKey and lowercase repositorySlug for reopenPullRequest', async () => {
3002
+ (PullRequestsService.reopen as jest.Mock).mockResolvedValue({ id: 1, state: 'OPEN' });
3003
+
3004
+ await bitbucketService.reopenPullRequest({
3005
+ projectKey: 'test',
3006
+ repositorySlug: 'Test-Repo',
3007
+ pullRequestId: '1',
3008
+ version: 2,
3009
+ });
3010
+
3011
+ expect(PullRequestsService.reopen).toHaveBeenCalledWith('TEST', '1', 'test-repo', '2', { version: 2 });
3012
+ });
3013
+
3014
+ it('should uppercase projectKey and lowercase repositorySlug for getPullRequestBuilds', async () => {
3015
+ (PullRequestsService.get3 as jest.Mock).mockResolvedValue({ id: 1, fromRef: { latestCommit: 'abc123' } });
3016
+ (DeprecatedService.getBuildStatus as jest.Mock).mockResolvedValue({ values: [] });
3017
+
3018
+ await bitbucketService.getPullRequestBuilds('test', 'Test-Repo', '1');
3019
+
3020
+ expect(PullRequestsService.get3).toHaveBeenCalledWith('TEST', '1', 'test-repo');
3021
+ });
2974
3022
  });
2975
3023
 
2976
3024
  describe('searchCode', () => {
@@ -16,8 +16,9 @@ describe('Bitbucket config', () => {
16
16
  });
17
17
 
18
18
  afterEach(() => {
19
- fs.rmSync(tempDir, { recursive: true, force: true });
19
+ // Windows refuses to remove a directory that is still the process cwd.
20
20
  process.chdir(originalCwd);
21
+ fs.rmSync(tempDir, { recursive: true, force: true });
21
22
  });
22
23
 
23
24
  afterAll(() => {
@@ -0,0 +1,93 @@
1
+ import { DeprecatedService, PullRequestsService } from '../bitbucket-client/index.js';
2
+ import { fetchPullRequestBuilds } from '../pr-builds.js';
3
+
4
+ jest.mock('../bitbucket-client/index.js', () => ({
5
+ PullRequestsService: {
6
+ get3: jest.fn(),
7
+ },
8
+ DeprecatedService: {
9
+ getBuildStatus: jest.fn(),
10
+ },
11
+ OpenAPI: { BASE: '', TOKEN: '', VERSION: '' },
12
+ }));
13
+
14
+ const getPullRequest = PullRequestsService.get3 as jest.Mock;
15
+ const getBuildStatus = DeprecatedService.getBuildStatus as jest.Mock;
16
+
17
+ const HEAD = '0780e1428c06abf1c96ab9cb995d129255511c93';
18
+ const OPEN_PR = { id: 42, fromRef: { id: 'refs/heads/feature', latestCommit: HEAD } };
19
+
20
+ const BUILD = {
21
+ key: 'a1',
22
+ name: 'Builds » demo » PR-42 #1',
23
+ state: 'SUCCESSFUL',
24
+ url: 'https://ci.example.com/job/demo/job/PR-42/1/',
25
+ description: 'This commit looks good.',
26
+ dateAdded: 1791464936354,
27
+ };
28
+ const E2E = { ...BUILD, key: 'b2', name: 'E2E » demo » PR-42 #1', state: 'FAILED' };
29
+ const REVIEW = { ...BUILD, key: 'c3', name: 'AI Code Review » demo » PR-42 #1', state: 'INPROGRESS' };
30
+
31
+ describe('fetchPullRequestBuilds', () => {
32
+ beforeEach(() => jest.clearAllMocks());
33
+
34
+ it('lists the build statuses of the latest source commit with per-state counts', async () => {
35
+ getPullRequest.mockResolvedValue(OPEN_PR);
36
+ getBuildStatus.mockResolvedValue({ size: 3, isLastPage: true, values: [BUILD, E2E, REVIEW] });
37
+
38
+ const result = await fetchPullRequestBuilds('PROJ', 'demo', '42');
39
+
40
+ expect(getPullRequest).toHaveBeenCalledWith('PROJ', '42', 'demo');
41
+ expect(getBuildStatus).toHaveBeenCalledWith(HEAD, 'NEWEST', 0, 100);
42
+ expect(result).toEqual({
43
+ success: true,
44
+ data: {
45
+ commitId: HEAD,
46
+ counts: { successful: 1, failed: 1, inProgress: 1, cancelled: 0, unknown: 0 },
47
+ builds: [BUILD, E2E, REVIEW],
48
+ },
49
+ });
50
+ });
51
+
52
+ it('counts a build without a recognised state as unknown', async () => {
53
+ getPullRequest.mockResolvedValue(OPEN_PR);
54
+ getBuildStatus.mockResolvedValue({ values: [{ key: 'x' }, { key: 'y', state: 'QUEUED' }] });
55
+
56
+ const result = await fetchPullRequestBuilds('PROJ', 'demo', '42');
57
+
58
+ expect(result.data?.counts).toEqual({ successful: 0, failed: 0, inProgress: 0, cancelled: 0, unknown: 2 });
59
+ });
60
+
61
+ it('returns empty builds when no CI has reported on the commit', async () => {
62
+ getPullRequest.mockResolvedValue(OPEN_PR);
63
+ getBuildStatus.mockResolvedValue({ size: 0, isLastPage: true, values: [] });
64
+
65
+ const result = await fetchPullRequestBuilds('PROJ', 'demo', '42');
66
+
67
+ expect(result.data).toEqual({
68
+ commitId: HEAD,
69
+ counts: { successful: 0, failed: 0, inProgress: 0, cancelled: 0, unknown: 0 },
70
+ builds: [],
71
+ });
72
+ });
73
+
74
+ it('fails without querying builds when the pull request has no source commit', async () => {
75
+ getPullRequest.mockResolvedValue({ id: 42, fromRef: { id: 'refs/heads/feature' } });
76
+
77
+ const result = await fetchPullRequestBuilds('PROJ', 'demo', '42');
78
+
79
+ expect(result.success).toBe(false);
80
+ expect(result.error).toBe('Pull request 42 has no source commit');
81
+ expect(getBuildStatus).not.toHaveBeenCalled();
82
+ });
83
+
84
+ it('reports an API error with the status line', async () => {
85
+ getPullRequest.mockRejectedValue({ status: 404, statusText: 'Not Found', body: { errors: [] } });
86
+
87
+ const result = await fetchPullRequestBuilds('PROJ', 'demo', '42');
88
+
89
+ expect(result.success).toBe(false);
90
+ expect(result.error).toBe('Error fetching pull request builds: 404 Not Found');
91
+ expect(getBuildStatus).not.toHaveBeenCalled();
92
+ });
93
+ });
@@ -1,6 +1,6 @@
1
1
  import { PullRequestsService } from '../bitbucket-client/index.js';
2
2
  import { fetchMergeability, mergePullRequest } from '../pr-merge.js';
3
- import type { MergeGateway } from '../merge-gateway.js';
3
+ import { MERGE_POLICY, type RepoGateway } from '../repo-gateway.js';
4
4
 
5
5
  jest.mock('../bitbucket-client/index.js', () => ({
6
6
  PullRequestsService: {
@@ -15,7 +15,7 @@ const canMerge = PullRequestsService.canMerge as jest.Mock;
15
15
  const merge = PullRequestsService.merge as jest.Mock;
16
16
  const getPullRequest = PullRequestsService.get3 as jest.Mock;
17
17
 
18
- const OPEN_GATEWAY: MergeGateway = { enabled: true, repos: ['PROJ/demo'], targetRefs: [] };
18
+ const OPEN_GATEWAY: RepoGateway = { enabled: true, repos: ['PROJ/demo'], targetRefs: [], policy: MERGE_POLICY };
19
19
  const CLEAN = { canMerge: true, conflicted: false, outcome: 'CLEAN', vetoes: [] };
20
20
  const MERGED_PR = {
21
21
  id: 42,
@@ -111,7 +111,7 @@ describe('mergePullRequest', () => {
111
111
  it('refuses without any request when merging is disabled', async () => {
112
112
  const result = await mergePullRequest({
113
113
  ...baseParams,
114
- gateway: { enabled: false, repos: [], targetRefs: [] },
114
+ gateway: { enabled: false, repos: [], targetRefs: [], policy: MERGE_POLICY },
115
115
  });
116
116
 
117
117
  expect(result.success).toBe(false);
@@ -137,7 +137,7 @@ describe('mergePullRequest', () => {
137
137
 
138
138
  const result = await mergePullRequest({
139
139
  ...baseParams,
140
- gateway: { enabled: true, repos: ['PROJ/demo'], targetRefs: ['refs/heads/develop'] },
140
+ gateway: { enabled: true, repos: ['PROJ/demo'], targetRefs: ['refs/heads/develop'], policy: MERGE_POLICY },
141
141
  });
142
142
 
143
143
  expect(result.success).toBe(false);
@@ -153,7 +153,7 @@ describe('mergePullRequest', () => {
153
153
 
154
154
  const result = await mergePullRequest({
155
155
  ...baseParams,
156
- gateway: { enabled: true, repos: ['PROJ/demo'], targetRefs: ['refs/heads/release/*'] },
156
+ gateway: { enabled: true, repos: ['PROJ/demo'], targetRefs: ['refs/heads/release/*'], policy: MERGE_POLICY },
157
157
  });
158
158
 
159
159
  expect(result.success).toBe(true);
@@ -0,0 +1,150 @@
1
+ import { PullRequestsService } from '../bitbucket-client/index.js';
2
+ import { declinePullRequest, reopenPullRequest } from '../pr-state.js';
3
+ import { DECLINE_POLICY, type RepoGateway } from '../repo-gateway.js';
4
+
5
+ jest.mock('../bitbucket-client/index.js', () => ({
6
+ PullRequestsService: {
7
+ decline: jest.fn(),
8
+ reopen: jest.fn(),
9
+ },
10
+ OpenAPI: { BASE: '', TOKEN: '', VERSION: '' },
11
+ }));
12
+
13
+ const decline = PullRequestsService.decline as jest.Mock;
14
+ const reopen = PullRequestsService.reopen as jest.Mock;
15
+
16
+ const OPEN_GATEWAY: RepoGateway = {
17
+ enabled: true,
18
+ repos: ['PROJ/demo'],
19
+ targetRefs: [],
20
+ policy: DECLINE_POLICY,
21
+ };
22
+
23
+ const DECLINED_PR = {
24
+ id: 42,
25
+ version: 3,
26
+ title: 'Add decline tool',
27
+ state: 'DECLINED',
28
+ fromRef: { id: 'refs/heads/feature' },
29
+ toRef: { id: 'refs/heads/develop' },
30
+ reviewers: [],
31
+ };
32
+
33
+ const REOPENED_PR = { ...DECLINED_PR, version: 4, state: 'OPEN' };
34
+
35
+ const baseParams = {
36
+ projectKey: 'PROJ',
37
+ repositorySlug: 'demo',
38
+ pullRequestId: '42',
39
+ version: 2,
40
+ };
41
+
42
+ describe('declinePullRequest', () => {
43
+ beforeEach(() => jest.clearAllMocks());
44
+
45
+ it('declines with the version as an optimistic lock and returns a compact ack', async () => {
46
+ decline.mockResolvedValue(DECLINED_PR);
47
+
48
+ const result = await declinePullRequest({ ...baseParams, gateway: OPEN_GATEWAY });
49
+
50
+ expect(decline).toHaveBeenCalledWith('PROJ', '42', 'demo', '2', { version: 2 });
51
+ expect(result.success).toBe(true);
52
+ expect(result.data).toEqual({
53
+ id: 42,
54
+ version: 3,
55
+ title: 'Add decline tool',
56
+ state: 'DECLINED',
57
+ fromRefId: 'refs/heads/feature',
58
+ toRefId: 'refs/heads/develop',
59
+ reviewerCount: 0,
60
+ });
61
+ });
62
+
63
+ it('posts the reason atomically with the decline when a comment is given', async () => {
64
+ decline.mockResolvedValue(DECLINED_PR);
65
+
66
+ await declinePullRequest({
67
+ ...baseParams,
68
+ gateway: OPEN_GATEWAY,
69
+ comment: 'Superseded by PR 99.',
70
+ });
71
+
72
+ expect(decline).toHaveBeenCalledWith('PROJ', '42', 'demo', '2', {
73
+ version: 2,
74
+ comment: 'Superseded by PR 99.',
75
+ });
76
+ });
77
+
78
+ it('can return the full payload', async () => {
79
+ decline.mockResolvedValue(DECLINED_PR);
80
+
81
+ const result = await declinePullRequest({ ...baseParams, gateway: OPEN_GATEWAY, output: 'full' });
82
+
83
+ expect(result.data).toBe(DECLINED_PR);
84
+ });
85
+
86
+ it('refuses without any request when declining is disabled', async () => {
87
+ const result = await declinePullRequest({
88
+ ...baseParams,
89
+ gateway: { enabled: false, repos: [], targetRefs: [], policy: DECLINE_POLICY },
90
+ });
91
+
92
+ expect(result.success).toBe(false);
93
+ expect(result.error).toMatch(/Declining pull requests is disabled on this server/);
94
+ expect(decline).not.toHaveBeenCalled();
95
+ });
96
+
97
+ it('refuses a repository outside the allowed list without any request', async () => {
98
+ const result = await declinePullRequest({
99
+ ...baseParams,
100
+ repositorySlug: 'other-repo',
101
+ gateway: OPEN_GATEWAY,
102
+ });
103
+
104
+ expect(result.success).toBe(false);
105
+ expect(result.error).toMatch(/Declining is not allowed in PROJ\/other-repo/);
106
+ expect(decline).not.toHaveBeenCalled();
107
+ });
108
+
109
+ it('surfaces a server-side failure such as a stale version', async () => {
110
+ decline.mockRejectedValue({ status: 409, statusText: 'Conflict', body: { errors: [{ message: 'stale' }] } });
111
+
112
+ const result = await declinePullRequest({ ...baseParams, gateway: OPEN_GATEWAY });
113
+
114
+ expect(result.success).toBe(false);
115
+ expect(result.error).toBe('Error declining pull request: 409 Conflict');
116
+ expect(result.details).toEqual({ errors: [{ message: 'stale' }] });
117
+ });
118
+ });
119
+
120
+ describe('reopenPullRequest', () => {
121
+ beforeEach(() => jest.clearAllMocks());
122
+
123
+ it('reopens with the version as an optimistic lock and returns a compact ack', async () => {
124
+ reopen.mockResolvedValue(REOPENED_PR);
125
+
126
+ const result = await reopenPullRequest(baseParams);
127
+
128
+ expect(reopen).toHaveBeenCalledWith('PROJ', '42', 'demo', '2', { version: 2 });
129
+ expect(result.success).toBe(true);
130
+ expect(result.data).toMatchObject({ id: 42, version: 4, state: 'OPEN' });
131
+ });
132
+
133
+ it('can return the full payload', async () => {
134
+ reopen.mockResolvedValue(REOPENED_PR);
135
+
136
+ const result = await reopenPullRequest({ ...baseParams, output: 'full' });
137
+
138
+ expect(result.data).toBe(REOPENED_PR);
139
+ });
140
+
141
+ it('surfaces a server-side failure such as a pull request that is not declined', async () => {
142
+ reopen.mockRejectedValue({ status: 409, statusText: 'Conflict', body: { errors: [{ message: 'not declined' }] } });
143
+
144
+ const result = await reopenPullRequest(baseParams);
145
+
146
+ expect(result.success).toBe(false);
147
+ expect(result.error).toBe('Error reopening pull request: 409 Conflict');
148
+ expect(result.details).toEqual({ errors: [{ message: 'not declined' }] });
149
+ });
150
+ });