Skip to content

Commit edac72a

Browse files
committed
bugfix-303-bugbot-first-message-bugs: Enhance tests for Execution, MergeRepository, and PrepareBranchesUseCase by adding scenarios for early returns based on release type and tag availability. Introduce new test cases for handling hotfix and release branch creation, ensuring comprehensive coverage for various edge cases and error conditions.
1 parent a1787b2 commit edac72a

4 files changed

Lines changed: 499 additions & 0 deletions

File tree

src/data/model/__tests__/execution.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,5 +710,45 @@ describe('Execution', () => {
710710
expect(e.hotfix.version).toBeDefined();
711711
expect(e.hotfix.branch).toBe('hotfix/1.0.1');
712712
});
713+
714+
it('setup returns early when release type from GetReleaseTypeUseCase is undefined', async () => {
715+
mockGetLabels.mockResolvedValue(['release']);
716+
mockConfigGet.mockResolvedValue(undefined);
717+
mockGetReleaseVersionInvoke.mockResolvedValue([{ executed: true, success: false }]);
718+
mockGetReleaseTypeInvoke.mockResolvedValue([
719+
{ executed: true, success: true, payload: { releaseType: undefined } },
720+
]);
721+
const issue = makeIssue({ eventName: 'issues', issue: { number: 1 } } as never);
722+
const e = buildExecution({ eventName: 'issues', issue: { number: 1 } } as never, { issue });
723+
await e.setup();
724+
expect(e.release.version).toBeUndefined();
725+
expect(e.release.branch).toBeUndefined();
726+
});
727+
728+
it('setup returns early when getLatestTag returns undefined in release path', async () => {
729+
mockGetLabels.mockResolvedValue(['release']);
730+
mockConfigGet.mockResolvedValue(undefined);
731+
mockGetReleaseVersionInvoke.mockResolvedValue([{ executed: true, success: false }]);
732+
mockGetReleaseTypeInvoke.mockResolvedValue([
733+
{ executed: true, success: true, payload: { releaseType: 'Minor' } },
734+
]);
735+
mockGetLatestTag.mockResolvedValue(undefined);
736+
const issue = makeIssue({ eventName: 'issues', issue: { number: 1 } } as never);
737+
const e = buildExecution({ eventName: 'issues', issue: { number: 1 } } as never, { issue });
738+
await e.setup();
739+
expect(e.release.version).toBeUndefined();
740+
});
741+
742+
it('setup returns early when getLatestTag returns undefined in hotfix path', async () => {
743+
mockGetLabels.mockResolvedValue(['hotfix']);
744+
mockConfigGet.mockResolvedValue(undefined);
745+
mockGetHotfixVersionInvoke.mockResolvedValue([{ executed: true, success: false }]);
746+
mockGetLatestTag.mockResolvedValue(undefined);
747+
const issue = makeIssue({ eventName: 'issues', issue: { number: 1 } } as never);
748+
const e = buildExecution({ eventName: 'issues', issue: { number: 1 } } as never, { issue });
749+
await e.setup();
750+
expect(e.hotfix.baseVersion).toBeUndefined();
751+
expect(e.hotfix.version).toBeUndefined();
752+
});
713753
});
714754
});
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/**
2+
* Unit tests for BranchRepository: getListOfBranches, removeBranch, manageBranches.
3+
*/
4+
5+
import { Execution } from '../../model/execution';
6+
import { BranchRepository } from '../branch_repository';
7+
8+
jest.mock('../../../utils/logger', () => ({
9+
logDebugInfo: jest.fn(),
10+
logError: jest.fn(),
11+
}));
12+
13+
const mockListBranches = jest.fn();
14+
const mockGetRef = jest.fn();
15+
const mockDeleteRef = jest.fn();
16+
const mockGraphql = jest.fn();
17+
18+
jest.mock('@actions/github', () => ({
19+
getOctokit: () => ({
20+
rest: {
21+
repos: {
22+
listBranches: (...args: unknown[]) => mockListBranches(...args),
23+
},
24+
git: {
25+
getRef: (...args: unknown[]) => mockGetRef(...args),
26+
deleteRef: (...args: unknown[]) => mockDeleteRef(...args),
27+
},
28+
},
29+
graphql: (...args: unknown[]) => mockGraphql(...args),
30+
}),
31+
}));
32+
33+
function mockExecution(overrides: Partial<Execution> = {}): Execution {
34+
const e = {
35+
branches: {
36+
featureTree: 'feature',
37+
bugfixTree: 'bugfix',
38+
docsTree: 'docs',
39+
choreTree: 'chore',
40+
},
41+
currentConfiguration: { parentBranch: undefined as string | undefined },
42+
...overrides,
43+
};
44+
return e as unknown as Execution;
45+
}
46+
47+
describe('BranchRepository', () => {
48+
const repo = new BranchRepository();
49+
50+
beforeEach(() => {
51+
mockListBranches.mockReset();
52+
mockGetRef.mockReset();
53+
mockDeleteRef.mockReset();
54+
mockGraphql.mockReset();
55+
});
56+
57+
describe('getListOfBranches', () => {
58+
it('returns branch names from single page', async () => {
59+
mockListBranches
60+
.mockResolvedValueOnce({ data: [{ name: 'main' }, { name: 'develop' }] })
61+
.mockResolvedValueOnce({ data: [] });
62+
63+
const result = await repo.getListOfBranches('owner', 'repo', 'token');
64+
65+
expect(result).toEqual(['main', 'develop']);
66+
expect(mockListBranches).toHaveBeenCalledWith({
67+
owner: 'owner',
68+
repo: 'repo',
69+
per_page: 100,
70+
page: 1,
71+
});
72+
});
73+
74+
it('paginates until empty data', async () => {
75+
mockListBranches
76+
.mockResolvedValueOnce({ data: [{ name: 'a' }] })
77+
.mockResolvedValueOnce({ data: [{ name: 'b' }] })
78+
.mockResolvedValueOnce({ data: [] });
79+
80+
const result = await repo.getListOfBranches('o', 'r', 't');
81+
82+
expect(result).toEqual(['a', 'b']);
83+
expect(mockListBranches).toHaveBeenCalledTimes(3);
84+
});
85+
});
86+
87+
describe('removeBranch', () => {
88+
it('deletes ref and returns true', async () => {
89+
mockGetRef.mockResolvedValue({ data: { ref: 'refs/heads/feature/1-foo' } });
90+
mockDeleteRef.mockResolvedValue({});
91+
92+
const result = await repo.removeBranch('owner', 'repo', 'feature/1-foo', 'token');
93+
94+
expect(result).toBe(true);
95+
expect(mockGetRef).toHaveBeenCalledWith({
96+
owner: 'owner',
97+
repo: 'repo',
98+
ref: 'heads/feature/1-foo',
99+
});
100+
expect(mockDeleteRef).toHaveBeenCalledWith({
101+
owner: 'owner',
102+
repo: 'repo',
103+
ref: 'heads/feature/1-foo',
104+
});
105+
});
106+
107+
it('throws when getRef fails', async () => {
108+
mockGetRef.mockRejectedValue(new Error('Not found'));
109+
110+
await expect(
111+
repo.removeBranch('o', 'r', 'branch', 't'),
112+
).rejects.toThrow('Not found');
113+
});
114+
});
115+
116+
describe('manageBranches', () => {
117+
it('returns error result when hotfixBranch is undefined and isHotfix is true', async () => {
118+
mockListBranches
119+
.mockResolvedValueOnce({ data: [] });
120+
const param = mockExecution();
121+
const result = await repo.manageBranches(
122+
param,
123+
'owner',
124+
'repo',
125+
1,
126+
'Title',
127+
'hotfix',
128+
'develop',
129+
undefined,
130+
true,
131+
'token',
132+
);
133+
134+
expect(result).toHaveLength(1);
135+
expect(result[0].success).toBe(false);
136+
expect(result[0].executed).toBe(true);
137+
expect(result[0].steps).toContainEqual(
138+
expect.stringContaining('hotfix branch was not found'),
139+
);
140+
});
141+
142+
it('returns success executed false when branch already exists', async () => {
143+
mockListBranches
144+
.mockResolvedValueOnce({ data: [{ name: 'feature/1-title' }, { name: 'develop' }] })
145+
.mockResolvedValueOnce({ data: [] });
146+
147+
const param = mockExecution();
148+
const result = await repo.manageBranches(
149+
param,
150+
'owner',
151+
'repo',
152+
1,
153+
'Title',
154+
'feature',
155+
'develop',
156+
undefined,
157+
false,
158+
'token',
159+
);
160+
161+
expect(result).toHaveLength(1);
162+
expect(result[0].success).toBe(true);
163+
expect(result[0].executed).toBe(false);
164+
});
165+
166+
it('creates linked branch when branch does not exist', async () => {
167+
mockListBranches
168+
.mockResolvedValueOnce({ data: [{ name: 'develop' }] })
169+
.mockResolvedValueOnce({ data: [] })
170+
.mockResolvedValueOnce({ data: [] });
171+
mockGraphql
172+
.mockResolvedValueOnce({
173+
repository: {
174+
id: 'R_1',
175+
issue: { id: 'I_1' },
176+
ref: { target: { oid: 'abc123' } },
177+
},
178+
})
179+
.mockResolvedValueOnce({
180+
createLinkedBranch: { linkedBranch: { id: 'LB_1', ref: { name: 'feature/1-mytitle' } } },
181+
});
182+
183+
const param = mockExecution();
184+
const result = await repo.manageBranches(
185+
param,
186+
'owner',
187+
'repo',
188+
1,
189+
'My Title',
190+
'feature',
191+
'develop',
192+
undefined,
193+
false,
194+
'token',
195+
);
196+
197+
expect(result.length).toBeGreaterThanOrEqual(1);
198+
expect(result.some(r => r.success === true && r.executed === true)).toBe(true);
199+
expect(mockGraphql).toHaveBeenCalledTimes(2);
200+
});
201+
});
202+
});

