fix(onboard): validate reset preflight (#111348)

This commit is contained in:
Peter Steinberger
2026-07-19 02:42:49 -07:00
committed by GitHub
parent a868d34804
commit d4ed08994d
8 changed files with 1000 additions and 35 deletions

View File

@@ -0,0 +1,35 @@
import { describe, expect, it, vi } from "vitest";
import type { RuntimeEnv } from "../runtime.js";
import { createProviderApiKeyAuthMethod } from "./provider-api-key-auth.js";
describe("createProviderApiKeyAuthMethod", () => {
it("exposes side-effect-free non-interactive credential validation", async () => {
const method = createProviderApiKeyAuthMethod({
providerId: "example",
methodId: "api-key",
label: "Example",
optionKey: "exampleApiKey",
flagName: "--example-api-key",
envVar: "EXAMPLE_API_KEY",
promptMessage: "Example API key",
});
const resolveApiKey = vi.fn(async () => ({ key: "test-token", source: "flag" as const }));
const valid = await method.validateNonInteractive?.({
authChoice: "example-api-key",
config: {},
baseConfig: {},
opts: { exampleApiKey: "test-token" },
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as unknown as RuntimeEnv,
resolveApiKey,
});
expect(valid).toBe(true);
expect(resolveApiKey).toHaveBeenCalledWith({
provider: "example",
flagValue: "test-token",
flagName: "--example-api-key",
envVar: "EXAMPLE_API_KEY",
});
});
});

View File

@@ -13,6 +13,10 @@ import type {
ProviderPluginWizardSetup,
} from "./types.js";
type ProviderAuthMethodNonInteractiveValidationContext = Parameters<
NonNullable<ProviderAuthMethod["validateNonInteractive"]>
>[0];
type ProviderApiKeyAuthMethodOptions = {
providerId: string;
methodId: string;
@@ -107,6 +111,18 @@ async function applyApiKeyConfig(params: {
export function createProviderApiKeyAuthMethod(
params: ProviderApiKeyAuthMethodOptions,
): ProviderAuthMethod {
const resolveNonInteractiveCredential = async (
ctx: ProviderAuthMethodNonInteractiveValidationContext,
) => {
const opts = ctx.opts as Record<string, unknown> | undefined;
return await ctx.resolveApiKey({
provider: params.providerId,
flagValue: resolveStringOption(opts, params.optionKey),
flagName: params.flagName,
envVar: params.envVar,
...(params.allowProfile === false ? { allowProfile: false } : {}),
});
};
return {
id: params.methodId,
label: params.label,
@@ -179,15 +195,9 @@ export function createProviderApiKeyAuthMethod(
...(params.defaultModel ? { defaultModel: params.defaultModel } : {}),
};
},
validateNonInteractive: async (ctx) => Boolean(await resolveNonInteractiveCredential(ctx)),
runNonInteractive: async (ctx) => {
const opts = ctx.opts as Record<string, unknown> | undefined;
const resolved = await ctx.resolveApiKey({
provider: params.providerId,
flagValue: resolveStringOption(opts, params.optionKey),
flagName: params.flagName,
envVar: params.envVar,
...(params.allowProfile === false ? { allowProfile: false } : {}),
});
const resolved = await resolveNonInteractiveCredential(ctx);
if (!resolved) {
return null;
}

View File

@@ -113,6 +113,11 @@ export type ProviderAuthMethodNonInteractiveContext = {
) => ApiKeyCredential | null;
};
type ProviderAuthMethodNonInteractiveValidationContext = Omit<
ProviderAuthMethodNonInteractiveContext,
"toApiKeyCredential"
>;
/** Read-only context for app-guided discovery of already available inference. */
export type ProviderAppGuidedSetupContext = {
config: OpenClawConfig;
@@ -156,6 +161,10 @@ export type ProviderAuthMethod = {
runNonInteractive?: (
ctx: ProviderAuthMethodNonInteractiveContext,
) => Promise<OpenClawConfig | null>;
/** Side-effect-free prerequisite validation used before destructive reset handling. */
validateNonInteractive?: (
ctx: ProviderAuthMethodNonInteractiveValidationContext,
) => Promise<boolean>;
/** Provider-owned local model discovery for the shared guided setup ladder. */
appGuidedSetup?: ProviderAppGuidedSetup;
};