test: add channel resolution helper coverage

This commit is contained in:
Peter Steinberger 2026-03-13 20:13:53 +00:00
parent c3fadff0ce
commit fdbfdec341
2 changed files with 352 additions and 0 deletions

View File

@ -0,0 +1,196 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const loadConfigMock = vi.hoisted(() => vi.fn());
const listEnabledDiscordAccountsMock = vi.hoisted(() => vi.fn());
const isDiscordExecApprovalClientEnabledMock = vi.hoisted(() => vi.fn());
const listEnabledTelegramAccountsMock = vi.hoisted(() => vi.fn());
const isTelegramExecApprovalClientEnabledMock = vi.hoisted(() => vi.fn());
const normalizeMessageChannelMock = vi.hoisted(() => vi.fn());
vi.mock("../config/config.js", () => ({
loadConfig: (...args: unknown[]) => loadConfigMock(...args),
}));
vi.mock("../discord/accounts.js", () => ({
listEnabledDiscordAccounts: (...args: unknown[]) => listEnabledDiscordAccountsMock(...args),
}));
vi.mock("../discord/exec-approvals.js", () => ({
isDiscordExecApprovalClientEnabled: (...args: unknown[]) =>
isDiscordExecApprovalClientEnabledMock(...args),
}));
vi.mock("../telegram/accounts.js", () => ({
listEnabledTelegramAccounts: (...args: unknown[]) => listEnabledTelegramAccountsMock(...args),
}));
vi.mock("../telegram/exec-approvals.js", () => ({
isTelegramExecApprovalClientEnabled: (...args: unknown[]) =>
isTelegramExecApprovalClientEnabledMock(...args),
}));
vi.mock("../utils/message-channel.js", () => ({
INTERNAL_MESSAGE_CHANNEL: "web",
normalizeMessageChannel: (...args: unknown[]) => normalizeMessageChannelMock(...args),
}));
import {
hasConfiguredExecApprovalDmRoute,
resolveExecApprovalInitiatingSurfaceState,
} from "./exec-approval-surface.js";
describe("resolveExecApprovalInitiatingSurfaceState", () => {
beforeEach(() => {
loadConfigMock.mockReset();
listEnabledDiscordAccountsMock.mockReset();
isDiscordExecApprovalClientEnabledMock.mockReset();
listEnabledTelegramAccountsMock.mockReset();
isTelegramExecApprovalClientEnabledMock.mockReset();
normalizeMessageChannelMock.mockReset();
normalizeMessageChannelMock.mockImplementation((value?: string | null) =>
typeof value === "string" ? value.trim().toLowerCase() : undefined,
);
});
it("treats web UI, terminal UI, and missing channels as enabled", () => {
expect(resolveExecApprovalInitiatingSurfaceState({ channel: null })).toEqual({
kind: "enabled",
channel: undefined,
channelLabel: "this platform",
});
expect(resolveExecApprovalInitiatingSurfaceState({ channel: "tui" })).toEqual({
kind: "enabled",
channel: "tui",
channelLabel: "terminal UI",
});
expect(resolveExecApprovalInitiatingSurfaceState({ channel: "web" })).toEqual({
kind: "enabled",
channel: "web",
channelLabel: "Web UI",
});
});
it("uses the provided cfg for telegram and discord client enablement", () => {
isTelegramExecApprovalClientEnabledMock.mockReturnValueOnce(true);
isDiscordExecApprovalClientEnabledMock.mockReturnValueOnce(false);
const cfg = { channels: {} };
expect(
resolveExecApprovalInitiatingSurfaceState({
channel: "telegram",
accountId: "main",
cfg: cfg as never,
}),
).toEqual({
kind: "enabled",
channel: "telegram",
channelLabel: "Telegram",
});
expect(
resolveExecApprovalInitiatingSurfaceState({
channel: "discord",
accountId: "main",
cfg: cfg as never,
}),
).toEqual({
kind: "disabled",
channel: "discord",
channelLabel: "Discord",
});
expect(loadConfigMock).not.toHaveBeenCalled();
});
it("loads config lazily when cfg is omitted and marks unsupported channels", () => {
loadConfigMock.mockReturnValueOnce({ loaded: true });
isTelegramExecApprovalClientEnabledMock.mockReturnValueOnce(false);
expect(
resolveExecApprovalInitiatingSurfaceState({
channel: "telegram",
accountId: "main",
}),
).toEqual({
kind: "disabled",
channel: "telegram",
channelLabel: "Telegram",
});
expect(loadConfigMock).toHaveBeenCalledOnce();
expect(resolveExecApprovalInitiatingSurfaceState({ channel: "signal" })).toEqual({
kind: "unsupported",
channel: "signal",
channelLabel: "Signal",
});
});
});
describe("hasConfiguredExecApprovalDmRoute", () => {
beforeEach(() => {
listEnabledDiscordAccountsMock.mockReset();
listEnabledTelegramAccountsMock.mockReset();
});
it("returns true when any enabled account routes approvals to DM or both", () => {
listEnabledDiscordAccountsMock.mockReturnValueOnce([
{
config: {
execApprovals: {
enabled: true,
approvers: ["a"],
target: "channel",
},
},
},
]);
listEnabledTelegramAccountsMock.mockReturnValueOnce([
{
config: {
execApprovals: {
enabled: true,
approvers: ["a"],
target: "both",
},
},
},
]);
expect(hasConfiguredExecApprovalDmRoute({} as never)).toBe(true);
});
it("returns false when exec approvals are disabled or have no DM route", () => {
listEnabledDiscordAccountsMock.mockReturnValueOnce([
{
config: {
execApprovals: {
enabled: false,
approvers: ["a"],
target: "dm",
},
},
},
]);
listEnabledTelegramAccountsMock.mockReturnValueOnce([
{
config: {
execApprovals: {
enabled: true,
approvers: [],
target: "dm",
},
},
},
{
config: {
execApprovals: {
enabled: true,
approvers: ["a"],
target: "channel",
},
},
},
]);
expect(hasConfiguredExecApprovalDmRoute({} as never)).toBe(false);
});
});

