diff --git a/CHANGELOG.md b/CHANGELOG.md index ef3bb669834..3d177e8f88a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,6 +97,7 @@ Docs: https://docs.openclaw.ai - Cron/Isolated sessions list: persist the intended pre-run model/provider on isolated cron session entries so `sessions_list` reflects payload/session model overrides even when runs fail before post-run telemetry persistence. (#21279) Thanks @altaywtf. - Web UI/Chat sessions: add a cron-session visibility toggle in the session selector, fix cron-key detection across `cron:*` and `agent:*:cron:*` formats, and localize the new control labels/tooltips. (#26976) Thanks @ianderrington. - Web UI/Cron jobs: add schedule-kind and last-run-status filters to the Jobs list, with reset control and client-side filtering over loaded results. (#9510) Thanks @guxu11. +- Web UI/Control UI WebSocket defaults: include normalized `gateway.controlUi.basePath` (or inferred nested route base path) in the default `gatewayUrl` so first-load dashboard connections work behind path-based reverse proxies. (#30228) Thanks @gittb. - Cron/One-shot reliability: retry transient one-shot failures with bounded backoff and configurable retry policy before disabling. (#24435) Thanks . - Gateway/Cron auditability: add gateway info logs for successful cron create, update, and remove operations. (#25090) Thanks . - Cron/Schedule errors: notify users when a job is auto-disabled after repeated schedule computation failures. (#29098) Thanks . diff --git a/ui/src/ui/storage.node.test.ts b/ui/src/ui/storage.node.test.ts new file mode 100644 index 00000000000..18b91c6a898 --- /dev/null +++ b/ui/src/ui/storage.node.test.ts @@ -0,0 +1,63 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +function createStorageMock(): Storage { + const store = new Map(); + return { + get length() { + return store.size; + }, + clear() { + store.clear(); + }, + getItem(key: string) { + return store.get(key) ?? null; + }, + key(index: number) { + return Array.from(store.keys())[index] ?? null; + }, + removeItem(key: string) { + store.delete(key); + }, + setItem(key: string, value: string) { + store.set(key, String(value)); + }, + }; +} + +describe("loadSettings default gateway URL derivation", () => { + beforeEach(() => { + vi.resetModules(); + vi.stubGlobal("localStorage", createStorageMock()); + vi.stubGlobal("navigator", { language: "en-US" } as Navigator); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it("uses configured base path and normalizes trailing slash", async () => { + vi.stubGlobal("location", { + protocol: "https:", + host: "gateway.example:8443", + pathname: "/ignored/path", + } as Location); + vi.stubGlobal("window", { __OPENCLAW_CONTROL_UI_BASE_PATH__: " /openclaw/ " } as Window & + typeof globalThis); + + const { loadSettings } = await import("./storage.ts"); + expect(loadSettings().gatewayUrl).toBe("wss://gateway.example:8443/openclaw"); + }); + + it("infers base path from nested pathname when configured base path is not set", async () => { + vi.stubGlobal("location", { + protocol: "http:", + host: "gateway.example:18789", + pathname: "/apps/openclaw/chat", + } as Location); + vi.stubGlobal("window", {} as Window & typeof globalThis); + + const { loadSettings } = await import("./storage.ts"); + expect(loadSettings().gatewayUrl).toBe("ws://gateway.example:18789/apps/openclaw"); + }); +});