/// /** * Multi-user pattern test for cozy poll. * * main.test.tsx documents the single-identity caveat (CT-1598): one runtime * cannot simulate a second user. This runs ONE shared poll across two * worker-isolated runtimes and covers that gap: second-user join isolation, * the host gate rejecting a real non-host user, cross-runtime vote * visibility, and open host takeover. The deeper voting flows stay in * main.test.tsx; lunch-poll/multi-user.test.tsx covers the richer variant. * * Joins go through the `joinAs` event-name override (the headless seam kept * by the profile migration); the profile-wish UI path needs a browser. * Cross-runtime reads use INLINE literal accesses — see * scrabble/multi-user.test.tsx. */ import { action, computed, multiUserTest, pattern } from "commonfabric"; import CozyPoll, { type CozyPollOutput } from "./main.tsx"; interface Setup { poll: CozyPollOutput; } export const setup = pattern(() => ({ poll: CozyPoll({}), })); export const alice = pattern<{ setup: Setup }>(({ setup }) => { const poll = setup.poll; const action_join = action(() => { poll.joinAs.send({ name: "Alice" }); }); const action_add_ramen = action(() => { poll.addOption.send({ title: "Ramen" }); }); const assert_joined_as_host = computed(() => poll.myName === "Alice" && poll.adminName === "Alice" && poll.isAdmin === true && (poll.users ?? []).length === 1 && poll.users?.[0]?.name === "Alice" ); const assert_option_added = computed(() => (poll.options ?? []).length === 1 && poll.options?.[0]?.title === "Ramen" ); // Bob joined and voted; his gated addOption attempt left no trace. const assert_sees_bobs_vote = computed(() => (poll.users ?? []).length === 2 && poll.users?.[1]?.name === "Bob" && (poll.votes ?? []).length === 1 && poll.votes?.[0]?.voterName === "Bob" && poll.votes?.[0]?.voteType === "red" && (poll.options ?? []).length === 1 && poll.myName === "Alice" ); // Host takeover observed from the deposed host's runtime. const assert_deposed = computed(() => poll.adminName === "Bob" && poll.isAdmin === false ); return { tests: [ { action: action_join }, { assertion: assert_joined_as_host }, { action: action_add_ramen }, { assertion: assert_option_added }, { label: "alice-set-up" }, { await: "bob-voted" }, { assertion: assert_sees_bobs_vote }, { await: "bob-claimed-host" }, { assertion: assert_deposed }, ], }; }); export const bob = pattern<{ setup: Setup }>(({ setup }) => { const poll = setup.poll; const action_join = action(() => { poll.joinAs.send({ name: "Bob" }); }); const action_try_add_as_non_host = action(() => { poll.addOption.send({ title: "Should not appear" }); }); const action_vote_red = action(() => { const first = poll.options?.[0]; if (first) poll.castVote.send({ optionId: first.id, voteType: "red" }); }); const action_claim_host = action(() => { poll.claimHost.send({}); }); const assert_sees_alice_setup = computed(() => (poll.users ?? []).length === 1 && poll.users?.[0]?.name === "Alice" && poll.adminName === "Alice" && (poll.options ?? []).length === 1 && poll.options?.[0]?.title === "Ramen" ); // PerUser isolation: Alice's join must not leak into Bob's identity. const assert_not_joined_yet = computed(() => poll.myName === "" && poll.isJoined === false ); const assert_joined_not_host = computed(() => poll.myName === "Bob" && poll.isAdmin === false && (poll.users ?? []).length === 2 && poll.users?.[1]?.name === "Bob" ); // The host gate rejected a real second user (the CT-1598 gap). const assert_gating_held = computed(() => (poll.options ?? []).length === 1); const assert_own_vote = computed(() => (poll.votes ?? []).length === 1 && poll.votes?.[0]?.voterName === "Bob" && poll.votes?.[0]?.voteType === "red" ); const assert_is_host_now = computed(() => poll.adminName === "Bob" && poll.isAdmin === true ); return { tests: [ { await: "alice-set-up" }, { assertion: assert_sees_alice_setup }, { assertion: assert_not_joined_yet }, { action: action_join }, { assertion: assert_joined_not_host }, { action: action_try_add_as_non_host }, { assertion: assert_gating_held }, { action: action_vote_red }, { assertion: assert_own_vote }, { label: "bob-voted" }, { action: action_claim_host }, { assertion: assert_is_host_now }, { label: "bob-claimed-host" }, ], }; }); export default multiUserTest({ setup, participants: { alice, bob } });