Skip to content

Commit 3d4b7ca

Browse files
authored
fix: gate group activation changes by owner (#97838)
1 parent 444a093 commit 3d4b7ca

2 files changed

Lines changed: 102 additions & 5 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Tests owner gating for group activation session changes.
2+
import { beforeEach, describe, expect, it, vi } from "vitest";
3+
import type { HandleCommandsParams } from "./commands-types.js";
4+
5+
const persistSessionEntryMock = vi.hoisted(() => vi.fn(async () => true));
6+
7+
vi.mock("./commands-session-store.js", () => ({
8+
persistSessionEntry: persistSessionEntryMock,
9+
}));
10+
11+
function buildActivationParams(
12+
overrides: {
13+
commandBody?: string;
14+
isAuthorizedSender?: boolean;
15+
senderIsOwner?: boolean;
16+
} = {},
17+
): HandleCommandsParams {
18+
const commandBody = overrides.commandBody ?? "/activation always";
19+
return {
20+
cfg: { commands: { text: true } },
21+
ctx: {
22+
CommandSource: "text",
23+
CommandAuthorized: overrides.isAuthorizedSender ?? true,
24+
CommandBody: commandBody,
25+
Surface: "telegram",
26+
Provider: "telegram",
27+
},
28+
command: {
29+
commandBodyNormalized: commandBody,
30+
rawBodyNormalized: commandBody,
31+
isAuthorizedSender: overrides.isAuthorizedSender ?? true,
32+
senderIsOwner: overrides.senderIsOwner ?? true,
33+
senderId: "group-member",
34+
channel: "telegram",
35+
channelId: "telegram",
36+
surface: "telegram",
37+
ownerList: ["owner"],
38+
from: "group-member",
39+
to: "bot",
40+
},
41+
directives: {},
42+
elevated: { enabled: true, allowed: true, failures: [] },
43+
sessionKey: "telegram:group:main",
44+
sessionEntry: {
45+
sessionId: "session-1",
46+
updatedAt: 1,
47+
channel: "telegram",
48+
chatType: "group",
49+
groupActivation: "mention",
50+
},
51+
sessionStore: {},
52+
workspaceDir: "/tmp/workspace",
53+
defaultGroupActivation: () => "mention",
54+
resolvedVerboseLevel: "off",
55+
resolvedReasoningLevel: "off",
56+
resolveDefaultThinkingLevel: async () => undefined,
57+
provider: "openai",
58+
model: "gpt-5.5",
59+
contextTokens: 0,
60+
isGroup: true,
61+
} as unknown as HandleCommandsParams;
62+
}
63+
64+
describe("handleActivationCommand", () => {
65+
beforeEach(() => {
66+
persistSessionEntryMock.mockClear();
67+
});
68+
69+
it("rejects authorized non-owner senders without changing group activation", async () => {
70+
const { handleActivationCommand } = await import("./commands-session.js");
71+
const params = buildActivationParams({ senderIsOwner: false });
72+
73+
const result = await handleActivationCommand(params, true);
74+
75+
expect(result).toEqual({ shouldContinue: false });
76+
expect(params.sessionEntry?.groupActivation).toBe("mention");
77+
expect(params.sessionEntry?.groupActivationNeedsSystemIntro).toBeUndefined();
78+
expect(persistSessionEntryMock).not.toHaveBeenCalled();
79+
});
80+
81+
it("allows owners to change group activation", async () => {
82+
const { handleActivationCommand } = await import("./commands-session.js");
83+
const params = buildActivationParams();
84+
85+
const result = await handleActivationCommand(params, true);
86+
87+
expect(result).toEqual({
88+
shouldContinue: false,
89+
reply: { text: "⚙️ Group activation set to always." },
90+
});
91+
expect(params.sessionEntry?.groupActivation).toBe("always");
92+
expect(params.sessionEntry?.groupActivationNeedsSystemIntro).toBe(true);
93+
expect(persistSessionEntryMock).toHaveBeenCalledWith(params);
94+
});
95+
});

src/auto-reply/reply/commands-session.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -225,11 +225,13 @@ export const handleActivationCommand: CommandHandler = async (params, allowTextC
225225
reply: { text: "⚙️ Group activation only applies to group chats." },
226226
};
227227
}
228-
if (!params.command.isAuthorizedSender) {
229-
logVerbose(
230-
`Ignoring /activation from unauthorized sender in group: ${params.command.senderId || "<unknown>"}`,
231-
);
232-
return { shouldContinue: false };
228+
const unauthorizedResult = rejectUnauthorizedCommand(params, "/activation");
229+
if (unauthorizedResult) {
230+
return unauthorizedResult;
231+
}
232+
const nonOwnerResult = rejectNonOwnerCommand(params, "/activation");
233+
if (nonOwnerResult) {
234+
return nonOwnerResult;
233235
}
234236
if (!activationCommand.mode) {
235237
return {

0 commit comments

Comments
 (0)