Skip to content

Commit 3ba7438

Browse files
committed
fix(doctor): address review findings
- Node check reuses the CLI runtime guard (shouldRejectNodeVersion) instead of a hardcoded major floor, so the verdict matches what the entrypoint enforces at startup; the precise 20.19+/22.13+/24+ engines are enforced by npm engine-strict. - --request-timeout raises a validation error on malformed input instead of silently defaulting, matching the other commands. - The --output json test asserts the API key never appears in the JSON path (distinct from the text renderer already covered).
1 parent f371e53 commit 3ba7438

2 files changed

Lines changed: 26 additions & 14 deletions

File tree

src/commands/doctor.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,18 @@ describe('runDoctor — healthy environment', () => {
8989
expect(all).not.toContain('sk-super-secret-value');
9090
});
9191

92-
it('emits a machine-readable report under --output json', async () => {
93-
writeProfile('default', { apiKey: 'sk-abc' }, { path: credentialsPath });
92+
it('emits a machine-readable report under --output json without leaking the API key', async () => {
93+
writeProfile('default', { apiKey: 'sk-json-secret-value' }, { path: credentialsPath });
9494
const { capture, deps } = makeCapture();
9595
await runDoctor(
9696
{ profile: 'default', output: 'json', debug: false },
9797
{ ...healthyDeps(credentialsPath), ...deps },
9898
);
99-
const parsed = JSON.parse(capture.stdout.join('')) as DoctorReport;
99+
const raw = capture.stdout.join('');
100+
// Security: the JSON serialization path is distinct from the text renderer,
101+
// so assert the key never leaks here either.
102+
expect(raw).not.toContain('sk-json-secret-value');
103+
const parsed = JSON.parse(raw) as DoctorReport;
100104
expect(parsed.failures).toBe(0);
101105
expect(Array.isArray(parsed.checks)).toBe(true);
102106
expect(

src/commands/doctor.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,12 @@ import {
2222
type CommonOptions as FactoryCommonOptions,
2323
} from '../lib/client-factory.js';
2424
import { loadConfig } from '../lib/config.js';
25-
import { ApiError, CLIError } from '../lib/errors.js';
25+
import { ApiError, CLIError, localValidationError } from '../lib/errors.js';
2626
import type { FetchImpl } from '../lib/http.js';
2727
import { GLOBAL_OPTS_HINT, Output, type OutputMode } from '../lib/output.js';
2828
import { isVerifySkillInstalled } from '../lib/skill-nudge.js';
2929
import { VERSION } from '../version.js';
30-
31-
/** Minimum Node major version. sourceRef: package.json engines.node ">=20". */
32-
const MIN_NODE_MAJOR = 20;
30+
import { MIN_SUPPORTED_NODE_MAJOR, shouldRejectNodeVersion } from '../version-guard.js';
3331

3432
export type DoctorStatus = 'ok' | 'warn' | 'fail';
3533

@@ -113,14 +111,17 @@ export async function runDoctor(opts: CommonOptions, deps: DoctorDeps = {}): Pro
113111
}
114112

115113
function checkNodeVersion(nodeVersion: string): DoctorCheck {
116-
const major = parseInt(nodeVersion.split('.')[0] ?? '', 10);
117-
const ok = Number.isInteger(major) && major >= MIN_NODE_MAJOR;
114+
// Reuse the CLI's own runtime guard so the verdict matches exactly what the
115+
// entrypoint enforces at startup, rather than a divergent hardcoded check.
116+
// The precise engines floor (20.19+/22.13+/24+) is enforced by npm at install
117+
// time via .npmrc engine-strict. sourceRef: src/version-guard.ts.
118+
const rejected = shouldRejectNodeVersion(nodeVersion);
118119
return {
119120
name: 'Node.js',
120-
status: ok ? 'ok' : 'fail',
121-
detail: ok
122-
? `v${nodeVersion} (>=${MIN_NODE_MAJOR} required)`
123-
: `v${nodeVersion} is below the required Node ${MIN_NODE_MAJOR}; upgrade Node.js`,
121+
status: rejected ? 'fail' : 'ok',
122+
detail: rejected
123+
? `v${nodeVersion} is below the required Node ${MIN_SUPPORTED_NODE_MAJOR}; upgrade Node.js`
124+
: `v${nodeVersion} (>=${MIN_SUPPORTED_NODE_MAJOR} required)`,
124125
};
125126
}
126127

@@ -268,7 +269,14 @@ function resolveCommonOptions(command: Command): CommonOptions {
268269
function parseRequestTimeoutFlag(raw: string | undefined): number | undefined {
269270
if (raw === undefined) return undefined;
270271
const seconds = Number(raw);
271-
if (!Number.isFinite(seconds) || seconds <= 0) return undefined;
272+
if (!Number.isFinite(seconds) || seconds <= 0) {
273+
// Match the other commands: a malformed --request-timeout is a validation
274+
// error, not a silently-ignored default.
275+
throw localValidationError(
276+
'request-timeout',
277+
`must be a positive number of seconds (got "${raw}")`,
278+
);
279+
}
272280
return Math.round(seconds * 1000);
273281
}
274282

0 commit comments

Comments
 (0)