test: split extension-owned core coverage

This commit is contained in:
Peter Steinberger 2026-03-27 16:06:20 +00:00
parent 6ade9c474c
commit cd92549119
14 changed files with 1255 additions and 131 deletions

View File

@ -0,0 +1,90 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../../../src/config/config.js";
const runtimeModule = await import("./runtime.js");
const handleDiscordActionMock = vi
.spyOn(runtimeModule, "handleDiscordAction")
.mockResolvedValue({ content: [], details: { ok: true } });
const { handleDiscordMessageAction } = await import("./handle-action.js");
describe("handleDiscordMessageAction", () => {
beforeEach(() => {
handleDiscordActionMock.mockClear();
});
it("uses trusted requesterSenderId for moderation and ignores params senderUserId", async () => {
await handleDiscordMessageAction({
action: "timeout",
params: {
guildId: "guild-1",
userId: "user-2",
durationMin: 5,
senderUserId: "spoofed-admin-id",
},
cfg: {
channels: { discord: { token: "tok", actions: { moderation: true } } },
} as OpenClawConfig,
requesterSenderId: "trusted-sender-id",
toolContext: { currentChannelProvider: "discord" },
});
expect(handleDiscordActionMock).toHaveBeenCalledWith(
expect.objectContaining({
action: "timeout",
guildId: "guild-1",
userId: "user-2",
durationMinutes: 5,
senderUserId: "trusted-sender-id",
}),
expect.objectContaining({
channels: {
discord: expect.objectContaining({
token: "tok",
}),
},
}),
);
});
it("falls back to toolContext.currentMessageId for reactions", async () => {
await handleDiscordMessageAction({
action: "react",
params: {
channelId: "123",
emoji: "ok",
},
cfg: {
channels: { discord: { token: "tok" } },
} as OpenClawConfig,
toolContext: { currentMessageId: "9001" },
});
expect(handleDiscordActionMock).toHaveBeenCalledWith(
expect.objectContaining({
action: "react",
channelId: "123",
messageId: "9001",
emoji: "ok",
}),
expect.any(Object),
expect.any(Object),
);
});
it("rejects reactions when no message id source is available", async () => {
await expect(
handleDiscordMessageAction({
action: "react",
params: {
channelId: "123",
emoji: "ok",
},
cfg: {
channels: { discord: { token: "tok" } },
} as OpenClawConfig,
}),
).rejects.toThrow(/messageId required/i);
expect(handleDiscordActionMock).not.toHaveBeenCalled();
});
});

View File

@ -0,0 +1,171 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { beforeEach, describe, expect, it, vi } from "vitest";
const sendReactionsModule = await import("./send-reactions.js");
const sendReactionSignalMock = vi
.spyOn(sendReactionsModule, "sendReactionSignal")
.mockResolvedValue({ ok: true });
const removeReactionSignalMock = vi
.spyOn(sendReactionsModule, "removeReactionSignal")
.mockResolvedValue({ ok: true });
const { signalMessageActions } = await import("./message-actions.js");
function createSignalAccountOverrideCfg(): OpenClawConfig {
return {
channels: {
signal: {
actions: { reactions: false },
accounts: {
work: { account: "+15550001111", actions: { reactions: true } },
},
},
},
} as OpenClawConfig;
}
describe("signalMessageActions", () => {
beforeEach(() => {
sendReactionSignalMock.mockClear();
removeReactionSignalMock.mockClear();
});
it("lists actions based on configured accounts and reaction gates", () => {
expect(
signalMessageActions.describeMessageTool?.({ cfg: {} as OpenClawConfig })?.actions ?? [],
).toEqual([]);
expect(
signalMessageActions.describeMessageTool?.({
cfg: {
channels: { signal: { account: "+15550001111", actions: { reactions: false } } },
} as OpenClawConfig,
})?.actions,
).toEqual(["send"]);
expect(
signalMessageActions.describeMessageTool?.({ cfg: createSignalAccountOverrideCfg() })
?.actions,
).toEqual(["send", "react"]);
});
it("skips send for plugin dispatch", () => {
expect(signalMessageActions.supportsAction?.({ action: "send" })).toBe(false);
expect(signalMessageActions.supportsAction?.({ action: "react" })).toBe(true);
});
it("blocks reactions when the action gate is disabled", async () => {
const cfg = {
channels: { signal: { account: "+15550001111", actions: { reactions: false } } },
} as OpenClawConfig;
await expect(
signalMessageActions.handleAction?.({
channel: "signal",
action: "react",
params: { to: "+15550001111", messageId: "123", emoji: "✅" },
cfg,
}),
).rejects.toThrow(/actions\.reactions/);
});
it("maps reaction targets into sendReactionSignal calls", async () => {
const cases = [
{
name: "uses account-level actions when enabled",
cfg: createSignalAccountOverrideCfg(),
accountId: "work",
params: { to: "+15550001111", messageId: "123", emoji: "👍" },
expectedRecipient: "+15550001111",
expectedTimestamp: 123,
expectedEmoji: "👍",
expectedOptions: { accountId: "work" },
},
{
name: "normalizes uuid recipients",
cfg: { channels: { signal: { account: "+15550001111" } } } as OpenClawConfig,
params: {
recipient: "uuid:123e4567-e89b-12d3-a456-426614174000",
messageId: "123",
emoji: "🔥",
},
expectedRecipient: "123e4567-e89b-12d3-a456-426614174000",
expectedTimestamp: 123,
expectedEmoji: "🔥",
expectedOptions: {},
},
{
name: "passes groupId and targetAuthor for group reactions",
cfg: { channels: { signal: { account: "+15550001111" } } } as OpenClawConfig,
params: {
to: "signal:group:group-id",
targetAuthor: "uuid:123e4567-e89b-12d3-a456-426614174000",
messageId: "123",
emoji: "✅",
},
expectedRecipient: "",
expectedTimestamp: 123,
expectedEmoji: "✅",
expectedOptions: {
groupId: "group-id",
targetAuthor: "uuid:123e4567-e89b-12d3-a456-426614174000",
},
},
{
name: "falls back to toolContext.currentMessageId when messageId is omitted",
cfg: { channels: { signal: { account: "+15550001111" } } } as OpenClawConfig,
params: { to: "+15559999999", emoji: "🔥" },
expectedRecipient: "+15559999999",
expectedTimestamp: 1737630212345,
expectedEmoji: "🔥",
expectedOptions: {},
toolContext: { currentMessageId: "1737630212345" },
},
] as const;
for (const testCase of cases) {
sendReactionSignalMock.mockClear();
await signalMessageActions.handleAction?.({
channel: "signal",
action: "react",
params: testCase.params,
cfg: testCase.cfg,
accountId: "accountId" in testCase ? testCase.accountId : undefined,
toolContext: "toolContext" in testCase ? testCase.toolContext : undefined,
});
expect(sendReactionSignalMock, testCase.name).toHaveBeenCalledWith(
testCase.expectedRecipient,
testCase.expectedTimestamp,
testCase.expectedEmoji,
expect.objectContaining({
cfg: testCase.cfg,
...testCase.expectedOptions,
}),
);
}
});
it("rejects invalid reaction inputs before dispatch", async () => {
const cfg = {
channels: { signal: { account: "+15550001111" } },
} as OpenClawConfig;
await expect(
signalMessageActions.handleAction?.({
channel: "signal",
action: "react",
params: { to: "+15559999999", emoji: "✅" },
cfg,
}),
).rejects.toThrow(/messageId.*required/);
await expect(
signalMessageActions.handleAction?.({
channel: "signal",
action: "react",
params: { to: "signal:group:group-id", messageId: "123", emoji: "✅" },
cfg,
}),
).rejects.toThrow(/targetAuthor/);
});
});

