/** * Offering a new FabriChat room through a member's share inbox. Bob starts a * direct chat with Alice from her participant chip, whose click names her * profile, which points at her private inbox; the room is offered there, in * the envelope a share inbox takes, and she is added to its participants, * with no notice queued for her. A request naming a profile other than the * counterpart's is refused. Each person writes their own profile here, so its * label names them, as a Fabric profile's does. */ import { action, assert, type Cell, currentPrincipal, equals, handler, isWellFormedDID, multiUserTest, pattern, type RepresentsCurrentUser, type Stream, TESTS, type TrustedActionWrite, Writable, } from "commonfabric"; import type { SharedSpaceCatalogStorage } from "../system/shared-space-catalog.ts"; import PrivateInbox, { type Offer, type OfferEvent, } from "../system/private-inbox.tsx"; import type { ShareInboxPiece } from "../system/profile-home.tsx"; import { FabriChatManagerCore } from "./manager.tsx"; import { type MessagesValue } from "./room-records.tsx"; import { FabriChatRoomCore } from "./room.tsx"; import { CHAT_ROOM_OFFER_KIND, CHAT_SEND_ACTION, CHAT_SEND_SURFACE, CHAT_START_ACTION, CHAT_START_SURFACE, type ChatIndexEntry, type ChatManagerNotice, type ChatManagerProfile, type ChatRequestOutcome, type ChatRoomLink, type ProfileCell, } from "./schemas.tsx"; type RoomArg = Parameters[0]; type ManagerArg = Parameters[0]; /** An empty shared-space catalog, as a manager lists its rooms from. */ const emptyCatalog = () => Writable.of({ entries: {}, offers: {} }); /** The reviewed surface and action a person writes their own profile from. */ const PROFILE_SURFACE = "FabriChatTestProfileSurface"; const PROFILE_ACTION = "FabriChatTestWriteProfile"; /** The gesture writing a profile takes. */ const profileGesture = { surface: PROFILE_SURFACE, action: PROFILE_ACTION }; const sendGesture = { surface: CHAT_SEND_SURFACE, action: CHAT_SEND_ACTION }; const startGesture = { surface: CHAT_START_SURFACE, action: CHAT_START_ACTION }; const typed = (text: string) => ({ type: "click", target: { value: text } }); /** * A person's own profile, labeled with the principal who wrote it, which only * `writeOwnProfile` writes, from its reviewed surface. */ type OwnProfile = RepresentsCurrentUser< TrustedActionWrite< ChatManagerProfile, typeof writeOwnProfile, typeof PROFILE_ACTION, typeof PROFILE_SURFACE > >; /** What `writeOwnProfile` is bound to. */ interface ProfileWriteState { /** The profile to write. */ profile: Writable; /** The name to write into it. */ name: string; /** The inbox the profile points at, if any. */ inbox?: Cell; } /** Writes the acting person's own profile, under `name`. */ const writeOwnProfile = handler(( _event, { profile, name, inbox }, ) => { profile.set( (inbox === undefined ? { name } : { name, inbox: { piece: inbox } }) as OwnProfile, ); }); /** What `countAndForward` is bound to. */ interface CountingState { /** The id of every offer received, in order, repeats included. */ received: Writable; /** The inbox each offer is forwarded to. */ inbox: { receive: Stream }; } /** Records the offer's id, then hands the offer to the inbox it wraps. */ const countAndForward = handler(( event, { received, inbox }, ) => { received.push(event?.id ?? ""); inbox.receive.send(event); }); /** * A share inbox that counts every offer sent to it, repeats included, before * the inbox it wraps keeps one per sender and `id`. */ const CountingInbox = pattern< { inbox: { receive: Stream } }, { received: string[]; receive: Stream } >(({ inbox }) => { const received = Writable.of([]); return { received, receive: countAndForward({ received, inbox }) }; }); /** An inbox's result, as the link a profile holds. */ function inboxLinkOf(inbox: unknown): Cell; function inboxLinkOf(inbox: unknown): unknown { return inbox; } /** A person's own profile, as the profile an event names. */ function profileOf(profile: unknown): Cell; function profileOf(profile: unknown): unknown { return profile; } /** A room, as the test reads it: a link, and its participants. */ type HeldRoom = ChatRoomLink & { participants?: ProfileCell[] }; /** Whether `value` is an origin written as its own canonical origin. */ const isOrigin = (value: string): boolean => { try { return new URL(value).origin === value; } catch { return false; } }; /** The room's records, which every participant's room shares. */ interface Records { messages: Writable; } /** What every session receives from the setup. */ interface Setup { records: Records; /** Alice's principal, as her own run of a handler finds it. */ aliceDid: Writable; /** Bob's principal, as his own run of a handler finds it. */ bobDid: Writable; } export const setup = pattern(() => ({ records: { messages: Writable.of([] as MessagesValue), }, aliceDid: Writable.of(""), bobDid: Writable.of(""), })); // Points her profile at her private inbox, and sends a message, which makes // her one of the room's participants and hands Bob her profile. Then finds // Bob's room offered in her inbox. export const alice = pattern<{ setup: Setup }>(({ setup }) => { const inbox = PrivateInbox({ offers: [] }); const counting = CountingInbox({ inbox }); const profile = Writable.of(); const writeProfile = writeOwnProfile({ profile, name: "Alice", inbox: inboxLinkOf(counting), }); const action_note_principal = action(() => setup.aliceDid.set(currentPrincipal() ?? "") ); const room = FabriChatRoomCore({ myProfile: profile, about: { kind: "group" as const }, messages: setup.records.messages, } as RoomArg); return { [TESTS]: [ { action: writeProfile, event: {}, trustedUi: profileGesture }, { action: action_note_principal }, { action: room.composerSend, event: typed("Hello from Alice"), trustedUi: sendGesture, }, // Nothing is offered yet. { assertion: assert(() => inbox.offers.length === 0) }, { label: "alice-sent" }, { await: "bob-done" }, // Bob's room is offered once, keyed by his request, from him, naming // the room's space and the host serving it. { assertion: assert(() => { const offers = inbox.offers as readonly (Offer | undefined)[]; const offer = offers[0]; return offers.length === 1 && offer !== undefined && offer.kind === CHAT_ROOM_OFFER_KIND && offer.id === "d-alice" && setup.bobDid.get() !== "" && offer.from === setup.bobDid.get() && isWellFormedDID(offer.space) && isOrigin(offer.host) && offer.ownerOrigin === offer.host && offer.title === ""; }), }, // And it was sent once, not merely kept once. { assertion: assert(() => counting.received.length === 1 && counting.received[0] === "d-alice" ), }, ], }; }); // Starts a direct chat with Alice from her chip, after a request naming a // profile other than hers is refused. export const bob = pattern<{ setup: Setup }>(({ setup }) => { const profile = Writable.of(); const writeProfile = writeOwnProfile({ profile, name: "Bob" }); const action_note_principal = action(() => setup.bobDid.set(currentPrincipal() ?? "") ); const requests = Writable.of>({}); const notices = Writable.of([]); const manager = FabriChatManagerCore({ myProfile: profile, sharedSpaceCatalog: emptyCatalog(), direct: Writable.of>({}), requests, outgoingNotices: notices, } as ManagerArg); // Alice's profile, reached as a room reaches it: through her message. const aliceProfile = profileOf( setup.records.messages.key(0).key("authorProfile"), ); const ownProfile = profileOf(profile); const held = Writable.of<{ room?: Cell }>({}); const action_hold = action(() => held.key("room").set( requests.key("d-alice").key("entry").key("room").resolveAsCell(), ) ); return { [TESTS]: [ { action: writeProfile, event: {}, trustedUi: profileGesture }, { action: action_note_principal }, { await: "alice-sent" }, { action: manager.openDirect, event: { requestId: "not-hers", counterpart: setup.aliceDid, profile: ownProfile, }, trustedUi: startGesture, }, { assertion: assert(() => { const outcome = requests.get()["not-hers"]; return outcome?.status === "refused" && outcome.reason === "The profile is not the counterpart's." && manager.rooms.length === 0; }), }, // Her chip's click names her profile as its target's `name`, and names // no principal: the manager reads hers off the profile. The request id // is the test's own, so the offer's can be checked. { action: manager.openDirect, event: { requestId: "d-alice", type: "click", target: { name: aliceProfile }, }, trustedUi: startGesture, }, // The room is created, and offered to Alice, so no notice is queued // for her. { assertion: assert(() => setup.aliceDid.get() !== "" && manager.rooms.length === 1 && manager.rooms[0]?.counterpart === setup.aliceDid.get() && notices.get().length === 0 ), }, // Bob added her to the room's participants, beside himself, without a // step of her own. { action: action_hold }, { assertion: assert(() => { const participants = held.key("room").get()?.get()?.participants ?? []; return participants.length === 2 && participants.some((known) => equals(known, aliceProfile)) && participants.some((known) => equals(known, profile)); }), }, { label: "bob-done" }, ], }; }); export default multiUserTest({ setup, participants: { alice, bob } });