/** * @file WorkspaceTree.owners.test.tsx * @description Server mode, the sidebar workspace list for an analyst: every * user's workspaces, grouped under their owner (with their avatar, else an * initial; yours first, no owner last), * groups folding and remembered; another user's workspace is read only — no * menu, no new session, and a session opens its analysis page, never a pane. * @author Nguyễn Ngọc Trí Vĩ */ import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom"; import type { Lane, Session } from "../../lib/types"; const ME = { id: 2, username: "ana", role: "analyst" as const }; const lane = (id: number, owner: number | null, name: string | null, title: string) => ({ id, title, cwd: `/w/${id}`, kind: "adopted", tool: "claude", status: "idle", liveness: "idle", pipeline_nodes: [], owner_id: owner, owner_name: name, }) as unknown as Lane; const lanes = [ { ...lane(1, 3, "bob", "Bob app"), owner_display_name: "Bob Nguyen", owner_avatar_at: "t1", } as Lane, lane(2, null, null, "Old"), lane(3, ME.id, "ana", "Mine"), lane(4, 1, "admin", "Admin site"), ]; vi.mock("../AuthGate", () => ({ useAuth: () => ({ state: { mode: "server", user: ME } }), usePermissions: () => ({ isAdmin: false, seesAll: true, canEditLane: (l?: { owner_id?: number | null } | null) => l?.owner_id === ME.id, }), })); vi.mock("../../lib/api", async (importOriginal) => { const actual = await importOriginal>(); return { ...actual, api: { lanes: { list: vi.fn(async () => ({ lanes, counts: {} })) }, sessions: { list: vi.fn(async () => ({ sessions: [ { id: "s-1", name: "Bob's run", status: "completed", started_at: "2026-10-01" }, ] as Session[], total: 1, limit: 6, offset: 0, })), }, run: { list: vi.fn(async () => ({ items: [] })) }, users: { avatarUrl: (id: number, at?: string | null) => at ? `/api/users/${id}/avatar?v=${at}` : null, }, me: { avatarUrl: () => null }, aiTools: { list: vi.fn(async () => ({ tools: [], default: "claude" })), iconUrl: () => "" }, }, }; }); import { WorkspaceTree } from "../WorkspaceTree"; function Where() { const loc = useLocation(); return {loc.pathname + loc.search}; } function renderTree() { return render( } /> ); } beforeEach(() => localStorage.clear()); describe("WorkspaceTree for an analyst", () => { it("groups every workspace under its owner: yours first, no owner last", async () => { renderTree(); await screen.findByTestId("workspace-row-1"); const order = screen .getAllByTestId(/^workspace-(owner-\w+|row-\d+)$/) .map((el) => el.dataset.testid); expect(order).toEqual([ "workspace-owner-2", "workspace-row-3", "workspace-owner-1", "workspace-row-4", "workspace-owner-3", "workspace-row-1", "workspace-owner-none", "workspace-row-2", ]); expect(screen.getByTestId("workspace-owner-2")).toHaveTextContent("ana (you)"); }); it("names an owner by their display name when they set one", async () => { renderTree(); await screen.findByTestId("workspace-row-1"); expect(screen.getByTestId("workspace-owner-3")).toHaveTextContent("Bob Nguyen"); expect(screen.getByTestId("workspace-owner-3")).not.toHaveTextContent("bob"); // Bob set a picture: shown beside the name; Ana did not: her initial. expect( within(screen.getByTestId("workspace-owner-3")) .getByTestId("user-avatar-img") .getAttribute("src") ).toBe("/api/users/3/avatar?v=t1"); expect( within(screen.getByTestId("workspace-owner-2")).getByTestId("user-avatar-initials") ).toHaveTextContent("a"); expect(screen.getByTestId("workspace-owner-1")).toHaveTextContent("admin"); }); it("folds a group and remembers it", async () => { const user = userEvent.setup(); renderTree(); await user.click(await screen.findByTestId("workspace-owner-3")); expect(screen.queryByTestId("workspace-row-1")).toBeNull(); expect(screen.getByTestId("workspace-owner-3")).toHaveAttribute("aria-expanded", "false"); expect(JSON.parse(localStorage.getItem("agenthub.sidebar.projects")!).collapsedOwners).toEqual({ "3": true, }); }); it("keeps another user's workspace read only", async () => { const user = userEvent.setup(); renderTree(); await screen.findByTestId("workspace-row-1"); // Yours keeps its controls; theirs has none. expect(screen.getByTestId("workspace-menu-3")).toBeInTheDocument(); expect(screen.queryByTestId("workspace-menu-1")).toBeNull(); expect(screen.queryByTestId("workspace-new-1")).toBeNull(); // Picking it unfolds it instead of opening a workspace home. await user.click(screen.getByTestId("workspace-open-1")); expect(screen.getByTestId("where")).toHaveTextContent(/^\/$/); const list = await screen.findByTestId("workspace-sessions-1"); expect(within(list).queryByTestId("workspace-session-archive-s-1")).toBeNull(); await user.click(within(list).getByTestId("workspace-session-s-1")); expect(screen.getByTestId("where")).toHaveTextContent("/sessions/s-1"); }); });