mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 22:26:07 +00:00
fix(ui): surface coalesced update restarts (#93082)
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -143,3 +143,25 @@ describe("application approval overlays", () => {
|
||||
overlays.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe("application update overlays", () => {
|
||||
it("surfaces a coalesced restart while reconnect verification remains active", async () => {
|
||||
const request = vi.fn<RequestFn>().mockResolvedValue({
|
||||
ok: true,
|
||||
restart: { coalesced: true },
|
||||
result: { status: "ok", after: { version: "2.0.0" } },
|
||||
});
|
||||
const harness = createGatewayHarness(client(request));
|
||||
const overlays = createApplicationOverlays(harness.gateway);
|
||||
|
||||
await overlays.runUpdate();
|
||||
|
||||
expect(request).toHaveBeenCalledWith("update.run", {});
|
||||
expect(overlays.snapshot.updateStatusBanner).toEqual({
|
||||
tone: "info",
|
||||
text: "Update installed. A gateway restart is already in progress; status will refresh after it reconnects.",
|
||||
});
|
||||
expect(overlays.snapshot.updateRunning).toBe(false);
|
||||
overlays.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -191,6 +191,7 @@ type UpdateRunResponse = {
|
||||
after?: { version?: string | null } | null;
|
||||
};
|
||||
handoff?: { status?: string };
|
||||
restart?: { coalesced?: boolean } | null;
|
||||
};
|
||||
|
||||
export function createApplicationOverlays(gateway: ApplicationGateway): ApplicationOverlays {
|
||||
@@ -521,6 +522,15 @@ export function createApplicationOverlays(gateway: ApplicationGateway): Applicat
|
||||
if (response.ok === true && status === "ok") {
|
||||
pendingUpdateExpectedVersion = expectedVersion;
|
||||
pendingUpdateHandoff = false;
|
||||
if (response.restart?.coalesced === true) {
|
||||
snapshot = {
|
||||
...snapshot,
|
||||
updateStatusBanner: {
|
||||
tone: "info",
|
||||
text: "Update installed. A gateway restart is already in progress; status will refresh after it reconnects.",
|
||||
},
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
pendingUpdateExpectedVersion = null;
|
||||
|
||||
81
ui/src/e2e/update-coalesced.e2e.test.ts
Normal file
81
ui/src/e2e/update-coalesced.e2e.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import path from "node:path";
|
||||
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;
|
||||
|
||||
describeControlUiE2e("Control UI coalesced update E2E", () => {
|
||||
beforeAll(async () => {
|
||||
if (!chromiumAvailable) {
|
||||
throw new Error(`Playwright Chromium is unavailable at ${chromiumExecutablePath}`);
|
||||
}
|
||||
server = await startControlUiE2eServer();
|
||||
browser = await chromium.launch({ executablePath: chromiumExecutablePath });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close();
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
it("shows coalesced restart feedback after the Update click", async () => {
|
||||
const artifactDir = path.resolve(".artifacts/control-ui-e2e/update-coalesced");
|
||||
const context = await browser.newContext({
|
||||
locale: "en-US",
|
||||
recordVideo: { dir: artifactDir, size: { height: 720, width: 1280 } },
|
||||
serviceWorkers: "block",
|
||||
viewport: { height: 720, width: 1280 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const pageErrors: string[] = [];
|
||||
page.on("pageerror", (error) => pageErrors.push(String(error)));
|
||||
const gateway = await installMockGateway(page, {
|
||||
methodResponses: {
|
||||
"update.run": {
|
||||
ok: true,
|
||||
restart: { coalesced: true },
|
||||
result: { after: { version: "2.0.0" }, status: "ok" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
expect((await page.goto(`${server.baseUrl}chat`))?.status()).toBe(200);
|
||||
await gateway.emitGatewayEvent("update.available", {
|
||||
updateAvailable: {
|
||||
channel: "stable",
|
||||
currentVersion: "1.0.0",
|
||||
latestVersion: "2.0.0",
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole("button", { name: "Update now" }).click();
|
||||
await page
|
||||
.getByText(
|
||||
"Update installed. A gateway restart is already in progress; status will refresh after it reconnects.",
|
||||
{ exact: true },
|
||||
)
|
||||
.waitFor();
|
||||
|
||||
expect(await gateway.getRequests("update.run")).toHaveLength(1);
|
||||
expect(await page.getByRole("button", { name: "Update now" }).isEnabled()).toBe(true);
|
||||
expect(pageErrors).toEqual([]);
|
||||
await page.screenshot({ path: path.join(artifactDir, "coalesced-restart-banner.png") });
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user