mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(ui): preserve worktree mutation errors (#106196)
This commit is contained in:
committed by
GitHub
parent
4f287dd740
commit
95d9457465
@@ -0,0 +1,81 @@
|
||||
// Control UI tests cover Worktrees mutation failures through the rendered settings page.
|
||||
import { chromium, type Browser } from "playwright";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
installMockGateway,
|
||||
resolvePlaywrightChromiumExecutablePath,
|
||||
startControlUiE2eServer,
|
||||
type ControlUiE2eServer,
|
||||
} from "../test-helpers/control-ui-e2e.ts";
|
||||
|
||||
const chromiumExecutablePath = resolvePlaywrightChromiumExecutablePath(chromium.executablePath());
|
||||
const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
|
||||
const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1";
|
||||
const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
|
||||
|
||||
let browser: Browser;
|
||||
let server: ControlUiE2eServer;
|
||||
|
||||
const restorableWorktree = {
|
||||
baseRef: "main",
|
||||
branch: "openclaw/test",
|
||||
createdAt: 1,
|
||||
id: "worktree-1",
|
||||
lastActiveAt: 2,
|
||||
name: "restorable-test",
|
||||
ownerKind: "manual",
|
||||
path: "/tmp/repo/.worktrees/restorable-test",
|
||||
removedAt: 3,
|
||||
repoFingerprint: "0123456789abcdef",
|
||||
repoRoot: "/tmp/repo",
|
||||
snapshotRef: "refs/openclaw/worktree-snapshots/test",
|
||||
};
|
||||
|
||||
describeControlUiE2e("Control UI Worktrees mocked Gateway E2E", () => {
|
||||
beforeAll(async () => {
|
||||
if (!chromiumAvailable) {
|
||||
throw new Error(
|
||||
`Playwright Chromium is not installed or cannot start at ${chromiumExecutablePath}. Run \`pnpm --dir ui exec playwright install --with-deps chromium\`, or set OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM=1 only when intentionally skipping this lane.`,
|
||||
);
|
||||
}
|
||||
server = await startControlUiE2eServer();
|
||||
browser = await chromium.launch({ executablePath: chromiumExecutablePath });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close();
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
it("keeps a restore failure visible after the automatic list refresh succeeds", async () => {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
const gateway = await installMockGateway(page, {
|
||||
deferredMethods: ["worktrees.restore"],
|
||||
methodResponses: {
|
||||
"worktrees.list": { worktrees: [restorableWorktree] },
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await page.goto(`${server.baseUrl}settings/worktrees`);
|
||||
expect(response?.status()).toBe(200);
|
||||
await page.getByRole("button", { name: "Restore" }).click();
|
||||
await gateway.waitForRequest("worktrees.restore");
|
||||
await gateway.rejectDeferred("worktrees.restore", {
|
||||
message: "source repository is unavailable",
|
||||
});
|
||||
|
||||
await expect
|
||||
.poll(async () => (await gateway.getRequests("worktrees.list")).length)
|
||||
.toBeGreaterThanOrEqual(2);
|
||||
await expect(page.locator(".callout.danger").textContent()).resolves.toContain(
|
||||
"source repository is unavailable",
|
||||
);
|
||||
await expect(page.getByRole("button", { name: "Restore" }).count()).resolves.toBe(1);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -18,7 +18,7 @@ type WorktreesPageTestElement = HTMLElement & {
|
||||
createBranches: string[];
|
||||
updateComplete: Promise<boolean>;
|
||||
requestUpdate: () => void;
|
||||
load: () => Promise<void>;
|
||||
load: (options?: { preserveError?: boolean }) => Promise<void>;
|
||||
loadCreateBranches: () => void;
|
||||
createWorktree: () => Promise<void>;
|
||||
removeWorktree: (record: WorktreeRecord) => Promise<void>;
|
||||
@@ -311,6 +311,64 @@ describe("WorktreesPage lifecycle", () => {
|
||||
expect(page.busyId).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps a restore error after the reconciliation refresh succeeds", async () => {
|
||||
const record = worktree();
|
||||
let listRequests = 0;
|
||||
const request = vi.fn((method: string) => {
|
||||
if (method === "worktrees.list") {
|
||||
listRequests += 1;
|
||||
return Promise.resolve({ worktrees: [record] });
|
||||
}
|
||||
if (method === "worktrees.restore") {
|
||||
return Promise.reject(new Error("restore failed"));
|
||||
}
|
||||
return Promise.resolve({});
|
||||
});
|
||||
const page = document.createElement("openclaw-worktrees-page") as WorktreesPageTestElement;
|
||||
page.context = contextWithGateway(
|
||||
gatewayWithClient({ request } as unknown as GatewayBrowserClient),
|
||||
);
|
||||
document.body.append(page);
|
||||
await vi.waitFor(() => expect(listRequests).toBe(1));
|
||||
await vi.waitFor(() => expect(page.loading).toBe(false));
|
||||
|
||||
await page.restore(record);
|
||||
|
||||
expect(listRequests).toBe(2);
|
||||
expect(page.error).toBe("Error: restore failed");
|
||||
expect(page.busyId).toBeNull();
|
||||
});
|
||||
|
||||
it("replaces a mutation error when the reconciliation refresh also fails", async () => {
|
||||
const record = worktree();
|
||||
let listRequests = 0;
|
||||
const request = vi.fn((method: string) => {
|
||||
if (method === "worktrees.list") {
|
||||
listRequests += 1;
|
||||
return listRequests === 1
|
||||
? Promise.resolve({ worktrees: [record] })
|
||||
: Promise.reject(new Error("list failed"));
|
||||
}
|
||||
if (method === "worktrees.restore") {
|
||||
return Promise.reject(new Error("restore failed"));
|
||||
}
|
||||
return Promise.resolve({});
|
||||
});
|
||||
const page = document.createElement("openclaw-worktrees-page") as WorktreesPageTestElement;
|
||||
page.context = contextWithGateway(
|
||||
gatewayWithClient({ request } as unknown as GatewayBrowserClient),
|
||||
);
|
||||
document.body.append(page);
|
||||
await vi.waitFor(() => expect(listRequests).toBe(1));
|
||||
await vi.waitFor(() => expect(page.loading).toBe(false));
|
||||
|
||||
await page.restore(record);
|
||||
|
||||
expect(listRequests).toBe(2);
|
||||
expect(page.error).toBe("Error: list failed");
|
||||
expect(page.busyId).toBeNull();
|
||||
});
|
||||
|
||||
it("clears pending create state across a same-client reconnect", async () => {
|
||||
const pendingCreate = deferred<unknown>();
|
||||
const request = vi.fn((method: string) => {
|
||||
|
||||
@@ -144,14 +144,16 @@ class WorktreesPage extends OpenClawLightDomElement {
|
||||
return this.loading || this.busyId !== null || this.creating;
|
||||
}
|
||||
|
||||
private async load() {
|
||||
private async load(options: { preserveError?: boolean } = {}) {
|
||||
const client = this.client;
|
||||
if (!client || !this.gatewayConnected || this.operationPending) {
|
||||
return;
|
||||
}
|
||||
const generation = ++this.loadGeneration;
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
if (!options.preserveError) {
|
||||
this.error = null;
|
||||
}
|
||||
try {
|
||||
const result = await client.request<WorktreesListResult>("worktrees.list", {});
|
||||
if (generation === this.loadGeneration && client === this.client) {
|
||||
@@ -212,7 +214,7 @@ class WorktreesPage extends OpenClawLightDomElement {
|
||||
} finally {
|
||||
if (this.isOperationScopeCurrent(scope)) {
|
||||
this.busyId = null;
|
||||
await this.load();
|
||||
await this.load({ preserveError: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,7 +235,7 @@ class WorktreesPage extends OpenClawLightDomElement {
|
||||
} finally {
|
||||
if (this.isOperationScopeCurrent(scope)) {
|
||||
this.busyId = null;
|
||||
await this.load();
|
||||
await this.load({ preserveError: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,7 +256,7 @@ class WorktreesPage extends OpenClawLightDomElement {
|
||||
} finally {
|
||||
if (this.isOperationScopeCurrent(scope)) {
|
||||
this.loading = false;
|
||||
await this.load();
|
||||
await this.load({ preserveError: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -325,7 +327,7 @@ class WorktreesPage extends OpenClawLightDomElement {
|
||||
} finally {
|
||||
if (this.isOperationScopeCurrent(scope)) {
|
||||
this.creating = false;
|
||||
await this.load();
|
||||
await this.load({ preserveError: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user