diff --git a/packages/launcher/e2e/respond.spec.ts b/packages/launcher/e2e/respond.spec.ts index a13ed03eb..f4dbebff0 100644 --- a/packages/launcher/e2e/respond.spec.ts +++ b/packages/launcher/e2e/respond.spec.ts @@ -1,5 +1,6 @@ -// Full keyed GUI flow: install → create instance → configure LLM → connect -// workspace → start → send a message → poll the workspace API for a real reply. +// Full keyed GUI flow: install → join the workspace on this device → create +// instance → configure LLM → bind to the workspace → start → send a message → +// poll the workspace API for a real reply. // // Gated so a cell without the needed credentials skips cleanly (not fails): // - needs E2E_WS_TOKEN / E2E_WS_SLUG (workspace) @@ -11,6 +12,8 @@ import { test, expect } from "./fixtures" import { agentBySlug } from "./agents" import { haveWorkspaceCreds, + createPairingCode, + WS_SLUG, sendMessage, baselineCursor, pollForReply, @@ -192,11 +195,54 @@ test.describe("launcher full flow", () => { }) } - // 2. Create an agent instance. The working directory is normally async- - // prefilled from listPaths(); fill it explicitly so Create never rejects - // on an empty path (the prefill can lose the race, esp. on Windows). + // 2. Join the workspace on this device, then create the agent instance. + // + // Joining is device-level: `connectWorkspace` refuses a workspace this + // device holds no pairing for, and the agent's Connect dialog offers only + // the ones it does. Every test gets a fresh HOME, so nothing is paired — + // redeem a code first, through the same Workspaces page a user would use. + // The code is minted at run time from the workspace token the reply + // assertion already needs, so no human has to stage one per run. + const pairingCode = await createPairingCode() + await page.getByTestId("nav-workspaces").click() + // Other pages can arrive here with the dialog already requested, so open it + // only when it isn't — its overlay would swallow the click that opens it. + const codeField = page.locator("#quick-connect-code") + if (!(await codeField.isVisible().catch(() => false))) + await page.getByTestId("workspace-join-open").click() + await codeField.fill(pairingCode) + await page.getByTestId("ws-pair-submit").click() + // Redeeming writes node.json; assert on that rather than on the card the + // page draws, so a rendering hiccup cannot read as a failed pairing. + await expect + .poll( + async () => + page.evaluate(async () => { + const status = await ( + window as unknown as { + api: { + getNodeStatus: () => Promise<{ + workspaces?: Array<{ workspaceSlug?: string }> + }> + } + } + ).api.getNodeStatus() + return (status.workspaces || []).map((w) => w.workspaceSlug) + }), + { timeout: 60_000, intervals: [2_000] }, + ) + .toContain(WS_SLUG) + + // Now the instance. The working directory is normally async-prefilled + // from listPaths(); fill it explicitly so Create never rejects on an + // empty path (the prefill can lose the race, esp. on Windows). + await page.getByTestId("nav-agents").click() await page.getByTestId("new-agent-open").click() - await page.locator("#agent-type").selectOption(SLUG) + // Agent type is a Radix Select, not a native - + {/* The e2e matrix picks the type by slug, so the handle is on + the trigger and on every option — the label is translated + and the value lives in Radix state, not in the DOM. */} + {supportedInstalled.map((c) => ( - + {c.label || c.name} ))} diff --git a/packages/launcher/src/renderer/pages/workspaces/index.tsx b/packages/launcher/src/renderer/pages/workspaces/index.tsx index 67cdd926c..d7f6fc5fc 100644 --- a/packages/launcher/src/renderer/pages/workspaces/index.tsx +++ b/packages/launcher/src/renderer/pages/workspaces/index.tsx @@ -160,7 +160,7 @@ export default function Workspaces({ showToast }: Props): React.JSX.Element { // Joins, never creates: the dialog takes a pairing code for a // workspace that already exists. A device can hold several at once, // so this stays available however many are listed. -