View File

@ -0,0 +1,156 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const resolveDefaultAgentIdMock = vi.hoisted(() => vi.fn());
const resolveAgentWorkspaceDirMock = vi.hoisted(() => vi.fn());
const getChannelPluginMock = vi.hoisted(() => vi.fn());
const applyPluginAutoEnableMock = vi.hoisted(() => vi.fn());
const loadOpenClawPluginsMock = vi.hoisted(() => vi.fn());
const getActivePluginRegistryMock = vi.hoisted(() => vi.fn());
const getActivePluginRegistryKeyMock = vi.hoisted(() => vi.fn());
const normalizeMessageChannelMock = vi.hoisted(() => vi.fn());
const isDeliverableMessageChannelMock = vi.hoisted(() => vi.fn());
vi.mock("../../agents/agent-scope.js", () => ({
resolveDefaultAgentId: (...args: unknown[]) => resolveDefaultAgentIdMock(...args),
resolveAgentWorkspaceDir: (...args: unknown[]) => resolveAgentWorkspaceDirMock(...args),
}));
vi.mock("../../channels/plugins/index.js", () => ({
getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args),
}));
vi.mock("../../config/plugin-auto-enable.js", () => ({
applyPluginAutoEnable: (...args: unknown[]) => applyPluginAutoEnableMock(...args),
}));
vi.mock("../../plugins/loader.js", () => ({
loadOpenClawPlugins: (...args: unknown[]) => loadOpenClawPluginsMock(...args),
}));
vi.mock("../../plugins/runtime.js", () => ({
getActivePluginRegistry: (...args: unknown[]) => getActivePluginRegistryMock(...args),
getActivePluginRegistryKey: (...args: unknown[]) => getActivePluginRegistryKeyMock(...args),
}));
vi.mock("../../utils/message-channel.js", () => ({
normalizeMessageChannel: (...args: unknown[]) => normalizeMessageChannelMock(...args),
isDeliverableMessageChannel: (...args: unknown[]) => isDeliverableMessageChannelMock(...args),
}));
import { importFreshModule } from "../../../test/helpers/import-fresh.js";
async function importChannelResolution(scope: string) {
return await importFreshModule<typeof import("./channel-resolution.js")>(
import.meta.url,
`./channel-resolution.js?scope=${scope}`,
);
}
describe("outbound channel resolution", () => {
beforeEach(() => {
resolveDefaultAgentIdMock.mockReset();
resolveAgentWorkspaceDirMock.mockReset();
getChannelPluginMock.mockReset();
applyPluginAutoEnableMock.mockReset();
loadOpenClawPluginsMock.mockReset();
getActivePluginRegistryMock.mockReset();
getActivePluginRegistryKeyMock.mockReset();
normalizeMessageChannelMock.mockReset();
isDeliverableMessageChannelMock.mockReset();
normalizeMessageChannelMock.mockImplementation((value?: string | null) =>
typeof value === "string" ? value.trim().toLowerCase() : undefined,
);
isDeliverableMessageChannelMock.mockImplementation((value?: string) =>
["telegram", "discord", "slack"].includes(String(value)),
);
getActivePluginRegistryMock.mockReturnValue({ channels: [] });
getActivePluginRegistryKeyMock.mockReturnValue("registry-key");
applyPluginAutoEnableMock.mockReturnValue({ config: { autoEnabled: true } });
resolveDefaultAgentIdMock.mockReturnValue("main");
resolveAgentWorkspaceDirMock.mockReturnValue("/tmp/workspace");
});
it("normalizes deliverable channels and rejects unknown ones", async () => {
const channelResolution = await importChannelResolution("normalize");
expect(channelResolution.normalizeDeliverableOutboundChannel(" Telegram ")).toBe("telegram");
expect(channelResolution.normalizeDeliverableOutboundChannel("unknown")).toBeUndefined();
expect(channelResolution.normalizeDeliverableOutboundChannel(null)).toBeUndefined();
});
it("returns the already-registered plugin without bootstrapping", async () => {
const plugin = { id: "telegram" };
getChannelPluginMock.mockReturnValueOnce(plugin);
const channelResolution = await importChannelResolution("existing-plugin");
expect(
channelResolution.resolveOutboundChannelPlugin({
channel: "telegram",
cfg: {} as never,
}),
).toBe(plugin);
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
});
it("falls back to the active registry when getChannelPlugin misses", async () => {
const plugin = { id: "telegram" };
getChannelPluginMock.mockReturnValue(undefined);
getActivePluginRegistryMock.mockReturnValue({
channels: [{ plugin }],
});
const channelResolution = await importChannelResolution("direct-registry");
expect(
channelResolution.resolveOutboundChannelPlugin({
channel: "telegram",
cfg: {} as never,
}),
).toBe(plugin);
});
it("bootstraps plugins once per registry key and returns the newly loaded plugin", async () => {
const plugin = { id: "telegram" };
getChannelPluginMock.mockReturnValueOnce(undefined).mockReturnValueOnce(plugin);
const channelResolution = await importChannelResolution("bootstrap-success");
expect(
channelResolution.resolveOutboundChannelPlugin({
channel: "telegram",
cfg: { channels: {} } as never,
}),
).toBe(plugin);
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith({
config: { autoEnabled: true },
workspaceDir: "/tmp/workspace",
});
getChannelPluginMock.mockReturnValue(undefined);
channelResolution.resolveOutboundChannelPlugin({
channel: "telegram",
cfg: { channels: {} } as never,
});
expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(1);
});
it("retries bootstrap after a transient load failure", async () => {
getChannelPluginMock.mockReturnValue(undefined);
loadOpenClawPluginsMock.mockImplementationOnce(() => {
throw new Error("transient");
});
const channelResolution = await importChannelResolution("bootstrap-retry");
expect(
channelResolution.resolveOutboundChannelPlugin({
channel: "telegram",
cfg: { channels: {} } as never,
}),
).toBeUndefined();
channelResolution.resolveOutboundChannelPlugin({
channel: "telegram",
cfg: { channels: {} } as never,
});
expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(2);
});
});