Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]
// @vitest-environment node
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createStorageMock } from "../test-helpers/storage.ts";
import {
loadLocalUserIdentity,
loadSettings,
saveLocalUserIdentity,
saveSettings,
} from "./storage.ts";
import { normalizeImportedCustomTheme } from "./custom-theme.ts";
function setTestLocation(params: { protocol: string; host: string; pathname: string }) {
vi.stubGlobal("location", {
protocol: params.protocol,
host: params.host,
hostname: params.host.replace(/:\d+$/, ""),
pathname: params.pathname,
} as Location);
}
function setControlUiBasePath(value: string | undefined) {
if (typeof window === "undefined") {
vi.stubGlobal(
"window",
value == null
? ({} as Window & typeof globalThis)
: ({ __OPENCLAW_CONTROL_UI_BASE_PATH__: value } as Window & typeof globalThis),
);
return;
}
if (value == null) {
delete window.__OPENCLAW_CONTROL_UI_BASE_PATH__;
return;
}
Object.defineProperty(window, "__OPENCLAW_CONTROL_UI_BASE_PATH__", {
value,
writable: true,
configurable: true,
});
}
function expectedGatewayUrl(basePath: string): string {
const proto = location.protocol === "https:" ? "wss" : "ws";
return `${proto}://${location.host}${basePath}`;
}
function createCustomThemeFixture() {
return normalizeImportedCustomTheme(
{
name: "Light Green",
cssVars: {
theme: {
"font-sans": "Inter, system-ui, sans-serif",
"font-mono": "JetBrains Mono, monospace",
},
light: {
background: "oklch(0.98 0.01 120)",
foreground: "oklch(0.2 0.03 265)",
card: "oklch(1 0 0)",
"card-foreground": "oklch(0.2 0.03 265)",
popover: "oklch(1 0 0)",
"popover-foreground": "oklch(0.2 0.03 265)",
primary: "oklch(0.8 0.2 128)",
"primary-foreground": "oklch(0 0 0)",
secondary: "oklch(0.35 0.03 257)",
"secondary-foreground": "oklch(0.98 0.01 248)",
muted: "oklch(0.96 0.01 248)",
"muted-foreground": "oklch(0.55 0.04 257)",
accent: "oklch(0.98 0.02 155)",
"accent-foreground": "oklch(0.45 0.1 151)",
destructive: "oklch(0.64 0.2 25)",
"destructive-foreground": "oklch(1 0 0)",
border: "oklch(0.92 0.01 255)",
input: "oklch(0.92 0.01 255)",
ring: "oklch(0.8 0.2 128)",
},
dark: {
background: "oklch(0.12 0.04 265)",
foreground: "oklch(0.98 0.01 248)",
card: "oklch(0.2 0.04 266)",
"card-foreground": "oklch(0.98 0.01 248)",
popover: "oklch(0.2 0.04 266)",
"popover-foreground": "oklch(0.98 0.01 248)",
primary: "oklch(0.8 0.2 128)",
"primary-foreground": "oklch(0 0 0)",
secondary: "oklch(0.28 0.04 260)",
"secondary-foreground": "oklch(0.98 0.01 248)",
muted: "oklch(0.28 0.04 260)",
"muted-foreground": "oklch(0.71 0.03 257)",
accent: "oklch(0.39 0.09 152)",
"accent-foreground": "oklch(0.8 0.2 128)",
destructive: "oklch(0.44 0.16 27)",
"destructive-foreground": "oklch(1 0 0)",
border: "oklch(0.28 0.04 260)",
input: "oklch(0.28 0.04 260)",
ring: "oklch(0.8 0.2 128)",
},
},
},
{
sourceUrl: " https://tweakcn.com/themes/cmlhfpjhw000004l4f4ax3m7z",
themeId: "cmlhfpjhw000004l4f4ax3m7z",
},
);
}
describe("loadSettings default gateway URL derivation", () => {
beforeEach(() => {
vi.stubGlobal("localStorage", createStorageMock());
vi.stubGlobal("sessionStorage", createStorageMock());
vi.stubGlobal("navigator", { language: "en-US" } as Navigator);
localStorage.clear();
sessionStorage.clear();
setControlUiBasePath(undefined);
});
afterEach(() => {
vi.restoreAllMocks();
setControlUiBasePath(undefined);
vi.unstubAllGlobals();
});
it("uses configured base path and normalizes trailing slash", async () => {
setTestLocation({
protocol: "https:",
host: "gateway.example:8443",
pathname: "/ignored/path",
});
setControlUiBasePath(" /openclaw/ ");
expect(loadSettings().gatewayUrl).toBe(expectedGatewayUrl("/openclaw"));
});
it("infers base path from nested pathname when configured base path is not set", async () => {
setTestLocation({
protocol: "http:",
host: "gateway.example:18789",
pathname: "/apps/openclaw/chat",
});
expect(loadSettings().gatewayUrl).toBe(expectedGatewayUrl("/apps/openclaw"));
});
it("skips node sessionStorage accessors that warn without a storage file", async () => {
vi.unstubAllGlobals();
vi.stubGlobal("localStorage", createStorageMock());
vi.stubGlobal("navigator", { language: "en-US" } as Navigator);
setTestLocation({
protocol: "https:",
host: "gateway.example:8443",
pathname: "/",
});
setControlUiBasePath(undefined);
const warningSpy = vi.spyOn(process, "emitWarning").mockImplementation(() => undefined) ;
expect(loadSettings()).toMatchObject({
gatewayUrl: expectedGatewayUrl(""),
token: "",
});
expect(warningSpy).not.toHaveBeenCalledWith(
"`--localstorage-file` was provided without a valid path",
expect.anything(),
expect.anything(),
);
});
it("ignores and scrubs legacy persisted tokens", async () => {
setTestLocation({
protocol: "https:",
host: "gateway.example:8443",
pathname: "/",
});
sessionStorage.setItem("openclaw.control.token.v1", "legacy-session-token");
localStorage.setItem(
"openclaw.control.settings.v1",
JSON.stringify({
gatewayUrl: "wss://gateway.example:8443/openclaw",
token: "persisted-token",
sessionKey: "agent",
}),
);
expect(loadSettings()).toMatchObject({
gatewayUrl: "wss://gateway.example:8443/openclaw",
token: "",
sessionKey: "agent",
});
const scopedKey = "openclaw.control.settings.v1:wss://gateway.example:8443/openclaw";
expect(JSON.parse(localStorage.getItem(scopedKey) ?? "{}")).toEqual({
gatewayUrl: "wss://gateway.example:8443/openclaw",
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
borderRadius: 50,
sessionsByGateway: {
"wss://gateway.example:8443/openclaw": {
sessionKey: "agent",
lastActiveSessionKey: "agent",
},
},
});
expect(sessionStorage.length).toBe(0);
});
it("loads the current-tab token from sessionStorage", async () => {
setTestLocation({
protocol: "https:",
host: "gateway.example:8443",
pathname: "/",
});
const gwUrl = expectedGatewayUrl("");
saveSettings({
gatewayUrl: gwUrl,
token: "session-token",
sessionKey: "main",
lastActiveSessionKey: "main",
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
borderRadius: 50,
});
expect(loadSettings()).toMatchObject({
gatewayUrl: gwUrl,
token: "session-token",
});
});
it("does not reuse a session token for a different gatewayUrl", async () => {
setTestLocation({
protocol: "https:",
host: "gateway.example:8443",
pathname: "/",
});
const gwUrl = expectedGatewayUrl("");
const otherUrl = "wss://other-gateway.example:8443";
saveSettings({
gatewayUrl: gwUrl,
token: "gateway-a-token",
sessionKey: "main",
lastActiveSessionKey: "main",
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
borderRadius: 50,
});
saveSettings({
gatewayUrl: otherUrl,
token: "",
sessionKey: "main",
lastActiveSessionKey: "main",
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
borderRadius: 50,
});
expect(loadSettings()).toMatchObject({
gatewayUrl: gwUrl,
token: "gateway-a-token",
});
});
it("does not persist gateway tokens when saving settings", async () => {
setTestLocation({
protocol: "https:",
host: "gateway.example:8443",
pathname: "/",
});
const gwUrl = expectedGatewayUrl("");
saveSettings({
gatewayUrl: gwUrl,
token: "memory-only-token",
sessionKey: "main",
lastActiveSessionKey: "main",
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
borderRadius: 50,
});
expect(loadSettings()).toMatchObject({
gatewayUrl: gwUrl,
token: "memory-only-token",
});
const scopedKey = `openclaw.control.settings.v1:${gwUrl}`;
expect(JSON.parse(localStorage.getItem(scopedKey) ?? "{}")).toEqual({
gatewayUrl: gwUrl,
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
borderRadius: 50,
sessionsByGateway: {
[gwUrl]: {
sessionKey: "main",
lastActiveSessionKey: "main",
},
},
});
expect(sessionStorage.length).toBe(1);
});
it("clears the current-tab token when saving an empty token", async () => {
setTestLocation({
protocol: "https:",
host: "gateway.example:8443",
pathname: "/",
});
const gwUrl = expectedGatewayUrl("");
saveSettings({
gatewayUrl: gwUrl,
token: "stale-token",
sessionKey: "main",
lastActiveSessionKey: "main",
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
borderRadius: 50,
});
saveSettings({
gatewayUrl: gwUrl,
token: "",
sessionKey: "main",
lastActiveSessionKey: "main",
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
borderRadius: 50,
});
expect(loadSettings().token).toBe("");
expect(sessionStorage.length).toBe(0);
});
it("persists themeMode and navWidth alongside the selected theme", async () => {
setTestLocation({
protocol: "https:",
host: "gateway.example:8443",
pathname: "/",
});
const gwUrl = expectedGatewayUrl("");
saveSettings({
gatewayUrl: gwUrl,
token: "",
sessionKey: "main",
lastActiveSessionKey: "main",
theme: "dash",
themeMode: "light",
chatFocusMode: false,
chatShowThinking: true,
chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 320,
navGroupsCollapsed: {},
borderRadius: 50,
});
const scopedKey = `openclaw.control.settings.v1:${gwUrl}`;
expect(JSON.parse(localStorage.getItem(scopedKey) ?? "{}")).toMatchObject({
theme: "dash",
themeMode: "light",
navWidth: 320,
});
});
it("persists the browser-local custom theme payload when present", async () => {
setTestLocation({
protocol: "https:",
host: "gateway.example:8443",
pathname: "/",
});
const gwUrl = expectedGatewayUrl("");
const customTheme = createCustomThemeFixture();
saveSettings({
gatewayUrl: gwUrl,
token: "",
sessionKey: "main",
lastActiveSessionKey: "main",
theme: "custom",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
borderRadius: 50,
customTheme,
});
expect(loadSettings()).toMatchObject({
theme: "custom",
customTheme: {
label: "Light Green",
themeId: "cmlhfpjhw000004l4f4ax3m7z",
},
});
});
it("falls back to claw when persisted custom theme data is invalid", async () => {
setTestLocation({
protocol: "https:",
host: "gateway.example:8443",
pathname: "/",
});
const gwUrl = expectedGatewayUrl("");
localStorage.setItem(
`openclaw.control.settings.v1:${gwUrl}`,
JSON.stringify({
gatewayUrl: gwUrl,
theme: "custom",
themeMode: "dark",
chatFocusMode: false,
chatShowThinking: true,
chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
borderRadius: 50,
customTheme: {
sourceUrl: "https://tweakcn.com/themes/broken",
themeId: "broken",
label: "Broken",
importedAt: "2026-04-22T00:00:00.000Z",
light: {},
dark: {},
},
sessionsByGateway: {
[gwUrl]: {
sessionKey: "main",
lastActiveSessionKey: "main",
},
},
}),
);
expect(loadSettings()).toMatchObject({
theme: "claw",
themeMode: "dark",
});
});
it("scopes persisted session selection per gateway", async () => {
setTestLocation({
protocol: "https:",
host: "gateway-a.example:8443",
pathname: "/",
});
const gwUrl = expectedGatewayUrl("");
saveSettings({
gatewayUrl: gwUrl,
token: "",
sessionKey: "agent:test_old:main",
lastActiveSessionKey: "agent:test_old:main",
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
borderRadius: 50,
});
expect(loadSettings()).toMatchObject({
gatewayUrl: gwUrl,
sessionKey: "agent:test_old:main",
lastActiveSessionKey: "agent:test_old:main",
});
});
it("caps persisted session scopes to the most recent gateways", async () => {
setTestLocation({
protocol: "https:",
host: "gateway.example:8443",
pathname: "/",
});
const gwUrl = expectedGatewayUrl("");
const scopedKey = `openclaw.control.settings.v1:wss://gateway.example:8443`;
// Pre-seed sessionsByGateway with 11 stale gateway entries so the next
// saveSettings call pushes the total to 12 and triggers the cap (10).
const staleEntries: Record<string, { sessionKey: string; lastActiveSessionKey: string }> = {};
for (let i = 0; i < 11; i += 1) {
staleEntries[`wss://stale-${i}.example:8443`] = {
sessionKey: `agent:stale_${i}:main`,
lastActiveSessionKey: `agent:stale_${i}:main`,
};
}
localStorage.setItem(scopedKey, JSON.stringify({ sessionsByGateway: staleEntries }));
saveSettings({
gatewayUrl: gwUrl,
token: "",
sessionKey: "agent:current:main",
lastActiveSessionKey: "agent:current:main",
theme: "claw",
themeMode: "system",
chatFocusMode: false,
chatShowThinking: true,
chatShowToolCalls: true,
splitRatio: 0.6,
navCollapsed: false,
navWidth: 220,
navGroupsCollapsed: {},
borderRadius: 50,
});
const persisted = JSON.parse(localStorage.getItem(scopedKey) ?? "{}");
expect(persisted.sessionsByGateway).toBeDefined();
const scopes = Object.keys(persisted.sessionsByGateway);
expect(scopes).toHaveLength(10);
// oldest stale entries should be evicted
expect(scopes).not.toContain("wss://stale-0.example:8443");
expect(scopes).not.toContain("wss://stale-1.example:8443");
// newest stale entries and the current gateway should be retained
expect(scopes).toContain("wss://stale-10.example:8443");
expect(scopes).toContain("wss://gateway.example:8443");
expect(persisted.sessionsByGateway["wss://gateway.example:8443"]).toEqual({
sessionKey: "agent:current:main",
lastActiveSessionKey: "agent:current:main",
});
});
it("persists local user identity separately from gateway settings", async () => {
setTestLocation({
protocol: "https:",
host: "gateway.example:8443",
pathname: "/",
});
saveLocalUserIdentity({ name: "Buns", avatar: "" });
expect(loadLocalUserIdentity()).toEqual({
name: "Buns",
avatar: "",
});
expect(JSON.parse(localStorage.getItem("openclaw.control.user.v1") ?? "{}")).toEqual({
name: "Buns",
avatar: "",
});
});
it("normalizes invalid local user identity values on load", async () => {
localStorage.setItem(
"openclaw.control.user.v1",
JSON.stringify({
name: " ",
avatar: "https://example.com/avatar.png",
}),
);
expect(loadLocalUserIdentity()).toEqual({
name: null,
avatar: null,
});
});
it("removes the persisted local user identity when cleared", async () => {
saveLocalUserIdentity({ name: "Buns", avatar: "data:image/png;base64,AAA" });
saveLocalUserIdentity({ name: null, avatar: null });
expect(loadLocalUserIdentity()).toEqual({
name: null,
avatar: null,
});
expect(localStorage.getItem("openclaw.control.user.v1")).toBeNull();
});
});
¤ Dauer der Verarbeitung: 0.30 Sekunden
(vorverarbeitet am 2026-04-27)
¤
*© Formatika GbR, Deutschland
|
|