mirror of
https://github.com/block/buzz.git
synced 2026-10-30 05:51:46 +00:00
**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 |  |  | | Private forum channel tooltip |  |  | | Public forum channel tooltip |  |  | | Channel references, including unresolved |  |  | | Unknown channel permalink |  |  | | Archived channel tooltip |  |  | | Repository entity |  |  | | Pull request entity |  |  | | Entity metadata fallback |  |  | ### Tooltip semantic surface spot check | Buzz light | Catppuccin Mocha dark | |---|---| |  |  | --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
1159 lines
38 KiB
TypeScript
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();
|
|
});
|