From a2b76f77f37776ea1adbc9328be016a2d21cf1b7 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 2 Nov 2026 06:09:27 +0100 Subject: [PATCH] test(plugins): lock draft option ownership without a subscription, name the overflow-sheet steps The draft option test subscribed the options model to the form, which in the app happens in a render effect, so it passed without the model observing the form's provider itself. A case with no subscription now fails when that observation is removed. The overflow-sheet spec reads as steps through a helper. --- .../e2e/browser/plugin-composer-items.spec.ts | 47 +++------------ .../support/helpers/plugin-composer-items.ts | 59 +++++++++++++++++++ .../src/composer/draft/composer-agent.test.ts | 19 ++++++ 3 files changed, 85 insertions(+), 40 deletions(-) create mode 100644 packages/app/e2e/support/helpers/plugin-composer-items.ts diff --git a/packages/app/e2e/browser/plugin-composer-items.spec.ts b/packages/app/e2e/browser/plugin-composer-items.spec.ts index bccdaed4b5..19218c55d4 100644 --- a/packages/app/e2e/browser/plugin-composer-items.spec.ts +++ b/packages/app/e2e/browser/plugin-composer-items.spec.ts @@ -1,45 +1,12 @@ -import { expect, test } from "../support/fixtures"; -import { gotoAppShell } from "../support/helpers/app"; -import { buildAgentRoute } from "../support/helpers/mock-agent"; -import { connectNewWorkspaceDaemonClient } from "../support/helpers/new-workspace"; -import { copyPluginExample } from "../support/helpers/plugin-fixture"; -import { seedWorkspace } from "../support/helpers/seed-client"; - -const NARROW = { width: 500, height: 800 }; +import { test } from "../support/fixtures"; +import { withComposerControlsExample } from "../support/helpers/plugin-composer-items"; test("a narrow composer's overflow sheet shows a plugin item's row", async ({ page }) => { test.setTimeout(120_000); - const workspace = await seedWorkspace({ - repoPrefix: "composer-items-", - title: "Composer items", + await withComposerControlsExample(page, async (composer) => { + await composer.narrowTheWindow(); + await composer.openAgentFeatures(); + await composer.expectFeatureRow("Needs review"); + await composer.expectFeatureRow("Insert checklist"); }); - const agent = await workspace.client.createAgent({ - provider: "mock", - cwd: workspace.repoPath, - workspaceId: workspace.workspaceId, - title: "Composer items agent", - model: "ten-second-stream", - modeId: "load-test", - }); - const client = await connectNewWorkspaceDaemonClient({ ownProjects: false }); - const previous = await client.getDaemonConfig(); - const example = await copyPluginExample("composer-controls"); - try { - await client.patchDaemonConfig({ pluginsEnabled: true }); - await client.installDirectoryPlugin(example.directory); - await page.setViewportSize(NARROW); - await gotoAppShell(page); - await page.goto(buildAgentRoute(workspace.workspaceId, agent.id)); - - await page.getByRole("button", { name: "Open agent features", exact: true }).click(); - - await expect(page.getByRole("button", { name: "Needs review", exact: true })).toBeVisible(); - await expect(page.getByRole("button", { name: "Insert checklist", exact: true })).toBeVisible(); - } finally { - await client.removePlugin("composer-controls-example"); - await client.patchDaemonConfig({ pluginsEnabled: previous.config.pluginsEnabled }); - await client.close(); - await example.cleanup(); - await workspace.cleanup(); - } }); diff --git a/packages/app/e2e/support/helpers/plugin-composer-items.ts b/packages/app/e2e/support/helpers/plugin-composer-items.ts new file mode 100644 index 0000000000..bac676ab38 --- /dev/null +++ b/packages/app/e2e/support/helpers/plugin-composer-items.ts @@ -0,0 +1,59 @@ +import { expect, test, type Page } from "@playwright/test"; +import { gotoAppShell } from "./app"; +import { openAgentRoute } from "./mock-agent"; +import { connectNewWorkspaceDaemonClient } from "./new-workspace"; +import { copyPluginExample } from "./plugin-fixture"; +import { seedWorkspace } from "./seed-client"; + +const PLUGIN_ID = "composer-controls-example"; +const NARROW = { width: 500, height: 800 }; + +/** Runs `run` in an agent's composer with the composer-controls example installed. */ +export async function withComposerControlsExample( + page: Page, + run: (composer: { + narrowTheWindow(): Promise; + openAgentFeatures(): Promise; + expectFeatureRow(name: string): Promise; + }) => Promise, +) { + const workspace = await seedWorkspace({ + repoPrefix: "composer-items-", + title: "Composer items", + }); + const agent = await workspace.client.createAgent({ + provider: "mock", + cwd: workspace.repoPath, + workspaceId: workspace.workspaceId, + title: "Composer items agent", + model: "ten-second-stream", + modeId: "load-test", + }); + const client = await connectNewWorkspaceDaemonClient({ ownProjects: false }); + const previous = await client.getDaemonConfig(); + const example = await copyPluginExample("composer-controls"); + try { + await client.patchDaemonConfig({ pluginsEnabled: true }); + await client.installDirectoryPlugin(example.directory); + await gotoAppShell(page); + await openAgentRoute(page, { workspaceId: workspace.workspaceId, agentId: agent.id }); + await run({ + narrowTheWindow: () => + test.step("narrow the window until the composer folds its controls away", () => + page.setViewportSize(NARROW)), + openAgentFeatures: () => + test.step("open the agent features sheet", async () => { + await page.getByRole("button", { name: "Open agent features", exact: true }).click(); + }), + expectFeatureRow: (name) => + test.step(`the sheet has a row for "${name}"`, () => + expect(page.getByRole("button", { name, exact: true })).toBeVisible()), + }); + } finally { + await client.removePlugin(PLUGIN_ID); + await client.patchDaemonConfig({ pluginsEnabled: previous.config.pluginsEnabled }); + await client.close(); + await example.cleanup(); + await workspace.cleanup(); + } +} diff --git a/packages/app/src/composer/draft/composer-agent.test.ts b/packages/app/src/composer/draft/composer-agent.test.ts index 86c7f3f031..e09b71d92f 100644 --- a/packages/app/src/composer/draft/composer-agent.test.ts +++ b/packages/app/src/composer/draft/composer-agent.test.ts @@ -85,6 +85,25 @@ function openDraft(context: "draft" | "new-workspace") { return { form, agent, request }; } +describe("a draft's options", () => { + it("belong to the provider the form has now, before the screen has rendered the change", () => { + const form = openAgentForm(() => catalog); + const options = openDraftOptions({ + provider: null, + currentProvider: () => form.getState().form.provider, + }); + // No subscription: the screen observes the provider in an effect, after the command ran. + form.setProviderAndModel("claude", ""); + options.observeProvider("claude"); + options.setOption("context", "1m"); + + form.setProviderAndModel("codex", ""); + options.setOption("effort", "high"); + + expect(options.getState()).toMatchObject({ provider: "codex", edits: { effort: "high" } }); + }); +}); + describe.each(["draft", "new-workspace"] as const)("the %s composer's agent", (context) => { it("applies each awaited command before the next one runs", async () => { const draft = openDraft(context);