Files
buzz/desktop/tests/e2e/where-to-run-config.spec.ts
Taylor Ho 02f640bc45 feat(desktop): unify add agent flows (#5015)
**Category:** improvement
**User Impact:** Users can create, discover, and import agents from one
consistent Add agent dialog.

**Problem:** Agent creation, discovery, and import were split across a
dropdown and separate dialogs, making the Add agent flow fragmented. The
existing E2E suite also continued targeting the deleted dropdown after
the flows were unified.

**Solution:** Route the new-agent card directly into a unified dialog
with dedicated Create, catalog, and Import navigation, then update the
affected E2E coverage to exercise that interface and its current empty
state.

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

**desktop/src/features/agents/ui/AgentDefinitionDialog.tsx**
Supports rendering the agent definition form inside the unified Add
agent experience while retaining the standalone dialog behavior.

**desktop/src/features/agents/ui/AgentDefinitionDialogShell.tsx**
Adds the shared shell used to present agent-definition content
consistently in embedded and standalone contexts.

**desktop/src/features/agents/ui/AgentDialog.tsx**
Passes the revised dialog state and close behavior through the existing
agent dialog entry point.

**desktop/src/features/agents/ui/AgentsView.tsx**
Connects the Agents page to the unified Add agent dialog and opens newly
added catalog agents in their profile panel.

**desktop/src/features/agents/ui/PersonaCatalogDialog.tsx**
Combines catalog browsing, agent creation, and snapshot import behind
persistent navigation, including dirty-navigation confirmation.

**desktop/src/features/agents/ui/UnifiedAgentsSection.tsx**
Replaces the new-agent dropdown with a direct Add agent entry point and
adjusts the responsive card grid.

**desktop/src/features/agents/ui/personaLibraryCopy.ts**
Updates catalog-facing copy for the unified experience.

**desktop/src/features/agents/ui/usePersonaActions.ts**
Returns the resolved local persona after catalog activation so the
caller can open the added agent.

**desktop/tests/e2e/agent-readiness-screenshots.spec.ts**
Opens the embedded create pane directly for readiness screenshots.

**desktop/tests/e2e/agents.spec.ts**
Covers unified Create, catalog, and Import navigation and asserts the
current shared-agent empty state.

**desktop/tests/e2e/global-agent-config-screenshots.spec.ts**
Updates global configuration screenshot setup for direct create-pane
entry.

**desktop/tests/e2e/inline-custom-harness.spec.ts**
Updates custom harness setup for the embedded create form.

**desktop/tests/e2e/persona-env-vars.spec.ts**
Updates environment-variable and model-provider scenarios for direct
create-pane entry.

**desktop/tests/e2e/persona-model-combobox-screenshots.spec.ts**
Updates model combobox screenshot setup for direct create-pane entry.

**desktop/tests/e2e/smoke.spec.ts**
Updates agent-creation smoke coverage for the unified Add agent dialog.

**desktop/tests/e2e/where-to-run-config.spec.ts**
Updates provider-selection coverage for the embedded create form.

</details>

## Reproduction steps

1. Open the Agents page and select the new-agent card.
2. Confirm the Add agent dialog opens directly on Create without an
intermediate dropdown.
3. Use the left navigation to browse shared agents and open Import.
4. Select a catalog agent and confirm the dialog closes and the added
agent's profile panel opens.
5. Run the affected desktop Playwright smoke and integration specs and
confirm all scenarios pass.

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
2026-08-07 16:25:49 -07:00

232 lines
8.2 KiB
TypeScript

/**
* E2E spec for the create-agent "Run on" provider config fields.
*
* Pins the fix for the "Typewriter Eraser": WhereToRunSection's probe effect
* used to depend on the whole draft, so every keystroke re-probed the
* provider and every probe resolution reset providerConfig to schema
* defaults — typing into a defaultless field (the k8s "Kubeconfig context")
* looked completely dead, and the provider binary respawned in a loop.
*
* Covers:
* - typing into a defaultless provider field sticks, and the provider is
* probed exactly once for the selection (not once per keystroke or
* Advanced disclosure toggle)
* - the config form is gated on probe resolution (no half-rendered form),
* and defaults prefill exactly once when a slow probe lands
* - collapsing Advanced during an incomplete remote setup keeps the submit
* blocker visible through the Required badge
* - switching provider → local → provider re-probes and resets cleanly
*
* The stale-closure merge on probe resolution (defaults beneath in-flight
* typing) is unreachable through this UI because the fields render only
* after the probe resolves; it is pinned at the unit level in
* whereToRunIntent.test.mjs (applyProbeResult).
*/
import { expect, test } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
type Page = import("@playwright/test").Page;
const PROVIDER = {
id: "kubernetes",
binaryPath: "/mock/buzz-backend-kubernetes",
};
const PROBE_RESULT = {
ok: true,
name: "kubernetes",
version: "0.0.0-mock",
config_schema: {
type: "object",
properties: {
context: {
type: "string",
title: "Kubeconfig context",
description: "Context from your kubeconfig.",
},
namespace: {
type: "string",
title: "Namespace",
default: "buzz-agents-mock01",
},
},
required: ["namespace"],
},
};
async function probeInvocations(page: Page): Promise<number> {
return page.evaluate(
() =>
(
window as Window & { __BUZZ_E2E_COMMANDS__?: string[] }
).__BUZZ_E2E_COMMANDS__?.filter(
(command) => command === "probe_backend_provider",
).length ?? 0,
);
}
async function selectRunOnOption(
page: Page,
dialog: import("@playwright/test").Locator,
optionName: string,
) {
const trigger = dialog.locator("#agent-run-on");
await expect(trigger).toHaveAttribute("aria-expanded", "false");
await trigger.press("Enter");
const option = page.getByRole("menuitemradio", {
exact: true,
name: optionName,
});
await expect(option).toBeVisible();
// The shared PersonaDropdownField supports keyboard selection. Using it here
// avoids racing the menu's open animation when this test changes locations
// repeatedly.
await option.press("Enter");
await expect(trigger).toHaveAttribute("aria-expanded", "false");
}
/** Open Advanced in the create-agent dialog and select the mocked provider. */
async function openCreateDialogOnProvider(page: Page) {
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-agents-view").click();
await page.getByTestId("new-agent-card").click();
const dialog = page.getByTestId("persona-dialog");
await expect(dialog).toBeVisible({ timeout: 10_000 });
const advanced = dialog.getByRole("button", {
name: "Advanced",
exact: true,
});
await expect(advanced).toHaveAttribute("aria-expanded", "false");
await expect(dialog.locator("#agent-run-on")).toHaveCount(0);
await advanced.click();
await expect(advanced).toHaveAttribute("aria-expanded", "true");
const respondTo = dialog.getByTestId("agent-respond-to");
const runOn = dialog.locator("#agent-run-on");
await expect(respondTo).toBeVisible();
await expect(runOn).toBeVisible();
expect(await respondTo.evaluate((element) => element.offsetTop)).toBeLessThan(
await runOn.evaluate((element) => element.offsetTop),
);
await selectRunOnOption(page, dialog, PROVIDER.id);
return dialog;
}
test("typing into a defaultless provider field sticks and probes only once", async ({
page,
}) => {
await installMockBridge(page, {
backendProviders: [PROVIDER],
backendProviderProbeResult: PROBE_RESULT,
});
const dialog = await openCreateDialogOnProvider(page);
const contextField = dialog.locator("#provider-cfg-context");
await expect(contextField).toBeVisible({ timeout: 10_000 });
// Defaults prefilled from the schema; context has none.
await expect(dialog.locator("#provider-cfg-namespace")).toHaveValue(
"buzz-agents-mock01",
);
await expect(contextField).toHaveValue("");
await contextField.fill("prod-us-west");
await expect(contextField).toHaveValue("prod-us-west");
// One selection, one probe — keystrokes and Advanced disclosure toggles
// must not refire executable provider discovery after it has completed.
expect(await probeInvocations(page)).toBe(1);
const advanced = dialog.getByRole("button", {
name: "Advanced",
exact: true,
});
await advanced.click();
await expect(advanced).toHaveAttribute("aria-expanded", "false");
await expect(dialog.locator("#agent-run-on")).toHaveCount(0);
await advanced.click();
await expect(advanced).toHaveAttribute("aria-expanded", "true");
await expect(dialog.locator("#provider-cfg-context")).toHaveValue(
"prod-us-west",
);
expect(await probeInvocations(page)).toBe(1);
});
test("config fields render only after a slow probe resolves, with defaults", async ({
page,
}) => {
// The fields are gated on the probe result (draft.probedProvider), which is
// what makes mid-flight typing unreachable through the UI — the stale-probe
// merge seam (applyProbeResult) is pinned at the unit level instead. This
// spec holds the gate: no half-rendered form before the probe lands, and
// defaults appear exactly once when it does.
await installMockBridge(page, {
backendProviders: [PROVIDER],
backendProviderProbeResult: PROBE_RESULT,
backendProviderProbeDelayMs: 1_000,
});
const dialog = await openCreateDialogOnProvider(page);
// Pre-resolution: the security warning is up, the form is not.
await expect(dialog.getByText("will receive your agent")).toBeVisible();
await expect(dialog.locator("#provider-cfg-context")).toHaveCount(0);
// Post-resolution: fields render with schema defaults prefilled.
await expect(dialog.locator("#provider-cfg-context")).toBeVisible({
timeout: 10_000,
});
await expect(dialog.locator("#provider-cfg-namespace")).toHaveValue(
"buzz-agents-mock01",
);
expect(await probeInvocations(page)).toBe(1);
});
test("collapsed Advanced marks incomplete remote setup as required", async ({
page,
}) => {
await installMockBridge(page, {
backendProviders: [PROVIDER],
backendProviderProbeResult: PROBE_RESULT,
backendProviderProbeDelayMs: 10_000,
});
const dialog = await openCreateDialogOnProvider(page);
const advanced = dialog.getByRole("button", {
name: "Advanced",
exact: true,
});
const submit = dialog.getByTestId("persona-dialog-submit");
await expect(submit).toBeDisabled();
await advanced.click();
await expect(advanced).toHaveAttribute("aria-expanded", "false");
await expect(dialog.locator("#agent-run-on")).toHaveCount(0);
await expect(
dialog.getByTestId("persona-advanced-required-badge"),
).toHaveText("Required");
await expect(submit).toBeDisabled();
});
test("provider → local → provider re-probes and resets the config", async ({
page,
}) => {
await installMockBridge(page, {
backendProviders: [PROVIDER],
backendProviderProbeResult: PROBE_RESULT,
});
const dialog = await openCreateDialogOnProvider(page);
const contextField = dialog.locator("#provider-cfg-context");
await expect(contextField).toBeVisible({ timeout: 10_000 });
await contextField.fill("stale-value");
await selectRunOnOption(page, dialog, "This computer");
await expect(contextField).toHaveCount(0);
await selectRunOnOption(page, dialog, PROVIDER.id);
await expect(dialog.locator("#provider-cfg-context")).toBeVisible({
timeout: 10_000,
});
// Fresh selection = fresh draft: the stale value must not leak back.
await expect(dialog.locator("#provider-cfg-context")).toHaveValue("");
expect(await probeInvocations(page)).toBe(2);
});