diff --git a/canvas/src/components/__tests__/SidePanel.general.test.tsx b/canvas/src/components/__tests__/SidePanel.general.test.tsx
new file mode 100644
index 00000000..88710372
--- /dev/null
+++ b/canvas/src/components/__tests__/SidePanel.general.test.tsx
@@ -0,0 +1,390 @@
+// @vitest-environment jsdom
+/**
+ * Tests for SidePanel — general rendering and non-tab behaviors.
+ *
+ * Companion to SidePanel.tabs.test.tsx which covers tablist ARIA
+ * and localStorage width persistence.
+ *
+ * Covers:
+ * - Null when no node is selected
+ * - Null when selectedNodeId points to a missing node
+ * - Header: node name, role, tier badge
+ * - MetaPill capability summary pills
+ * - Resize handle: role=separator, aria-valuenow/min/max, aria-orientation
+ * - Resize handle: ArrowLeft/Right/Home/End keyboard nav
+ * - Needs-restart banner + Restart Now button
+ * - Current-task banner with pulsing dot
+ * - Footer shows workspace ID
+ * - Close button calls selectNode(null)
+ * - Tab switch via onClick fires setPanelTab
+ * - setSidePanelWidth called on mount
+ */
+import React from "react";
+import { render, screen, fireEvent, cleanup } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { SidePanel } from "../SidePanel";
+
+// ── Tab content stubs ───────────────────────────────────────────────────────
+vi.mock("../tabs/DetailsTab", () => ({ DetailsTab: () => null }));
+vi.mock("../tabs/SkillsTab", () => ({ SkillsTab: () => null }));
+vi.mock("../tabs/ChatTab", () => ({ ChatTab: () => null }));
+vi.mock("../tabs/ConfigTab", () => ({ ConfigTab: () => null }));
+vi.mock("../tabs/TerminalTab", () => ({ TerminalTab: () => null }));
+vi.mock("../tabs/FilesTab", () => ({ FilesTab: () => null }));
+vi.mock("../MemoryInspectorPanel", () => ({ MemoryInspectorPanel: () => null }));
+vi.mock("../tabs/TracesTab", () => ({ TracesTab: () => null }));
+vi.mock("../tabs/EventsTab", () => ({ EventsTab: () => null }));
+vi.mock("../tabs/ActivityTab", () => ({ ActivityTab: () => null }));
+vi.mock("../tabs/ScheduleTab", () => ({ ScheduleTab: () => null }));
+vi.mock("../tabs/ChannelsTab", () => ({ ChannelsTab: () => null }));
+vi.mock("../AuditTrailPanel", () => ({ AuditTrailPanel: () => null }));
+vi.mock("../StatusDot", () => ({ StatusDot: () => null }));
+vi.mock("../Tooltip", () => ({
+ Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}>,
+}));
+vi.mock("@/components/Toaster", () => ({ showToast: vi.fn() }));
+
+// ── Canvas store mock — mutable so each test can reconfigure ───────────────
+const mockSetPanelTab = vi.fn();
+const mockSelectNode = vi.fn();
+const mockSetSidePanelWidth = vi.fn();
+const mockRestartWorkspace = vi.fn().mockResolvedValue(undefined);
+
+const BASE_NODE = {
+ id: "ws-1",
+ data: {
+ name: "Test Workspace",
+ status: "online" as const,
+ tier: 2,
+ role: "Engineer",
+ parentId: null,
+ needsRestart: false,
+ currentTask: null,
+ agentCard: null,
+ },
+};
+
+// Mutable store state — tests reassign fields to test different states
+let storeState = {
+ selectedNodeId: "ws-1" as string | null,
+ panelTab: "chat",
+ setPanelTab: mockSetPanelTab,
+ selectNode: mockSelectNode,
+ setSidePanelWidth: mockSetSidePanelWidth,
+ nodes: [BASE_NODE],
+ restartWorkspace: mockRestartWorkspace,
+};
+
+vi.mock("@/store/canvas", () => ({
+ useCanvasStore: Object.assign(
+ vi.fn((selector: (s: typeof storeState) => unknown) => selector(storeState)),
+ { getState: () => storeState }
+ ),
+ summarizeWorkspaceCapabilities: () => ({ runtime: "claude-code", skillCount: 3 }),
+}));
+
+beforeEach(() => {
+ mockSetPanelTab.mockReset();
+ mockSelectNode.mockReset();
+ mockSetSidePanelWidth.mockReset();
+ mockRestartWorkspace.mockReset().mockResolvedValue(undefined);
+ localStorage.clear();
+ // Reset store state to default
+ storeState = {
+ selectedNodeId: "ws-1",
+ panelTab: "chat",
+ setPanelTab: mockSetPanelTab,
+ selectNode: mockSelectNode,
+ setSidePanelWidth: mockSetSidePanelWidth,
+ nodes: [BASE_NODE],
+ restartWorkspace: mockRestartWorkspace,
+ };
+});
+
+afterEach(() => {
+ cleanup();
+});
+
+// ─── Null guard ──────────────────────────────────────────────────────────────
+
+describe("SidePanel — null guard", () => {
+ it("returns null when selectedNodeId is null", () => {
+ storeState.selectedNodeId = null;
+ const { container } = render();
+ expect(container.firstChild).toBeNull();
+ });
+
+ it("returns null when selectedNodeId does not match any node", () => {
+ storeState.selectedNodeId = "nonexistent-ws";
+ storeState.nodes = [];
+ const { container } = render();
+ expect(container.firstChild).toBeNull();
+ });
+});
+
+// ─── Header ─────────────────────────────────────────────────────────────────
+
+describe("SidePanel — header", () => {
+ it("shows node name in heading", () => {
+ render();
+ expect(screen.getByRole("heading", { name: "Test Workspace" })).toBeTruthy();
+ });
+
+ it("shows node role", () => {
+ render();
+ expect(screen.getByText("Engineer")).toBeTruthy();
+ });
+
+ it("shows tier badge with correct value", () => {
+ render();
+ // T2 appears in header badge AND meta pill — confirm at least one
+ const all = screen.getAllByText("T2");
+ expect(all.length).toBeGreaterThanOrEqual(1);
+ });
+
+ it("close button is present with aria-label", () => {
+ render();
+ expect(screen.getByRole("button", { name: /close workspace panel/i })).toBeTruthy();
+ });
+
+ it("close button calls selectNode(null)", () => {
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /close workspace panel/i }));
+ expect(mockSelectNode).toHaveBeenCalledWith(null);
+ });
+});
+
+// ─── MetaPills ─────────────────────────────────────────────────────────────
+
+describe("SidePanel — meta pills", () => {
+ it("renders Tier, Runtime, Skills, and Status pills in the meta row", () => {
+ render();
+ // All four labels appear somewhere in the meta pills row
+ expect(screen.getByText(/tier/i)).toBeTruthy();
+ expect(screen.getByText(/runtime/i)).toBeTruthy();
+ expect(screen.getByText(/skills/i)).toBeTruthy();
+ expect(screen.getByText(/status/i)).toBeTruthy();
+ });
+
+ it("shows correct runtime value in meta pill", () => {
+ render();
+ expect(screen.getByText("claude-code")).toBeTruthy();
+ });
+
+ it("shows skill count in meta pill", () => {
+ render();
+ expect(screen.getByText("3")).toBeTruthy();
+ });
+});
+
+// ─── Resize handle ──────────────────────────────────────────────────────────
+
+describe("SidePanel — resize handle", () => {
+ it("has role=separator", () => {
+ render();
+ expect(screen.getByRole("separator")).toBeTruthy();
+ });
+
+ it("has aria-label='Resize workspace panel'", () => {
+ render();
+ expect(screen.getByRole("separator").getAttribute("aria-label")).toBe(
+ "Resize workspace panel"
+ );
+ });
+
+ it("has aria-valuenow=480 (default width)", () => {
+ render();
+ expect(screen.getByRole("separator").getAttribute("aria-valuenow")).toBe("480");
+ });
+
+ it("has aria-valuemin=320", () => {
+ render();
+ expect(screen.getByRole("separator").getAttribute("aria-valuemin")).toBe("320");
+ });
+
+ it("has aria-valuemax=800", () => {
+ render();
+ expect(screen.getByRole("separator").getAttribute("aria-valuemax")).toBe("800");
+ });
+
+ it("has aria-orientation=vertical", () => {
+ render();
+ expect(screen.getByRole("separator").getAttribute("aria-orientation")).toBe("vertical");
+ });
+
+ it("has tabIndex=0 (focusable)", () => {
+ render();
+ expect(screen.getByRole("separator").getAttribute("tabindex")).toBe("0");
+ });
+
+ it("ArrowLeft increases width by 16px (STEP — moves left edge rightward, widens panel)", () => {
+ render();
+ const sep = screen.getByRole("separator");
+ fireEvent.keyDown(sep, { key: "ArrowLeft" });
+ const panel = document.querySelector(".fixed") as HTMLElement;
+ expect(parseInt(panel.style.width, 10)).toBe(480 + 16); // widens
+ });
+
+ it("ArrowRight decreases width by 16px (STEP — moves left edge leftward, narrows panel)", () => {
+ render();
+ const sep = screen.getByRole("separator");
+ fireEvent.keyDown(sep, { key: "ArrowRight" });
+ const panel = document.querySelector(".fixed") as HTMLElement;
+ expect(parseInt(panel.style.width, 10)).toBe(480 - 16); // narrows
+ });
+
+ it("Home key sets width to MIN (320)", () => {
+ render();
+ fireEvent.keyDown(screen.getByRole("separator"), { key: "Home" });
+ const panel = document.querySelector(".fixed") as HTMLElement;
+ expect(parseInt(panel.style.width, 10)).toBe(320);
+ });
+
+ it("End key sets width to MAX (800)", () => {
+ render();
+ fireEvent.keyDown(screen.getByRole("separator"), { key: "End" });
+ const panel = document.querySelector(".fixed") as HTMLElement;
+ expect(parseInt(panel.style.width, 10)).toBe(800);
+ });
+
+ it("ArrowLeft persists new width to localStorage", () => {
+ render();
+ fireEvent.keyDown(screen.getByRole("separator"), { key: "ArrowLeft" });
+ expect(localStorage.getItem("molecule:sidepanel-width")).toBe(String(480 + 16));
+ });
+
+ it("Home persists new width to localStorage", () => {
+ render();
+ fireEvent.keyDown(screen.getByRole("separator"), { key: "Home" });
+ expect(localStorage.getItem("molecule:sidepanel-width")).toBe("320");
+ });
+});
+
+// ─── Needs-restart banner ────────────────────────────────────────────────────
+
+describe("SidePanel — needs-restart banner", () => {
+ it("shows banner when needsRestart=true and no currentTask", () => {
+ storeState.nodes = [{ ...BASE_NODE, data: { ...BASE_NODE.data, needsRestart: true, currentTask: null } }];
+ render();
+ expect(screen.getByText(/config changed/i)).toBeTruthy();
+ expect(screen.getByRole("button", { name: /restart now/i })).toBeTruthy();
+ });
+
+ it("does NOT show banner when needsRestart=false", () => {
+ render();
+ expect(screen.queryByText(/config changed/i)).toBeNull();
+ expect(screen.queryByRole("button", { name: /restart now/i })).toBeNull();
+ });
+
+ it("Restart Now button calls restartWorkspace(selectedNodeId)", () => {
+ storeState.nodes = [{ ...BASE_NODE, data: { ...BASE_NODE.data, needsRestart: true, currentTask: null } }];
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /restart now/i }));
+ expect(mockRestartWorkspace).toHaveBeenCalledWith("ws-1");
+ });
+});
+
+// ─── Current-task banner ────────────────────────────────────────────────────
+
+describe("SidePanel — current-task banner", () => {
+ it("shows banner when currentTask is set", () => {
+ storeState.nodes = [{ ...BASE_NODE, data: { ...BASE_NODE.data, currentTask: "Deploying bundle..." } }];
+ render();
+ expect(screen.getByText("Deploying bundle...")).toBeTruthy();
+ });
+
+ it("does NOT show banner when currentTask is null", () => {
+ render();
+ expect(screen.queryByText(/deploying bundle/i)).toBeNull();
+ });
+});
+
+// ─── Footer ─────────────────────────────────────────────────────────────────
+
+describe("SidePanel — footer", () => {
+ it("footer shows workspace ID in monospace font", () => {
+ render();
+ // ws-1 appears in the footer with font-mono class
+ expect(screen.getByText("ws-1")).toBeTruthy();
+ });
+});
+
+// ─── Tab switching ─────────────────────────────────────────────────────────
+
+describe("SidePanel — tab switching", () => {
+ it("clicking Details tab calls setPanelTab('details')", () => {
+ render();
+ fireEvent.click(screen.getByRole("tab", { name: /details/i }));
+ expect(mockSetPanelTab).toHaveBeenCalledWith("details");
+ });
+
+ it("clicking Plugins tab calls setPanelTab('skills')", () => {
+ render();
+ fireEvent.click(screen.getByRole("tab", { name: /plugins/i }));
+ expect(mockSetPanelTab).toHaveBeenCalledWith("skills");
+ });
+
+ it("clicking Terminal tab calls setPanelTab('terminal')", () => {
+ render();
+ fireEvent.click(screen.getByRole("tab", { name: /terminal/i }));
+ expect(mockSetPanelTab).toHaveBeenCalledWith("terminal");
+ });
+});
+
+// ─── setSidePanelWidth ─────────────────────────────────────────────────────
+
+describe("SidePanel — setSidePanelWidth side-effect", () => {
+ it("calls setSidePanelWidth with 480 (default width) on mount", () => {
+ render();
+ expect(mockSetSidePanelWidth).toHaveBeenCalledWith(480);
+ });
+
+ it("updates setSidePanelWidth after keyboard resize", () => {
+ render();
+ mockSetSidePanelWidth.mockClear();
+ fireEvent.keyDown(screen.getByRole("separator"), { key: "ArrowLeft" });
+ expect(mockSetSidePanelWidth).toHaveBeenCalledWith(480 + 16);
+ });
+});
+
+// ─── Width localStorage ────────────────────────────────────────────────────
+
+describe("SidePanel — width localStorage", () => {
+ it("does not persist default width to localStorage on initial mount (only on user resize)", () => {
+ render();
+ // localStorage is only written by the keyboard resize handler, not on mount
+ expect(localStorage.getItem("molecule:sidepanel-width")).toBeNull();
+ });
+
+ it("reads saved width from localStorage", () => {
+ localStorage.setItem("molecule:sidepanel-width", "600");
+ const { container } = render();
+ const panel = container.firstChild as HTMLElement;
+ expect(panel.style.width).toBe("600px");
+ });
+
+ it("caps saved width to default when below minimum", () => {
+ localStorage.setItem("molecule:sidepanel-width", "100");
+ const { container } = render();
+ const panel = container.firstChild as HTMLElement;
+ expect(panel.style.width).toBe("480px");
+ });
+});
+
+// ─── Offline status ─────────────────────────────────────────────────────────
+
+describe("SidePanel — offline status", () => {
+ it("shows tier badge even when node is offline", () => {
+ storeState.nodes = [{ ...BASE_NODE, data: { ...BASE_NODE.data, status: "offline" as const } }];
+ render();
+ // T2 appears in both header badge and meta pill — just confirm at least one exists
+ const all = screen.getAllByText("T2");
+ expect(all.length).toBeGreaterThanOrEqual(1);
+ });
+
+ it("shows 'offline' in the Status meta pill when node is offline", () => {
+ storeState.nodes = [{ ...BASE_NODE, data: { ...BASE_NODE.data, status: "offline" as const } }];
+ render();
+ expect(screen.getByText("offline")).toBeTruthy();
+ });
+});
diff --git a/canvas/src/components/__tests__/TemplatePalette.test.tsx b/canvas/src/components/__tests__/TemplatePalette.test.tsx
new file mode 100644
index 00000000..7a5ffd10
--- /dev/null
+++ b/canvas/src/components/__tests__/TemplatePalette.test.tsx
@@ -0,0 +1,260 @@
+// @vitest-environment jsdom
+/**
+ * Tests for TemplatePalette — the floating sidebar drawer.
+ *
+ * Covers:
+ * - Toggle button aria-label (open / closed)
+ * - Sidebar renders when open, hides when closed
+ * - Sidebar header: "Templates" heading, subtitle
+ * - Loading state
+ * - Empty state ("No templates found")
+ * - Template cards: name, description, tier badge, skill pills
+ * - Deploy button calls deploy()
+ * - Errors swallowed → empty state shown
+ * - setTemplatePaletteOpen called on open/close
+ * - OrgTemplatesSection rendered inside sidebar
+ * - Import Agent Folder button in footer
+ * - Refresh templates button in footer
+ */
+import React from "react";
+import { render, screen, fireEvent, cleanup, act, waitFor } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+// ── Hoisted mocks — vi.hoisted() so they're available when vi.mock runs ──────
+// IMPORTANT: use plain vi.fn() in the return object (NOT `const fn = vi.fn(); return { fn }`)
+const { mockDeploy, mockSetTemplatePaletteOpen, mockGet } = vi.hoisted(() => ({
+ mockDeploy: vi.fn(),
+ mockSetTemplatePaletteOpen: vi.fn(),
+ mockGet: vi.fn(),
+}));
+
+vi.mock("@/hooks/useTemplateDeploy", () => ({
+ useTemplateDeploy: () => ({
+ deploy: mockDeploy,
+ deploying: null,
+ error: null,
+ modal: null,
+ }),
+}));
+
+vi.mock("@/store/canvas", () => ({
+ useCanvasStore: vi.fn((selector: (s: { setTemplatePaletteOpen: typeof mockSetTemplatePaletteOpen }) => unknown) =>
+ selector({ setTemplatePaletteOpen: mockSetTemplatePaletteOpen })
+ ),
+}));
+
+vi.mock("@/lib/api", () => ({
+ api: { get: mockGet },
+}));
+
+vi.mock("../OrgImportPreflightModal", () => ({
+ OrgImportPreflightModal: () => null,
+}));
+
+vi.mock("../ConfirmDialog", () => ({
+ ConfirmDialog: () => null,
+}));
+
+vi.mock("../Spinner", () => ({
+ Spinner: () => ,
+}));
+
+vi.mock("../Toaster", () => ({ showToast: vi.fn() }));
+
+// ── Component import — after all mocks ──────────────────────────────────────
+import { TemplatePalette } from "../TemplatePalette";
+
+beforeEach(() => {
+ mockDeploy.mockReset();
+ mockSetTemplatePaletteOpen.mockReset();
+ mockGet.mockReset().mockResolvedValue([]);
+});
+
+afterEach(() => {
+ cleanup();
+});
+
+// ── Helpers ──────────────────────────────────────────────────────────────────
+
+async function flush() {
+ await act(async () => { await Promise.resolve(); });
+}
+
+const MOCK_TEMPLATES = [
+ {
+ id: "tmpl-1",
+ name: "Software Engineer",
+ description: "Best for writing code",
+ tier: 1,
+ skills: ["web-search", "read-file", "write-file"],
+ },
+ {
+ id: "tmpl-2",
+ name: "Researcher",
+ description: "Deep research agent",
+ tier: 2,
+ skills: [],
+ },
+];
+
+// ─── Toggle button ─────────────────────────────────────────────────────────
+
+describe("TemplatePalette — toggle button", () => {
+ it("has aria-label='Open template palette' when closed", () => {
+ render();
+ expect(screen.getByRole("button", { name: /open template palette/i })).toBeTruthy();
+ });
+
+ it("has aria-label='Close template palette' when open", async () => {
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /open template palette/i }));
+ await flush();
+ expect(screen.getByRole("button", { name: /close template palette/i })).toBeTruthy();
+ });
+
+ it("clicking toggle opens sidebar", async () => {
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /open template palette/i }));
+ await flush();
+ expect(screen.getByRole("heading", { name: "Templates" })).toBeTruthy();
+ });
+
+ it("clicking toggle again closes sidebar", async () => {
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /open template palette/i }));
+ await flush();
+ fireEvent.click(screen.getByRole("button", { name: /close template palette/i }));
+ await flush();
+ expect(screen.queryByRole("heading", { name: "Templates" })).toBeNull();
+ });
+
+ it("calls setTemplatePaletteOpen(true) when opened", async () => {
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /open template palette/i }));
+ await flush();
+ expect(mockSetTemplatePaletteOpen).toHaveBeenCalledWith(true);
+ });
+
+ it("calls setTemplatePaletteOpen(false) when closed", async () => {
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /open template palette/i }));
+ await flush();
+ mockSetTemplatePaletteOpen.mockClear();
+ fireEvent.click(screen.getByRole("button", { name: /close template palette/i }));
+ await flush();
+ expect(mockSetTemplatePaletteOpen).toHaveBeenCalledWith(false);
+ });
+});
+
+// ─── Sidebar content ───────────────────────────────────────────────────────
+
+describe("TemplatePalette — sidebar", () => {
+ async function openSidebar() {
+ fireEvent.click(screen.getByRole("button", { name: /open template palette/i }));
+ await flush();
+ }
+
+ it("shows 'Templates' heading", async () => {
+ render();
+ await openSidebar();
+ expect(screen.getByRole("heading", { name: "Templates" })).toBeTruthy();
+ });
+
+ it("shows subtitle 'Click to deploy a workspace'", async () => {
+ render();
+ await openSidebar();
+ expect(screen.getByText(/click to deploy a workspace/i)).toBeTruthy();
+ });
+
+ it("shows loading state", async () => {
+ mockGet.mockReturnValue(new Promise(() => {}));
+ render();
+ await openSidebar();
+ expect(screen.getByTestId("spinner")).toBeTruthy();
+ expect(screen.getByText(/loading/i)).toBeTruthy();
+ });
+
+ it("shows empty state when no templates", async () => {
+ mockGet.mockResolvedValue([]);
+ render();
+ await openSidebar();
+ expect(screen.getByText(/no templates found/i)).toBeTruthy();
+ });
+
+ it("renders template cards", async () => {
+ mockGet.mockResolvedValue(MOCK_TEMPLATES);
+ render();
+ await openSidebar();
+ expect(screen.getByText("Software Engineer")).toBeTruthy();
+ expect(screen.getByText("Researcher")).toBeTruthy();
+ });
+
+ it("shows template description", async () => {
+ mockGet.mockResolvedValue(MOCK_TEMPLATES);
+ render();
+ await openSidebar();
+ expect(screen.getByText(/best for writing code/i)).toBeTruthy();
+ });
+
+ it("shows tier badge on template card", async () => {
+ mockGet.mockResolvedValue(MOCK_TEMPLATES);
+ render();
+ await openSidebar();
+ // T1 appears in tier badge
+ expect(screen.getAllByText("T1").length).toBeGreaterThanOrEqual(1);
+ });
+
+ it("shows up to 3 skill pills", async () => {
+ mockGet.mockResolvedValue(MOCK_TEMPLATES);
+ render();
+ await openSidebar();
+ expect(screen.getByText("web-search")).toBeTruthy();
+ expect(screen.getByText("read-file")).toBeTruthy();
+ expect(screen.getByText("write-file")).toBeTruthy();
+ });
+
+ it("shows '+N more' when more than 3 skills", async () => {
+ mockGet.mockResolvedValue([
+ { id: "tmpl-many", name: "Full Stack", description: "", tier: 1, skills: ["a", "b", "c", "d", "e"] },
+ ]);
+ render();
+ await openSidebar();
+ expect(screen.getByText("+2")).toBeTruthy();
+ });
+
+ it("deploy button calls deploy(t)", async () => {
+ mockGet.mockResolvedValue(MOCK_TEMPLATES);
+ render();
+ await openSidebar();
+ const deployBtns = screen.getAllByRole("button", { name: /software engineer/i });
+ await act(async () => { deployBtns[0].click(); });
+ expect(mockDeploy).toHaveBeenCalledWith(MOCK_TEMPLATES[0]);
+ });
+
+ it("shows empty state when api.get rejects (error is swallowed)", async () => {
+ mockGet.mockRejectedValue(new Error("server error"));
+ render();
+ await openSidebar();
+ await waitFor(() => {
+ expect(screen.getByText(/no templates found/i)).toBeTruthy();
+ });
+ });
+
+ it("renders OrgTemplatesSection inside sidebar", async () => {
+ render();
+ await openSidebar();
+ expect(document.querySelector("[data-testid='org-templates-section']")).toBeTruthy();
+ });
+
+ it("renders Import Agent Folder button in footer", async () => {
+ render();
+ await openSidebar();
+ expect(screen.getByRole("button", { name: /import agent folder/i })).toBeTruthy();
+ });
+
+ it("renders Refresh templates button in footer", async () => {
+ render();
+ await openSidebar();
+ expect(screen.getByRole("button", { name: /^refresh templates$/i })).toBeTruthy();
+ });
+});