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 <loganj@squareup.com>
This commit is contained in:
Logan Johnson
2026-08-31 14:13:27 -04:00
parent 8519a6e16e
commit fb535da05d
3 changed files with 119 additions and 33 deletions

View File

@@ -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<T>(
page: Page,
command: string,
payload?: Record<string, unknown>,
) {
return page.evaluate(
async ({ command, payload }) => {
const bridgeWindow = window as Window & {
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
command: string,
payload?: Record<string, unknown>,
) => Promise<unknown>;
__TAURI_INTERNALS__?: {
invoke?: (
command: string,
payload?: Record<string, unknown>,
) => Promise<unknown>;
};
};
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<Record<string, string>>(
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");

View File

@@ -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<T>(
page: Page,
command: string,
payload?: Record<string, unknown>,
) {
return page.evaluate(
async ({ command, payload }) => {
const bridgeWindow = window as Window & {
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
command: string,
payload?: Record<string, unknown>,
) => Promise<unknown>;
__TAURI_INTERNALS__?: {
invoke?: (
command: string,
payload?: Record<string, unknown>,
) => Promise<unknown>;
};
};
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<WelcomeAgent[]> {
let team: WelcomeAgent[] = [];
await expect
.poll(async () => {
const agents = await invokeMockCommand<WelcomeAgent[]>(
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),
);
}

View File

@@ -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