Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export default defineConfig({
"**/activity-scope-label-screenshots.spec.ts",
"**/local-archive-screenshots.spec.ts",
"**/agent-readiness-screenshots.spec.ts",
"**/edit-agent.spec.ts",
"**/pubkey-display-screenshots.spec.ts",
"**/file-attachment.spec.ts",
"**/image-attachment-gallery.spec.ts",
Expand Down
201 changes: 201 additions & 0 deletions desktop/tests/e2e/edit-agent.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { expect, test } from "@playwright/test";

import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";

// Edit-agent dialog coverage (Phase 1B.3b-pre). Written against TODAY'S
// EditAgentDialog, before the B3b re-host, so the re-host is guarded by a
// pre-existing spec rather than one written alongside it.
//
// Mock-boundary caveat: the e2eBridge `update_managed_agent` handler echoes
// name/model/systemPrompt/envVars/respondTo/respondToAllowlist into the
// mock store — it does NOT
// model the diff-based partial-update wire semantics (change-detected-or-omit,
// tri-state provider, harnessOverride derivation), and it ignores
// agentCommand/harnessOverride entirely. This spec therefore pins UI behavior
// (open → edit → save → persisted in UI), not wire semantics. The inherit
// toggle is not reachable here at all (see the routing pin below) — its
// behavior is covered by B3b's component-level pinning test (inherit-toggle
// → gate → submit); wire semantics stay component-test territory
// (personaRuntimeModel.test.mjs).

// Tyler's pubkey maps to gooseSurface in the mock bridge (runtimeId "goose"),
// which supports LLM provider selection — same seed the readiness-screenshot
// spec uses for its edit-dialog shot.
const AGENT_PUBKEY = TEST_IDENTITIES.tyler.pubkey;
const AGENT_NAME = "Tyler Agent";
const PERSONA_ID = "persona-edit-e2e";

/**
* Open the Edit Agent dialog for the seeded managed agent via the profile
* panel (agents view → agent card → Edit quick action) — EditAgentDialog's
* only mount path.
*/
async function openEditDialog(page: import("@playwright/test").Page) {
await page.goto("/");
await page.getByTestId("open-agents-view").click();

const agentButton = page.getByRole("button", {
name: `${AGENT_NAME} agent profile`,
});
await expect(agentButton).toBeVisible({ timeout: 10_000 });
await agentButton.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,
});
// Provider field visible = runtime catalog loaded and form settled.
await expect(page.locator("#edit-agent-llm-provider")).toBeVisible({
timeout: 10_000,
});
}

/**
* Pick an option from a PersonaDropdownField (menu-based, not a native
* <select> — Create's fields are selects, Edit's are not).
*/
async function pickDropdownOption(
page: import("@playwright/test").Page,
triggerId: string,
optionName: string | RegExp,
) {
await page.locator(`#${triggerId}`).click();
await page.getByRole("menuitemradio", { name: optionName }).click();
}

test.describe("edit agent dialog", () => {
test("edits the agent name and persists it across a dialog reopen", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: AGENT_PUBKEY,
name: AGENT_NAME,
status: "stopped",
channelNames: ["agents"],
},
],
});

await openEditDialog(page);

const nameInput = page.locator("#edit-agent-name");
await expect(nameInput).toHaveValue(AGENT_NAME);
await nameInput.fill("Tyler Agent Renamed");

await page.getByTestId("edit-agent-dialog-submit").click();
await expect(page.getByTestId("edit-agent-dialog")).not.toBeVisible();

// Reopen: the dialog re-reads the managed-agents store, proving the save
// survived the dialog lifecycle rather than living in local state. (The
// panel HEADER is not asserted — it renders the relay profile name, which
// the update path does not touch.)
await page.getByTestId("user-profile-edit-agent").click();
await expect(page.locator("#edit-agent-name")).toHaveValue(
"Tyler Agent Renamed",
{ timeout: 10_000 },
);
});

test("changes the model via custom entry and persists it", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: AGENT_PUBKEY,
name: AGENT_NAME,
status: "stopped",
channelNames: ["agents"],
},
],
});

await openEditDialog(page);

// Pick a provider so model discovery has a scope, then set a custom model.
await pickDropdownOption(page, "edit-agent-llm-provider", "Anthropic");
await pickDropdownOption(page, "edit-agent-model", "Custom model...");
await page.locator("#edit-agent-custom-model").fill("claude-opus-4-5");
// Anthropic requires a credential before save unlocks.
await page
.getByTestId("env-vars-required-value")
.fill("sk-test-edit-agent-e2e");

const submit = page.getByTestId("edit-agent-dialog-submit");
await expect(submit).toBeEnabled({ timeout: 10_000 });
await submit.click();
await expect(page.getByTestId("edit-agent-dialog")).not.toBeVisible();

await page.getByTestId("user-profile-edit-agent").click();
await expect(page.getByTestId("edit-agent-dialog")).toBeVisible({
timeout: 10_000,
});
// Custom model round-trips: the reopened dialog shows it in the custom
// input (the discovered-model lists don't contain it).
await expect(page.locator("#edit-agent-custom-model")).toHaveValue(
"claude-opus-4-5",
{ timeout: 10_000 },
);
});

test("profile Edit routes persona-linked agents to the definition editor", async ({
page,
}) => {
// Routing pin for handleEditAgent (UserProfilePanel): when the agent has
// a resolvable non-built-in persona, the Edit quick action opens the
// DEFINITION editor (persona dialog), not EditAgentDialog. The instance
// editor (and its inherit-runtime toggle) is reachable for persona-linked
// agents only via the requestOpenEditAgent event (ConfigNudgeCard) — no
// plain UI path — so its inherit-toggle behavior is covered by B3b's
// component-level pinning test, not e2e.
await installMockBridge(page, {
managedAgents: [
{
pubkey: AGENT_PUBKEY,
name: AGENT_NAME,
personaId: PERSONA_ID,
status: "stopped",
channelNames: ["agents"],
},
],
personas: [
{
id: PERSONA_ID,
displayName: "Edit E2E Persona",
systemPrompt: "You are the edit-agent e2e persona.",
},
],
});

await page.goto("/");
await page.getByTestId("open-agents-view").click();

// Persona-linked agents render grouped under the persona's card name.
const agentButton = page.getByRole("button", {
name: "Edit E2E Persona agent profile",
});
await expect(agentButton).toBeVisible({ timeout: 10_000 });
await agentButton.click();

await expect(page.getByTestId("user-profile-panel")).toBeVisible({
timeout: 10_000,
});
await page.getByTestId("user-profile-edit-agent").click();

// Definition editor opens; the instance editor does not.
await expect(page.getByTestId("persona-dialog")).toBeVisible({
timeout: 10_000,
});
await expect(page.getByTestId("edit-agent-dialog")).not.toBeVisible();
// And it is the persona's record that's being edited.
await expect(page.locator("#persona-display-name")).toHaveValue(
"Edit E2E Persona",
);
});
});
Loading