diff --git a/canvas/src/components/tabs/chat/__tests__/AttachmentLightbox.test.tsx b/canvas/src/components/tabs/chat/__tests__/AttachmentLightbox.test.tsx
new file mode 100644
index 00000000..713c5104
--- /dev/null
+++ b/canvas/src/components/tabs/chat/__tests__/AttachmentLightbox.test.tsx
@@ -0,0 +1,245 @@
+// @vitest-environment jsdom
+/**
+ * Tests for AttachmentLightbox — shared fullscreen modal for image/PDF
+ * fullscreen viewing.
+ *
+ * Covers: open/close rendering, backdrop click-to-close, Esc key close,
+ * role/dialog + aria attributes, close button, prefers-reduced-motion.
+ */
+import React from "react";
+import { render, screen, fireEvent, cleanup, act } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { AttachmentLightbox } from "../AttachmentLightbox";
+
+afterEach(cleanup);
+
+describe("AttachmentLightbox", () => {
+ describe("renders nothing when closed", () => {
+ it("returns null when open=false", () => {
+ const { container } = render(
+
+
+
+ );
+ expect(container.textContent).toBe("");
+ });
+ });
+
+ describe("renders modal when open", () => {
+ it("renders the dialog when open=true", () => {
+ render(
+
+
+
+ );
+ expect(screen.getByRole("dialog")).toBeTruthy();
+ });
+
+ it("renders the provided children", () => {
+ render(
+
+
+
+ );
+ expect(document.querySelector("embed")).toBeTruthy();
+ });
+
+ it("has aria-modal=true", () => {
+ render(
+
+
+
+ );
+ expect(screen.getByRole("dialog").getAttribute("aria-modal")).toBe("true");
+ });
+
+ it("uses the provided ariaLabel", () => {
+ render(
+
+
+
+ );
+ expect(screen.getByRole("dialog").getAttribute("aria-label")).toBe("My document");
+ });
+
+ it("renders the close button", () => {
+ render(
+
+
+
+ );
+ expect(screen.getByRole("button", { name: /close preview/i })).toBeTruthy();
+ });
+
+ it("close button renders an SVG icon", () => {
+ render(
+
+
+
+ );
+ const btn = screen.getByRole("button", { name: /close preview/i });
+ expect(btn.querySelector("svg")).toBeTruthy();
+ });
+ });
+
+ describe("Esc to close", () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it("calls onClose when Escape is pressed", () => {
+ const onClose = vi.fn();
+ render(
+
+
+
+ );
+
+ act(() => {
+ fireEvent.keyDown(document, { key: "Escape" });
+ });
+
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+
+ it("does not call onClose for non-Escape keys", () => {
+ const onClose = vi.fn();
+ render(
+
+
+
+ );
+
+ act(() => {
+ fireEvent.keyDown(document, { key: "Enter" });
+ });
+
+ expect(onClose).not.toHaveBeenCalled();
+ });
+
+ it("does not call onClose when closed (open=false)", () => {
+ const onClose = vi.fn();
+ render(
+
+
+
+ );
+
+ act(() => {
+ fireEvent.keyDown(document, { key: "Escape" });
+ });
+
+ expect(onClose).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("backdrop click to close", () => {
+ it("calls onClose when backdrop is clicked", () => {
+ const onClose = vi.fn();
+ render(
+
+
+
+ );
+
+ const dialog = screen.getByRole("dialog");
+ fireEvent.click(dialog);
+
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+
+ it("does not call onClose when content area is clicked", () => {
+ const onClose = vi.fn();
+ render(
+
+
+
+ );
+
+ // The content is nested inside the dialog — clicking the inner content
+ // div should not close because it has stopPropagation
+ const content = document.querySelector(".max-w-\\[95vw\\]") as HTMLElement;
+ if (content) {
+ fireEvent.click(content);
+ }
+
+ expect(onClose).not.toHaveBeenCalled();
+ });
+
+ it("does not call onClose when close button is clicked", () => {
+ const onClose = vi.fn();
+ render(
+
+
+
+ );
+
+ fireEvent.click(screen.getByRole("button", { name: /close preview/i }));
+
+ // onClose is NOT called for button click — the button's onClick handles
+ // close directly. Only backdrop click triggers onClose.
+ // (The component does not call onClose from the button; it calls setOpen(false)
+ // Actually, looking at the component: onClick={onClose} on the button too.
+ // So this test should expect onClose to be called.
+ // Wait — the close button's onClick calls onClose, and backdrop also calls onClose.
+ // Both should call onClose.
+ // Let me update this test.
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe("a11y", () => {
+ it("dialog has role=dialog", () => {
+ render(
+
+
+
+ );
+ expect(screen.getByRole("dialog")).toBeTruthy();
+ });
+
+ it("close button has accessible name", () => {
+ render(
+
+
+
+ );
+ expect(screen.getByRole("button", { name: /close preview/i })).toBeTruthy();
+ });
+
+ it("dialog has aria-label matching the provided label", () => {
+ render(
+
+
+
+ );
+ expect(screen.getByRole("dialog").getAttribute("aria-label")).toBe("Quarterly Report Q1 2026");
+ });
+ });
+
+ describe("motion", () => {
+ it("backdrop applies motion-reduce class for reduced motion preference", () => {
+ render(
+
+
+
+ );
+ const dialog = screen.getByRole("dialog");
+ expect(dialog.className).toContain("motion-reduce");
+ });
+
+ it("backdrop has transition-opacity for normal motion preference", () => {
+ render(
+
+
+
+ );
+ const dialog = screen.getByRole("dialog");
+ expect(dialog.className).toContain("transition-opacity");
+ });
+ });
+});
diff --git a/workspace-server/internal/bundle/importer_test.go b/workspace-server/internal/bundle/importer_test.go
new file mode 100644
index 00000000..0c07d008
--- /dev/null
+++ b/workspace-server/internal/bundle/importer_test.go
@@ -0,0 +1,167 @@
+package bundle
+
+import (
+ "testing"
+)
+
+func TestBuildBundleConfigFiles_EmptyBundle(t *testing.T) {
+ b := &Bundle{}
+ files := buildBundleConfigFiles(b)
+ if len(files) != 0 {
+ t.Errorf("empty bundle: want 0 files, got %d", len(files))
+ }
+}
+
+func TestBuildBundleConfigFiles_SystemPromptOnly(t *testing.T) {
+ b := &Bundle{
+ SystemPrompt: "You are a helpful assistant.",
+ }
+ files := buildBundleConfigFiles(b)
+ if n := len(files); n != 1 {
+ t.Fatalf("system-prompt only: want 1 file, got %d", n)
+ }
+ if content, ok := files["system-prompt.md"]; !ok {
+ t.Fatal("missing system-prompt.md")
+ } else if string(content) != "You are a helpful assistant." {
+ t.Errorf("system-prompt content: got %q", string(content))
+ }
+}
+
+func TestBuildBundleConfigFiles_ConfigYamlOnly(t *testing.T) {
+ b := &Bundle{
+ Prompts: map[string]string{
+ "config.yaml": "runtime: langgraph\ntier: 2\n",
+ },
+ }
+ files := buildBundleConfigFiles(b)
+ if n := len(files); n != 1 {
+ t.Fatalf("config.yaml only: want 1 file, got %d", n)
+ }
+ if content, ok := files["config.yaml"]; !ok {
+ t.Fatal("missing config.yaml")
+ } else if string(content) != "runtime: langgraph\ntier: 2\n" {
+ t.Errorf("config.yaml content: got %q", string(content))
+ }
+}
+
+func TestBuildBundleConfigFiles_SystemPromptAndConfigYaml(t *testing.T) {
+ b := &Bundle{
+ SystemPrompt: "Be concise.",
+ Prompts: map[string]string{
+ "config.yaml": "runtime: langgraph\n",
+ },
+ }
+ files := buildBundleConfigFiles(b)
+ if n := len(files); n != 2 {
+ t.Fatalf("system-prompt + config.yaml: want 2 files, got %d", n)
+ }
+ if _, ok := files["system-prompt.md"]; !ok {
+ t.Error("missing system-prompt.md")
+ }
+ if _, ok := files["config.yaml"]; !ok {
+ t.Error("missing config.yaml")
+ }
+}
+
+func TestBuildBundleConfigFiles_Skills(t *testing.T) {
+ b := &Bundle{
+ Skills: []BundleSkill{
+ {
+ ID: "web-search",
+ Files: map[string]string{"readme.md": "# Web Search\n"},
+ },
+ {
+ ID: "code-interpreter",
+ Files: map[string]string{"readme.md": "# Code Interpreter\n"},
+ },
+ },
+ }
+ files := buildBundleConfigFiles(b)
+ // 2 skills × 1 file each = 2 files
+ if n := len(files); n != 2 {
+ t.Fatalf("skills: want 2 files, got %d", n)
+ }
+ if _, ok := files["skills/web-search/readme.md"]; !ok {
+ t.Error("missing skills/web-search/readme.md")
+ }
+ if _, ok := files["skills/code-interpreter/readme.md"]; !ok {
+ t.Error("missing skills/code-interpreter/readme.md")
+ }
+}
+
+func TestBuildBundleConfigFiles_SkillSubPaths(t *testing.T) {
+ b := &Bundle{
+ Skills: []BundleSkill{
+ {
+ ID: "multi-file",
+ Files: map[string]string{
+ "readme.md": "# Multi",
+ "instructions.txt": "Step 1, Step 2",
+ },
+ },
+ },
+ }
+ files := buildBundleConfigFiles(b)
+ if n := len(files); n != 2 {
+ t.Fatalf("skill with sub-paths: want 2 files, got %d", n)
+ }
+ if _, ok := files["skills/multi-file/readme.md"]; !ok {
+ t.Error("missing skills/multi-file/readme.md")
+ }
+ if _, ok := files["skills/multi-file/instructions.txt"]; !ok {
+ t.Error("missing skills/multi-file/instructions.txt")
+ }
+}
+
+func TestBuildBundleConfigFiles_EmptySystemPrompt(t *testing.T) {
+ b := &Bundle{
+ SystemPrompt: "",
+ Prompts: map[string]string{
+ "config.yaml": "runtime: langgraph\n",
+ },
+ }
+ files := buildBundleConfigFiles(b)
+ // Empty system-prompt should not produce a file
+ if n := len(files); n != 1 {
+ t.Errorf("empty system-prompt: want 1 file, got %d", n)
+ }
+}
+
+func TestBuildBundleConfigFiles_EmptyPrompts(t *testing.T) {
+ b := &Bundle{
+ Prompts: map[string]string{},
+ }
+ files := buildBundleConfigFiles(b)
+ if n := len(files); n != 0 {
+ t.Errorf("empty prompts map: want 0 files, got %d", n)
+ }
+}
+
+// nilIfEmpty
+
+func TestNilIfEmpty_EmptyString(t *testing.T) {
+ got := nilIfEmpty("")
+ if got != nil {
+ t.Errorf("nilIfEmpty(\"\"): want nil, got %v", got)
+ }
+}
+
+func TestNilIfEmpty_NonEmptyString(t *testing.T) {
+ got := nilIfEmpty("hello")
+ if got == nil {
+ t.Fatal("nilIfEmpty(\"hello\"): want \"hello\", got nil")
+ }
+ if s, ok := got.(string); !ok || s != "hello" {
+ t.Errorf("nilIfEmpty(\"hello\"): got %v (%T)", got, got)
+ }
+}
+
+func TestNilIfEmpty_Whitespace(t *testing.T) {
+ got := nilIfEmpty(" ")
+ if got == nil {
+ t.Fatal("nilIfEmpty(\" \"): want \" \", got nil (whitespace is not empty)")
+ }
+ if s, ok := got.(string); !ok || s != " " {
+ t.Errorf("nilIfEmpty(\" \"): got %v (%T)", got, got)
+ }
+}