/** * @file WorkspaceTree.test.tsx * @description Tests for the sidebar's workspace list: one flat row per * workspace in the stored order, a row expanding to its sessions (remembered), * a live session opening its pane and an ended one its transcript, the * workspace's OWN sessions only, a session's state dot, rename and archive, * selection * by URL, Alt+arrow reordering kept in localStorage, double-click rename, the * search, the row menu's items by kind and tool, the status dot, and the * user's own groups (menu, Alt+arrow past a group's edge, a folded group's * state dot, dragging a header to reorder, fold all). * @author Nguyễn Ngọc Trí Vĩ */ import { describe, it, expect, vi, beforeEach } from "vitest"; import { act, render, screen, waitFor, within, fireEvent } 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"; function lane(id: number, cwd: string, extra: Partial = {}): Lane { return { id, title: "", cwd, branch: null, kind: "adopted", tool: "claude", router_profile: null, source_repo: null, pipeline: "default", session_id: null, run_id: null, stage: "idle", stage_since: null, status: "idle", needs_action: null, liveness: "idle", pipeline_nodes: [], ...extra, } as unknown as Lane; } const session = (id: string, name: string) => ({ id, name, cwd: "/r", status: "completed", started_at: "2026-09-30T10:00:00Z" }) as Session; let lanes: Lane[] = []; // Archiving a live session asks through the themed confirm (App's host): yes. vi.mock("../../lib/confirm", () => ({ confirmDialog: vi.fn(async () => true), confirmDiscard: vi.fn(async () => true), })); vi.mock("../../lib/api", async (importOriginal) => { const actual = await importOriginal>(); return { ...actual, api: { lanes: { list: vi.fn(async () => ({ lanes, counts: {} })), update: vi.fn(async () => ({ lane: lanes[0] })), action: vi.fn(async () => ({ ok: true })), removeSessionWorktree: vi.fn(async () => ({ removed: true, branch: "x", branchKept: false, })), routerList: vi.fn(async () => ({ routers: [] })), }, sessions: { list: vi.fn(async () => ({ sessions: [session("s-live", "Live one"), session("s-old", "Old one")], })), patch: vi.fn(async () => ({ session: {} })), // A tab's session with no row yet (a bare tab): 404. get: vi.fn(async () => { throw Object.assign(new Error("not found"), { status: 404 }); }), }, run: { list: vi.fn(async () => ({ items: [{ id: "agenthub-lane-1-2", laneId: 1, status: "running", sessionId: "s-live" }], })), cwds: vi.fn(async () => ({ items: [] })), kill: vi.fn(async () => ({ ok: true })), }, aiTools: { list: vi.fn(async () => ({ tools: [], default: "claude" })), iconUrl: () => "", }, aiProxies: { list: vi.fn(async () => ({ items: [] })) }, }, }; }); // The Add modal reports the workspace it made (id 2 here); the tree does the rest. vi.mock("../lanes/AddLaneModal", () => ({ AddLaneModal: ({ onAdded, onClose }: { onAdded: (l: Lane) => void; onClose: () => void }) => (