diff --git a/packages/gittensory-miner/lib/laptop-init.js b/packages/gittensory-miner/lib/laptop-init.js index 6cd4c7c228..3aa7299499 100644 --- a/packages/gittensory-miner/lib/laptop-init.js +++ b/packages/gittensory-miner/lib/laptop-init.js @@ -103,15 +103,30 @@ function resolveCodexAuthPath(env = process.env) { return join(base, "auth.json"); } -/** Informational only — a coding-agent CLI is only needed once a driver provider is configured (#4289), so a - * missing or unauthenticated CLI is advisory (`ok: true`), mirroring checkDockerPresent's optional tone. The - * auth probe is read-only and never spawns the CLI: it surfaces, proactively, the SAME condition claude - * checks at call time — `CLAUDE_CODE_OAUTH_TOKEN` present (see createClaudeCodeAi, src/selfhost/ai.ts). */ +/** A coding-agent CLI is only needed once a driver provider is configured (#4289) — gated by + * `MINER_CODING_AGENT_PROVIDER` (#5165). When that provider is NOT the CLI being checked, absence is + * advisory (`ok: true`), mirroring checkDockerPresent's optional tone. When it IS configured and the CLI is + * missing, `ok: false` — every attempt will fail without it. The auth probe (once found) stays advisory + * either way, since an unauthenticated-but-installed CLI is a separate, already-visible warning. */ +function codingAgentProviderConfiguredFor(env, providerName) { + return env.MINER_CODING_AGENT_PROVIDER === providerName; +} + +/** Informational unless `MINER_CODING_AGENT_PROVIDER=claude-cli` (#5165), in which case a missing CLI fails + * doctor. The auth probe is read-only and never spawns the CLI: it surfaces, proactively, the SAME condition + * claude checks at call time — `CLAUDE_CODE_OAUTH_TOKEN` present (see createClaudeCodeAi, src/selfhost/ai.ts). */ export function checkClaudeCliPresent(options = {}) { const env = options.env ?? process.env; const claudePath = (options.resolveClaudePath ?? (() => findExecutableOnPath("claude", env)))(); if (!claudePath) { - return { name: "claude-cli-present", ok: true, detail: "not installed (optional until a coding-agent driver is configured)" }; + const configured = codingAgentProviderConfiguredFor(env, "claude-cli"); + return { + name: "claude-cli-present", + ok: !configured, + detail: configured + ? "not installed — MINER_CODING_AGENT_PROVIDER is set to claude-cli, every attempt will fail without it" + : "not installed (optional until a coding-agent driver is configured)", + }; } const authed = typeof env.CLAUDE_CODE_OAUTH_TOKEN === "string" && env.CLAUDE_CODE_OAUTH_TOKEN.length > 0; return { @@ -121,13 +136,21 @@ export function checkClaudeCliPresent(options = {}) { }; } -/** Informational only — mirrors {@link checkClaudeCliPresent} for the codex CLI. The auth probe checks the - * same read-only condition assertCodexAuthConfigured uses at call time: codex's `auth.json` is readable. */ +/** Informational unless `MINER_CODING_AGENT_PROVIDER=codex-cli` (#5165), in which case a missing CLI fails + * doctor — mirrors {@link checkClaudeCliPresent}. The auth probe checks the same read-only condition + * assertCodexAuthConfigured uses at call time: codex's `auth.json` is readable. */ export function checkCodexCliPresent(options = {}) { const env = options.env ?? process.env; const codexPath = (options.resolveCodexPath ?? (() => findExecutableOnPath("codex", env)))(); if (!codexPath) { - return { name: "codex-cli-present", ok: true, detail: "not installed (optional until a coding-agent driver is configured)" }; + const configured = codingAgentProviderConfiguredFor(env, "codex-cli"); + return { + name: "codex-cli-present", + ok: !configured, + detail: configured + ? "not installed — MINER_CODING_AGENT_PROVIDER is set to codex-cli, every attempt will fail without it" + : "not installed (optional until a coding-agent driver is configured)", + }; } const authPath = (options.resolveCodexAuthPath ?? (() => resolveCodexAuthPath(env)))(); let authed = false; diff --git a/test/unit/miner-cli-doctor-checks.test.ts b/test/unit/miner-cli-doctor-checks.test.ts index 74d7e2a487..848cac7624 100644 --- a/test/unit/miner-cli-doctor-checks.test.ts +++ b/test/unit/miner-cli-doctor-checks.test.ts @@ -58,4 +58,89 @@ describe("gittensory-miner doctor — coding-agent CLI checks (#4304)", () => { expect(names).toContain("claude-cli-present"); expect(names).toContain("codex-cli-present"); }); + + describe("provider-gated CLI-presence failures (#5165)", () => { + it("claude: regression -- CLI missing while unconfigured no longer breaks doctor (ok stays true)", () => { + const check = checkClaudeCliPresent({ env: {}, resolveClaudePath: () => null }); + expect(check.ok).toBe(true); + }); + + it("claude: CLI missing + a DIFFERENT provider configured stays advisory (ok true)", () => { + const check = checkClaudeCliPresent({ + env: { MINER_CODING_AGENT_PROVIDER: "codex-cli" }, + resolveClaudePath: () => null, + }); + expect(check.ok).toBe(true); + expect(check.detail).toMatch(/^not installed \(optional/); + }); + + it("claude: CLI missing + claude-cli configured fails doctor with an actionable message", () => { + const check = checkClaudeCliPresent({ + env: { MINER_CODING_AGENT_PROVIDER: "claude-cli" }, + resolveClaudePath: () => null, + }); + expect(check.ok).toBe(false); + expect(check.detail).toBe( + "not installed — MINER_CODING_AGENT_PROVIDER is set to claude-cli, every attempt will fail without it", + ); + }); + + it("claude: CLI present + claude-cli configured still reports the normal present/authenticated detail", () => { + const check = checkClaudeCliPresent({ + env: { MINER_CODING_AGENT_PROVIDER: "claude-cli", CLAUDE_CODE_OAUTH_TOKEN: "present" }, + resolveClaudePath: () => "/usr/bin/claude", + }); + expect(check.ok).toBe(true); + expect(check.detail).toBe("found at /usr/bin/claude (authenticated)"); + }); + + it("codex: regression -- CLI missing while unconfigured no longer breaks doctor (ok stays true)", () => { + const check = checkCodexCliPresent({ env: {}, resolveCodexPath: () => null }); + expect(check.ok).toBe(true); + }); + + it("codex: CLI missing + a DIFFERENT provider configured stays advisory (ok true)", () => { + const check = checkCodexCliPresent({ + env: { MINER_CODING_AGENT_PROVIDER: "claude-cli" }, + resolveCodexPath: () => null, + }); + expect(check.ok).toBe(true); + expect(check.detail).toMatch(/^not installed \(optional/); + }); + + it("codex: CLI missing + codex-cli configured fails doctor with an actionable message", () => { + const check = checkCodexCliPresent({ + env: { MINER_CODING_AGENT_PROVIDER: "codex-cli" }, + resolveCodexPath: () => null, + }); + expect(check.ok).toBe(false); + expect(check.detail).toBe( + "not installed — MINER_CODING_AGENT_PROVIDER is set to codex-cli, every attempt will fail without it", + ); + }); + + it("codex: CLI present + codex-cli configured still reports the normal present/authenticated detail", () => { + const authFile = join(tempRoot(), "auth.json"); + writeFileSync(authFile, "{}"); + const check = checkCodexCliPresent({ + env: { MINER_CODING_AGENT_PROVIDER: "codex-cli" }, + resolveCodexPath: () => "/usr/bin/codex", + resolveCodexAuthPath: () => authFile, + }); + expect(check.ok).toBe(true); + expect(check.detail).toBe("found at /usr/bin/codex (authenticated)"); + }); + + it("invariant: an unconfigured (or differently-configured) provider's CLI check is never reported as ok: false regardless of CLI presence", () => { + const missingUnconfigured = checkClaudeCliPresent({ env: {}, resolveClaudePath: () => null }); + const presentUnconfigured = checkClaudeCliPresent({ env: {}, resolveClaudePath: () => "/usr/bin/claude" }); + const missingOtherProvider = checkCodexCliPresent({ + env: { MINER_CODING_AGENT_PROVIDER: "claude-cli" }, + resolveCodexPath: () => null, + }); + expect(missingUnconfigured.ok).toBe(true); + expect(presentUnconfigured.ok).toBe(true); + expect(missingOtherProvider.ok).toBe(true); + }); + }); });