fix: add tests to recover branch coverage regression in cli.ts#1458
Conversation
There was a problem hiding this comment.
Pull request overview
Adds missing unit tests to restore/raise branch coverage in src/cli.ts by exercising previously-uncovered error/exit handling and default-parameter branches, addressing the Test Coverage Report CI failure.
Changes:
- Added tests for
handlePredownloadAction’s catch path to cover botherror.exitCode ?? 1branches. - Added tests calling
extractGhesDomainsFromEngineApiTarget()/extractGhecDomainsFromServerUrl()without arguments to cover theenv = process.envdefault branch.
Comments suppressed due to low confidence (1)
src/cli.test.ts:2534
- This test mutates
process.env.GITHUB_SERVER_URL/process.env.GITHUB_API_URLand restores them only at the end. To guarantee cleanup even if the expectation fails, restore the saved env values in afinallyblock (or inafterEach).
it('should use process.env by default when no env argument is provided', () => {
const savedServerUrl = process.env.GITHUB_SERVER_URL;
const savedApiUrl = process.env.GITHUB_API_URL;
delete process.env.GITHUB_SERVER_URL;
delete process.env.GITHUB_API_URL;
const domains = extractGhecDomainsFromServerUrl();
expect(domains).toEqual([]);
if (savedServerUrl !== undefined) process.env.GITHUB_SERVER_URL = savedServerUrl;
if (savedApiUrl !== undefined) process.env.GITHUB_API_URL = savedApiUrl;
});
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const errorWithExitCode = Object.assign(new Error('pull failed'), { exitCode: 2 }); | ||
| jest.resetModules(); | ||
| jest.doMock('./commands/predownload', () => ({ | ||
| predownloadCommand: jest.fn().mockRejectedValue(errorWithExitCode), | ||
| })); | ||
|
|
||
| await expect(handlePredownloadAction({ | ||
| imageRegistry: 'test', | ||
| imageTag: 'latest', | ||
| agentImage: 'default', | ||
| enableApiProxy: false, | ||
| })).rejects.toThrow('process.exit called'); | ||
|
|
||
| expect(mockExit).toHaveBeenCalledWith(2); | ||
| mockExit.mockRestore(); | ||
| }); | ||
|
|
||
| it('should default to exit code 1 when error has no exitCode property', async () => { | ||
| const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => { | ||
| throw new Error('process.exit called'); | ||
| }); | ||
| const errorWithoutExitCode = new Error('unexpected failure'); | ||
| jest.resetModules(); | ||
| jest.doMock('./commands/predownload', () => ({ | ||
| predownloadCommand: jest.fn().mockRejectedValue(errorWithoutExitCode), | ||
| })); | ||
|
|
||
| await expect(handlePredownloadAction({ | ||
| imageRegistry: 'test', | ||
| imageTag: 'latest', | ||
| agentImage: 'default', | ||
| enableApiProxy: false, | ||
| })).rejects.toThrow('process.exit called'); | ||
|
|
||
| expect(mockExit).toHaveBeenCalledWith(1); | ||
| mockExit.mockRestore(); |
There was a problem hiding this comment.
These tests mutate global state (process.exit spy) and only restore it at the end of the test body. If an assertion throws before mockRestore() runs, the spy can leak into subsequent tests and cause cascading failures. Prefer wrapping the test body in try/finally, or move the mockRestore() into an afterEach within this describe block so cleanup is guaranteed.
| const errorWithExitCode = Object.assign(new Error('pull failed'), { exitCode: 2 }); | |
| jest.resetModules(); | |
| jest.doMock('./commands/predownload', () => ({ | |
| predownloadCommand: jest.fn().mockRejectedValue(errorWithExitCode), | |
| })); | |
| await expect(handlePredownloadAction({ | |
| imageRegistry: 'test', | |
| imageTag: 'latest', | |
| agentImage: 'default', | |
| enableApiProxy: false, | |
| })).rejects.toThrow('process.exit called'); | |
| expect(mockExit).toHaveBeenCalledWith(2); | |
| mockExit.mockRestore(); | |
| }); | |
| it('should default to exit code 1 when error has no exitCode property', async () => { | |
| const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => { | |
| throw new Error('process.exit called'); | |
| }); | |
| const errorWithoutExitCode = new Error('unexpected failure'); | |
| jest.resetModules(); | |
| jest.doMock('./commands/predownload', () => ({ | |
| predownloadCommand: jest.fn().mockRejectedValue(errorWithoutExitCode), | |
| })); | |
| await expect(handlePredownloadAction({ | |
| imageRegistry: 'test', | |
| imageTag: 'latest', | |
| agentImage: 'default', | |
| enableApiProxy: false, | |
| })).rejects.toThrow('process.exit called'); | |
| expect(mockExit).toHaveBeenCalledWith(1); | |
| mockExit.mockRestore(); | |
| try { | |
| const errorWithExitCode = Object.assign(new Error('pull failed'), { exitCode: 2 }); | |
| jest.resetModules(); | |
| jest.doMock('./commands/predownload', () => ({ | |
| predownloadCommand: jest.fn().mockRejectedValue(errorWithExitCode), | |
| })); | |
| await expect(handlePredownloadAction({ | |
| imageRegistry: 'test', | |
| imageTag: 'latest', | |
| agentImage: 'default', | |
| enableApiProxy: false, | |
| })).rejects.toThrow('process.exit called'); | |
| expect(mockExit).toHaveBeenCalledWith(2); | |
| } finally { | |
| mockExit.mockRestore(); | |
| } | |
| }); | |
| it('should default to exit code 1 when error has no exitCode property', async () => { | |
| const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => { | |
| throw new Error('process.exit called'); | |
| }); | |
| try { | |
| const errorWithoutExitCode = new Error('unexpected failure'); | |
| jest.resetModules(); | |
| jest.doMock('./commands/predownload', () => ({ | |
| predownloadCommand: jest.fn().mockRejectedValue(errorWithoutExitCode), | |
| })); | |
| await expect(handlePredownloadAction({ | |
| imageRegistry: 'test', | |
| imageTag: 'latest', | |
| agentImage: 'default', | |
| enableApiProxy: false, | |
| })).rejects.toThrow('process.exit called'); | |
| expect(mockExit).toHaveBeenCalledWith(1); | |
| } finally { | |
| mockExit.mockRestore(); | |
| } |
| delete process.env.ENGINE_API_TARGET; | ||
| const domains = extractGhesDomainsFromEngineApiTarget(); | ||
| expect(domains).toEqual([]); | ||
| if (saved !== undefined) process.env.ENGINE_API_TARGET = saved; |
There was a problem hiding this comment.
This test deletes/modifies process.env.ENGINE_API_TARGET and restores it only at the end of the test. To avoid leaking environment changes when an assertion fails (which can make later tests flaky), restore the original value in a finally block (or use beforeEach/afterEach around these env-mutation tests).
This issue also appears on line 2525 of the same file.
| delete process.env.ENGINE_API_TARGET; | |
| const domains = extractGhesDomainsFromEngineApiTarget(); | |
| expect(domains).toEqual([]); | |
| if (saved !== undefined) process.env.ENGINE_API_TARGET = saved; | |
| try { | |
| delete process.env.ENGINE_API_TARGET; | |
| const domains = extractGhesDomainsFromEngineApiTarget(); | |
| expect(domains).toEqual([]); | |
| } finally { | |
| if (saved !== undefined) { | |
| process.env.ENGINE_API_TARGET = saved; | |
| } else { | |
| delete process.env.ENGINE_API_TARGET; | |
| } | |
| } |
* Initial plan * feat: add --env-file support for injecting env vars from a file Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/d00dc39f-919c-4900-91e9-4fdeb00d0abf * fix: trim leading whitespace in env file lines; add edge case tests Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/d00dc39f-919c-4900-91e9-4fdeb00d0abf * fix: add tests to recover branch coverage regression in cli.ts (#1458) * Initial plan * fix: add tests to resolve branch coverage regression in cli.ts Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/41835a48-8b69-46e0-9485-8fb41265babf --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
The previous commit added new code to
src/cli.ts(the--env-fileoption and catch block inhandlePredownloadAction) without sufficient test coverage, dropping overall branch coverage from 76.06% → 76.02% and failing the Test Coverage Report CI job.Changes
handlePredownloadActionerror path — the catch block on lines 1951–1952 was never exercised; added two tests covering both sides of the??operator:exitCodeproperty →process.exit(exitCode)exitCode→process.exit(1)(default)Default parameter branches —
extractGhecDomainsFromServerUrl()andextractGhesDomainsFromEngineApiTarget()were always called with an explicitenvargument in tests, leaving theenv = process.envdefault branch uncovered; added tests that call each without argumentsResult: overall branch coverage 75.96% → 76.19% (+0.23%),
src/cli.tsbranch coverage 58.41% → 59.37%.Original prompt
💬 Send tasks to Copilot coding agent from Slack and Teams to turn conversations into code. Copilot posts an update in your thread when it's finished.