Files
paseo/packages/app/src/stores/workspace-subagents-integration.test.ts
T
Mohamed Boudra 3eda7dd15b fix(app): subagents track display + archive flicker
- Bucket finished subagents as `done` instead of `attention`; drop
  "needs attention" segment from the section header — subagents are
  transient and don't carry an unread state worth surfacing.
- Gate the subagents selector on the existing pending-archive query so
  optimistic archives survive authoritative agent snapshots that wipe
  the local `archivedAt`. Last subagent archived now cleanly hides the
  whole track via the existing empty-rows guard.
2026-05-09 16:28:09 +07:00

223 lines
6.4 KiB
TypeScript

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: <T>(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<string, string>();
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<Agent> & Pick<Agent, "id">): 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"]);
});
});