Files
buzz/desktop/tests/e2e/onboarding-agent-defaults.spec.ts
Taylor Ho de8a2741c7 feat(desktop-messages): show compact Buzz link metadata (#6252)
**Category:** improvement
**User Impact:** Buzz links in messages now resolve into compact,
descriptive chips with predictable navigation, while tooltips use the
neutral secondary surface consistently across light and dark themes.

**Problem:** Raw Buzz-native links and opaque identifiers make message
context difficult to scan, while oversized previews disrupt the
conversation. Missing, delayed, or deleted metadata also needs a stable
presentation that does not mislead users or break navigation.

**Solution:** Keep message and issue chips metadata-independent: message
chips render only the channel name, while issue chips render only the
repository name—never fetched content/title or an event hash. Fetched
context remains available in each hover tooltip. Other native links
continue to resolve into concise inline labels, with muted secondary
fallbacks, distinct deleted states, and existing navigation paths
preserved. This also makes two explicit shared-tooltip behavior changes:

1. **Color:** shared and rich tooltips move from the primary color pair
to the neutral secondary semantic pair, including dedicated huddle-token
handling and matching rich-descendant foregrounds.
2. **Hover behavior:** tooltip roots set Radix
`disableHoverableContent`, and tooltip content is pointer-transparent,
so moving off the trigger dismisses the tooltip instead of letting the
pointer enter and keep the popover open.

Together these preserve readable contrast, authoritative project
context, and predictable hover dismissal.

<details>
<summary>File changes</summary>

**desktop/src/features/communities/useCommunityInit.ts**
Primes channel metadata needed to resolve native links promptly.

**desktop/src/features/messages/lib/messageLinkMetadata.test.mjs**
Covers message preview extraction, fallbacks, and deleted-message
semantics.

**desktop/src/features/messages/lib/messageLinkMetadata.ts**
Normalizes fetched message metadata into compact labels and status-aware
presentation.

**desktop/src/features/messages/ui/SentFromThreadLine.tsx**
Provides thread context to linked-message metadata resolution.

**desktop/src/shared/lib/useResolvedLinkPreviews.ts**
Exposes resolved preview state for native entity chips.

**desktop/src/shared/styles/globals/markdown.css**
Adds compact chip layout and semantic disabled-state styling.

**desktop/src/shared/styles/globals/theme.css**
Defines the semantic colors used by unavailable and deleted chips.

**desktop/src/shared/ui/markdown.test.mjs**
Exercises native-link rendering and provider-backed entity metadata
behavior.

**desktop/src/shared/ui/markdown.tsx**
Routes native markdown links through metadata-aware chip components
without changing navigation contracts.

**desktop/src/shared/ui/markdown/ChannelDeepLink.test.mjs**
Covers resolved, unresolved, private, archived, and forum channel
presentations.

**desktop/src/shared/ui/markdown/ChannelDeepLink.tsx**
Renders channel names and concise channel metadata inline with stable
fallbacks.

**desktop/src/shared/ui/markdown/MessageLinkPill.tsx**
Renders loading, ready, unavailable, DM, forum, and deleted message
states compactly.

**desktop/src/shared/ui/markdown/entityLinks.tsx**
Enriches repository, issue, and pull-request chips and uses
containing-project context in tooltips.

**desktop/src/shared/ui/markdown/types.ts**
Extends markdown runtime metadata required by native chip rendering.

**desktop/src/shared/ui/markdown/useMessageLinkMetadata.ts**
Fetches and classifies linked-message metadata while preserving
navigable fallbacks.

**desktop/src/testing/e2eBridge.ts**
Adds deterministic metadata controls for runtime visual and navigation
tests.

**desktop/tailwind.config.js**
Makes semantic disabled colors available to chip styles.

**desktop/tests/e2e/entity-link-recipient-cards.spec.ts**
Verifies entity labels, project-aware tooltips, delayed metadata, and
navigation behavior.

**desktop/tests/e2e/navigation.spec.ts**
Verifies channel and message chip states, metadata resolution, deletion
handling, and navigation.

**desktop/src/shared/ui/tooltip.tsx**
Migrates the shared tooltip surface from the primary color pair to the
semantic secondary pair. It also sets Radix `disableHoverableContent`
and makes tooltip content pointer-transparent so leaving the trigger
dismisses the tooltip predictably.

**desktop/src/features/agents/ui/RestartDiffBadge.tsx**
Aligns restart-diff tooltip values and supporting copy with the
secondary tooltip foreground.

**desktop/src/features/channels/ui/AddChannelBotTeamsSection.tsx**
Aligns rich team tooltip descriptions, persona chips, avatars, and names
with secondary tooltip semantics.

**desktop/src/features/projects/ui/ProjectAuthorIdentity.tsx**
Uses the matching secondary tooltip foreground for project author
metadata.

**desktop/src/features/projects/ui/ProjectCards.tsx**
Aligns repository-unavailable tooltip descriptions with the secondary
tooltip foreground.

**desktop/src/shared/styles/globals/utilities.css**
Makes huddle tooltips consume their dedicated palette directly without
misleading primary or secondary aliases.

**desktop/src/shared/styles/globals/tooltipSemantics.test.mjs**
Pins direct huddle-token consumption and rejects semantic alias drift.

**desktop/tests/e2e/tooltip-semantics.spec.ts**
Verifies simple and rich tooltip surfaces and descendant tokens in Buzz
light and Catppuccin Mocha dark themes.

**desktop/playwright.config.ts**
Registers tooltip semantic coverage in the desktop smoke suite.

</details>

## Reproduction steps

1. Run the desktop app and open a message containing Buzz links for a
channel, message, repository, issue, and pull request.
2. Confirm message links show only the channel name and issue links show
only the repository name inline, with no fetched content/title or event
hash; confirm other native links remain compact rather than rendering
raw identifiers or expanded previews.
3. Hover the chips and confirm the neutral secondary tooltip adds
concise context without duplicating the visible label.
4. Open each chip and confirm it follows the same channel, message,
repository, issue, or pull-request navigation as before.
5. Inspect links while metadata is loading, unavailable, or definitively
deleted and confirm the fallback remains compact, legible, and navigable
where appropriate.
6. In Buzz light and Catppuccin Mocha dark themes, compare a simple
channel-members tooltip and the rich Add agents team tooltip; both
should use secondary semantics with readable nested content.
7. Move the pointer from a trigger toward its tooltip; confirm the
tooltip dismisses rather than remaining open as hoverable content.

## Screenshots

The matrix below covers both the default light theme and Catppuccin
Mocha with an orange accent. Message/issue-chip rows affected by the
stable-label pivot are temporarily omitted until they can be recaptured
from the real built UI; the prior images showed the obsolete expanding
inline label.

| State | Default light | Dark · orange accent |
|---|---|---|
| Public stream channel tooltip | ![Public stream channel tooltip —
default
light](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/light-default/01-public-stream-channel-tooltip.png)
| ![Public stream channel tooltip — dark orange
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/dark-orange/01-public-stream-channel-tooltip.png)
|
| Private forum channel tooltip | ![Private forum channel tooltip —
default
light](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/light-default/02-private-forum-channel-tooltip.png)
| ![Private forum channel tooltip — dark orange
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/dark-orange/02-private-forum-channel-tooltip.png)
|
| Public forum channel tooltip | ![Public forum channel tooltip —
default
light](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/light-default/03-forum-channel-tooltip.png)
| ![Public forum channel tooltip — dark orange
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/dark-orange/03-forum-channel-tooltip.png)
|
| Channel references, including unresolved | ![Channel references,
including unresolved — default
light](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/light-default/04-unresolved-channel-reference.png)
| ![Channel references, including unresolved — dark orange
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/dark-orange/04-unresolved-channel-reference.png)
|
| Unknown channel permalink | ![Unknown channel permalink — default
light](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/light-default/05-unknown-channel-permalink.png)
| ![Unknown channel permalink — dark orange
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/dark-orange/05-unknown-channel-permalink.png)
|
| Archived channel tooltip | ![Archived channel tooltip — default
light](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/light-default/06-archived-channel-tooltip.png)
| ![Archived channel tooltip — dark orange
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/dark-orange/06-archived-channel-tooltip.png)
|
| Repository entity | ![Repository entity — default
light](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/light-default/13-repository-entity.png)
| ![Repository entity — dark orange
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/dark-orange/13-repository-entity.png)
|
| Pull request entity | ![Pull request entity — default
light](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/light-default/14-pull-request-entity.png)
| ![Pull request entity — dark orange
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/chip-metadata/dark-orange/14-pull-request-entity.png)
|
| Entity metadata fallback | ![Corrected entity metadata fallback
tooltip — default
light](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/17-entity-metadata-fallback-corrected-light.png)
| ![Corrected entity metadata fallback tooltip — dark orange
accent](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6252/17-entity-metadata-fallback-corrected-dark-orange.png)
|



### Tooltip semantic surface spot check

| Buzz light | Catppuccin Mocha dark |
|---|---|
| ![Rich team tooltip on the secondary surface in Buzz
light](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6290/buzz-team-tooltip.png)
| ![Rich team tooltip on the secondary surface in Catppuccin Mocha
dark](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6290/catppuccin-mocha-team-tooltip.png)
|

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-08-20 19:01:18 -07:00

1159 lines
38 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
import { passThroughBackupStep } from "../helpers/onboarding";
function runtime(
id: "buzz-agent" | "claude" | "codex" | "goose",
availability: string,
authStatus: Record<string, unknown>,
overrides: Record<string, unknown> = {},
) {
return {
id,
label:
id === "buzz-agent"
? "Buzz Agent"
: id === "claude"
? "Claude Code"
: id === "codex"
? "Codex"
: "Goose",
avatar_url: "",
availability,
command: availability === "available" ? id : null,
binary_path: availability === "available" ? `/usr/local/bin/${id}` : null,
default_args: [],
mcp_command: null,
install_hint: `Install ${id}`,
install_instructions_url: "https://example.com",
can_auto_install: true,
underlying_cli_path: null,
node_required: false,
auth_status: authStatus,
login_hint: `Sign in to ${id}`,
...overrides,
};
}
async function navigateToSetupPage(
page: Parameters<typeof installMockBridge>[0],
) {
await page.getByRole("button", { name: "Create a new identity key" }).click();
await passThroughBackupStep(page);
await expect(page.getByTestId("onboarding-page-2")).toBeVisible();
}
async function readSavedRuntime(page: Parameters<typeof installMockBridge>[0]) {
return await page.evaluate(async () => {
const result = await (
window as Window & {
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
command: string,
payload: unknown,
) => Promise<{ preferred_runtime?: string | null }>;
}
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__?.("get_global_agent_config", null);
return result?.preferred_runtime ?? null;
});
}
async function readGlobalConfigSetterCallCount(
page: Parameters<typeof installMockBridge>[0],
) {
return await page.evaluate(async () => {
return await (
window as Window & {
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
command: string,
payload: unknown,
) => Promise<number>;
}
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__?.(
"get_global_agent_config_set_call_count",
null,
);
});
}
test("setup shows all bundled harnesses as detected", async ({ page }) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("buzz-agent", "available", { status: "not_applicable" }),
runtime("goose", "available", { status: "not_applicable" }),
runtime("codex", "available", { status: "logged_in" }),
runtime("claude", "available", { status: "logged_in" }),
],
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await expect(page.getByTestId("onboarding-runtime-claude")).toBeVisible();
await expect(page.getByTestId("onboarding-runtime-codex")).toBeVisible();
await expect(page.getByTestId("onboarding-runtime-goose")).toBeVisible();
await expect(page.getByTestId("onboarding-runtime-buzz-agent")).toBeVisible();
await expect(page.getByRole("checkbox")).toHaveCount(0);
const setupSkip = page.getByTestId("onboarding-setup-skip");
await expect(setupSkip).toBeVisible();
await expect(setupSkip).not.toHaveClass(/animate-in|fade-in/);
const harnessNote = page.getByText(/More harnesses \(Cursor, Grok, Amp…\)/);
await expect(harnessNote).toBeVisible();
const [lastHarnessBox, harnessNoteBox] = await Promise.all([
page.getByTestId("onboarding-runtime-buzz-agent").boundingBox(),
harnessNote.boundingBox(),
]);
if (!lastHarnessBox || !harnessNoteBox) {
throw new Error("Could not measure harness note placement");
}
expect(harnessNoteBox.y).toBeGreaterThan(
lastHarnessBox.y + lastHarnessBox.height,
);
});
test("setup distinguishes a missing CLI from an installed desktop app", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime(
"codex",
"not_installed",
{ status: "unknown" },
{
install_hint: "Buzz talks to Codex through the Codex CLI.",
install_instructions_url:
"https://developers.openai.com/codex/cli/",
},
),
],
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
const card = page.getByTestId("onboarding-runtime-codex");
await expect(card).toContainText("CLI not detected.");
await expect(card.getByTestId("onboarding-runtime-install-codex")).toHaveText(
"INSTALL",
);
});
test("ready state is detected and enables Next without persisting a default", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("claude", "available", { status: "logged_in" }),
runtime("codex", "available", { status: "logged_out" }),
],
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await expect(page.getByTestId("onboarding-runtime-ready-claude")).toHaveText(
"READY",
);
await expect(
page.getByTestId("onboarding-runtime-checkmark-claude"),
).toHaveCount(0);
await expect(
page.getByTestId("onboarding-runtime-checkmark-codex"),
).toHaveCount(0);
await expect(page.getByTestId("onboarding-setup-next")).toBeEnabled();
expect(await readSavedRuntime(page)).toBeNull();
});
test("setup shows runtime discovery loading before rendering harnesses", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("claude", "available", { status: "logged_in" }),
],
acpRuntimesDelayMs: 500,
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await expect(page.getByTestId("onboarding-runtime-loading")).toBeVisible();
await expect(page.getByTestId("onboarding-runtime-claude")).toBeVisible();
await expect(page.getByTestId("onboarding-runtime-loading")).toHaveCount(0);
});
test("unknown authentication can be checked again", async ({ page }) => {
const unknown = runtime("claude", "available", { status: "unknown" });
const loggedIn = runtime("claude", "available", { status: "logged_in" });
await installMockBridge(
page,
{ acpRuntimesCatalogSequence: [[unknown], [loggedIn]] },
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
const checkAgain = page.getByRole("button", {
name: "Check Claude Code again",
});
await expect(checkAgain).toHaveText("CHECK AGAIN");
await checkAgain.click();
await expect(page.getByTestId("onboarding-runtime-ready-claude")).toHaveText(
"READY",
);
});
test("auth discovery failure stays actionable without exposing internals", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("claude", "available", { status: "logged_out" }),
],
acpAuthMethodsError: "sensitive auth discovery details",
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
const card = page.getByTestId("onboarding-runtime-claude");
await expect(
card.getByRole("status", { name: /Sign-in unavailable/ }),
).toBeVisible();
await expect(
card.getByTestId("onboarding-runtime-instructions-claude"),
).toHaveText("SIGN IN");
await expect(card).not.toContainText("sensitive auth discovery details");
});
test("terminal launch failure keeps Sign in available", async ({ page }) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("claude", "available", { status: "logged_out" }),
],
acpAuthMethods: {
claude: {
methods: [
{
id: "subscription",
name: "Claude.ai subscription",
description: null,
type: "terminal",
},
],
},
},
connectAcpRuntimeError: "sensitive launch details",
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
const card = page.getByTestId("onboarding-runtime-claude");
const signIn = card.getByRole("button", { name: "Sign in to Claude Code" });
await signIn.click();
await expect(
card.getByRole("status", { name: /Sign-in failed/ }),
).toBeVisible();
await expect(signIn).toHaveText("SIGN IN");
await expect(card).not.toContainText("sensitive launch details");
});
test("sign in stays pending until catalog detection confirms Ready", async ({
page,
}) => {
const loggedOut = runtime("claude", "available", { status: "logged_out" });
const loggedIn = runtime("claude", "available", { status: "logged_in" });
await installMockBridge(
page,
{
acpRuntimesCatalogSequence: [[loggedOut], [loggedOut], [loggedIn]],
acpAuthMethods: {
claude: {
methods: [
{
id: "subscription",
name: "Claude.ai subscription",
description: null,
type: "terminal",
},
],
},
},
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
const signIn = page.getByRole("button", { name: "Sign in to Claude Code" });
await expect(signIn).toHaveText("SIGN IN");
await expect(page.getByTestId("onboarding-setup-next")).toBeDisabled();
await signIn.click();
await expect(signIn).toHaveText("CHECKING…");
await expect(page.getByTestId("onboarding-setup-next")).toBeDisabled();
await expect(page.getByTestId("onboarding-runtime-ready-claude")).toHaveText(
"READY",
{ timeout: 5_000 },
);
await expect(page.getByTestId("onboarding-setup-next")).toBeEnabled();
});
test("failed install can be retried without shifting card content", async ({
page,
}) => {
const notInstalled = runtime("claude", "adapter_missing", {
status: "unknown",
});
await installMockBridge(
page,
{
acpRuntimesCatalog: [notInstalled],
installAcpRuntimeResults: [
{
success: false,
steps: [
{
step: "adapter",
command: "mock install claude",
success: false,
stdout: "",
stderr: "sensitive install details",
exit_code: 1,
},
],
},
{
success: true,
steps: [
{
step: "adapter",
command: "mock install claude",
success: true,
stdout: "installed",
stderr: "",
exit_code: 0,
},
],
},
],
acpRuntimesCatalogAfterInstall: [
runtime("claude", "available", { status: "logged_in" }),
],
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
const card = page.getByTestId("onboarding-runtime-claude");
const heading = card.getByRole("heading", { name: "Claude Code" });
const headingTop = await heading.evaluate(
(element) => element.getBoundingClientRect().top,
);
const install = page.getByTestId("onboarding-runtime-install-claude");
await install.click();
const error = page.getByTestId("onboarding-runtime-error-claude");
await expect(error).toBeVisible();
await expect(install).toHaveText("RETRY INSTALL");
await expect(error).not.toContainText("sensitive install details");
expect(
await heading.evaluate((element) => element.getBoundingClientRect().top),
).toBe(headingTop);
await install.click();
await expect(page.getByTestId("onboarding-runtime-ready-claude")).toHaveText(
"READY",
);
});
test("install transitions through Sign in to Ready", async ({ page }) => {
const notInstalled = runtime("claude", "adapter_missing", {
status: "unknown",
});
const loggedOut = runtime("claude", "available", { status: "logged_out" });
const loggedIn = runtime("claude", "available", { status: "logged_in" });
await installMockBridge(
page,
{
acpRuntimesCatalog: [notInstalled],
acpRuntimesCatalogAfterInstallSequence: [[loggedOut], [loggedIn]],
installAcpRuntimeDelayMs: 500,
acpAuthMethods: {
claude: {
methods: [
{
id: "subscription",
name: "Claude.ai subscription",
description: null,
type: "terminal",
},
],
},
},
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
const install = page.getByTestId("onboarding-runtime-install-claude");
await expect(install).toHaveText("INSTALL");
await install.click();
const signIn = page.getByRole("button", { name: "Sign in to Claude Code" });
await expect(signIn).toHaveText("SIGN IN");
await expect(page.getByTestId("onboarding-setup-next")).toBeDisabled();
await signIn.click();
await expect(page.getByTestId("onboarding-runtime-ready-claude")).toHaveText(
"READY",
{ timeout: 5_000 },
);
await expect(
page.getByTestId("onboarding-runtime-checkmark-claude"),
).toHaveCount(0);
});
test("defaults waits for baked configuration before rendering fields", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("claude", "available", { status: "logged_in" }),
],
bakedBuildEnv: [
{ key: "ANTHROPIC_API_KEY", masked: true, value: "••••••" },
],
bakedBuildEnvDelayMs: 500,
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await page.getByTestId("onboarding-setup-next").click();
await expect(page.getByText("Loading…")).toBeVisible();
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
"Claude Code",
);
});
test("defaults renders only fields supported by the selected harness", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("claude", "available", { status: "logged_in" }),
],
globalAgentConfig: {
env_vars: { BUZZ_AGENT_THINKING_EFFORT: "high" },
provider: null,
model: "stale-model",
preferred_runtime: null,
},
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await page.getByTestId("onboarding-setup-next").click();
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
"Claude Code",
);
await expect(page.getByTestId("global-agent-provider")).toHaveCount(0);
await expect(page.getByTestId("global-agent-model")).toHaveText(
"Default model",
);
await expect(
page.getByTestId("global-agent-thinking-effort-select"),
).toHaveCount(0);
});
test("defaults hides model when optional harness has empty discovery", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("claude", "available", { status: "logged_in" }),
],
discoverAgentModels: {
models: [],
supportsSwitching: false,
},
globalAgentConfig: {
env_vars: {},
provider: null,
model: null,
preferred_runtime: null,
},
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await page.getByTestId("onboarding-setup-next").click();
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
"Claude Code",
);
// Confirmed successful empty catalog — omit the Model control; harness
// default applies and Finish stays available.
await expect(page.getByTestId("global-agent-model")).toHaveCount(0);
await expect(page.getByTestId("onboarding-finish")).toBeEnabled();
});
test("defaults keeps model control when optional harness discovery fails", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("claude", "available", { status: "logged_in" }),
],
discoverAgentModelsError: "CLI discovery timed out",
globalAgentConfig: {
env_vars: {},
provider: null,
model: null,
preferred_runtime: null,
},
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await page.getByTestId("onboarding-setup-next").click();
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
"Claude Code",
);
// Failed discovery must not look like successful empty: keep the control
// and surface #2246 failure UI (status line bypasses onboarding-essential).
await expect(page.getByTestId("global-agent-model")).toBeVisible();
await expect(page.getByText(/Could not load live models/i)).toBeVisible();
await expect(page.getByTestId("onboarding-finish")).toBeEnabled();
});
test("defaults can be skipped while loading without persisting configuration", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("claude", "available", { status: "logged_in" }),
],
bakedBuildEnvDelayMs: 500,
globalAgentConfig: {
env_vars: {},
provider: null,
model: null,
preferred_runtime: null,
},
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await page.getByTestId("onboarding-setup-next").click();
await expect(page.getByText("Loading…")).toBeVisible();
await page.getByTestId("onboarding-config-skip").click();
await expect(page.getByText("Join or create a community")).toBeVisible();
expect(await readSavedRuntime(page)).toBeNull();
});
test("defaults stages auto-selection and edits without writing when skipped", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("claude", "available", { status: "logged_in" }),
],
globalAgentConfig: {
env_vars: {},
provider: null,
model: null,
preferred_runtime: null,
},
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await page.getByTestId("onboarding-setup-next").click();
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
"Claude Code",
);
await page.getByTestId("global-agent-model").click();
await page
.getByTestId("global-agent-model-option-claude-opus-4-20250514")
.click();
expect(await readGlobalConfigSetterCallCount(page)).toBe(0);
await expect(
page.getByText(
"Configure default models in Settings → Agents after setup.",
),
).toHaveCount(0);
await page.getByTestId("onboarding-config-skip").click();
await expect(page.getByText("Join or create a community")).toBeVisible();
expect(await readSavedRuntime(page)).toBeNull();
expect(await readGlobalConfigSetterCallCount(page)).toBe(0);
});
test("Back preserves incomplete defaults draft without writing", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("buzz-agent", "available", { status: "not_applicable" }),
runtime("claude", "available", { status: "logged_in" }),
],
discoverAgentModels: {
models: [{ id: "claude-sonnet-4", name: "Claude Sonnet 4" }],
supportsSwitching: true,
},
globalAgentConfig: {
env_vars: {},
provider: null,
model: null,
preferred_runtime: null,
},
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await page.getByTestId("onboarding-setup-next").click();
await expect(
page
.getByTestId("onboarding-page-config")
.locator(".buzz-onboarding-transition-line"),
).toHaveAttribute("data-onboarding-direction", "forward");
const harness = page.getByTestId("global-agent-default-harness");
await harness.click();
await page
.getByTestId("global-agent-default-harness-option-buzz-agent")
.click();
await page.getByTestId("global-agent-provider").click();
await page.getByTestId("global-agent-provider-option-anthropic").click();
await expect(page.getByTestId("onboarding-finish")).toBeDisabled();
await page.getByTestId("onboarding-back").click();
await expect(page.getByTestId("onboarding-page-2")).toBeVisible();
await expect(
page
.getByTestId("onboarding-page-2")
.locator(".buzz-onboarding-transition-line"),
).toHaveAttribute("data-onboarding-direction", "backward");
expect(await readSavedRuntime(page)).toBeNull();
expect(await readGlobalConfigSetterCallCount(page)).toBe(0);
await page.getByTestId("onboarding-setup-next").click();
await expect(
page
.getByTestId("onboarding-page-config")
.locator(".buzz-onboarding-transition-line"),
).toHaveAttribute("data-onboarding-direction", "forward");
await expect(harness).toHaveText("Buzz");
await expect(page.getByTestId("global-agent-provider")).toHaveText(
"Anthropic",
);
await expect(page.getByTestId("onboarding-finish")).toBeDisabled();
expect(await readGlobalConfigSetterCallCount(page)).toBe(0);
});
test("defaults auto-selects the only ready visible harness", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("buzz-agent", "not_installed", { status: "not_applicable" }),
runtime("goose", "not_installed", { status: "not_applicable" }),
runtime("claude", "available", { status: "logged_in" }),
runtime("codex", "available", { status: "logged_out" }),
],
globalAgentConfig: {
env_vars: {},
provider: null,
model: null,
preferred_runtime: null,
},
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await page.getByTestId("onboarding-setup-next").click();
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
"Claude Code",
);
await expect(page.getByTestId("onboarding-finish")).toBeEnabled();
expect(await readSavedRuntime(page)).toBeNull();
});
test("Next persists the latest staged harness choice", async ({ page }) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("claude", "available", { status: "logged_in" }),
runtime("codex", "available", { status: "logged_in" }),
],
globalAgentConfig: {
env_vars: {},
provider: null,
model: null,
preferred_runtime: null,
},
setGlobalAgentConfigDelayMs: 300,
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await page.getByTestId("onboarding-setup-next").click();
const harness = page.getByTestId("global-agent-default-harness");
await harness.click();
await page.getByTestId("global-agent-default-harness-option-claude").click();
await harness.click();
await page.getByTestId("global-agent-default-harness-option-codex").click();
const finish = page.getByTestId("onboarding-finish");
await expect(finish).toBeEnabled();
expect(await readGlobalConfigSetterCallCount(page)).toBe(0);
await finish.click();
await expect(page.getByText("Join or create a community")).toBeVisible();
await expect.poll(() => readSavedRuntime(page)).toBe("codex");
});
test("Next shows saving state and advances only after persistence", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("claude", "available", { status: "logged_in" }),
runtime("codex", "available", { status: "logged_in" }),
],
globalAgentConfig: {
env_vars: {},
provider: null,
model: null,
preferred_runtime: null,
},
setGlobalAgentConfigDelayMs: 500,
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await page.getByTestId("onboarding-setup-next").click();
const harness = page.getByTestId("global-agent-default-harness");
await harness.click();
await page.getByTestId("global-agent-default-harness-option-codex").click();
await page.getByTestId("onboarding-finish").click();
await expect(page.getByTestId("onboarding-finish")).toHaveText("Saving…");
await expect(page.getByTestId("onboarding-config-skip")).toBeDisabled();
await expect(page.getByTestId("onboarding-back")).toBeDisabled();
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
expect(await readSavedRuntime(page)).toBeNull();
await expect(page.getByText("Join or create a community")).toBeVisible();
expect(await readSavedRuntime(page)).toBe("codex");
});
test("Next keeps the draft and retries after a save failure", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("claude", "available", { status: "logged_in" }),
],
globalAgentConfig: {
env_vars: {},
provider: null,
model: null,
preferred_runtime: null,
},
setGlobalAgentConfigErrors: ["Disk is read-only", null],
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await page.getByTestId("onboarding-setup-next").click();
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
"Claude Code",
);
await page.getByTestId("onboarding-finish").click();
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
await expect(page.getByTestId("onboarding-config-save-error")).toContainText(
"Disk is read-only",
);
await expect(page.getByTestId("onboarding-finish")).toBeEnabled();
expect(await readSavedRuntime(page)).toBeNull();
expect(await readGlobalConfigSetterCallCount(page)).toBe(1);
await page.getByTestId("onboarding-finish").click();
await expect(page.getByText("Join or create a community")).toBeVisible();
expect(await readSavedRuntime(page)).toBe("claude");
expect(await readGlobalConfigSetterCallCount(page)).toBe(2);
});
test("defaults requires a choice when multiple visible harnesses are ready", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("buzz-agent", "available", { status: "not_applicable" }),
runtime("goose", "available", { status: "not_applicable" }),
runtime("claude", "available", { status: "logged_in" }),
runtime("codex", "available", { status: "logged_in" }),
],
globalAgentConfig: {
env_vars: {},
provider: null,
model: null,
preferred_runtime: null,
},
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await page.getByTestId("onboarding-setup-next").click();
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
const harness = page.getByTestId("global-agent-default-harness");
await expect(harness).toHaveText("Select a harness");
await expect(page.getByTestId("onboarding-finish")).toBeDisabled();
await harness.click();
await expect(
page.getByTestId("global-agent-default-harness-option-claude"),
).toBeVisible();
await expect(
page.getByTestId("global-agent-default-harness-option-codex"),
).toBeVisible();
await expect(
page.getByTestId("global-agent-default-harness-option-goose"),
).toBeVisible();
await expect(
page.getByTestId("global-agent-default-harness-option-buzz-agent"),
).toBeVisible();
await page.getByTestId("global-agent-default-harness-option-codex").click();
await expect(harness).toHaveText("Codex");
await expect(page.getByTestId("onboarding-finish")).toBeEnabled();
expect(await readSavedRuntime(page)).toBeNull();
});
/**
* Two installs started concurrently — claude fails with a multiline error
* (rich hint+stderr in the tooltip) while codex succeeds. Each card must
* keep its own independent spinner and its own terminal result; neither card
* may show the other's outcome.
*
* This is the behavioral regression test for the per-card mutation fix
* (Bug B) and the multiline tooltip fix (Bug A / F3 from Thufir pass 1).
*/
test("concurrent installs each keep their own state — one fails, one succeeds", async ({
page,
}) => {
// Realistic 512-head + 1024-tail shape: many short lines followed by one
// long unbroken Windows path. This exercises both overflow axes:
// • vertical: enough lines to exceed max-h-48 (192px at ~16px/line)
// • horizontal: the long path has no spaces, so only break-words prevents
// scrollWidth > clientWidth.
const longWindowsPath =
"C:\\Users\\willp\\AppData\\Roaming\\npm\\node_modules\\@agentclientprotocol\\claude-agent-acp\\dist\\bin\\claude-agent-acp.exe";
const multilineError = [
"npm ERR! code EACCES",
"npm ERR! syscall mkdir",
"npm ERR! path C:\\Users\\willp\\AppData\\Roaming\\npm",
"npm ERR! errno -4048",
"npm ERR! Error: EACCES: permission denied, mkdir 'C:\\Users\\willp\\AppData\\Roaming\\npm'",
"npm ERR! { [Error: EACCES: permission denied, mkdir 'C:\\Users\\willp\\AppData\\Roaming\\npm']",
"npm ERR! errno: -4048,",
"npm ERR! code: 'EACCES',",
"npm ERR! syscall: 'mkdir',",
"npm ERR! path: 'C:\\\\Users\\\\willp\\\\AppData\\\\Roaming\\\\npm' }",
"npm ERR!",
"npm ERR! The operation was rejected by your operating system.",
"npm ERR! It is likely you do not have the permissions to access this file as the current user",
"npm ERR!",
`npm ERR! If you believe this might be a permissions issue, please double-check the`,
`npm ERR! permissions of the file and its containing directories, or try running`,
`npm ERR! the command again as root/Administrator.`,
"",
`Hint: Run as Administrator or change npm prefix: npm config set prefix ${longWindowsPath}`,
].join("\n");
const claudeNotInstalled = runtime("claude", "adapter_missing", {
status: "unknown",
});
const codexNotInstalled = runtime("codex", "adapter_missing", {
status: "unknown",
});
await installMockBridge(
page,
{
acpRuntimesCatalog: [claudeNotInstalled, codexNotInstalled],
// Claude: long delay then failure with multiline stderr + hint.
// Codex: short delay then success.
// Per-runtime config lets both be in flight simultaneously.
installAcpRuntimeByRuntime: {
claude: {
delayMs: 600,
result: {
success: false,
steps: [
{
step: "adapter",
command: "npm install -g @agentclientprotocol/claude-agent-acp",
success: false,
stdout: "",
stderr: multilineError,
exit_code: 1,
},
],
},
},
codex: {
delayMs: 200,
result: {
success: true,
steps: [
{
step: "adapter",
command: "npm install -g @zed-industries/codex-acp",
success: true,
stdout: "added 1 package",
stderr: "",
exit_code: 0,
},
],
},
},
},
acpRuntimesCatalogAfterInstall: [
runtime("claude", "adapter_missing", { status: "unknown" }),
runtime("codex", "available", { status: "logged_in" }),
],
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
const claudeInstall = page.getByTestId("onboarding-runtime-install-claude");
const codexInstall = page.getByTestId("onboarding-runtime-install-codex");
// Start both installs before either settles.
await claudeInstall.click();
await codexInstall.click();
// While in flight: both install buttons must be absent (no duplicate clicks).
await expect(claudeInstall).toHaveCount(0);
await expect(codexInstall).toHaveCount(0);
// Codex settles first (shorter delay): success indicator, no error.
await expect(page.getByTestId("onboarding-runtime-ready-codex")).toBeVisible({
timeout: 3_000,
});
await expect(page.getByTestId("onboarding-runtime-error-codex")).toHaveCount(
0,
);
// Claude still in flight: its install button must still be absent.
await expect(claudeInstall).toHaveCount(0);
// Claude settles: failure error visible; codex still shows ready (not reset).
const claudeError = page.getByTestId("onboarding-runtime-error-claude");
await expect(claudeError).toBeVisible({ timeout: 3_000 });
await expect(
page.getByTestId("onboarding-runtime-ready-codex"),
).toBeVisible();
await expect(page.getByTestId("onboarding-runtime-error-codex")).toHaveCount(
0,
);
// The error trigger has the full aria-label (label + detail).
await expect(claudeError).toHaveAttribute("aria-label", /npm ERR!/);
// Open the tooltip and verify the detail span handles overflow correctly:
// • vertical overflow exists and is scrollable (max-h-48 + overflow-y-auto)
// • no horizontal overflow (break-words forces the long unbroken path to wrap)
await claudeError.focus();
const tooltip = page.getByRole("tooltip");
await expect(tooltip).toBeVisible({ timeout: 2_000 });
await expect(tooltip).toContainText("npm ERR! code EACCES");
await expect(tooltip).toContainText("Hint: Run as Administrator");
// Moving from the trigger into the portalled tooltip must keep it open so
// pointer users can operate the scrollable error detail.
const tooltipBox = await tooltip.boundingBox();
if (!tooltipBox) throw new Error("Expected runtime error tooltip bounds");
await page.mouse.move(tooltipBox.x + 8, tooltipBox.y + 8);
await expect(tooltip).toBeVisible();
await expect(tooltip).toHaveCSS("pointer-events", "auto");
// Locate the scroll container using page-level locator since Radix portals
// can place content outside the tooltip role element's subtree in the DOM.
// Use .first() because Radix keeps a hidden duplicate in the light DOM.
const detailSpan = page.locator("span.overflow-y-auto").first();
await expect(detailSpan).toBeVisible();
// Vertical: scrollHeight must exceed clientHeight (content taller than max-h-48).
// Scroll position must advance when set, proving scrollability.
const isVerticallyScrollable = await detailSpan.evaluate((el) => {
return el.scrollHeight > el.clientHeight;
});
expect(isVerticallyScrollable).toBe(true);
// Confirm scroll position can actually advance.
await detailSpan.evaluate((el) => {
el.scrollTop = 9999;
});
const scrolledDown = await detailSpan.evaluate((el) => el.scrollTop > 0);
expect(scrolledDown).toBe(true);
// Horizontal: break-words must prevent horizontal overflow.
const hasHorizontalOverflow = await detailSpan.evaluate((el) => {
return el.scrollWidth > el.clientWidth;
});
expect(hasHorizontalOverflow).toBe(false);
});
test("Finish stays disabled until a provider-required harness is fully configured", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("buzz-agent", "available", { status: "not_applicable" }),
],
discoverAgentModels: {
models: [{ id: "claude-sonnet-4", name: "Claude Sonnet 4" }],
supportsSwitching: true,
},
globalAgentConfig: {
env_vars: {},
provider: null,
model: null,
preferred_runtime: null,
},
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await page.getByTestId("onboarding-setup-next").click();
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
// buzz-agent auto-selects as the only ready harness, but with no provider
// configured the default is not launchable — Finish must be gated.
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
"Buzz",
);
const finish = page.getByTestId("onboarding-finish");
await expect(finish).toBeDisabled();
// Configure provider + credential; model resolves via discovery/fallback.
await page.getByTestId("global-agent-provider").click();
await page.getByTestId("global-agent-provider-option-anthropic").click();
await page.getByTestId("persona-provider-api-key").fill("sk-test-key");
await expect(finish).toBeEnabled();
await finish.click();
await expect(page.getByText("Join or create a community")).toBeVisible();
expect(await readSavedRuntime(page)).toBe("buzz-agent");
});
test("baked build config keeps Finish enabled without manual provider setup", async ({
page,
}) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("buzz-agent", "available", { status: "not_applicable" }),
],
bakedBuildEnv: [
{ key: "BUZZ_AGENT_PROVIDER", masked: false, value: "databricks_v2" },
{
key: "DATABRICKS_HOST",
masked: false,
value: "https://example.cloud.databricks.com",
},
{ key: "DATABRICKS_MODEL", masked: false, value: "baked-model" },
],
globalAgentConfig: {
env_vars: {},
provider: null,
model: null,
preferred_runtime: null,
},
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await page.getByTestId("onboarding-setup-next").click();
await expect(page.getByTestId("onboarding-page-config")).toBeVisible();
// Internal builds bake provider/model/credentials — the gate must treat
// baked config as complete and never block Finish.
await expect(page.getByTestId("global-agent-default-harness")).toHaveText(
"Buzz",
);
await expect(page.getByTestId("onboarding-finish")).toBeEnabled();
});