723 lines
22 KiB
TypeScript
723 lines
22 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "../runtime-api.js";
|
|
import "./monitor.send-mocks.js";
|
|
import { resolveZalouserAccountSync } from "./accounts.js";
|
|
import { __testing } from "./monitor.js";
|
|
import {
|
|
sendDeliveredZalouserMock,
|
|
sendMessageZalouserMock,
|
|
sendSeenZalouserMock,
|
|
sendTypingZalouserMock,
|
|
} from "./monitor.send-mocks.js";
|
|
import { setZalouserRuntime } from "./runtime.js";
|
|
import { createZalouserRuntimeEnv } from "./test-helpers.js";
|
|
import type { ResolvedZalouserAccount, ZaloInboundMessage } from "./types.js";
|
|
|
|
function createAccount(): ResolvedZalouserAccount {
|
|
return {
|
|
accountId: "default",
|
|
enabled: true,
|
|
profile: "default",
|
|
authenticated: true,
|
|
config: {
|
|
groupPolicy: "open",
|
|
groups: {
|
|
"*": { requireMention: true },
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function createConfig(): OpenClawConfig {
|
|
return {
|
|
channels: {
|
|
zalouser: {
|
|
enabled: true,
|
|
groups: {
|
|
"*": { requireMention: true },
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
const createRuntimeEnv = () => createZalouserRuntimeEnv();
|
|
|
|
function installRuntime(params: {
|
|
commandAuthorized?: boolean;
|
|
replyPayload?: { text?: string; mediaUrl?: string; mediaUrls?: string[] };
|
|
resolveCommandAuthorizedFromAuthorizers?: (params: {
|
|
useAccessGroups: boolean;
|
|
authorizers: Array<{ configured: boolean; allowed: boolean }>;
|
|
}) => boolean;
|
|
}) {
|
|
const dispatchReplyWithBufferedBlockDispatcher = vi.fn(async ({ dispatcherOptions, ctx }) => {
|
|
await dispatcherOptions.typingCallbacks?.onReplyStart?.();
|
|
if (params.replyPayload) {
|
|
await dispatcherOptions.deliver(params.replyPayload);
|
|
}
|
|
return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 }, ctx };
|
|
});
|
|
const resolveCommandAuthorizedFromAuthorizers = vi.fn(
|
|
(input: {
|
|
useAccessGroups: boolean;
|
|
authorizers: Array<{ configured: boolean; allowed: boolean }>;
|
|
}) => {
|
|
if (params.resolveCommandAuthorizedFromAuthorizers) {
|
|
return params.resolveCommandAuthorizedFromAuthorizers(input);
|
|
}
|
|
return params.commandAuthorized ?? false;
|
|
},
|
|
);
|
|
const resolveAgentRoute = vi.fn((input: { peer?: { kind?: string; id?: string } }) => {
|
|
const peerKind = input.peer?.kind === "direct" ? "direct" : "group";
|
|
const peerId = input.peer?.id ?? "1";
|
|
return {
|
|
agentId: "main",
|
|
sessionKey:
|
|
peerKind === "direct" ? "agent:main:main" : `agent:main:zalouser:${peerKind}:${peerId}`,
|
|
accountId: "default",
|
|
mainSessionKey: "agent:main:main",
|
|
};
|
|
});
|
|
const readAllowFromStore = vi.fn(async () => []);
|
|
const readSessionUpdatedAt = vi.fn(
|
|
(_params?: { storePath: string; sessionKey: string }): number | undefined => undefined,
|
|
);
|
|
const buildAgentSessionKey = vi.fn(
|
|
(input: {
|
|
agentId: string;
|
|
channel: string;
|
|
accountId?: string;
|
|
peer?: { kind?: string; id?: string };
|
|
dmScope?: string;
|
|
}) => {
|
|
const peerKind = input.peer?.kind === "direct" ? "direct" : "group";
|
|
const peerId = input.peer?.id ?? "1";
|
|
if (peerKind === "direct") {
|
|
if (input.dmScope === "per-account-channel-peer") {
|
|
return `agent:${input.agentId}:${input.channel}:${input.accountId ?? "default"}:direct:${peerId}`;
|
|
}
|
|
if (input.dmScope === "per-peer") {
|
|
return `agent:${input.agentId}:direct:${peerId}`;
|
|
}
|
|
if (input.dmScope === "main" || !input.dmScope) {
|
|
return "agent:main:main";
|
|
}
|
|
}
|
|
return `agent:${input.agentId}:${input.channel}:${peerKind}:${peerId}`;
|
|
},
|
|
);
|
|
|
|
setZalouserRuntime({
|
|
logging: {
|
|
shouldLogVerbose: () => false,
|
|
},
|
|
channel: {
|
|
pairing: {
|
|
readAllowFromStore,
|
|
upsertPairingRequest: vi.fn(async () => ({ code: "PAIR", created: true })),
|
|
buildPairingReply: vi.fn(() => "pair"),
|
|
},
|
|
commands: {
|
|
shouldComputeCommandAuthorized: vi.fn((body: string) => body.trim().startsWith("/")),
|
|
resolveCommandAuthorizedFromAuthorizers,
|
|
isControlCommandMessage: vi.fn((body: string) => body.trim().startsWith("/")),
|
|
shouldHandleTextCommands: vi.fn(() => true),
|
|
},
|
|
mentions: {
|
|
buildMentionRegexes: vi.fn(() => []),
|
|
matchesMentionWithExplicit: vi.fn(
|
|
(input) => input.explicit?.isExplicitlyMentioned === true,
|
|
),
|
|
},
|
|
groups: {
|
|
resolveRequireMention: vi.fn((input) => {
|
|
const cfg = input.cfg as OpenClawConfig;
|
|
const groupCfg = cfg.channels?.zalouser?.groups ?? {};
|
|
const groupEntry = input.groupId ? groupCfg[input.groupId] : undefined;
|
|
const defaultEntry = groupCfg["*"];
|
|
if (typeof groupEntry?.requireMention === "boolean") {
|
|
return groupEntry.requireMention;
|
|
}
|
|
if (typeof defaultEntry?.requireMention === "boolean") {
|
|
return defaultEntry.requireMention;
|
|
}
|
|
return true;
|
|
}),
|
|
},
|
|
routing: {
|
|
buildAgentSessionKey,
|
|
resolveAgentRoute,
|
|
},
|
|
session: {
|
|
resolveStorePath: vi.fn(() => "/tmp"),
|
|
readSessionUpdatedAt,
|
|
recordInboundSession: vi.fn(async () => {}),
|
|
},
|
|
reply: {
|
|
resolveEnvelopeFormatOptions: vi.fn(() => undefined),
|
|
formatAgentEnvelope: vi.fn(({ body }) => body),
|
|
finalizeInboundContext: vi.fn((ctx) => ctx),
|
|
dispatchReplyWithBufferedBlockDispatcher,
|
|
},
|
|
text: {
|
|
resolveMarkdownTableMode: vi.fn(() => "code"),
|
|
convertMarkdownTables: vi.fn((text: string) => text),
|
|
resolveChunkMode: vi.fn(() => "length"),
|
|
resolveTextChunkLimit: vi.fn(() => 1200),
|
|
chunkMarkdownTextWithMode: vi.fn((text: string) => [text]),
|
|
},
|
|
},
|
|
} as unknown as PluginRuntime);
|
|
|
|
return {
|
|
dispatchReplyWithBufferedBlockDispatcher,
|
|
resolveAgentRoute,
|
|
resolveCommandAuthorizedFromAuthorizers,
|
|
readAllowFromStore,
|
|
readSessionUpdatedAt,
|
|
buildAgentSessionKey,
|
|
};
|
|
}
|
|
|
|
function installGroupCommandAuthRuntime() {
|
|
return installRuntime({
|
|
resolveCommandAuthorizedFromAuthorizers: ({ useAccessGroups, authorizers }) =>
|
|
useAccessGroups && authorizers.some((entry) => entry.configured && entry.allowed),
|
|
});
|
|
}
|
|
|
|
async function processGroupControlCommand(params: {
|
|
account: ResolvedZalouserAccount;
|
|
content?: string;
|
|
commandContent?: string;
|
|
}) {
|
|
await __testing.processMessage({
|
|
message: createGroupMessage({
|
|
content: params.content ?? "/new",
|
|
commandContent: params.commandContent ?? "/new",
|
|
hasAnyMention: true,
|
|
wasExplicitlyMentioned: true,
|
|
}),
|
|
account: params.account,
|
|
config: createConfig(),
|
|
runtime: createRuntimeEnv(),
|
|
});
|
|
}
|
|
|
|
function createGroupMessage(overrides: Partial<ZaloInboundMessage> = {}): ZaloInboundMessage {
|
|
return {
|
|
threadId: "g-1",
|
|
isGroup: true,
|
|
senderId: "123",
|
|
senderName: "Alice",
|
|
groupName: "Team",
|
|
content: "hello",
|
|
timestampMs: Date.now(),
|
|
msgId: "m-1",
|
|
hasAnyMention: false,
|
|
wasExplicitlyMentioned: false,
|
|
canResolveExplicitMention: true,
|
|
implicitMention: false,
|
|
raw: { source: "test" },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createDmMessage(overrides: Partial<ZaloInboundMessage> = {}): ZaloInboundMessage {
|
|
return {
|
|
threadId: "u-1",
|
|
isGroup: false,
|
|
senderId: "321",
|
|
senderName: "Bob",
|
|
groupName: undefined,
|
|
content: "hello",
|
|
timestampMs: Date.now(),
|
|
msgId: "dm-1",
|
|
raw: { source: "test" },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("zalouser monitor group mention gating", () => {
|
|
beforeEach(() => {
|
|
sendMessageZalouserMock.mockClear();
|
|
sendTypingZalouserMock.mockClear();
|
|
sendDeliveredZalouserMock.mockClear();
|
|
sendSeenZalouserMock.mockClear();
|
|
});
|
|
|
|
async function processMessageWithDefaults(params: {
|
|
message: ZaloInboundMessage;
|
|
account?: ResolvedZalouserAccount;
|
|
historyState?: {
|
|
historyLimit: number;
|
|
groupHistories: Map<
|
|
string,
|
|
Array<{ sender: string; body: string; timestamp?: number; messageId?: string }>
|
|
>;
|
|
};
|
|
}) {
|
|
await __testing.processMessage({
|
|
message: params.message,
|
|
account: params.account ?? createAccount(),
|
|
config: createConfig(),
|
|
runtime: createZalouserRuntimeEnv(),
|
|
historyState: params.historyState,
|
|
});
|
|
}
|
|
|
|
async function expectSkippedGroupMessage(message?: Partial<ZaloInboundMessage>) {
|
|
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
commandAuthorized: false,
|
|
});
|
|
await processMessageWithDefaults({
|
|
message: createGroupMessage(message),
|
|
});
|
|
expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
expect(sendTypingZalouserMock).not.toHaveBeenCalled();
|
|
}
|
|
|
|
async function expectGroupCommandAuthorizers(params: {
|
|
accountConfig: ResolvedZalouserAccount["config"];
|
|
expectedAuthorizers: Array<{ configured: boolean; allowed: boolean }>;
|
|
}) {
|
|
const { dispatchReplyWithBufferedBlockDispatcher, resolveCommandAuthorizedFromAuthorizers } =
|
|
installGroupCommandAuthRuntime();
|
|
await processGroupControlCommand({
|
|
account: {
|
|
...createAccount(),
|
|
config: params.accountConfig,
|
|
},
|
|
});
|
|
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
|
const authCall = resolveCommandAuthorizedFromAuthorizers.mock.calls[0]?.[0];
|
|
expect(authCall?.authorizers).toEqual(params.expectedAuthorizers);
|
|
}
|
|
|
|
async function processOpenDmMessage(params?: {
|
|
message?: Partial<ZaloInboundMessage>;
|
|
readSessionUpdatedAt?: (input?: {
|
|
storePath: string;
|
|
sessionKey: string;
|
|
}) => number | undefined;
|
|
}) {
|
|
const runtime = installRuntime({
|
|
commandAuthorized: false,
|
|
});
|
|
if (params?.readSessionUpdatedAt) {
|
|
runtime.readSessionUpdatedAt.mockImplementation(params.readSessionUpdatedAt);
|
|
}
|
|
const account = createAccount();
|
|
await processMessageWithDefaults({
|
|
message: createDmMessage(params?.message),
|
|
account: {
|
|
...account,
|
|
config: {
|
|
...account.config,
|
|
dmPolicy: "open",
|
|
},
|
|
},
|
|
});
|
|
return runtime;
|
|
}
|
|
|
|
async function expectDangerousNameMatching(params: {
|
|
dangerouslyAllowNameMatching?: boolean;
|
|
expectedDispatches: number;
|
|
}) {
|
|
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
commandAuthorized: false,
|
|
});
|
|
await processMessageWithDefaults({
|
|
message: createGroupMessage({
|
|
threadId: "g-attacker-001",
|
|
groupName: "Trusted Team",
|
|
senderId: "666",
|
|
hasAnyMention: true,
|
|
wasExplicitlyMentioned: true,
|
|
content: "ping @bot",
|
|
}),
|
|
account: {
|
|
...createAccount(),
|
|
config: {
|
|
...createAccount().config,
|
|
...(params.dangerouslyAllowNameMatching ? { dangerouslyAllowNameMatching: true } : {}),
|
|
groupPolicy: "allowlist",
|
|
groupAllowFrom: ["*"],
|
|
groups: {
|
|
"group:g-trusted-001": { allow: true },
|
|
"Trusted Team": { allow: true },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(
|
|
params.expectedDispatches,
|
|
);
|
|
return dispatchReplyWithBufferedBlockDispatcher;
|
|
}
|
|
|
|
async function dispatchGroupMessage(params: {
|
|
commandAuthorized: boolean;
|
|
message: Partial<ZaloInboundMessage>;
|
|
}) {
|
|
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
commandAuthorized: params.commandAuthorized,
|
|
});
|
|
await processMessageWithDefaults({
|
|
message: createGroupMessage(params.message),
|
|
});
|
|
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
|
return dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
|
|
}
|
|
|
|
it("skips unmentioned group messages when requireMention=true", async () => {
|
|
await expectSkippedGroupMessage();
|
|
});
|
|
|
|
it("blocks mentioned group messages by default when groupPolicy is omitted", async () => {
|
|
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
commandAuthorized: false,
|
|
});
|
|
const cfg: OpenClawConfig = {
|
|
channels: {
|
|
zalouser: {
|
|
enabled: true,
|
|
},
|
|
},
|
|
};
|
|
const account = resolveZalouserAccountSync({ cfg, accountId: "default" });
|
|
|
|
await __testing.processMessage({
|
|
message: createGroupMessage({
|
|
content: "ping @bot",
|
|
hasAnyMention: true,
|
|
wasExplicitlyMentioned: true,
|
|
}),
|
|
account,
|
|
config: cfg,
|
|
runtime: createRuntimeEnv(),
|
|
});
|
|
|
|
expect(account.config.groupPolicy).toBe("allowlist");
|
|
expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("fails closed when requireMention=true but mention detection is unavailable", async () => {
|
|
await expectSkippedGroupMessage({
|
|
canResolveExplicitMention: false,
|
|
hasAnyMention: false,
|
|
wasExplicitlyMentioned: false,
|
|
});
|
|
});
|
|
|
|
it("dispatches explicitly-mentioned group messages and marks WasMentioned", async () => {
|
|
const callArg = await dispatchGroupMessage({
|
|
commandAuthorized: false,
|
|
message: {
|
|
hasAnyMention: true,
|
|
wasExplicitlyMentioned: true,
|
|
content: "ping @bot",
|
|
},
|
|
});
|
|
expect(callArg?.ctx?.WasMentioned).toBe(true);
|
|
expect(callArg?.ctx?.To).toBe("zalouser:group:g-1");
|
|
expect(callArg?.ctx?.OriginatingTo).toBe("zalouser:group:g-1");
|
|
expect(sendTypingZalouserMock).toHaveBeenCalledWith("g-1", {
|
|
profile: "default",
|
|
isGroup: true,
|
|
});
|
|
});
|
|
|
|
it("allows authorized control commands to bypass mention gating", async () => {
|
|
const callArg = await dispatchGroupMessage({
|
|
commandAuthorized: true,
|
|
message: {
|
|
content: "/status",
|
|
hasAnyMention: false,
|
|
wasExplicitlyMentioned: false,
|
|
},
|
|
});
|
|
expect(callArg?.ctx?.WasMentioned).toBe(true);
|
|
});
|
|
|
|
it("passes long markdown replies through once so formatting happens before chunking", async () => {
|
|
const replyText = `**${"a".repeat(2501)}**`;
|
|
installRuntime({
|
|
commandAuthorized: false,
|
|
replyPayload: { text: replyText },
|
|
});
|
|
|
|
await __testing.processMessage({
|
|
message: createDmMessage({
|
|
content: "hello",
|
|
}),
|
|
account: {
|
|
...createAccount(),
|
|
config: {
|
|
...createAccount().config,
|
|
dmPolicy: "open",
|
|
},
|
|
},
|
|
config: createConfig(),
|
|
runtime: createRuntimeEnv(),
|
|
});
|
|
|
|
expect(sendMessageZalouserMock).toHaveBeenCalledTimes(1);
|
|
expect(sendMessageZalouserMock).toHaveBeenCalledWith(
|
|
"u-1",
|
|
replyText,
|
|
expect.objectContaining({
|
|
isGroup: false,
|
|
profile: "default",
|
|
textMode: "markdown",
|
|
textChunkMode: "length",
|
|
textChunkLimit: 1200,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("uses commandContent for mention-prefixed control commands", async () => {
|
|
const callArg = await dispatchGroupMessage({
|
|
commandAuthorized: true,
|
|
message: {
|
|
content: "@Bot /new",
|
|
commandContent: "/new",
|
|
hasAnyMention: true,
|
|
wasExplicitlyMentioned: true,
|
|
},
|
|
});
|
|
expect(callArg?.ctx?.CommandBody).toBe("/new");
|
|
expect(callArg?.ctx?.BodyForCommands).toBe("/new");
|
|
});
|
|
|
|
it("allows group control commands when only allowFrom is configured", async () => {
|
|
await expectGroupCommandAuthorizers({
|
|
accountConfig: {
|
|
...createAccount().config,
|
|
allowFrom: ["123"],
|
|
},
|
|
expectedAuthorizers: [
|
|
{ configured: true, allowed: true },
|
|
{ configured: true, allowed: true },
|
|
],
|
|
});
|
|
});
|
|
|
|
it("allows allowlisted group replies without inheriting the DM allowlist", async () => {
|
|
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
commandAuthorized: false,
|
|
replyPayload: { text: "ok" },
|
|
});
|
|
await __testing.processMessage({
|
|
message: createGroupMessage({
|
|
content: "ping @bot",
|
|
hasAnyMention: true,
|
|
wasExplicitlyMentioned: true,
|
|
senderId: "456",
|
|
}),
|
|
account: {
|
|
...createAccount(),
|
|
config: {
|
|
...createAccount().config,
|
|
groupPolicy: "allowlist",
|
|
allowFrom: ["123"],
|
|
groups: {
|
|
"group:g-1": { allow: true, requireMention: true },
|
|
},
|
|
},
|
|
},
|
|
config: createConfig(),
|
|
runtime: createRuntimeEnv(),
|
|
});
|
|
|
|
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("blocks group messages when sender is not in groupAllowFrom", async () => {
|
|
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
commandAuthorized: false,
|
|
});
|
|
await __testing.processMessage({
|
|
message: createGroupMessage({
|
|
content: "ping @bot",
|
|
hasAnyMention: true,
|
|
wasExplicitlyMentioned: true,
|
|
}),
|
|
account: {
|
|
...createAccount(),
|
|
config: {
|
|
...createAccount().config,
|
|
groupPolicy: "allowlist",
|
|
allowFrom: ["999"],
|
|
groupAllowFrom: ["999"],
|
|
},
|
|
},
|
|
config: createConfig(),
|
|
runtime: createRuntimeEnv(),
|
|
});
|
|
|
|
expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not accept a different group id by matching only the mutable group name by default", async () => {
|
|
await expectDangerousNameMatching({ expectedDispatches: 0 });
|
|
});
|
|
|
|
it("accepts mutable group-name matches only when dangerouslyAllowNameMatching is enabled", async () => {
|
|
const dispatchReplyWithBufferedBlockDispatcher = await expectDangerousNameMatching({
|
|
dangerouslyAllowNameMatching: true,
|
|
expectedDispatches: 1,
|
|
});
|
|
const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
|
|
expect(callArg?.ctx?.To).toBe("zalouser:group:g-attacker-001");
|
|
});
|
|
|
|
it("allows group control commands when sender is in groupAllowFrom", async () => {
|
|
await expectGroupCommandAuthorizers({
|
|
accountConfig: {
|
|
...createAccount().config,
|
|
allowFrom: ["999"],
|
|
groupAllowFrom: ["123"],
|
|
},
|
|
expectedAuthorizers: [
|
|
{ configured: true, allowed: false },
|
|
{ configured: true, allowed: true },
|
|
],
|
|
});
|
|
});
|
|
|
|
it("routes DM messages with direct peer kind", async () => {
|
|
const { dispatchReplyWithBufferedBlockDispatcher, resolveAgentRoute, buildAgentSessionKey } =
|
|
await processOpenDmMessage();
|
|
|
|
expect(resolveAgentRoute).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
peer: { kind: "direct", id: "321" },
|
|
}),
|
|
);
|
|
expect(buildAgentSessionKey).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
peer: { kind: "direct", id: "321" },
|
|
dmScope: "per-channel-peer",
|
|
}),
|
|
);
|
|
const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
|
|
expect(callArg?.ctx?.SessionKey).toBe("agent:main:zalouser:direct:321");
|
|
});
|
|
|
|
it("reuses the legacy DM session key when only the old group-shaped session exists", async () => {
|
|
const { dispatchReplyWithBufferedBlockDispatcher } = await processOpenDmMessage({
|
|
readSessionUpdatedAt: (input?: { storePath: string; sessionKey: string }) =>
|
|
input?.sessionKey === "agent:main:zalouser:group:321" ? 123 : undefined,
|
|
});
|
|
|
|
const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
|
|
expect(callArg?.ctx?.SessionKey).toBe("agent:main:zalouser:group:321");
|
|
});
|
|
|
|
it("reads pairing store for open DM control commands", async () => {
|
|
const { readAllowFromStore } = installRuntime({
|
|
commandAuthorized: false,
|
|
});
|
|
const account = createAccount();
|
|
await __testing.processMessage({
|
|
message: createDmMessage({ content: "/new", commandContent: "/new" }),
|
|
account: {
|
|
...account,
|
|
config: {
|
|
...account.config,
|
|
dmPolicy: "open",
|
|
},
|
|
},
|
|
config: createConfig(),
|
|
runtime: createRuntimeEnv(),
|
|
});
|
|
|
|
expect(readAllowFromStore).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("skips pairing store read for open DM non-command messages", async () => {
|
|
const { readAllowFromStore } = installRuntime({
|
|
commandAuthorized: false,
|
|
});
|
|
const account = createAccount();
|
|
await __testing.processMessage({
|
|
message: createDmMessage({ content: "hello there" }),
|
|
account: {
|
|
...account,
|
|
config: {
|
|
...account.config,
|
|
dmPolicy: "open",
|
|
},
|
|
},
|
|
config: createConfig(),
|
|
runtime: createRuntimeEnv(),
|
|
});
|
|
|
|
expect(readAllowFromStore).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("includes skipped group messages as InboundHistory on the next processed message", async () => {
|
|
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
commandAuthorized: false,
|
|
});
|
|
const historyState = {
|
|
historyLimit: 5,
|
|
groupHistories: new Map<
|
|
string,
|
|
Array<{ sender: string; body: string; timestamp?: number; messageId?: string }>
|
|
>(),
|
|
};
|
|
const account = createAccount();
|
|
const config = createConfig();
|
|
await __testing.processMessage({
|
|
message: createGroupMessage({
|
|
content: "first unmentioned line",
|
|
hasAnyMention: false,
|
|
wasExplicitlyMentioned: false,
|
|
}),
|
|
account,
|
|
config,
|
|
runtime: createRuntimeEnv(),
|
|
historyState,
|
|
});
|
|
expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
|
|
await __testing.processMessage({
|
|
message: createGroupMessage({
|
|
content: "second line @bot",
|
|
hasAnyMention: true,
|
|
wasExplicitlyMentioned: true,
|
|
}),
|
|
account,
|
|
config,
|
|
runtime: createRuntimeEnv(),
|
|
historyState,
|
|
});
|
|
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
|
const firstDispatch = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
|
|
expect(firstDispatch?.ctx?.InboundHistory).toEqual([
|
|
expect.objectContaining({ sender: "Alice", body: "first unmentioned line" }),
|
|
]);
|
|
expect(String(firstDispatch?.ctx?.Body ?? "")).toContain("first unmentioned line");
|
|
|
|
await __testing.processMessage({
|
|
message: createGroupMessage({
|
|
content: "third line @bot",
|
|
hasAnyMention: true,
|
|
wasExplicitlyMentioned: true,
|
|
}),
|
|
account,
|
|
config,
|
|
runtime: createRuntimeEnv(),
|
|
historyState,
|
|
});
|
|
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(2);
|
|
const secondDispatch = dispatchReplyWithBufferedBlockDispatcher.mock.calls[1]?.[0];
|
|
expect(secondDispatch?.ctx?.InboundHistory).toEqual([]);
|
|
});
|
|
});
|