Skip to content

Commit 84673fe

Browse files
committed
feature-296-bugbot-autofix: Enhance CLI test coverage by adding scenarios for handling non-git repository errors and validating issue number inputs. Introduce JSON output verification for the CLI command. Update common action tests to handle non-Error exceptions gracefully. This improves robustness and error handling in the CLI and action execution logic.
1 parent 2bd5472 commit 84673fe

6 files changed

Lines changed: 481 additions & 0 deletions

File tree

src/__tests__/cli.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,5 +148,57 @@ describe('CLI', () => {
148148
expect(params[INPUT_KEYS.SINGLE_ACTION]).toBe(ACTIONS.INITIAL_SETUP);
149149
expect(params[INPUT_KEYS.WELCOME_TITLE]).toContain('Initial Setup');
150150
});
151+
152+
it('exits when not inside a git repo', async () => {
153+
(execSync as jest.Mock).mockImplementation((cmd: string) => {
154+
if (typeof cmd === 'string' && cmd.includes('is-inside-work-tree')) throw new Error('not a repo');
155+
return Buffer.from('https://github.com/o/r.git');
156+
});
157+
158+
await program.parseAsync(['node', 'cli', 'setup']);
159+
160+
expect(exitSpy).toHaveBeenCalledWith(1);
161+
const { logError } = require('../utils/logger');
162+
expect(logError).toHaveBeenCalledWith(expect.stringContaining('Not a git repository'));
163+
});
164+
});
165+
166+
describe('detect-potential-problems', () => {
167+
it('calls runLocalAction with DETECT_POTENTIAL_PROBLEMS', async () => {
168+
await program.parseAsync(['node', 'cli', 'detect-potential-problems', '-i', '10']);
169+
170+
expect(runLocalAction).toHaveBeenCalledTimes(1);
171+
const params = (runLocalAction as jest.Mock).mock.calls[0][0];
172+
expect(params[INPUT_KEYS.SINGLE_ACTION]).toBe(ACTIONS.DETECT_POTENTIAL_PROBLEMS);
173+
expect(params.issue?.number).toBe(10);
174+
expect(params[INPUT_KEYS.WELCOME_TITLE]).toContain('Detect potential problems');
175+
});
176+
177+
it('shows message when issue number is missing or invalid', async () => {
178+
(runLocalAction as jest.Mock).mockClear();
179+
const logSpy = jest.spyOn(console, 'log').mockImplementation();
180+
181+
await program.parseAsync(['node', 'cli', 'detect-potential-problems', '-i', 'x']);
182+
183+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('valid issue number'));
184+
expect(runLocalAction).not.toHaveBeenCalled();
185+
logSpy.mockRestore();
186+
});
187+
});
188+
189+
describe('do --output json', () => {
190+
it('prints JSON when --output json', async () => {
191+
const { AiRepository } = require('../data/repository/ai_repository');
192+
AiRepository.mockImplementation(() => ({
193+
copilotMessage: jest.fn().mockResolvedValue({ text: 'Hi', sessionId: 'sid-1' }),
194+
}));
195+
const logSpy = jest.spyOn(console, 'log').mockImplementation();
196+
197+
await program.parseAsync(['node', 'cli', 'do', '-p', 'hello', '--output', 'json']);
198+
199+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('"response":'));
200+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('"sessionId":'));
201+
logSpy.mockRestore();
202+
});
151203
});
152204
});

src/actions/__tests__/common_action.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,16 @@ describe('mainRun', () => {
296296
expect(results).toEqual([]);
297297
});
298298

