mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-01 12:23:35 +00:00
Doctor: expose auth profile findings (#97125)
Expose `core/doctor/auth-profiles` as a default-disabled structured Doctor lint check. This maps the existing auth-profile health diagnostics into structured findings while preserving legacy Doctor repair behavior. The check remains opt-in through `--only core/doctor/auth-profiles` or `--all`; default `doctor --lint` behavior is unchanged. Validation: - focused auth-profile/hints/contribution Vitest passed - changed-file oxfmt and oxlint passed - core tsgo passed - git diff --check passed - exact-head hosted CI/Testbox gates passed Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import {
|
||||
collectAuthProfileHealthFindings,
|
||||
formatOAuthRefreshFailureDoctorLine,
|
||||
legacyCodexProviderOverrideToHealthFinding,
|
||||
noteLegacyCodexProviderOverride,
|
||||
resolveUnusableProfileHint,
|
||||
} from "./doctor-auth.js";
|
||||
@@ -127,6 +129,52 @@ describe("resolveUnusableProfileHint", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("maps legacy Codex overrides to structured auth profile findings", () => {
|
||||
expect(
|
||||
legacyCodexProviderOverrideToHealthFinding({
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
}),
|
||||
).toMatchObject({
|
||||
checkId: "core/doctor/auth-profiles",
|
||||
severity: "warning",
|
||||
message:
|
||||
"Legacy openai-codex transport override can shadow configured Codex OAuth credentials.",
|
||||
path: "models.providers.openai-codex",
|
||||
target: "openai-codex",
|
||||
});
|
||||
});
|
||||
|
||||
it("collects legacy Codex override structured findings", async () => {
|
||||
const findings = await collectAuthProfileHealthFindings({
|
||||
cfg: doctorFixtureConfig({
|
||||
auth: {
|
||||
profiles: {
|
||||
"openai:default": {
|
||||
provider: "openai",
|
||||
mode: "oauth",
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
"openai-codex": {
|
||||
api: "openai-responses",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(findings).toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: "core/doctor/auth-profiles",
|
||||
path: "models.providers.openai-codex",
|
||||
target: "openai-codex",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("warns when a legacy Codex override shadows stored legacy OAuth state", () => {
|
||||
mocks.ensureAuthProfileStore.mockReturnValue({
|
||||
version: 1,
|
||||
|
||||
@@ -3,21 +3,16 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { AuthProfileStore } from "../agents/auth-profiles/types.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { DoctorPrompter } from "./doctor-prompter.js";
|
||||
|
||||
type MockAuthProfileStore = {
|
||||
version: number;
|
||||
profiles: Record<
|
||||
string,
|
||||
| { type: "oauth"; provider: string; access: string; refresh: string; expires: number }
|
||||
| { type: "api_key"; provider: string; key: string }
|
||||
>;
|
||||
};
|
||||
|
||||
const authProfileMocks = vi.hoisted(() => ({
|
||||
ensureAuthProfileStore: vi.fn<
|
||||
(agentDir?: string, options?: { allowKeychainPrompt?: boolean }) => MockAuthProfileStore
|
||||
(
|
||||
agentDir?: string,
|
||||
options?: { allowKeychainPrompt?: boolean; readOnly?: boolean },
|
||||
) => AuthProfileStore
|
||||
>(() => {
|
||||
throw new Error("unexpected auth profile load");
|
||||
}),
|
||||
@@ -38,7 +33,7 @@ vi.mock("../agents/auth-profiles.js", () => ({
|
||||
vi.mock("../../packages/terminal-core/src/note.js", () => ({ note: vi.fn() }));
|
||||
|
||||
import { note } from "../../packages/terminal-core/src/note.js";
|
||||
import { noteAuthProfileHealth } from "./doctor-auth.js";
|
||||
import { collectAuthProfileHealthFindings, noteAuthProfileHealth } from "./doctor-auth.js";
|
||||
|
||||
const noteMock = vi.mocked(note);
|
||||
|
||||
@@ -67,6 +62,10 @@ describe("noteAuthProfileHealth", () => {
|
||||
fs.writeFileSync(path.join(agentDir, "auth-profiles.json"), "{}\n", "utf8");
|
||||
}
|
||||
|
||||
function expectedAuthStorePath(agentDir: string): string {
|
||||
return path.join(agentDir, "openclaw-agent.sqlite");
|
||||
}
|
||||
|
||||
function expiredStore(profileId: string, expires: number) {
|
||||
return {
|
||||
version: 1,
|
||||
@@ -79,8 +78,142 @@ describe("noteAuthProfileHealth", () => {
|
||||
expires,
|
||||
},
|
||||
},
|
||||
};
|
||||
} satisfies AuthProfileStore;
|
||||
}
|
||||
|
||||
it("maps expired stored auth profiles to structured findings without refreshing", 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(
|
||||
expiredStore("openai:default", now - 60_000),
|
||||
);
|
||||
|
||||
const findings = await collectAuthProfileHealthFindings({
|
||||
cfg: {
|
||||
agents: {
|
||||
list: [{ id: "main", default: true, agentDir: mainDir }],
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
});
|
||||
|
||||
expect(authProfileMocks.resolveApiKeyForProfile).not.toHaveBeenCalled();
|
||||
expect(findings).toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: "core/doctor/auth-profiles",
|
||||
severity: "warning",
|
||||
message: "Auth profile openai:default is expired (0m).",
|
||||
path: expectedAuthStorePath(mainDir),
|
||||
target: "openai:default",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("maps disabled auth profiles to structured findings", 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.resolveProfileUnusableUntilForDisplay.mockReturnValue(now + 5 * 60_000);
|
||||
authProfileMocks.ensureAuthProfileStore.mockReturnValue({
|
||||
version: 1,
|
||||
profiles: {},
|
||||
usageStats: {
|
||||
"openai:billing": {
|
||||
disabledUntil: now + 5 * 60_000,
|
||||
disabledReason: "billing",
|
||||
},
|
||||
},
|
||||
} satisfies AuthProfileStore);
|
||||
|
||||
const findings = await collectAuthProfileHealthFindings({
|
||||
cfg: {
|
||||
agents: {
|
||||
list: [{ id: "main", default: true, agentDir: mainDir }],
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
});
|
||||
|
||||
expect(findings).toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: "core/doctor/auth-profiles",
|
||||
message: "Auth profile openai:billing is disabled:billing (5m).",
|
||||
path: expectedAuthStorePath(mainDir),
|
||||
target: "openai:billing",
|
||||
fixHint: "Top up credits (provider billing) or switch provider.",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("maps malformed API-key auth profiles to structured findings", async () => {
|
||||
const mainDir = path.join(tempDir, "main-agent");
|
||||
authProfileMocks.hasAnyAuthProfileStoreSource.mockReturnValue(true);
|
||||
authProfileMocks.ensureAuthProfileStore.mockReturnValue({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"zai:default": {
|
||||
type: "api_key",
|
||||
provider: "zai",
|
||||
key: "openclaw onboard --auth-choice zai-coding-global",
|
||||
},
|
||||
},
|
||||
} satisfies AuthProfileStore);
|
||||
|
||||
const findings = await collectAuthProfileHealthFindings({
|
||||
cfg: {
|
||||
agents: {
|
||||
list: [{ id: "main", default: true, agentDir: mainDir }],
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
});
|
||||
|
||||
expect(findings).toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: "core/doctor/auth-profiles",
|
||||
severity: "warning",
|
||||
message: "Auth profile zai:default is missing [malformed_api_key].",
|
||||
path: expectedAuthStorePath(mainDir),
|
||||
target: "zai:default",
|
||||
requirement: "malformed_api_key",
|
||||
fixHint: "Paste the API key value, not an OpenClaw onboarding command.",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("labels structured auth profile findings by agent when multiple stores are checked", async () => {
|
||||
const now = 1_700_000_000_000;
|
||||
vi.spyOn(Date, "now").mockReturnValue(now);
|
||||
const mainDir = path.join(tempDir, "main-agent");
|
||||
const coderDir = path.join(tempDir, "coder-agent");
|
||||
authProfileMocks.hasAnyAuthProfileStoreSource.mockReturnValue(true);
|
||||
authProfileMocks.hasLocalAuthProfileStoreSource.mockReturnValue(true);
|
||||
authProfileMocks.ensureAuthProfileStore.mockImplementation((agentDir) => {
|
||||
if (agentDir === mainDir) {
|
||||
return expiredStore("openai:main", now - 60_000);
|
||||
}
|
||||
if (agentDir === coderDir) {
|
||||
return expiredStore("openai:coder", now - 60_000);
|
||||
}
|
||||
throw new Error(`unexpected agent dir: ${agentDir ?? "<default>"}`);
|
||||
});
|
||||
|
||||
const findings = await collectAuthProfileHealthFindings({
|
||||
cfg: {
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "main", default: true, agentDir: mainDir },
|
||||
{ id: "coder", agentDir: coderDir },
|
||||
],
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
});
|
||||
|
||||
expect(findings.map((finding) => finding.message)).toEqual([
|
||||
"Agent main auth profile openai:main is expired (0m).",
|
||||
"Agent coder auth profile openai:coder is expired (0m).",
|
||||
]);
|
||||
});
|
||||
it("skips external auth profile resolution when no auth source exists", async () => {
|
||||
await noteAuthProfileHealth({
|
||||
cfg: { channels: { telegram: { enabled: true } } } as OpenClawConfig,
|
||||
@@ -209,7 +342,7 @@ describe("noteAuthProfileHealth", () => {
|
||||
writeAuthStore(agentDir);
|
||||
authProfileMocks.hasAnyAuthProfileStoreSource.mockReturnValue(true);
|
||||
authProfileMocks.ensureAuthProfileStore.mockImplementation(
|
||||
(receivedAgentDir): MockAuthProfileStore => {
|
||||
(receivedAgentDir): AuthProfileStore => {
|
||||
if (receivedAgentDir === agentDir) {
|
||||
return {
|
||||
version: 1,
|
||||
|
||||
@@ -26,8 +26,10 @@ import {
|
||||
classifyOAuthRefreshFailure,
|
||||
type OAuthRefreshFailureReason,
|
||||
} from "../agents/auth-profiles/oauth-refresh-failure.js";
|
||||
import { resolveAuthStorePathForDisplay } from "../agents/auth-profiles/path-resolve.js";
|
||||
import { buildProviderAuthRecoveryHint } from "../agents/provider-auth-recovery-hint.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { HealthFinding } from "../flows/health-checks.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import type { DoctorPrompter } from "./doctor-prompter.js";
|
||||
@@ -37,6 +39,7 @@ const LEGACY_CODEX_PROVIDER_ID = "openai-codex";
|
||||
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"]);
|
||||
const AUTH_PROFILES_CHECK_ID = "core/doctor/auth-profiles";
|
||||
const DOCTOR_REAUTH_PROVIDER_ALIASES: Readonly<Record<string, string>> = {
|
||||
[LEGACY_CODEX_PROVIDER_ID]: OPENAI_PROVIDER_ID,
|
||||
};
|
||||
@@ -50,7 +53,7 @@ function hasConfiguredCodexOAuthProfile(cfg: OpenClawConfig): boolean {
|
||||
}
|
||||
|
||||
function hasStoredCodexOAuthProfile(): boolean {
|
||||
const store = ensureAuthProfileStore(undefined, { allowKeychainPrompt: false });
|
||||
const store = ensureAuthProfileStore(undefined, { allowKeychainPrompt: false, readOnly: true });
|
||||
return Object.values(store.profiles).some(
|
||||
(profile) =>
|
||||
(profile.provider === OPENAI_PROVIDER_ID || profile.provider === LEGACY_CODEX_PROVIDER_ID) &&
|
||||
@@ -114,6 +117,22 @@ function buildCodexProviderOverrideWarning(providerOverride: unknown): string {
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export function legacyCodexProviderOverrideToHealthFinding(
|
||||
providerOverride: unknown,
|
||||
): HealthFinding {
|
||||
const message =
|
||||
"Legacy openai-codex transport override can shadow configured Codex OAuth credentials.";
|
||||
const details = buildCodexProviderOverrideWarning(providerOverride);
|
||||
return {
|
||||
checkId: AUTH_PROFILES_CHECK_ID,
|
||||
severity: "warning",
|
||||
message,
|
||||
path: `models.providers.${LEGACY_CODEX_PROVIDER_ID}`,
|
||||
target: LEGACY_CODEX_PROVIDER_ID,
|
||||
fixHint: details,
|
||||
};
|
||||
}
|
||||
|
||||
/** Emits a warning when legacy Codex transport overrides can shadow configured Codex OAuth. */
|
||||
export function noteLegacyCodexProviderOverride(cfg: OpenClawConfig): void {
|
||||
const providerOverride = cfg.models?.providers?.[LEGACY_CODEX_PROVIDER_ID];
|
||||
@@ -263,6 +282,169 @@ async function formatAuthIssueLine(
|
||||
return `- ${issue.profileId}: ${issue.status}${reason}${remaining}${hint ? ` — ${hint}` : ""}`;
|
||||
}
|
||||
|
||||
function resolveAuthProfileStorePath(target: AuthProfileHealthTarget): string {
|
||||
return resolveAuthStorePathForDisplay(target.agentDir);
|
||||
}
|
||||
|
||||
function authProfileIssueToHealthFinding(params: {
|
||||
issue: AuthIssue;
|
||||
target: AuthProfileHealthTarget;
|
||||
labelAgents: boolean;
|
||||
hint: string | null;
|
||||
}): HealthFinding {
|
||||
const remaining =
|
||||
params.issue.remainingMs !== undefined
|
||||
? ` (${formatRemainingShort(params.issue.remainingMs)})`
|
||||
: "";
|
||||
const reason = params.issue.reasonCode ? ` [${params.issue.reasonCode}]` : "";
|
||||
const owner = params.labelAgents ? `Agent ${params.target.agentId} auth profile` : "Auth profile";
|
||||
return {
|
||||
checkId: AUTH_PROFILES_CHECK_ID,
|
||||
severity: "warning",
|
||||
message: `${owner} ${params.issue.profileId} is ${params.issue.status}${reason}${remaining}.`,
|
||||
path: resolveAuthProfileStorePath(params.target),
|
||||
target: params.issue.profileId,
|
||||
...(params.issue.reasonCode ? { requirement: params.issue.reasonCode } : {}),
|
||||
fixHint:
|
||||
params.hint ??
|
||||
(params.issue.status === "expiring"
|
||||
? "Run `openclaw doctor --fix` to refresh expiring OAuth profiles, or re-authenticate static tokens."
|
||||
: "Run `openclaw doctor --fix` to refresh OAuth profiles, or re-authenticate this provider."),
|
||||
};
|
||||
}
|
||||
|
||||
function authProfileCooldownToHealthFinding(params: {
|
||||
profileId: string;
|
||||
target: AuthProfileHealthTarget;
|
||||
labelAgents: boolean;
|
||||
kind: string;
|
||||
remaining: string;
|
||||
hint: string;
|
||||
}): HealthFinding {
|
||||
return {
|
||||
checkId: AUTH_PROFILES_CHECK_ID,
|
||||
severity: "warning",
|
||||
message: params.labelAgents
|
||||
? `Agent ${params.target.agentId} auth profile ${params.profileId} is ${params.kind} (${params.remaining}).`
|
||||
: `Auth profile ${params.profileId} is ${params.kind} (${params.remaining}).`,
|
||||
path: resolveAuthProfileStorePath(params.target),
|
||||
target: params.profileId,
|
||||
fixHint: params.hint,
|
||||
};
|
||||
}
|
||||
|
||||
async function collectAuthProfileHealthFindingsForTarget(params: {
|
||||
cfg: OpenClawConfig;
|
||||
allowKeychainPrompt: boolean;
|
||||
target: AuthProfileHealthTarget;
|
||||
labelAgents: boolean;
|
||||
}): Promise<readonly HealthFinding[]> {
|
||||
const store = ensureAuthProfileStore(params.target.agentDir, {
|
||||
allowKeychainPrompt: params.allowKeychainPrompt,
|
||||
readOnly: true,
|
||||
});
|
||||
const findings: HealthFinding[] = [];
|
||||
const now = Date.now();
|
||||
for (const profileId of Object.keys(store.usageStats ?? {})) {
|
||||
const until = resolveProfileUnusableUntilForDisplay(store, profileId);
|
||||
if (!until || now >= until) {
|
||||
continue;
|
||||
}
|
||||
const stats = store.usageStats?.[profileId];
|
||||
const remaining = formatRemainingShort(until - now);
|
||||
const disabledActive = typeof stats?.disabledUntil === "number" && now < stats.disabledUntil;
|
||||
const kind = disabledActive
|
||||
? `disabled${stats.disabledReason ? `:${stats.disabledReason}` : ""}`
|
||||
: "cooldown";
|
||||
const hint = resolveUnusableProfileHint({
|
||||
kind: disabledActive ? "disabled" : "cooldown",
|
||||
reason: stats?.disabledReason,
|
||||
});
|
||||
findings.push(
|
||||
authProfileCooldownToHealthFinding({
|
||||
profileId,
|
||||
target: params.target,
|
||||
labelAgents: params.labelAgents,
|
||||
kind,
|
||||
remaining,
|
||||
hint,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const summary = buildAuthHealthSummary({
|
||||
store,
|
||||
cfg: params.cfg,
|
||||
warnAfterMs: DEFAULT_OAUTH_WARN_MS,
|
||||
allowKeychainPrompt: params.allowKeychainPrompt,
|
||||
});
|
||||
const issues = summary.profiles.filter((profile) => {
|
||||
if (profile.type === "api_key") {
|
||||
return profile.status === "missing";
|
||||
}
|
||||
return (
|
||||
(profile.type === "oauth" || profile.type === "token") &&
|
||||
(profile.status === "expired" ||
|
||||
profile.status === "expiring" ||
|
||||
profile.status === "missing")
|
||||
);
|
||||
});
|
||||
for (const issue of issues) {
|
||||
const authIssue: AuthIssue = {
|
||||
profileId: issue.profileId,
|
||||
provider: issue.provider,
|
||||
status: issue.status,
|
||||
reasonCode: issue.reasonCode,
|
||||
remainingMs: issue.remainingMs,
|
||||
};
|
||||
findings.push(
|
||||
authProfileIssueToHealthFinding({
|
||||
issue: authIssue,
|
||||
target: params.target,
|
||||
labelAgents: params.labelAgents,
|
||||
hint: await resolveAuthIssueHint(authIssue, params.cfg, store),
|
||||
}),
|
||||
);
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
/** Collects read-only structured findings for auth profile health. */
|
||||
export async function collectAuthProfileHealthFindings(params: {
|
||||
cfg: OpenClawConfig;
|
||||
allowKeychainPrompt?: boolean;
|
||||
}): Promise<readonly HealthFinding[]> {
|
||||
const configuredProfiles = Object.keys(params.cfg.auth?.profiles ?? {}).length > 0;
|
||||
const targets = listAuthProfileHealthTargets(params.cfg);
|
||||
const activeTargets = targets.filter((target) =>
|
||||
target.isDefault
|
||||
? hasAnyAuthProfileStoreSource(target.agentDir) || configuredProfiles
|
||||
: hasLocalAuthProfileStoreSource(target.agentDir),
|
||||
);
|
||||
const findings: HealthFinding[] = [];
|
||||
const labelAgents = activeTargets.length > 1;
|
||||
for (const target of activeTargets) {
|
||||
findings.push(
|
||||
...(await collectAuthProfileHealthFindingsForTarget({
|
||||
cfg: params.cfg,
|
||||
allowKeychainPrompt: params.allowKeychainPrompt ?? false,
|
||||
target,
|
||||
labelAgents,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
const providerOverride = params.cfg.models?.providers?.[LEGACY_CODEX_PROVIDER_ID];
|
||||
if (
|
||||
providerOverride &&
|
||||
hasLegacyCodexTransportOverride(providerOverride) &&
|
||||
(hasConfiguredCodexOAuthProfile(params.cfg) || hasStoredCodexOAuthProfile())
|
||||
) {
|
||||
findings.push(legacyCodexProviderOverrideToHealthFinding(providerOverride));
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
async function noteAuthProfileHealthForTarget(params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: DoctorPrompter;
|
||||
|
||||
@@ -21,6 +21,7 @@ const mocks = vi.hoisted(() => ({
|
||||
maybeRepairGatewayDaemon: vi.fn().mockResolvedValue(undefined),
|
||||
maybeRepairLegacyOAuthProfileIds: vi.fn(async (cfg: unknown) => cfg),
|
||||
maybeRepairLegacyOAuthSidecarProfiles: vi.fn().mockResolvedValue(undefined),
|
||||
collectAuthProfileHealthFindings: vi.fn(async () => []),
|
||||
noteAuthProfileHealth: vi.fn().mockResolvedValue(undefined),
|
||||
noteLegacyCodexProviderOverride: vi.fn(),
|
||||
buildGatewayConnectionDetails: vi.fn(() => ({ message: "gateway details" })),
|
||||
@@ -145,6 +146,7 @@ vi.mock("../commands/doctor-auth-oauth-sidecar.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../commands/doctor-auth.js", () => ({
|
||||
collectAuthProfileHealthFindings: mocks.collectAuthProfileHealthFindings,
|
||||
noteAuthProfileHealth: mocks.noteAuthProfileHealth,
|
||||
noteLegacyCodexProviderOverride: mocks.noteLegacyCodexProviderOverride,
|
||||
}));
|
||||
@@ -327,6 +329,8 @@ describe("doctor health contributions", () => {
|
||||
mocks.maybeRepairLegacyOAuthProfileIds.mockImplementation(async (cfg: unknown) => cfg);
|
||||
mocks.maybeRepairLegacyOAuthSidecarProfiles.mockClear();
|
||||
mocks.maybeRepairLegacyOAuthSidecarProfiles.mockResolvedValue(undefined);
|
||||
mocks.collectAuthProfileHealthFindings.mockClear();
|
||||
mocks.collectAuthProfileHealthFindings.mockResolvedValue([]);
|
||||
mocks.noteAuthProfileHealth.mockClear();
|
||||
mocks.noteAuthProfileHealth.mockResolvedValue(undefined);
|
||||
mocks.noteLegacyCodexProviderOverride.mockClear();
|
||||
@@ -1007,6 +1011,32 @@ describe("doctor health contributions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("registers auth profile health as an opt-in structured check", async () => {
|
||||
const contribution = requireDoctorContribution("doctor:auth-profiles");
|
||||
const [check] = contribution.healthChecks;
|
||||
|
||||
expect(contribution.healthCheckIds).toEqual(["core/doctor/auth-profiles"]);
|
||||
expect(check).toMatchObject({
|
||||
id: "core/doctor/auth-profiles",
|
||||
kind: "core",
|
||||
defaultEnabled: false,
|
||||
});
|
||||
if (!check || !("detect" in check)) {
|
||||
throw new Error("expected split auth profile health check");
|
||||
}
|
||||
|
||||
await check.detect({
|
||||
mode: "lint",
|
||||
cfg: { auth: { profiles: {} } },
|
||||
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() },
|
||||
});
|
||||
|
||||
expect(mocks.collectAuthProfileHealthFindings).toHaveBeenCalledWith({
|
||||
cfg: { auth: { profiles: {} } },
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards skipped Gateway health to daemon repair", async () => {
|
||||
const contribution = requireDoctorContribution("doctor:gateway-daemon");
|
||||
const ctx = {
|
||||
|
||||
@@ -1222,6 +1222,19 @@ export function resolveDoctorHealthContributions(): DoctorHealthContribution[] {
|
||||
createDoctorHealthContribution({
|
||||
id: "doctor:auth-profiles",
|
||||
label: "Auth profiles",
|
||||
healthChecks: {
|
||||
id: "core/doctor/auth-profiles",
|
||||
kind: "core",
|
||||
description: "Auth profile cooldown, expiry, missing credential, and legacy override state",
|
||||
defaultEnabled: false,
|
||||
async detect(ctx) {
|
||||
const { collectAuthProfileHealthFindings } = await import("../commands/doctor-auth.js");
|
||||
return collectAuthProfileHealthFindings({
|
||||
cfg: ctx.cfg,
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
},
|
||||
},
|
||||
run: runAuthProfileHealth,
|
||||
}),
|
||||
createDoctorHealthContribution({
|
||||
|
||||
Reference in New Issue
Block a user