* fix(app): keep an open workspace menu from unmounting its own trigger The sidebar kebab is revealed by hover, and on a compact layout the menu it opens is a sheet that slides up over the row it came from. The row un-hovers, the trailing overlay unmounts the trigger, and the menu's open state goes with it — nothing is left that can close the surface already presented in the portal, so a full-screen backdrop swallows every click until reload. Lift the menu's open state to the component that decides whether to render the kebab, and keep the kebab rendered while the menu is up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(app): follow the sidebar and host badge redesigns in the e2e suite Three specs were still driving surfaces that moved out from under them, so CI has been red on main since the sidebar polish landed. Display preferences became one row per decision with the options a page down, so grouping and the host filter need that page walked first. Five specs had their own copy of the two-line dance; it lives in the sidebar helper now, and the host filter has its own opener next to the menu it belongs to. The host badge is plain secondary text on the meta line and its identity colour lives on the server icon alone, so the helpers stop asserting a tinted label and a pill fill that the redesign removed. Alignment rails are compared across canvas glyph metrics and SVG bounding boxes, so the residual depends on the host's font; a 1px and a 0.5px tolerance both landed on the wrong side of it in CI. Two pixels still catches the whole- pixel offsets these rails encode. The compact sheet spec also read its rails while the panel was still sliding in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(app): let a browser e2e run take a private Metro cache Metro's on-disk cache is shared by every checkout on the machine. Editing one app source file rebuilds that module against everyone else's cached output and the Unistyles styles can come back desynced, which surfaces as alignment specs failing with real-looking pixel numbers that go away when the edit is reverted. E2E_METRO_CACHE_VERSION namespaces a run's cache without deleting anyone else's entries, so the cache can be ruled out before a layout failure is believed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(app): stop the chat scroll helper from hanging on a stalled sampler `scrollChatAwayFromBottom` waited on a promise that could only ever resolve from a `requestAnimationFrame` sampler started by a `wheel` listener. rAF does not fire while the page is occluded, which is reachable on a CI runner, so the sampler could stall on its first frame and the wait never returned — the test died on its own 240s timeout with nothing to read. A missed wheel event did the same. Settling the scroll before measuring is an optimization; the assertion after it is what decides the test. Sample on a timer with a deadline so a stall falls through to that assertion instead of consuming the test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(app): remove layout geometry e2e coverage Rail and pixel-alignment assertions depend on host rendering details and should not define the browser E2E contract. Preserve the surrounding behavior coverage while dropping the Metro cache escape hatch that only supported diagnosing those assertions. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
1129 lines
38 KiB
TypeScript
1129 lines
38 KiB
TypeScript
import { existsSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
|
|
import { expect, test } from "../support/fixtures";
|
|
import { gotoAppShell } from "../support/helpers/app";
|
|
import {
|
|
archiveWorkspaceFromDaemon,
|
|
archiveLocalWorkspaceFromDaemon,
|
|
assertNewWorkspaceSidebarAndHeader,
|
|
closeBranchPicker,
|
|
connectNewWorkspaceDaemonClient,
|
|
createWorktreeViaDaemon,
|
|
delayBrowserAgentCreatedStatus,
|
|
expectComposerGithubAttachmentPill,
|
|
expectNewWorkspaceProjectSelected,
|
|
expectPickerClosed,
|
|
expectPickerOpen,
|
|
expectPickerSelected,
|
|
expectStartingRefPickerTriggerPr,
|
|
fillNewWorkspaceDraft,
|
|
openGlobalNewWorkspaceComposer,
|
|
openBranchPicker,
|
|
openNewWorkspaceComposer,
|
|
openProjectViaDaemon,
|
|
openStartingRefPicker,
|
|
pasteGithubPrUrl,
|
|
captureStartingRefPicker,
|
|
expectStartingRefRows,
|
|
startingRefRow,
|
|
submitNewWorkspaceEmpty,
|
|
searchAndSelectBranchInPicker,
|
|
selectBranchInPicker,
|
|
selectGitHubPrInPicker,
|
|
selectPickerOptionByKeyboard,
|
|
selectWorkspaceIsolation,
|
|
submitNewWorkspacePrompt,
|
|
} from "../support/helpers/new-workspace";
|
|
import {
|
|
commitLocalOnly,
|
|
createTempGitRepo,
|
|
readRepoRef,
|
|
readWorktreeBaseMetadata,
|
|
readWorktreeBranchInfo,
|
|
trackForkUpstream,
|
|
} from "../support/helpers/workspace";
|
|
import {
|
|
createLocalGithubPrFixture,
|
|
cloneGithubRepoDefaultBranchOnly,
|
|
createTempGithubRepo,
|
|
hasGithubAuth,
|
|
type LocalGhPrFixture,
|
|
} from "../support/helpers/github-fixtures";
|
|
import { getServerId } from "../support/helpers/server-id";
|
|
import { selectSidebarStatusGrouping } from "../support/helpers/sidebar";
|
|
import { getE2EDaemonPort } from "../support/helpers/daemon-port";
|
|
import { chooseAddProjectMethod, expectAddProjectPage } from "../support/helpers/add-project-flow";
|
|
import { seedSavedSettingsHosts } from "../support/helpers/settings";
|
|
import {
|
|
expectSidebarWorkspaceSelected,
|
|
expectWorkspaceHeader,
|
|
switchWorkspaceViaSidebar,
|
|
waitForSidebarHydration,
|
|
waitForWorkspaceInSidebar,
|
|
} from "../support/helpers/workspace-ui";
|
|
import { dropFileOnComposer, expectAttachmentPill } from "../support/helpers/composer";
|
|
|
|
const BACKGROUND_RESOLUTION_FILE = {
|
|
name: "background-context.json",
|
|
mimeType: "application/json",
|
|
buffer: Buffer.from(JSON.stringify({ composer: "background-resolution" })),
|
|
};
|
|
|
|
interface WorkspaceStatusGroupEvent {
|
|
rowTestId: string;
|
|
bucket: string;
|
|
indicatorTestId: string | null;
|
|
label: string;
|
|
at: number;
|
|
}
|
|
|
|
async function switchSidebarToStatusGrouping(page: import("@playwright/test").Page) {
|
|
await selectSidebarStatusGrouping(page);
|
|
await expect(page.getByTestId("sidebar-status-group-done")).toBeVisible({ timeout: 30_000 });
|
|
}
|
|
|
|
async function startTrackingSidebarStatusGroups(page: import("@playwright/test").Page) {
|
|
await page.evaluate(() => {
|
|
interface StatusGroupEvent {
|
|
rowTestId: string;
|
|
bucket: string;
|
|
indicatorTestId: string | null;
|
|
label: string;
|
|
at: number;
|
|
}
|
|
const win = window as typeof window & {
|
|
__workspaceStatusGroupEvents?: StatusGroupEvent[];
|
|
__workspaceStatusGroupObserver?: MutationObserver;
|
|
};
|
|
win.__workspaceStatusGroupEvents = [];
|
|
win.__workspaceStatusGroupObserver?.disconnect();
|
|
|
|
const capture = () => {
|
|
const events = win.__workspaceStatusGroupEvents;
|
|
if (!events) return;
|
|
const groups = document.querySelectorAll<HTMLElement>(
|
|
'[data-testid^="sidebar-status-group-"]',
|
|
);
|
|
for (const group of groups) {
|
|
const groupTestId = group.getAttribute("data-testid") ?? "";
|
|
const bucket = groupTestId.replace("sidebar-status-group-", "");
|
|
const label = group.textContent ?? "";
|
|
const block = group.parentElement?.parentElement;
|
|
if (!block) continue;
|
|
const rows = block.querySelectorAll<HTMLElement>('[data-testid^="sidebar-workspace-row-"]');
|
|
for (const row of rows) {
|
|
const rowTestId = row.getAttribute("data-testid");
|
|
if (!rowTestId) continue;
|
|
const indicatorTestId =
|
|
row
|
|
.querySelector<HTMLElement>('[data-testid^="workspace-status-indicator-"]')
|
|
?.getAttribute("data-testid") ?? null;
|
|
const last = events.at(-1);
|
|
if (
|
|
last?.rowTestId === rowTestId &&
|
|
last.bucket === bucket &&
|
|
last.indicatorTestId === indicatorTestId
|
|
) {
|
|
continue;
|
|
}
|
|
events.push({ rowTestId, bucket, indicatorTestId, label, at: performance.now() });
|
|
}
|
|
}
|
|
};
|
|
|
|
capture();
|
|
const observer = new MutationObserver(capture);
|
|
observer.observe(document.body, {
|
|
childList: true,
|
|
subtree: true,
|
|
characterData: true,
|
|
attributes: true,
|
|
attributeFilter: ["data-testid"],
|
|
});
|
|
win.__workspaceStatusGroupObserver = observer;
|
|
});
|
|
}
|
|
|
|
async function getTrackedSidebarStatusGroups(
|
|
page: import("@playwright/test").Page,
|
|
): Promise<WorkspaceStatusGroupEvent[]> {
|
|
return page.evaluate(() => {
|
|
const win = window as typeof window & {
|
|
__workspaceStatusGroupEvents?: WorkspaceStatusGroupEvent[];
|
|
};
|
|
return win.__workspaceStatusGroupEvents ?? [];
|
|
});
|
|
}
|
|
|
|
async function waitForWorkspaceStatusGroupEvent(input: {
|
|
page: import("@playwright/test").Page;
|
|
rowTestId: string;
|
|
bucket: string;
|
|
}) {
|
|
await input.page.waitForFunction(
|
|
({ expectedRowTestId, expectedBucket }) => {
|
|
const win = window as typeof window & {
|
|
__workspaceStatusGroupEvents?: WorkspaceStatusGroupEvent[];
|
|
};
|
|
for (const event of win.__workspaceStatusGroupEvents ?? []) {
|
|
if (event.rowTestId === expectedRowTestId && event.bucket === expectedBucket) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
{ expectedRowTestId: input.rowTestId, expectedBucket: input.bucket },
|
|
{ timeout: 30_000 },
|
|
);
|
|
}
|
|
|
|
async function expectWorkspaceStatusGroupEvents(input: {
|
|
page: import("@playwright/test").Page;
|
|
rowTestId: string;
|
|
includes: string;
|
|
excludes: string;
|
|
excludesIndicator?: string;
|
|
}) {
|
|
await waitForWorkspaceStatusGroupEvent({
|
|
page: input.page,
|
|
rowTestId: input.rowTestId,
|
|
bucket: input.includes,
|
|
});
|
|
const createdWorkspaceEvents = (await getTrackedSidebarStatusGroups(input.page)).filter(
|
|
(event) => event.rowTestId === input.rowTestId,
|
|
);
|
|
expect(createdWorkspaceEvents.map((event) => event.bucket)).toContain(input.includes);
|
|
expect(createdWorkspaceEvents.filter((event) => event.bucket === input.excludes)).toEqual([]);
|
|
if (input.excludesIndicator) {
|
|
expect(
|
|
createdWorkspaceEvents.filter((event) => event.indicatorTestId === input.excludesIndicator),
|
|
).toEqual([]);
|
|
}
|
|
}
|
|
|
|
async function submitNewWorkspaceWithoutPrompt(page: import("@playwright/test").Page) {
|
|
const createButton = page
|
|
.getByTestId("message-input-root")
|
|
.getByRole("button", { name: "Create" });
|
|
await expect(createButton).toBeVisible({ timeout: 30_000 });
|
|
await createButton.click();
|
|
}
|
|
|
|
test.describe("New workspace flow", () => {
|
|
let client: Awaited<ReturnType<typeof connectNewWorkspaceDaemonClient>>;
|
|
const localWorkspaceIds = new Set<string>();
|
|
const localProjectIds = new Set<string>();
|
|
const createdWorktreeDirectories = new Set<string>();
|
|
const localGithubFixtures = new Set<LocalGhPrFixture>();
|
|
|
|
test.describe.configure({ timeout: 240_000 });
|
|
|
|
test.beforeEach(async () => {
|
|
client = await connectNewWorkspaceDaemonClient();
|
|
});
|
|
|
|
test.afterEach(async () => {
|
|
if (client) {
|
|
for (const workspaceDirectory of createdWorktreeDirectories) {
|
|
await archiveWorkspaceFromDaemon(client, workspaceDirectory).catch(() => undefined);
|
|
}
|
|
for (const workspaceId of localWorkspaceIds) {
|
|
await archiveLocalWorkspaceFromDaemon(client, workspaceId).catch(() => undefined);
|
|
}
|
|
for (const projectId of localProjectIds) {
|
|
await client.removeProject(projectId).catch(() => undefined);
|
|
}
|
|
}
|
|
createdWorktreeDirectories.clear();
|
|
localWorkspaceIds.clear();
|
|
localProjectIds.clear();
|
|
await client?.close().catch(() => undefined);
|
|
});
|
|
|
|
test.afterAll(async () => {
|
|
for (const fixture of localGithubFixtures) {
|
|
await fixture.cleanup();
|
|
}
|
|
localGithubFixtures.clear();
|
|
});
|
|
|
|
test("adds a project from the selected empty host", async ({ page }) => {
|
|
const repo = await createTempGitRepo("new-workspace-project-picker-");
|
|
const primaryServerId = getServerId();
|
|
const emptyServerId = "empty-new-workspace-host";
|
|
|
|
try {
|
|
const openedProject = await openProjectViaDaemon(client, repo.path);
|
|
localWorkspaceIds.add(openedProject.workspaceId);
|
|
await seedSavedSettingsHosts(page, [
|
|
{
|
|
serverId: primaryServerId,
|
|
label: "Primary host",
|
|
endpoint: `127.0.0.1:${getE2EDaemonPort()}`,
|
|
},
|
|
{
|
|
serverId: emptyServerId,
|
|
label: "Empty host",
|
|
endpoint: "127.0.0.1:9",
|
|
},
|
|
]);
|
|
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
await openGlobalNewWorkspaceComposer(page);
|
|
|
|
const projectTrigger = page.getByTestId("new-workspace-project-picker-trigger");
|
|
await projectTrigger.click();
|
|
await page.getByPlaceholder("Search projects").fill("no matching project");
|
|
await expect(page.getByTestId("new-workspace-project-picker-add-project")).toBeVisible();
|
|
await page.keyboard.press("Escape");
|
|
|
|
await page.getByTestId("host-picker-trigger").click();
|
|
await page.getByTestId(`new-workspace-host-picker-option-${emptyServerId}`).click();
|
|
await expect(projectTrigger).toContainText("Choose project");
|
|
await projectTrigger.click();
|
|
|
|
const addProject = page.getByTestId("new-workspace-project-picker-add-project");
|
|
await expect(addProject).toContainText("Add project");
|
|
await expect(addProject).toContainText(/(?:⌘|Ctrl\+)O/);
|
|
await addProject.click();
|
|
|
|
await expectAddProjectPage(page, "method");
|
|
await chooseAddProjectMethod(page, "directory-search");
|
|
} finally {
|
|
await repo.cleanup();
|
|
}
|
|
});
|
|
|
|
test("sidebar workspace navigation updates URL and header", async ({ page }) => {
|
|
const serverId = getServerId();
|
|
|
|
const firstRepo = await createTempGitRepo("workspace-nav-a-");
|
|
const secondRepo = await createTempGitRepo("workspace-nav-b-");
|
|
|
|
try {
|
|
const firstWorkspace = await openProjectViaDaemon(client, firstRepo.path);
|
|
const secondWorkspace = await openProjectViaDaemon(client, secondRepo.path);
|
|
localWorkspaceIds.add(firstWorkspace.workspaceId);
|
|
localWorkspaceIds.add(secondWorkspace.workspaceId);
|
|
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
|
|
await switchWorkspaceViaSidebar({
|
|
page,
|
|
serverId,
|
|
workspaceId: firstWorkspace.workspaceId,
|
|
});
|
|
await expectWorkspaceHeader(page, {
|
|
title: firstWorkspace.workspaceName,
|
|
subtitle: firstWorkspace.projectDisplayName,
|
|
});
|
|
|
|
await switchWorkspaceViaSidebar({
|
|
page,
|
|
serverId,
|
|
workspaceId: secondWorkspace.workspaceId,
|
|
});
|
|
await waitForWorkspaceInSidebar(page, {
|
|
serverId,
|
|
workspaceId: secondWorkspace.workspaceId,
|
|
});
|
|
await expectWorkspaceHeader(page, {
|
|
title: secondWorkspace.workspaceName,
|
|
subtitle: secondWorkspace.projectDisplayName,
|
|
});
|
|
|
|
await switchWorkspaceViaSidebar({
|
|
page,
|
|
serverId,
|
|
workspaceId: firstWorkspace.workspaceId,
|
|
});
|
|
await expectWorkspaceHeader(page, {
|
|
title: firstWorkspace.workspaceName,
|
|
subtitle: firstWorkspace.projectDisplayName,
|
|
});
|
|
} finally {
|
|
await secondRepo.cleanup();
|
|
await firstRepo.cleanup();
|
|
}
|
|
});
|
|
|
|
test("same-project workspaces switch content without requiring refresh", async ({ page }) => {
|
|
const serverId = getServerId();
|
|
|
|
const repo = await createTempGitRepo("workspace-nav-same-project-");
|
|
|
|
try {
|
|
const rootWorkspace = await openProjectViaDaemon(client, repo.path);
|
|
const worktreeWorkspace = await createWorktreeViaDaemon(client, {
|
|
cwd: repo.path,
|
|
slug: `nav-${Date.now()}`,
|
|
});
|
|
localWorkspaceIds.add(rootWorkspace.workspaceId);
|
|
createdWorktreeDirectories.add(worktreeWorkspace.workspaceDirectory);
|
|
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
|
|
await switchWorkspaceViaSidebar({
|
|
page,
|
|
serverId,
|
|
workspaceId: rootWorkspace.workspaceId,
|
|
});
|
|
await expectWorkspaceHeader(page, {
|
|
title: rootWorkspace.workspaceName,
|
|
subtitle: rootWorkspace.projectDisplayName,
|
|
});
|
|
await expectSidebarWorkspaceSelected({
|
|
page,
|
|
serverId,
|
|
workspaceId: rootWorkspace.workspaceId,
|
|
});
|
|
|
|
await switchWorkspaceViaSidebar({
|
|
page,
|
|
serverId,
|
|
workspaceId: worktreeWorkspace.workspaceId,
|
|
});
|
|
await expectWorkspaceHeader(page, {
|
|
title: worktreeWorkspace.workspaceName,
|
|
subtitle: worktreeWorkspace.projectDisplayName,
|
|
});
|
|
await expectSidebarWorkspaceSelected({
|
|
page,
|
|
serverId,
|
|
workspaceId: worktreeWorkspace.workspaceId,
|
|
});
|
|
await expectSidebarWorkspaceSelected({
|
|
page,
|
|
serverId,
|
|
workspaceId: rootWorkspace.workspaceId,
|
|
selected: false,
|
|
});
|
|
|
|
await switchWorkspaceViaSidebar({
|
|
page,
|
|
serverId,
|
|
workspaceId: rootWorkspace.workspaceId,
|
|
});
|
|
await expectWorkspaceHeader(page, {
|
|
title: rootWorkspace.workspaceName,
|
|
subtitle: rootWorkspace.projectDisplayName,
|
|
});
|
|
await expectSidebarWorkspaceSelected({
|
|
page,
|
|
serverId,
|
|
workspaceId: rootWorkspace.workspaceId,
|
|
});
|
|
await expectSidebarWorkspaceSelected({
|
|
page,
|
|
serverId,
|
|
workspaceId: worktreeWorkspace.workspaceId,
|
|
selected: false,
|
|
});
|
|
} finally {
|
|
await repo.cleanup();
|
|
}
|
|
});
|
|
|
|
test("global new workspace uses the last active project and creates one agent tab", async ({
|
|
page,
|
|
}) => {
|
|
const serverId = getServerId();
|
|
|
|
const tempRepo = await createTempGitRepo("new-workspace-");
|
|
|
|
try {
|
|
const openedProject = await openProjectViaDaemon(client, tempRepo.path);
|
|
localWorkspaceIds.add(openedProject.workspaceId);
|
|
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
|
|
await switchWorkspaceViaSidebar({
|
|
page,
|
|
serverId,
|
|
workspaceId: openedProject.workspaceId,
|
|
});
|
|
await expectWorkspaceHeader(page, {
|
|
title: openedProject.workspaceName,
|
|
subtitle: openedProject.projectDisplayName,
|
|
});
|
|
|
|
await openGlobalNewWorkspaceComposer(page);
|
|
await expectNewWorkspaceProjectSelected(page, openedProject.projectDisplayName);
|
|
await submitNewWorkspacePrompt(page);
|
|
|
|
const createdWorkspace = await assertNewWorkspaceSidebarAndHeader(page, {
|
|
serverId,
|
|
client,
|
|
previousWorkspaceId: openedProject.workspaceId,
|
|
projectDisplayName: openedProject.projectDisplayName,
|
|
assertSidebarRow: false,
|
|
assertHeader: false,
|
|
});
|
|
createdWorktreeDirectories.add(createdWorkspace.workspaceDirectory);
|
|
|
|
expect(createdWorkspace.workspaceId).not.toBe(openedProject.workspaceId);
|
|
await expect(page).toHaveURL(
|
|
buildHostWorkspaceRoute(serverId, createdWorkspace.workspaceId),
|
|
{
|
|
timeout: 30_000,
|
|
},
|
|
);
|
|
|
|
const createdWorkspaceRow = page.getByTestId(
|
|
`sidebar-workspace-row-${serverId}:${createdWorkspace.workspaceId}`,
|
|
);
|
|
await expect(createdWorkspaceRow).toBeVisible({ timeout: 30_000 });
|
|
|
|
await expectWorkspaceHeader(page, {
|
|
title: createdWorkspace.workspaceName,
|
|
subtitle: openedProject.projectDisplayName,
|
|
});
|
|
|
|
const activeWorkspaceDeckEntry = page
|
|
.getByTestId(`workspace-deck-entry-${serverId}:${createdWorkspace.workspaceId}`)
|
|
.filter({ visible: true });
|
|
await expect(activeWorkspaceDeckEntry).toBeVisible({ timeout: 30_000 });
|
|
|
|
const agentTabs = activeWorkspaceDeckEntry.locator('[data-testid^="workspace-tab-agent_"]');
|
|
await expect(agentTabs).toHaveCount(1, { timeout: 30_000 });
|
|
|
|
// Workspace setup may auto-open a setup tab that steals focus,
|
|
// hiding the agent panel (display:none removes it from the
|
|
// accessibility tree). Click the agent tab to ensure it's active.
|
|
await agentTabs.first().click();
|
|
|
|
const composer = page.getByRole("textbox", { name: "Message agent..." });
|
|
await expect(composer).toBeVisible({ timeout: 30_000 });
|
|
} finally {
|
|
await tempRepo.cleanup();
|
|
}
|
|
});
|
|
|
|
test("redirects to the optimistic draft tab before agent creation resolves", async ({ page }) => {
|
|
const serverId = getServerId();
|
|
|
|
const tempRepo = await createTempGitRepo("new-workspace-optimistic-");
|
|
const agentCreatedDelay = await delayBrowserAgentCreatedStatus(page);
|
|
|
|
try {
|
|
const openedProject = await openProjectViaDaemon(client, tempRepo.path);
|
|
localWorkspaceIds.add(openedProject.workspaceId);
|
|
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
|
|
await switchWorkspaceViaSidebar({
|
|
page,
|
|
serverId,
|
|
workspaceId: openedProject.workspaceId,
|
|
});
|
|
await expectWorkspaceHeader(page, {
|
|
title: openedProject.workspaceName,
|
|
subtitle: openedProject.projectDisplayName,
|
|
});
|
|
|
|
await openNewWorkspaceComposer(page, {
|
|
projectKey: openedProject.projectKey,
|
|
projectDisplayName: openedProject.projectDisplayName,
|
|
});
|
|
|
|
const composer = page.getByRole("textbox", { name: "Message agent..." });
|
|
await expect(composer).toBeVisible({ timeout: 30_000 });
|
|
await composer.fill("Hello from e2e");
|
|
|
|
const createButton = page
|
|
.getByTestId("message-input-root")
|
|
.getByRole("button", { name: "Create" });
|
|
await expect(createButton).toBeVisible({ timeout: 30_000 });
|
|
await createButton.click();
|
|
|
|
await agentCreatedDelay.waitForCreateRequest();
|
|
await agentCreatedDelay.waitForDelayedCreatedStatus();
|
|
|
|
const createdWorkspace = await assertNewWorkspaceSidebarAndHeader(page, {
|
|
serverId,
|
|
client,
|
|
previousWorkspaceId: openedProject.workspaceId,
|
|
projectDisplayName: openedProject.projectDisplayName,
|
|
assertSidebarRow: false,
|
|
assertHeader: false,
|
|
});
|
|
createdWorktreeDirectories.add(createdWorkspace.workspaceDirectory);
|
|
|
|
await expect(page).toHaveURL(
|
|
buildHostWorkspaceRoute(serverId, createdWorkspace.workspaceId),
|
|
{
|
|
timeout: 30_000,
|
|
},
|
|
);
|
|
|
|
const activeWorkspaceDeckEntry = page
|
|
.getByTestId(`workspace-deck-entry-${serverId}:${createdWorkspace.workspaceId}`)
|
|
.filter({ visible: true });
|
|
await expect(activeWorkspaceDeckEntry).toBeVisible({ timeout: 30_000 });
|
|
|
|
const draftTabs = activeWorkspaceDeckEntry.locator('[data-testid^="workspace-tab-draft_"]');
|
|
await expect(draftTabs).toHaveCount(1, { timeout: 30_000 });
|
|
await expect(
|
|
activeWorkspaceDeckEntry.locator('[data-testid^="workspace-tab-agent_"]'),
|
|
).toHaveCount(0);
|
|
|
|
agentCreatedDelay.release();
|
|
await expect(
|
|
activeWorkspaceDeckEntry.locator('[data-testid^="workspace-tab-agent_"]'),
|
|
).toHaveCount(1, { timeout: 30_000 });
|
|
} finally {
|
|
agentCreatedDelay.release();
|
|
await tempRepo.cleanup();
|
|
}
|
|
});
|
|
|
|
test("new workspace with initial agent never appears in the Done status group", async ({
|
|
page,
|
|
}) => {
|
|
const serverId = getServerId();
|
|
|
|
const tempRepo = await createTempGitRepo("new-workspace-status-optimistic-");
|
|
|
|
try {
|
|
const openedProject = await openProjectViaDaemon(client, tempRepo.path);
|
|
localWorkspaceIds.add(openedProject.workspaceId);
|
|
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
|
|
await switchWorkspaceViaSidebar({
|
|
page,
|
|
serverId,
|
|
workspaceId: openedProject.workspaceId,
|
|
});
|
|
await expectWorkspaceHeader(page, {
|
|
title: openedProject.workspaceName,
|
|
subtitle: openedProject.projectDisplayName,
|
|
});
|
|
|
|
await switchSidebarToStatusGrouping(page);
|
|
await startTrackingSidebarStatusGroups(page);
|
|
|
|
await openGlobalNewWorkspaceComposer(page);
|
|
await expectNewWorkspaceProjectSelected(page, openedProject.projectDisplayName);
|
|
await submitNewWorkspacePrompt(page);
|
|
|
|
const createdWorkspace = await assertNewWorkspaceSidebarAndHeader(page, {
|
|
serverId,
|
|
client,
|
|
previousWorkspaceId: openedProject.workspaceId,
|
|
projectDisplayName: openedProject.projectDisplayName,
|
|
assertSidebarRow: false,
|
|
assertHeader: false,
|
|
});
|
|
createdWorktreeDirectories.add(createdWorkspace.workspaceDirectory);
|
|
|
|
const rowTestId = `sidebar-workspace-row-${serverId}:${createdWorkspace.workspaceId}`;
|
|
await expectWorkspaceStatusGroupEvents({
|
|
page,
|
|
rowTestId,
|
|
includes: "running",
|
|
excludes: "done",
|
|
});
|
|
} finally {
|
|
await tempRepo.cleanup();
|
|
}
|
|
});
|
|
|
|
test("new workspace without an initial agent appears in the Done status group", async ({
|
|
page,
|
|
}) => {
|
|
const serverId = getServerId();
|
|
|
|
const tempRepo = await createTempGitRepo("new-workspace-status-empty-");
|
|
|
|
try {
|
|
const openedProject = await openProjectViaDaemon(client, tempRepo.path);
|
|
localWorkspaceIds.add(openedProject.workspaceId);
|
|
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
|
|
await switchWorkspaceViaSidebar({
|
|
page,
|
|
serverId,
|
|
workspaceId: openedProject.workspaceId,
|
|
});
|
|
await expectWorkspaceHeader(page, {
|
|
title: openedProject.workspaceName,
|
|
subtitle: openedProject.projectDisplayName,
|
|
});
|
|
|
|
await switchSidebarToStatusGrouping(page);
|
|
await startTrackingSidebarStatusGroups(page);
|
|
|
|
await openGlobalNewWorkspaceComposer(page);
|
|
await expectNewWorkspaceProjectSelected(page, openedProject.projectDisplayName);
|
|
await submitNewWorkspaceWithoutPrompt(page);
|
|
|
|
const createdWorkspace = await assertNewWorkspaceSidebarAndHeader(page, {
|
|
serverId,
|
|
client,
|
|
previousWorkspaceId: openedProject.workspaceId,
|
|
projectDisplayName: openedProject.projectDisplayName,
|
|
});
|
|
createdWorktreeDirectories.add(createdWorkspace.workspaceDirectory);
|
|
|
|
const rowTestId = `sidebar-workspace-row-${serverId}:${createdWorkspace.workspaceId}`;
|
|
await expectWorkspaceStatusGroupEvents({
|
|
page,
|
|
rowTestId,
|
|
includes: "done",
|
|
excludes: "running",
|
|
excludesIndicator: "workspace-status-indicator-loading",
|
|
});
|
|
await expectWorkspaceStatusGroupEvents({
|
|
page,
|
|
rowTestId,
|
|
includes: "done",
|
|
excludes: "running",
|
|
excludesIndicator: "workspace-status-indicator-running",
|
|
});
|
|
} finally {
|
|
await tempRepo.cleanup();
|
|
}
|
|
});
|
|
|
|
test("selected branch becomes the base of a new workspace worktree", async ({ page }) => {
|
|
const serverId = getServerId();
|
|
|
|
const tempRepo = await createTempGitRepo("new-workspace-ref-", {
|
|
branches: ["main", "dev"],
|
|
});
|
|
|
|
try {
|
|
const openedProject = await openProjectViaDaemon(client, tempRepo.path);
|
|
localWorkspaceIds.add(openedProject.workspaceId);
|
|
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
|
|
await switchWorkspaceViaSidebar({
|
|
page,
|
|
serverId,
|
|
workspaceId: openedProject.workspaceId,
|
|
});
|
|
await expectWorkspaceHeader(page, {
|
|
title: openedProject.workspaceName,
|
|
subtitle: openedProject.projectDisplayName,
|
|
});
|
|
|
|
await openNewWorkspaceComposer(page, {
|
|
projectKey: openedProject.projectKey,
|
|
projectDisplayName: openedProject.projectDisplayName,
|
|
});
|
|
await selectWorkspaceIsolation(page, "worktree");
|
|
await openStartingRefPicker(page);
|
|
await selectBranchInPicker(page, "dev");
|
|
|
|
const createButton = page
|
|
.getByTestId("message-input-root")
|
|
.getByRole("button", { name: "Create" });
|
|
await expect(createButton).toBeVisible({ timeout: 30_000 });
|
|
await createButton.click();
|
|
|
|
const createdWorkspace = await assertNewWorkspaceSidebarAndHeader(page, {
|
|
serverId,
|
|
client,
|
|
previousWorkspaceId: openedProject.workspaceId,
|
|
projectDisplayName: openedProject.projectDisplayName,
|
|
});
|
|
createdWorktreeDirectories.add(createdWorkspace.workspaceDirectory);
|
|
|
|
expect(existsSync(createdWorkspace.workspaceDirectory)).toBe(true);
|
|
|
|
const branchInfo = await readWorktreeBranchInfo({
|
|
worktreePath: createdWorkspace.workspaceDirectory,
|
|
});
|
|
expect(branchInfo.currentBranch).toBe(path.basename(createdWorkspace.workspaceDirectory));
|
|
expect(branchInfo.hasAncestor(tempRepo.branchHeads.main)).toBe(true);
|
|
expect(branchInfo.hasAncestor(tempRepo.branchHeads.dev)).toBe(true);
|
|
} finally {
|
|
await tempRepo.cleanup();
|
|
}
|
|
});
|
|
|
|
// The starting ref the daemon actually cuts from is the thing that broke: the picker said
|
|
// one ref and the worktree was created from another. Every assertion here reads the
|
|
// created worktree's commits or its recorded base, never the trigger text alone.
|
|
test.describe("default starting ref", () => {
|
|
async function openWorktreeComposerForRepo(
|
|
page: import("@playwright/test").Page,
|
|
repoPath: string,
|
|
) {
|
|
const openedProject = await openProjectViaDaemon(client, repoPath);
|
|
localWorkspaceIds.add(openedProject.workspaceId);
|
|
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
await openNewWorkspaceComposer(page, {
|
|
projectKey: openedProject.projectKey,
|
|
projectDisplayName: openedProject.projectDisplayName,
|
|
});
|
|
await selectWorkspaceIsolation(page, "worktree");
|
|
return openedProject;
|
|
}
|
|
|
|
async function createWorktreeAndRead(
|
|
page: import("@playwright/test").Page,
|
|
openedProject: Awaited<ReturnType<typeof openProjectViaDaemon>>,
|
|
) {
|
|
await submitNewWorkspaceEmpty(page);
|
|
const createdWorkspace = await assertNewWorkspaceSidebarAndHeader(page, {
|
|
serverId: getServerId(),
|
|
client,
|
|
previousWorkspaceId: openedProject.workspaceId,
|
|
projectDisplayName: openedProject.projectDisplayName,
|
|
});
|
|
createdWorktreeDirectories.add(createdWorkspace.workspaceDirectory);
|
|
return {
|
|
...createdWorkspace,
|
|
branchInfo: await readWorktreeBranchInfo({
|
|
worktreePath: createdWorkspace.workspaceDirectory,
|
|
}),
|
|
};
|
|
}
|
|
|
|
test("branches off the upstream when the local branch is ahead and the picker is untouched", async ({
|
|
page,
|
|
}) => {
|
|
const tempRepo = await createTempGitRepo("ref-default-ahead-", { withRemote: true });
|
|
|
|
try {
|
|
const originHead = readRepoRef(tempRepo.path, "refs/remotes/origin/main");
|
|
commitLocalOnly(tempRepo.path, "one");
|
|
const localHead = commitLocalOnly(tempRepo.path, "two");
|
|
|
|
const openedProject = await openWorktreeComposerForRepo(page, tempRepo.path);
|
|
const created = await createWorktreeAndRead(page, openedProject);
|
|
|
|
expect(created.branchInfo.hasAncestor(originHead)).toBe(true);
|
|
expect(created.branchInfo.hasAncestor(localHead)).toBe(false);
|
|
} finally {
|
|
await tempRepo.cleanup();
|
|
}
|
|
});
|
|
|
|
test("branches off the local ref when the local row is chosen explicitly", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
const tempRepo = await createTempGitRepo("ref-default-local-pick-", { withRemote: true });
|
|
|
|
try {
|
|
commitLocalOnly(tempRepo.path, "one");
|
|
const localHead = commitLocalOnly(tempRepo.path, "two");
|
|
|
|
const openedProject = await openWorktreeComposerForRepo(page, tempRepo.path);
|
|
|
|
await openStartingRefPicker(page);
|
|
await expectStartingRefRows(page, [
|
|
"main, origin branch",
|
|
"main, local branch, 2 commits ahead of origin main",
|
|
]);
|
|
const screenshotPath = testInfo.outputPath("ref-picker-local-ahead.png");
|
|
await captureStartingRefPicker(page, screenshotPath);
|
|
await testInfo.attach("Ref picker: local ahead of upstream", {
|
|
path: screenshotPath,
|
|
contentType: "image/png",
|
|
});
|
|
await startingRefRow(page, "main, local branch, 2 commits ahead of origin main").click();
|
|
await expectPickerSelected(page, "main (local)");
|
|
|
|
const created = await createWorktreeAndRead(page, openedProject);
|
|
expect(created.branchInfo.hasAncestor(localHead)).toBe(true);
|
|
} finally {
|
|
await tempRepo.cleanup();
|
|
}
|
|
});
|
|
|
|
test("branches off a fork's upstream remote and records the branch name", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
const tempRepo = await createTempGitRepo("ref-default-fork-", { withRemote: true });
|
|
|
|
try {
|
|
const upstreamHead = await trackForkUpstream(tempRepo.path);
|
|
const originHead = readRepoRef(tempRepo.path, "refs/remotes/origin/main");
|
|
|
|
const openedProject = await openWorktreeComposerForRepo(page, tempRepo.path);
|
|
|
|
await openStartingRefPicker(page);
|
|
// Branch suggestions only know about origin, so the upstream the fork actually
|
|
// tracks gets its own row rather than silently sharing origin's. Two rows reading
|
|
// "main" is the ambiguity this whole change exists to remove.
|
|
await expectStartingRefRows(page, [
|
|
"main (upstream), upstream branch",
|
|
"main, origin branch",
|
|
]);
|
|
await expectPickerSelected(page, "main (upstream)");
|
|
const screenshotPath = testInfo.outputPath("ref-picker-fork.png");
|
|
await captureStartingRefPicker(page, screenshotPath);
|
|
await testInfo.attach("Ref picker: fork tracking upstream/main", {
|
|
path: screenshotPath,
|
|
contentType: "image/png",
|
|
});
|
|
await closeBranchPicker(page);
|
|
|
|
const created = await createWorktreeAndRead(page, openedProject);
|
|
|
|
expect(created.branchInfo.hasAncestor(upstreamHead)).toBe(true);
|
|
expect(upstreamHead).not.toBe(originHead);
|
|
// The name is what the UI shows; the ref is what resolves back to this commit.
|
|
expect(await readWorktreeBaseMetadata(created.workspaceDirectory)).toEqual({
|
|
baseRefName: "main",
|
|
baseRef: "refs/remotes/upstream/main",
|
|
});
|
|
} finally {
|
|
await tempRepo.cleanup();
|
|
}
|
|
});
|
|
});
|
|
|
|
test("branch picker opens via keyboard and selects the filtered option on Enter", async ({
|
|
page,
|
|
}) => {
|
|
const tempRepo = await createTempGitRepo("picker-keyboard-", { branches: ["main", "dev"] });
|
|
|
|
try {
|
|
const openedProject = await openProjectViaDaemon(client, tempRepo.path);
|
|
localWorkspaceIds.add(openedProject.workspaceId);
|
|
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
await openNewWorkspaceComposer(page, {
|
|
projectKey: openedProject.projectKey,
|
|
projectDisplayName: openedProject.projectDisplayName,
|
|
});
|
|
await selectWorkspaceIsolation(page, "worktree");
|
|
|
|
await openBranchPicker(page);
|
|
await expectPickerOpen(page);
|
|
await selectPickerOptionByKeyboard(page, "dev");
|
|
await expectPickerSelected(page, "dev");
|
|
await expectPickerClosed(page);
|
|
} finally {
|
|
await tempRepo.cleanup();
|
|
}
|
|
});
|
|
|
|
test("branch picker closes on Escape without selecting an option", async ({ page }) => {
|
|
const tempRepo = await createTempGitRepo("picker-escape-");
|
|
|
|
try {
|
|
const openedProject = await openProjectViaDaemon(client, tempRepo.path);
|
|
localWorkspaceIds.add(openedProject.workspaceId);
|
|
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
await openNewWorkspaceComposer(page, {
|
|
projectKey: openedProject.projectKey,
|
|
projectDisplayName: openedProject.projectDisplayName,
|
|
});
|
|
await selectWorkspaceIsolation(page, "worktree");
|
|
|
|
await openBranchPicker(page);
|
|
await expectPickerOpen(page);
|
|
await closeBranchPicker(page);
|
|
await expectPickerClosed(page);
|
|
} finally {
|
|
await tempRepo.cleanup();
|
|
}
|
|
});
|
|
|
|
test("selected GitHub PR shows PR context in the trigger and composer", async ({ page }) => {
|
|
test.skip(!hasGithubAuth(), "Requires GitHub authentication (gh auth login)");
|
|
|
|
const ghRepo = await createTempGithubRepo({
|
|
category: "new-workspace-pr-ref",
|
|
prs: [{ title: "Review selected start ref", state: "open" }],
|
|
});
|
|
const pr = ghRepo.prs[0]!;
|
|
|
|
try {
|
|
const openedProject = await openProjectViaDaemon(client, pr.localPath);
|
|
localWorkspaceIds.add(openedProject.workspaceId);
|
|
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
await openNewWorkspaceComposer(page, {
|
|
projectKey: openedProject.projectKey,
|
|
projectDisplayName: openedProject.projectDisplayName,
|
|
});
|
|
await selectWorkspaceIsolation(page, "worktree");
|
|
await openStartingRefPicker(page);
|
|
await selectGitHubPrInPicker(page, pr.number);
|
|
|
|
await expectStartingRefPickerTriggerPr(page, {
|
|
number: pr.number,
|
|
title: pr.title,
|
|
headRef: pr.branch,
|
|
});
|
|
await expectComposerGithubAttachmentPill(page, {
|
|
number: pr.number,
|
|
title: pr.title,
|
|
});
|
|
} finally {
|
|
await ghRepo.cleanup();
|
|
}
|
|
});
|
|
|
|
test("pasted GitHub PR replaces a selected branch and creates its worktree", async ({
|
|
page,
|
|
context,
|
|
}) => {
|
|
const fixture = await createLocalGithubPrFixture();
|
|
localGithubFixtures.add(fixture);
|
|
const { pr, mainCheckout } = fixture;
|
|
|
|
const openedProject = await openProjectViaDaemon(client, mainCheckout.path);
|
|
localWorkspaceIds.add(openedProject.workspaceId);
|
|
localProjectIds.add(openedProject.projectId);
|
|
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
await openNewWorkspaceComposer(page, {
|
|
projectKey: openedProject.projectKey,
|
|
projectDisplayName: openedProject.projectDisplayName,
|
|
});
|
|
await selectWorkspaceIsolation(page, "worktree");
|
|
await openStartingRefPicker(page);
|
|
await selectBranchInPicker(page, "main");
|
|
|
|
await pasteGithubPrUrl(page, context, pr.url);
|
|
|
|
const createButton = page.getByTestId("workspace-create-submit");
|
|
await expect(createButton).toBeDisabled();
|
|
await expect(createButton.getByRole("progressbar")).toHaveCount(0);
|
|
|
|
await dropFileOnComposer(page, BACKGROUND_RESOLUTION_FILE);
|
|
await expectAttachmentPill(page, "composer-file-attachment-pill");
|
|
|
|
await expectComposerGithubAttachmentPill(page, {
|
|
number: pr.number,
|
|
title: pr.title,
|
|
});
|
|
await expectStartingRefPickerTriggerPr(page, {
|
|
number: pr.number,
|
|
title: pr.title,
|
|
headRef: pr.branch,
|
|
});
|
|
|
|
await submitNewWorkspaceWithoutPrompt(page);
|
|
|
|
const worktree = await assertNewWorkspaceSidebarAndHeader(page, {
|
|
serverId: getServerId(),
|
|
client,
|
|
previousWorkspaceId: openedProject.workspaceId,
|
|
projectDisplayName: openedProject.projectDisplayName,
|
|
});
|
|
createdWorktreeDirectories.add(worktree.workspaceDirectory);
|
|
|
|
const branchInfo = await readWorktreeBranchInfo({
|
|
worktreePath: worktree.workspaceDirectory,
|
|
});
|
|
expect(branchInfo.currentBranch).toBe(pr.branch);
|
|
expect(existsSync(path.join(worktree.workspaceDirectory, "pr-1.txt"))).toBe(true);
|
|
});
|
|
|
|
test("branches remain searchable after a pasted PR and determine the created worktree", async ({
|
|
page,
|
|
context,
|
|
}) => {
|
|
const fixture = await createLocalGithubPrFixture();
|
|
localGithubFixtures.add(fixture);
|
|
const { pr, mainCheckout } = fixture;
|
|
|
|
const openedProject = await openProjectViaDaemon(client, mainCheckout.path);
|
|
localWorkspaceIds.add(openedProject.workspaceId);
|
|
localProjectIds.add(openedProject.projectId);
|
|
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
await openNewWorkspaceComposer(page, {
|
|
projectKey: openedProject.projectKey,
|
|
projectDisplayName: openedProject.projectDisplayName,
|
|
});
|
|
await selectWorkspaceIsolation(page, "worktree");
|
|
await pasteGithubPrUrl(page, context, pr.url);
|
|
await expectStartingRefPickerTriggerPr(page, {
|
|
number: pr.number,
|
|
title: pr.title,
|
|
headRef: pr.branch,
|
|
});
|
|
|
|
await openStartingRefPicker(page);
|
|
await searchAndSelectBranchInPicker(page, "main");
|
|
await expectPickerSelected(page, "main");
|
|
await fillNewWorkspaceDraft(page, `${pr.url}\nKeep this checkout on main`);
|
|
await expectPickerSelected(page, "main");
|
|
await submitNewWorkspaceWithoutPrompt(page);
|
|
|
|
const worktree = await assertNewWorkspaceSidebarAndHeader(page, {
|
|
serverId: getServerId(),
|
|
client,
|
|
previousWorkspaceId: openedProject.workspaceId,
|
|
projectDisplayName: openedProject.projectDisplayName,
|
|
});
|
|
createdWorktreeDirectories.add(worktree.workspaceDirectory);
|
|
|
|
expect(existsSync(path.join(worktree.workspaceDirectory, "pr-1.txt"))).toBe(false);
|
|
});
|
|
|
|
test("selected GitHub PR creates the worktree from the PR head even when the head branch is not fetched", async ({
|
|
page,
|
|
}) => {
|
|
test.skip(!hasGithubAuth(), "Requires GitHub authentication (gh auth login)");
|
|
|
|
const ghRepo = await createTempGithubRepo({
|
|
category: "new-workspace-pr-worktree",
|
|
prs: [{ title: "Checkout PR worktree", state: "open" }],
|
|
});
|
|
const pr = ghRepo.prs[0]!;
|
|
const mainCheckout = await cloneGithubRepoDefaultBranchOnly(ghRepo);
|
|
|
|
try {
|
|
const openedProject = await openProjectViaDaemon(client, mainCheckout.path);
|
|
localWorkspaceIds.add(openedProject.workspaceId);
|
|
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
await openNewWorkspaceComposer(page, {
|
|
projectKey: openedProject.projectKey,
|
|
projectDisplayName: openedProject.projectDisplayName,
|
|
});
|
|
await selectWorkspaceIsolation(page, "worktree");
|
|
await openStartingRefPicker(page);
|
|
await selectGitHubPrInPicker(page, pr.number);
|
|
await submitNewWorkspaceWithoutPrompt(page);
|
|
|
|
const worktree = await assertNewWorkspaceSidebarAndHeader(page, {
|
|
serverId: getServerId(),
|
|
client,
|
|
previousWorkspaceId: openedProject.workspaceId,
|
|
projectDisplayName: openedProject.projectDisplayName,
|
|
});
|
|
createdWorktreeDirectories.add(worktree.workspaceDirectory);
|
|
|
|
const branchInfo = await readWorktreeBranchInfo({
|
|
worktreePath: worktree.workspaceDirectory,
|
|
});
|
|
expect(branchInfo.currentBranch).toBe(pr.branch);
|
|
expect(existsSync(path.join(worktree.workspaceDirectory, "pr-1.txt"))).toBe(true);
|
|
} finally {
|
|
await mainCheckout.cleanup();
|
|
await ghRepo.cleanup();
|
|
}
|
|
});
|
|
});
|