mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
fix(ui): re-arm Model Setup detection when stale route data settles after mount (#127695)
The Model Setup page could sit at "Checking this Gateway for available AI access…" forever on fresh clients: the route loader ran before the Gateway websocket connected and returned a loading state with a null-client connection, the lazy page module finished importing after hello, and the page mounted with routeData undefined under an already-connected Gateway. When the stale loader result settled a microtask later, willUpdate correctly discarded it, but synchronizeGateway hit the identity-equality early return and nothing ever armed detectTask again - a silent dead-end (fresh headless clients hit it ~100%; warm tabs recovered only because their cached module mounted before hello). Replace the mount-branch routeData-connection comparison with a single self-healing invariant, ensureRouteSettledDetection(), also called from the identity-equality early return: once route data has settled, a page still holding phase "loading" with an idle detect task and a connected, capable Gateway starts detection itself. Guards keep the normal path duplicate-free: undefined routeData means the loader's own detect is still in flight, and hasUpdated defers to willUpdate's seeding since subscriptions fire before the first render. Regression test mounts the page without routeData under a connected Gateway, then delivers the stale pre-connect loader result and asserts exactly one openclaw.setup.detect and a rendered result; it fails on the pre-fix code with zero detect calls. Live-verified with a headless Playwright client against a dev gateway: pre-fix stalls past 45s, fixed build reaches ready in ~5s with a single detect request.
This commit is contained in:
committed by
GitHub
parent
5f6de37e20
commit
eaef2c1be7
@@ -1,5 +1,5 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { initialState, Task } from "@lit/task";
|
||||
import { initialState, Task, TaskStatus } from "@lit/task";
|
||||
import { html, type PropertyValues } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import type { GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
@@ -305,15 +305,7 @@ export class ModelSetupPage extends OpenClawLightDomElement {
|
||||
};
|
||||
if (!this.observedConnection) {
|
||||
this.observedConnection = connection;
|
||||
if (
|
||||
connection.connected &&
|
||||
this.routeData &&
|
||||
(this.routeData.connection.client !== connection.client ||
|
||||
this.routeData.connection.hello !== connection.hello ||
|
||||
this.routeData.connection.agentId !== connection.agentId)
|
||||
) {
|
||||
void this.detect();
|
||||
}
|
||||
this.ensureRouteSettledDetection();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
@@ -322,6 +314,7 @@ export class ModelSetupPage extends OpenClawLightDomElement {
|
||||
connection.agentId === this.observedConnection.agentId &&
|
||||
connection.connected === this.observedConnection.connected
|
||||
) {
|
||||
this.ensureRouteSettledDetection();
|
||||
return;
|
||||
}
|
||||
this.observedConnection = connection;
|
||||
@@ -344,6 +337,24 @@ export class ModelSetupPage extends OpenClawLightDomElement {
|
||||
}
|
||||
}
|
||||
|
||||
// Route data can settle after mount and be discarded as another
|
||||
// connection's result. Nothing else re-arms detection then, so a loading
|
||||
// page with a connected, capable Gateway self-heals here instead of
|
||||
// dead-ending silently.
|
||||
private ensureRouteSettledDetection(): void {
|
||||
if (
|
||||
!this.hasUpdated ||
|
||||
!this.routeData ||
|
||||
this.pageState.phase !== "loading" ||
|
||||
this.detectTask.status !== TaskStatus.INITIAL
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (this.canUseSetup(this.context.gateway.snapshot.client)) {
|
||||
void this.detect();
|
||||
}
|
||||
}
|
||||
|
||||
private canUseSetup(client: GatewayBrowserClient | null): client is GatewayBrowserClient {
|
||||
const snapshot = this.context.gateway.snapshot;
|
||||
return Boolean(
|
||||
|
||||
@@ -122,6 +122,35 @@ describe("ModelSetupPage Gateway reconnect ownership", () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("recovers when stale route data settles after mounting under a connected gateway", async () => {
|
||||
const { context, request, runtimeConfig } = createFixture();
|
||||
request.mockImplementation(async (method) =>
|
||||
method === "openclaw.setup.detect" ? detection : {},
|
||||
);
|
||||
const provider = createApplicationContextProvider(context);
|
||||
const page = document.createElement("openclaw-model-setup-page") as TestModelSetupPage;
|
||||
provider.append(page);
|
||||
document.body.append(provider);
|
||||
await page.updateComplete;
|
||||
|
||||
expect(request).not.toHaveBeenCalled();
|
||||
|
||||
page.routeData = {
|
||||
state: { phase: "loading" },
|
||||
connection: { client: null, hello: null, agentId: null },
|
||||
firstRun: false,
|
||||
};
|
||||
await page.updateComplete;
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(
|
||||
request.mock.calls.filter(([method]) => method === "openclaw.setup.detect"),
|
||||
).toHaveLength(1);
|
||||
expect(page.querySelector('[data-auth-choice="provider-auth"]')).not.toBeNull();
|
||||
});
|
||||
runtimeConfig.dispose();
|
||||
});
|
||||
|
||||
it("does not expose stale route data when the page mounts during reconnect", async () => {
|
||||
const { client, context, request, runtimeConfig, setGatewayPhase } = createFixture();
|
||||
request.mockImplementation(async (method) =>
|
||||
|
||||
Reference in New Issue
Block a user