Files
buzz/desktop/tests/e2e/inline-custom-harness.spec.ts
T
Will PflegerandGitHub b0503d80c2 feat(desktop): add custom harness inline from agent dialogs (#3252)
Registering a custom ACP harness works today, but only from Settings →
Agents. Anyone whose first touchpoint is "New agent" has no way to
discover the custom path — the dropdown just lists the baked-in presets
plus whatever was registered earlier. This adds an inline "Add custom
harness…" entry to the harness dropdown in all three agent surfaces:
create, edit-definition (`AgentDefinitionDialog`), and instance edit
(`AgentInstanceEditDialog`).

The entry is a sentinel option (`ADD_CUSTOM_HARNESS_VALUE`, NUL-prefixed
so it can never collide with a real harness id — backend ids match
`[a-z0-9_][a-z0-9_-]*`), mirroring the `CUSTOM_ENTRY_ID` trick already
used in `HarnessCatalogDialog`. Picking it never writes into form state;
it opens `AddCustomHarnessDialog`, a thin modal wrapper hosting the
existing `CustomHarnessForm` in `chromeless` mode. `CustomHarnessForm`'s
`onSaved` now carries the saved `definition.id` (the form may rewrite
it); the two existing call sites ignore the argument, so their behavior
is unchanged.

Selection after save is deferred rather than immediate.
`usePendingHarnessSelection` holds the saved id until the runtime
catalog actually publishes it via discovery, then selects it exactly
once — so the dialog never selects an id it cannot render, and
back-to-back registrations resolve correctly. The wait is scoped to the
owning dialog's `open` state: both host dialogs stay mounted when
closed, so an unpublished id is dropped on close rather than selecting
into reset form state when discovery later catches up. Selection is
routed through each dialog's normal dropdown change handler, so
provider/model reset (and command pinning in the instance dialog) behave
identically to a hand-picked harness. Dismissing the modal leaves the
previous selection untouched. `AgentInstanceEditDialog`'s existing
"Custom command" option is a different feature (ad-hoc command override
vs. a registered reusable harness) and is untouched.

Coverage is 16 unit tests in `addCustomHarness.test.mjs` (real React
mount, following the existing `.test.mjs` pattern) plus 4 Playwright
specs in `inline-custom-harness.spec.ts` covering all three surfaces
end-to-end. Both suites were mutation-verified: treating the sentinel as
a real selection, selecting before the catalog publishes, never clearing
the pending id, ignoring the dialog's open state, and reversing
latest-save-wins each turn the unit tests red; reverting the two dialog
diffs turns all four e2e specs red. The `check-file-sizes.mjs` overrides
for the two dialogs are ratcheted to their exact new counts (1048 and
1229) — verified tight in both directions, N passes and N−1 fails, so no
headroom is introduced.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-07-28 16:01:41 -04:00

195 lines
7.1 KiB
TypeScript

/**
* E2E spec for the inline "Add custom harness…" entry in the agent dialogs.
*
* Registering a custom harness used to be reachable only from Settings →
* Agents, so anyone whose first touchpoint was "New agent" never learned the
* path existed. The harness dropdowns now carry the entry directly.
*
* Covers, on all three surfaces (create, edit definition, edit instance):
* - choosing the entry opens the registration form
* - saving registers the harness and selects it in the dropdown
*
* Create and instance edit additionally assert the sentinel never becomes the
* selection; create alone covers dismissing the form leaving the previous
* selection untouched. The three surfaces share the same routing, so those
* checks are not repeated on every one.
*/
import { expect, test } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
const ADD_ENTRY = "Add custom harness…";
const HARNESS_LABEL = "My Weird Agent";
const HARNESS_COMMAND = "my-weird-acp";
type Page = import("@playwright/test").Page;
type Locator = import("@playwright/test").Locator;
/** Open a PersonaDropdownField (button trigger + menuitemradio options). */
async function openDropdown(trigger: Locator) {
await expect(trigger).toBeVisible({ timeout: 10_000 });
await trigger.click();
}
/** Register a harness through the inline form and wait for it to close. */
async function registerHarness(page: Page) {
const form = page.getByTestId("custom-harness-form");
await expect(form).toBeVisible({ timeout: 8_000 });
await page.fill("#ch-label", HARNESS_LABEL);
await page.fill("#ch-command", HARNESS_COMMAND);
// The id auto-derives from the label; it is what the dropdown selects on.
await expect(page.locator("#ch-id")).toHaveValue("my-weird-agent");
await form.getByRole("button", { name: "Save", exact: true }).click();
await expect(page.getByTestId("add-custom-harness-dialog")).not.toBeVisible({
timeout: 8_000,
});
}
/** Open the create-agent dialog (AgentDefinitionDialog, create mode). */
async function openCreateDialog(page: Page) {
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-agents-view").click();
await page.getByTestId("new-agent-card").click();
await page.getByRole("menuitem", { name: "Create agent" }).click();
const dialog = page.getByTestId("persona-dialog");
await expect(dialog).toBeVisible({ timeout: 10_000 });
await dialog.getByRole("tab", { name: "Customize for this agent" }).click();
return dialog;
}
/** Open the edit dialog for a saved definition (same dialog, edit mode). */
async function openDefinitionEditDialog(page: Page, name: string) {
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-agents-view").click();
await expect(page.getByTestId("agents-library-personas")).toBeVisible({
timeout: 10_000,
});
await page.getByRole("button", { name: `Open actions for ${name}` }).click();
await page.getByRole("menuitem", { name: "Edit" }).click();
const dialog = page.getByTestId("persona-dialog");
await expect(dialog).toBeVisible({ timeout: 10_000 });
await dialog.getByRole("tab", { name: "Customize for this agent" }).click();
return dialog;
}
test.describe("inline add custom harness", () => {
test("create dialog registers a harness inline and selects it", async ({
page,
}) => {
await installMockBridge(page);
const dialog = await openCreateDialog(page);
const harness = dialog.locator("#persona-runtime");
await openDropdown(harness);
await page.getByRole("menuitemradio", { name: ADD_ENTRY }).click();
// The sentinel opens the form; it must never become the selection.
await expect(page.getByTestId("add-custom-harness-dialog")).toBeVisible({
timeout: 8_000,
});
await expect(harness).not.toContainText(ADD_ENTRY);
await registerHarness(page);
// The saved harness is now the selected harness.
await expect(harness).toContainText(HARNESS_LABEL, { timeout: 8_000 });
});
test("dismissing the form leaves the create dialog's harness unchanged", async ({
page,
}) => {
await installMockBridge(page);
const dialog = await openCreateDialog(page);
const harness = dialog.locator("#persona-runtime");
await expect(harness).toBeVisible({ timeout: 10_000 });
const before = await harness.textContent();
await openDropdown(harness);
await page.getByRole("menuitemradio", { name: ADD_ENTRY }).click();
await expect(page.getByTestId("add-custom-harness-dialog")).toBeVisible({
timeout: 8_000,
});
await page.keyboard.press("Escape");
await expect(
page.getByTestId("add-custom-harness-dialog"),
).not.toBeVisible();
// No harness was registered, so the prior selection must survive.
await expect(harness).toHaveText(before ?? "");
});
test("definition edit dialog registers a harness inline and selects it", async ({
page,
}) => {
await installMockBridge(page, {
personas: [
{
displayName: "Editable Agent",
systemPrompt: "An agent whose harness gets replaced.",
},
],
});
const dialog = await openDefinitionEditDialog(page, "Editable Agent");
const harness = dialog.locator("#persona-runtime");
await openDropdown(harness);
await page.getByRole("menuitemradio", { name: ADD_ENTRY }).click();
await expect(page.getByTestId("add-custom-harness-dialog")).toBeVisible({
timeout: 8_000,
});
await registerHarness(page);
await expect(harness).toContainText(HARNESS_LABEL, { timeout: 8_000 });
});
test("instance edit dialog registers a harness inline and keeps Custom command", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey:
"npub1e2e00000000000000000000000000000000000000000000000000000000",
name: "Instance Agent",
status: "stopped",
channelNames: ["agents"],
},
],
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-agents-view").click();
await page
.getByRole("button", { name: "Instance Agent agent profile" })
.click();
await expect(page.getByTestId("user-profile-panel")).toBeVisible({
timeout: 10_000,
});
await page.getByTestId("user-profile-edit-agent").click();
await expect(page.getByTestId("edit-agent-dialog")).toBeVisible({
timeout: 10_000,
});
const provider = page.locator("#edit-agent-runtime");
await openDropdown(provider);
// "Custom command" is a different feature (ad-hoc command override) and
// must survive alongside the new entry.
await expect(
page.getByRole("menuitemradio", { name: "Custom command" }),
).toBeVisible();
await page.getByRole("menuitemradio", { name: ADD_ENTRY }).click();
await expect(page.getByTestId("add-custom-harness-dialog")).toBeVisible({
timeout: 8_000,
});
await expect(provider).not.toContainText(ADD_ENTRY);
await registerHarness(page);
await expect(provider).toContainText(HARNESS_LABEL, { timeout: 8_000 });
});
});