From fe2ec82f32c2365427ed800a6c61addf72aef382 Mon Sep 17 00:00:00 2001 From: lixiang <1014027506@qq.com> Date: Tue, 8 Sep 2026 14:11:15 +0800 Subject: [PATCH] fix(e2e): drive the new device-pairing join flow in respond.spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The keyed matrix has reported connect 0/6 since the workspace join model changed. Four of the handles respond.spec drives no longer exist: - `#agent-type` — the Agent type picker is a Radix Select now, with the value in React state rather than in the DOM. - `ws-join-toggle` / `#workspace-url-or-token` / `ws-join` — the whole per-agent token form is gone. `ws-join-toggle` was also the spec's "configure succeeded" signal, so fixing only the type picker would have moved the failure a few lines without unblocking a single cell. 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 ever paired and the dialog can only ever show its empty state — the spec was missing an entire step, not a selector. So the spec now joins the workspace on the device first, through the Workspaces page a user would use, and binds the agent by picking that workspace. The pairing code is minted at run time from the workspace token the reply assertion already needs (a machine credential is trusted by POST /v1/workspaces/{slug}/pairing-codes), so no human has to stage a short-lived single-use code per run, and it is minted after install so it cannot expire waiting for one. Product side is five data-testid attributes and nothing else: the Agent type trigger and its options, the Connect dialog shell (so "never opened" and "opened with nothing to offer" stay separate failures), each workspace option, its empty state, and the pairing dialog's submit button. No behaviour, styling or i18n changes, so no version bump. The `// 2.` and `// 6.` markers the orchestrator reads to split the report into install / connect / respond keep their meaning: pairing sits inside the connect phase, where a failure belongs. --- packages/launcher/e2e/respond.spec.ts | 89 ++++++++++++++----- packages/launcher/e2e/workspace.ts | 28 ++++++ .../workspaces/WorkspaceQuickConnect.tsx | 1 + .../components/connect-workspace-dialog.tsx | 11 ++- .../agents/components/new-agent-dialog.tsx | 11 ++- .../src/renderer/pages/workspaces/index.tsx | 2 +- 6 files changed, 117 insertions(+), 25 deletions(-) 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. -