Skip to content

Commit cfd8783

Browse files
committed
fix(doctor): validate output mode
1 parent 3305dfa commit cfd8783

2 files changed

Lines changed: 44 additions & 3 deletions

File tree

src/commands/doctor.test.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
import { mkdtempSync } from 'node:fs';
1010
import { tmpdir } from 'node:os';
1111
import { join } from 'node:path';
12+
import { Command } from 'commander';
1213
import { beforeEach, describe, expect, it, vi } from 'vitest';
13-
import { CLIError } from '../lib/errors.js';
14+
import { ApiError, CLIError } from '../lib/errors.js';
1415
import { writeProfile } from '../lib/credentials.js';
1516
import type { DoctorDeps, DoctorReport } from './doctor.js';
1617
import { createDoctorCommand, runDoctor } from './doctor.js';
@@ -56,6 +57,14 @@ function healthyDeps(credentialsPath: string, extra: Partial<DoctorDeps> = {}):
5657
};
5758
}
5859

60+
function makeDoctorProgram(deps: DoctorDeps = {}): Command {
61+
const program = new Command();
62+
program.exitOverride();
63+
program.option('--output <mode>', 'output', 'text');
64+
program.addCommand(createDoctorCommand(deps));
65+
return program;
66+
}
67+
5968
let credentialsPath: string;
6069

6170
beforeEach(() => {
@@ -226,4 +235,36 @@ describe('createDoctorCommand wiring', () => {
226235
it('--help describes the diagnostic', () => {
227236
expect(createDoctorCommand().helpInformation()).toContain('Diagnose');
228237
});
238+
239+
it('rejects invalid --output with the shared VALIDATION_ERROR', async () => {
240+
const rejection = await makeDoctorProgram()
241+
.parseAsync(['node', 'ts', '--output', 'yaml', 'doctor'])
242+
.catch((error: unknown) => error);
243+
expect(rejection).toBeInstanceOf(ApiError);
244+
expect(rejection).toMatchObject({
245+
code: 'VALIDATION_ERROR',
246+
exitCode: 5,
247+
nextAction: 'Flag `--output` is invalid: must be one of: json, text.',
248+
});
249+
});
250+
251+
it('accepts valid --output modes through command wiring', async () => {
252+
writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath });
253+
for (const mode of ['text', 'json'] as const) {
254+
const { capture, deps } = makeCapture();
255+
await makeDoctorProgram({ ...healthyDeps(credentialsPath), ...deps }).parseAsync([
256+
'node',
257+
'ts',
258+
'--output',
259+
mode,
260+
'doctor',
261+
]);
262+
const raw = capture.stdout.join('');
263+
if (mode === 'json') {
264+
expect((JSON.parse(raw) as DoctorReport).failures).toBe(0);
265+
} else {
266+
expect(raw).toContain('All checks passed.');
267+
}
268+
}
269+
});
229270
});

src/commands/doctor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
import { loadConfig } from '../lib/config.js';
2525
import { ApiError, CLIError, localValidationError } from '../lib/errors.js';
2626
import type { FetchImpl } from '../lib/http.js';
27-
import { GLOBAL_OPTS_HINT, Output, type OutputMode } from '../lib/output.js';
27+
import { GLOBAL_OPTS_HINT, Output, resolveOutputMode, type OutputMode } from '../lib/output.js';
2828
import { isVerifySkillInstalled } from '../lib/skill-nudge.js';
2929
import { VERSION } from '../version.js';
3030
import { MIN_SUPPORTED_NODE_MAJOR, shouldRejectNodeVersion } from '../version-guard.js';
@@ -257,7 +257,7 @@ function resolveCommonOptions(command: Command): CommonOptions {
257257
};
258258
return {
259259
profile: globals.profile ?? 'default',
260-
output: globals.output ?? 'text',
260+
output: resolveOutputMode(globals.output),
261261
endpointUrl: globals.endpointUrl,
262262
debug: globals.debug ?? false,
263263
verbose: globals.verbose ?? false,

0 commit comments

Comments
 (0)