Skip to content

feat(cli): add "testsprite doctor" environment diagnostic#183

Merged
zeshi-du merged 2 commits into
TestSprite:mainfrom
Andy00L:feat/doctor
Jul 6, 2026
Merged

feat(cli): add "testsprite doctor" environment diagnostic#183
zeshi-du merged 2 commits into
TestSprite:mainfrom
Andy00L:feat/doctor

Conversation

@Andy00L

@Andy00L Andy00L commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Adds testsprite doctor: a one-shot environment diagnostic.

Runs a fixed checklist and prints an OK/WARN/FAIL report: CLI version, Node.js runtime, active profile, API endpoint, credentials, live connectivity (GET /me + key validity), and whether the verify skill is installed. Exits non-zero when any check FAILS, so it gates a CI step or an agent preflight (testsprite doctor && testsprite test run ).

Every check reuses the same helpers the real commands use (loadConfig, assertValidEndpointUrl, makeHttpClient, isVerifySkillInstalled), so the report reflects what a subsequent command resolves. The API key is never printed (credentials check confirms presence only).

  • New command: src/commands/doctor.ts
  • 12 unit tests, fully offline
  • --output json for a machine-readable report

Fixes #73

Summary by CodeRabbit

  • New Features
    • Added a new doctor command to diagnose CLI setup and environment health.
    • Supports human-readable and JSON output for automated checks, including an OK/WARN/FAIL checklist.
  • Bug Fixes
    • Improved handling of missing credentials, invalid endpoints, authentication/connectivity errors, and outdated Node.js versions.
    • Sensitive values are no longer exposed in diagnostic output.
    • Dry-run mode now clearly skips live connectivity checks and reports skipped items.

One-shot preflight: checks CLI version, Node runtime, profile, API endpoint,
credentials, live connectivity (GET /me), and verify-skill install. Prints an
OK/WARN/FAIL report and exits non-zero when any check fails, so it gates a CI
step or agent preflight. Reuses the real resolution helpers; the API key is
never printed.

Fixes TestSprite#73
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: be629913-4752-446a-bf98-73f5b4da6859

📥 Commits

Reviewing files that changed from the base of the PR and between f371e53 and 3ba7438.

📒 Files selected for processing (2)
  • src/commands/doctor.test.ts
  • src/commands/doctor.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/commands/doctor.test.ts
  • src/commands/doctor.ts

Walkthrough

Adds a new testsprite doctor CLI command that performs environment diagnostics for CLI version, Node.js runtime, active profile, endpoint, credentials, live connectivity, and verify-skill presence. It prints an OK/WARN/FAIL report, exits non-zero on failures, registers the command, and adds unit tests.

Changes

Doctor Command Feature

Layer / File(s) Summary
Doctor types and orchestration
src/commands/doctor.ts
Defines DoctorStatus, DoctorCheck, DoctorReport, and DoctorDeps, and implements runDoctor to load config, run the checklist, count failures and warnings, print the report, and throw CLIError on failure.
Individual diagnostic checks
src/commands/doctor.ts
Implements the Node.js runtime, endpoint, credentials, skill, and connectivity checks, including skip handling and /me error classification.
Report rendering and command wiring
src/commands/doctor.ts
Adds STATUS_LABEL, renderDoctor, createDoctorCommand, resolveCommonOptions, parseRequestTimeoutFlag, and makeOutput.
CLI registration
src/index.ts
Imports createDoctorCommand and registers it as a top-level subcommand.
Doctor command test suite
src/commands/doctor.test.ts
Adds tests covering healthy runs, secret masking, JSON output, failure scenarios, warning-only cases, dry-run behavior, and command metadata.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DoctorCommand
  participant runDoctor
  participant HttpClient
  participant Output

  DoctorCommand->>runDoctor: resolveCommonOptions + invoke
  runDoctor->>runDoctor: build checklist
  runDoctor->>HttpClient: GET /me for connectivity
  HttpClient-->>runDoctor: response or ApiError
  runDoctor->>Output: renderDoctor(report)
  Output-->>DoctorCommand: printed report
  runDoctor-->>DoctorCommand: DoctorReport or CLIError(exitCode 1)
Loading

Related issues: #73

Suggested reviewers: ruili-testsprite, zeshi-du

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The command, tests, and registration match #73, but the root --help snapshot update is not shown in the changes. Add or update the root --help snapshot to reflect the new doctor subcommand.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately names the new doctor command and its diagnostic purpose.
Out of Scope Changes check ✅ Passed The changes stay focused on the new doctor command, its tests, and command registration.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/commands/doctor.test.ts (2)

65-106: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

No test for --debug/verbose logging excluding the API key.

