diff --git a/canvas/src/components/tabs/FilesTab/__tests__/FilesToolbar.test.tsx b/canvas/src/components/tabs/FilesTab/__tests__/FilesToolbar.test.tsx
new file mode 100644
index 00000000..cb23c95d
--- /dev/null
+++ b/canvas/src/components/tabs/FilesTab/__tests__/FilesToolbar.test.tsx
@@ -0,0 +1,349 @@
+// @vitest-environment jsdom
+/**
+ * Tests for FilesToolbar — the top-of-panel bar for the Files tab.
+ * Covers: directory select, file count, New/Upload/Clear (configs-only),
+ * Export, Refresh, and aria-labels.
+ */
+import React from "react";
+import { render, screen, fireEvent, cleanup } from "@testing-library/react";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { FilesToolbar } from "../FilesToolbar";
+
+afterEach(cleanup);
+
+describe("FilesToolbar", () => {
+ describe("renders base toolbar", () => {
+ it("renders the directory select with aria-label", () => {
+ render(
+
+ );
+ expect(
+ screen.getByRole("combobox", { name: /file root directory/i })
+ ).toBeTruthy();
+ });
+
+ it("renders the file count", () => {
+ render(
+
+ );
+ expect(screen.getByText("7 files")).toBeTruthy();
+ });
+
+ it("renders Export button", () => {
+ render(
+
+ );
+ expect(
+ screen.getByRole("button", { name: /download all files/i })
+ ).toBeTruthy();
+ });
+
+ it("renders Refresh button", () => {
+ render(
+
+ );
+ expect(screen.getByRole("button", { name: /refresh file list/i })).toBeTruthy();
+ });
+
+ it("renders 0 files when count is 0", () => {
+ render(
+
+ );
+ expect(screen.getByText("0 files")).toBeTruthy();
+ });
+ });
+
+ describe("configs-only buttons", () => {
+ it("shows New and Upload buttons when root is /configs", () => {
+ render(
+
+ );
+ expect(
+ screen.getByRole("button", { name: /create new file/i })
+ ).toBeTruthy();
+ expect(
+ screen.getByRole("button", { name: /upload folder/i })
+ ).toBeTruthy();
+ expect(screen.getByRole("button", { name: /delete all files/i })).toBeTruthy();
+ });
+
+ it("hides New and Upload when root is /workspace", () => {
+ render(
+
+ );
+ expect(
+ screen.queryByRole("button", { name: /create new file/i })
+ ).toBeNull();
+ expect(
+ screen.queryByRole("button", { name: /upload folder/i })
+ ).toBeNull();
+ expect(
+ screen.queryByRole("button", { name: /delete all files/i })
+ ).toBeNull();
+ // Export and Refresh are still present
+ expect(
+ screen.getByRole("button", { name: /download all files/i })
+ ).toBeTruthy();
+ });
+
+ it("hides New and Upload when root is /home", () => {
+ render(
+
+ );
+ expect(
+ screen.queryByRole("button", { name: /create new file/i })
+ ).toBeNull();
+ expect(
+ screen.queryByRole("button", { name: /upload folder/i })
+ ).toBeNull();
+ });
+
+ it("hides New and Upload when root is /plugins", () => {
+ render(
+
+ );
+ expect(
+ screen.queryByRole("button", { name: /create new file/i })
+ ).toBeNull();
+ expect(
+ screen.queryByRole("button", { name: /upload folder/i })
+ ).toBeNull();
+ });
+ });
+
+ describe("callbacks", () => {
+ it("calls setRoot when directory is changed", () => {
+ const setRoot = vi.fn();
+ render(
+
+ );
+ fireEvent.change(screen.getByRole("combobox"), {
+ target: { value: "/workspace" },
+ });
+ expect(setRoot).toHaveBeenCalledWith("/workspace");
+ });
+
+ it("calls onNewFile when New button is clicked", () => {
+ const onNewFile = vi.fn();
+ render(
+
+ );
+ fireEvent.click(screen.getByRole("button", { name: /create new file/i }));
+ expect(onNewFile).toHaveBeenCalledTimes(1);
+ });
+
+ it("calls onDownloadAll when Export button is clicked", () => {
+ const onDownloadAll = vi.fn();
+ render(
+
+ );
+ fireEvent.click(screen.getByRole("button", { name: /download all files/i }));
+ expect(onDownloadAll).toHaveBeenCalledTimes(1);
+ });
+
+ it("calls onClearAll when Clear button is clicked", () => {
+ const onClearAll = vi.fn();
+ render(
+
+ );
+ fireEvent.click(screen.getByRole("button", { name: /delete all files/i }));
+ expect(onClearAll).toHaveBeenCalledTimes(1);
+ });
+
+ it("calls onRefresh when Refresh button is clicked", () => {
+ const onRefresh = vi.fn();
+ render(
+
+ );
+ fireEvent.click(screen.getByRole("button", { name: /refresh file list/i }));
+ expect(onRefresh).toHaveBeenCalledTimes(1);
+ });
+
+ it("calls onUpload when the hidden file input changes", () => {
+ const onUpload = vi.fn();
+ render(
+
+ );
+ // Find the hidden file input
+ const fileInput = document.querySelector(
+ 'input[type="file"]'
+ ) as HTMLInputElement;
+ expect(fileInput).toBeTruthy();
+ expect(fileInput?.getAttribute("aria-label")).toBe("Upload folder files");
+ });
+ });
+
+ describe("a11y", () => {
+ it("all buttons have aria-label or accessible name", () => {
+ render(
+
+ );
+ // All buttons should be findable by role
+ const buttons = screen.getAllByRole("button");
+ for (const btn of buttons) {
+ expect(btn.getAttribute("aria-label") ?? btn.textContent).toBeTruthy();
+ }
+ });
+
+ it("directory select has aria-label", () => {
+ render(
+
+ );
+ const select = screen.getByRole("combobox");
+ expect(select.getAttribute("aria-label")).toBe("File root directory");
+ });
+ });
+});
diff --git a/canvas/src/components/tabs/FilesTab/__tests__/NotAvailablePanel.test.tsx b/canvas/src/components/tabs/FilesTab/__tests__/NotAvailablePanel.test.tsx
new file mode 100644
index 00000000..c670bb50
--- /dev/null
+++ b/canvas/src/components/tabs/FilesTab/__tests__/NotAvailablePanel.test.tsx
@@ -0,0 +1,101 @@
+// @vitest-environment jsdom
+/**
+ * Tests for NotAvailablePanel — the full-tab placeholder shown when a
+ * workspace's runtime doesn't own a platform-managed filesystem (today:
+ * runtime === "external"). Covers rendering, a11y, and runtime prop
+ * display.
+ */
+import React from "react";
+import { render, screen, cleanup } from "@testing-library/react";
+import { afterEach, describe, expect, it } from "vitest";
+import { NotAvailablePanel } from "../NotAvailablePanel";
+
+afterEach(cleanup);
+
+describe("NotAvailablePanel", () => {
+ describe("renders", () => {
+ it("renders the heading", () => {
+ render();
+ expect(screen.getByText("Files not available")).toBeTruthy();
+ });
+
+ it("renders the description text", () => {
+ render();
+ expect(
+ screen.getByText(/whose filesystem isn't owned by the platform/i)
+ ).toBeTruthy();
+ });
+
+ it("displays the runtime name in the description", () => {
+ render();
+ // The runtime name appears inside the paragraph
+ const para = screen.getByText(/whose filesystem isn't owned/i);
+ expect(para.textContent).toContain("aws-lambda");
+ });
+
+ it("renders the SVG folder icon with aria-hidden", () => {
+ render();
+ const svg = document.querySelector("svg");
+ expect(svg).toBeTruthy();
+ expect(svg?.getAttribute("aria-hidden")).toBe("true");
+ });
+
+ it("uses the provided runtime prop verbatim", () => {
+ render();
+ const monoRuntime = document.querySelector(".font-mono");
+ expect(monoRuntime?.textContent).toBe("cloud-run");
+ });
+
+ it("renders the 'Use the Chat tab' guidance text", () => {
+ render();
+ expect(screen.getByText(/Use the Chat tab/i)).toBeTruthy();
+ });
+
+ it("is contained in a full-height flex column", () => {
+ render();
+ const container = screen.getByText("Files not available").closest("div");
+ expect(container?.className).toContain("flex");
+ expect(container?.className).toContain("flex-col");
+ expect(container?.className).toContain("items-center");
+ expect(container?.className).toContain("justify-center");
+ expect(container?.className).toContain("h-full");
+ });
+ });
+
+ describe("a11y", () => {
+ it("heading is an h3", () => {
+ render();
+ expect(screen.getByRole("heading", { level: 3 })).toBeTruthy();
+ });
+
+ it("SVG icon has aria-hidden so screen readers skip it", () => {
+ render();
+ const svg = document.querySelector("svg");
+ expect(svg?.getAttribute("aria-hidden")).toBe("true");
+ });
+
+ it("description paragraph is present with descriptive text", () => {
+ render();
+ const paras = document.querySelectorAll("p");
+ expect(paras.length).toBeGreaterThan(0);
+ const text = Array.from(paras)
+ .map((p) => p.textContent)
+ .join(" ");
+ expect(text.toLowerCase()).toContain("runtime");
+ });
+ });
+
+ describe("props", () => {
+ it("renders with a short runtime name", () => {
+ render();
+ const monoRuntime = document.querySelector(".font-mono");
+ expect(monoRuntime?.textContent).toBe("ext");
+ });
+
+ it("renders with a complex runtime name", () => {
+ render();
+ const monoRuntime = document.querySelector(".font-mono");
+ expect(monoRuntime?.textContent).toBe("gcp-cloud-functions-v2");
+ });
+ });
+});