mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
fix(ui): preserve manually entered worktree base (#105351)
This commit is contained in:
committed by
GitHub
parent
96aff2787d
commit
879b66ab46
@@ -1,7 +1,7 @@
|
||||
// Control UI tests cover the full-page new-session draft and its folder browser
|
||||
// against a mocked Gateway: sidebar entry, fs.listDir browsing, and the final
|
||||
// sessions.create payload.
|
||||
import { chromium, type Browser } from "playwright";
|
||||
import { chromium, type Browser, type Locator, type Page } from "playwright";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
resolvePlaywrightChromiumExecutablePath,
|
||||
startControlUiE2eServer,
|
||||
type ControlUiE2eServer,
|
||||
type MockGatewayControls,
|
||||
} from "../test-helpers/control-ui-e2e.ts";
|
||||
|
||||
const chromiumExecutablePath = resolvePlaywrightChromiumExecutablePath(chromium.executablePath());
|
||||
@@ -24,6 +25,96 @@ const NODE_HOME = "/Users/peter";
|
||||
const NODE_PICKED = "/Users/peter/Projects";
|
||||
const NODE_UNC = "\\\\server\\share\\repo";
|
||||
|
||||
function installRepositorySwitchGateway(page: Page, sessionKey: string) {
|
||||
return installMockGateway(page, {
|
||||
workspaceGit: true,
|
||||
methodResponses: {
|
||||
"agents.list": {
|
||||
agents: [
|
||||
{
|
||||
id: "main",
|
||||
identity: { name: "Main" },
|
||||
name: "Main",
|
||||
workspace: SOURCE_REPO,
|
||||
workspaceGit: true,
|
||||
},
|
||||
],
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "agent",
|
||||
},
|
||||
"fs.listDir": {
|
||||
cases: [
|
||||
{
|
||||
match: { path: SOURCE_REPO },
|
||||
response: {
|
||||
path: SOURCE_REPO,
|
||||
parent: "/tmp",
|
||||
home: "/home/peter",
|
||||
entries: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"worktrees.branches": {
|
||||
cases: [
|
||||
{
|
||||
match: { repoRoot: SOURCE_REPO },
|
||||
response: {
|
||||
branches: [{ kind: "local", name: "alpha" }],
|
||||
headBranch: "alpha",
|
||||
repoRoot: SOURCE_REPO,
|
||||
},
|
||||
},
|
||||
{
|
||||
match: { repoRoot: TARGET_REPO },
|
||||
response: {
|
||||
branches: [
|
||||
{ kind: "local", name: "main" },
|
||||
{ kind: "local", name: "feature-choice" },
|
||||
],
|
||||
headBranch: "main",
|
||||
repoRoot: TARGET_REPO,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"sessions.create": { key: sessionKey },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function deferTargetRepositorySelection(
|
||||
page: Page,
|
||||
gateway: MockGatewayControls,
|
||||
): Promise<Locator> {
|
||||
await page.goto(`${server.baseUrl}new`);
|
||||
await gateway.waitForRequest("worktrees.branches");
|
||||
|
||||
const whereSelect = page.locator(
|
||||
".new-session-page__select:not(.new-session-page__select--folder)",
|
||||
);
|
||||
await whereSelect.locator("summary").click();
|
||||
await page.getByRole("menuitemradio", { name: "Worktree" }).click();
|
||||
const baseInput = page.getByLabel("Base branch");
|
||||
await expect.poll(() => baseInput.inputValue()).toBe("alpha");
|
||||
const requestsBeforeSwitch = (await gateway.getRequests("worktrees.branches")).length;
|
||||
|
||||
await gateway.deferNext("worktrees.branches");
|
||||
const folderSelect = page.locator(".new-session-page__select--folder");
|
||||
await folderSelect.locator("summary").click();
|
||||
await page
|
||||
.locator(".new-session-page__browser-list")
|
||||
.getByRole("button", { name: "Gateway" })
|
||||
.click();
|
||||
await page.locator("input.new-session-page__browser-path").fill(TARGET_REPO);
|
||||
await page.getByRole("button", { name: "Use this folder" }).click();
|
||||
await expect
|
||||
.poll(async () => (await gateway.getRequests("worktrees.branches")).length)
|
||||
.toBe(requestsBeforeSwitch + 1);
|
||||
return baseInput;
|
||||
}
|
||||
|
||||
let browser: Browser;
|
||||
let server: ControlUiE2eServer;
|
||||
|
||||
@@ -183,85 +274,10 @@ describeControlUiE2e("Control UI new-session page mocked Gateway E2E", () => {
|
||||
viewport: { height: 900, width: 1280 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const gateway = await installMockGateway(page, {
|
||||
workspaceGit: true,
|
||||
methodResponses: {
|
||||
"agents.list": {
|
||||
agents: [
|
||||
{
|
||||
id: "main",
|
||||
identity: { name: "Main" },
|
||||
name: "Main",
|
||||
workspace: SOURCE_REPO,
|
||||
workspaceGit: true,
|
||||
},
|
||||
],
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "agent",
|
||||
},
|
||||
"fs.listDir": {
|
||||
cases: [
|
||||
{
|
||||
match: { path: SOURCE_REPO },
|
||||
response: {
|
||||
path: SOURCE_REPO,
|
||||
parent: "/tmp",
|
||||
home: "/home/peter",
|
||||
entries: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"worktrees.branches": {
|
||||
cases: [
|
||||
{
|
||||
match: { repoRoot: SOURCE_REPO },
|
||||
response: {
|
||||
branches: [{ kind: "local", name: "alpha" }],
|
||||
headBranch: "alpha",
|
||||
repoRoot: SOURCE_REPO,
|
||||
},
|
||||
},
|
||||
{
|
||||
match: { repoRoot: TARGET_REPO },
|
||||
response: {
|
||||
branches: [{ kind: "local", name: "main" }],
|
||||
headBranch: "main",
|
||||
repoRoot: TARGET_REPO,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"sessions.create": { key: "agent:main:repo-switch" },
|
||||
},
|
||||
});
|
||||
const gateway = await installRepositorySwitchGateway(page, "agent:main:repo-switch");
|
||||
|
||||
try {
|
||||
await page.goto(`${server.baseUrl}new`);
|
||||
await gateway.waitForRequest("worktrees.branches");
|
||||
|
||||
const whereSelect = page.locator(
|
||||
".new-session-page__select:not(.new-session-page__select--folder)",
|
||||
);
|
||||
await whereSelect.locator("summary").click();
|
||||
await page.getByRole("menuitemradio", { name: "Worktree" }).click();
|
||||
const baseInput = page.getByLabel("Base branch");
|
||||
await expect.poll(() => baseInput.inputValue()).toBe("alpha");
|
||||
const branchRequestsBeforeSwitch = (await gateway.getRequests("worktrees.branches")).length;
|
||||
|
||||
await gateway.deferNext("worktrees.branches");
|
||||
const folderSelect = page.locator(".new-session-page__select--folder");
|
||||
await folderSelect.locator("summary").click();
|
||||
await page
|
||||
.locator(".new-session-page__browser-list")
|
||||
.getByRole("button", { name: "Gateway" })
|
||||
.click();
|
||||
await page.locator("input.new-session-page__browser-path").fill(TARGET_REPO);
|
||||
await page.getByRole("button", { name: "Use this folder" }).click();
|
||||
await expect
|
||||
.poll(async () => (await gateway.getRequests("worktrees.branches")).length)
|
||||
.toBe(branchRequestsBeforeSwitch + 1);
|
||||
const baseInput = await deferTargetRepositorySelection(page, gateway);
|
||||
|
||||
expect(await baseInput.inputValue()).toBe("");
|
||||
expect(await baseInput.getAttribute("placeholder")).toBe("Loading…");
|
||||
@@ -280,6 +296,38 @@ describeControlUiE2e("Control UI new-session page mocked Gateway E2E", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves a manually entered worktree base when branch discovery resolves", async () => {
|
||||
const context = await browser.newContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
viewport: { height: 900, width: 1280 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const gateway = await installRepositorySwitchGateway(page, "agent:main:manual-base");
|
||||
|
||||
try {
|
||||
const baseInput = await deferTargetRepositorySelection(page, gateway);
|
||||
await page
|
||||
.locator(".new-session-page__select:not(.new-session-page__select--folder) summary")
|
||||
.click();
|
||||
await baseInput.fill("feature-choice");
|
||||
await gateway.resolveDeferred("worktrees.branches");
|
||||
await expect.poll(() => baseInput.getAttribute("placeholder")).not.toBe("Loading…");
|
||||
expect(await baseInput.inputValue()).toBe("feature-choice");
|
||||
|
||||
await page.locator(".new-session-page__message").fill("use my selected base");
|
||||
await page.getByRole("button", { name: "Start session" }).click();
|
||||
const create = await gateway.waitForRequest("sessions.create");
|
||||
expect(create.params).toMatchObject({
|
||||
cwd: TARGET_REPO,
|
||||
worktree: true,
|
||||
worktreeBaseRef: "feature-choice",
|
||||
});
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps a rejected first message visible and retryable after reload", async () => {
|
||||
const context = await browser.newContext({
|
||||
locale: "en-US",
|
||||
|
||||
@@ -85,6 +85,7 @@ class NewSessionPage extends OpenClawLightDomElement {
|
||||
private openedFor: string | null = null;
|
||||
private agentsHydrated = false;
|
||||
private branchesRequestToken = 0;
|
||||
private baseRefEditGeneration = 0;
|
||||
private browserRequestToken = 0;
|
||||
|
||||
// Re-render when agents/sessions hydrate so the hero identity and the
|
||||
@@ -321,6 +322,7 @@ class NewSessionPage extends OpenClawLightDomElement {
|
||||
// Branch data belongs to one repository selection. Clear it before any
|
||||
// exit or request so a previous repo's ref can never reach sessions.create.
|
||||
const requestId = ++this.branchesRequestToken;
|
||||
const baseRefEditGeneration = this.baseRefEditGeneration;
|
||||
this.branches = null;
|
||||
this.branchesLoading = false;
|
||||
this.baseRef = "";
|
||||
@@ -346,7 +348,11 @@ class NewSessionPage extends OpenClawLightDomElement {
|
||||
return;
|
||||
}
|
||||
this.branches = result ? { ...result, repoRoot } : null;
|
||||
this.baseRef = result?.defaultBranch ?? result?.headBranch ?? "";
|
||||
// Discovery supplies a default only while the field is untouched;
|
||||
// a user edit made during the request remains authoritative.
|
||||
if (baseRefEditGeneration === this.baseRefEditGeneration) {
|
||||
this.baseRef = result?.defaultBranch ?? result?.headBranch ?? "";
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (requestId === this.branchesRequestToken) {
|
||||
@@ -902,6 +908,7 @@ class NewSessionPage extends OpenClawLightDomElement {
|
||||
: (branches?.defaultBranch ?? t("newSession.baseBranch"))}
|
||||
.value=${this.baseRef}
|
||||
@input=${(event: Event) => {
|
||||
this.baseRefEditGeneration += 1;
|
||||
this.baseRef = (event.target as HTMLInputElement).value.trim();
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user