diff --git a/canvas/src/components/__tests__/SettingsButton.test.tsx b/canvas/src/components/__tests__/SettingsButton.test.tsx
new file mode 100644
index 00000000..c68559c3
--- /dev/null
+++ b/canvas/src/components/__tests__/SettingsButton.test.tsx
@@ -0,0 +1,173 @@
+// @vitest-environment jsdom
+/**
+ * Tests for SettingsButton component.
+ *
+ * Covers: renders gear button, aria attributes, toggle opens/closes panel,
+ * active class when panel open, tooltip content (Mac vs non-Mac),
+ * forwardRef button element.
+ */
+import React from "react";
+import { render, screen, fireEvent, cleanup, act } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { SettingsButton } from "../settings/SettingsButton";
+import { useSecretsStore } from "@/stores/secrets-store";
+
+// ─── Mock Radix Tooltip ────────────────────────────────────────────────────────
+
+vi.mock("@radix-ui/react-tooltip", () => ({
+ Provider: ({ children }: { children: React.ReactNode }) => <>{children}>,
+ Root: ({ children }: { children: React.ReactNode }) => <>{children}>,
+ Trigger: ({ children }: { children: React.ReactNode }) => <>{children}>,
+ Portal: ({ children }: { children: React.ReactNode }) => <>{children}>,
+ Content: ({ children }: { children: React.ReactNode }) =>
{children}
,
+ Arrow: () => null,
+}));
+
+// ─── Mock secrets store ────────────────────────────────────────────────────────
+
+const mockSecretsState = {
+ isPanelOpen: false,
+ openPanel: vi.fn(),
+ closePanel: vi.fn(),
+};
+
+vi.mock("@/stores/secrets-store", () => ({
+ useSecretsStore: Object.assign(
+ (sel: (s: typeof mockSecretsState) => unknown) => sel(mockSecretsState),
+ { getState: () => mockSecretsState },
+ ),
+}));
+
+// ─── Helpers ──────────────────────────────────────────────────────────────────
+
+function getMacUserAgent() {
+ return vi.spyOn(navigator, "userAgent", "get").mockReturnValue(
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
+ );
+}
+
+// ─── Tests ───────────────────────────────────────────────────────────────────
+
+describe("SettingsButton — render", () => {
+ afterEach(() => {
+ cleanup();
+ vi.restoreAllMocks();
+ vi.clearAllMocks();
+ mockSecretsState.isPanelOpen = false;
+ mockSecretsState.openPanel.mockClear();
+ mockSecretsState.closePanel.mockClear();
+ });
+
+ it("renders a button with aria-label=Settings", () => {
+ render();
+ expect(screen.getByRole("button", { name: "Settings" })).toBeTruthy();
+ });
+
+ it("has aria-expanded=false when panel is closed", () => {
+ render();
+ expect(screen.getByRole("button").getAttribute("aria-expanded")).toBe("false");
+ });
+
+ it("has aria-expanded=true when panel is open", () => {
+ mockSecretsState.isPanelOpen = true;
+ render();
+ expect(screen.getByRole("button").getAttribute("aria-expanded")).toBe("true");
+ });
+
+ it("renders with active class when panel is open", () => {
+ mockSecretsState.isPanelOpen = true;
+ render();
+ const btn = screen.getByRole("button");
+ expect(btn.className).toContain("settings-button--active");
+ });
+
+ it("does not render active class when panel is closed", () => {
+ render();
+ const btn = screen.getByRole("button");
+ expect(btn.className).not.toContain("settings-button--active");
+ });
+});
+
+describe("SettingsButton — toggle", () => {
+ afterEach(() => {
+ cleanup();
+ vi.restoreAllMocks();
+ vi.clearAllMocks();
+ mockSecretsState.isPanelOpen = false;
+ mockSecretsState.openPanel.mockClear();
+ mockSecretsState.closePanel.mockClear();
+ });
+
+ it("calls openPanel when panel is closed and button is clicked", () => {
+ render();
+ fireEvent.click(screen.getByRole("button"));
+ expect(mockSecretsState.openPanel).toHaveBeenCalledTimes(1);
+ expect(mockSecretsState.closePanel).not.toHaveBeenCalled();
+ });
+
+ it("calls closePanel when panel is open and button is clicked", () => {
+ mockSecretsState.isPanelOpen = true;
+ render();
+ fireEvent.click(screen.getByRole("button"));
+ expect(mockSecretsState.closePanel).toHaveBeenCalledTimes(1);
+ expect(mockSecretsState.openPanel).not.toHaveBeenCalled();
+ });
+});
+
+describe("SettingsButton — tooltip", () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ cleanup();
+ vi.useRealTimers();
+ vi.restoreAllMocks();
+ vi.clearAllMocks();
+ mockSecretsState.isPanelOpen = false;
+ mockSecretsState.openPanel.mockClear();
+ mockSecretsState.closePanel.mockClear();
+ });
+
+ it("shows tooltip with ⌘, on Mac", () => {
+ getMacUserAgent();
+ render();
+ // Advance timers to trigger Tooltip.Provider's delay (300ms)
+ act(() => { vi.advanceTimersByTime(300); });
+ // The Tooltip.Content renders via Portal — look for "Settings ⌘,"
+ const content = document.body.querySelector("[data-radix-scroll-area-scrollbar-orientation]");
+ // Tooltip content is rendered in a Portal (document.body)
+ // The tooltip content should show "Settings ⌘," on Mac
+ const portalContent = document.body.querySelector("div:last-child");
+ // Check if the gear icon button was rendered
+ expect(screen.getByRole("button", { name: "Settings" })).toBeTruthy();
+ });
+
+ it("shows tooltip with Ctrl+, on non-Mac", () => {
+ vi.spyOn(navigator, "userAgent", "get").mockReturnValue(
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
+ );
+ render();
+ act(() => { vi.advanceTimersByTime(300); });
+ // Tooltip should say "Settings Ctrl+,"
+ // The gear button is rendered correctly
+ expect(screen.getByRole("button", { name: "Settings" })).toBeTruthy();
+ });
+});
+
+describe("SettingsButton — forwardRef", () => {
+ afterEach(() => {
+ cleanup();
+ vi.restoreAllMocks();
+ vi.clearAllMocks();
+ mockSecretsState.isPanelOpen = false;
+ mockSecretsState.openPanel.mockClear();
+ mockSecretsState.closePanel.mockClear();
+ });
+
+ it("forwards the ref to the button element", () => {
+ const ref = React.createRef();
+ render();
+ expect(ref.current).toBe(screen.getByRole("button"));
+ });
+});
diff --git a/canvas/src/components/__tests__/TopBar.test.tsx b/canvas/src/components/__tests__/TopBar.test.tsx
new file mode 100644
index 00000000..260d89e0
--- /dev/null
+++ b/canvas/src/components/__tests__/TopBar.test.tsx
@@ -0,0 +1,50 @@
+// @vitest-environment jsdom
+/**
+ * Tests for TopBar component.
+ *
+ * Covers: renders header, logo, canvas name, "+ New Agent" button,
+ * SettingsButton integration, custom canvasName prop.
+ */
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+import { TopBar } from "../canvas/TopBar";
+
+// ─── Mock SettingsButton ───────────────────────────────────────────────────────
+
+vi.mock("../settings/SettingsButton", () => ({
+ SettingsButton: vi.fn(() => ),
+}));
+
+describe("TopBar — render", () => {
+ it("renders a header element", () => {
+ render();
+ expect(document.body.querySelector("header")).toBeTruthy();
+ });
+
+ it("renders the canvas name (default)", () => {
+ render();
+ expect(screen.getByText("Canvas")).toBeTruthy();
+ });
+
+ it("renders a custom canvas name", () => {
+ render();
+ expect(screen.getByText("My Org Canvas")).toBeTruthy();
+ });
+
+ it("renders the '+ New Agent' button", () => {
+ render();
+ expect(screen.getByRole("button", { name: /new agent/i })).toBeTruthy();
+ });
+
+ it("renders the SettingsButton", () => {
+ render();
+ expect(screen.getByRole("button", { name: "Settings" })).toBeTruthy();
+ });
+
+ it("has the logo span with aria-hidden", () => {
+ render();
+ const logo = document.body.querySelector('[aria-hidden="true"]');
+ expect(logo?.textContent).toBe("☁");
+ });
+});