fix(test): stabilize line and irc extension suites

This commit is contained in:
Peter Steinberger
2026-03-27 19:32:04 +00:00
parent 1dae6cc617
commit 496a1a35bd
3 changed files with 96 additions and 142 deletions

View File

@@ -6,69 +6,39 @@ import { setIrcRuntime } from "./runtime.js";
import type { CoreConfig, IrcInboundMessage } from "./types.js";
const {
createChannelPairingControllerMock,
deliverFormattedTextWithAttachmentsMock,
dispatchInboundReplyWithBaseMock,
isDangerousNameMatchingEnabledMock,
logInboundDropMock,
readStoreAllowFromForDmPolicyMock,
resolveAllowlistProviderRuntimeGroupPolicyMock,
resolveControlCommandGateMock,
resolveDefaultGroupPolicyMock,
resolveEffectiveAllowFromListsMock,
warnMissingProviderGroupPolicyFallbackOnceMock,
buildMentionRegexesMock,
hasControlCommandMock,
matchesMentionPatternsMock,
readAllowFromStoreMock,
shouldHandleTextCommandsMock,
upsertPairingRequestMock,
} = vi.hoisted(() => {
return {
createChannelPairingControllerMock: vi.fn(),
deliverFormattedTextWithAttachmentsMock: vi.fn(),
dispatchInboundReplyWithBaseMock: vi.fn(),
isDangerousNameMatchingEnabledMock: vi.fn(),
logInboundDropMock: vi.fn(),
readStoreAllowFromForDmPolicyMock: vi.fn(),
resolveAllowlistProviderRuntimeGroupPolicyMock: vi.fn(),
resolveControlCommandGateMock: vi.fn(),
resolveDefaultGroupPolicyMock: vi.fn(),
resolveEffectiveAllowFromListsMock: vi.fn(),
warnMissingProviderGroupPolicyFallbackOnceMock: vi.fn(),
buildMentionRegexesMock: vi.fn(() => []),
hasControlCommandMock: vi.fn(() => false),
matchesMentionPatternsMock: vi.fn(() => false),
readAllowFromStoreMock: vi.fn(async () => []),
shouldHandleTextCommandsMock: vi.fn(() => false),
upsertPairingRequestMock: vi.fn(async () => ({ code: "CODE", created: true })),
};
});
const sendMessageIrcMock = vi.hoisted(() => vi.fn());
vi.mock("./runtime-api.js", async () => {
const actual = await vi.importActual<typeof import("./runtime-api.js")>("./runtime-api.js");
return {
...actual,
createChannelPairingController: createChannelPairingControllerMock,
deliverFormattedTextWithAttachments: deliverFormattedTextWithAttachmentsMock,
dispatchInboundReplyWithBase: dispatchInboundReplyWithBaseMock,
isDangerousNameMatchingEnabled: isDangerousNameMatchingEnabledMock,
logInboundDrop: logInboundDropMock,
readStoreAllowFromForDmPolicy: readStoreAllowFromForDmPolicyMock,
resolveAllowlistProviderRuntimeGroupPolicy: resolveAllowlistProviderRuntimeGroupPolicyMock,
resolveControlCommandGate: resolveControlCommandGateMock,
resolveDefaultGroupPolicy: resolveDefaultGroupPolicyMock,
resolveEffectiveAllowFromLists: resolveEffectiveAllowFromListsMock,
warnMissingProviderGroupPolicyFallbackOnce: warnMissingProviderGroupPolicyFallbackOnceMock,
};
});
vi.mock("./send.js", () => ({
sendMessageIrc: sendMessageIrcMock,
}));
function installIrcRuntime() {
setIrcRuntime({
channel: {
pairing: {
readAllowFromStore: readAllowFromStoreMock,
upsertPairingRequest: upsertPairingRequestMock,
},
commands: {
shouldHandleTextCommands: vi.fn(() => false),
shouldHandleTextCommands: shouldHandleTextCommandsMock,
},
text: {
hasControlCommand: vi.fn(() => false),
hasControlCommand: hasControlCommandMock,
},
mentions: {
buildMentionRegexes: vi.fn(() => []),
matchesMentionPatterns: vi.fn(() => false),
buildMentionRegexes: buildMentionRegexesMock,
matchesMentionPatterns: matchesMentionPatternsMock,
},
},
} as never);
@@ -115,36 +85,10 @@ describe("irc inbound behavior", () => {
beforeEach(() => {
vi.clearAllMocks();
installIrcRuntime();
resolveDefaultGroupPolicyMock.mockReturnValue("allowlist");
resolveAllowlistProviderRuntimeGroupPolicyMock.mockReturnValue({
groupPolicy: "allowlist",
providerMissingFallbackApplied: false,
});
warnMissingProviderGroupPolicyFallbackOnceMock.mockReturnValue(undefined);
readStoreAllowFromForDmPolicyMock.mockResolvedValue([]);
isDangerousNameMatchingEnabledMock.mockReturnValue(false);
resolveEffectiveAllowFromListsMock.mockReturnValue({
effectiveAllowFrom: [],
effectiveGroupAllowFrom: [],
});
deliverFormattedTextWithAttachmentsMock.mockImplementation(async ({ payload, send }) => {
await send({ text: payload.text, replyToId: undefined });
return true;
});
readAllowFromStoreMock.mockResolvedValue([]);
});
it("issues a DM pairing challenge and sends the reply to the sender nick", async () => {
const issueChallenge = vi.fn(async ({ sendPairingReply }) => {
await sendPairingReply("pair me");
});
createChannelPairingControllerMock.mockReturnValue({
readStoreForDmPolicy: vi.fn(),
issueChallenge,
});
resolveControlCommandGateMock.mockReturnValue({
commandAuthorized: false,
shouldBlock: false,
});
const sendReply = vi.fn(async () => {});
await handleIrcInbound({
@@ -155,25 +99,30 @@ describe("irc inbound behavior", () => {
sendReply,
});
expect(issueChallenge).toHaveBeenCalledTimes(1);
expect(sendReply).toHaveBeenCalledWith("alice", "pair me", undefined);
expect(dispatchInboundReplyWithBaseMock).not.toHaveBeenCalled();
expect(upsertPairingRequestMock).toHaveBeenCalledWith({
channel: "irc",
accountId: "default",
id: "alice!ident@example.com",
meta: { name: "alice" },
});
expect(sendReply).toHaveBeenCalledTimes(1);
expect(sendReply).toHaveBeenCalledWith(
"alice",
expect.stringContaining("OpenClaw: access not configured."),
undefined,
);
expect(sendReply).toHaveBeenCalledWith(
"alice",
expect.stringContaining("Your IRC id: alice!ident@example.com"),
undefined,
);
expect(sendReply).toHaveBeenCalledWith("alice", expect.stringContaining("CODE"), undefined);
});
it("drops unauthorized group control commands before dispatch", async () => {
createChannelPairingControllerMock.mockReturnValue({
readStoreForDmPolicy: vi.fn(),
issueChallenge: vi.fn(),
});
resolveEffectiveAllowFromListsMock.mockReturnValue({
effectiveAllowFrom: [],
effectiveGroupAllowFrom: ["alice!ident@example.com"],
});
resolveControlCommandGateMock.mockReturnValue({
commandAuthorized: false,
shouldBlock: true,
});
const runtime = createRuntimeEnv();
shouldHandleTextCommandsMock.mockReturnValue(true);
hasControlCommandMock.mockReturnValue(true);
await handleIrcInbound({
message: createMessage({
@@ -186,9 +135,11 @@ describe("irc inbound behavior", () => {
dmPolicy: "pairing",
allowFrom: [],
groupPolicy: "allowlist",
groupAllowFrom: ["alice!ident@example.com"],
groupAllowFrom: ["bob!ident@example.com"],
groups: {
"#ops": {},
"#ops": {
allowFrom: ["alice!ident@example.com"],
},
},
},
}),
@@ -196,13 +147,8 @@ describe("irc inbound behavior", () => {
runtime,
});
expect(logInboundDropMock).toHaveBeenCalledWith(
expect.objectContaining({
channel: "irc",
reason: "control command (unauthorized)",
target: "alice!ident@example.com",
}),
expect(runtime.log).toHaveBeenCalledWith(
"irc: drop control command (unauthorized) target=alice!ident@example.com",
);
expect(dispatchInboundReplyWithBaseMock).not.toHaveBeenCalled();
});
});

View File

@@ -1,10 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
createSendCfgThreadingRuntime,
expectProvidedCfgSkipsRuntimeLoad,
expectRuntimeCfgFallback,
} from "../../../test/helpers/extensions/send-config.js";
import { createSendCfgThreadingRuntime } from "../../../test/helpers/extensions/send-config.js";
import type { IrcClient } from "./client.js";
import { setIrcRuntime } from "./runtime.js";
import type { CoreConfig } from "./types.js";
const hoisted = vi.hoisted(() => {
@@ -17,28 +14,12 @@ const hoisted = vi.hoisted(() => {
resolveMarkdownTableMode,
convertMarkdownTables,
record,
resolveIrcAccount: vi.fn(() => ({
configured: true,
accountId: "default",
host: "irc.example.com",
nick: "openclaw",
port: 6697,
tls: true,
})),
normalizeIrcMessagingTarget: vi.fn((value: string) => value.trim()),
connectIrcClient: vi.fn(),
buildIrcConnectOptions: vi.fn(() => ({})),
};
});
vi.mock("./runtime.js", () => ({
getIrcRuntime: () => createSendCfgThreadingRuntime(hoisted),
}));
vi.mock("./accounts.js", () => ({
resolveIrcAccount: hoisted.resolveIrcAccount,
}));
vi.mock("./normalize.js", () => ({
normalizeIrcMessagingTarget: hoisted.normalizeIrcMessagingTarget,
}));
@@ -64,10 +45,24 @@ import { sendMessageIrc } from "./send.js";
describe("sendMessageIrc cfg threading", () => {
beforeEach(() => {
vi.clearAllMocks();
setIrcRuntime(createSendCfgThreadingRuntime(hoisted) as never);
});
it("uses explicitly provided cfg without loading runtime config", async () => {
const providedCfg = { source: "provided" } as unknown as CoreConfig;
const providedCfg = {
channels: {
irc: {
host: "irc.example.com",
nick: "openclaw",
accounts: {
work: {
host: "irc.example.com",
nick: "workbot",
},
},
},
},
} as unknown as CoreConfig;
const client = {
isReady: vi.fn(() => true),
sendPrivmsg: vi.fn(),
@@ -79,18 +74,27 @@ describe("sendMessageIrc cfg threading", () => {
accountId: "work",
});
expectProvidedCfgSkipsRuntimeLoad({
loadConfig: hoisted.loadConfig,
resolveAccount: hoisted.resolveIrcAccount,
cfg: providedCfg,
accountId: "work",
});
expect(hoisted.loadConfig).not.toHaveBeenCalled();
expect(client.sendPrivmsg).toHaveBeenCalledWith("#room", "hello");
expect(result).toEqual({ messageId: "irc-msg-1", target: "#room" });
expect(hoisted.record).toHaveBeenCalledWith({
channel: "irc",
accountId: "work",
direction: "outbound",
});
expect(result.target).toBe("#room");
expect(result.messageId).toEqual(expect.any(String));
expect(result.messageId.length).toBeGreaterThan(0);
});
it("falls back to runtime config when cfg is omitted", async () => {
const runtimeCfg = { source: "runtime" } as unknown as CoreConfig;
const runtimeCfg = {
channels: {
irc: {
host: "irc.example.com",
nick: "openclaw",
},
},
} as unknown as CoreConfig;
hoisted.loadConfig.mockReturnValueOnce(runtimeCfg);
const client = {
isReady: vi.fn(() => true),
@@ -99,12 +103,12 @@ describe("sendMessageIrc cfg threading", () => {
await sendMessageIrc("#ops", "ping", { client });
expectRuntimeCfgFallback({
loadConfig: hoisted.loadConfig,
resolveAccount: hoisted.resolveIrcAccount,
cfg: runtimeCfg,
accountId: undefined,
});
expect(hoisted.loadConfig).toHaveBeenCalledTimes(1);
expect(client.sendPrivmsg).toHaveBeenCalledWith("#ops", "ping");
expect(hoisted.record).toHaveBeenCalledWith({
channel: "irc",
accountId: "default",
direction: "outbound",
});
});
});

View File

@@ -20,11 +20,15 @@ const { readAllowFromStoreMock, upsertPairingRequestMock } = vi.hoisted(() => ({
upsertPairingRequestMock: vi.fn(async () => ({ code: "CODE", created: true })),
}));
vi.mock("openclaw/plugin-sdk/conversation-runtime", () => ({
resolvePairingIdLabel: () => "lineUserId",
readChannelAllowFromStore: readAllowFromStoreMock,
upsertChannelPairingRequest: upsertPairingRequestMock,
}));
vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
return {
...actual,
resolvePairingIdLabel: () => "lineUserId",
readChannelAllowFromStore: readAllowFromStoreMock,
upsertChannelPairingRequest: upsertPairingRequestMock,
};
});
vi.mock("./download.js", () => ({
downloadLineMedia: async () => {