Skip to content

Commit 2119353

Browse files
committed
fix: handle oclif errors correctly
1 parent c48bdab commit 2119353

File tree

4 files changed

+48
-9
lines changed

4 files changed

+48
-9
lines changed

src/SfCommandError.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,13 @@ export class SfCommandError extends SfError {
7575
): SfCommandError {
7676
// SfError.wrap() does most of what we want so start with that.
7777
const sfError = SfError.wrap(err);
78-
const exitCode = computeErrorCode(sfError);
79-
sfError.code = 'code' in err ? err.code : exitCode.toString(10);
78+
const exitCode = computeErrorCode(err);
8079
return new this({
8180
message: sfError.message,
8281
name: err.name ?? 'Error',
8382
actions: 'actions' in err ? err.actions : undefined,
8483
exitCode,
85-
code: sfError.code,
84+
code: 'code' in err && err.code ? err.code : exitCode.toString(10),
8685
cause: sfError.cause,
8786
commandName: 'commandName' in err ? err.commandName : commandName,
8887
data: 'data' in err ? err.data : undefined,

src/errorHandling.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
*/
77

88
import { SfError } from '@salesforce/core';
9-
import { CLIError } from '@oclif/core/errors';
10-
import { SfCommandError } from './SfCommandError.js';
9+
import { Errors } from '@oclif/core';
1110

1211
/**
1312
*
@@ -20,7 +19,7 @@ import { SfCommandError } from './SfCommandError.js';
2019
* - use the process exitCode
2120
* - default to 1
2221
*/
23-
export const computeErrorCode = (e: Error | SfError | SfCommandError): number => {
22+
export const computeErrorCode = (e: Error | SfError | Errors.CLIError): number => {
2423
// regardless of the exitCode, we'll set gacks and TypeError to a specific exit code
2524
if (errorIsGack(e)) {
2625
return 20;
@@ -66,5 +65,5 @@ export const errorIsTypeError = (error: Error | SfError): boolean =>
6665
('cause' in error && error.cause instanceof Error && errorIsTypeError(error.cause));
6766

6867
/** custom typeGuard for handling the fact the SfCommand doesn't know about oclif error structure */
69-
const isOclifError = <T extends Error | SfError | SfCommandError>(e: T): e is T & CLIError =>
68+
const isOclifError = <T extends Error | SfError | Errors.CLIError>(e: T): e is T & Errors.CLIError =>
7069
'oclif' in e ? true : false;

src/sfCommand.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77
import os from 'node:os';
8-
import { Command, Config, HelpSection, Flags } from '@oclif/core';
8+
import { Errors, Command, Config, HelpSection, Flags } from '@oclif/core';
99
import {
1010
envVars,
1111
Messages,
@@ -367,7 +367,7 @@ export abstract class SfCommand<T> extends Command {
367367
}
368368

369369
// eslint-disable-next-line @typescript-eslint/require-await
370-
protected async catch(error: Error | SfError | SfCommand.Error): Promise<never> {
370+
protected async catch(error: Error | SfError | Errors.CLIError): Promise<never> {
371371
// stop any spinners to prevent it from unintentionally swallowing output.
372372
// If there is an active spinner, it'll say "Error" instead of "Done"
373373
this.spinner.stop(StandardColors.error('Error'));

test/unit/sfCommand.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77
import { Flags } from '@oclif/core';
8+
import { Errors } from '@oclif/core';
89
import { Lifecycle } from '@salesforce/core';
910
import { TestContext } from '@salesforce/core/testSetup';
1011
import { assert, expect } from 'chai';
@@ -70,12 +71,20 @@ class TestCommandErrors extends SfCommand<void> {
7071
data: errData,
7172
});
7273

74+
public static buildOclifError = () => {
75+
const err = new Errors.CLIError('Nonexistent flag: --INVALID\nSee more help with --help');
76+
err.oclif = { exit: 2 };
77+
err.code = undefined;
78+
return err;
79+
};
80+
7381
// eslint-disable-next-line @typescript-eslint/member-ordering
7482
public static errors: { [x: string]: Error } = {
7583
error: new Error('error message'),
7684
sfError: new SfError('sfError message'),
7785
fullError: TestCommandErrors.buildFullError(),
7886
fullSfError: TestCommandErrors.buildFullSfError(),
87+
oclifError: TestCommandErrors.buildOclifError(),
7988
};
8089

8190
// eslint-disable-next-line @typescript-eslint/member-ordering
@@ -239,6 +248,38 @@ describe('error standardization', () => {
239248

240249
afterEach(() => {
241250
process.removeListener('sfCommandError', sfCommandErrorCb);
251+
process.exitCode = undefined;
252+
});
253+
254+
it('should log correct error when command throws an oclif Error', async () => {
255+
const logToStderrStub = $$.SANDBOX.stub(SfCommand.prototype, 'logToStderr');
256+
try {
257+
await TestCommandErrors.run(['--error', 'oclifError']);
258+
expect(false, 'error should have been thrown').to.be.true;
259+
} catch (e: unknown) {
260+
expect(e).to.be.instanceOf(SfCommandError);
261+
const err = e as SfCommand.Error;
262+
263+
// Ensure the error was logged to the console
264+
expect(logToStderrStub.callCount).to.equal(1);
265+
expect(logToStderrStub.firstCall.firstArg).to.contain(err.message);
266+
267+
// Ensure the error has expected properties
268+
expect(err).to.have.property('actions', undefined);
269+
expect(err).to.have.property('exitCode', 2);
270+
expect(err).to.have.property('context', 'TestCommandErrors');
271+
expect(err).to.have.property('data', undefined);
272+
expect(err).to.have.property('cause').and.be.ok;
273+
expect(err).to.have.property('code', '2');
274+
expect(err).to.have.property('status', 2);
275+
expect(err).to.have.property('stack').and.be.ok;
276+
expect(err).to.have.property('skipOclifErrorHandling', true);
277+
expect(err).to.have.deep.property('oclif', { exit: 2 });
278+
279+
// Ensure a sfCommandError event was emitted with the expected data
280+
expect(sfCommandErrorData[0]).to.equal(err);
281+
expect(sfCommandErrorData[1]).to.equal('testcommanderrors');
282+
}
242283
});
243284

244285
it('should log correct error when command throws an Error', async () => {

0 commit comments

Comments
 (0)