refactor: share zalouser group gating helpers

This commit is contained in:
Peter Steinberger
2026-03-13 20:15:02 +00:00
parent ea82458290
commit d53d4dc22f

View File

@@ -254,57 +254,152 @@ describe("zalouser monitor group mention gating", () => {
sendSeenZalouserMock.mockClear();
});
it("skips unmentioned group messages when requireMention=true", async () => {
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
commandAuthorized: false,
});
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: createGroupMessage(),
account: createAccount(),
message: params.message,
account: params.account ?? createAccount(),
config: createConfig(),
runtime: createRuntimeEnv(),
historyState: params.historyState,
});
}
expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
expect(sendTypingZalouserMock).not.toHaveBeenCalled();
});
it("fails closed when requireMention=true but mention detection is unavailable", async () => {
async function expectSkippedGroupMessage(message?: Partial<ZaloInboundMessage>) {
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
commandAuthorized: false,
});
await __testing.processMessage({
message: createGroupMessage({
canResolveExplicitMention: false,
hasAnyMention: false,
wasExplicitlyMentioned: false,
}),
account: createAccount(),
config: createConfig(),
runtime: createRuntimeEnv(),
await processMessageWithDefaults({
message: createGroupMessage(message),
});
expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
expect(sendTypingZalouserMock).not.toHaveBeenCalled();
});
}
it("dispatches explicitly-mentioned group messages and marks WasMentioned", async () => {
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 __testing.processMessage({
await processMessageWithDefaults({
message: createGroupMessage({
threadId: "g-attacker-001",
groupName: "Trusted Team",
senderId: "666",
hasAnyMention: true,
wasExplicitlyMentioned: true,
content: "ping @bot",
}),
account: createAccount(),
config: createConfig(),
runtime: createRuntimeEnv(),
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);
const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
return dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
}
it("skips unmentioned group messages when requireMention=true", async () => {
await expectSkippedGroupMessage();
});
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");
@@ -315,22 +410,14 @@ describe("zalouser monitor group mention gating", () => {
});
it("allows authorized control commands to bypass mention gating", async () => {
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
const callArg = await dispatchGroupMessage({
commandAuthorized: true,
});
await __testing.processMessage({
message: createGroupMessage({
message: {
content: "/status",
hasAnyMention: false,
wasExplicitlyMentioned: false,
}),
account: createAccount(),
config: createConfig(),
runtime: createRuntimeEnv(),
},
});
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
expect(callArg?.ctx?.WasMentioned).toBe(true);
});
@@ -371,46 +458,30 @@ describe("zalouser monitor group mention gating", () => {
});
it("uses commandContent for mention-prefixed control commands", async () => {
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
const callArg = await dispatchGroupMessage({
commandAuthorized: true,
});
await __testing.processMessage({
message: createGroupMessage({
message: {
content: "@Bot /new",
commandContent: "/new",
hasAnyMention: true,
wasExplicitlyMentioned: true,
}),
account: createAccount(),
config: createConfig(),
runtime: createRuntimeEnv(),
},
});
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
expect(callArg?.ctx?.CommandBody).toBe("/new");
expect(callArg?.ctx?.BodyForCommands).toBe("/new");
});
it("allows group control commands when only allowFrom is configured", async () => {
const { dispatchReplyWithBufferedBlockDispatcher, resolveCommandAuthorizedFromAuthorizers } =
installGroupCommandAuthRuntime();
await processGroupControlCommand({
account: {
...createAccount(),
config: {
...createAccount().config,
allowFrom: ["123"],
},
await expectGroupCommandAuthorizers({
accountConfig: {
...createAccount().config,
allowFrom: ["123"],
},
expectedAuthorizers: [
{ configured: true, allowed: true },
{ configured: true, allowed: true },
],
});
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
const authCall = resolveCommandAuthorizedFromAuthorizers.mock.calls[0]?.[0];
expect(authCall?.authorizers).toEqual([
{ configured: true, allowed: true },
{ configured: true, allowed: true },
]);
});
it("blocks group messages when sender is not in groupAllowFrom/allowFrom", async () => {
@@ -439,112 +510,35 @@ describe("zalouser monitor group mention gating", () => {
});
it("does not accept a different group id by matching only the mutable group name by default", async () => {
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
commandAuthorized: false,
});
await __testing.processMessage({
message: createGroupMessage({
threadId: "g-attacker-001",
groupName: "Trusted Team",
senderId: "666",
hasAnyMention: true,
wasExplicitlyMentioned: true,
content: "ping @bot",
}),
account: {
...createAccount(),
config: {
...createAccount().config,
groupPolicy: "allowlist",
groupAllowFrom: ["*"],
groups: {
"group:g-trusted-001": { allow: true },
"Trusted Team": { allow: true },
},
},
},
config: createConfig(),
runtime: createRuntimeEnv(),
});
expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
await expectDangerousNameMatching({ expectedDispatches: 0 });
});
it("accepts mutable group-name matches only when dangerouslyAllowNameMatching is enabled", async () => {
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
commandAuthorized: false,
const dispatchReplyWithBufferedBlockDispatcher = await expectDangerousNameMatching({
dangerouslyAllowNameMatching: true,
expectedDispatches: 1,
});
await __testing.processMessage({
message: createGroupMessage({
threadId: "g-attacker-001",
groupName: "Trusted Team",
senderId: "666",
hasAnyMention: true,
wasExplicitlyMentioned: true,
content: "ping @bot",
}),
account: {
...createAccount(),
config: {
...createAccount().config,
dangerouslyAllowNameMatching: true,
groupPolicy: "allowlist",
groupAllowFrom: ["*"],
groups: {
"group:g-trusted-001": { allow: true },
"Trusted Team": { allow: true },
},
},
},
config: createConfig(),
runtime: createRuntimeEnv(),
});
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(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 () => {
const { dispatchReplyWithBufferedBlockDispatcher, resolveCommandAuthorizedFromAuthorizers } =
installGroupCommandAuthRuntime();
await processGroupControlCommand({
account: {
...createAccount(),
config: {
...createAccount().config,
allowFrom: ["999"],
groupAllowFrom: ["123"],
},
await expectGroupCommandAuthorizers({
accountConfig: {
...createAccount().config,
allowFrom: ["999"],
groupAllowFrom: ["123"],
},
expectedAuthorizers: [
{ configured: true, allowed: false },
{ configured: true, allowed: true },
],
});
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
const authCall = resolveCommandAuthorizedFromAuthorizers.mock.calls[0]?.[0];
expect(authCall?.authorizers).toEqual([
{ configured: true, allowed: false },
{ configured: true, allowed: true },
]);
});
it("routes DM messages with direct peer kind", async () => {
const { dispatchReplyWithBufferedBlockDispatcher, resolveAgentRoute, buildAgentSessionKey } =
installRuntime({
commandAuthorized: false,
});
const account = createAccount();
await __testing.processMessage({
message: createDmMessage(),
account: {
...account,
config: {
...account.config,
dmPolicy: "open",
},
},
config: createConfig(),
runtime: createRuntimeEnv(),
});
await processOpenDmMessage();
expect(resolveAgentRoute).toHaveBeenCalledWith(
expect.objectContaining({
@@ -562,24 +556,9 @@ describe("zalouser monitor group mention gating", () => {
});
it("reuses the legacy DM session key when only the old group-shaped session exists", async () => {
const { dispatchReplyWithBufferedBlockDispatcher, readSessionUpdatedAt } = installRuntime({
commandAuthorized: false,
});
readSessionUpdatedAt.mockImplementation((input?: { storePath: string; sessionKey: string }) =>
input?.sessionKey === "agent:main:zalouser:group:321" ? 123 : undefined,
);
const account = createAccount();
await __testing.processMessage({
message: createDmMessage(),
account: {
...account,
config: {
...account.config,
dmPolicy: "open",
},
},
config: createConfig(),
runtime: createRuntimeEnv(),
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];