mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:00:42 +00:00
test: tighten message and onboarding hotspots
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user