import { describe, it, expect } from "vitest"; import { sanitizeLayout } from "./dockview-layout-restore"; const VALID_COMPONENTS = new Set(["chat", "files", "shell", "git", "terminal"]); /** * Build a minimal valid SerializedDockview-shaped object — matches what * dockview's api.toJSON() produces for a 3-column layout (sidebar | center | right). */ function buildLayout(opts?: { centerSize?: number; sidebarSize?: number; rightSize?: number }) { return { grid: { root: { type: "branch" as const, size: 600, data: [ { type: "leaf" as const, size: opts?.sidebarSize ?? 350, data: { id: "g-sidebar", views: ["files"], activeView: "files" }, }, { type: "leaf" as const, size: opts?.centerSize ?? 800, data: { id: "g-center", views: ["chat"], activeView: "chat" }, }, { type: "leaf" as const, size: opts?.rightSize ?? 450, data: { id: "g-right", views: ["git", "shell"], activeView: "git" }, }, ], }, height: 600, width: 1600, orientation: "HORIZONTAL" as const, }, panels: { files: { id: "files", contentComponent: "files" }, chat: { id: "chat", contentComponent: "chat" }, git: { id: "git", contentComponent: "git" }, shell: { id: "shell", contentComponent: "shell" }, }, activeGroup: "g-center", }; } describe("sanitizeLayout - size validation", () => { it("returns the layout unchanged when all sizes are positive", () => { const layout = buildLayout(); const result = sanitizeLayout(layout, VALID_COMPONENTS); expect(result).not.toBeNull(); expect(result.grid.root.data).toHaveLength(3); }); it("returns null when a leaf node has size 0", () => { const layout = buildLayout({ centerSize: 0 }); const result = sanitizeLayout(layout, VALID_COMPONENTS); expect(result).toBeNull(); }); it("returns null when a leaf node has negative size", () => { const layout = buildLayout({ sidebarSize: -50 }); const result = sanitizeLayout(layout, VALID_COMPONENTS); expect(result).toBeNull(); }); it("returns null when a branch node has size 0", () => { const layout = buildLayout(); layout.grid.root.size = 0; const result = sanitizeLayout(layout, VALID_COMPONENTS); expect(result).toBeNull(); }); it("returns null when nested branch has invalid size", () => { const layout = { grid: { root: { type: "branch" as const, size: 800, data: [ { type: "leaf" as const, size: 350, data: { id: "g-sidebar", views: ["files"], activeView: "files" }, }, { type: "branch" as const, size: 0, data: [ { type: "leaf" as const, size: 800, data: { id: "g-center", views: ["chat"], activeView: "chat" }, }, ], }, ], }, height: 600, width: 1600, orientation: "HORIZONTAL" as const, }, panels: { files: { id: "files", contentComponent: "files" }, chat: { id: "chat", contentComponent: "chat" }, }, activeGroup: "g-center", }; const result = sanitizeLayout(layout, VALID_COMPONENTS); expect(result).toBeNull(); }); it("returns null when grid.width is 0", () => { const layout = buildLayout(); layout.grid.width = 0; const result = sanitizeLayout(layout, VALID_COMPONENTS); expect(result).toBeNull(); }); it("returns null when grid.height is 0", () => { const layout = buildLayout(); layout.grid.height = 0; const result = sanitizeLayout(layout, VALID_COMPONENTS); expect(result).toBeNull(); }); it("preserves existing component-validation behavior alongside size checks", () => { const layout = buildLayout(); // Add an unknown panel — sanitizer should remove it but keep the rest. layout.panels = { ...layout.panels, // @ts-expect-error - injecting an extra unknown panel unknown: { id: "unknown", contentComponent: "unknown-component" }, }; const result = sanitizeLayout(layout, VALID_COMPONENTS); expect(result).not.toBeNull(); expect(Object.keys(result.panels)).not.toContain("unknown"); }); }); describe("sanitizeLayout - session panel handling", () => { const SESSION_PANEL_ID = "session:abc-123"; function buildLayoutWithSession() { const layout = buildLayout(); layout.grid.root.data[1].data.views = ["chat", SESSION_PANEL_ID]; layout.panels = { ...layout.panels, // @ts-expect-error - injecting a session panel without a matching valid component [SESSION_PANEL_ID]: { id: SESSION_PANEL_ID, contentComponent: "session-panel" }, }; return layout; } it("keeps session:* panels by default (per-env restore)", () => { const result = sanitizeLayout(buildLayoutWithSession(), VALID_COMPONENTS); expect(result).not.toBeNull(); expect(Object.keys(result.panels)).toContain(SESSION_PANEL_ID); }); it("strips session:* panels when stripSessionPanels=true (global fallback)", () => { const result = sanitizeLayout(buildLayoutWithSession(), VALID_COMPONENTS, { stripSessionPanels: true, }); expect(result).not.toBeNull(); expect(Object.keys(result.panels)).not.toContain(SESSION_PANEL_ID); // The non-session panels stay so the rest of the layout is still usable. expect(Object.keys(result.panels)).toContain("chat"); expect(Object.keys(result.panels)).toContain("files"); }); it("strips session:* panels even when contentComponent is a valid component (e.g. 'chat')", () => { // dockview serializes panels added via api.addPanel({ component: "chat" }) // with contentComponent: "chat", which is in VALID_COMPONENTS. The strip // logic must key off the panel id, not the component name. const layout = buildLayout(); layout.grid.root.data[1].data.views = ["chat", SESSION_PANEL_ID]; layout.panels = { ...layout.panels, // @ts-expect-error - injecting a session panel with the chat component [SESSION_PANEL_ID]: { id: SESSION_PANEL_ID, contentComponent: "chat" }, }; const result = sanitizeLayout(layout, VALID_COMPONENTS, { stripSessionPanels: true }); expect(result).not.toBeNull(); expect(Object.keys(result.panels)).not.toContain(SESSION_PANEL_ID); expect(Object.keys(result.panels)).toContain("chat"); }); });