299+
it('calls core.setFailed with String(error) when use case throws non-Error', async () => {
300+
const execution = mockExecution({ isPush: true });
301+
mockCommitInvoke.mockRejectedValue('plain string error');
302+
303+
const results = await mainRun(execution);
304+
305+
expect(core.setFailed).toHaveBeenCalledWith('plain string error');
306+
expect(results).toEqual([]);
307+
});
308+
299309
it('exits process when waitForPreviousRuns rejects and welcome is false', async () => {
300310
const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => {}) as () => never);
301311
(waitForPreviousRuns as jest.Mock).mockRejectedValue(new Error('Queue error'));
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import * as github from '@actions/github';
2+
import { Commit } from '../commit';
3+
4+
jest.mock('@actions/github', () => ({
5+
context: {
6+
payload: {} as Record<string, unknown>,
7+
},
8+
}));
9+
10+
describe('Commit', () => {
11+
beforeEach(() => {
12+
(github.context as { payload: Record<string, unknown> }).payload = {};
13+
});
14+
15+
it('uses inputs when provided for branchReference and branch', () => {
16+
const inputs = { commits: { ref: 'refs/heads/feature/123-x' } };
17+
const c = new Commit(inputs);
18+
expect(c.branchReference).toBe('refs/heads/feature/123-x');
19+
expect(c.branch).toBe('feature/123-x');
20+
});
21+
22+
it('falls back to context.payload.ref when inputs have no commits.ref', () => {
23+
(github.context as { payload: Record<string, unknown> }).payload = { ref: 'refs/heads/main' };
24+
const c = new Commit(undefined);
25+
expect(c.branchReference).toBe('refs/heads/main');
26+
expect(c.branch).toBe('main');
27+
});
28+
29+
it('returns empty string for branchReference when no inputs and no context ref', () => {
30+
const c = new Commit(undefined);
31+
expect(c.branchReference).toBe('');
32+
expect(c.branch).toBe('');
33+
});
34+
35+
it('returns commits from context.payload when no inputs', () => {
36+
const payloadCommits = [{ id: '1', message: 'fix' }];
37+
(github.context as { payload: Record<string, unknown> }).payload = { commits: payloadCommits };
38+
const c = new Commit(undefined);
39+
expect(c.commits).toEqual(payloadCommits);
40+
});
41+
42+
it('returns empty array when context has no commits', () => {
43+
const c = new Commit(undefined);
44+
expect(c.commits).toEqual([]);
45+
});
46+
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import * as github from '@actions/github';
2+
import { Issue } from '../issue';
3+
4+
jest.mock('@actions/github', () => ({
5+
context: {
6+
payload: {} as Record<string, unknown>,
7+
eventName: '',
8+
},
9+
}));
10+
11+
function getContext(): { payload: Record<string, unknown>; eventName: string } {
12+
return github.context as unknown as { payload: Record<string, unknown>; eventName: string };
13+
}
14+
15+
describe('Issue', () => {
16+
const issuePayload = {
17+
title: 'Add feature',
18+
number: 10,
19+
html_url: 'https://github.com/o/r/issues/10',
20+
body: 'Body text',
21+
user: { login: 'bob' },
22+
};
23+
24+
beforeEach(() => {
25+
getContext().payload = {};
26+
getContext().eventName = 'issues';
27+
});
28+
29+
it('uses inputs when provided', () => {
30+
const inputs = { action: 'opened', issue: issuePayload, eventName: 'issues' };
31+
const i = new Issue(false, false, 1, inputs);
32+
expect(i.title).toBe('Add feature');
33+
expect(i.number).toBe(10);
34+
expect(i.creator).toBe('bob');
35+
expect(i.url).toBe('https://github.com/o/r/issues/10');
36+
expect(i.body).toBe('Body text');
37+
expect(i.opened).toBe(true);
38+
expect(i.labeled).toBe(false);
39+
expect(i.isIssue).toBe(true);
40+
expect(i.isIssueComment).toBe(false);
41+
});
42+
43+
it('falls back to context.payload when inputs missing', () => {
44+
getContext().payload = {
45+
action: 'opened',
46+
issue: issuePayload,
47+
};
48+
getContext().eventName = 'issues';
49+
const i = new Issue(false, false, 1, undefined);
50+
expect(i.title).toBe('Add feature');
51+
expect(i.number).toBe(10);
52+
expect(i.isIssue).toBe(true);
53+
});
54+
55+
it('labeled and labelAdded when action is labeled', () => {
56+
const inputs = { action: 'labeled', issue: issuePayload, label: { name: 'bug' } };
57+
const i = new Issue(false, false, 1, inputs);
58+
expect(i.labeled).toBe(true);
59+
expect(i.labelAdded).toBe('bug');
60+
});
61+
62+
it('isIssueComment when eventName is issue_comment', () => {
63+
const inputs = { eventName: 'issue_comment', issue: issuePayload, comment: { id: 5, body: 'Hi', user: { login: 'alice' }, html_url: 'url' } };
64+
const i = new Issue(false, false, 1, inputs);
65+
expect(i.isIssueComment).toBe(true);
66+
expect(i.isIssue).toBe(false);
67+
expect(i.commentId).toBe(5);
68+
expect(i.commentBody).toBe('Hi');
69+
expect(i.commentAuthor).toBe('alice');
70+
expect(i.commentUrl).toBe('url');
71+
});
72+
});
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { Labels } from '../labels';
2+
3+
function createLabels(overrides: Partial<Record<keyof Labels, string>> = {}): Labels {
4+
const base = {
5+
branchManagementLauncherLabel: 'launch',
6+
bug: 'bug',
7+
bugfix: 'bugfix',
8+
hotfix: 'hotfix',
9+
enhancement: 'enhancement',
10+
feature: 'feature',
11+
release: 'release',
12+
question: 'question',
13+
help: 'help',
14+
deploy: 'deploy',
15+
deployed: 'deployed',
16+
docs: 'docs',
17+
documentation: 'documentation',
18+
chore: 'chore',
19+
maintenance: 'maintenance',
20+
sizeXxl: 'size/xxl',
21+
sizeXl: 'size/xl',
22+
sizeL: 'size/l',
23+
sizeM: 'size/m',
24+
sizeS: 'size/s',
25+
sizeXs: 'size/xs',
26+
priorityHigh: 'priority/high',
27+
priorityMedium: 'priority/medium',
28+
priorityLow: 'priority/low',
29+
priorityNone: 'priority/none',
30+
};
31+
const l = new Labels(
32+
base.branchManagementLauncherLabel,
33+
base.bug,
34+
base.bugfix,
35+
base.hotfix,
36+
base.enhancement,
37+
base.feature,
38+
base.release,
39+
base.question,
40+
base.help,
41+
base.deploy,
42+
base.deployed,
43+
base.docs,
44+
base.documentation,
45+
base.chore,
46+
base.maintenance,
47+
base.priorityHigh,
48+
base.priorityMedium,
49+
base.priorityLow,
50+
base.priorityNone,
51+
base.sizeXxl,
52+
base.sizeXl,
53+
base.sizeL,
54+
base.sizeM,
55+
base.sizeS,
56+
base.sizeXs
57+
);
58+
Object.assign(l, overrides);
59+
return l;
60+
}
61+
62+
describe('Labels', () => {
63+
it('isMandatoryBranchedLabel is true when isHotfix or isRelease', () => {
64+
const l = createLabels();
65+
l.currentIssueLabels = [];
66+
expect(l.isMandatoryBranchedLabel).toBe(false);
67+
l.currentIssueLabels = [l.hotfix];
68+
expect(l.isMandatoryBranchedLabel).toBe(true);
69+
l.currentIssueLabels = [l.release];
70+
expect(l.isMandatoryBranchedLabel).toBe(true);
71+
});
72+
73+
it('containsBranchedLabel reflects branchManagementLauncherLabel in currentIssueLabels', () => {
74+
const l = createLabels();
75+
l.currentIssueLabels = [];
76+
expect(l.containsBranchedLabel).toBe(false);
77+
l.currentIssueLabels = [l.branchManagementLauncherLabel];
78+
expect(l.containsBranchedLabel).toBe(true);
79+
});
80+
81+
it('isDeploy and isDeployed from currentIssueLabels', () => {
82+
const l = createLabels();
83+
l.currentIssueLabels = [l.deploy];
84+
expect(l.isDeploy).toBe(true);
85+
expect(l.isDeployed).toBe(false);
86+
l.currentIssueLabels = [l.deployed];
87+
expect(l.isDeploy).toBe(false);
88+
expect(l.isDeployed).toBe(true);
89+
});
90+
91+
it('isHelp and isQuestion from currentIssueLabels', () => {
92+
const l = createLabels();
93+
l.currentIssueLabels = [l.help];
94+
expect(l.isHelp).toBe(true);
95+
expect(l.isQuestion).toBe(false);
96+
l.currentIssueLabels = [l.question];
97+
expect(l.isQuestion).toBe(true);
98+
});
99+
100+
it('isFeature, isEnhancement, isBugfix, isBug, isHotfix, isRelease', () => {
101+
const l = createLabels();
102+
l.currentIssueLabels = [l.feature];
103+
expect(l.isFeature).toBe(true);
104+
l.currentIssueLabels = [l.enhancement];
105+
expect(l.isEnhancement).toBe(true);
106+
l.currentIssueLabels = [l.bugfix];
107+
expect(l.isBugfix).toBe(true);
108+
l.currentIssueLabels = [l.bug];
109+
expect(l.isBug).toBe(true);
110+
l.currentIssueLabels = [l.hotfix];
111+
expect(l.isHotfix).toBe(true);
112+
l.currentIssueLabels = [l.release];
113+
expect(l.isRelease).toBe(true);
114+
});
115+
116+
it('isDocs, isDocumentation, isChore, isMaintenance', () => {
117+
const l = createLabels();
118+
l.currentIssueLabels = [l.docs];
119+
expect(l.isDocs).toBe(true);
120+
l.currentIssueLabels = [l.documentation];
121+
expect(l.isDocumentation).toBe(true);
122+
l.currentIssueLabels = [l.chore];
123+
expect(l.isChore).toBe(true);
124+
l.currentIssueLabels = [l.maintenance];
125+
expect(l.isMaintenance).toBe(true);
126+
});
127+
128+
it('sizedLabelOnIssue returns first matching size label', () => {
129+
const l = createLabels();
130+
l.currentIssueLabels = [l.sizeM];
131+
expect(l.sizedLabelOnIssue).toBe(l.sizeM);
132+
l.currentIssueLabels = [l.sizeXxl, l.sizeM];
133+
expect(l.sizedLabelOnIssue).toBe(l.sizeXxl);
134+
l.currentIssueLabels = [];
135+
expect(l.sizedLabelOnIssue).toBeUndefined();
136+
});
137+
138+
it('sizedLabelOnPullRequest returns first matching size label', () => {
139+
const l = createLabels();
140+
l.currentPullRequestLabels = [l.sizeS];
141+
expect(l.sizedLabelOnPullRequest).toBe(l.sizeS);
142+
l.currentPullRequestLabels = [];
143+
expect(l.sizedLabelOnPullRequest).toBeUndefined();
144+
});
145+
146+
it('isIssueSized and isPullRequestSized', () => {
147+
const l = createLabels();
148+
l.currentIssueLabels = [];
149+
l.currentPullRequestLabels = [];
150+
expect(l.isIssueSized).toBe(false);
151+
expect(l.isPullRequestSized).toBe(false);
152+
l.currentIssueLabels = [l.sizeM];
153+
expect(l.isIssueSized).toBe(true);
154+
l.currentPullRequestLabels = [l.sizeL];
155+
expect(l.isPullRequestSized).toBe(true);
156+
});
157+
158+
it('sizeLabels and priorityLabels return arrays', () => {
159+
const l = createLabels();
160+
expect(l.sizeLabels).toEqual([l.sizeXxl, l.sizeXl, l.sizeL, l.sizeM, l.sizeS, l.sizeXs]);
161+
expect(l.priorityLabels).toEqual([l.priorityHigh, l.priorityMedium, l.priorityLow, l.priorityNone]);
162+
});
163+
164+
it('priorityLabelOnIssue and priorityLabelOnPullRequest', () => {
165+
const l = createLabels();
166+
l.currentIssueLabels = [l.priorityHigh];
167+
l.currentPullRequestLabels = [l.priorityLow];
168+
expect(l.priorityLabelOnIssue).toBe(l.priorityHigh);
169+
expect(l.priorityLabelOnPullRequest).toBe(l.priorityLow);
170+
l.currentIssueLabels = [];
171+
expect(l.priorityLabelOnIssue).toBeUndefined();
172+
});
173+
174+
it('priorityLabelOnIssueProcessable and priorityLabelOnPullRequestProcessable', () => {
175+
const l = createLabels();
176+
l.currentIssueLabels = [l.priorityNone];
177+
expect(l.priorityLabelOnIssueProcessable).toBe(false);
178+
l.currentIssueLabels = [l.priorityHigh];
179+
expect(l.priorityLabelOnIssueProcessable).toBe(true);
180+
l.currentPullRequestLabels = [l.priorityMedium];
181+
expect(l.priorityLabelOnPullRequestProcessable).toBe(true);
182+
});
183+
184+
it('isIssuePrioritized and isPullRequestPrioritized', () => {
185+
const l = createLabels();
186+
l.currentIssueLabels = [l.priorityHigh];
187+
expect(l.isIssuePrioritized).toBe(true);
188+
l.currentIssueLabels = [l.priorityNone];
189+
expect(l.isIssuePrioritized).toBe(false);
190+
l.currentPullRequestLabels = [l.priorityLow];
191+
expect(l.isPullRequestPrioritized).toBe(true);
192+
});
193+
});

0 commit comments

Comments
 (0)