Files
buzz/desktop/tests/e2e/onboarding-agent-defaults.spec.ts
Taylor Ho d98da7389e feat(desktop): redesign agent runtime settings (#3093)
**Category:** improvement
**User Impact:** Users can understand, install, authenticate, and manage
agent runtimes from one progressively disclosed Agents settings
experience.

**Problem:** Runtime health and custom harness management were split
across overlapping settings surfaces, exposing low-level configuration
too early while leaving setup and authentication states hard to
understand. **Solution:** Consolidate those operations into one stable
runtime list and an Add runtimes catalog, with task-oriented state
labels, direct setup actions, and custom configuration contained in a
dedicated form.

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

**desktop/playwright.config.ts**
Registers the visual coverage needed for the redesigned runtime catalog.

**desktop/public/harness-logos/CREDITS.md**
Documents bundled runtime-mark provenance and the decision not to ship
the withdrawn OpenAI mark.

**desktop/public/runtime-icons/codex.png**
Removes the obsolete Codex bitmap in favor of the neutral fallback.

**desktop/public/runtime-icons/goose.svg**
Removes the old Goose asset now replaced by the theme-adaptive mark.

**desktop/src-tauri/src/managed_agents/discovery.rs**
Aligns runtime discovery guidance with the new task-oriented setup
language.

**desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs**
Updates runtime metadata used by the redesigned settings states.

**desktop/src/features/agents/ui/runtimeAvailabilityWarning.test.mjs**
Updates availability-warning expectations for the revised runtime
guidance.

**desktop/src/features/onboarding/assets/harness-logos/chatgpt.png**
Removes the redundant bitmap from the unified runtime icon pipeline.

**desktop/src/features/onboarding/assets/harness-logos/goose.png**
Removes the redundant Goose bitmap.

**desktop/src/features/onboarding/ui/HarnessMarks.tsx**
Adds theme-adaptive bundled runtime marks with safe fallbacks.

**desktop/src/features/onboarding/ui/RuntimeIcon.tsx**
Centralizes runtime logo rendering so settings and catalog rows cannot
drift.

**desktop/src/features/onboarding/ui/SetupStep.tsx**
Aligns onboarding runtime setup copy with the settings terminology.

**desktop/src/features/onboarding/ui/presetLogos.test.mjs**
Guards bundled-logo behavior and prevents the withdrawn Codex mark from
returning.

**desktop/src/features/settings/ui/CustomHarnessForm.tsx**
Reworks custom runtime creation and editing into a clear, dedicated
catalog form.

**desktop/src/features/settings/ui/HarnessCatalogDialog.tsx**
Introduces the Add runtimes master-detail catalog, grouped setup states,
loading treatment, and pinned actions.

**desktop/src/features/settings/ui/HarnessManagementCard.tsx**
Removes the superseded standalone custom-harness management surface.

**desktop/src/features/settings/ui/HarnessRow.tsx**
Provides stable operational runtime rows with install, update,
authentication, edit, and delete behavior.

**desktop/src/features/settings/ui/HarnessesSettingsPanel.tsx**
Consolidates runtime health and custom management into one Agents
settings panel.

**desktop/src/features/settings/ui/SettingsPanels.tsx**
Wires the consolidated panel into Agents settings.

**desktop/src/features/settings/ui/harnessCatalogCopy.ts**
Adds restrained, source-annotated runtime descriptions and setup
guidance.

**desktop/src/features/settings/ui/harnessCatalogLogic.test.mjs**
Covers grouping, state labels, stable row order, actions, and adapter
warnings.

**desktop/src/features/settings/ui/harnessCatalogLogic.ts**
Centralizes catalog grouping, actions, status labels, and runtime-safe
warning copy.

**desktop/src/features/settings/ui/harnessGalleryLogic.test.mjs**
Removes obsolete gallery-only tests after consolidation.

**desktop/src/features/settings/ui/harnessGalleryLogic.ts**
Retains only the shared custom-runtime safety logic needed by the new
surface.

**desktop/src/shared/ui/config-nudge-attachment.tsx**
Points configuration nudges to Agent runtimes with matching terminology.

**desktop/src/testing/e2eBridge.ts**
Adds deterministic runtime states for authentication and catalog E2E
coverage.

**desktop/tests/e2e/doctor-states.spec.ts**
Verifies ready, setup-required, node-gated, authentication, loading, and
error contracts.

**desktop/tests/e2e/harness-catalog-screenshots.spec.ts**
Captures whole-pane visual states for the runtime catalog experience.

**desktop/tests/e2e/harness-management.spec.ts**
Exercises catalog actions and custom runtime create, edit,
authentication, and deletion flows.

**desktop/tests/e2e/onboarding-agent-defaults.spec.ts**
Updates default-runtime expectations for the consolidated experience.

**desktop/tests/e2e/profile.spec.ts**
Aligns profile navigation assertions with the new settings surface.

</details>

## Reproduction steps

1. Open Settings → Agents and inspect Agent runtimes; ready, signed-out,
installable, and setup-required runtimes should have stable rows and
explicit actions.
2. Open Add runtimes and browse the Setup and Installed groups; select
entries to see sourced guidance and a pinned Install or Setup guide
action.
3. Select Custom harness, create a runtime, then edit and delete it;
verify required-field gating and the blast-radius confirmation.
4. Exercise a signed-out runtime and connect it; the row should move
from Sign-in needed to Ready without reordering.
5. Resize the window and switch themes to verify responsive layout and
adaptive bundled marks.

## Screenshots

| Mixed runtime states | Goose not installed | Sign-in needed |
|---|---|---|
| <img width="980" height="1000" alt="image"
src="https://github.com/user-attachments/assets/28705b39-1d91-440a-a954-fb2d7d8ee759"
/> | <img width="980" height="1000" alt="image"
src="https://github.com/user-attachments/assets/e65ce8e1-8c85-4272-afb5-e6001b94d560"
/> | <img width="980" height="1000" alt="image"
src="https://github.com/user-attachments/assets/3987f355-960d-4744-8c26-b3746944a33c"
/> |

| Add runtimes catalog | Custom runtime | Setup guide |
|---|---|---|
| <img width="896" height="672" alt="image"
src="https://github.com/user-attachments/assets/a2f32c8a-4dff-4929-af96-b07346933335"
/> | <img width="896" height="672" alt="image"
src="https://github.com/user-attachments/assets/b2e85a0c-3940-4875-a138-4035423c9de7"
/> | <img width="896" height="672" alt="image"
src="https://github.com/user-attachments/assets/5a690c1c-368e-4ee3-98b9-ffefdc3cf804"
/> |

Full 13-state screenshot matrix and review evidence:
https://buzz.block.builderlab.xyz/channels/c5af9e3e-4317-4853-b3e3-ed9c15bc511d?event=ec88f6e472f2b08d6318ae76fd754fd4f218385f67cfe59b73034c4bc34c9252

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
2026-07-27 11:01:42 -07:00

918 lines
30 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;
});
}
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);
});
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 Back returns to harness setup", async ({ page }) => {
await installMockBridge(
page,
{
acpRuntimesCatalog: [
runtime("claude", "available", { status: "logged_in" }),
],
},
{ skipCommunitySeed: true, skipOnboardingSeed: true },
);
await page.goto("/");
await navigateToSetupPage(page);
await page.getByTestId("onboarding-setup-next").click();
await page.getByTestId("onboarding-back").click();
await expect(page.getByTestId("onboarding-page-2")).toBeVisible();
});
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();
await expect.poll(() => readSavedRuntime(page)).toBe("claude");
});
test("Finish waits for the latest rapid harness choice to persist", 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).toBeDisabled();
await expect(finish).toBeEnabled({ timeout: 2_000 });
await finish.click();
await expect(page.getByText("Join or create a community")).toBeVisible();
expect(await readSavedRuntime(page)).toBe("codex");
});
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();
await expect.poll(() => readSavedRuntime(page)).toBe("codex");
});
/**
* 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");
// 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();
});