Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 31 additions & 8 deletions packages/gittensory-miner/lib/laptop-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down
85 changes: 85 additions & 0 deletions test/unit/miner-cli-doctor-checks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});