mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix: stale Control UI tabs stop retrying after updates (#123932)
* fix(ui): preserve stale client refresh compatibility * fix(ui): cover build-aware frozen clients
This commit is contained in:
committed by
GitHub
parent
8266adfb45
commit
6abfedcca5
@@ -323,18 +323,16 @@ export async function attachAuthenticatedGatewayConnect(
|
||||
requestOrigin,
|
||||
});
|
||||
if (controlUiBuildMismatch) {
|
||||
const message = "Control UI updated; reload this page to continue";
|
||||
// Build identity predates this rejection. Frozen clients recognize the shipped
|
||||
// protocol-mismatch signal and surface its literal reload guidance.
|
||||
const message = "protocol mismatch: Control UI updated; reload this page to continue";
|
||||
markHandshakeFailure("control-ui-build-mismatch", {
|
||||
clientBuildId: controlUiBuildMismatch.clientBuildId ?? "legacy",
|
||||
gatewayBuildId: controlUiBuildMismatch.gatewayBuildId,
|
||||
});
|
||||
sendHandshakeErrorResponse(ErrorCodes.UNAVAILABLE, message, {
|
||||
retryable: false,
|
||||
details: {
|
||||
code: ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH,
|
||||
gatewayBuildId: controlUiBuildMismatch.gatewayBuildId,
|
||||
reloadRequired: true,
|
||||
},
|
||||
details: { code: ConnectErrorDetailCodes.PROTOCOL_MISMATCH },
|
||||
});
|
||||
logWsControl.warn(
|
||||
`control ui build rejected conn=${connId} clientBuild=${formatForLog(controlUiBuildMismatch.clientBuildId ?? "legacy")} gatewayBuild=${formatForLog(controlUiBuildMismatch.gatewayBuildId)}; reload required`,
|
||||
|
||||
+35
-9
@@ -12,11 +12,13 @@ const {
|
||||
handleGatewayRequestMock,
|
||||
incrementPresenceVersionMock,
|
||||
resolveRuntimeServiceBuildIdMock,
|
||||
setLastFrameMetaMock,
|
||||
upsertPresenceMock,
|
||||
} = vi.hoisted(() => ({
|
||||
handleGatewayRequestMock: vi.fn(),
|
||||
incrementPresenceVersionMock: vi.fn(() => 2),
|
||||
resolveRuntimeServiceBuildIdMock: vi.fn<() => string | null>(() => "gateway-build"),
|
||||
setLastFrameMetaMock: vi.fn(),
|
||||
upsertPresenceMock: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -98,7 +100,17 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("Control UI build admission over WebSocket", () => {
|
||||
it("rejects a legacy same-origin document before registration or RPC dispatch", async () => {
|
||||
it.each([
|
||||
{
|
||||
name: "legacy same-origin document",
|
||||
clientBuildId: undefined,
|
||||
},
|
||||
{
|
||||
name: "explicit stale same-origin document",
|
||||
clientBuildId: "stale-build",
|
||||
},
|
||||
])("rejects a $name before registration or RPC dispatch", async (testCase) => {
|
||||
const { clientBuildId } = testCase;
|
||||
const wss = new WebSocketServer({ host: "127.0.0.1", port: 0 });
|
||||
await withDeadline(
|
||||
new Promise<void>((resolve) => {
|
||||
@@ -138,7 +150,9 @@ describe("Control UI build admission over WebSocket", () => {
|
||||
nodeLifecycleDispatch: new GatewayNodeLifecycleDispatchTracker(),
|
||||
refreshHealthSnapshot: vi.fn(),
|
||||
send,
|
||||
close: (code, reason) => socket.close(code, reason),
|
||||
close: (code, reason) => {
|
||||
setTimeout(() => socket.close(code, reason), 25);
|
||||
},
|
||||
isClosed: () => socket.readyState >= WebSocket.CLOSING,
|
||||
clearHandshakeTimer: vi.fn(),
|
||||
getClient: () => connectedClient as never,
|
||||
@@ -149,7 +163,7 @@ describe("Control UI build admission over WebSocket", () => {
|
||||
setHandshakeState: vi.fn(),
|
||||
advanceHandshakePhase: vi.fn(),
|
||||
setCloseCause: vi.fn(),
|
||||
setLastFrameMeta: vi.fn(),
|
||||
setLastFrameMeta: setLastFrameMetaMock,
|
||||
originCheckMetrics: { hostHeaderFallbackAccepted: 0 },
|
||||
logGateway: createLogger() as never,
|
||||
logHealth: createLogger() as never,
|
||||
@@ -196,6 +210,7 @@ describe("Control UI build admission over WebSocket", () => {
|
||||
version: "2026.8.1",
|
||||
platform: "web",
|
||||
mode: "webchat",
|
||||
...(clientBuildId ? { buildId: clientBuildId } : {}),
|
||||
},
|
||||
role: "operator",
|
||||
caps: [],
|
||||
@@ -204,21 +219,32 @@ describe("Control UI build admission over WebSocket", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
expect(await response).toMatchObject({
|
||||
const rejection = await response;
|
||||
expect(rejection).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
code: ErrorCodes.UNAVAILABLE,
|
||||
message: "protocol mismatch: Control UI updated; reload this page to continue",
|
||||
retryable: false,
|
||||
details: {
|
||||
code: ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH,
|
||||
gatewayBuildId: "gateway-build",
|
||||
reloadRequired: true,
|
||||
},
|
||||
details: { code: ConnectErrorDetailCodes.PROTOCOL_MISMATCH },
|
||||
},
|
||||
});
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "req",
|
||||
id: "post-rejection-rpc",
|
||||
method: "health",
|
||||
params: {},
|
||||
}),
|
||||
);
|
||||
expect(await closed).toBe(1008);
|
||||
expect(connectedClient).toBeNull();
|
||||
expect(upsertPresenceMock).not.toHaveBeenCalled();
|
||||
expect(setLastFrameMetaMock).toHaveBeenCalledWith({
|
||||
type: "req",
|
||||
method: "health",
|
||||
id: "post-rejection-rpc",
|
||||
});
|
||||
expect(handleGatewayRequestMock).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
ws.terminate();
|
||||
|
||||
@@ -120,7 +120,7 @@ suite.define(() => {
|
||||
await gateway.waitForRequest("connect");
|
||||
await gateway.rejectDeferred("connect", {
|
||||
code: "INVALID_REQUEST",
|
||||
message: "protocol mismatch",
|
||||
message: "protocol mismatch: Control UI updated; reload this page to continue",
|
||||
details: { code: ConnectErrorDetailCodes.PROTOCOL_MISMATCH },
|
||||
});
|
||||
|
||||
@@ -129,6 +129,7 @@ suite.define(() => {
|
||||
expect((await failure.textContent())?.toLowerCase()).toContain(
|
||||
"supported connection protocol",
|
||||
);
|
||||
expect(await page.locator(".login-gate__failure-refresh").isVisible()).toBe(true);
|
||||
await page.clock.runFor(1_600);
|
||||
expect(await gateway.getRequests("connect")).toHaveLength(1);
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user