From 9bb1e6cfa099bd8e5ed1d12be5ec54f9c7197a4c Mon Sep 17 00:00:00 2001 From: Jubarte Date: Sat, 11 Jul 2026 10:09:20 -0300 Subject: [PATCH 1/2] fix: add doctor command for checking git, gh, API key, repo, upstream --- lib/doctor.ts | 325 ++++++++++++++++++++++++++++++++++++++++++++ scripts/cli.ts | 71 ++++++++++ test/doctor.test.ts | 110 +++++++++++++++ 3 files changed, 506 insertions(+) create mode 100644 lib/doctor.ts create mode 100644 test/doctor.test.ts diff --git a/lib/doctor.ts b/lib/doctor.ts new file mode 100644 index 0000000..86c3635 --- /dev/null +++ b/lib/doctor.ts @@ -0,0 +1,325 @@ +/** + * Environment health checks for `gitgen doctor`. + * Framework-agnostic — safe to unit-test with injected command runners. + */ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { + type CommitLanguage, + type GitgenConfig, + getConfigPath, + loadConfig, + looksLikeOpenRouterKey, + maskApiKey, + resolveRuntimeSettings, +} from "./config"; + +const pexec = promisify(execFile); + +export type DoctorStatus = "ok" | "warn" | "fail"; + +export type DoctorCheck = { + id: string; + label: string; + status: DoctorStatus; + detail: string; + tip?: string; +}; + +export type DoctorSummary = { + checks: DoctorCheck[]; + ok: number; + warn: number; + fail: number; + exitCode: number; +}; + +type EnvLike = Record; + +type RunCmdResult = { stdout: string; stderr: string; code: number }; + +export type DoctorDeps = { + cwd: string; + env?: EnvLike; + nodeVersion?: string; + platform?: NodeJS.Platform; + /** Override command runner (tests). Default: execFile with timeout. */ + runCmd?: (cmd: string, args: string[], cwd: string) => Promise; + /** Override fetch (tests). Default: global fetch. */ + fetchFn?: typeof fetch; + loadConfigFn?: (path: string) => GitgenConfig; + configPath?: string; +}; + +const MIN_NODE_MAJOR = 18; + +async function defaultRunCmd(cmd: string, args: string[], cwd: string): Promise { + try { + const { stdout, stderr } = await pexec(cmd, args, { + cwd, + maxBuffer: 4 * 1024 * 1024, + windowsHide: true, + timeout: 15_000, + }); + return { stdout: String(stdout), stderr: String(stderr), code: 0 }; + } catch (e) { + const err = e as { stdout?: string; stderr?: string; code?: number; message?: string }; + return { + stdout: String(err.stdout || ""), + stderr: String(err.stderr || err.message || ""), + code: typeof err.code === "number" ? err.code : 1, + }; + } +} + +/** Parse `v22.3.0` → 22. Returns 0 when unparseable. */ +export function parseNodeMajor(version: string): number { + const m = version.trim().match(/^v?(\d+)/); + return m ? parseInt(m[1], 10) : 0; +} + +export function summarizeDoctorChecks(checks: DoctorCheck[]): DoctorSummary { + const ok = checks.filter((c) => c.status === "ok").length; + const warn = checks.filter((c) => c.status === "warn").length; + const fail = checks.filter((c) => c.status === "fail").length; + return { checks, ok, warn, fail, exitCode: fail > 0 ? 1 : 0 }; +} + +async function checkNode(deps: DoctorDeps): Promise { + const major = parseNodeMajor(deps.nodeVersion ?? process.version); + if (major >= MIN_NODE_MAJOR) { + return { + id: "node", + label: "Node.js", + status: "ok", + detail: `v${major} (>= ${MIN_NODE_MAJOR})`, + }; + } + return { + id: "node", + label: "Node.js", + status: "fail", + detail: major ? `v${major} — need >= ${MIN_NODE_MAJOR}` : "version unknown", + tip: "Install Node 18+ from https://nodejs.org", + }; +} + +async function checkGit(deps: DoctorDeps, run: DoctorDeps["runCmd"]): Promise { + const result = await run!("git", ["--version"], deps.cwd); + if (result.code !== 0) { + return { + id: "git", + label: "Git", + status: "fail", + detail: "not found on PATH", + tip: "Install Git from https://git-scm.com", + }; + } + const line = (result.stdout || result.stderr).trim().split("\n")[0] || "git"; + return { id: "git", label: "Git", status: "ok", detail: line }; +} + +async function checkRepo(deps: DoctorDeps, run: DoctorDeps["runCmd"]): Promise { + const result = await run!("git", ["rev-parse", "--is-inside-work-tree"], deps.cwd); + const inside = result.code === 0 && result.stdout.trim() === "true"; + if (inside) { + return { id: "repo", label: "Git repo", status: "ok", detail: deps.cwd }; + } + return { + id: "repo", + label: "Git repo", + status: "fail", + detail: "not inside a work tree", + tip: "cd into a project folder or run git init", + }; +} + +async function checkBranch(deps: DoctorDeps, run: DoctorDeps["runCmd"]): Promise { + const repo = await run!("git", ["rev-parse", "--is-inside-work-tree"], deps.cwd); + if (repo.code !== 0 || repo.stdout.trim() !== "true") return null; + + const branch = await run!("git", ["branch", "--show-current"], deps.cwd); + const name = branch.stdout.trim(); + if (name) { + return { id: "branch", label: "Branch", status: "ok", detail: name }; + } + return { + id: "branch", + label: "Branch", + status: "warn", + detail: "detached HEAD", + tip: "checkout a branch before commit/push/PR workflows", + }; +} + +async function checkUpstream(deps: DoctorDeps, run: DoctorDeps["runCmd"]): Promise { + const repo = await run!("git", ["rev-parse", "--is-inside-work-tree"], deps.cwd); + if (repo.code !== 0 || repo.stdout.trim() !== "true") return null; + + const up = await run!("git", ["rev-parse", "--abbrev-ref", "@{upstream}"], deps.cwd); + const name = up.stdout.trim(); + if (name) { + return { id: "upstream", label: "Upstream", status: "ok", detail: name }; + } + return { + id: "upstream", + label: "Upstream", + status: "warn", + detail: "none — push may need -u", + tip: "gg cnp or gg b sets upstream on first push", + }; +} + +function checkApiKey( + configPath: string, + file: GitgenConfig, + env: EnvLike +): DoctorCheck { + const { apiKey, model, language } = resolveRuntimeSettings(file, env); + if (!apiKey) { + return { + id: "api-key", + label: "OpenRouter key", + status: "warn", + detail: "not set — AI commits use defaults", + tip: "run gg setup", + }; + } + const masked = maskApiKey(apiKey); + if (!looksLikeOpenRouterKey(apiKey)) { + return { + id: "api-key", + label: "OpenRouter key", + status: "warn", + detail: `${masked} — unexpected format`, + tip: "OpenRouter keys usually start with sk-or-", + }; + } + return { + id: "api-key", + label: "OpenRouter key", + status: "ok", + detail: `${masked} · ${model} · ${language as CommitLanguage}`, + }; +} + +function checkConfigPath(configPath: string): DoctorCheck { + return { + id: "config", + label: "Config", + status: "ok", + detail: configPath, + }; +} + +async function checkGh(run: DoctorDeps["runCmd"], cwd: string): Promise { + const ver = await run!("gh", ["--version"], cwd); + if (ver.code !== 0) { + return { + id: "gh", + label: "GitHub CLI", + status: "warn", + detail: "not found — PR commands need gh", + tip: "https://cli.github.com", + }; + } + const line = ver.stdout.trim().split("\n")[0] || "gh"; + return { id: "gh", label: "GitHub CLI", status: "ok", detail: line }; +} + +async function checkGhAuth(run: DoctorDeps["runCmd"], cwd: string): Promise { + const ver = await run!("gh", ["--version"], cwd); + if (ver.code !== 0) return null; + + const auth = await run!("gh", ["auth", "status"], cwd); + if (auth.code === 0) { + const host = auth.stderr.includes("github.com") || auth.stdout.includes("github.com") + ? "github.com" + : "authenticated"; + return { id: "gh-auth", label: "gh auth", status: "ok", detail: host }; + } + return { + id: "gh-auth", + label: "gh auth", + status: "warn", + detail: "not logged in", + tip: "run: gh auth login", + }; +} + +async function checkOpenRouter( + apiKey: string, + fetchFn: typeof fetch +): Promise { + if (!apiKey || !looksLikeOpenRouterKey(apiKey)) return null; + + try { + const res = await fetchFn("https://openrouter.ai/api/v1/models", { + method: "GET", + headers: { Authorization: `Bearer ${apiKey}` }, + signal: AbortSignal.timeout(12_000), + }); + if (res.ok) { + return { id: "openrouter", label: "OpenRouter API", status: "ok", detail: "reachable" }; + } + if (res.status === 401 || res.status === 403) { + return { + id: "openrouter", + label: "OpenRouter API", + status: "fail", + detail: `HTTP ${res.status} — key rejected`, + tip: "check the key at https://openrouter.ai/keys", + }; + } + return { + id: "openrouter", + label: "OpenRouter API", + status: "warn", + detail: `HTTP ${res.status}`, + }; + } catch (e) { + const reason = e instanceof Error ? e.message : String(e); + return { + id: "openrouter", + label: "OpenRouter API", + status: "warn", + detail: `unreachable (${reason})`, + tip: "check network / firewall", + }; + } +} + +/** Run all doctor checks and return a summary (exitCode 1 when any check failed). */ +export async function runDoctorChecks(deps: DoctorDeps): Promise { + const env = deps.env ?? process.env; + const run = deps.runCmd ?? defaultRunCmd; + const fetchFn = deps.fetchFn ?? fetch; + const configPath = deps.configPath ?? getConfigPath(env, deps.platform); + const loadConfigFn = deps.loadConfigFn ?? loadConfig; + const file = loadConfigFn(configPath); + const { apiKey } = resolveRuntimeSettings(file, env); + + const checks: DoctorCheck[] = [ + await checkNode(deps), + await checkGit(deps, run), + await checkRepo(deps, run), + ]; + + const branch = await checkBranch(deps, run); + if (branch) checks.push(branch); + + const upstream = await checkUpstream(deps, run); + if (upstream) checks.push(upstream); + + checks.push(checkApiKey(configPath, file, env)); + checks.push(checkConfigPath(configPath)); + checks.push(await checkGh(run, deps.cwd)); + + const ghAuth = await checkGhAuth(run, deps.cwd); + if (ghAuth) checks.push(ghAuth); + + const or = await checkOpenRouter(apiKey, fetchFn); + if (or) checks.push(or); + + return summarizeDoctorChecks(checks); +} \ No newline at end of file diff --git a/scripts/cli.ts b/scripts/cli.ts index 2082cdb..8e10001 100644 --- a/scripts/cli.ts +++ b/scripts/cli.ts @@ -18,6 +18,8 @@ * checkout ck git checkout (sw alias) * remote r init -> remote add origin -> first push * restore [file] rs [file] git restore . (or one file) — destructive + * pull [--merge] pl [--merge] git pull --rebase (default) or merge pull + * doctor dr check git, gh, API key, repo, upstream * model [slug] mo [slug] show or switch the AI model * setup / onboard setup OpenRouter onboard (hidden key + model + lang) * config […|reset] config show/set config · reset = re-onboard @@ -29,6 +31,7 @@ * user-only config file (0600). It stays local — only sent to OpenRouter. * * Flags: -m / --message "msg" · -y / --yes (skip restore confirm; PR uses default base) + * --merge (pull uses merge instead of rebase) · --rebase (pull default, explicit) * Also: --version / -v / -V (same as version) * PR needs GitHub CLI (`gh`) authenticated (`gh auth login`). */ @@ -72,6 +75,7 @@ import { parseNpmLatestVersion, } from "../lib/update-check"; import { sanitizeBranchName } from "../lib/branch-name"; +import { runDoctorChecks } from "../lib/doctor"; import { APP_NAME, CLI_NAME, getPackageName, getVersion } from "../lib/version"; import { c, header, row, sym, visibleLength } from "../lib/ui"; @@ -330,6 +334,16 @@ async function hasUpstream(): Promise { return Boolean(up); } +async function isInsideGitRepo(): Promise { + return (await gitQuiet(["rev-parse", "--is-inside-work-tree"])).trim() === "true"; +} + +async function ensureGitRepo(): Promise { + if (!(await isInsideGitRepo())) { + die("not inside a git repository — cd into a project or run git init"); + } +} + function isPrToken(t: string | undefined): boolean { const v = (t || "").toLowerCase(); return v === "pr" || v === "pull" || v === "pull-request"; @@ -736,11 +750,14 @@ async function runSteps(steps: () => Promise): Promise { const argv = process.argv.slice(2); let messageFlag: string | undefined; let yes = false; +let pullMerge = false; const positional: string[] = []; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a === "-m" || a === "--message") messageFlag = argv[++i]; else if (a === "-y" || a === "--yes") yes = true; + else if (a === "--merge") pullMerge = true; + else if (a === "--rebase") pullMerge = false; else positional.push(a); } @@ -758,6 +775,9 @@ const SHORT_CMDS: Record = { sw: "switch", r: "remote", rs: "restore", + pl: "pull", + dr: "doctor", + doctor: "doctor", mo: "model", v: "version", h: "help", @@ -978,6 +998,36 @@ async function fetchLatestFromNpm(packageName: string): Promise { return parseNpmLatestVersion(json); } +async function cmdDoctor(): Promise { + banner("doctor"); + const summary = await runDoctorChecks({ cwd, env: process.env }); + for (const check of summary.checks) { + const mark = + check.status === "ok" ? sym.ok : check.status === "warn" ? sym.warn : sym.fail; + const tone = + check.status === "ok" + ? c.green(check.label) + : check.status === "warn" + ? c.yellow(check.label) + : c.red(check.label); + log(` ${mark} ${tone} ${c.dim(check.detail)}`); + if (check.tip) log(` ${c.dim(check.tip)}`); + } + log(""); + if (summary.fail === 0 && summary.warn === 0) { + log(` ${sym.ok} ${c.green("all checks passed")}`); + } else if (summary.fail === 0) { + log( + ` ${sym.ok} ${c.green("ready")} ${c.dim(`(${summary.warn} warning${summary.warn === 1 ? "" : "s"})`)}` + ); + } else { + log( + ` ${sym.fail} ${c.red(`${summary.fail} check${summary.fail === 1 ? "" : "s"} failed`)} ${c.dim(`· ${summary.ok} ok · ${summary.warn} warn`)}` + ); + process.exitCode = summary.exitCode; + } +} + async function cmdUpdate(): Promise { const packageName = getPackageName(); const current = getVersion(); @@ -1055,6 +1105,8 @@ function helpText(): string { ["checkout ", "ck ", "git checkout "], ["remote [-m]", "r [-m]", "git init → remote add origin → first push"], ["restore [file] [-y]", "rs [file] [-y]", "discard changes (all, or one file)"], + ["pull [--merge]", "pl [--merge]", "pull upstream (rebase default; --merge to merge)"], + ["doctor", "dr", "check git, gh, API key, repo, upstream"], ["model [slug]", "mo [slug]", "show or switch the AI model"], ["setup / onboard", "setup", "OpenRouter onboard (hidden key + model)"], ["config [show|set|path|reset]", "config", "show/set config · reset = re-onboard"], @@ -1125,6 +1177,25 @@ async function main() { await cmdUpdate(); return; + case "doctor": + await cmdDoctor(); + return; + + case "pull": { + await ensureGitRepo(); + const mode = pullMerge ? "merge" : "rebase"; + banner(`pull (${mode})`); + if (!(await hasUpstream())) { + die( + "no upstream configured — push first with upstream, e.g. gg cnp or gg b on a new branch" + ); + } + await runSteps(async () => { + await git(pullMerge ? ["pull"] : ["pull", "--rebase"], { progress: true }); + }); + return; + } + case "start": { await openApp(); return; diff --git a/test/doctor.test.ts b/test/doctor.test.ts new file mode 100644 index 0000000..2ce4f3d --- /dev/null +++ b/test/doctor.test.ts @@ -0,0 +1,110 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { + parseNodeMajor, + runDoctorChecks, + summarizeDoctorChecks, + type DoctorCheck, +} from "../lib/doctor"; + +describe("parseNodeMajor", () => { + it("parses v22.3.0", () => { + assert.equal(parseNodeMajor("v22.3.0"), 22); + }); + + it("parses bare major", () => { + assert.equal(parseNodeMajor("18.0.0"), 18); + }); + + it("returns 0 for garbage", () => { + assert.equal(parseNodeMajor(""), 0); + }); +}); + +describe("summarizeDoctorChecks", () => { + it("exit 0 when only ok and warn", () => { + const checks: DoctorCheck[] = [ + { id: "a", label: "A", status: "ok", detail: "fine" }, + { id: "b", label: "B", status: "warn", detail: "meh" }, + ]; + const s = summarizeDoctorChecks(checks); + assert.equal(s.ok, 1); + assert.equal(s.warn, 1); + assert.equal(s.fail, 0); + assert.equal(s.exitCode, 0); + }); + + it("exit 1 when any fail", () => { + const checks: DoctorCheck[] = [ + { id: "a", label: "A", status: "fail", detail: "bad" }, + ]; + assert.equal(summarizeDoctorChecks(checks).exitCode, 1); + }); +}); + +describe("runDoctorChecks", () => { + it("reports missing git and repo with injected runner", async () => { + const summary = await runDoctorChecks({ + cwd: "/tmp/proj", + nodeVersion: "v20.0.0", + configPath: "/tmp/cfg.json", + loadConfigFn: () => ({}), + runCmd: async (cmd, args) => { + if (cmd === "git" && args[0] === "--version") { + return { stdout: "git version 2.43.0", stderr: "", code: 0 }; + } + if (cmd === "git" && args[0] === "rev-parse" && args[1] === "--is-inside-work-tree") { + return { stdout: "false\n", stderr: "", code: 0 }; + } + if (cmd === "gh") { + return { stdout: "", stderr: "not found", code: 1 }; + } + return { stdout: "", stderr: "", code: 1 }; + }, + fetchFn: async () => new Response("{}", { status: 200 }), + }); + + const repo = summary.checks.find((c) => c.id === "repo"); + assert.equal(repo?.status, "fail"); + const gh = summary.checks.find((c) => c.id === "gh"); + assert.equal(gh?.status, "warn"); + const node = summary.checks.find((c) => c.id === "node"); + assert.equal(node?.status, "ok"); + }); + + it("validates OpenRouter key via fetch", async () => { + const summary = await runDoctorChecks({ + cwd: "/tmp/proj", + nodeVersion: "v20.0.0", + env: { OPENROUTER_API_KEY: "sk-or-v1-test-key-abcdefghij" }, + configPath: "/tmp/cfg.json", + loadConfigFn: () => ({}), + runCmd: async (cmd, args) => { + if (cmd === "git" && args[0] === "--version") { + return { stdout: "git version 2.43.0", stderr: "", code: 0 }; + } + if (cmd === "git" && args[0] === "rev-parse" && args[1] === "--is-inside-work-tree") { + return { stdout: "true\n", stderr: "", code: 0 }; + } + if (cmd === "git" && args[0] === "branch") { + return { stdout: "feature/x\n", stderr: "", code: 0 }; + } + if (cmd === "git" && args[0] === "rev-parse" && args[1] === "--abbrev-ref") { + return { stdout: "origin/feature/x\n", stderr: "", code: 0 }; + } + if (cmd === "gh" && args[0] === "--version") { + return { stdout: "gh version 2.40.0", stderr: "", code: 0 }; + } + if (cmd === "gh" && args[0] === "auth") { + return { stdout: "", stderr: "Logged in to github.com", code: 0 }; + } + return { stdout: "", stderr: "", code: 1 }; + }, + fetchFn: async () => new Response("{}", { status: 401 }), + }); + + const or = summary.checks.find((c) => c.id === "openrouter"); + assert.equal(or?.status, "fail"); + assert.match(or?.detail || "", /401/); + }); +}); \ No newline at end of file From 400a82a47c3eb6bfe3a338282d40e9413d638c09 Mon Sep 17 00:00:00 2001 From: Jubarte Date: Sat, 11 Jul 2026 10:17:06 -0300 Subject: [PATCH 2/2] fix: skip upstream check in detached HEAD --- lib/doctor.ts | 35 ++++++++++++++++++++++------------- test/doctor.test.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/lib/doctor.ts b/lib/doctor.ts index 86c3635..9d6843a 100644 --- a/lib/doctor.ts +++ b/lib/doctor.ts @@ -3,6 +3,7 @@ * Framework-agnostic — safe to unit-test with injected command runners. */ import { execFile } from "node:child_process"; +import { existsSync } from "node:fs"; import { promisify } from "node:util"; import { type CommitLanguage, @@ -156,6 +157,9 @@ async function checkUpstream(deps: DoctorDeps, run: DoctorDeps["runCmd"]): Promi const repo = await run!("git", ["rev-parse", "--is-inside-work-tree"], deps.cwd); if (repo.code !== 0 || repo.stdout.trim() !== "true") return null; + const branch = await run!("git", ["branch", "--show-current"], deps.cwd); + if (!branch.stdout.trim()) return null; // detached HEAD — checkBranch already warns + const up = await run!("git", ["rev-parse", "--abbrev-ref", "@{upstream}"], deps.cwd); const name = up.stdout.trim(); if (name) { @@ -170,11 +174,7 @@ async function checkUpstream(deps: DoctorDeps, run: DoctorDeps["runCmd"]): Promi }; } -function checkApiKey( - configPath: string, - file: GitgenConfig, - env: EnvLike -): DoctorCheck { +function checkApiKey(file: GitgenConfig, env: EnvLike): DoctorCheck { const { apiKey, model, language } = resolveRuntimeSettings(file, env); if (!apiKey) { return { @@ -204,11 +204,15 @@ function checkApiKey( } function checkConfigPath(configPath: string): DoctorCheck { + if (existsSync(configPath)) { + return { id: "config", label: "Config", status: "ok", detail: configPath }; + } return { id: "config", label: "Config", - status: "ok", - detail: configPath, + status: "warn", + detail: `${configPath} — not found, using defaults`, + tip: "run gg setup", }; } @@ -227,9 +231,12 @@ async function checkGh(run: DoctorDeps["runCmd"], cwd: string): Promise { - const ver = await run!("gh", ["--version"], cwd); - if (ver.code !== 0) return null; +async function checkGhAuth( + run: DoctorDeps["runCmd"], + cwd: string, + ghAvailable: boolean +): Promise { + if (!ghAvailable) return null; const auth = await run!("gh", ["auth", "status"], cwd); if (auth.code === 0) { @@ -311,11 +318,13 @@ export async function runDoctorChecks(deps: DoctorDeps): Promise const upstream = await checkUpstream(deps, run); if (upstream) checks.push(upstream); - checks.push(checkApiKey(configPath, file, env)); + checks.push(checkApiKey(file, env)); checks.push(checkConfigPath(configPath)); - checks.push(await checkGh(run, deps.cwd)); - const ghAuth = await checkGhAuth(run, deps.cwd); + const gh = await checkGh(run, deps.cwd); + checks.push(gh); + + const ghAuth = await checkGhAuth(run, deps.cwd, gh.status === "ok"); if (ghAuth) checks.push(ghAuth); const or = await checkOpenRouter(apiKey, fetchFn); diff --git a/test/doctor.test.ts b/test/doctor.test.ts index 2ce4f3d..1b3bf7b 100644 --- a/test/doctor.test.ts +++ b/test/doctor.test.ts @@ -72,6 +72,32 @@ describe("runDoctorChecks", () => { assert.equal(node?.status, "ok"); }); + it("skips upstream check in detached HEAD", async () => { + const summary = await runDoctorChecks({ + cwd: "/tmp/proj", + nodeVersion: "v20.0.0", + configPath: "/tmp/cfg.json", + loadConfigFn: () => ({}), + runCmd: async (cmd, args) => { + if (cmd === "git" && args[0] === "--version") { + return { stdout: "git version 2.43.0", stderr: "", code: 0 }; + } + if (cmd === "git" && args[0] === "rev-parse" && args[1] === "--is-inside-work-tree") { + return { stdout: "true\n", stderr: "", code: 0 }; + } + if (cmd === "git" && args[0] === "branch") { + return { stdout: "", stderr: "", code: 0 }; + } + return { stdout: "", stderr: "", code: 1 }; + }, + fetchFn: async () => new Response("{}", { status: 200 }), + }); + + const branch = summary.checks.find((c) => c.id === "branch"); + assert.equal(branch?.status, "warn"); + assert.equal(summary.checks.find((c) => c.id === "upstream"), undefined); + }); + it("validates OpenRouter key via fetch", async () => { const summary = await runDoctorChecks({ cwd: "/tmp/proj",