Files
openclaw/ui/src/lib/connection-hints.node.test.ts
T
Peter Steinberger 2829fe107c improve(ui): retire the Overview page for a chat-first start screen (#104819)
* feat(ui): replace Overview page with Connection settings and sidebar attention chips

* feat(ui): open new-session drafts on the chat start screen hero

* refactor(ui): drop old overview-hints paths after rename

* chore(i18n): sync control-ui locale bundles for connection/palette/attention keys

* test(ui): expect attention slot above the sidebar update card

* chore(i18n): re-sync locale bundles after rebase onto main

* test(ui): stub sidebar-attention RPCs in app-sidebar unit tests

* test(ui): use a non-secret-shaped token fixture in connection view test

* refactor(ui): destructure gateway connection in connection settings draft

* refactor(ui): keep connection settings code out of secret-scanner shapes

* test(ui): expect Connection in the settings Connections group

* improve(ui): idle-refresh sidebar attention chips for always-visible windows

* chore(i18n): refresh raw-copy baseline after rebase

* docs(ui): note the unknown-route chat fallback covers retired paths
2026-07-11 18:11:14 -07:00

186 lines
5.5 KiB
TypeScript

// @vitest-environment node
import { afterEach, describe, expect, it, vi } from "vitest";
import { ConnectErrorDetailCodes } from "../../../packages/gateway-protocol/src/connect-error-details.js";
import { resolveGatewayTokenForUrlEdit } from "../app/settings.ts";
import { createStorageMock } from "../test-helpers/storage.ts";
import {
resolveAuthHintKind,
resolvePairingHint,
shouldShowInsecureContextHint,
} from "./connection-hints.ts";
afterEach(() => {
vi.unstubAllGlobals();
});
describe("resolveGatewayTokenForUrlEdit", () => {
it("preserves the current token for same normalized gateway endpoint edits", () => {
expect(
resolveGatewayTokenForUrlEdit(
"wss://gateway.example/openclaw",
" wss://gateway.example/openclaw/ ",
"abc123",
),
).toBe("abc123");
});
it("loads a scoped token when the normalized gateway endpoint changes", () => {
vi.stubGlobal("sessionStorage", createStorageMock());
sessionStorage.setItem(
"openclaw.control.token.v1:wss://other-gateway.example/openclaw",
"other-token",
);
expect(
resolveGatewayTokenForUrlEdit(
"wss://gateway.example/openclaw",
"wss://other-gateway.example/openclaw/",
"abc123",
),
).toBe("other-token");
});
it("clears the token when the changed gateway endpoint has no scoped token", () => {
vi.stubGlobal("sessionStorage", createStorageMock());
expect(
resolveGatewayTokenForUrlEdit(
"wss://gateway.example/openclaw",
"wss://other-gateway.example/openclaw",
"abc123",
),
).toBe("");
});
it("does not restore legacy durable tokens when the gateway endpoint changes", () => {
vi.stubGlobal("localStorage", createStorageMock());
vi.stubGlobal("sessionStorage", createStorageMock());
localStorage.setItem(
"openclaw.control.settings.v1",
JSON.stringify({
gatewayUrl: "wss://other-gateway.example/openclaw",
token: "gateway-token",
}),
);
expect(
resolveGatewayTokenForUrlEdit(
"wss://gateway.example/openclaw",
"wss://other-gateway.example/openclaw",
"abc123",
),
).toBe("");
});
});
describe("resolvePairingHint", () => {
it.each([
["close reason", "disconnected (1008): pairing required", undefined],
["case-insensitive close reason", "Pairing Required", undefined],
[
"structured pairing code",
"disconnected (4008): connect failed",
ConnectErrorDetailCodes.PAIRING_REQUIRED,
],
])("detects pairing required from %s", (_name, lastError, lastErrorCode) => {
expect(resolvePairingHint(false, lastError, lastErrorCode)).toEqual({
kind: "pairing-required",
requestId: null,
});
});
it.each([
["connected clients", true, "disconnected (1008): pairing required"],
["missing errors", false, null],
["unrelated errors", false, "disconnected (1006): no reason"],
["auth errors", false, "disconnected (4008): unauthorized"],
])("ignores %s", (_name, connected, lastError) => {
expect(resolvePairingHint(connected, lastError)).toBeNull();
});
it("detects scope-upgrade pending approval and keeps the request id", () => {
expect(
resolvePairingHint(
false,
"scope upgrade pending approval (requestId: req-123)",
ConnectErrorDetailCodes.PAIRING_REQUIRED,
),
).toEqual({
kind: "scope-upgrade-pending",
requestId: "req-123",
});
});
});
describe("resolveAuthHintKind", () => {
it("returns required for structured auth-required codes", () => {
expect(
resolveAuthHintKind({
connected: false,
lastError: "disconnected (4008): connect failed",
lastErrorCode: ConnectErrorDetailCodes.AUTH_TOKEN_MISSING,
hasToken: false,
hasPassword: false,
}),
).toBe("required");
});
it("returns failed for structured auth mismatch codes", () => {
expect(
resolveAuthHintKind({
connected: false,
lastError: "disconnected (4008): connect failed",
lastErrorCode: ConnectErrorDetailCodes.AUTH_TOKEN_MISMATCH,
hasToken: true,
hasPassword: false,
}),
).toBe("failed");
});
it("does not treat generic connect failures as auth failures", () => {
expect(
resolveAuthHintKind({
connected: false,
lastError: "disconnected (4008): connect failed",
lastErrorCode: ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED,
hasToken: true,
hasPassword: false,
}),
).toBeNull();
});
it("falls back to unauthorized string matching without structured codes", () => {
expect(
resolveAuthHintKind({
connected: false,
lastError: "disconnected (4008): unauthorized",
lastErrorCode: null,
hasToken: true,
hasPassword: false,
}),
).toBe("failed");
});
});
describe("shouldShowInsecureContextHint", () => {
it("returns true for browser WebSocket security errors", () => {
expect(
shouldShowInsecureContextHint(
false,
"Browser refused the Gateway WebSocket for security reasons.",
"BROWSER_WEBSOCKET_SECURITY_ERROR",
),
).toBe(true);
});
it("does not treat generic WebSocket constructor errors as insecure context", () => {
expect(
shouldShowInsecureContextHint(
false,
"Could not create the Gateway WebSocket: constructor failed",
"BROWSER_WEBSOCKET_CONSTRUCTOR_ERROR",
),
).toBe(false);
});
});