diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.test.ts index c9d8a345b04..ebe6fc3323d 100644 --- a/src/commands/auth-choice.test.ts +++ b/src/commands/auth-choice.test.ts @@ -1092,21 +1092,50 @@ describe("applyAuthChoice", () => { } }); - it("maps apiKey tokenProvider aliases to provider flow", async () => { + it("uses provided tokens without prompting across alias and direct provider choices", async () => { const scenarios: Array<{ + authChoice: "apiKey" | "opencode-zen" | "gemini-api-key"; + config?: OpenClawConfig; + setDefaultModel: boolean; tokenProvider: string; token: string; profileId: string; provider: string; - expectedModel: string; + expectedModel?: string; + expectedModelPrefix?: string; + expectedAgentModelOverride?: string; + extraProfiles?: string[]; }> = [ { + authChoice: "apiKey", + setDefaultModel: true, tokenProvider: " GOOGLE ", token: "sk-gemini-token-provider-test", profileId: "google:default", provider: "google", expectedModel: GOOGLE_GEMINI_DEFAULT_MODEL, }, + { + authChoice: "opencode-zen", + setDefaultModel: true, + tokenProvider: "opencode", + token: "sk-opencode-test", + profileId: "opencode:default", + provider: "opencode", + expectedModelPrefix: "opencode/", + extraProfiles: ["opencode-go:default"], + }, + { + authChoice: "gemini-api-key", + config: { agents: { defaults: { model: { primary: "openai/gpt-4o-mini" } } } }, + setDefaultModel: false, + tokenProvider: "google", + token: "sk-gemini-test", + profileId: "google:default", + provider: "google", + expectedModel: "openai/gpt-4o-mini", + expectedAgentModelOverride: GOOGLE_GEMINI_DEFAULT_MODEL, + }, ]; await setupTempState(); for (const scenario of scenarios) { @@ -1118,124 +1147,40 @@ describe("applyAuthChoice", () => { const { prompter, runtime } = createApiKeyPromptHarness({ text, confirm }); const result = await applyAuthChoice({ - authChoice: "apiKey", - config: {}, + authChoice: scenario.authChoice, + config: scenario.config ?? {}, prompter, runtime, - setDefaultModel: true, + setDefaultModel: scenario.setDefaultModel, opts: { tokenProvider: scenario.tokenProvider, token: scenario.token, }, }); + expect(text).not.toHaveBeenCalled(); + expect(confirm).not.toHaveBeenCalled(); expect(result.config.auth?.profiles?.[scenario.profileId]).toMatchObject({ provider: scenario.provider, mode: "api_key", }); - expect(resolveAgentModelPrimaryValue(result.config.agents?.defaults?.model)).toBe( - scenario.expectedModel, - ); - expect(text).not.toHaveBeenCalled(); - expect(confirm).not.toHaveBeenCalled(); + const selectedModel = resolveAgentModelPrimaryValue(result.config.agents?.defaults?.model); + if (scenario.expectedModel) { + expect(selectedModel).toBe(scenario.expectedModel); + } + if (scenario.expectedModelPrefix) { + expect(selectedModel?.startsWith(scenario.expectedModelPrefix)).toBe(true); + } + if (scenario.expectedAgentModelOverride) { + expect(result.agentModelOverride).toBe(scenario.expectedAgentModelOverride); + } expect((await readAuthProfile(scenario.profileId))?.key).toBe(scenario.token); - } - }); - - it("uses opts token for direct provider choices without prompting", async () => { - await setupTempState(); - const scenarios: Array<{ - authChoice: "opencode-zen"; - tokenProvider: "opencode"; - profileId: "opencode:default"; - provider: "opencode"; - modelPrefix: "opencode/"; - extraProfiles: string[]; - }> = [ - { - authChoice: "opencode-zen", - tokenProvider: "opencode", - profileId: "opencode:default", - provider: "opencode", - modelPrefix: "opencode/", - extraProfiles: ["opencode-go:default"], - }, - ]; - for (const { - authChoice, - tokenProvider, - profileId, - provider, - modelPrefix, - extraProfiles, - } of scenarios) { - const text = vi.fn(); - const confirm = vi.fn(async () => false); - const { prompter, runtime } = createApiKeyPromptHarness({ text, confirm }); - const token = `sk-${tokenProvider}-test`; - - const result = await applyAuthChoice({ - authChoice, - config: {}, - prompter, - runtime, - setDefaultModel: true, - opts: { - tokenProvider, - token, - }, - }); - - expect(text).not.toHaveBeenCalled(); - expect(confirm).not.toHaveBeenCalled(); - expect(result.config.auth?.profiles?.[profileId]).toMatchObject({ - provider, - mode: "api_key", - }); - expect( - resolveAgentModelPrimaryValue(result.config.agents?.defaults?.model)?.startsWith( - modelPrefix, - ), - ).toBe(true); - expect((await readAuthProfile(profileId))?.key).toBe(token); - for (const extraProfile of extraProfiles ?? []) { - expect((await readAuthProfile(extraProfile))?.key).toBe(token); + for (const extraProfile of scenario.extraProfiles ?? []) { + expect((await readAuthProfile(extraProfile))?.key).toBe(scenario.token); } } }); - it("uses opts token for Gemini and keeps global default model when setDefaultModel=false", async () => { - await setupTempState(); - - const text = vi.fn(); - const confirm = vi.fn(async () => false); - const { prompter, runtime } = createApiKeyPromptHarness({ text, confirm }); - - const result = await applyAuthChoice({ - authChoice: "gemini-api-key", - config: { agents: { defaults: { model: { primary: "openai/gpt-4o-mini" } } } }, - prompter, - runtime, - setDefaultModel: false, - opts: { - tokenProvider: "google", - token: "sk-gemini-test", - }, - }); - - expect(text).not.toHaveBeenCalled(); - expect(confirm).not.toHaveBeenCalled(); - expect(result.config.auth?.profiles?.["google:default"]).toMatchObject({ - provider: "google", - mode: "api_key", - }); - expect(resolveAgentModelPrimaryValue(result.config.agents?.defaults?.model)).toBe( - "openai/gpt-4o-mini", - ); - expect(result.agentModelOverride).toBe(GOOGLE_GEMINI_DEFAULT_MODEL); - expect((await readAuthProfile("google:default"))?.key).toBe("sk-gemini-test"); - }); - it("prompts for Venice API key and shows the Venice note when no token is provided", async () => { await setupTempState(); process.env.VENICE_API_KEY = ""; diff --git a/src/commands/onboard-non-interactive.gateway.test.ts b/src/commands/onboard-non-interactive.gateway.test.ts index 6ce4d0cb577..01510646785 100644 --- a/src/commands/onboard-non-interactive.gateway.test.ts +++ b/src/commands/onboard-non-interactive.gateway.test.ts @@ -635,7 +635,7 @@ describe("onboard (non-interactive): gateway and remote auth", () => { }); }, 60_000); - it("uses a longer Windows health deadline when daemon install was requested", async () => { + it("uses longer Windows health timeouts when daemon install was requested", async () => { await withStateDir("state-local-daemon-health-win-", async (stateDir) => { const captured = mockGatewayReachableWithCapturedTimeouts(); @@ -646,17 +646,6 @@ describe("onboard (non-interactive): gateway and remote auth", () => { expect(installGatewayDaemonNonInteractiveMock).toHaveBeenCalledTimes(1); expect(captured.deadlineMs).toBe(90_000); expect(captured.probeTimeoutMs).toBe(15_000); - }); - }, 60_000); - - it("uses a longer Windows health command timeout when daemon install was requested", async () => { - await withStateDir("state-local-daemon-health-command-win-", async (stateDir) => { - waitForGatewayReachableMock = vi.fn(async () => ({ ok: true })); - - await withMockedPlatform("win32", async () => { - await runLocalDaemonSetup(stateDir); - }); - expect(healthCommandMock).toHaveBeenCalledTimes(1); expect(healthCommandMock).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/src/commands/onboard-non-interactive.provider-auth.test.ts b/src/commands/onboard-non-interactive.provider-auth.test.ts index e00797f402e..84ecbffdef7 100644 --- a/src/commands/onboard-non-interactive.provider-auth.test.ts +++ b/src/commands/onboard-non-interactive.provider-auth.test.ts @@ -1178,49 +1178,51 @@ describe("onboard (non-interactive): provider auth", () => { }); }); - it("configures a custom provider from non-interactive flags", async () => { - await withOnboardEnv("openclaw-onboard-custom-provider-", async ({ configPath, runtime }) => { - await runNonInteractiveSetupWithDefaults(runtime, { - authChoice: "custom-api-key", - customBaseUrl: "https://llm.example.com/v1", - customApiKey: "custom-test-key", // pragma: allowlist secret - customModelId: "foo-large", - customCompatibility: "anthropic", - skipSkills: true, - }); - - const cfg = await readJsonFile(configPath); - - const provider = cfg.models?.providers?.["custom-llm-example-com"]; - expect(provider?.baseUrl).toBe("https://llm.example.com/v1"); - expect(provider?.api).toBe("anthropic-messages"); - expect(provider?.apiKey).toBe("custom-test-key"); - expect(provider?.models?.some((model) => model.id === "foo-large")).toBe(true); - expect(cfg.agents?.defaults?.model?.primary).toBe("custom-llm-example-com/foo-large"); - }); - }); - - it("infers custom provider auth choice from custom flags", async () => { - await withOnboardEnv( - "openclaw-onboard-custom-provider-infer-", - async ({ configPath, runtime }) => { - await runNonInteractiveSetupWithDefaults(runtime, { + it("configures custom providers from explicit or inferred non-interactive flags", async () => { + const scenarios = [ + { + options: { + authChoice: "custom-api-key", + customBaseUrl: "https://llm.example.com/v1", + customApiKey: "custom-test-key", // pragma: allowlist secret + customModelId: "foo-large", + customCompatibility: "anthropic", + skipSkills: true, + }, + providerId: "custom-llm-example-com", + expectedBaseUrl: "https://llm.example.com/v1", + expectedApi: "anthropic-messages", + expectedModel: "custom-llm-example-com/foo-large", + modelId: "foo-large", + }, + { + options: { customBaseUrl: "https://models.custom.local/v1", customModelId: "local-large", customApiKey: "custom-test-key", // pragma: allowlist secret skipSkills: true, - }); - - const cfg = await readJsonFile(configPath); - - expect(cfg.models?.providers?.["custom-models-custom-local"]?.baseUrl).toBe( - "https://models.custom.local/v1", - ); - expect(cfg.models?.providers?.["custom-models-custom-local"]?.api).toBe( - "openai-completions", - ); - expect(cfg.agents?.defaults?.model?.primary).toBe("custom-models-custom-local/local-large"); + }, + providerId: "custom-models-custom-local", + expectedBaseUrl: "https://models.custom.local/v1", + expectedApi: "openai-completions", + expectedModel: "custom-models-custom-local/local-large", + modelId: "local-large", }, - ); + ] as const; + + await withOnboardEnv("openclaw-onboard-custom-provider-", async ({ configPath, runtime }) => { + for (const scenario of scenarios) { + await fs.rm(configPath, { force: true }); + resetProviderAuthTestState(); + await runNonInteractiveSetupWithDefaults(runtime, scenario.options); + const cfg = await readJsonFile(configPath); + const provider = cfg.models?.providers?.[scenario.providerId]; + expect(provider?.baseUrl).toBe(scenario.expectedBaseUrl); + expect(provider?.api).toBe(scenario.expectedApi); + expect(provider?.apiKey).toBe("custom-test-key"); + expect(provider?.models?.some((model) => model.id === scenario.modelId)).toBe(true); + expect(cfg.agents?.defaults?.model?.primary).toBe(scenario.expectedModel); + } + }); }); });