View File

@ -1,4 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../../src/config/config.js";
import { telegramMessageActions, telegramMessageActionRuntime } from "./channel-actions.js";
const handleTelegramActionMock = vi.hoisted(() => vi.fn());
@ -58,4 +59,211 @@ describe("telegramMessageActions", () => {
}),
);
});
it("computes poll/topic action availability from config gates", () => {
const cases = [
{
name: "configured telegram enables poll",
cfg: { channels: { telegram: { botToken: "tok" } } } as OpenClawConfig,
expectPoll: true,
expectTopicEdit: true,
},
{
name: "sendMessage disabled hides poll",
cfg: {
channels: {
telegram: {
botToken: "tok",
actions: { sendMessage: false },
},
},
} as OpenClawConfig,
expectPoll: false,
expectTopicEdit: true,
},
{
name: "poll gate disabled hides poll",
cfg: {
channels: {
telegram: {
botToken: "tok",
actions: { poll: false },
},
},
} as OpenClawConfig,
expectPoll: false,
expectTopicEdit: true,
},
{
name: "split account gates do not expose poll",
cfg: {
channels: {
telegram: {
accounts: {
senderOnly: {
botToken: "tok-send",
actions: {
sendMessage: true,
poll: false,
},
},
pollOnly: {
botToken: "tok-poll",
actions: {
sendMessage: false,
poll: true,
},
},
},
},
},
} as OpenClawConfig,
expectPoll: false,
expectTopicEdit: true,
},
] as const;
for (const testCase of cases) {
const actions =
telegramMessageActions.describeMessageTool?.({
cfg: testCase.cfg,
})?.actions ?? [];
if (testCase.expectPoll) {
expect(actions, testCase.name).toContain("poll");
} else {
expect(actions, testCase.name).not.toContain("poll");
}
if (testCase.expectTopicEdit) {
expect(actions, testCase.name).toContain("topic-edit");
} else {
expect(actions, testCase.name).not.toContain("topic-edit");
}
}
});
it("lists sticker actions only when enabled by config", () => {
const cases = [
{
name: "default config",
cfg: { channels: { telegram: { botToken: "tok" } } } as OpenClawConfig,
expectSticker: false,
},
{
name: "per-account sticker enabled",
cfg: {
channels: {
telegram: {
accounts: {
media: { botToken: "tok", actions: { sticker: true } },
},
},
},
} as OpenClawConfig,
expectSticker: true,
},
{
name: "all accounts omit sticker",
cfg: {
channels: {
telegram: {
accounts: {
a: { botToken: "tok1" },
b: { botToken: "tok2" },
},
},
},
} as OpenClawConfig,
expectSticker: false,
},
] as const;
for (const testCase of cases) {
const actions =
telegramMessageActions.describeMessageTool?.({
cfg: testCase.cfg,
})?.actions ?? [];
if (testCase.expectSticker) {
expect(actions, testCase.name).toEqual(
expect.arrayContaining(["sticker", "sticker-search"]),
);
} else {
expect(actions, testCase.name).not.toContain("sticker");
expect(actions, testCase.name).not.toContain("sticker-search");
}
}
});
it("normalizes reaction message identifiers before dispatch", async () => {
const cfg = { channels: { telegram: { botToken: "tok" } } } as OpenClawConfig;
const cases = [
{
name: "numeric channelId/messageId",
params: {
channelId: 123,
messageId: 456,
emoji: "ok",
},
expectedChannelField: "channelId",
expectedChannelValue: "123",
expectedMessageId: "456",
},
{
name: "snake_case message_id",
params: {
channelId: 123,
message_id: "456",
emoji: "ok",
},
expectedChannelField: "channelId",
expectedChannelValue: "123",
expectedMessageId: "456",
},
{
name: "toolContext fallback",
params: {
chatId: "123",
emoji: "ok",
},
toolContext: { currentMessageId: "9001" },
expectedChannelField: "chatId",
expectedChannelValue: "123",
expectedMessageId: "9001",
},
{
name: "missing messageId soft-falls through",
params: {
chatId: "123",
emoji: "ok",
},
expectedChannelField: "chatId",
expectedChannelValue: "123",
expectedMessageId: undefined,
},
] as const;
for (const testCase of cases) {
handleTelegramActionMock.mockClear();
await telegramMessageActions.handleAction?.({
channel: "telegram",
action: "react",
params: testCase.params,
cfg,
toolContext: "toolContext" in testCase ? testCase.toolContext : undefined,
});
const call = handleTelegramActionMock.mock.calls[0]?.[0] as
| Record<string, unknown>
| undefined;
expect(call, testCase.name).toBeDefined();
expect(call?.action, testCase.name).toBe("react");
expect(String(call?.[testCase.expectedChannelField]), testCase.name).toBe(
testCase.expectedChannelValue,
);
if (testCase.expectedMessageId === undefined) {
expect(call?.messageId, testCase.name).toBeUndefined();
} else {
expect(String(call?.messageId), testCase.name).toBe(testCase.expectedMessageId);
}
}
});
});

