test: tighten message and onboarding hotspots

This commit is contained in:
Peter Steinberger
2026-04-17 09:00:59 +01:00
parent f7f88e52e4
commit d3b70f9823
4 changed files with 181 additions and 546 deletions

View File

@@ -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<string, unknown>;
};
let testConfig: Record<string, unknown> = {};
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<typeof captureEnv>;
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> = {}): CliDeps => ({
sendMessageWhatsApp: vi.fn(),
sendMessageTelegram: vi.fn(),
@@ -113,114 +121,6 @@ const makeDeps = (overrides: Partial<CliDeps> = {}): 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<NonNullable<ChannelPlugin["actions"]>["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<ChannelPlugin["actions"]>["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<ChannelPlugin["actions"]>["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<ChannelPlugin["actions"]>["handleAction"],
},
}),
});
function createTelegramSecretRawConfig() {
return {
channels: {
@@ -254,78 +154,61 @@ function mockResolvedCommandConfig(params: {
});
}
async function runTelegramDirectOutboundSend(params: {
rawConfig: Record<string, unknown>;
resolvedConfig: Record<string, unknown>;
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<string, unknown> = {}) {
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<string, unknown>,
resolvedConfig: resolvedConfig as unknown as Record<string, unknown>,
});
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<string>;
};
@@ -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<string, unknown>,
resolvedConfig: locallyResolvedConfig as unknown as Record<string, unknown>,
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();
});
});

View File

@@ -15,7 +15,16 @@ import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import { buildMessageCliJson, formatMessageCliText } from "./message-format.js";
function buildMessageCliJson(result: Awaited<ReturnType<typeof runMessageAction>>) {
return {
action: result.action,
channel: result.channel,
dryRun: result.dryRun,
handledBy: result.handledBy,
payload: result.payload,
};
}
export async function messageCommand(
opts: Record<string, unknown>,
@@ -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);
}

View File

@@ -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<string, unknown>;
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<string, unknown> = {
authChoice,
secretInputMode: "ref", // pragma: allowlist secret
[optionKey]: providedSecret,
skipSkills: true,
};
const envOverrides: Record<string, string | undefined> = {
[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<ProviderAuthConfigSnapshot>();
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<ProviderAuthConfigSnapshot>();
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");
});
});
});

View File

@@ -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",
},
],
});
});
});