Files
kandev/apps/web/lib/state/hydration/hydrator.test.ts
T
Carlos Florêncio b9eb6ae058 refactor(web): remove legacy settings migration (#1704)
* refactor(web): remove legacy settings migration

* fix(web): hydrate portable settings from boot state

* fix(web): preserve sidebar sync contracts

* fix(web): retain portable settings filters

* fix(web): retry sidebar settings writes

* fix(web): retain sidebar boot settings

* fix(websocket): avoid shutdown teardown leaks

* fix(boot): normalize sidebar task preferences

* fix(settings): preserve saved views during hydration

* fix(jira): avoid duplicate saved-view syncs

* fix(jira): preserve saved views on fetch failure

* fix(jira): retry saved-view hydration failures

* refactor(jira): simplify hydration request cleanup

* fix(websocket): close late clients after hub shutdown
2026-07-16 00:06:41 +01:00

288 lines
8.8 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { produce } from "immer";
import type { Draft } from "immer";
import { hydrateState, hydrateUI } from "./hydrator";
import { defaultUIState } from "@/lib/state/slices/ui/ui-slice";
import { defaultState, mergeInitialState } from "@/lib/state/default-state";
import type { AppState } from "@/lib/state/store";
function makeDraft(): AppState {
// hydrateUI only touches UI-slice fields; an empty object cast satisfies
// the rest without dragging the full AppState shape into this test.
return { ...defaultUIState } as unknown as AppState;
}
function makeAppDraft(): AppState {
return structuredClone(defaultState) as AppState;
}
describe("hydrateUI — quick chat name overlay", () => {
beforeEach(() => {
window.localStorage.clear();
});
it("overlays a locally-renamed name onto the SSR-provided session name", () => {
window.localStorage.setItem(
"kandev.quickChat.names",
JSON.stringify({ "sess-1": "My custom name" }),
);
const result = produce(makeDraft(), (draft: Draft<AppState>) => {
hydrateUI(draft, {
quickChat: {
isOpen: false,
activeSessionId: null,
sessions: [{ sessionId: "sess-1", workspaceId: "ws-1", name: "Agent A - Chat 1" }],
},
});
});
expect(result.quickChat.sessions[0].name).toBe("My custom name");
});
it("keeps the SSR-provided name when no local rename exists", () => {
const result = produce(makeDraft(), (draft: Draft<AppState>) => {
hydrateUI(draft, {
quickChat: {
isOpen: false,
activeSessionId: null,
sessions: [{ sessionId: "sess-2", workspaceId: "ws-1", name: "Agent A - Chat 1" }],
},
});
});
expect(result.quickChat.sessions[0].name).toBe("Agent A - Chat 1");
});
it("only overlays sessions that have a stored rename, leaving siblings untouched", () => {
window.localStorage.setItem(
"kandev.quickChat.names",
JSON.stringify({ "sess-a": "Renamed A" }),
);
const result = produce(makeDraft(), (draft: Draft<AppState>) => {
hydrateUI(draft, {
quickChat: {
isOpen: false,
activeSessionId: null,
sessions: [
{ sessionId: "sess-a", workspaceId: "ws-1", name: "Original A" },
{ sessionId: "sess-b", workspaceId: "ws-1", name: "Original B" },
],
},
});
});
expect(result.quickChat.sessions.map((s) => s.name)).toEqual(["Renamed A", "Original B"]);
});
it("clears stale quick chat sessions when the backend returns none", () => {
const result = produce(makeDraft(), (draft: Draft<AppState>) => {
draft.quickChat = {
isOpen: true,
activeSessionId: "stale-session",
sessions: [{ sessionId: "stale-session", workspaceId: "ws-1", name: "Stale" }],
};
hydrateUI(draft, {
quickChat: {
isOpen: false,
activeSessionId: null,
sessions: [],
},
});
});
expect(result.quickChat.sessions).toEqual([]);
expect(result.quickChat.isOpen).toBe(false);
expect(result.quickChat.activeSessionId).toBeNull();
});
});
describe("mergeInitialState — quick chat name overlay", () => {
beforeEach(() => {
window.localStorage.clear();
});
it("overlays locally-renamed names during boot payload merge", () => {
window.localStorage.setItem(
"kandev.quickChat.names",
JSON.stringify({ "sess-boot": "Local boot name" }),
);
const result = mergeInitialState({
quickChat: {
isOpen: false,
activeSessionId: null,
sessions: [
{ sessionId: "sess-boot", workspaceId: "ws-1", name: "Backend task title" },
{ sessionId: "sess-other", workspaceId: "ws-1", name: "Other title" },
],
},
});
expect(result.quickChat.sessions.map((s) => s.name)).toEqual([
"Local boot name",
"Other title",
]);
});
});
describe("mergeInitialState — sidebar views from boot settings", () => {
it("bridges backend sidebar settings into the UI slice before the store is created", () => {
const result = mergeInitialState({
userSettings: {
sidebarViews: [
{
id: "server",
name: "Server",
filters: [],
sort: { key: "state", direction: "asc" },
group: "none",
collapsedGroups: [],
},
],
sidebarActiveViewId: "server",
sidebarDraft: {
baseViewId: "server",
filters: [],
sort: { key: "updatedAt", direction: "desc" },
group: "workflow",
},
loaded: true,
},
} as unknown as Partial<AppState>);
expect(result.sidebarViews).toMatchObject({
views: [{ id: "server", name: "Server" }],
activeViewId: "server",
draft: { baseViewId: "server", group: "workflow" },
});
});
it("bridges backend task preferences into the UI slice before the store is created", () => {
const result = mergeInitialState({
userSettings: {
sidebarTaskPrefs: {
pinnedTaskIds: ["task-1"],
orderedTaskIds: ["task-2", "task-1"],
subtaskOrderByParentId: { "task-1": ["subtask-1"] },
},
loaded: true,
},
} as unknown as Partial<AppState>);
expect(result.sidebarTaskPrefs).toMatchObject({
pinnedTaskIds: ["task-1"],
orderedTaskIds: ["task-2", "task-1"],
subtaskOrderByParentId: { "task-1": ["subtask-1"] },
});
});
});
describe("hydrateState — sidebar views from user settings", () => {
it("hydrates active view and draft from backend user settings", () => {
const result = produce(makeAppDraft(), (draft: Draft<AppState>) => {
draft.sidebarViews.activeViewId = "local";
hydrateState(draft, {
userSettings: {
sidebarViews: [
{
id: "server",
name: "Server",
filters: [],
sort: { key: "state", direction: "asc" },
group: "none",
collapsedGroups: [],
},
],
sidebarActiveViewId: "server",
sidebarDraft: {
baseViewId: "server",
filters: [],
sort: { key: "updatedAt", direction: "desc" },
group: "workflow",
},
},
} as unknown as Partial<AppState>);
});
expect(result.sidebarViews.activeViewId).toBe("server");
expect(result.sidebarViews.draft).toEqual({
baseViewId: "server",
filters: [],
sort: { key: "updatedAt", direction: "desc" },
group: "workflow",
});
});
it("clears stale local draft when backend draft is null", () => {
const result = produce(makeAppDraft(), (draft: Draft<AppState>) => {
draft.sidebarViews.draft = {
baseViewId: "local",
filters: [],
sort: { key: "state", direction: "asc" },
group: "state",
};
hydrateState(draft, {
userSettings: {
sidebarDraft: null,
},
} as unknown as Partial<AppState>);
});
expect(result.sidebarViews.draft).toBeNull();
});
it("hydrates sidebar task prefs from backend, including explicit clears", () => {
const result = produce(makeAppDraft(), (draft: Draft<AppState>) => {
draft.sidebarTaskPrefs = {
pinnedTaskIds: ["local-pin"],
orderedTaskIds: ["local-order"],
subtaskOrderByParentId: { parent: ["child"] },
};
hydrateState(draft, {
userSettings: {
sidebarTaskPrefs: {
pinnedTaskIds: [],
orderedTaskIds: [],
subtaskOrderByParentId: {},
},
},
} as unknown as Partial<AppState>);
});
expect(result.sidebarTaskPrefs).toEqual({
pinnedTaskIds: [],
orderedTaskIds: [],
subtaskOrderByParentId: {},
});
});
it("uses backend sidebar task prefs as the authoritative hydrated value", () => {
const result = produce(makeAppDraft(), (draft: Draft<AppState>) => {
draft.sidebarTaskPrefs = {
pinnedTaskIds: ["local-pin"],
orderedTaskIds: ["local-order"],
subtaskOrderByParentId: { shared: ["local-child"], localOnly: ["child"] },
syncError: "retry",
};
hydrateState(draft, {
userSettings: {
sidebarTaskPrefs: {
pinnedTaskIds: ["server-pin"],
orderedTaskIds: ["server-order"],
subtaskOrderByParentId: { shared: ["server-child"], serverOnly: ["child"] },
},
},
} as unknown as Partial<AppState>);
});
expect(result.sidebarTaskPrefs).toEqual({
pinnedTaskIds: ["server-pin"],
orderedTaskIds: ["server-order"],
subtaskOrderByParentId: { shared: ["server-child"], serverOnly: ["child"] },
syncError: "retry",
});
});
});