import { describe, expect, it } from "vitest"; import { audienceWithin, intersectAudience, narrowestAudience, frameForMember, memberBody, memberBot, memberFramePayload, noteSeen, parseVisibility, bodyThreadIds, pathSubject, requestThreadIds, roomFeeds, routineRunVisible, routineVisible, sameAudience, webhookVisible, SEES_EVERYTHING, storedVisibility, viewerSees, VisibleSet, type FrameContext, type StreamSeen, type Viewer, } from "./bot-visibility.ts"; const ADA: Viewer = { kind: "member", email: "ada@example.test" }; const BOB: Viewer = { kind: "member", email: "bob@example.test" }; const NOBODY: Viewer = { kind: "member" }; /** A Team Cloud's admin: every bot, and only their own private chats (audience.ts). */ const BEN_ADMIN: Viewer = { kind: "all", email: "ben@example.test" }; const bots = [ { id: "pub", threadId: "t-pub", section: "Ops", tasks: [{ threadId: "t-pub" }, { threadId: "t-pub-2" }] }, { id: "hr", threadId: "t-hr", section: "People", visibility: { people: ["ada@example.test"] }, avatarUrl: "/api/attachments/hr.png", tasks: [{ threadId: "t-hr" }] }, { id: "adm", threadId: "t-adm", section: "People", visibility: "admins" }, { id: "corp", threadId: "t-corp", visibility: { people: ["@example.test"] } }, ]; const groups = [ { id: "room-pub", threadId: "t-room-pub", memberIds: ["pub"], section: "Ops" }, { id: "room-mixed", threadId: "t-room-mixed", memberIds: ["pub", "hr"], tasks: [{ threadId: "t-room-mixed-2" }] }, { id: "room-empty", threadId: "t-room-empty", memberIds: ["gone"] }, ]; describe("visibility values", () => { it("reads an admin's input and fails closed on anything it cannot read", () => { expect(parseVisibility("everyone")).toEqual({ ok: true, visibility: "everyone" }); expect(parseVisibility(null)).toEqual({ ok: true, visibility: "everyone" }); expect(parseVisibility(undefined).ok).toBe(false); expect(parseVisibility("admins")).toEqual({ ok: true, visibility: "admins" }); expect(parseVisibility({ people: [" Ada@Example.test ", "ada@example.test", "@corp.test", ""] })) .toEqual({ ok: true, visibility: { people: ["ada@example.test", "@corp.test"] } }); expect(parseVisibility({ people: [] })).toEqual({ ok: true, visibility: "admins" }); expect(parseVisibility({ people: ["not an email"] }).ok).toBe(false); expect(parseVisibility({ people: ["a@b.c"], extra: 1 }).ok).toBe(false); expect(parseVisibility("members").ok).toBe(false); expect(parseVisibility({ people: Array.from({ length: 501 }, (_, i) => `p${i}@x.test`) }).ok).toBe(false); expect(storedVisibility(undefined)).toBe("everyone"); expect(storedVisibility("garbage")).toBe("admins"); expect(storedVisibility({ people: "ada@example.test" })).toBe("admins"); }); it("lets admins and the owner see everything, and members only what names them", () => { expect(viewerSees(SEES_EVERYTHING, "admins")).toBe(true); expect(viewerSees(ADA, undefined)).toBe(true); expect(viewerSees(ADA, "admins")).toBe(false); expect(viewerSees(ADA, { people: ["ada@example.test"] })).toBe(true); expect(viewerSees({ kind: "member", email: "ADA@example.test" }, { people: ["ada@example.test"] })).toBe(true); expect(viewerSees(BOB, { people: ["ada@example.test"] })).toBe(false); expect(viewerSees(BOB, { people: ["@example.test"] })).toBe(true); expect(viewerSees({ kind: "member", email: "eve@evilexample.test" }, { people: ["@example.test"] })).toBe(false); // a paired device with no email is only ever shown bots everyone sees expect(viewerSees(NOBODY, { people: ["@example.test"] })).toBe(false); expect(viewerSees(NOBODY, undefined)).toBe(true); }); it("knows when one audience sits inside another", () => { expect(audienceWithin({ people: ["ada@example.test"] }, undefined)).toBe(true); expect(audienceWithin(undefined, { people: ["ada@example.test"] })).toBe(false); expect(audienceWithin("admins", { people: ["ada@example.test"] })).toBe(true); expect(audienceWithin({ people: ["ada@example.test"] }, "admins")).toBe(false); expect(audienceWithin({ people: ["ada@example.test"] }, { people: ["ada@example.test", "bob@example.test"] })).toBe(true); expect(audienceWithin({ people: ["ada@example.test", "bob@example.test"] }, { people: ["ada@example.test"] })).toBe(false); expect(audienceWithin({ people: ["ada@example.test"] }, { people: ["@example.test"] })).toBe(true); expect(audienceWithin({ people: ["@example.test"] }, { people: ["ada@example.test"] })).toBe(false); expect(roomFeeds([undefined, { people: ["ada@example.test"] }], undefined)).toBe(false); expect(roomFeeds([undefined, { people: ["ada@example.test"] }], { people: ["ada@example.test"] })).toBe(true); expect(roomFeeds([undefined, undefined], undefined)).toBe(true); }); it("finds the people two audiences share", () => { expect(intersectAudience(undefined, "admins")).toBe("admins"); expect(intersectAudience(undefined, undefined)).toBe("everyone"); expect(intersectAudience({ people: ["ada@example.test"] }, undefined)).toEqual({ people: ["ada@example.test"] }); expect(intersectAudience({ people: ["ada@example.test", "bob@example.test"] }, { people: ["bob@example.test", "cy@example.test"] })).toEqual({ people: ["bob@example.test"] }); expect(intersectAudience({ people: ["@example.test"] }, { people: ["ada@example.test", "eve@other.test"] })).toEqual({ people: ["ada@example.test"] }); expect(intersectAudience({ people: ["ada@example.test"] }, { people: ["bob@example.test"] })).toBe("admins"); expect(narrowestAudience([undefined, { people: ["ada@example.test", "bob@example.test"] }, { people: ["ada@example.test"] }])).toEqual({ people: ["ada@example.test"] }); }); it("keeps a room at its floor after the bot that set it is gone", () => { const open = bots.map(({ visibility: _v, ...bot }) => bot); const floored = [{ id: "room-old", threadId: "t-room-old", memberIds: ["pub"], audienceFloor: { people: ["ada@example.test"] } }]; const bob = new VisibleSet(open, floored, BOB); expect(bob.everything).toBe(false); expect([bob.group("room-old"), bob.thread("t-room-old"), bob.bot("pub")]).toEqual([false, false, true]); expect(new VisibleSet(open, floored, ADA).group("room-old")).toBe(true); expect(roomFeeds([undefined, floored[0]!.audienceFloor], undefined)).toBe(false); }); it("compares audiences exactly", () => { expect(sameAudience(undefined, "everyone")).toBe(true); expect(sameAudience(undefined, "admins")).toBe(false); expect(sameAudience({ people: ["a@x.test", "b@x.test"] }, { people: ["b@x.test", "a@x.test"] })).toBe(true); expect(sameAudience({ people: ["a@x.test"] }, { people: ["a@x.test", "b@x.test"] })).toBe(false); expect(sameAudience({ people: ["a@x.test"] }, "admins")).toBe(false); }); }); describe("VisibleSet", () => { it("filters nothing when no bot is restricted, or for someone who sees everything", () => { const open = new VisibleSet(bots.map(({ visibility: _v, ...bot }) => bot), groups, BOB); expect(open.everything).toBe(true); expect(open.bot("anything")).toBe(true); const admin = new VisibleSet(bots, groups, SEES_EVERYTHING); expect(admin.everything).toBe(true); expect(admin.thread("t-hr")).toBe(true); }); it("answers bots, rooms, threads and sections for a member", () => { const bob = new VisibleSet(bots, groups, BOB); expect(bob.everything).toBe(false); expect([bob.bot("pub"), bob.bot("hr"), bob.bot("adm"), bob.bot("corp"), bob.bot("unknown")]).toEqual([true, false, false, true, false]); expect([bob.group("room-pub"), bob.group("room-mixed"), bob.group("room-empty")]).toEqual([true, false, false]); expect([bob.thread("t-pub-2"), bob.thread("t-hr"), bob.thread("t-room-mixed-2"), bob.thread("t-unknown")]).toEqual([true, false, false, false]); expect(bob.sections(["Ops", "People", "Empty"])).toEqual(["Ops"]); const ada = new VisibleSet(bots, groups, ADA); expect([ada.bot("hr"), ada.bot("adm"), ada.group("room-mixed"), ada.thread("t-room-mixed-2")]).toEqual([true, false, true, true]); expect(ada.sections(["Ops", "People"])).toEqual(["Ops", "People"]); }); it("refuses an attachment only when everything that uses it is hidden", () => { const bob = new VisibleSet(bots, groups, BOB); const refs: Record = { "in-hr.png": ["t-hr"], "shared.png": ["t-hr", "t-pub"], "fresh.png": [] }; const lookup = (name: string) => refs[name] ?? []; expect(bob.attachment("hr.png", lookup)).toBe(false); // the restricted bot's avatar expect(bob.attachment("in-hr.png", lookup)).toBe(false); expect(bob.attachment("shared.png", lookup)).toBe(true); expect(bob.attachment("fresh.png", lookup)).toBe(true); expect(new VisibleSet(bots, groups, ADA).attachment("in-hr.png", lookup)).toBe(true); }); it("routes each path to the entity it names", () => { expect(pathSubject("/api/bots/hr/messages")).toEqual({ kind: "bot", id: "hr" }); expect(pathSubject("/api/bots/hr")).toEqual({ kind: "bot", id: "hr" }); expect(pathSubject("/api/threads/t-hr/export")).toEqual({ kind: "thread", id: "t-hr" }); expect(pathSubject("/api/groups/room-mixed/read")).toEqual({ kind: "group", id: "room-mixed" }); expect(pathSubject("/api/routines/r1/run")).toEqual({ kind: "routine", id: "r1" }); expect(pathSubject("/api/routines/wake")).toBeNull(); expect(pathSubject("/api/routine-runs/run1/seen")).toEqual({ kind: "routine-run", id: "run1" }); expect(pathSubject("/api/routine-runs/seen-all")).toBeNull(); expect(pathSubject("/api/attachments/a.png")).toEqual({ kind: "attachment", name: "a.png" }); expect(pathSubject("/api/attachments")).toBeNull(); expect(pathSubject("/api/bots")).toBeNull(); expect(pathSubject("/api/groups")).toBeNull(); }); it("follows a routine to its room when it runs in one", () => { const bob = new VisibleSet(bots, groups, BOB); expect(routineVisible({ botId: "pub" }, bob)).toBe(true); expect(routineVisible({ botId: "hr" }, bob)).toBe(false); expect(routineVisible({ botId: "pub", groupId: "room-mixed" }, bob)).toBe(false); }); it("shows a room webhook only to someone who sees both its bot and its room", () => { const bob = new VisibleSet(bots, groups, BOB); expect(webhookVisible({ botId: "pub" }, bob)).toBe(true); expect(webhookVisible({ botId: "pub", groupId: "room-pub" }, bob)).toBe(true); expect(webhookVisible({ botId: "pub", groupId: "room-mixed" }, bob)).toBe(false); expect(webhookVisible({ botId: "hr", groupId: "room-pub" }, bob)).toBe(false); expect(webhookVisible({ botId: "hr", groupId: "room-mixed" }, new VisibleSet(bots, groups, SEES_EVERYTHING))).toBe(true); }); }); describe("live frames for a member", () => { const context = (visible: VisibleSet): FrameContext => ({ visible, webhook: (id) => ({ "hook-hr": { botId: "hr" }, "hook-pub": { botId: "pub" }, "hook-room-mixed": { botId: "pub", groupId: "room-mixed" } } as Record)[id], freshBot: (id) => ({ id, messages: [{ id: "m1" }] }), freshGroup: (id) => ({ id, messages: [] }), }); const seen = (): StreamSeen => ({ bots: new Set(["pub", "corp"]), groups: new Set(["room-pub"]) }); it("drops every frame about a hidden bot, thread or room", () => { const bob = new VisibleSet(bots, groups, BOB); const ctx = context(bob); const hidden: Array> = [ { kind: "message", threadId: "t-hr", message: { id: "x" } }, { kind: "message.patch", threadId: "t-room-mixed", message: { id: "x" } }, { kind: "thread", threadId: "t-hr", activeLeafId: "x" }, { kind: "bot", bot: { id: "hr" } }, { kind: "group", group: { id: "room-mixed" } }, { kind: "bot.deleted", botId: "hr" }, { kind: "group.deleted", groupId: "room-mixed" }, { kind: "notify", notification: { botId: "hr", threadId: "t-hr" } }, { kind: "runtime", event: { type: "content.delta", threadId: "t-hr" } }, { kind: "screen", botId: "hr", threadId: "t-hr", png: "…" }, { kind: "computer", botId: "adm", state: "waking" }, { kind: "computer-control", botId: "adm", held: true }, { kind: "routine", routine: { id: "r", botId: "hr" } }, { kind: "routine.run", run: { id: "r", botId: "pub", groupId: "room-mixed" } }, { kind: "webhook", webhook: { id: "hook-hr", botId: "hr" } }, { kind: "webhook.attempt", attempt: { webhookId: "hook-hr" } }, { kind: "webhook", webhook: { id: "hook-room-mixed", botId: "pub", groupId: "room-mixed" } }, { kind: "webhook.attempt", attempt: { webhookId: "hook-room-mixed" } }, { kind: "future-kind", threadId: "t-hr" }, ]; for (const frame of hidden) expect(frameForMember(frame, ctx, seen()), JSON.stringify(frame)).toBeUndefined(); }); it("passes visible frames through untouched and narrows the lists", () => { const bob = new VisibleSet(bots, groups, BOB); const ctx = context(bob); const message = { kind: "message", threadId: "t-pub-2", message: { id: "x" } }; expect(frameForMember(message, ctx, seen())).toBe(message); const hook = { kind: "webhook.attempt", attempt: { webhookId: "hook-pub" } }; expect(frameForMember(hook, ctx, seen())).toBe(hook); expect(frameForMember({ kind: "sections", sections: ["Ops", "People"] }, ctx, seen())).toEqual({ kind: "sections", sections: ["Ops"] }); expect(frameForMember({ kind: "bot.queued", queues: { "t-pub": [1], "t-hr": [2] } }, ctx, seen())).toEqual({ kind: "bot.queued", queues: { "t-pub": [1] } }); expect(frameForMember({ kind: "bots.order", botIds: ["hr", "pub", "corp"] }, ctx, seen())).toEqual({ kind: "bots.order", botIds: ["pub", "corp"] }); const config = { kind: "config", membership: {} }; expect(frameForMember(config, ctx, seen())).toBe(config); expect(frameForMember({ kind: "bot", bot: { id: "corp", visibility: { people: ["@example.test"] } } }, ctx, seen())) .toEqual({ kind: "bot", bot: { id: "corp" } }); }); it("withdraws a bot once when it becomes hidden, and sends it whole when it appears", () => { const state = seen(); const before = new VisibleSet(bots, groups, BOB); // Bob was shown "pub"; now an admin restricts it to Ada. const restricted = bots.map((bot) => (bot.id === "pub" ? { ...bot, visibility: { people: ["ada@example.test"] } } : bot)); const after = new VisibleSet(restricted, groups, BOB); expect(frameForMember({ kind: "bot", bot: { id: "pub" } }, context(after), state)).toEqual({ kind: "bot.deleted", botId: "pub" }); expect(frameForMember({ kind: "bot", bot: { id: "pub" } }, context(after), state)).toBeUndefined(); expect(frameForMember({ kind: "bot", bot: { id: "pub", name: "Pub" } }, context(before), state)) .toEqual({ kind: "bot", bot: { id: "pub", name: "Pub", messages: [{ id: "m1" }] } }); expect(state.bots.has("pub")).toBe(true); const roomState = seen(); const hiddenRoom = new VisibleSet(restricted, groups, BOB); expect(frameForMember({ kind: "group", group: { id: "room-pub" } }, context(hiddenRoom), roomState)).toEqual({ kind: "group.deleted", groupId: "room-pub" }); }); it("keeps a stream's record current while nothing is restricted", () => { const state: StreamSeen = { bots: new Set(), groups: new Set() }; noteSeen({ kind: "bot", bot: { id: "new" } }, state); noteSeen({ kind: "group", group: { id: "room" } }, state); expect([...state.bots, ...state.groups]).toEqual(["new", "room"]); noteSeen({ kind: "bot.deleted", botId: "new" }, state); expect(state.bots.size).toBe(0); }); it("narrows every bot in a JSON answer for a member", () => { const bob = new VisibleSet(bots, groups, BOB); const body = { bot: { id: "corp", visibility: { people: ["@example.test"] } }, bots: [{ id: "pub", visibility: "everyone" }], result: { bot: { id: "corp", visibility: "admins" } }, messages: [{ bot: { visibility: "untouched" } }], ok: true, }; expect(memberBody(body, bob)).toEqual({ bot: { id: "corp" }, bots: [{ id: "pub" }], result: { bot: { id: "corp" } }, messages: [{ bot: { visibility: "untouched" } }], ok: true, }); const admin = new VisibleSet(bots, groups, SEES_EVERYTHING); expect(memberBody(body, admin)).toBe(body); }); it("strips nothing for someone who sees everything", () => { const bot = { id: "hr", visibility: "admins" }; expect(memberBot(bot, new VisibleSet(bots, groups, SEES_EVERYTHING))).toBe(bot); }); it("keeps a bot's own server alias from every member, whether or not they can see every bot", () => { const open = new VisibleSet(bots.map(({ visibility: _v, ...bot }) => bot), [{ ...groups[0]!, audienceFloor: "everyone" }], BOB); const narrowed = new VisibleSet(bots, groups, ADA); expect(open.everything).toBe(true); expect(narrowed.everything).toBe(false); const bot = { id: "pub", cloudBackend: "vps", vpsAlias: "alpha-vps" }; expect(memberBot(bot, open)).toEqual({ id: "pub", cloudBackend: "vps" }); expect(memberBot(bot, narrowed)).toEqual({ id: "pub", cloudBackend: "vps" }); // The owner and admins share the same everything-visible path and keep it. const admin = new VisibleSet(bots, groups, SEES_EVERYTHING); expect(memberBot(bot, admin)).toBe(bot); expect(memberBody({ bots: [bot] }, admin)).toEqual({ bots: [bot] }); const room = { id: "room-pub", audienceFloor: "everyone" }; const body = { bots: [bot, { id: "hr" }], group: room, result: { bot } }; const shown = memberBody(body, open) as typeof body; expect(JSON.stringify(shown)).not.toContain("alpha-vps"); expect(shown.bots[1]).toBe(body.bots[1]); // A member who sees every room gets it as it is; only a narrowed member loses the audience floor. expect(shown.group).toBe(room); expect((memberBody(body, narrowed) as typeof body).group).toEqual({ id: "room-pub" }); // The live frame every member shares, and an untouched frame kept as the same object for the shared bytes. expect(memberFramePayload({ kind: "bot", bot })).toEqual({ kind: "bot", bot: { id: "pub", cloudBackend: "vps" } }); const plain = { kind: "bot", bot: { id: "pub" } }; expect(memberFramePayload(plain)).toBe(plain); const config = { kind: "config", vpsAlias: "not a bot" }; expect(memberFramePayload(config)).toBe(config); }); }); describe("a Team Cloud's audiences", () => { const ADA_KEY = "p_adaadaadaadaadaadaada1", BEN_KEY = "p_benbenbenbenbenbenben1"; const audienceOf = (threadId: string) => threadId === "t-pub-2" ? `member:${ADA_KEY}` as const : threadId === "t-pub" ? "team" as const : threadId.startsWith("t-room") ? "room:room-pub" as const : undefined; const open = bots.map(({ visibility: _v, ...bot }) => bot); const openGroups = [{ id: "room-pub", threadId: "t-room-pub", memberIds: ["pub"], tasks: [{ threadId: "t-room-pub-2" }] }]; it("a member sees the team's conversations, the rooms they see and their own; never another member's", () => { const ada = new VisibleSet(open, openGroups, ADA, undefined, { of: audienceOf, person: ADA_KEY }); expect(ada.everything).toBe(false); expect(ada.filtersThreads).toBe(true); expect([ada.thread("t-pub"), ada.thread("t-pub-2"), ada.thread("t-room-pub"), ada.thread("t-room-pub-2")]).toEqual([true, true, true, true]); const ben = new VisibleSet(open, openGroups, BOB, undefined, { of: audienceOf, person: BEN_KEY }); expect([ben.thread("t-pub"), ben.thread("t-pub-2"), ben.thread("t-room-pub")]).toEqual([true, false, true]); expect(ben.bot("pub")).toBe(true); const nobody = new VisibleSet(open, openGroups, NOBODY, undefined, { of: audienceOf, person: null }); expect(nobody.thread("t-pub-2")).toBe(false); // The server itself (no person, no audiences) sees everything. expect(new VisibleSet(open, openGroups, SEES_EVERYTHING).thread("t-pub-2")).toBe(true); // Without audiences (a My Cloud, a desktop) nothing changes: an open workspace is everything to a member. expect(new VisibleSet(open, openGroups, BOB).everything).toBe(true); }); it("an admin, the Owner too, sees every bot and room and the team's conversations, but only their own private chat", () => { const audiences = (threadId: string) => threadId === "t-pub-3" ? `member:${BEN_KEY}` as const : audienceOf(threadId); const withBens = [{ ...open[0]!, tasks: [...open[0]!.tasks!, { threadId: "t-pub-3" }] }, ...open.slice(1)]; const admin = new VisibleSet(withBens, openGroups, BEN_ADMIN, undefined, { of: audiences, person: BEN_KEY }); expect(admin.everything).toBe(false); expect(admin.filtersThreads).toBe(true); expect(admin.member).toBe(false); expect([admin.thread("t-pub"), admin.thread("t-pub-3"), admin.thread("t-room-pub"), admin.thread("t-room-pub-2")]).toEqual([true, true, true, true]); expect(admin.thread("t-pub-2")).toBe(false); expect(admin.hides("t-pub-2")).toBe(true); expect(admin.hides("t-pub")).toBe(false); // Every bot (a restricted one too), every room and every section stay an admin's. expect(withBens.every((bot) => admin.bot(bot.id))).toBe(true); expect(admin.group("room-pub")).toBe(true); expect(admin.sections(["Ops", "People", "Empty"])).toEqual(["Ops", "People", "Empty"]); // A deleted private chat stays its person's: what still names it (a usage row) is never an admin's. const gone = new VisibleSet(withBens, openGroups, BEN_ADMIN, (threadId) => (threadId === "t-pub-2" ? undefined : { bot: "pub" }), { of: audiences, person: BEN_KEY }); expect(gone.thread("t-pub-2")).toBe(false); expect(gone.hides("t-pub-2")).toBe(true); // An unknown conversation with no audience is as before: the route answers it. expect(admin.thread("t-unknown")).toBe(true); expect(admin.hides("t-unknown")).toBe(false); }); it("a bot sent to an admin keeps its setup but loses another person's chats, their messages and their unread mark", () => { const admin = new VisibleSet(open, openGroups, BEN_ADMIN, undefined, { of: audienceOf, person: BEN_KEY }); const bot = { id: "pub", threadId: "t-pub-2", mainThreadId: "t-pub", visibility: "everyone", vpsAlias: "box", unread: true, messages: [{ text: "Kumquat" }], hasMore: true, pinnedMessageId: "m1", tasks: [{ threadId: "t-pub" }, { threadId: "t-pub-2", unread: true }] }; const sent = memberBot(bot, admin) as Record; // It also says what they may do (an admin edits every bot) and carries the bot's sharing. expect(sent).toEqual({ id: "pub", threadId: "t-pub", mainThreadId: "t-pub", visibility: "everyone", vpsAlias: "box", unread: false, tasks: [{ threadId: "t-pub", title: "Shared chat", sharedChat: true }], access: { everyone: "chat", people: {} }, yourAccess: "edit" }); expect(JSON.stringify(memberBody({ bots: [bot], bot }, admin))).not.toContain("Kumquat"); // A room keeps its audience floor for an admin. const room = { id: "room-pub", audienceFloor: "admins" }; expect((memberBody({ group: room }, admin) as { group: unknown }).group).toBe(room); // The selected chat with no main chat to name instead: the field goes, never the hidden id. expect(memberBot({ id: "pub", threadId: "t-pub-2" }, admin)).toEqual({ id: "pub", access: { everyone: "chat", people: {} }, yourAccess: "edit" }); }); it("says on everyone's copy when only the people added see a bot, without naming them to anyone who can't edit it", () => { const restricted = { id: "pub", threadId: "t-pub", tasks: [{ threadId: "t-pub" }], visibility: { people: ["ada@x.test"] } }; const member = memberBot(restricted, new VisibleSet(open, openGroups, ADA, undefined, { of: audienceOf, person: ADA_KEY })) as Record; expect(member).toMatchObject({ onlyPeopleAdded: true }); expect(JSON.stringify(member)).not.toContain("ada@x.test"); const admin = new VisibleSet(open, openGroups, BEN_ADMIN, undefined, { of: audienceOf, person: BEN_KEY }); expect(memberBot(restricted, admin)).toMatchObject({ onlyPeopleAdded: true }); expect(memberBot({ ...restricted, visibility: "admins" }, admin)).toMatchObject({ onlyPeopleAdded: true }); expect(memberBot({ ...restricted, visibility: "everyone" }, admin)).not.toHaveProperty("onlyPeopleAdded"); expect(memberBot({ id: "pub", threadId: "t-pub", tasks: [{ threadId: "t-pub" }] }, admin)).not.toHaveProperty("onlyPeopleAdded"); }); it("a main chat named as another person's private chat is replaced by a conversation the viewer reads, never sent", () => { const admin = new VisibleSet(open, openGroups, BEN_ADMIN, undefined, { of: audienceOf, person: BEN_KEY }); const bot = { id: "pub", threadId: "t-pub-2", mainThreadId: "t-pub-2", tasks: [{ threadId: "t-pub-2" }, { threadId: "t-pub" }] }; const sent = memberBot(bot, admin) as Record; expect(sent).toEqual({ id: "pub", threadId: "t-pub", mainThreadId: "t-pub", tasks: [{ threadId: "t-pub", title: "Shared chat", sharedChat: true }], access: { everyone: "chat", people: {} }, yourAccess: "edit" }); expect(memberBot({ ...bot, threadId: "t-pub" }, admin)).toEqual({ id: "pub", threadId: "t-pub", mainThreadId: "t-pub", tasks: [{ threadId: "t-pub", title: "Shared chat", sharedChat: true }], access: { everyone: "chat", people: {} }, yourAccess: "edit" }); }); it("calls a team bot's main chat what it is on every person's copy, and nothing else", () => { const ada = new VisibleSet(open, openGroups, ADA, undefined, { of: audienceOf, person: ADA_KEY }); const bot = { id: "pub", threadId: "t-pub-2", mainThreadId: "t-pub", tasks: [{ threadId: "t-pub", title: "Rooms today: 12" }, { threadId: "t-pub-2", title: "New thread", private: true }] }; expect((memberBot(bot, ada) as { tasks: unknown[] }).tasks).toEqual([{ threadId: "t-pub", title: "Shared chat", sharedChat: true }, { threadId: "t-pub-2", title: "New thread", private: true }]); const ownMain = { ...bot, threadId: "t-pub-2", mainThreadId: "t-pub-2" }; expect((memberBot(ownMain, ada) as { tasks: Array<{ title: string }> }).tasks[1]!.title, "a private chat is never the shared one").toBe("New thread"); const ownHelper = { ...bot, defaultBot: { person: "ada@x.test" } }; expect((memberBot(ownHelper, ada) as { tasks: Array<{ title: string }> }).tasks[0]!.title, "a member's own MausBot shares nothing").toBe("Rooms today: 12"); const offTeam = new VisibleSet(open, openGroups, ADA); expect((memberBot(bot, offTeam) as { tasks: Array<{ title: string }> }).tasks[0]!.title, "off a team nothing changes").toBe("Rooms today: 12"); }); it("a live frame from another person's private chat never reaches an admin's stream", () => { const admin = new VisibleSet(open, openGroups, BEN_ADMIN, undefined, { of: audienceOf, person: BEN_KEY }); const ctx: FrameContext = { visible: admin, webhook: () => undefined, freshBot: () => undefined, freshGroup: () => undefined }; const seen: StreamSeen = { bots: new Set(open.map((bot) => bot.id)), groups: new Set(["room-pub"]) }; for (const frame of [ { kind: "message", threadId: "t-pub-2", message: { text: "Kumquat" } }, { kind: "message.patch", threadId: "t-pub-2", message: { text: "Kumquat" } }, { kind: "notify", notification: { threadId: "t-pub-2", botId: "pub", body: "Kumquat" } }, { kind: "runtime", event: { threadId: "t-pub-2" } }, { kind: "screen", botId: "pub", threadId: "t-pub-2" }, { kind: "notifications.read", threadId: "t-pub-2", ids: ["n1"] }, { kind: "routine.run", run: { botId: "pub", threadId: "t-pub-2", output: "Kumquat" } }, ]) expect(frameForMember(frame, ctx, seen), frame.kind).toBeUndefined(); expect(frameForMember({ kind: "message", threadId: "t-pub", message: {} }, ctx, seen)).toBeDefined(); const queued = frameForMember({ kind: "bot.queued", queues: { "t-pub": [1], "t-pub-2": [2] } }, ctx, seen); expect(queued).toEqual({ kind: "bot.queued", queues: { "t-pub": [1] } }); const bot = frameForMember({ kind: "bot", bot: { id: "pub", threadId: "t-pub", visibility: "everyone", tasks: [{ threadId: "t-pub" }, { threadId: "t-pub-2" }] } }, ctx, seen); expect(bot).toEqual({ kind: "bot", bot: { id: "pub", threadId: "t-pub", visibility: "everyone", tasks: [{ threadId: "t-pub" }], access: { everyone: "chat", people: {} }, yourAccess: "edit" } }); const group = { kind: "group", group: { id: "room-pub", audienceFloor: "admins" } }; expect(frameForMember(group, ctx, seen)).toBe(group); // A plea for a hand from Ada's chat: the computer is held, nothing more. expect(frameForMember({ kind: "computer-control", botId: "pub", held: true, helpReason: "Sign in to the bank", help: { botId: "pub", threadId: "t-pub-2", site: "bank.test" } }, ctx, seen)) .toEqual({ kind: "computer-control", botId: "pub", held: true, helpReason: null }); const teams = { kind: "computer-control", botId: "pub", held: true, helpReason: "Sign in", help: { botId: "pub", threadId: "t-pub", site: "x.test" } }; expect(frameForMember(teams, ctx, seen)).toBe(teams); expect(routineRunVisible({ botId: "pub", threadId: "t-pub-2" }, admin)).toBe(false); expect(routineRunVisible({ botId: "pub", threadId: "t-pub", resultsThreadId: "t-pub-2" }, admin)).toBe(false); expect(routineRunVisible({ botId: "pub", threadId: "t-pub" }, admin)).toBe(true); }); it("sends what a bot is doing now only to someone who may see that chat, as the Activity panel does", () => { const admin = new VisibleSet(open, openGroups, BEN_ADMIN, undefined, { of: audienceOf, person: BEN_KEY }); const ctx: FrameContext = { visible: admin, webhook: () => undefined, freshBot: () => undefined, freshGroup: () => undefined }; const seen: StreamSeen = { bots: new Set(open.map((bot) => bot.id)), groups: new Set(["room-pub"]) }; const step = (threadId: string) => ({ kind: "runtime", event: { type: "turn.progress", threadId, activity: "command", itemId: "c1", target: "payroll.py", detail: "python3 payroll.py --month 10", quiet: { kind: "busy" }, tail: ["paid 12 of 40"] } }); const started = (threadId: string) => ({ kind: "runtime", event: { type: "item.started", itemType: "tool", threadId, itemId: "c1", title: "Bash", step: { activity: "command", target: "payroll.py", detail: "python3 payroll.py --month 10" } } }); for (const frame of [step("t-pub-2"), started("t-pub-2"), { kind: "message", threadId: "t-pub-2", message: { tool: { name: "Bash", step: { activity: "command", target: "payroll.py" } } } }]) { expect(frameForMember(frame, ctx, seen), JSON.stringify(frame)).toBeUndefined(); } const own = step("t-pub"); expect(frameForMember(own, ctx, seen)).toBe(own); }); it("finds every conversation a request names: a task in the path, the query and the body", () => { expect(requestThreadIds("/api/bots/pub/tasks/t-pub-2", new URLSearchParams())).toEqual(["t-pub-2"]); expect(requestThreadIds("/api/bots/pub/tasks/t-pub-2/title", new URLSearchParams("threadId=t-x"))).toEqual(["t-pub-2", "t-x"]); expect(requestThreadIds("/api/groups/g/tasks/t-g", new URLSearchParams())).toEqual(["t-g"]); expect(requestThreadIds("/api/bots/pub/browser/live", new URLSearchParams("threadId=t-pub-2"))).toEqual(["t-pub-2"]); expect(requestThreadIds("/api/bots/pub", new URLSearchParams())).toEqual([]); expect(bodyThreadIds({ threadId: "a", resultsThreadId: "b", threadIds: ["c", 1], envelope: { threadId: "d" }, text: "threadId" })).toEqual(["a", "b", "c", "d"]); expect(bodyThreadIds({ deep: { deeper: { threadId: "x" } } })).toEqual([]); expect(bodyThreadIds([{ threadId: "x" }])).toEqual([]); expect(bodyThreadIds(null)).toEqual([]); }); it.each([false, true])("reads a request-sized threadIds array, nested: %s", (nested) => { const threadIds = Array(200_000).fill(""); threadIds[199_999] = "private-thread"; const body = nested ? { envelope: { threadIds } } : { threadIds }; expect(bodyThreadIds(body)).toEqual(threadIds); }); it("a bot sent to a member lists only the conversations that member may see", () => { const ben = new VisibleSet(open, openGroups, BOB, undefined, { of: audienceOf, person: BEN_KEY }); const sent = memberBot({ id: "pub", tasks: [{ threadId: "t-pub" }, { threadId: "t-pub-2" }], vpsAlias: "x" }, ben); expect(sent).toEqual({ id: "pub", tasks: [{ threadId: "t-pub" }], yourAccess: "chat" }); const ada = new VisibleSet(open, openGroups, ADA, undefined, { of: audienceOf, person: ADA_KEY }); expect(memberBot({ id: "pub", tasks: [{ threadId: "t-pub" }, { threadId: "t-pub-2" }] }, ada).tasks).toHaveLength(2); // Off a team the list is untouched. expect(memberBot({ id: "pub", tasks: [{ threadId: "t-pub-2" }] }, new VisibleSet(bots, groups, BOB)).tasks).toEqual([{ threadId: "t-pub-2" }]); }); it("a person's own MausBot exists for them alone: no admin and no other member sees it, whatever its sharing says", () => { // As an admin could once leave it: listed for everyone. const adas = { id: "adas-maus", threadId: "t-maus", defaultBot: { person: "ada@example.test" }, visibility: "everyone", access: { everyone: "chat", people: {} } }; const withMaus = [...open, adas]; const rooms = [...openGroups, { id: "room-maus", threadId: "t-room-maus", memberIds: ["pub", "adas-maus"] }]; const set = (viewer: Viewer, person: string | null) => new VisibleSet(withMaus, rooms, viewer, undefined, { of: audienceOf, person }); const ada = set(ADA, ADA_KEY), admin = set(BEN_ADMIN, BEN_KEY), member = set(BOB, BEN_KEY); expect([ada.bot("adas-maus"), ada.level("adas-maus"), ada.group("room-maus")]).toEqual([true, "edit", true]); for (const other of [admin, member, set({ kind: "all", email: "" }, null)]) { expect([other.bot("adas-maus"), other.level("adas-maus"), other.group("room-maus")]).toEqual([false, "none", false]); expect(other.bot("pub"), "team bots are as they were").toBe(true); expect(frameForMember({ kind: "bot", bot: { id: "adas-maus", name: "Confidant" } }, { visible: other, webhook: () => undefined, freshBot: () => undefined, freshGroup: () => undefined }, { bots: new Set(), groups: new Set() })).toBeUndefined(); expect(routineVisible({ botId: "adas-maus" }, other)).toBe(false); } expect(admin.bot("no-such-bot"), "an admin's unknown id is still the route's to answer").toBe(true); // The server itself, and anywhere that is not a team, sees it as before. expect(new VisibleSet(withMaus, rooms, SEES_EVERYTHING, undefined, { of: audienceOf, person: null }).bot("adas-maus")).toBe(true); expect(new VisibleSet(withMaus, rooms, BEN_ADMIN).bot("adas-maus")).toBe(true); }); it("a bot's reorder is no bot's route", () => { expect(pathSubject("/api/bots/order")).toBeNull(); expect(pathSubject("/api/bots/order-1/tasks")).toEqual({ kind: "bot", id: "order-1" }); }); });