The healthy-path suite covers text and JSON output but has no case exercising debug: true alongside a real API key to confirm debug logging never surfaces it. The referenced documentation explicitly calls out this requirement separately from the general "never print" case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/doctor.test.ts` around lines 65 - 106, Add a healthy-path test
in runDoctor that runs with debug enabled and a real API key, then assert the
combined stdout/stderr still does not contain the secret. Reuse the existing
runDoctor, makeCapture, and healthyDeps helpers from doctor.test.ts, and keep
the assertion focused on debug/verbose output specifically so the masking
behavior is covered separately from the current “never prints” case.

Source: Path instructions


122-176: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Failure tests don't consistently assert the exit code.

Only the first failing-check test (Line 116) checks rejection toMatchObject({ exitCode: 1 }). The invalid-endpoint (Lines 122-133), rejected-key (135-149), non-auth-error (151-163), and outdated-Node (165-176) tests only assert instanceOf(CLIError), without pinning the exit code. Given the path instructions require exit codes to map to the documented table for every error path, these tests should each also assert exitCode: 1 to guard against a future ad-hoc exit code creeping into one specific check path.

Example fix (repeat for each failing test)
     expect(rejection).toBeInstanceOf(CLIError);
+    expect(rejection).toMatchObject({ exitCode: 1 });

As per path instructions, "Exit-code mapping: error paths must map to the documented exit code (see DOCUMENTATION.md / the exit-code table); don't introduce ad-hoc codes."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/doctor.test.ts` around lines 122 - 176, The failing-path tests
in doctor.test.ts are missing the documented exit-code assertion, so update each
of the affected runDoctor rejection checks to also verify the CLIError carries
exitCode: 1. Use the existing rejection variables in the invalid endpoint,
rejected API key, non-auth /me error, and outdated Node cases, and add the same
exit-code expectation already used in the first failing-check test so all error
paths are pinned to the required mapping.

Source: Path instructions

src/commands/doctor.ts (1)

127-138: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Discards the specific validation reason from assertValidEndpointUrl.

The catch block replaces the thrown error's actual message (which distinguishes an unparsable URL from an unsupported scheme, per client-factory.ts) with a generic templated string. Since doctor's purpose is diagnosing setup issues, surfacing the original message would give users more actionable detail.

