fix(ui): recover authorized deep links after Control UI updates (#124328)

* fix(ui): refresh stale dashboard documents

* test(ui): count recovery document requests
This commit is contained in:
Peter Steinberger
2026-08-15 18:32:59 -07:00
committed by GitHub
parent e9d70a1169
commit 1fda883530
4 changed files with 73 additions and 14 deletions
+4 -1
View File
@@ -50,7 +50,10 @@ export function isStaleChunkImportError(error: unknown): boolean {
}
function reloadControlUiDocument(): void {
window.location.reload();
const url = new URL(window.location.href);
// The pre-app mount recovery strips this one-shot cache buster before bootstrap.
url.searchParams.set("openclaw_mount_recovery", String(Date.now()));
window.location.replace(url.href);
}
function sessionStorageOrNull(): Pick<Storage, "getItem" | "setItem"> | null {
+7 -7
View File
@@ -100,14 +100,14 @@ suite.define(() => {
await route.abort("internetdisconnected");
};
await page.route(frenchLocaleModule, abortFrenchLocale);
let navigationCount = 0;
let documentRequestCount = 0;
try {
const response = await page.goto(`${suite.server.baseUrl}settings/appearance`);
expect(response?.status()).toBe(200);
page.on("framenavigated", (frame) => {
if (frame === page.mainFrame()) {
navigationCount += 1;
page.on("request", (request) => {
if (request.resourceType() === "document") {
documentRequestCount += 1;
}
});
await page.locator(".settings-row__title", { hasText: "Language" }).waitFor();
@@ -128,7 +128,7 @@ suite.define(() => {
await page.locator(".settings-row__title", { hasText: "Language" }).waitFor();
await page.locator(".settings-page").waitFor();
expect(await documentMarker(page)).toBe("same-document");
expect(navigationCount).toBe(0);
expect(documentRequestCount).toBe(0);
await page.unroute(frenchLocaleModule, abortFrenchLocale);
await reconnect(page, gateway);
@@ -137,14 +137,14 @@ suite.define(() => {
.locator(".settings-row__title", { hasText: "Langue" })
.waitFor({ timeout: 10_000 });
expect(await documentMarker(page)).toBeUndefined();
expect(navigationCount).toBe(1);
expect(documentRequestCount).toBe(1);
expect(
await page.evaluate(() =>
sessionStorage.getItem("openclaw.controlUi.staleChunkReloadBuildId"),
),
).toBe("e2e");
await page.waitForTimeout(500);
expect(navigationCount).toBe(1);
expect(documentRequestCount).toBe(1);
expect(new URL(page.url()).pathname).toBe("/settings/appearance");
} finally {
await page.unroute(frenchLocaleModule, abortFrenchLocale);
+56
View File
@@ -59,6 +59,62 @@ async function closeContext(context: BrowserContext): Promise<void> {
}
suite.define(() => {
it("cache-busts stale-build recovery on a first dashboard navigation", async () => {
const context = await suite.browser.newContext({
serviceWorkers: "block",
viewport: { height: 900, width: 1280 },
});
const page = await context.newPage();
const documentRequests: Array<{ fresh: boolean; pathname: string }> = [];
const appOrigin = new URL(suite.server.baseUrl).origin;
await page.route(`${appOrigin}/**`, async (route) => {
const request = route.request();
if (request.resourceType() === "document") {
const url = new URL(request.url());
documentRequests.push({
fresh: url.searchParams.has("openclaw_mount_recovery"),
pathname: url.pathname,
});
}
await route.continue();
});
const gateway = await installMockGateway(page, {
deferredMethods: ["connect"],
sessionKey: "agent:example-agent:example-session",
});
const mismatch = {
code: "UNAVAILABLE",
message: "Control UI updated; reload this page to continue",
details: {
code: ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH,
gatewayBuildId: "replacement-build",
reloadRequired: true,
},
retryable: false,
};
const target = new URL("dashboard/example-agent/example-session", suite.server.baseUrl);
try {
await page.goto(target.href);
await gateway.waitForRequest("connect");
await gateway.rejectDeferred("connect", mismatch);
await expect.poll(() => documentRequests.length).toBe(2);
await gateway.waitForRequest("connect");
expect(documentRequests).toEqual([
{ fresh: false, pathname: target.pathname },
{ fresh: true, pathname: target.pathname },
]);
await gateway.resolveDeferred("connect");
await page.locator("openclaw-app-shell").waitFor();
expect(await page.locator("openclaw-login-gate").count()).toBe(0);
await expect.poll(() => page.url()).toBe(target.href);
} finally {
await closeContext(context);
}
});
it("reloads once for a build rejection, then keeps visible recovery guidance", async () => {
const context = await suite.browser.newContext({ viewport: { height: 900, width: 1280 } });
const page = await context.newPage();
@@ -100,10 +100,10 @@ suite.define(() => {
path: path.join(artifactDir, "failure.png"),
});
let navigationCount = 0;
page.on("framenavigated", (frame) => {
if (frame === page.mainFrame()) {
navigationCount += 1;
let documentRequestCount = 0;
page.on("request", (request) => {
if (request.resourceType() === "document") {
documentRequestCount += 1;
}
});
markDocumentReachable();
@@ -112,13 +112,13 @@ suite.define(() => {
await alert.waitFor();
await page.waitForTimeout(500);
expect(await alert.count()).toBe(1);
expect(navigationCount).toBe(1);
expect(documentRequestCount).toBe(1);
await alert.getByRole("button", { name: "Reload" }).click();
await page.locator(".logbook").waitFor();
expect(await alert.count()).toBe(0);
expect(assetRequests).toBeGreaterThan(2);
expect(navigationCount).toBe(2);
expect(documentRequestCount).toBe(2);
await gateway.waitForRequest("logbook.status");
await page.screenshot({
fullPage: true,