From fb535da05d5358fba199b86cca3ff9411fe54f18 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Mon, 31 Aug 2026 14:13:27 -0400 Subject: [PATCH] test(desktop): publish explicit Welcome teammate presence Keep the onboarding fixture independent of lifecycle bookkeeping. Prove started Welcome agents remain offline until scenario-authored presence, then retain the original kickoff and guidance assertions. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson --- desktop/tests/e2e/onboarding.spec.ts | 61 +++++++++---------- desktop/tests/helpers/welcomeTeam.ts | 87 ++++++++++++++++++++++++++++ docs/agent-availability.md | 4 ++ 3 files changed, 119 insertions(+), 33 deletions(-) create mode 100644 desktop/tests/helpers/welcomeTeam.ts diff --git a/desktop/tests/e2e/onboarding.spec.ts b/desktop/tests/e2e/onboarding.spec.ts index b085e7dab6..fdfd5eeb4d 100644 --- a/desktop/tests/e2e/onboarding.spec.ts +++ b/desktop/tests/e2e/onboarding.spec.ts @@ -8,6 +8,11 @@ import { TEST_IDENTITIES, } from "../helpers/bridge"; import { expectEmojiMartStylesInstalled } from "../helpers/css"; +import { + invokeMockCommand, + publishWelcomeTeamPresence, + waitForWelcomeTeam, +} from "../helpers/welcomeTeam"; import { installFakeCamera } from "../helpers/fakeCamera"; import { E2E_IDENTITY_OVERRIDE_STORAGE_KEY, @@ -519,39 +524,6 @@ async function getMockChannels(page: Page) { }); } -async function invokeMockCommand( - page: Page, - command: string, - payload?: Record, -) { - return page.evaluate( - async ({ command, payload }) => { - const bridgeWindow = window as Window & { - __BUZZ_E2E_INVOKE_MOCK_COMMAND__?: ( - command: string, - payload?: Record, - ) => Promise; - __TAURI_INTERNALS__?: { - invoke?: ( - command: string, - payload?: Record, - ) => Promise; - }; - }; - const invoke = - bridgeWindow.__BUZZ_E2E_INVOKE_MOCK_COMMAND__ ?? - bridgeWindow.__TAURI_INTERNALS__?.invoke; - - if (!invoke) { - throw new Error("Mock invoke bridge is unavailable."); - } - - return (await invoke(command, payload)) as T; - }, - { command, payload }, - ); -} - async function seedCurrentAvatar(page: Page, avatarUrl: string) { await page.waitForFunction(() => { const bridgeWindow = window as Window & { @@ -3025,6 +2997,7 @@ test("avatar step accepts an avatar URL before completing onboarding", async ({ await page.getByTestId("onboarding-next").click(); await expect(page.getByTestId("onboarding-gate")).toHaveCount(0); + await publishWelcomeTeamPresence(page); await expectWelcomeView(page); }); @@ -3060,6 +3033,7 @@ test("failed avatar saves can continue without saving the avatar", async ({ await page.getByTestId("onboarding-next-without-saving").click(); await expect(page.getByTestId("onboarding-gate")).toHaveCount(0); + await publishWelcomeTeamPresence(page); await expectWelcomeView(page); }); @@ -3158,6 +3132,7 @@ test("first-run onboarding keeps the shell hidden and lands on private Welcome a await page.getByTestId("onboarding-display-name").fill("Alice"); await completeProfileOnboarding(page); await expect(page.getByTestId("onboarding-gate")).toHaveCount(0); + await publishWelcomeTeamPresence(page); await expectWelcomeView(page); await expectStarterChannels(page); await expectWelcomeGuideIntro(page); @@ -3215,6 +3190,22 @@ test("first-run onboarding posts the live Fizz kickoff", async ({ page }) => { await completeProfileOnboarding(page); await expectPrivateWelcomeLanding(page); + // Runtime start alone cannot satisfy the kickoff's relay-presence wait. + const team = await waitForWelcomeTeam(page); + const presence = await invokeMockCommand>( + page, + "get_presence", + { pubkeys: team.map((agent) => agent.pubkey) }, + ); + expect(team.map((agent) => presence[agent.pubkey])).toEqual([ + "offline", + "offline", + "offline", + ]); + await expect(page.getByTestId("message-timeline")).not.toContainText( + "Hi Morty QA, I'm Fizz. Welcome to Buzz.", + ); + await publishWelcomeTeamPresence(page); // Greeted by the name typed above — the @mention pill also files the opener // into the new user's Inbox mentions feed. await expect(page.getByTestId("message-timeline")).toContainText( @@ -3241,6 +3232,7 @@ test("first-run onboarding lands before Welcome team bootstrap completes", async await expectPrivateWelcomeLanding(page); await expect(page.getByTestId("app-loading-gate")).toHaveCount(0); + await publishWelcomeTeamPresence(page); await expect(page.getByTestId("message-timeline")).toContainText( "Hi Morty QA, I'm Fizz. Welcome to Buzz.", ); @@ -3315,6 +3307,7 @@ test("finishing onboarding creates starter channels and focuses welcome-everyone await page.getByTestId("onboarding-display-name").fill("Morty QA"); await completeProfileOnboarding(page); + await publishWelcomeTeamPresence(page); await expectWelcomeView(page); await expect(page.getByTestId("channel-general")).toBeVisible(); await expectStarterChannels(page); @@ -3331,6 +3324,7 @@ test("welcome-everywhere banner: X dismiss removes the guidance surface", async await page.getByTestId("onboarding-display-name").fill("Morty QA"); await completeProfileOnboarding(page); + await publishWelcomeTeamPresence(page); const banner = page.getByTestId("welcome-composer-guide-banner"); const guidanceLayer = page.getByTestId("welcome-composer-guidance-layer"); @@ -3357,6 +3351,7 @@ test("welcome-everywhere banner: dismiss persists after channel re-entry", async await page.getByTestId("onboarding-display-name").fill("Morty QA"); await completeProfileOnboarding(page); + await publishWelcomeTeamPresence(page); const banner = page.getByTestId("welcome-composer-guide-banner"); diff --git a/desktop/tests/helpers/welcomeTeam.ts b/desktop/tests/helpers/welcomeTeam.ts new file mode 100644 index 0000000000..87384163ff --- /dev/null +++ b/desktop/tests/helpers/welcomeTeam.ts @@ -0,0 +1,87 @@ +import { expect, type Page } from "@playwright/test"; + +/** Invoke the mock command boundary without seeding product query caches. */ +export async function invokeMockCommand( + page: Page, + command: string, + payload?: Record, +) { + return page.evaluate( + async ({ command, payload }) => { + const bridgeWindow = window as Window & { + __BUZZ_E2E_INVOKE_MOCK_COMMAND__?: ( + command: string, + payload?: Record, + ) => Promise; + __TAURI_INTERNALS__?: { + invoke?: ( + command: string, + payload?: Record, + ) => Promise; + }; + }; + const invoke = + bridgeWindow.__BUZZ_E2E_INVOKE_MOCK_COMMAND__ ?? + bridgeWindow.__TAURI_INTERNALS__?.invoke; + + if (!invoke) { + throw new Error("Mock invoke bridge is unavailable."); + } + + return (await invoke(command, payload)) as T; + }, + { command, payload }, + ); +} + +type WelcomeAgent = { + pubkey: string; + persona_id: string; + status: string; +}; +// Pollen retains the builtin:bumble persona identity. +const WELCOME_PERSONAS = ["builtin:bumble", "builtin:fizz", "builtin:honey"]; + +/** Wait for bootstrap identities so a scenario can author their relay events. */ +export async function waitForWelcomeTeam(page: Page): Promise { + let team: WelcomeAgent[] = []; + await expect + .poll(async () => { + const agents = await invokeMockCommand( + page, + "list_managed_agents", + ); + const { channels } = await invokeMockCommand<{ + channels: Array<{ id: string; name: string }>; + }>(page, "get_channels"); + const welcome = channels.find((channel) => channel.name === "Welcome"); + if (!welcome) return []; + const { members } = await invokeMockCommand<{ + members: Array<{ pubkey: string }>; + }>(page, "get_channel_members", { channelId: welcome.id }); + // Select the started Welcome members, not inactive seeded siblings. + // This only identifies the event authors; Start does not publish presence. + team = agents.filter( + (agent) => + agent.status === "running" && + members.some((member) => member.pubkey === agent.pubkey) && + WELCOME_PERSONAS.includes(agent.persona_id), + ); + return team.map((agent) => `${agent.persona_id}:${agent.status}`).sort(); + }) + .toEqual(WELCOME_PERSONAS.map((persona) => `${persona}:running`)); + return team; +} + +/** Author healthy Welcome-team presence explicitly, independently of Start. */ +export async function publishWelcomeTeamPresence(page: Page) { + const team = await waitForWelcomeTeam(page); + await page.evaluate( + (pubkeys) => { + const emit = window.__BUZZ_E2E_EMIT_MOCK_PRESENCE__; + if (!emit) throw new Error("Mock presence emitter is unavailable."); + for (const pubkey of pubkeys) emit({ pubkey, status: "online" }); + }, + team.map((agent) => agent.pubkey), + ); +} diff --git a/docs/agent-availability.md b/docs/agent-availability.md index c21c02a7d3..60b36cb1f9 100644 --- a/docs/agent-availability.md +++ b/docs/agent-availability.md @@ -22,6 +22,10 @@ is not permission to deploy a duplicate body. Local Stop/Start routing is unchan runtime-only negative control, then authored Online presence. - `desktop/tests/e2e/profile.spec.ts`: created-agent initial Online snapshot and live Offline/Online transitions alongside independent Stop/Start controls. +- `desktop/tests/e2e/onboarding.spec.ts`: Welcome kickoff stays pending after + runtime start alone, then greets the owner and addresses Honey/Pollen only + after the scenario publishes their explicit presence. Healthy-team scenarios + use `tests/helpers/welcomeTeam.ts`, without changing the product readiness wait. Mock create/start/stop only model runtime bookkeeping. Scenarios that require availability use `__BUZZ_E2E_EMIT_MOCK_PRESENCE__` to seed the snapshot and emit