Files
kandev/apps/web/e2e/tests/task/editors-menu-worktree-picker.spec.ts
T
João Salavisa ea84b913b4 feat: choose worktree when opening IDE on multi-repo tasks (#1743)
* feat: choose worktree when opening IDE on multi-repo tasks

On multi-repo tasks the open-in-IDE button always opened the first
worktree. The editors menu now shows a worktree picker (repo + branch)
when a session has multiple worktrees, and each editor in the dropdown
expands into a per-worktree submenu.

- open-editor/open-folder accept an optional worktree_id; empty keeps
  the first-worktree fallback so existing clients are unaffected
- session DTOs now serialize the full worktrees list instead of only
  the flattened first-worktree fields
- useSessionWorktrees unions API worktrees with the WS-populated list
- new Playwright spec covers the picker on a real multi-repo session

* fix: address review feedback on worktree picker

- fall back on empty-string worktree fields, not only null/undefined
- bind optional open-folder body via EOF check instead of ContentLength
  so chunked requests keep their worktree_id
- wrap disabled editor buttons in a focusable span so tooltips work
- add hook test asserting worktreeId is forwarded as worktree_id

* fix: map editor service errors to HTTP status, drop e2e sleep

- open-editor/open-folder return 404/400/409 for known service errors
  (unknown worktree or editor, invalid config, unavailable editor)
  instead of a blanket 500
- replace the picker spec's 750ms sleep with an explicit composer click
  so the autofocus race is deterministic

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-07-16 22:43:06 +01:00

75 lines
2.8 KiB
TypeScript

import { execSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { test, expect } from "../../fixtures/test-base";
import { SessionPage } from "../../pages/session-page";
import { makeGitEnv } from "../../helpers/git-helper";
const DONE_STATES = ["COMPLETED", "WAITING_FOR_INPUT", "IDLE"];
/**
* Multi-repo tasks create one worktree per repository. The IDE button in the
* task top bar must offer a worktree picker instead of silently opening the
* first worktree (regression: it always opened Worktrees[0]).
*/
test.describe("Editors menu worktree picker", () => {
test("multi-repo task offers a worktree choice on the IDE button", async ({
testPage,
apiClient,
seedData,
backend,
}) => {
// Two disposable repos so the session materializes two worktrees.
const gitEnv = makeGitEnv(backend.tmpDir);
const repoIds: string[] = [];
for (const name of ["orders-api", "storefront-web"]) {
const repoDir = path.join(backend.tmpDir, "repos", name);
fs.mkdirSync(repoDir, { recursive: true });
execSync("git init -b main", { cwd: repoDir, env: gitEnv });
execSync('git commit --allow-empty -m "init"', { cwd: repoDir, env: gitEnv });
const repo = await apiClient.createRepository(seedData.workspaceId, repoDir, "main", {
name,
});
repoIds.push(repo.id);
}
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Sync order totals across services",
seedData.agentProfileId,
{
description: "/e2e:simple-message",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: repoIds,
executor_profile_id: seedData.worktreeExecutorProfileId,
},
);
await expect
.poll(
async () => {
const { sessions } = await apiClient.listTaskSessions(task.id);
return DONE_STATES.includes(sessions[0]?.state ?? "");
},
{ timeout: 60_000, message: "Waiting for the agent session to finish" },
)
.toBe(true);
await testPage.goto(`/t/${task.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
// Park focus on the idle chat composer before opening the menu so a late
// composer autofocus can't steal focus and dismiss the Radix dropdown.
await session.idleInput().click();
// The IDE button now opens a worktree picker instead of launching directly.
await testPage.getByTestId("editors-menu-open").click();
const items = testPage.getByTestId("editors-menu-worktree-item");
await expect(items).toHaveCount(2);
await expect(items.filter({ hasText: "orders-api" })).toBeVisible();
await expect(items.filter({ hasText: "storefront-web" })).toBeVisible();
});
});