♻️ Proposed fix
 function checkEndpoint(apiUrl: string): DoctorCheck {
   try {
     assertValidEndpointUrl(apiUrl);
     return { name: 'API endpoint', status: 'ok', detail: apiUrl };
-  } catch {
+  } catch (error) {
     return {
       name: 'API endpoint',
       status: 'fail',
-      detail: `"${apiUrl}" is not a valid http(s) URL`,
+      detail: error instanceof Error ? error.message : `"${apiUrl}" is not a valid http(s) URL`,
     };
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/doctor.ts` around lines 127 - 138, The checkEndpoint helper is
swallowing the specific error from assertValidEndpointUrl and replacing it with
a generic message. Update the catch in checkEndpoint to capture the thrown error
and use its actual message in the fail detail, so doctor reports the exact
validation reason instead of only saying the URL is invalid. Keep the existing
API endpoint status shape and preserve the success path unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/commands/doctor.test.ts`:
- Around line 92-105: The JSON report path in runDoctor is not covered by the
existing API-key masking assertion, so a leak in --output json could go
unnoticed. Extend the doctor test that parses JSON output to verify the raw
serialized stdout and/or parsed report does not contain the API key value from
writeProfile. Use the existing helpers in src/commands/doctor.test.ts,
especially runDoctor, makeCapture, and DoctorReport, and assert that secret
masking is applied in the JSON serialization path as well as the text renderer.

In `@src/commands/doctor.ts`:
- Around line 31-32: The Node.js validation in checkNodeVersion is too coarse
because it only checks the major version via MIN_NODE_MAJOR, so it accepts
unsupported 20.x, 21.x, and 23.x releases. Update the doctor command’s version
check to compare full semver versions against the documented minimums (20.19+,
22.13+, or 24+), and adjust the logic in checkNodeVersion to reject any release
below those thresholds instead of relying on major-only comparison.
- Around line 253-273: The request-timeout handling in
resolveCommonOptions/parseRequestTimeoutFlag silently ignores invalid values
instead of using the shared validation path. Update doctor.ts to reuse the
shared --request-timeout parser from client-factory.ts so malformed,
non-numeric, or non-positive values raise the same validation error as other
commands, while still returning the parsed milliseconds for valid input.

---

Nitpick comments:
In `@src/commands/doctor.test.ts`:
- Around line 65-106: Add a healthy-path test in runDoctor that runs with debug
enabled and a real API key, then assert the combined stdout/stderr still does
not contain the secret. Reuse the existing runDoctor, makeCapture, and
healthyDeps helpers from doctor.test.ts, and keep the assertion focused on
debug/verbose output specifically so the masking behavior is covered separately
from the current “never prints” case.
- Around line 122-176: The failing-path tests in doctor.test.ts are missing the
documented exit-code assertion, so update each of the affected runDoctor
rejection checks to also verify the CLIError carries exitCode: 1. Use the
existing rejection variables in the invalid endpoint, rejected API key, non-auth
/me error, and outdated Node cases, and add the same exit-code expectation
already used in the first failing-check test so all error paths are pinned to
the required mapping.

In `@src/commands/doctor.ts`:
- Around line 127-138: The checkEndpoint helper is swallowing the specific error
from assertValidEndpointUrl and replacing it with a generic message. Update the
catch in checkEndpoint to capture the thrown error and use its actual message in
the fail detail, so doctor reports the exact validation reason instead of only
saying the URL is invalid. Keep the existing API endpoint status shape and
preserve the success path unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 907a5170-c290-498b-ac9e-9d89f15154bd

📥 Commits

Reviewing files that changed from the base of the PR and between b9e9601 and f371e53.

⛔ Files ignored due to path filters (1)
  • test/__snapshots__/help.snapshot.test.ts.snap is excluded by !**/*.snap, !**/*.snap
📒 Files selected for processing (3)
  • src/commands/doctor.test.ts
  • src/commands/doctor.ts
  • src/index.ts

Comment thread src/commands/doctor.test.ts Outdated
Comment thread src/commands/doctor.ts Outdated
Comment thread src/commands/doctor.ts
Comment on lines +253 to +273
function resolveCommonOptions(command: Command): CommonOptions {
const globals = command.optsWithGlobals() as Partial<CommonOptions> & {
requestTimeout?: string;
};
return {
profile: globals.profile ?? 'default',
output: globals.output ?? 'text',
endpointUrl: globals.endpointUrl,
debug: globals.debug ?? false,
verbose: globals.verbose ?? false,
dryRun: globals.dryRun ?? false,
requestTimeoutMs: parseRequestTimeoutFlag(globals.requestTimeout),
};
}

function parseRequestTimeoutFlag(raw: string | undefined): number | undefined {
if (raw === undefined) return undefined;
const seconds = Number(raw);
if (!Number.isFinite(seconds) || seconds <= 0) return undefined;
return Math.round(seconds * 1000);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether request-timeout parsing/validation already exists elsewhere and whether the 1-600 range is enforced anywhere
rg -n "requestTimeout|request-timeout" src --type=ts -C3
ast-grep run --pattern 'function resolveCommonOptions($_) { $$$ }' --lang typescript src
ast-grep run --pattern 'function parseRequestTimeoutFlag($_) { $$$ }' --lang typescript src

Repository: TestSprite/testsprite-cli

Length of output: 37473


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the doctor command and the shared timeout parser/validation.
ast-grep outline src/commands/doctor.ts --view expanded
echo '--- doctor.ts around imports/helpers ---'
sed -n '1,340p' src/commands/doctor.ts

echo '--- shared request-timeout parser in client-factory.ts ---'
sed -n '190,245p' src/lib/client-factory.ts

echo '--- request-timeout option definition in src/index.ts ---'
sed -n '48,66p' src/index.ts

Repository: TestSprite/testsprite-cli

Length of output: 15215


Reuse the shared --request-timeout parser

doctor.ts should call the shared parser from src/lib/client-factory.ts instead of silently treating malformed values as “unset”. The 600s ceiling is already enforced downstream in makeHttpClient; the gap here is that non-numeric or <= 0 --request-timeout input falls back to the default 120s instead of raising the same validation error as the other commands.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/doctor.ts` around lines 253 - 273, The request-timeout handling
in resolveCommonOptions/parseRequestTimeoutFlag silently ignores invalid values
instead of using the shared validation path. Update doctor.ts to reuse the
shared --request-timeout parser from client-factory.ts so malformed,
non-numeric, or non-positive values raise the same validation error as other
commands, while still returning the parsed milliseconds for valid input.

Source: Path instructions

- 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).
@Andy00L

Andy00L commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review:

  • JSON key leak: the --output json test now asserts the API key never appears in the JSON serialization path (the existing masking test only covered text output).
  • --request-timeout: now raises a validation error on malformed input instead of silently falling back to the default, matching the other commands.
  • Node check: switched to the same shouldRejectNodeVersion guard the entrypoint uses, so doctor reports exactly what the CLI enforces at startup rather than a divergent hardcoded check. Kept at the runtime floor (not the precise 20.19+ minor) on purpose, so doctor never FAILs a Node the CLI itself runs fine on; the precise engines floor is enforced by npm engine-strict at install.

@zeshi-du zeshi-du left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed: scoped, tests included, CI green across the matrix.

@zeshi-du zeshi-du merged commit edc31c8 into TestSprite:main Jul 6, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Hackathon] Add a "doctor" command to diagnose CLI setup

2 participants