From d3b70f98230a60143a25915d59dc8bf522ef1fb8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 17 Apr 2026 09:00:59 +0100 Subject: [PATCH] test: tighten message and onboarding hotspots --- src/commands/message.test.ts | 479 ++++-------------- src/commands/message.ts | 12 +- ...oard-non-interactive.provider-auth.test.ts | 217 ++------ .../local/auth-choice-inference.test.ts | 19 + 4 files changed, 181 insertions(+), 546 deletions(-) diff --git a/src/commands/message.test.ts b/src/commands/message.test.ts index 3e18edabb15..9f937bd4334 100644 --- a/src/commands/message.test.ts +++ b/src/commands/message.test.ts @@ -1,15 +1,13 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import type { - ChannelMessageActionAdapter, - ChannelOutboundAdapter, - ChannelPlugin, -} from "../channels/plugins/types.js"; import type { CliDeps } from "../cli/deps.js"; -import { setActivePluginRegistry } from "../plugins/runtime.js"; import type { RuntimeEnv } from "../runtime.js"; -import { createTestRegistry } from "../test-utils/channel-plugins.js"; import { captureEnv } from "../test-utils/env.js"; +type RunMessageActionParams = { + action: string; + params: Record; +}; + let testConfig: Record = {}; const applyPluginAutoEnable = vi.hoisted(() => vi.fn(({ config }) => ({ config, changes: [] }))); vi.mock("../config/config.js", () => ({ @@ -20,14 +18,13 @@ vi.mock("../config/plugin-auto-enable.js", () => ({ applyPluginAutoEnable, })); -const { resolveCommandConfigWithSecrets, callGatewayMock } = vi.hoisted(() => ({ - resolveCommandConfigWithSecrets: vi.fn(async ({ config }: { config: unknown }) => ({ +const resolveCommandConfigWithSecrets = vi.hoisted(() => + vi.fn(async ({ config }: { config: unknown }) => ({ resolvedConfig: config, effectiveConfig: config, diagnostics: [] as string[], })), - callGatewayMock: vi.fn(), -})); +); vi.mock("../cli/command-config-resolution.js", () => ({ resolveCommandConfigWithSecrets: async (opts: { @@ -54,47 +51,39 @@ vi.mock("../cli/command-config-resolution.js", () => ({ }, })); -vi.mock("../gateway/call.js", () => ({ - callGateway: callGatewayMock, - callGatewayLeastPrivilege: callGatewayMock, - randomIdempotencyKey: () => "idem-1", +const getScopedChannelsCommandSecretTargets = vi.hoisted(() => + vi.fn(() => ({ + targetIds: new Set(["channels.telegram.token"]), + })), +); + +vi.mock("../cli/command-secret-targets.js", () => ({ + getScopedChannelsCommandSecretTargets, })); -const handleDiscordAction = vi.hoisted(() => - vi.fn(async (..._args: unknown[]) => ({ details: { ok: true } })), +const runMessageActionMock = vi.hoisted(() => + vi.fn(async ({ action, params }: RunMessageActionParams) => ({ + kind: action === "poll" ? "poll" : "send", + channel: typeof params.channel === "string" ? params.channel : "telegram", + action: action === "poll" ? "poll" : "send", + to: typeof params.target === "string" ? params.target : "123456", + handledBy: "plugin", + payload: { ok: true }, + dryRun: false, + })), ); -const handleTelegramAction = vi.hoisted(() => - vi.fn(async (..._args: unknown[]) => ({ details: { ok: true } })), -); +vi.mock("../infra/outbound/message-action-runner.js", () => ({ + runMessageAction: runMessageActionMock, +})); let messageCommand: typeof import("./message.js").messageCommand; - let envSnapshot: ReturnType; -const EMPTY_TEST_REGISTRY = createTestRegistry([]); beforeAll(async () => { ({ messageCommand } = await import("./message.js")); }); -beforeEach(() => { - envSnapshot = captureEnv(["TELEGRAM_BOT_TOKEN", "DISCORD_BOT_TOKEN"]); - process.env.TELEGRAM_BOT_TOKEN = ""; - process.env.DISCORD_BOT_TOKEN = ""; - testConfig = {}; - setActivePluginRegistry(EMPTY_TEST_REGISTRY); - callGatewayMock.mockClear(); - handleDiscordAction.mockClear(); - handleTelegramAction.mockClear(); - resolveCommandConfigWithSecrets.mockClear(); - applyPluginAutoEnable.mockClear(); - applyPluginAutoEnable.mockImplementation(({ config }) => ({ config, changes: [] })); -}); - -afterEach(() => { - envSnapshot.restore(); -}); - const runtime: RuntimeEnv = { log: vi.fn(), error: vi.fn(), @@ -103,6 +92,25 @@ const runtime: RuntimeEnv = { }), }; +beforeEach(() => { + envSnapshot = captureEnv(["TELEGRAM_BOT_TOKEN", "DISCORD_BOT_TOKEN"]); + process.env.TELEGRAM_BOT_TOKEN = ""; + process.env.DISCORD_BOT_TOKEN = ""; + testConfig = {}; + runMessageActionMock.mockClear(); + resolveCommandConfigWithSecrets.mockClear(); + getScopedChannelsCommandSecretTargets.mockClear(); + applyPluginAutoEnable.mockClear(); + applyPluginAutoEnable.mockImplementation(({ config }) => ({ config, changes: [] })); + vi.mocked(runtime.log).mockClear(); + vi.mocked(runtime.error).mockClear(); + vi.mocked(runtime.exit).mockClear(); +}); + +afterEach(() => { + envSnapshot.restore(); +}); + const makeDeps = (overrides: Partial = {}): CliDeps => ({ sendMessageWhatsApp: vi.fn(), sendMessageTelegram: vi.fn(), @@ -113,114 +121,6 @@ const makeDeps = (overrides: Partial = {}): CliDeps => ({ ...overrides, }); -const createStubPlugin = (params: { - id: ChannelPlugin["id"]; - label?: string; - actions?: ChannelMessageActionAdapter; - outbound?: ChannelOutboundAdapter; -}): ChannelPlugin => ({ - id: params.id, - meta: { - id: params.id, - label: params.label ?? String(params.id), - selectionLabel: params.label ?? String(params.id), - docsPath: `/channels/${params.id}`, - blurb: "test stub.", - }, - capabilities: { chatTypes: ["direct"] }, - config: { - listAccountIds: () => ["default"], - resolveAccount: () => ({}), - isConfigured: async () => true, - }, - actions: params.actions, - outbound: params.outbound, -}); - -type ChannelActionParams = Parameters< - NonNullable["handleAction"]> ->[0]; - -const createDiscordPollPluginRegistration = () => ({ - pluginId: "discord", - source: "test", - plugin: createStubPlugin({ - id: "discord", - label: "Discord", - actions: { - describeMessageTool: () => ({ actions: ["poll"] }), - handleAction: (async ({ action, params, cfg, accountId }: ChannelActionParams) => { - return await handleDiscordAction( - { action, to: params.to, accountId: accountId ?? undefined }, - cfg, - ); - }) as unknown as NonNullable["handleAction"], - }, - }), -}); - -const createTelegramSendPluginRegistration = () => ({ - pluginId: "telegram", - source: "test", - plugin: createStubPlugin({ - id: "telegram", - label: "Telegram", - actions: { - describeMessageTool: () => ({ actions: ["send"] }), - handleAction: (async ({ - action, - params, - cfg, - accountId, - agentId, - senderIsOwner, - }: ChannelActionParams) => { - return await handleTelegramAction( - { - action, - to: params.to, - accountId: accountId ?? undefined, - agentId, - senderIsOwner, - }, - cfg, - ); - }) as unknown as NonNullable["handleAction"], - }, - }), -}); - -const createTelegramPollPluginRegistration = () => ({ - pluginId: "telegram", - source: "test", - plugin: createStubPlugin({ - id: "telegram", - label: "Telegram", - actions: { - describeMessageTool: () => ({ actions: ["poll"] }), - handleAction: (async ({ - action, - params, - cfg, - accountId, - agentId, - senderIsOwner, - }: ChannelActionParams) => { - return await handleTelegramAction( - { - action, - to: params.to, - accountId: accountId ?? undefined, - agentId, - senderIsOwner, - }, - cfg, - ); - }) as unknown as NonNullable["handleAction"], - }, - }), -}); - function createTelegramSecretRawConfig() { return { channels: { @@ -254,78 +154,61 @@ function mockResolvedCommandConfig(params: { }); } -async function runTelegramDirectOutboundSend(params: { - rawConfig: Record; - resolvedConfig: Record; - diagnostics?: string[]; -}) { - mockResolvedCommandConfig(params); - const sendText = vi.fn(async (_ctx: { cfg?: unknown; to?: string; text?: string }) => ({ - channel: "telegram" as const, - messageId: "msg-1", - chatId: "123456", - })); - const sendMedia = vi.fn(async (_ctx: { cfg?: unknown }) => ({ - channel: "telegram" as const, - messageId: "msg-2", - chatId: "123456", - })); - setActivePluginRegistry( - createTestRegistry([ - { - pluginId: "telegram", - source: "test", - plugin: createStubPlugin({ - id: "telegram", - label: "Telegram", - outbound: { - deliveryMode: "direct", - sendText, - sendMedia, - }, - }), - }, - ]), - ); - - const deps = makeDeps(); +async function runMessageCommand(opts: Record = {}) { await messageCommand( { action: "send", channel: "telegram", target: "123456", message: "hi", + json: true, + ...opts, }, - deps, + makeDeps(), runtime, ); - - return { sendText }; } describe("messageCommand", () => { - it("threads resolved SecretRef config into outbound adapter sends", async () => { + it("threads resolved SecretRef config into message actions", async () => { const rawConfig = createTelegramSecretRawConfig(); const resolvedConfig = createTelegramResolvedTokenConfig("12345:resolved-token"); - const { sendText } = await runTelegramDirectOutboundSend({ + mockResolvedCommandConfig({ rawConfig: rawConfig as unknown as Record, resolvedConfig: resolvedConfig as unknown as Record, }); - expect(sendText).toHaveBeenCalledWith( + await runMessageCommand(); + + expect(runMessageActionMock).toHaveBeenCalledWith( expect.objectContaining({ cfg: resolvedConfig, - to: "123456", - text: "hi", + action: "send", + params: expect.objectContaining({ + channel: "telegram", + target: "123456", + message: "hi", + }), + agentId: "main", + senderIsOwner: true, + gateway: expect.objectContaining({ + clientName: "cli", + mode: "cli", + }), }), ); - expect(sendText.mock.calls[0]?.[0]?.cfg).not.toBe(rawConfig); + expect(runMessageActionMock.mock.calls[0]?.[0]?.cfg).not.toBe(rawConfig); expect(resolveCommandConfigWithSecrets).toHaveBeenCalledWith( expect.objectContaining({ config: rawConfig, commandName: "message", }), ); + expect(getScopedChannelsCommandSecretTargets).toHaveBeenCalledWith({ + config: rawConfig, + channel: "telegram", + accountId: undefined, + }); const call = resolveCommandConfigWithSecrets.mock.calls[0]?.[0] as { targetIds?: Set; }; @@ -335,7 +218,7 @@ describe("messageCommand", () => { ); }); - it("keeps local-fallback resolved cfg in outbound adapter sends", async () => { + it("keeps local-fallback resolved cfg and logs diagnostics", async () => { const rawConfig = { channels: { telegram: { @@ -343,63 +226,27 @@ describe("messageCommand", () => { }, }, }; - const locallyResolvedConfig = { - channels: { - telegram: { - token: "12345:local-fallback-token", - }, - }, - }; - const { sendText } = await runTelegramDirectOutboundSend({ + const locallyResolvedConfig = createTelegramResolvedTokenConfig("12345:local-fallback-token"); + mockResolvedCommandConfig({ rawConfig: rawConfig as unknown as Record, resolvedConfig: locallyResolvedConfig as unknown as Record, diagnostics: ["gateway secrets.resolve unavailable; used local resolver fallback."], }); - expect(sendText).toHaveBeenCalledWith( + await runMessageCommand(); + + expect(runMessageActionMock).toHaveBeenCalledWith( expect.objectContaining({ cfg: locallyResolvedConfig, }), ); - expect(sendText.mock.calls[0]?.[0]?.cfg).not.toBe(rawConfig); + expect(runMessageActionMock.mock.calls[0]?.[0]?.cfg).not.toBe(rawConfig); expect(runtime.log).toHaveBeenCalledWith( expect.stringContaining("[secrets] gateway secrets.resolve unavailable"), ); }); - it("defaults channel when only one configured", async () => { - process.env.TELEGRAM_BOT_TOKEN = "token-abc"; - testConfig = { - agents: { - list: [{ id: "alpha" }, { id: "ops", default: true }], - }, - }; - setActivePluginRegistry( - createTestRegistry([ - { - ...createTelegramSendPluginRegistration(), - }, - ]), - ); - const deps = makeDeps(); - await messageCommand( - { - target: "123456", - message: "hi", - }, - deps, - runtime, - ); - expect(handleTelegramAction).toHaveBeenCalledWith( - expect.objectContaining({ - agentId: "ops", - senderIsOwner: true, - }), - expect.any(Object), - ); - }); - - it("defaults channel from the auto-enabled config snapshot when only one channel becomes configured", async () => { + it("uses auto-enabled effective config for message actions", async () => { const rawConfig = {}; const resolvedConfig = {}; const autoEnabledConfig = { @@ -410,158 +257,48 @@ describe("messageCommand", () => { }, plugins: { allow: ["telegram"] }, }; - mockResolvedCommandConfig({ - rawConfig, - resolvedConfig, - diagnostics: [], - }); + mockResolvedCommandConfig({ rawConfig, resolvedConfig, diagnostics: [] }); applyPluginAutoEnable.mockReturnValue({ config: autoEnabledConfig, changes: [] }); - setActivePluginRegistry( - createTestRegistry([ - { - ...createTelegramSendPluginRegistration(), - }, - ]), - ); - const deps = makeDeps(); - await messageCommand( - { - target: "123456", - message: "hi", - }, - deps, - runtime, - ); + await runMessageCommand({ channel: undefined }); expect(applyPluginAutoEnable).toHaveBeenCalledWith({ config: resolvedConfig, env: process.env, }); - expect(handleTelegramAction).toHaveBeenCalledWith( + expect(runMessageActionMock).toHaveBeenCalledWith( expect.objectContaining({ - action: "send", - to: "123456", + cfg: autoEnabledConfig, + params: expect.objectContaining({ target: "123456" }), }), - autoEnabledConfig, ); }); - it("requires channel when multiple configured", async () => { - process.env.TELEGRAM_BOT_TOKEN = "token-abc"; - process.env.DISCORD_BOT_TOKEN = "token-discord"; - setActivePluginRegistry( - createTestRegistry([ - { - ...createTelegramSendPluginRegistration(), - }, - { - ...createDiscordPollPluginRegistration(), - }, - ]), - ); - const deps = makeDeps(); - await expect( - messageCommand( - { - target: "123", - message: "hi", - }, - deps, - runtime, - ), - ).rejects.toThrow(/Channel is required/); - }); + it("normalizes poll actions and sender ownership before dispatch", async () => { + await runMessageCommand({ + action: "poll", + channel: "telegram", + target: "123456789", + pollQuestion: "Ship it?", + pollOption: ["Yes", "No"], + senderIsOwner: false, + }); - it("sends via gateway for WhatsApp", async () => { - callGatewayMock.mockResolvedValueOnce({ messageId: "g1" }); - setActivePluginRegistry( - createTestRegistry([ - { - pluginId: "whatsapp", - source: "test", - plugin: createStubPlugin({ - id: "whatsapp", - label: "WhatsApp", - outbound: { - deliveryMode: "gateway", - }, - }), - }, - ]), - ); - const deps = makeDeps(); - await messageCommand( - { - action: "send", - channel: "whatsapp", - target: "+15551234567", - message: "hi", - }, - deps, - runtime, - ); - expect(callGatewayMock).toHaveBeenCalled(); - }); - - it("routes discord polls through message action", async () => { - setActivePluginRegistry( - createTestRegistry([ - { - ...createDiscordPollPluginRegistration(), - }, - ]), - ); - const deps = makeDeps(); - await messageCommand( - { - action: "poll", - channel: "discord", - target: "channel:123456789", - pollQuestion: "Snack?", - pollOption: ["Pizza", "Sushi"], - }, - deps, - runtime, - ); - expect(handleDiscordAction).toHaveBeenCalledWith( + expect(runMessageActionMock).toHaveBeenCalledWith( expect.objectContaining({ action: "poll", - to: "channel:123456789", - }), - expect.any(Object), - ); - }); - - it("routes telegram polls through message action", async () => { - setActivePluginRegistry( - createTestRegistry([ - { - ...createTelegramPollPluginRegistration(), - }, - ]), - ); - const deps = makeDeps(); - await messageCommand( - { - action: "poll", - channel: "telegram", - target: "123456789", - pollQuestion: "Ship it?", - pollOption: ["Yes", "No"], - pollDurationSeconds: 120, - senderIsOwner: false, - }, - deps, - runtime, - ); - expect(handleTelegramAction).toHaveBeenCalledWith( - expect.objectContaining({ - action: "poll", - to: "123456789", senderIsOwner: false, + params: expect.objectContaining({ + channel: "telegram", + target: "123456789", + pollQuestion: "Ship it?", + }), }), - expect.any(Object), ); }); + + it("rejects unknown message actions before dispatch", async () => { + await expect(runMessageCommand({ action: "nope" })).rejects.toThrow("Unknown message action"); + expect(runMessageActionMock).not.toHaveBeenCalled(); + }); }); diff --git a/src/commands/message.ts b/src/commands/message.ts index f7621872084..7730879e403 100644 --- a/src/commands/message.ts +++ b/src/commands/message.ts @@ -15,7 +15,16 @@ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, } from "../shared/string-coerce.js"; -import { buildMessageCliJson, formatMessageCliText } from "./message-format.js"; + +function buildMessageCliJson(result: Awaited>) { + return { + action: result.action, + channel: result.channel, + dryRun: result.dryRun, + handledBy: result.handledBy, + payload: result.payload, + }; +} export async function messageCommand( opts: Record, @@ -90,6 +99,7 @@ export async function messageCommand( return; } + const { formatMessageCliText } = await import("./message-format.js"); for (const line of formatMessageCliText(result)) { runtime.log(line); } diff --git a/src/commands/onboard-non-interactive.provider-auth.test.ts b/src/commands/onboard-non-interactive.provider-auth.test.ts index 24351d4f533..79dd9b505ab 100644 --- a/src/commands/onboard-non-interactive.provider-auth.test.ts +++ b/src/commands/onboard-non-interactive.provider-auth.test.ts @@ -946,32 +946,23 @@ describe("onboard (non-interactive): provider auth", () => { clearPluginManifestRegistryCache(); }); - it("stores MiniMax API keys for global and CN endpoint choices", async () => { - const scenarios = [ - { authChoice: "minimax-global-api", profileId: "minimax:global" }, - { authChoice: "minimax-cn-api", profileId: "minimax:cn" }, - ] as const; - + it("stores MiniMax API keys for the CN endpoint choice", async () => { await withOnboardEnv("openclaw-onboard-minimax-", async (env) => { - for (const scenario of scenarios) { - clearTestConfigFile(); - resetProviderAuthTestState(); - const cfg = await runOnboardingAndReadConfig(env, { - authChoice: scenario.authChoice, - minimaxApiKey: "sk-minimax-test", // pragma: allowlist secret - }); - expect(cfg.auth?.profiles?.[scenario.profileId]?.provider).toBe("minimax"); - expect(cfg.auth?.profiles?.[scenario.profileId]?.mode).toBe("api_key"); - await expectApiKeyProfile({ - profileId: scenario.profileId, - provider: "minimax", - key: "sk-minimax-test", - }); - } + const cfg = await runOnboardingAndReadConfig(env, { + authChoice: "minimax-cn-api", + minimaxApiKey: "sk-minimax-\r\ntest", // pragma: allowlist secret + }); + expect(cfg.auth?.profiles?.["minimax:cn"]?.provider).toBe("minimax"); + expect(cfg.auth?.profiles?.["minimax:cn"]?.mode).toBe("api_key"); + await expectApiKeyProfile({ + profileId: "minimax:cn", + provider: "minimax", + key: "sk-minimax-test", + }); }); }); - it("stores Z.AI API keys across global and coding endpoint choices", async () => { + it("stores Z.AI API keys across global and CN coding endpoint choices", async () => { const scenarios = [ { authChoice: "zai-api-key", @@ -989,13 +980,6 @@ describe("onboard (non-interactive): provider auth", () => { { url: `${ZAI_CODING_CN_BASE_URL}/chat/completions`, modelId: "glm-4.7" }, ], }, - { - authChoice: "zai-coding-global", - responses: { [`${ZAI_CODING_GLOBAL_BASE_URL}/chat/completions::glm-5.1`]: 200 }, - expectedCalls: [ - { url: `${ZAI_CODING_GLOBAL_BASE_URL}/chat/completions`, modelId: "glm-5.1" }, - ], - }, ] as const; await withOnboardEnv("openclaw-onboard-zai-", async (env) => { @@ -1021,63 +1005,23 @@ describe("onboard (non-interactive): provider auth", () => { }); }); - it("handles common provider API key onboarding choices", async () => { - const scenarios: Array<{ - options: Record; - profileId?: string; - provider?: string; - key?: string; - expectedModel?: string; - expectedBaseUrl?: string; - }> = [ - { - options: { - authChoice: "xai-api-key", - xaiApiKey: "xai-test-\r\nkey", - }, - profileId: "xai:default", - provider: "xai", - key: "xai-test-key", - expectedModel: "xai/grok-4", - }, - { - options: { - modelstudioApiKey: "modelstudio-test-key", // pragma: allowlist secret - }, + it("handles Qwen API key onboarding from inferred flags", async () => { + await withOnboardEnv("openclaw-onboard-provider-api-keys-", async (env) => { + const cfg = await runOnboardingAndReadConfig(env, { + modelstudioApiKey: "modelstudio-test-key", // pragma: allowlist secret + }); + + expect(cfg.auth?.profiles?.["qwen:default"]?.provider).toBe("qwen"); + expect(cfg.auth?.profiles?.["qwen:default"]?.mode).toBe("api_key"); + expect(cfg.agents?.defaults?.model?.primary).toBe("qwen/qwen3.5-plus"); + expect(cfg.models?.providers?.qwen?.baseUrl).toBe( + "https://coding-intl.dashscope.aliyuncs.com/v1", + ); + await expectApiKeyProfile({ profileId: "qwen:default", provider: "qwen", key: "modelstudio-test-key", - expectedModel: "qwen/qwen3.5-plus", - expectedBaseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", - }, - ]; - - await withOnboardEnv("openclaw-onboard-provider-api-keys-", async (env) => { - for (const scenario of scenarios) { - clearTestConfigFile(); - resetProviderAuthTestState(); - const cfg = await runOnboardingAndReadConfig(env, scenario.options); - - if (scenario.profileId && scenario.provider) { - expect(cfg.auth?.profiles?.[scenario.profileId]?.provider).toBe(scenario.provider); - expect(cfg.auth?.profiles?.[scenario.profileId]?.mode).toBe("api_key"); - } - if (scenario.expectedModel) { - expect(cfg.agents?.defaults?.model?.primary).toBe(scenario.expectedModel); - } - if (scenario.expectedBaseUrl) { - expect(cfg.models?.providers?.[scenario.provider ?? ""]?.baseUrl).toBe( - scenario.expectedBaseUrl, - ); - } - if (scenario.profileId && scenario.provider && scenario.key) { - await expectApiKeyProfile({ - profileId: scenario.profileId, - provider: scenario.provider, - key: scenario.key, - }); - } - } + }); }); }); @@ -1104,53 +1048,6 @@ describe("onboard (non-interactive): provider auth", () => { }); }); - it("fails fast when ref mode receives explicit provider keys without env and does not leak keys", async () => { - const scenarios = [ - { - name: "openai", - authChoice: "openai-api-key", - optionKey: "openaiApiKey", - flagName: "--openai-api-key", - envVar: "OPENAI_API_KEY", - }, - ] as const; - - await withOnboardEnv("openclaw-onboard-ref-flag-", async () => { - for (const { authChoice, optionKey, flagName, envVar } of scenarios) { - resetProviderAuthTestState(); - const runtime = createThrowingRuntime(); - const providedSecret = `${envVar.toLowerCase()}-should-not-leak`; // pragma: allowlist secret - const options: Record = { - authChoice, - secretInputMode: "ref", // pragma: allowlist secret - [optionKey]: providedSecret, - skipSkills: true, - }; - const envOverrides: Record = { - [envVar]: undefined, - }; - - await withEnvAsync(envOverrides, async () => { - let thrown: Error | undefined; - try { - await runNonInteractiveSetupWithDefaults(runtime, options); - } catch (error) { - thrown = error as Error; - } - expect(thrown).toBeDefined(); - const message = thrown?.message ?? ""; - expect(message).toContain( - `${flagName} cannot be used with --secret-input-mode ref unless ${envVar} is set in env.`, - ); - expect(message).toContain( - `Set ${envVar} in env and omit ${flagName}, or use --secret-input-mode plaintext.`, - ); - expect(message).not.toContain(providedSecret); - }); - } - }); - }); - it("stores the detected env alias as keyRef for both OpenCode runtime providers", async () => { await withOnboardEnv("openclaw-onboard-ref-opencode-alias-", async ({ runtime }) => { await withEnvAsync( @@ -1183,51 +1080,23 @@ describe("onboard (non-interactive): provider auth", () => { }); }); - 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, - }, - 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; - + it("configures custom providers from explicit non-interactive flags", async () => { await withOnboardEnv("openclaw-onboard-custom-provider-", async ({ runtime }) => { - for (const scenario of scenarios) { - clearTestConfigFile(); - resetProviderAuthTestState(); - await runNonInteractiveSetupWithDefaults(runtime, scenario.options); - const cfg = readTestConfig(); - 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); - } + 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 = readTestConfig(); + 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"); }); }); }); diff --git a/src/commands/onboard-non-interactive/local/auth-choice-inference.test.ts b/src/commands/onboard-non-interactive/local/auth-choice-inference.test.ts index 79ae35a5341..42adf8fb0fa 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice-inference.test.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice-inference.test.ts @@ -46,4 +46,23 @@ describe("inferAuthChoiceFromFlags", () => { ], }); }); + + it("infers the built-in custom provider from custom flags", () => { + const opts: OnboardOptions = { + customBaseUrl: "https://models.custom.local/v1", + customModelId: "local-large", + customApiKey: "custom-test-key", // pragma: allowlist secret + }; + + expect(inferAuthChoiceFromFlags(opts)).toEqual({ + choice: "custom-api-key", + matches: [ + { + optionKey: "customBaseUrl", + authChoice: "custom-api-key", + label: "--custom-base-url/--custom-model-id/--custom-api-key", + }, + ], + }); + }); });