mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
Configure: add Keep Existing auth method option
This commit is contained in:
92
src/commands/auth-choice-prompt.test.ts
Normal file
92
src/commands/auth-choice-prompt.test.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { AuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import type { WizardPrompter, WizardSelectParams } from "../wizard/prompts.js";
|
||||
import { buildAuthChoiceGroups } from "./auth-choice-options.js";
|
||||
import { promptAuthChoiceGrouped, resolveExistingAuthLinesForGroup } from "./auth-choice-prompt.js";
|
||||
import { createWizardPrompter } from "./test-wizard-helpers.js";
|
||||
|
||||
const ORIGINAL_OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
|
||||
function createStore(): AuthProfileStore {
|
||||
return {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai-codex:default": {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function requireOpenAiGroup(store: AuthProfileStore) {
|
||||
const group = buildAuthChoiceGroups({
|
||||
store,
|
||||
includeSkip: false,
|
||||
}).groups.find((entry) => entry.value === "openai");
|
||||
|
||||
if (!group) {
|
||||
throw new Error("openai auth choice group missing");
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
if (ORIGINAL_OPENAI_API_KEY === undefined) {
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
} else {
|
||||
process.env.OPENAI_API_KEY = ORIGINAL_OPENAI_API_KEY;
|
||||
}
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("auth choice keep existing", () => {
|
||||
it("lists existing APIKey and OAuth entries for OpenAI", () => {
|
||||
process.env.OPENAI_API_KEY = "sk-test";
|
||||
const store = createStore();
|
||||
|
||||
const lines = resolveExistingAuthLinesForGroup({
|
||||
group: requireOpenAiGroup(store),
|
||||
store,
|
||||
});
|
||||
|
||||
expect(lines).toContain("APIKey: OPENAI_API_KEY (openai)");
|
||||
expect(lines).toContain("OAuth: openai-codex:default (openai-codex)");
|
||||
});
|
||||
|
||||
it("offers Keep Existing in the method selector and returns skip", async () => {
|
||||
process.env.OPENAI_API_KEY = "sk-test";
|
||||
const store = createStore();
|
||||
|
||||
const select: WizardPrompter["select"] = vi.fn(async (params: WizardSelectParams) => {
|
||||
if (params.message === "Model/auth provider") {
|
||||
return "openai";
|
||||
}
|
||||
if (params.message === "OpenAI auth method") {
|
||||
const keepExisting = params.options.find((option) => option.value === "skip");
|
||||
expect(keepExisting?.label).toBe("Keep Existing");
|
||||
expect(keepExisting?.hint).toContain("APIKey:");
|
||||
expect(keepExisting?.hint).toContain("OAuth:");
|
||||
expect(keepExisting?.hint).toContain("\n");
|
||||
return "skip";
|
||||
}
|
||||
return params.options[0]?.value ?? "skip";
|
||||
});
|
||||
|
||||
const prompter = createWizardPrompter(
|
||||
{ select: select as unknown as WizardPrompter["select"] },
|
||||
{ defaultSelect: "" },
|
||||
);
|
||||
|
||||
await expect(
|
||||
promptAuthChoiceGrouped({
|
||||
prompter,
|
||||
store,
|
||||
includeSkip: true,
|
||||
}),
|
||||
).resolves.toBe("skip");
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,72 @@
|
||||
import type { AuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import { listProfilesForProvider } from "../agents/auth-profiles.js";
|
||||
import { resolveEnvApiKey } from "../agents/model-auth.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import type { AuthChoiceGroup, AuthChoiceOption } from "./auth-choice-options.js";
|
||||
import { buildAuthChoiceGroups } from "./auth-choice-options.js";
|
||||
import { resolvePreferredProviderForAuthChoice } from "./auth-choice.preferred-provider.js";
|
||||
import type { AuthChoice } from "./onboard-types.js";
|
||||
|
||||
const BACK_VALUE = "__back";
|
||||
|
||||
function toKindLabel(type: "api_key" | "oauth" | "token" | undefined): "APIKey" | "OAuth" {
|
||||
if (type === "oauth" || type === "token") {
|
||||
return "OAuth";
|
||||
}
|
||||
return "APIKey";
|
||||
}
|
||||
|
||||
function stripEnvSourcePrefix(source: string): string {
|
||||
return source.replace(/^shell env: /, "").replace(/^env: /, "");
|
||||
}
|
||||
|
||||
function resolveProviderKeysForGroup(group: AuthChoiceGroup): string[] {
|
||||
const keys = group.options
|
||||
.map((option) => resolvePreferredProviderForAuthChoice(option.value))
|
||||
.filter((provider): provider is string => Boolean(provider));
|
||||
return [...new Set(keys)];
|
||||
}
|
||||
|
||||
export function resolveExistingAuthLinesForGroup(params: {
|
||||
group: AuthChoiceGroup;
|
||||
store: AuthProfileStore;
|
||||
}): string[] {
|
||||
const providerKeys = resolveProviderKeysForGroup(params.group);
|
||||
const showProvider = providerKeys.length > 1;
|
||||
const lines = new Set<string>();
|
||||
|
||||
for (const providerKey of providerKeys) {
|
||||
const profileIds = listProfilesForProvider(params.store, providerKey);
|
||||
for (const profileId of profileIds) {
|
||||
const kind = toKindLabel(params.store.profiles[profileId]?.type);
|
||||
const providerSuffix = showProvider ? ` (${providerKey})` : "";
|
||||
lines.add(`${kind}: ${profileId}${providerSuffix}`);
|
||||
}
|
||||
|
||||
const envKey = resolveEnvApiKey(providerKey);
|
||||
if (envKey) {
|
||||
const kind = envKey.source.includes("OAUTH_TOKEN") ? "OAuth" : "APIKey";
|
||||
const source = stripEnvSourcePrefix(envKey.source);
|
||||
const providerSuffix = showProvider ? ` (${providerKey})` : "";
|
||||
lines.add(`${kind}: ${source}${providerSuffix}`);
|
||||
}
|
||||
}
|
||||
|
||||
return [...lines];
|
||||
}
|
||||
|
||||
export function buildKeepExistingOption(params: {
|
||||
group: AuthChoiceGroup;
|
||||
store: AuthProfileStore;
|
||||
}): AuthChoiceOption {
|
||||
const lines = resolveExistingAuthLinesForGroup(params);
|
||||
return {
|
||||
value: "skip",
|
||||
label: "Keep Existing",
|
||||
hint: lines.length > 0 ? lines.join("\n") : "Use existing auth",
|
||||
};
|
||||
}
|
||||
|
||||
export async function promptAuthChoiceGrouped(params: {
|
||||
prompter: WizardPrompter;
|
||||
store: AuthProfileStore;
|
||||
@@ -46,9 +108,15 @@ export async function promptAuthChoiceGrouped(params: {
|
||||
return group.options[0].value;
|
||||
}
|
||||
|
||||
const methodOptions: Array<{ value: string; label: string; hint?: string }> = [
|
||||
...group.options,
|
||||
...(params.includeSkip ? [buildKeepExistingOption({ group, store: params.store })] : []),
|
||||
{ value: BACK_VALUE, label: "Back" },
|
||||
];
|
||||
|
||||
const methodSelection = await params.prompter.select({
|
||||
message: `${group.label} auth method`,
|
||||
options: [...group.options, { value: BACK_VALUE, label: "Back" }],
|
||||
options: methodOptions,
|
||||
});
|
||||
|
||||
if (methodSelection === BACK_VALUE) {
|
||||
|
||||
Reference in New Issue
Block a user