src/data/repository/__tests__/merge_repository.test.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const mockPullsListCommits = jest.fn();
1414
const mockPullsUpdate = jest.fn();
1515
const mockPullsMerge = jest.fn();
1616
const mockReposMerge = jest.fn();
17+
const mockChecksListForRef = jest.fn();
18+
const mockReposGetCombinedStatusForRef = jest.fn();
1719

1820
jest.mock('@actions/github', () => ({
1921
getOctokit: () => ({
@@ -26,6 +28,10 @@ jest.mock('@actions/github', () => ({
2628
},
2729
repos: {
2830
merge: (...args: unknown[]) => mockReposMerge(...args),
31+
getCombinedStatusForRef: (...args: unknown[]) => mockReposGetCombinedStatusForRef(...args),
32+
},
33+
checks: {
34+
listForRef: (...args: unknown[]) => mockChecksListForRef(...args),
2935
},
3036
},
3137
}),
@@ -40,6 +46,8 @@ describe('MergeRepository', () => {
4046
mockPullsUpdate.mockReset();
4147
mockPullsMerge.mockReset();
4248
mockReposMerge.mockReset();
49+
mockChecksListForRef.mockReset();
50+
mockReposGetCombinedStatusForRef.mockReset();
4351
});
4452

4553
it('creates PR, updates body, merges and returns success (timeout <= 10 skips wait)', async () => {
@@ -128,4 +136,104 @@ describe('MergeRepository', () => {
128136
expect(result.some(r => r.success === false && r.steps?.some(s => s.includes('Failed to merge')))).toBe(true);
129137
expect(result.length).toBeGreaterThanOrEqual(2);
130138
});
139+
140+
it('when timeout > 10 waits for check runs (all completed) then merges', async () => {
141+
mockPullsCreate.mockResolvedValue({ data: { number: 1 } });
142+
mockPullsListCommits.mockResolvedValue({ data: [{ commit: { message: 'msg' } }] });
143+
mockPullsUpdate.mockResolvedValue({});
144+
mockPullsMerge.mockResolvedValue({});
145+
mockChecksListForRef.mockResolvedValue({
146+
data: {
147+
check_runs: [
148+
{ name: 'ci', status: 'completed', conclusion: 'success' },
149+
],
150+
},
151+
});
152+
mockReposGetCombinedStatusForRef.mockResolvedValue({
153+
data: { state: 'success', statuses: [] },
154+
});
155+
156+
const result = await repo.mergeBranch(
157+
'owner',
158+
'repo',
159+
'feature/1-x',
160+
'develop',
161+
30,
162+
'token',
163+
);
164+
165+
expect(result).toHaveLength(1);
166+
expect(result[0].success).toBe(true);
167+
expect(mockChecksListForRef).toHaveBeenCalledWith({
168+
owner: 'owner',
169+
repo: 'repo',
170+
ref: 'feature/1-x',
171+
});
172+
expect(mockPullsMerge).toHaveBeenCalled();
173+
});
174+
175+
it('when timeout > 10 and check runs have failure throws then direct merge fallback fails', async () => {
176+
mockPullsCreate.mockResolvedValue({ data: { number: 1 } });
177+
mockPullsListCommits.mockResolvedValue({ data: [] });
178+
mockPullsUpdate.mockResolvedValue({});
179+
mockChecksListForRef.mockResolvedValue({
180+
data: {
181+
check_runs: [
182+
{ name: 'ci', status: 'completed', conclusion: 'failure' },
183+
],
184+
},
185+
});
186+
mockReposGetCombinedStatusForRef.mockResolvedValue({
187+
data: { state: 'success', statuses: [] },
188+
});
189+
mockReposMerge.mockRejectedValue(new Error('Direct merge failed'));
190+
191+
const result = await repo.mergeBranch('o', 'r', 'head', 'base', 30, 'token');
192+
193+
expect(result.some(r => r.success === false && r.steps?.some(s => s.includes('Failed to merge')))).toBe(true);
194+
});
195+
196+
it('when timeout > 10 and no check runs uses status checks (all completed)', async () => {
197+
mockPullsCreate.mockResolvedValue({ data: { number: 1 } });
198+
mockPullsListCommits.mockResolvedValue({ data: [] });
199+
mockPullsUpdate.mockResolvedValue({});
200+
mockChecksListForRef.mockResolvedValue({
201+
data: { check_runs: [] },
202+
});
203+
mockReposGetCombinedStatusForRef.mockResolvedValue({
204+
data: { state: 'success', statuses: [{ context: 'ci', state: 'success' }] },
205+
});
206+
mockPullsMerge.mockResolvedValue({});
207+
208+
const result = await repo.mergeBranch(
209+
'o', 'r', 'head', 'base', 30, 'token',
210+
);
211+
212+
expect(result).toHaveLength(1);
213+
expect(result[0].success).toBe(true);
214+
expect(mockReposGetCombinedStatusForRef).toHaveBeenCalled();
215+
});
216+
217+
it('when timeout > 10 and checks never complete throws then direct merge succeeds', async () => {
218+
jest.useFakeTimers();
219+
mockPullsCreate.mockResolvedValue({ data: { number: 1 } });
220+
mockPullsListCommits.mockResolvedValue({ data: [] });
221+
mockPullsUpdate.mockResolvedValue({});
222+
mockChecksListForRef.mockResolvedValue({
223+
data: {
224+
check_runs: [{ name: 'ci', status: 'in_progress', conclusion: null }],
225+
},
226+
});
227+
mockReposGetCombinedStatusForRef.mockResolvedValue({
228+
data: { state: 'pending', statuses: [] },
229+
});
230+
mockReposMerge.mockResolvedValue({});
231+
232+
const promise = repo.mergeBranch('o', 'r', 'head', 'base', 30, 'token');
233+
await jest.runAllTimersAsync();
234+
const result = await promise;
235+
236+
jest.useRealTimers();
237+
expect(result.some(r => r.success === true && r.steps?.some(s => s.includes('direct merge')))).toBe(true);
238+
});
131239
});

0 commit comments

Comments
 (0)