mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:30:44 +00:00
fix(status): align oauth health with runtime
This commit is contained in:
@@ -1,4 +1,15 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { readCodexCliCredentialsCachedMock } = vi.hoisted(() => ({
|
||||
readCodexCliCredentialsCachedMock: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
vi.mock("./cli-credentials.js", () => ({
|
||||
readCodexCliCredentialsCached: readCodexCliCredentialsCachedMock,
|
||||
readMiniMaxCliCredentialsCached: () => null,
|
||||
resetCliCredentialCachesForTest: () => undefined,
|
||||
}));
|
||||
|
||||
import {
|
||||
buildAuthHealthSummary,
|
||||
DEFAULT_OAUTH_WARN_MS,
|
||||
@@ -16,6 +27,11 @@ describe("buildAuthHealthSummary", () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
readCodexCliCredentialsCachedMock.mockReset();
|
||||
readCodexCliCredentialsCachedMock.mockReturnValue(null);
|
||||
});
|
||||
|
||||
it("classifies OAuth and API key profiles", () => {
|
||||
vi.spyOn(Date, "now").mockReturnValue(now);
|
||||
const store = {
|
||||
@@ -58,13 +74,12 @@ describe("buildAuthHealthSummary", () => {
|
||||
const statuses = profileStatuses(summary);
|
||||
|
||||
expect(statuses["anthropic:ok"]).toBe("ok");
|
||||
// OAuth credentials with refresh tokens are auto-renewable, so they report "ok"
|
||||
expect(statuses["anthropic:expiring"]).toBe("ok");
|
||||
expect(statuses["anthropic:expired"]).toBe("ok");
|
||||
expect(statuses["anthropic:expiring"]).toBe("expiring");
|
||||
expect(statuses["anthropic:expired"]).toBe("expired");
|
||||
expect(statuses["anthropic:api"]).toBe("static");
|
||||
|
||||
const provider = summary.providers.find((entry) => entry.provider === "anthropic");
|
||||
expect(provider?.status).toBe("ok");
|
||||
expect(provider?.status).toBe("expired");
|
||||
});
|
||||
|
||||
it("reports expired for OAuth without a refresh token", () => {
|
||||
@@ -92,6 +107,38 @@ describe("buildAuthHealthSummary", () => {
|
||||
expect(statuses["google:no-refresh"]).toBe("expired");
|
||||
});
|
||||
|
||||
it("prefers fresher imported external OAuth credentials when reporting health", () => {
|
||||
vi.spyOn(Date, "now").mockReturnValue(now);
|
||||
readCodexCliCredentialsCachedMock.mockReturnValue({
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "fresh-cli-access",
|
||||
refresh: "fresh-cli-refresh",
|
||||
expires: now + DEFAULT_OAUTH_WARN_MS + 60_000,
|
||||
accountId: "acct-cli",
|
||||
});
|
||||
const store = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai-codex:default": {
|
||||
type: "oauth" as const,
|
||||
provider: "openai-codex",
|
||||
access: "expired-access",
|
||||
refresh: "expired-refresh",
|
||||
expires: now - 10_000,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const summary = buildAuthHealthSummary({
|
||||
store,
|
||||
warnAfterMs: DEFAULT_OAUTH_WARN_MS,
|
||||
});
|
||||
|
||||
const statuses = profileStatuses(summary);
|
||||
expect(statuses["openai-codex:default"]).toBe("ok");
|
||||
});
|
||||
|
||||
it("marks token profiles with invalid expires as missing with reason code", () => {
|
||||
vi.spyOn(Date, "now").mockReturnValue(now);
|
||||
const store = {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
resolveTokenExpiryState,
|
||||
} from "./auth-profiles/credential-state.js";
|
||||
import { resolveAuthProfileDisplayLabel } from "./auth-profiles/display.js";
|
||||
import { resolveEffectiveOAuthCredential } from "./auth-profiles/effective-oauth.js";
|
||||
import type { AuthProfileCredential, AuthProfileStore } from "./auth-profiles/types.js";
|
||||
import { normalizeProviderId } from "./provider-id.js";
|
||||
|
||||
@@ -161,22 +162,21 @@ function buildProfileHealth(params: {
|
||||
};
|
||||
}
|
||||
|
||||
const hasRefreshToken = typeof credential.refresh === "string" && credential.refresh.length > 0;
|
||||
const effectiveCredential = resolveEffectiveOAuthCredential({
|
||||
profileId,
|
||||
credential,
|
||||
});
|
||||
const { status: rawStatus, remainingMs } = resolveOAuthStatus(
|
||||
credential.expires,
|
||||
effectiveCredential.expires,
|
||||
now,
|
||||
warnAfterMs,
|
||||
);
|
||||
// OAuth credentials with a valid refresh token auto-renew on first API call,
|
||||
// so don't warn about access token expiration.
|
||||
const status =
|
||||
hasRefreshToken && (rawStatus === "expired" || rawStatus === "expiring") ? "ok" : rawStatus;
|
||||
return {
|
||||
profileId,
|
||||
provider,
|
||||
type: "oauth",
|
||||
status,
|
||||
expiresAt: credential.expires,
|
||||
status: rawStatus,
|
||||
expiresAt: effectiveCredential.expires,
|
||||
remainingMs,
|
||||
source,
|
||||
label,
|
||||
|
||||
21
src/agents/auth-profiles/effective-oauth.ts
Normal file
21
src/agents/auth-profiles/effective-oauth.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
readManagedExternalCliCredential,
|
||||
shouldReplaceStoredOAuthCredential,
|
||||
} from "./external-cli-sync.js";
|
||||
import type { OAuthCredential } from "./types.js";
|
||||
|
||||
export function resolveEffectiveOAuthCredential(params: {
|
||||
profileId: string;
|
||||
credential: OAuthCredential;
|
||||
}): OAuthCredential {
|
||||
const imported = readManagedExternalCliCredential({
|
||||
profileId: params.profileId,
|
||||
credential: params.credential,
|
||||
});
|
||||
if (!imported) {
|
||||
return params.credential;
|
||||
}
|
||||
return shouldReplaceStoredOAuthCredential(params.credential, imported)
|
||||
? imported
|
||||
: params.credential;
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "./constants.js";
|
||||
import { resolveTokenExpiryState } from "./credential-state.js";
|
||||
import { formatAuthDoctorHint } from "./doctor.js";
|
||||
import { resolveEffectiveOAuthCredential } from "./effective-oauth.js";
|
||||
import {
|
||||
areOAuthCredentialsEquivalent,
|
||||
readManagedExternalCliCredential,
|
||||
@@ -753,11 +754,16 @@ async function tryResolveOAuthProfile(
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Date.now() < cred.expires) {
|
||||
const effectiveCred = resolveEffectiveOAuthCredential({
|
||||
profileId,
|
||||
credential: cred,
|
||||
});
|
||||
|
||||
if (Date.now() < effectiveCred.expires) {
|
||||
return await buildOAuthProfileResult({
|
||||
provider: cred.provider,
|
||||
credentials: cred,
|
||||
email: cred.email,
|
||||
provider: effectiveCred.provider,
|
||||
credentials: effectiveCred,
|
||||
email: effectiveCred.email ?? cred.email,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -904,12 +910,16 @@ export async function resolveApiKeyForProfile(
|
||||
agentDir: params.agentDir,
|
||||
cred,
|
||||
}) ?? cred;
|
||||
const effectiveOAuthCred = resolveEffectiveOAuthCredential({
|
||||
profileId,
|
||||
credential: oauthCred,
|
||||
});
|
||||
|
||||
if (Date.now() < oauthCred.expires) {
|
||||
if (Date.now() < effectiveOAuthCred.expires) {
|
||||
return await buildOAuthProfileResult({
|
||||
provider: oauthCred.provider,
|
||||
credentials: oauthCred,
|
||||
email: oauthCred.email,
|
||||
provider: effectiveOAuthCred.provider,
|
||||
credentials: effectiveOAuthCred,
|
||||
email: effectiveOAuthCred.email,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user