View File

@ -0,0 +1,238 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { Command } from "commander";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
let runtimeStub: {
config: { toNumber?: string };
manager: {
initiateCall: ReturnType<typeof vi.fn>;
continueCall: ReturnType<typeof vi.fn>;
speak: ReturnType<typeof vi.fn>;
endCall: ReturnType<typeof vi.fn>;
getCall: ReturnType<typeof vi.fn>;
getCallByProviderCallId: ReturnType<typeof vi.fn>;
};
stop: ReturnType<typeof vi.fn>;
};
vi.mock("./runtime-entry.js", () => ({
createVoiceCallRuntime: vi.fn(async () => runtimeStub),
}));
import plugin from "./index.js";
const noopLogger = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
};
type Registered = {
methods: Map<string, unknown>;
tools: unknown[];
};
type RegisterVoiceCall = (api: Record<string, unknown>) => void | Promise<void>;
type RegisterCliContext = {
program: Command;
config: Record<string, unknown>;
workspaceDir?: string;
logger: typeof noopLogger;
};
function captureStdout() {
let output = "";
const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(((chunk: unknown) => {
output += String(chunk);
return true;
}) as typeof process.stdout.write);
return {
output: () => output,
restore: () => writeSpy.mockRestore(),
};
}
function setup(config: Record<string, unknown>): Registered {
const methods = new Map<string, unknown>();
const tools: unknown[] = [];
void plugin.register({
id: "voice-call",
name: "Voice Call",
description: "test",
version: "0",
source: "test",
config: {},
pluginConfig: config,
runtime: { tts: { textToSpeechTelephony: vi.fn() } } as unknown as Parameters<
typeof plugin.register
>[0]["runtime"],
logger: noopLogger,
registerGatewayMethod: (method: string, handler: unknown) => methods.set(method, handler),
registerTool: (tool: unknown) => tools.push(tool),
registerCli: () => {},
registerService: () => {},
resolvePath: (p: string) => p,
} as unknown as Parameters<typeof plugin.register>[0]);
return { methods, tools };
}
async function registerVoiceCallCli(program: Command) {
const { register } = plugin as unknown as {
register: RegisterVoiceCall;
};
await register({
id: "voice-call",
name: "Voice Call",
description: "test",
version: "0",
source: "test",
config: {},
pluginConfig: { provider: "mock" },
runtime: { tts: { textToSpeechTelephony: vi.fn() } },
logger: noopLogger,
registerGatewayMethod: () => {},
registerTool: () => {},
registerCli: (fn: (ctx: RegisterCliContext) => void) =>
fn({
program,
config: {},
workspaceDir: undefined,
logger: noopLogger,
}),
registerService: () => {},
resolvePath: (p: string) => p,
});
}
describe("voice-call plugin", () => {
beforeEach(() => {
runtimeStub = {
config: { toNumber: "+15550001234" },
manager: {
initiateCall: vi.fn(async () => ({ callId: "call-1", success: true })),
continueCall: vi.fn(async () => ({
success: true,
transcript: "hello",
})),
speak: vi.fn(async () => ({ success: true })),
endCall: vi.fn(async () => ({ success: true })),
getCall: vi.fn((id: string) => (id === "call-1" ? { callId: "call-1" } : undefined)),
getCallByProviderCallId: vi.fn(() => undefined),
},
stop: vi.fn(async () => {}),
};
});
afterEach(() => vi.restoreAllMocks());
it("registers gateway methods", () => {
const { methods } = setup({ provider: "mock" });
expect(methods.has("voicecall.initiate")).toBe(true);
expect(methods.has("voicecall.continue")).toBe(true);
expect(methods.has("voicecall.speak")).toBe(true);
expect(methods.has("voicecall.end")).toBe(true);
expect(methods.has("voicecall.status")).toBe(true);
expect(methods.has("voicecall.start")).toBe(true);
});
it("initiates a call via voicecall.initiate", async () => {
const { methods } = setup({ provider: "mock" });
const handler = methods.get("voicecall.initiate") as
| ((ctx: {
params: Record<string, unknown>;
respond: ReturnType<typeof vi.fn>;
}) => Promise<void>)
| undefined;
const respond = vi.fn();
await handler?.({ params: { message: "Hi" }, respond });
expect(runtimeStub.manager.initiateCall).toHaveBeenCalled();
const [ok, payload] = respond.mock.calls[0];
expect(ok).toBe(true);
expect(payload.callId).toBe("call-1");
});
it("returns call status", async () => {
const { methods } = setup({ provider: "mock" });
const handler = methods.get("voicecall.status") as
| ((ctx: {
params: Record<string, unknown>;
respond: ReturnType<typeof vi.fn>;
}) => Promise<void>)
| undefined;
const respond = vi.fn();
await handler?.({ params: { callId: "call-1" }, respond });
const [ok, payload] = respond.mock.calls[0];
expect(ok).toBe(true);
expect(payload.found).toBe(true);
});
it("tool get_status returns json payload", async () => {
const { tools } = setup({ provider: "mock" });
const tool = tools[0] as {
execute: (id: string, params: unknown) => Promise<unknown>;
};
const result = (await tool.execute("id", {
action: "get_status",
callId: "call-1",
})) as { details: { found?: boolean } };
expect(result.details.found).toBe(true);
});
it("legacy tool status without sid returns error payload", async () => {
const { tools } = setup({ provider: "mock" });
const tool = tools[0] as {
execute: (id: string, params: unknown) => Promise<unknown>;
};
const result = (await tool.execute("id", { mode: "status" })) as {
details: { error?: unknown };
};
expect(String(result.details.error)).toContain("sid required");
});
it("CLI latency summarizes turn metrics from JSONL", async () => {
const program = new Command();
const tmpFile = path.join(os.tmpdir(), `voicecall-latency-${Date.now()}.jsonl`);
fs.writeFileSync(
tmpFile,
[
JSON.stringify({ metadata: { lastTurnLatencyMs: 100, lastTurnListenWaitMs: 70 } }),
JSON.stringify({ metadata: { lastTurnLatencyMs: 200, lastTurnListenWaitMs: 110 } }),
].join("\n") + "\n",
"utf8",
);
const stdout = captureStdout();
try {
await registerVoiceCallCli(program);
await program.parseAsync(["voicecall", "latency", "--file", tmpFile, "--last", "10"], {
from: "user",
});
const printed = stdout.output();
expect(printed).toContain('"recordsScanned": 2');
expect(printed).toContain('"p50Ms": 100');
expect(printed).toContain('"p95Ms": 200');
} finally {
stdout.restore();
fs.unlinkSync(tmpFile);
}
});
it("CLI start prints JSON", async () => {
const program = new Command();
const stdout = captureStdout();
await registerVoiceCallCli(program);
try {
await program.parseAsync(["voicecall", "start", "--to", "+1", "--message", "Hello"], {
from: "user",
});
expect(stdout.output()).toContain('"callId": "call-1"');
} finally {
stdout.restore();
}
});
});

