refactor(doctor): redesign with 3-tier output and consolidated checks

Consolidate 16 separate checks into 5 (system, config, providers, tools, models).
Add 3-tier formatting: default (problems-only), --status (dashboard), --verbose (deep diagnostics).
Read actual loaded plugin version from opencode cache directory.
Check environment variables for provider authentication.
This commit is contained in:
YeonGyu-Kim
2026-02-13 17:29:38 +09:00
parent f7295db277
commit fe5c0ad8c2
45 changed files with 1808 additions and 3328 deletions
+102 -193
View File
@@ -1,218 +1,127 @@
import { describe, it, expect } from "bun:test"
import {
formatStatusSymbol,
formatCheckResult,
formatCategoryHeader,
formatSummary,
formatHeader,
formatFooter,
formatJsonOutput,
formatBox,
formatHelpSuggestions,
} from "./formatter"
import type { CheckResult, DoctorSummary, DoctorResult } from "./types"
import { afterEach, describe, expect, it, mock } from "bun:test"
import type { DoctorResult } from "./types"
function createDoctorResult(): DoctorResult {
return {
results: [
{ name: "System", status: "pass", message: "ok", issues: [] },
{ name: "Configuration", status: "warn", message: "warn", issues: [] },
],
systemInfo: {
opencodeVersion: "1.0.200",
opencodePath: "/usr/local/bin/opencode",
pluginVersion: "3.4.0",
loadedVersion: "3.4.0",
bunVersion: "1.2.0",
configPath: "/tmp/opencode.jsonc",
configValid: true,
isLocalDev: false,
},
providers: [{ id: "anthropic", name: "Anthropic", available: true, hasEnvVar: true, hasPlugin: true }],
tools: {
lspInstalled: 2,
lspTotal: 4,
astGrepCli: true,
astGrepNapi: false,
commentChecker: true,
ghCli: { installed: true, authenticated: true, username: "yeongyu" },
mcpBuiltin: ["context7", "grep_app"],
mcpUser: ["custom"],
},
summary: {
total: 2,
passed: 1,
failed: 0,
warnings: 1,
skipped: 0,
duration: 12,
},
exitCode: 0,
}
}
describe("formatter", () => {
describe("formatStatusSymbol", () => {
it("returns green check for pass", () => {
const symbol = formatStatusSymbol("pass")
expect(symbol).toContain("\u2713")
})
it("returns red cross for fail", () => {
const symbol = formatStatusSymbol("fail")
expect(symbol).toContain("\u2717")
})
it("returns yellow warning for warn", () => {
const symbol = formatStatusSymbol("warn")
expect(symbol).toContain("\u26A0")
})
it("returns dim circle for skip", () => {
const symbol = formatStatusSymbol("skip")
expect(symbol).toContain("\u25CB")
})
afterEach(() => {
mock.restore()
})
describe("formatCheckResult", () => {
it("includes name and message", () => {
const result: CheckResult = {
name: "Test Check",
status: "pass",
message: "All good",
}
describe("formatDoctorOutput", () => {
it("dispatches to default formatter for default mode", async () => {
//#given
const formatDefaultMock = mock(() => "default-output")
const formatStatusMock = mock(() => "status-output")
const formatVerboseMock = mock(() => "verbose-output")
mock.module("./format-default", () => ({ formatDefault: formatDefaultMock }))
mock.module("./format-status", () => ({ formatStatus: formatStatusMock }))
mock.module("./format-verbose", () => ({ formatVerbose: formatVerboseMock }))
const { formatDoctorOutput } = await import(`./formatter?default=${Date.now()}`)
const output = formatCheckResult(result, false)
//#when
const output = formatDoctorOutput(createDoctorResult(), "default")
expect(output).toContain("Test Check")
expect(output).toContain("All good")
//#then
expect(output).toBe("default-output")
expect(formatDefaultMock).toHaveBeenCalledTimes(1)
expect(formatStatusMock).toHaveBeenCalledTimes(0)
expect(formatVerboseMock).toHaveBeenCalledTimes(0)
})
it("includes details when verbose", () => {
const result: CheckResult = {
name: "Test Check",
status: "pass",
message: "OK",
details: ["Detail 1", "Detail 2"],
}
it("dispatches to status formatter for status mode", async () => {
//#given
const formatDefaultMock = mock(() => "default-output")
const formatStatusMock = mock(() => "status-output")
const formatVerboseMock = mock(() => "verbose-output")
mock.module("./format-default", () => ({ formatDefault: formatDefaultMock }))
mock.module("./format-status", () => ({ formatStatus: formatStatusMock }))
mock.module("./format-verbose", () => ({ formatVerbose: formatVerboseMock }))
const { formatDoctorOutput } = await import(`./formatter?status=${Date.now()}`)
const output = formatCheckResult(result, true)
//#when
const output = formatDoctorOutput(createDoctorResult(), "status")
expect(output).toContain("Detail 1")
expect(output).toContain("Detail 2")
//#then
expect(output).toBe("status-output")
expect(formatDefaultMock).toHaveBeenCalledTimes(0)
expect(formatStatusMock).toHaveBeenCalledTimes(1)
expect(formatVerboseMock).toHaveBeenCalledTimes(0)
})
it("hides details when not verbose", () => {
const result: CheckResult = {
name: "Test Check",
status: "pass",
message: "OK",
details: ["Detail 1"],
}
it("dispatches to verbose formatter for verbose mode", async () => {
//#given
const formatDefaultMock = mock(() => "default-output")
const formatStatusMock = mock(() => "status-output")
const formatVerboseMock = mock(() => "verbose-output")
mock.module("./format-default", () => ({ formatDefault: formatDefaultMock }))
mock.module("./format-status", () => ({ formatStatus: formatStatusMock }))
mock.module("./format-verbose", () => ({ formatVerbose: formatVerboseMock }))
const { formatDoctorOutput } = await import(`./formatter?verbose=${Date.now()}`)
const output = formatCheckResult(result, false)
//#when
const output = formatDoctorOutput(createDoctorResult(), "verbose")
expect(output).not.toContain("Detail 1")
})
})
describe("formatCategoryHeader", () => {
it("formats category name with styling", () => {
const header = formatCategoryHeader("installation")
expect(header).toContain("Installation")
})
})
describe("formatSummary", () => {
it("shows all counts", () => {
const summary: DoctorSummary = {
total: 10,
passed: 7,
failed: 1,
warnings: 2,
skipped: 0,
duration: 150,
}
const output = formatSummary(summary)
expect(output).toContain("7 passed")
expect(output).toContain("1 failed")
expect(output).toContain("2 warnings")
expect(output).toContain("10 checks")
expect(output).toContain("150ms")
})
})
describe("formatHeader", () => {
it("includes doctor branding", () => {
const header = formatHeader()
expect(header).toContain("Doctor")
})
})
describe("formatFooter", () => {
it("shows error message when failures", () => {
const summary: DoctorSummary = {
total: 5,
passed: 4,
failed: 1,
warnings: 0,
skipped: 0,
duration: 100,
}
const footer = formatFooter(summary)
expect(footer).toContain("Issues detected")
})
it("shows warning message when warnings only", () => {
const summary: DoctorSummary = {
total: 5,
passed: 4,
failed: 0,
warnings: 1,
skipped: 0,
duration: 100,
}
const footer = formatFooter(summary)
expect(footer).toContain("warnings")
})
it("shows success message when all pass", () => {
const summary: DoctorSummary = {
total: 5,
passed: 5,
failed: 0,
warnings: 0,
skipped: 0,
duration: 100,
}
const footer = formatFooter(summary)
expect(footer).toContain("operational")
//#then
expect(output).toBe("verbose-output")
expect(formatDefaultMock).toHaveBeenCalledTimes(0)
expect(formatStatusMock).toHaveBeenCalledTimes(0)
expect(formatVerboseMock).toHaveBeenCalledTimes(1)
})
})
describe("formatJsonOutput", () => {
it("returns valid JSON", () => {
const result: DoctorResult = {
results: [{ name: "Test", status: "pass", message: "OK" }],
summary: { total: 1, passed: 1, failed: 0, warnings: 0, skipped: 0, duration: 50 },
exitCode: 0,
}
it("returns valid JSON payload", async () => {
//#given
const { formatJsonOutput } = await import(`./formatter?json=${Date.now()}`)
const result = createDoctorResult()
//#when
const output = formatJsonOutput(result)
const parsed = JSON.parse(output)
const parsed = JSON.parse(output) as DoctorResult
expect(parsed.results.length).toBe(1)
expect(parsed.summary.total).toBe(1)
//#then
expect(parsed.summary.total).toBe(2)
expect(parsed.systemInfo.pluginVersion).toBe("3.4.0")
expect(parsed.tools.ghCli.username).toBe("yeongyu")
expect(parsed.exitCode).toBe(0)
})
})
describe("formatBox", () => {
it("wraps content in box", () => {
const box = formatBox("Test content")
expect(box).toContain("Test content")
expect(box).toContain("\u2500")
})
it("includes title when provided", () => {
const box = formatBox("Content", "My Title")
expect(box).toContain("My Title")
})
})
describe("formatHelpSuggestions", () => {
it("extracts suggestions from failed checks", () => {
const results: CheckResult[] = [
{ name: "Test", status: "fail", message: "Error", details: ["Run: fix-command"] },
{ name: "OK", status: "pass", message: "Good" },
]
const suggestions = formatHelpSuggestions(results)
expect(suggestions).toContain("Run: fix-command")
})
it("returns empty array when no failures", () => {
const results: CheckResult[] = [
{ name: "OK", status: "pass", message: "Good" },
]
const suggestions = formatHelpSuggestions(results)
expect(suggestions.length).toBe(0)
})
})
})