Skip to content

Commit d58ecf3

Browse files
fix(ui): surface coalesced update restarts
Co-authored-by: goutamadwant <[email protected]>
1 parent bf04d04 commit d58ecf3

3 files changed

Lines changed: 113 additions & 0 deletions

File tree

ui/src/app/overlays.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,25 @@ describe("application approval overlays", () => {
143143
overlays.dispose();
144144
});
145145
});
146+
147+
describe("application update overlays", () => {
148+
it("surfaces a coalesced restart while reconnect verification remains active", async () => {
149+
const request = vi.fn<RequestFn>().mockResolvedValue({
150+
ok: true,
151+
restart: { coalesced: true },
152+
result: { status: "ok", after: { version: "2.0.0" } },
153+
});
154+
const harness = createGatewayHarness(client(request));
155+
const overlays = createApplicationOverlays(harness.gateway);
156+
157+
await overlays.runUpdate();
158+
159+
expect(request).toHaveBeenCalledWith("update.run", {});
160+
expect(overlays.snapshot.updateStatusBanner).toEqual({
161+
tone: "info",
162+
text: "Update installed. A gateway restart is already in progress; status will refresh after it reconnects.",
163+
});
164+
expect(overlays.snapshot.updateRunning).toBe(false);
165+
overlays.dispose();
166+
});
167+
});

ui/src/app/overlays.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ type UpdateRunResponse = {
191191
after?: { version?: string | null } | null;
192192
};
193193
handoff?: { status?: string };
194+
restart?: { coalesced?: boolean } | null;
194195
};
195196

196197
export function createApplicationOverlays(gateway: ApplicationGateway): ApplicationOverlays {
@@ -521,6 +522,15 @@ export function createApplicationOverlays(gateway: ApplicationGateway): Applicat
521522
if (response.ok === true && status === "ok") {
522523
pendingUpdateExpectedVersion = expectedVersion;
523524
pendingUpdateHandoff = false;
525+
if (response.restart?.coalesced === true) {
526+
snapshot = {
527+
...snapshot,
528+
updateStatusBanner: {
529+
tone: "info",
530+
text: "Update installed. A gateway restart is already in progress; status will refresh after it reconnects.",
531+
},
532+
};
533+
}
524534
return;
525535
}
526536
pendingUpdateExpectedVersion = null;
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import path from "node:path";
2+
import { chromium, type Browser } from "playwright";
3+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
4+
import {
5+
canRunPlaywrightChromium,
6+
installMockGateway,
7+
resolvePlaywrightChromiumExecutablePath,
8+
startControlUiE2eServer,
9+
type ControlUiE2eServer,
10+
} from "../test-helpers/control-ui-e2e.ts";
11+
12+
const chromiumExecutablePath = resolvePlaywrightChromiumExecutablePath(chromium.executablePath());
13+
const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
14+
const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1";
15+
const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
16+
17+
let browser: Browser;
18+
let server: ControlUiE2eServer;
19+
20+
describeControlUiE2e("Control UI coalesced update E2E", () => {
21+
beforeAll(async () => {
22+
if (!chromiumAvailable) {
23+
throw new Error(`Playwright Chromium is unavailable at ${chromiumExecutablePath}`);
24+
}
25+
server = await startControlUiE2eServer();
26+
browser = await chromium.launch({ executablePath: chromiumExecutablePath });
27+
});
28+
29+
afterAll(async () => {
30+
await browser?.close();
31+
await server?.close();
32+
});
33+
34+
it("shows coalesced restart feedback after the Update click", async () => {
35+
const artifactDir = path.resolve(".artifacts/control-ui-e2e/update-coalesced");
36+
const context = await browser.newContext({
37+
locale: "en-US",
38+
recordVideo: { dir: artifactDir, size: { height: 720, width: 1280 } },
39+
serviceWorkers: "block",
40+
viewport: { height: 720, width: 1280 },
41+
});
42+
const page = await context.newPage();
43+
const pageErrors: string[] = [];
44+
page.on("pageerror", (error) => pageErrors.push(String(error)));
45+
const gateway = await installMockGateway(page, {
46+
methodResponses: {
47+
"update.run": {
48+
ok: true,
49+
restart: { coalesced: true },
50+
result: { after: { version: "2.0.0" }, status: "ok" },
51+
},
52+
},
53+
});
54+
55+
try {
56+
expect((await page.goto(`${server.baseUrl}chat`))?.status()).toBe(200);
57+
await gateway.emitGatewayEvent("update.available", {
58+
updateAvailable: {
59+
channel: "stable",
60+
currentVersion: "1.0.0",
61+
latestVersion: "2.0.0",
62+
},
63+
});
64+
65+
await page.getByRole("button", { name: "Update now" }).click();
66+
await page
67+
.getByText(
68+
"Update installed. A gateway restart is already in progress; status will refresh after it reconnects.",
69+
{ exact: true },
70+
)
71+
.waitFor();
72+
73+
expect(await gateway.getRequests("update.run")).toHaveLength(1);
74+
expect(await page.getByRole("button", { name: "Update now" }).isEnabled()).toBe(true);
75+
expect(pageErrors).toEqual([]);
76+
await page.screenshot({ path: path.join(artifactDir, "coalesced-restart-banner.png") });
77+
} finally {
78+
await context.close();
79+
}
80+
});
81+
});

0 commit comments

Comments
 (0)