View File

@ -1,10 +1,13 @@
import { afterEach, describe, expect, it } from "vitest";
import { slackPlugin } from "../../../extensions/slack/index.js";
import type { SubagentRunRecord } from "../../agents/subagent-registry.js";
import type { ChannelPlugin } from "../../channels/plugins/types.js";
import type { OpenClawConfig } from "../../config/config.js";
import { formatDurationCompact } from "../../infra/format-time/format-duration.js";
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../../plugins/runtime.js";
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
import {
createChannelTestPluginBase,
createTestRegistry,
} from "../../test-utils/channel-plugins.js";
import type { TemplateContext } from "../templating.js";
import { buildThreadingToolContext } from "./agent-runner-utils.js";
import { applyReplyThreading } from "./reply-payloads.js";
@ -15,6 +18,20 @@ import {
sortSubagentRuns,
} from "./subagents-utils.js";
function createSlackThreadingPlugin(): ChannelPlugin {
return {
...createChannelTestPluginBase({ id: "slack", label: "Slack" }),
threading: {
buildToolContext: ({ context }) => ({
currentChannelId: context.To?.replace(/^channel:/, ""),
currentThreadTs:
context.MessageThreadId != null ? String(context.MessageThreadId) : undefined,
replyToMode: "all",
}),
},
} as ChannelPlugin;
}
describe("buildThreadingToolContext", () => {
const cfg = {} as OpenClawConfig;
@ -157,7 +174,9 @@ describe("buildThreadingToolContext", () => {
it("uses Slack plugin threading context when the plugin registry is active", () => {
setActivePluginRegistry(
createTestRegistry([{ pluginId: "slack", plugin: slackPlugin, source: "test" }]),
createTestRegistry([
{ pluginId: "slack", plugin: createSlackThreadingPlugin(), source: "test" },
]),
);
const sessionCtx = {
Provider: "slack",
@ -244,7 +263,8 @@ describe("applyReplyThreading auto-threading", () => {
});
expect(result).toHaveLength(1);
expect(result[0].replyToId).toBe("42");
expect(result[0].replyToId).toBeUndefined();
expect(result[0].replyToTag).toBe(true);
});
it("keeps explicit tags for Telegram when off mode is enabled", () => {
@ -256,7 +276,7 @@ describe("applyReplyThreading auto-threading", () => {
});
expect(result).toHaveLength(1);
expect(result[0].replyToId).toBe("42");
expect(result[0].replyToId).toBeUndefined();
expect(result[0].replyToTag).toBe(true);
});

View File

@ -1,39 +1,18 @@
import fs from "node:fs/promises";
import type { OAuthCredentials } from "@mariozechner/pi-ai";
import { afterEach, describe, expect, it, vi } from "vitest";
import anthropicPlugin from "../../extensions/anthropic/index.js";
import chutesPlugin from "../../extensions/chutes/index.js";
import cloudflareAiGatewayPlugin from "../../extensions/cloudflare-ai-gateway/index.js";
import googlePlugin from "../../extensions/google/index.js";
import huggingfacePlugin from "../../extensions/huggingface/index.js";
import kimiCodingPlugin from "../../extensions/kimi-coding/index.js";
import litellmPlugin from "../../extensions/litellm/index.js";
import minimaxPlugin from "../../extensions/minimax/index.js";
import mistralPlugin from "../../extensions/mistral/index.js";
import moonshotPlugin from "../../extensions/moonshot/index.js";
import ollamaPlugin from "../../extensions/ollama/index.js";
import openAIPlugin from "../../extensions/openai/index.js";
import opencodeGoPlugin from "../../extensions/opencode-go/index.js";
import opencodePlugin from "../../extensions/opencode/index.js";
import openrouterPlugin from "../../extensions/openrouter/index.js";
import qianfanPlugin from "../../extensions/qianfan/index.js";
import syntheticPlugin from "../../extensions/synthetic/index.js";
import togetherPlugin from "../../extensions/together/index.js";
import venicePlugin from "../../extensions/venice/index.js";
import vercelAiGatewayPlugin from "../../extensions/vercel-ai-gateway/index.js";
import xaiPlugin from "../../extensions/xai/index.js";
import xiaomiPlugin from "../../extensions/xiaomi/index.js";
import { setDetectZaiEndpointForTesting } from "../../extensions/zai/detect.js";
import zaiPlugin from "../../extensions/zai/index.js";
import { resolveAgentDir } from "../agents/agent-scope.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
import type { ModelProviderConfig } from "../config/types.models.js";
import { createProviderApiKeyAuthMethod } from "../plugins/provider-api-key-auth.js";
import { providerApiKeyAuthRuntime } from "../plugins/provider-api-key-auth.runtime.js";
import {
MINIMAX_CN_API_BASE_URL,
ZAI_CODING_CN_BASE_URL,
ZAI_CODING_GLOBAL_BASE_URL,
} from "../plugins/provider-model-definitions.js";
import type { ProviderPlugin } from "../plugins/types.js";
import { registerProviderPlugins } from "../test-utils/plugin-registration.js";
import type { ProviderAuthMethod, ProviderPlugin } from "../plugins/types.js";
import type { WizardPrompter } from "../wizard/prompts.js";
import { applyAuthChoice, resolvePreferredProviderForAuthChoice } from "./auth-choice.js";
import { GOOGLE_GEMINI_DEFAULT_MODEL } from "./google-gemini-model-default.js";
@ -50,10 +29,6 @@ import {
type DetectZaiEndpoint = typeof import("./zai-endpoint-detect.js").detectZaiEndpoint;
vi.mock("../../extensions/github-copilot/login.js", () => ({
githubCopilotLoginCommand: vi.fn(async () => {}),
}));
const loginOpenAICodexOAuth = vi.hoisted(() =>
vi.fn<() => Promise<OAuthCredentials | null>>(async () => null),
);
@ -87,32 +62,512 @@ type StoredAuthProfile = {
metadata?: Record<string, string>;
};
function normalizeText(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
function providerConfigPatch(
providerId: string,
patch: Record<string, unknown>,
): Partial<OpenClawConfig> {
const providers: Record<string, ModelProviderConfig> = {
[providerId]: patch as ModelProviderConfig,
};
return {
models: {
providers,
},
};
}
function createApiKeyProvider(params: {
providerId: string;
label: string;
choiceId: string;
optionKey: string;
flagName: `--${string}`;
envVar: string;
promptMessage: string;
defaultModel?: string;
profileId?: string;
profileIds?: string[];
expectedProviders?: string[];
noteMessage?: string;
noteTitle?: string;
applyConfig?: Partial<OpenClawConfig>;
}): ProviderPlugin {
return {
id: params.providerId,
label: params.label,
auth: [
createProviderApiKeyAuthMethod({
providerId: params.providerId,
methodId: "api-key",
label: params.label,
optionKey: params.optionKey,
flagName: params.flagName,
envVar: params.envVar,
promptMessage: params.promptMessage,
...(params.profileId ? { profileId: params.profileId } : {}),
...(params.profileIds ? { profileIds: params.profileIds } : {}),
...(params.defaultModel ? { defaultModel: params.defaultModel } : {}),
...(params.expectedProviders ? { expectedProviders: params.expectedProviders } : {}),
...(params.noteMessage ? { noteMessage: params.noteMessage } : {}),
...(params.noteTitle ? { noteTitle: params.noteTitle } : {}),
...(params.applyConfig ? { applyConfig: () => params.applyConfig as OpenClawConfig } : {}),
wizard: {
choiceId: params.choiceId,
choiceLabel: params.label,
groupId: params.providerId,
groupLabel: params.label,
},
}),
],
};
}
function createFixedChoiceProvider(params: {
providerId: string;
label: string;
choiceId: string;
method: ProviderAuthMethod;
}): ProviderPlugin {
return {
id: params.providerId,
label: params.label,
auth: [
{
...params.method,
wizard: {
choiceId: params.choiceId,
choiceLabel: params.label,
groupId: params.providerId,
groupLabel: params.label,
},
},
],
};
}
function createDefaultProviderPlugins() {
return registerProviderPlugins(
anthropicPlugin,
chutesPlugin,
cloudflareAiGatewayPlugin,
googlePlugin,
huggingfacePlugin,
kimiCodingPlugin,
litellmPlugin,
minimaxPlugin,
mistralPlugin,
moonshotPlugin,
ollamaPlugin,
openAIPlugin,
opencodeGoPlugin,
opencodePlugin,
openrouterPlugin,
qianfanPlugin,
syntheticPlugin,
togetherPlugin,
venicePlugin,
vercelAiGatewayPlugin,
xaiPlugin,
xiaomiPlugin,
zaiPlugin,
);
const buildApiKeyCredential = providerApiKeyAuthRuntime.buildApiKeyCredential;
const ensureApiKeyFromOptionEnvOrPrompt =
providerApiKeyAuthRuntime.ensureApiKeyFromOptionEnvOrPrompt;
const normalizeApiKeyInput = providerApiKeyAuthRuntime.normalizeApiKeyInput;
const validateApiKeyInput = providerApiKeyAuthRuntime.validateApiKeyInput;
const createZaiMethod = (choiceId: "zai-api-key" | "zai-coding-global"): ProviderAuthMethod => ({
id: choiceId === "zai-api-key" ? "api-key" : "coding-global",
label: "Z.AI API key",
kind: "api_key",
wizard: {
choiceId,
choiceLabel: "Z.AI API key",
groupId: "zai",
groupLabel: "Z.AI",
},
run: async (ctx) => {
const token = normalizeText(await ctx.prompter.text({ message: "Enter Z.AI API key" }));
const detectResult = await detectZaiEndpoint(
choiceId === "zai-coding-global"
? { apiKey: token, endpoint: "coding-global" }
: { apiKey: token },
);
let baseUrl = detectResult?.baseUrl;
let modelId = detectResult?.modelId;
if (!baseUrl || !modelId) {
if (choiceId === "zai-coding-global") {
baseUrl = ZAI_CODING_GLOBAL_BASE_URL;
modelId = "glm-5";
} else {
const endpoint = await ctx.prompter.select({
message: "Select Z.AI endpoint",
initialValue: "global",
options: [
{ label: "Global", value: "global" },
{ label: "Coding CN", value: "coding-cn" },
],
});
baseUrl = endpoint === "coding-cn" ? ZAI_CODING_CN_BASE_URL : ZAI_CODING_GLOBAL_BASE_URL;
modelId = "glm-5";
}
}
return {
profiles: [
{
profileId: "zai:default",
credential: buildApiKeyCredential("zai", token),
},
],
configPatch: providerConfigPatch("zai", { baseUrl }) as OpenClawConfig,
defaultModel: `zai/${modelId}`,
};
},
});
const cloudflareAiGatewayMethod: ProviderAuthMethod = {
id: "api-key",
label: "Cloudflare AI Gateway API key",
kind: "api_key",
wizard: {
choiceId: "cloudflare-ai-gateway-api-key",
choiceLabel: "Cloudflare AI Gateway API key",
groupId: "cloudflare-ai-gateway",
groupLabel: "Cloudflare AI Gateway",
},
run: async (ctx) => {
const opts = (ctx.opts ?? {}) as Record<string, unknown>;
const accountId =
normalizeText(opts.cloudflareAiGatewayAccountId) ||
normalizeText(await ctx.prompter.text({ message: "Enter Cloudflare account ID" }));
const gatewayId =
normalizeText(opts.cloudflareAiGatewayGatewayId) ||
normalizeText(await ctx.prompter.text({ message: "Enter Cloudflare gateway ID" }));
let capturedSecretInput = "";
let capturedMode: "plaintext" | "ref" | undefined;
await ensureApiKeyFromOptionEnvOrPrompt({
token:
normalizeText(opts.cloudflareAiGatewayApiKey) ||
normalizeText(ctx.opts?.token) ||
undefined,
tokenProvider: "cloudflare-ai-gateway",
secretInputMode:
ctx.allowSecretRefPrompt === false
? (ctx.secretInputMode ?? "plaintext")
: ctx.secretInputMode,
config: ctx.config,
expectedProviders: ["cloudflare-ai-gateway"],
provider: "cloudflare-ai-gateway",
envLabel: "CLOUDFLARE_AI_GATEWAY_API_KEY",
promptMessage: "Enter Cloudflare AI Gateway API key",
normalize: normalizeApiKeyInput,
validate: validateApiKeyInput,
prompter: ctx.prompter,
setCredential: async (apiKey, mode) => {
capturedSecretInput = typeof apiKey === "string" ? apiKey : "";
capturedMode = mode;
},
});
return {
profiles: [
{
profileId: "cloudflare-ai-gateway:default",
credential: buildApiKeyCredential(
"cloudflare-ai-gateway",
capturedSecretInput,
{ accountId, gatewayId },
capturedMode ? { secretInputMode: capturedMode } : undefined,
),
},
],
defaultModel: "cloudflare-ai-gateway/claude-sonnet-4-5",
};
},
};
const chutesOAuthMethod: ProviderAuthMethod = {
id: "oauth",
label: "Chutes OAuth",
kind: "device_code",
wizard: {
choiceId: "chutes",
choiceLabel: "Chutes",
groupId: "chutes",
groupLabel: "Chutes",
},
run: async (ctx) => {
const state = "state-test";
ctx.runtime.log(`Open this URL: https://api.chutes.ai/idp/authorize?state=${state}`);
const redirect = String(
await ctx.prompter.text({ message: "Paste the redirect URL or code" }),
);
const params = new URLSearchParams(redirect.startsWith("?") ? redirect.slice(1) : redirect);
const code = params.get("code") ?? redirect;
const tokenResponse = await fetch("https://api.chutes.ai/idp/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code, client_id: process.env.CHUTES_CLIENT_ID }),
});
const tokenJson = (await tokenResponse.json()) as {
access_token: string;
refresh_token: string;
expires_in: number;
};
const userResponse = await fetch("https://api.chutes.ai/idp/userinfo", {
headers: { Authorization: `Bearer ${tokenJson.access_token}` },
});
const userJson = (await userResponse.json()) as { username: string };
return {
profiles: [
{
profileId: `chutes:${userJson.username}`,
credential: {
type: "oauth",
provider: "chutes",
access: tokenJson.access_token,
refresh: tokenJson.refresh_token,
expires: Date.now() + tokenJson.expires_in * 1000,
email: userJson.username,
},
},
],
};
},
};
return [
createApiKeyProvider({
providerId: "anthropic",
label: "Anthropic API key",
choiceId: "apiKey",
optionKey: "anthropicApiKey",
flagName: "--anthropic-api-key",
envVar: "ANTHROPIC_API_KEY",
promptMessage: "Enter Anthropic API key",
}),
createApiKeyProvider({
providerId: "google",
label: "Gemini API key",
choiceId: "gemini-api-key",
optionKey: "geminiApiKey",
flagName: "--gemini-api-key",
envVar: "GEMINI_API_KEY",
promptMessage: "Enter Gemini API key",
defaultModel: GOOGLE_GEMINI_DEFAULT_MODEL,
}),
createApiKeyProvider({
providerId: "huggingface",
label: "Hugging Face API key",
choiceId: "huggingface-api-key",
optionKey: "huggingfaceApiKey",
flagName: "--huggingface-api-key",
envVar: "HUGGINGFACE_HUB_TOKEN",
promptMessage: "Enter Hugging Face API key",
defaultModel: "huggingface/Qwen/Qwen3-Coder-480B-A35B-Instruct",
}),
createApiKeyProvider({
providerId: "litellm",
label: "LiteLLM API key",
choiceId: "litellm-api-key",
optionKey: "litellmApiKey",
flagName: "--litellm-api-key",
envVar: "LITELLM_API_KEY",
promptMessage: "Enter LiteLLM API key",
defaultModel: "litellm/anthropic/claude-opus-4.6",
}),
createApiKeyProvider({
providerId: "minimax",
label: "MiniMax API key (Global)",
choiceId: "minimax-global-api",
optionKey: "minimaxApiKey",
flagName: "--minimax-api-key",
envVar: "MINIMAX_API_KEY",
promptMessage: "Enter MiniMax API key",
profileId: "minimax:global",
defaultModel: "minimax/MiniMax-M2.7",
}),
createApiKeyProvider({
providerId: "minimax",
label: "MiniMax API key (CN)",
choiceId: "minimax-cn-api",
optionKey: "minimaxApiKey",
flagName: "--minimax-api-key",
envVar: "MINIMAX_API_KEY",
promptMessage: "Enter MiniMax CN API key",
profileId: "minimax:cn",
defaultModel: "minimax/MiniMax-M2.7",
applyConfig: providerConfigPatch("minimax", { baseUrl: MINIMAX_CN_API_BASE_URL }),
expectedProviders: ["minimax", "minimax-cn"],
}),
createApiKeyProvider({
providerId: "mistral",
label: "Mistral API key",
choiceId: "mistral-api-key",
optionKey: "mistralApiKey",
flagName: "--mistral-api-key",
envVar: "MISTRAL_API_KEY",
promptMessage: "Enter Mistral API key",
defaultModel: "mistral/mistral-large-latest",
}),
createApiKeyProvider({
providerId: "moonshot",
label: "Moonshot API key",
choiceId: "moonshot-api-key",
optionKey: "moonshotApiKey",
flagName: "--moonshot-api-key",
envVar: "MOONSHOT_API_KEY",
promptMessage: "Enter Moonshot API key",
defaultModel: "moonshot/moonshot-v1-128k",
}),
createFixedChoiceProvider({
providerId: "ollama",
label: "Ollama",
choiceId: "ollama",
method: {
id: "local",
label: "Ollama",
kind: "custom",
run: async () => ({ profiles: [] }),
},
}),
createApiKeyProvider({
providerId: "openai",
label: "OpenAI API key",
choiceId: "openai-api-key",
optionKey: "openaiApiKey",
flagName: "--openai-api-key",
envVar: "OPENAI_API_KEY",
promptMessage: "Enter OpenAI API key",
defaultModel: "openai/gpt-5.4",
}),
createApiKeyProvider({
providerId: "opencode",
label: "OpenCode Zen",
choiceId: "opencode-zen",
optionKey: "opencodeZenApiKey",
flagName: "--opencode-zen-api-key",
envVar: "OPENCODE_API_KEY",
promptMessage: "Enter OpenCode API key",
profileIds: ["opencode:default", "opencode-go:default"],
defaultModel: "opencode/claude-opus-4-6",
expectedProviders: ["opencode", "opencode-go"],
noteMessage: "OpenCode uses one API key across the Zen and Go catalogs.",
noteTitle: "OpenCode",
}),
createApiKeyProvider({
providerId: "opencode-go",
label: "OpenCode Go",
choiceId: "opencode-go",
optionKey: "opencodeGoApiKey",
flagName: "--opencode-go-api-key",
envVar: "OPENCODE_API_KEY",
promptMessage: "Enter OpenCode API key",
profileIds: ["opencode-go:default", "opencode:default"],
defaultModel: "opencode-go/kimi-k2.5",
expectedProviders: ["opencode", "opencode-go"],
noteMessage: "OpenCode uses one API key across the Zen and Go catalogs.",
noteTitle: "OpenCode",
}),
createApiKeyProvider({
providerId: "openrouter",
label: "OpenRouter API key",
choiceId: "openrouter-api-key",
optionKey: "openrouterApiKey",
flagName: "--openrouter-api-key",
envVar: "OPENROUTER_API_KEY",
promptMessage: "Enter OpenRouter API key",
defaultModel: "openrouter/auto",
}),
createApiKeyProvider({
providerId: "qianfan",
label: "Qianfan API key",
choiceId: "qianfan-api-key",
optionKey: "qianfanApiKey",
flagName: "--qianfan-api-key",
envVar: "QIANFAN_API_KEY",
promptMessage: "Enter Qianfan API key",
defaultModel: "qianfan/ernie-4.5-8k",
}),
createApiKeyProvider({
providerId: "synthetic",
label: "Synthetic API key",
choiceId: "synthetic-api-key",
optionKey: "syntheticApiKey",
flagName: "--synthetic-api-key",
envVar: "SYNTHETIC_API_KEY",
promptMessage: "Enter Synthetic API key",
defaultModel: "synthetic/Synthetic-1",
}),
createApiKeyProvider({
providerId: "together",
label: "Together API key",
choiceId: "together-api-key",
optionKey: "togetherApiKey",
flagName: "--together-api-key",
envVar: "TOGETHER_API_KEY",
promptMessage: "Enter Together API key",
defaultModel: "together/meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
}),
createApiKeyProvider({
providerId: "venice",
label: "Venice AI",
choiceId: "venice-api-key",
optionKey: "veniceApiKey",
flagName: "--venice-api-key",
envVar: "VENICE_API_KEY",
promptMessage: "Enter Venice AI API key",
defaultModel: "venice/venice-uncensored",
noteMessage: "Venice is a privacy-focused inference service.",
noteTitle: "Venice AI",
}),
createApiKeyProvider({
providerId: "vercel-ai-gateway",
label: "AI Gateway API key",
choiceId: "ai-gateway-api-key",
optionKey: "aiGatewayApiKey",
flagName: "--ai-gateway-api-key",
envVar: "AI_GATEWAY_API_KEY",
promptMessage: "Enter AI Gateway API key",
defaultModel: "vercel-ai-gateway/anthropic/claude-opus-4.6",
}),
createApiKeyProvider({
providerId: "xai",
label: "xAI API key",
choiceId: "xai-api-key",
optionKey: "xaiApiKey",
flagName: "--xai-api-key",
envVar: "XAI_API_KEY",
promptMessage: "Enter xAI API key",
defaultModel: "xai/grok-4",
}),
createApiKeyProvider({
providerId: "xiaomi",
label: "Xiaomi API key",
choiceId: "xiaomi-api-key",
optionKey: "xiaomiApiKey",
flagName: "--xiaomi-api-key",
envVar: "XIAOMI_API_KEY",
promptMessage: "Enter Xiaomi API key",
defaultModel: "xiaomi/mimo-v2-flash",
}),
{
id: "zai",
label: "Z.AI",
auth: [createZaiMethod("zai-api-key"), createZaiMethod("zai-coding-global")],
},
{
id: "cloudflare-ai-gateway",
label: "Cloudflare AI Gateway",
auth: [cloudflareAiGatewayMethod],
},
{
id: "chutes",
label: "Chutes",
auth: [chutesOAuthMethod],
},
createApiKeyProvider({
providerId: "kimi",
label: "Kimi Code API key",
choiceId: "kimi-code-api-key",
optionKey: "kimiApiKey",
flagName: "--kimi-api-key",
envVar: "KIMI_API_KEY",
promptMessage: "Enter Kimi Code API key",
defaultModel: "kimi/kimi-k2.5",
expectedProviders: ["kimi", "kimi-code", "kimi-coding"],
}),
createFixedChoiceProvider({
providerId: "github-copilot",
label: "GitHub Copilot",
choiceId: "github-copilot",
method: {
id: "device",
label: "GitHub device login",
kind: "device_code",
run: async () => ({ profiles: [] }),
},
}),
];
}
describe("applyAuthChoice", () => {
@ -190,14 +645,12 @@ describe("applyAuthChoice", () => {
resolvePluginProviders.mockReturnValue(createDefaultProviderPlugins());
detectZaiEndpoint.mockReset();
detectZaiEndpoint.mockResolvedValue(null);
setDetectZaiEndpointForTesting(detectZaiEndpoint);
loginOpenAICodexOAuth.mockReset();
loginOpenAICodexOAuth.mockResolvedValue(null);
await lifecycle.cleanup();
activeStateDir = null;
});
setDetectZaiEndpointForTesting(detectZaiEndpoint);
resolvePluginProviders.mockReturnValue(createDefaultProviderPlugins());
it("does not throw when openai-codex oauth fails", async () => {

View File

@ -1,8 +1,8 @@
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { resolveMatrixAccountStorageRoot } from "../../extensions/matrix/runtime-api.js";
import { withTempHome } from "../../test/helpers/temp-home.js";
import { resolveMatrixAccountStorageRoot } from "../plugin-sdk/matrix.js";
import * as noteModule from "../terminal/note.js";
import { loadAndMaybeMigrateDoctorConfig } from "./doctor-config-flow.js";
import { runDoctorConfigWithInput } from "./doctor-config-flow.test-utils.js";

View File

@ -42,7 +42,7 @@ async function loadFreshHealthModulesForTest() {
recordSessionMetaFromInbound: vi.fn().mockResolvedValue(undefined),
updateLastRoute: vi.fn().mockResolvedValue(undefined),
}));
vi.doMock("../../extensions/whatsapp/runtime-api.js", () => ({
vi.doMock("../plugins/runtime/runtime-whatsapp-boundary.js", () => ({
webAuthExists: vi.fn(async () => true),
getWebAuthAgeMs: vi.fn(() => 1234),
readWebSelfId: vi.fn(() => ({ e164: null, jid: null })),

View File

@ -37,54 +37,13 @@ vi.mock("../gateway/call.js", () => ({
randomIdempotencyKey: () => "idem-1",
}));
const webAuthExists = vi.hoisted(() => vi.fn(async () => false));
vi.mock("../../extensions/whatsapp/runtime-api.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../extensions/whatsapp/runtime-api.js")>();
return {
...actual,
webAuthExists,
};
});
const handleDiscordAction = vi.hoisted(() =>
vi.fn(async (..._args: unknown[]) => ({ details: { ok: true } })),
);
vi.mock("../../extensions/discord/runtime-api.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../extensions/discord/runtime-api.js")>();
return {
...actual,
handleDiscordAction,
};
});
const handleSlackAction = vi.hoisted(() =>
vi.fn(async (..._args: unknown[]) => ({ details: { ok: true } })),
);
vi.mock("../../extensions/slack/runtime-api.js", () => ({
handleSlackAction,
}));
const handleTelegramAction = vi.hoisted(() =>
vi.fn(async (..._args: unknown[]) => ({ details: { ok: true } })),
);
vi.mock("../../extensions/telegram/test-api.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../extensions/telegram/test-api.js")>();
return {
...actual,
handleTelegramAction,
};
});
const handleWhatsAppAction = vi.hoisted(() =>
vi.fn(async (..._args: unknown[]) => ({ details: { ok: true } })),
);
vi.mock("../../extensions/whatsapp/runtime-api.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../extensions/whatsapp/runtime-api.js")>();
return {
...actual,
handleWhatsAppAction,
};
});
let messageCommand: typeof import("./message.js").messageCommand;
@ -103,11 +62,8 @@ beforeEach(() => {
testConfig = {};
setActivePluginRegistry(EMPTY_TEST_REGISTRY);
callGatewayMock.mockClear();
webAuthExists.mockClear().mockResolvedValue(false);
handleDiscordAction.mockClear();
handleSlackAction.mockClear();
handleTelegramAction.mockClear();
handleWhatsAppAction.mockClear();
resolveCommandSecretRefsViaGateway.mockClear();
});

View File

@ -316,7 +316,7 @@ vi.mock("../channels/plugins/index.js", () => ({
},
] as unknown,
}));
vi.mock("../../extensions/whatsapp/runtime-api.js", () => ({
vi.mock("../plugins/runtime/runtime-whatsapp-boundary.js", () => ({
webAuthExists: mocks.webAuthExists,
getWebAuthAgeMs: mocks.getWebAuthAgeMs,
readWebSelfId: mocks.readWebSelfId,
@ -404,6 +404,12 @@ vi.mock("../daemon/service.js", () => ({
sourcePath: "/tmp/Library/LaunchAgents/ai.openclaw.gateway.plist",
}),
}),
readGatewayServiceState: async () => ({
installed: true,
loaded: true,
running: true,
runtime: { status: "running", pid: 1234 },
}),
}));
vi.mock("../daemon/node-service.js", () => ({
resolveNodeService: () => ({

View File

@ -26,24 +26,6 @@ async function loadExecApprovalSurfaceModule() {
getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args),
listChannelPlugins: (...args: unknown[]) => listChannelPluginsMock(...args),
}));
vi.doMock("../../extensions/discord/index.js", () => ({
discordPlugin: {},
}));
vi.doMock("../../extensions/telegram/index.js", () => ({
telegramPlugin: {},
}));
vi.doMock("../../extensions/slack/index.js", () => ({
slackPlugin: {},
}));
vi.doMock("../../extensions/whatsapp/index.js", () => ({
whatsappPlugin: {},
}));
vi.doMock("../../extensions/signal/index.js", () => ({
signalPlugin: {},
}));
vi.doMock("../../extensions/imessage/index.js", () => ({
imessagePlugin: {},
}));
vi.doMock("../utils/message-channel.js", () => ({
INTERNAL_MESSAGE_CHANNEL: "web",
normalizeMessageChannel: (...args: unknown[]) => normalizeMessageChannelMock(...args),

View File

@ -1,12 +1,12 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import {
findMatrixAccountEntry,
getMatrixScopedEnvVarNames,
requiresExplicitMatrixDefaultAccount,
resolveConfiguredMatrixAccountIds,
resolveMatrixDefaultOrOnlyAccountId,
} from "../../extensions/matrix/runtime-api.js";
import type { OpenClawConfig } from "../config/config.js";
} from "../plugin-sdk/matrix.js";
describe("matrix account selection", () => {
it("resolves configured account ids from non-canonical account keys", () => {

View File

@ -1,9 +1,9 @@
import fs from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { resolveMatrixAccountStorageRoot } from "../../extensions/matrix/runtime-api.js";
import { withTempHome } from "../../test/helpers/temp-home.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveMatrixAccountStorageRoot } from "../plugin-sdk/matrix.js";
import { autoPrepareLegacyMatrixCrypto, detectLegacyMatrixCrypto } from "./matrix-legacy-crypto.js";
import { MATRIX_LEGACY_CRYPTO_INSPECTOR_UNAVAILABLE_MESSAGE } from "./matrix-plugin-helper.js";
import {

View File

@ -1,8 +1,8 @@
import fs from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { resolveMatrixAccountStorageRoot } from "../../extensions/matrix/runtime-api.js";
import { withTempHome } from "../../test/helpers/temp-home.js";
import { resolveMatrixAccountStorageRoot } from "../plugin-sdk/matrix.js";
import { detectLegacyMatrixCrypto } from "./matrix-legacy-crypto.js";
const createBackupArchiveMock = vi.hoisted(() => vi.fn());