fix(doctor): ignore rotating Claude CLI OAuth expiry (#108205)

This commit is contained in:
Peter Steinberger
2026-07-15 03:15:21 -07:00
committed by GitHub
parent f7718d9a91
commit c6adbd233b
2 changed files with 110 additions and 0 deletions
@@ -31,6 +31,9 @@ vi.mock("../agents/auth-profiles.js", () => ({
}));
vi.mock("../../packages/terminal-core/src/note.js", () => ({ note: vi.fn() }));
vi.mock("../agents/auth-profiles/doctor.js", () => ({
formatAuthDoctorHint: vi.fn(async () => "Re-authenticate this profile."),
}));
import { note } from "../../packages/terminal-core/src/note.js";
import { collectAuthProfileHealthFindings, noteAuthProfileHealth } from "./doctor-auth.js";
@@ -110,6 +113,101 @@ describe("noteAuthProfileHealth", () => {
]);
});
it("does not warn while Claude CLI owns refresh of an expiring access token", async () => {
const now = 1_700_000_000_000;
vi.spyOn(Date, "now").mockReturnValue(now);
const mainDir = path.join(tempDir, "main-agent");
authProfileMocks.hasAnyAuthProfileStoreSource.mockReturnValue(true);
authProfileMocks.ensureAuthProfileStore.mockReturnValue({
version: 1,
profiles: {
"anthropic:claude-cli": {
type: "oauth",
provider: "claude-cli",
access: "access",
refresh: "refresh",
expires: now + 3 * 60 * 60_000,
},
},
});
const findings = await collectAuthProfileHealthFindings({
cfg: {
agents: { list: [{ id: "main", default: true, agentDir: mainDir }] },
} as OpenClawConfig,
});
expect(findings).toEqual([]);
});
it("keeps expiring warnings for static and custom Claude CLI profiles", async () => {
const now = 1_700_000_000_000;
vi.spyOn(Date, "now").mockReturnValue(now);
const mainDir = path.join(tempDir, "main-agent");
authProfileMocks.hasAnyAuthProfileStoreSource.mockReturnValue(true);
authProfileMocks.ensureAuthProfileStore.mockReturnValue({
version: 1,
profiles: {
"anthropic:claude-cli": {
type: "token",
provider: "claude-cli",
token: "token",
expires: now + 3 * 60 * 60_000,
},
"anthropic:custom-cli": {
type: "oauth",
provider: "claude-cli",
access: "access",
refresh: "refresh",
expires: now + 3 * 60 * 60_000,
},
},
});
const findings = await collectAuthProfileHealthFindings({
cfg: {
agents: { list: [{ id: "main", default: true, agentDir: mainDir }] },
} as OpenClawConfig,
});
expect(findings.map((finding) => finding.target)).toEqual([
"anthropic:claude-cli",
"anthropic:custom-cli",
]);
});
it("still warns once a Claude CLI access token is expired", async () => {
const now = 1_700_000_000_000;
vi.spyOn(Date, "now").mockReturnValue(now);
const mainDir = path.join(tempDir, "main-agent");
authProfileMocks.hasAnyAuthProfileStoreSource.mockReturnValue(true);
authProfileMocks.ensureAuthProfileStore.mockReturnValue({
version: 1,
profiles: {
"anthropic:claude-cli": {
type: "oauth",
provider: "claude-cli",
access: "access",
refresh: "refresh",
expires: now - 60_000,
},
},
});
const findings = await collectAuthProfileHealthFindings({
cfg: {
agents: { list: [{ id: "main", default: true, agentDir: mainDir }] },
} as OpenClawConfig,
});
expect(findings).toEqual([
expect.objectContaining({
message: "Auth profile anthropic:claude-cli is expired (0m).",
target: "anthropic:claude-cli",
}),
]);
});
it("maps disabled auth profiles to structured findings", async () => {
const now = 1_700_000_000_000;
vi.spyOn(Date, "now").mockReturnValue(now);
+12
View File
@@ -21,6 +21,7 @@ import {
resolveApiKeyForProfile,
resolveProfileUnusableUntilForDisplay,
} from "../agents/auth-profiles.js";
import { CLAUDE_CLI_PROFILE_ID } from "../agents/auth-profiles/constants.js";
import { formatAuthDoctorHint } from "../agents/auth-profiles/doctor.js";
import {
buildOAuthRefreshFailureLoginCommand,
@@ -38,6 +39,7 @@ import type { DoctorPrompter } from "./doctor-prompter.js";
const OPENAI_PROVIDER_ID = "openai";
const LEGACY_CODEX_PROVIDER_ID = "openai-codex";
const CLAUDE_CLI_PROVIDER_ID = "claude-cli";
const CODEX_OAUTH_WARNING_TITLE = "Codex OAuth";
const OPENAI_BASE_URL = "https://api.openai.com/v1";
const LEGACY_CODEX_APIS = new Set(["openai-responses", "openai-completions"]);
@@ -342,6 +344,16 @@ function isAuthProfileHealthIssue(profile: AuthHealthSummary["profiles"][number]
if (profile.type === "api_key") {
return profile.status === "missing";
}
// Claude CLI refreshes its short-lived access token when the process runs.
// Warn once that external credential is unusable, not throughout its normal lifetime.
if (
profile.profileId === CLAUDE_CLI_PROFILE_ID &&
profile.provider === CLAUDE_CLI_PROVIDER_ID &&
profile.type === "oauth" &&
profile.status === "expiring"
) {
return false;
}
return (
(profile.type === "oauth" || profile.type === "token") &&
(profile.status === "expired" || profile.status === "expiring" || profile.status === "missing")