Ctrl K
ComunidadGuías…

server/bot-visibility.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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<string, string[]> = { "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<string, { botId: string; groupId?: string }>)[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<Record<string, unknown>> = [
      { 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<string, unknown>;
    // 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<string, unknown>;
    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<string, unknown>;
    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<string>(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" });
  });
});