From cfd87830aa43a0811a4fb12c27a28735f42a428e Mon Sep 17 00:00:00 2001 From: Yazan-O Date: Tue, 7 Jul 2026 11:14:34 -0500 Subject: [PATCH] fix(doctor): validate output mode --- src/commands/doctor.test.ts | 43 ++++++++++++++++++++++++++++++++++++- src/commands/doctor.ts | 4 ++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/commands/doctor.test.ts b/src/commands/doctor.test.ts index 230398a..d712c11 100644 --- a/src/commands/doctor.test.ts +++ b/src/commands/doctor.test.ts @@ -9,8 +9,9 @@ import { mkdtempSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +import { Command } from 'commander'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { CLIError } from '../lib/errors.js'; +import { ApiError, CLIError } from '../lib/errors.js'; import { writeProfile } from '../lib/credentials.js'; import type { DoctorDeps, DoctorReport } from './doctor.js'; import { createDoctorCommand, runDoctor } from './doctor.js'; @@ -56,6 +57,14 @@ function healthyDeps(credentialsPath: string, extra: Partial = {}): }; } +function makeDoctorProgram(deps: DoctorDeps = {}): Command { + const program = new Command(); + program.exitOverride(); + program.option('--output ', 'output', 'text'); + program.addCommand(createDoctorCommand(deps)); + return program; +} + let credentialsPath: string; beforeEach(() => { @@ -226,4 +235,36 @@ describe('createDoctorCommand wiring', () => { it('--help describes the diagnostic', () => { expect(createDoctorCommand().helpInformation()).toContain('Diagnose'); }); + + it('rejects invalid --output with the shared VALIDATION_ERROR', async () => { + const rejection = await makeDoctorProgram() + .parseAsync(['node', 'ts', '--output', 'yaml', 'doctor']) + .catch((error: unknown) => error); + expect(rejection).toBeInstanceOf(ApiError); + expect(rejection).toMatchObject({ + code: 'VALIDATION_ERROR', + exitCode: 5, + nextAction: 'Flag `--output` is invalid: must be one of: json, text.', + }); + }); + + it('accepts valid --output modes through command wiring', async () => { + writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath }); + for (const mode of ['text', 'json'] as const) { + const { capture, deps } = makeCapture(); + await makeDoctorProgram({ ...healthyDeps(credentialsPath), ...deps }).parseAsync([ + 'node', + 'ts', + '--output', + mode, + 'doctor', + ]); + const raw = capture.stdout.join(''); + if (mode === 'json') { + expect((JSON.parse(raw) as DoctorReport).failures).toBe(0); + } else { + expect(raw).toContain('All checks passed.'); + } + } + }); }); diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 2fa0c57..aeb1e24 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -24,7 +24,7 @@ import { import { loadConfig } from '../lib/config.js'; import { ApiError, CLIError, localValidationError } from '../lib/errors.js'; import type { FetchImpl } from '../lib/http.js'; -import { GLOBAL_OPTS_HINT, Output, type OutputMode } from '../lib/output.js'; +import { GLOBAL_OPTS_HINT, Output, resolveOutputMode, type OutputMode } from '../lib/output.js'; import { isVerifySkillInstalled } from '../lib/skill-nudge.js'; import { VERSION } from '../version.js'; import { MIN_SUPPORTED_NODE_MAJOR, shouldRejectNodeVersion } from '../version-guard.js'; @@ -257,7 +257,7 @@ function resolveCommonOptions(command: Command): CommonOptions { }; return { profile: globals.profile ?? 'default', - output: globals.output ?? 'text', + output: resolveOutputMode(globals.output), endpointUrl: globals.endpointUrl, debug: globals.debug ?? false, verbose: globals.verbose ?? false,