import type { DaemonClient } from "@server/client/daemon-client"; import { afterEach, describe, expect, it, vi } from "vitest"; import { deriveWorkspaceAgentVisibility, type WorkspaceAgentVisibility, } from "@/screens/workspace/workspace-agent-visibility"; import { selectSubagentsForParent } from "@/subagents"; import { buildWorkspaceTabPersistenceKey, useWorkspaceLayoutStore } from "./workspace-layout-store"; import { useSessionStore, type Agent } from "./session-store"; vi.mock("lucide-react-native", () => ({ Archive: () => null, Check: () => null, ChevronDown: () => null, ChevronRight: () => null, })); vi.mock("react-native-unistyles", () => ({ StyleSheet: { create: (factory: unknown) => typeof factory === "function" ? factory({ spacing: { 0: 0, 1: 4, 2: 8, 3: 12, 4: 16 }, borderWidth: { 1: 1 }, borderRadius: { sm: 4, md: 6, lg: 8, "2xl": 16, full: 999 }, fontSize: { xs: 11, sm: 13, base: 15 }, fontWeight: { normal: "400", medium: "500" }, iconSize: { sm: 14, md: 18 }, colors: { foreground: "#fff", foregroundMuted: "#aaa", surface0: "#000", surface1: "#111", surface2: "#222", surface3: "#333", border: "#444", borderAccent: "#555", accent: "#0a84ff", palette: { amber: { 500: "#ffbf00", 700: "#aa8000" }, blue: { 500: "#0a84ff" }, red: { 500: "#ff453a" }, green: { 500: "#30d158" }, }, }, }) : factory, }, useUnistyles: () => ({ theme: { colors: {} } }), withUnistyles: (component: T) => component, })); vi.mock("@/components/provider-icons", () => ({ getProviderIcon: () => null, })); vi.mock("@/components/ui/tooltip", () => ({ Tooltip: ({ children }: { children: unknown }) => children, TooltipTrigger: ({ children }: { children: unknown }) => children, TooltipContent: () => null, })); vi.mock("@/screens/workspace/workspace-tab-presentation", () => ({ WorkspaceTabIcon: () => null, })); vi.mock("@react-native-async-storage/async-storage", () => { const storage = new Map(); return { default: { getItem: vi.fn(async (key: string) => storage.get(key) ?? null), setItem: vi.fn(async (key: string, value: string) => { storage.set(key, value); }), removeItem: vi.fn(async (key: string) => { storage.delete(key); }), }, }; }); const SERVER_ID = "server-1"; const WORKSPACE_ID = "ws-main"; const WORKSPACE_DIRECTORY = "/repo/worktree"; const AGENT_TIMESTAMP = new Date("2026-04-21T10:00:00.000Z"); const AGENT_DEFAULTS: Agent = { serverId: SERVER_ID, id: "agent", provider: "codex", status: "idle", createdAt: AGENT_TIMESTAMP, updatedAt: AGENT_TIMESTAMP, lastUserMessageAt: null, lastActivityAt: AGENT_TIMESTAMP, capabilities: { supportsStreaming: true, supportsSessionPersistence: true, supportsDynamicModes: true, supportsMcpServers: true, supportsReasoningStream: true, supportsToolInvocations: true, }, currentModeId: null, availableModes: [], pendingPermissions: [], persistence: null, runtimeInfo: undefined, lastUsage: undefined, lastError: null, title: "Agent", cwd: WORKSPACE_DIRECTORY, model: null, features: undefined, thinkingOptionId: undefined, requiresAttention: false, attentionReason: null, attentionTimestamp: null, archivedAt: null, parentAgentId: null, labels: {}, projectPlacement: null, }; function makeAgent(input: Partial & Pick): Agent { return { ...AGENT_DEFAULTS, ...input }; } function initializeAgents(agents: Agent[]): void { useSessionStore.getState().initializeSession(SERVER_ID, null as unknown as DaemonClient); useSessionStore .getState() .setAgents(SERVER_ID, new Map(agents.map((agent) => [agent.id, agent]))); } function appendAgent(agent: Agent): void { useSessionStore.getState().setAgents(SERVER_ID, (agents) => { const nextAgents = new Map(agents); nextAgents.set(agent.id, agent); return nextAgents; }); } function deriveVisibilityFromSession(): WorkspaceAgentVisibility { const sessionAgents = useSessionStore.getState().sessions[SERVER_ID]?.agents ?? new Map(); return deriveWorkspaceAgentVisibility({ sessionAgents, workspaceDirectory: WORKSPACE_DIRECTORY, }); } function reconcileWorkspaceTabs(workspaceKey: string, visibility: WorkspaceAgentVisibility): void { useWorkspaceLayoutStore.getState().reconcileTabs(workspaceKey, { agentsHydrated: true, terminalsHydrated: true, activeAgentIds: visibility.activeAgentIds, autoOpenAgentIds: visibility.autoOpenAgentIds, knownAgentIds: visibility.knownAgentIds, standaloneTerminalIds: [], hasActivePendingDraftCreate: false, }); } function getWorkspaceTabIds(workspaceKey: string): string[] { return useWorkspaceLayoutStore .getState() .getWorkspaceTabs(workspaceKey) .map((tab) => tab.tabId); } afterEach(() => { useSessionStore.getState().clearSession(SERVER_ID); useWorkspaceLayoutStore.setState({ layoutByWorkspace: {}, splitSizesByWorkspace: {}, pinnedAgentIdsByWorkspace: {}, hiddenAgentIdsByWorkspace: {}, }); }); describe("workspace subagents integration", () => { it("keeps a child ingested before its parent out of auto-tabs, then exposes it in the parent section", () => { const workspaceKey = buildWorkspaceTabPersistenceKey({ serverId: SERVER_ID, workspaceId: WORKSPACE_ID, }); expect(workspaceKey).toBeTruthy(); const child = makeAgent({ id: "child-agent", parentAgentId: "parent-agent", title: "Child agent", }); const parent = makeAgent({ id: "parent-agent", title: "Parent agent", }); initializeAgents([child]); reconcileWorkspaceTabs(workspaceKey!, deriveVisibilityFromSession()); expect(getWorkspaceTabIds(workspaceKey!)).toEqual([]); appendAgent(parent); reconcileWorkspaceTabs(workspaceKey!, deriveVisibilityFromSession()); expect(getWorkspaceTabIds(workspaceKey!)).toEqual(["agent_parent-agent"]); expect( selectSubagentsForParent( useSessionStore.getState(), { serverId: SERVER_ID, parentAgentId: "parent-agent", }, new Set(), ).map((row) => row.id), ).toEqual(["child-agent"]); }); });