diff --git a/packages/simplex-chat-mcp/src/control.ts b/packages/simplex-chat-mcp/src/control.ts index 3bd6597d73..a300786d80 100644 --- a/packages/simplex-chat-mcp/src/control.ts +++ b/packages/simplex-chat-mcp/src/control.ts @@ -66,6 +66,7 @@ export class Control implements Pick = Promise.resolve() private ownerCommands: Promise = Promise.resolve() + private events: Promise = Promise.resolve() private linkText?: string constructor( @@ -146,12 +147,18 @@ export class Control implements Pick logError("Main group command", error)) } - async onEvent(event: ChatEvent): Promise { + onEvent(event: ChatEvent): Promise { + const handled = this.events.then(() => this.handleEvent(event)) + this.events = handled.catch(() => undefined) + return handled + } + + private async handleEvent(event: ChatEvent): Promise { if (this.actions.lost) return if (losesControl(event)) return this.lose() const before = this.approvers() await this.refreshMembers() - if (event.type === "joinedGroupMember" && this.isHuman(event.member)) return this.ownerJoined(event.member) + if (event.type === "joinedGroupMember" && this.isHuman(event.member) && this.approvers().length === 0) return this.ownerJoined(event.member) if (before.length > 0 && this.approvers().length === 0) { log(noOwners) await this.openLink() @@ -230,8 +237,9 @@ export class Control implements Pick { const view = this.sessions.get(target.session) - const chatInfo = view === undefined ? undefined : await agentChat(view, target.chat) - if (view === undefined || chatInfo === undefined || approvalOf(customDataOf(chatInfo)) === undefined) return `Messages to ${targetText(target)} have no standing approval.` + if (view === undefined) return noSession(target.session) + const chatInfo = await agentChat(view, target.chat) + if (chatInfo === undefined || approvalOf(customDataOf(chatInfo)) === undefined) return `Messages to ${targetText(target)} have no standing approval.` const {simplexMcp, ...customData} = customDataOf(chatInfo) as {simplexMcp: unknown} await setCustomData(view.chat, target.chat, customData) return `Messages to ${chatName(chatSubject(chatInfo))} (${targetText(target)}) need approval again.` diff --git a/packages/simplex-chat-mcp/test/unit/commands.test.ts b/packages/simplex-chat-mcp/test/unit/commands.test.ts index d5a4252124..fe714de37c 100644 --- a/packages/simplex-chat-mcp/test/unit/commands.test.ts +++ b/packages/simplex-chat-mcp/test/unit/commands.test.ts @@ -237,7 +237,7 @@ describe("approval of all messages to a chat", () => { const {contact: directory} = chatInfoOf(chat, "@4") as T.ChatInfo.Direct directory.customData = {other: 1} await control.ownerCommand(alice, {keyword: "approve", params: "1:@4 all"}) - expect(await control.ownerCommand(alice, {keyword: "revoke", params: "2:@4"})).toBe("Messages to 2:@4 have no standing approval.") + expect(await control.ownerCommand(alice, {keyword: "revoke", params: "2:@4"})).toBe("No open session 2. /list shows the open sessions.") expect(await control.ownerCommand(alice, {keyword: "revoke", params: "1:@4"})).toBe("Messages to bot SimpleX Directory (1:@4) need approval again.") expect(customDataOf(chat, "@4")).toEqual({other: 1}) expect(control.standingApprover(chatInfoOf(chat, "@4"))).toBeUndefined() @@ -622,6 +622,20 @@ describe("sessions in the Main group", () => { expect(control.link).toBeUndefined() }) + it("makes only the first of two people who join in a row the owner", async () => { + const chat = agentChats() + chat.members.set(10, [self]) + const {control} = await controlWith(chat, actionsWith("requests", fakeHooks())) + await control.start() + const carol = member(9, "Carol", T.GroupMemberRole.Member) + const dave = member(11, "Dave", T.GroupMemberRole.Member) + chat.members.set(10, [self, carol, dave]) + const joined = (joiner: T.GroupMember) => control.onEvent({type: "joinedGroupMember", user, groupInfo: group(10, "SimpleX MCP"), member: joiner} as unknown as ChatEvent) + await Promise.all([joined(carol), joined(dave)]) + expect(chat.roles).toEqual([{groupId: 10, members: [9], role: T.GroupMemberRole.Owner}]) + expect(control.owners()).toEqual(["Carol"]) + }) + it("opens a new link when the last owner leaves", async () => { const {control, chat} = await setup() chat.members.set(10, [self, bob]) diff --git a/packages/simplex-chat-mcp/test/unit/fixtures.ts b/packages/simplex-chat-mcp/test/unit/fixtures.ts index 41227b0da2..c2a09fc144 100644 --- a/packages/simplex-chat-mcp/test/unit/fixtures.ts +++ b/packages/simplex-chat-mcp/test/unit/fixtures.ts @@ -202,6 +202,7 @@ export class FakeChat { async apiSetMembersRole(groupId: number, members: number[], role: T.GroupMemberRole): Promise { this.roles.push({groupId, members, role}) + this.members.set(groupId, (this.members.get(groupId) ?? []).map(m => (members.includes(m.groupMemberId) ? {...m, memberRole: role} : m))) } async apiNewGroup(userId: number, groupProfile: T.GroupProfile): Promise {