Skip to content

Commit 3abcf2d

Browse files
jangjos-128Yazan-O
andauthored
fix(doctor): validate output mode (#251)
Co-authored-by: Yazan-O <mohamad.yazan@ou.edu>
1 parent d2f0357 commit 3abcf2d

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(() => {
@@ -271,4 +280,36 @@ describe('createDoctorCommand wiring', () => {
271280
it('--help describes the diagnostic', () => {
272281
expect(createDoctorCommand().helpInformation()).toContain('Diagnose');
273282
});
283+
284+
it('rejects invalid --output with the shared VALIDATION_ERROR', async () => {
285+
const rejection = await makeDoctorProgram()
286+
.parseAsync(['node', 'ts', '--output', 'yaml', 'doctor'])
287+
.catch((error: unknown) => error);
288+
expect(rejection).toBeInstanceOf(ApiError);
289+
expect(rejection).toMatchObject({
290+
code: 'VALIDATION_ERROR',
291+
exitCode: 5,
292+
nextAction: 'Flag `--output` is invalid: must be one of: json, text.',
293+
});
294+
});
295+
296+
it('accepts valid --output modes through command wiring', async () => {
297+
writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath });
298+
for (const mode of ['text', 'json'] as const) {
299+
const { capture, deps } = makeCapture();
300+
await makeDoctorProgram({ ...healthyDeps(credentialsPath), ...deps }).parseAsync([
301+
'node',
302+
'ts',
303+
'--output',
304+
mode,
305+
'doctor',
306+
]);
307+
const raw = capture.stdout.join('');
308+
if (mode === 'json') {
309+
expect((JSON.parse(raw) as DoctorReport).failures).toBe(0);
310+
} else {
311+
expect(raw).toContain('All checks passed.');
312+
}
313+
}
314+
});
274315
});

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 { emitV3RoutingAdvisory, routingLabel } from '../lib/v3-advisory.js';
3030
import { VERSION } from '../version.js';
@@ -287,7 +287,7 @@ function resolveCommonOptions(command: Command): CommonOptions {
287287
};
288288
return {
289289
profile: globals.profile ?? 'default',
290-
output: globals.output ?? 'text',
290+
output: resolveOutputMode(globals.output),
291291
endpointUrl: globals.endpointUrl,
292292
debug: globals.debug ?? false,
293293
verbose: globals.verbose ?? false,

0 commit comments

Comments
 (0)