Start new workspaces from pasted pull requests (#2290)
* fix(workspaces): start pasted pull requests from their branch Recognized pull request links now select their branch directly instead of waiting for a separate confirmation. Ensure single-branch clones also record the fetched branch so the new worktree can track it. * fix(workspaces): keep pasted PR checkout target-safe * fix(workspaces): make pasted PR resolution deterministic * fix(workspaces): preserve explicit checkout selection A recognized pull request selects itself only when its attachment is added. Once the user chooses a branch, later composer edits must not derive checkout state from existing attachments. * fix(composer): ignore stale pull request lookups A lookup may finish after the workspace target changes. Only apply its attachment and selection event when the host, working directory, client, and remote still match the target that started it. * fix(workspaces): make checkout choices authoritative Arm automatic pull request selection only when a new PR link is detected. An explicit picker choice cancels that pending selection, existing draft attachments never infer checkout state, and target changes suppress carried-over lookups. * fix(workspaces): stabilize pull request auto-selection Keep the first PR selected when one edit contains several links, and accept a completed lookup when only the transport client changed for the same host and checkout. * fix(composer): release removed pull request lookups Release each auto-attach invocation when its composer state is abandoned, while continuing to discard any stale response.
This commit is contained in:
@@ -17,11 +17,14 @@ import {
|
||||
expectPickerOpen,
|
||||
expectPickerSelected,
|
||||
expectStartingRefPickerTriggerPr,
|
||||
fillNewWorkspaceDraft,
|
||||
openGlobalNewWorkspaceComposer,
|
||||
openBranchPicker,
|
||||
openNewWorkspaceComposer,
|
||||
openProjectViaDaemon,
|
||||
openStartingRefPicker,
|
||||
pasteGithubPrUrl,
|
||||
searchAndSelectBranchInPicker,
|
||||
selectBranchInPicker,
|
||||
selectGitHubPrInPicker,
|
||||
selectPickerOptionByKeyboard,
|
||||
@@ -30,9 +33,11 @@ import {
|
||||
} from "./helpers/new-workspace";
|
||||
import { createTempGitRepo, readWorktreeBranchInfo } from "./helpers/workspace";
|
||||
import {
|
||||
createLocalGithubPrFixture,
|
||||
cloneGithubRepoDefaultBranchOnly,
|
||||
createTempGithubRepo,
|
||||
hasGithubAuth,
|
||||
type LocalGhPrFixture,
|
||||
} from "./helpers/github-fixtures";
|
||||
import { getServerId } from "./helpers/server-id";
|
||||
import { getE2EDaemonPort } from "./helpers/daemon-port";
|
||||
@@ -191,6 +196,7 @@ test.describe("New workspace flow", () => {
|
||||
let client: Awaited<ReturnType<typeof connectNewWorkspaceDaemonClient>>;
|
||||
const localWorkspaceIds = new Set<string>();
|
||||
const createdWorktreeDirectories = new Set<string>();
|
||||
const localGithubFixtures = new Set<LocalGhPrFixture>();
|
||||
|
||||
test.describe.configure({ timeout: 240_000 });
|
||||
|
||||
@@ -212,6 +218,13 @@ test.describe("New workspace flow", () => {
|
||||
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();
|
||||
@@ -807,6 +820,100 @@ test.describe("New workspace flow", () => {
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
await expect(page.getByTestId("workspace-create-submit")).toBeDisabled();
|
||||
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);
|
||||
|
||||
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,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user