feat(01-01): create Zustand store with 6 slices, IPC batcher, and unit tests

- Install zustand 5.x and immer 11.x dependencies
- Create 6 Zustand slices: navigation, company, selection, ui, communication, social
- Create combined store with devtools + immer middleware
- Create IPC event batcher with 50ms debounce window (schedule/flush/dispose)
- Create typed selector hooks for narrow state subscriptions
- Add 14 unit tests covering store initialization, slice behaviors, and batcher timing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 4.6 committed 2026-03-22 01:30:37 -04:00
1 parent a4de64b876
commit 5e9d36c058
13 files changed
+818 -4

No files matched your search

+6 -4
View File
@@ -1,6 +1,6 @@
{
"name": "agent-company",
"version": "0.4.52",
"version": "0.4.53",
"private": true,
"type": "module",
"description": "Cross-platform Electron desktop control plane for local-first AI agent companies.",
@@ -26,29 +26,31 @@
"postinstall": "node scripts/postinstall.mjs"
},
"dependencies": {
"@fontsource-variable/jetbrains-mono": "5.2.8",
"@agentcompany/adapter-claude-local": "workspace:*",
"@agentcompany/adapter-codex-local": "workspace:*",
"@agentcompany/adapter-gemini-local": "workspace:*",
"@agentcompany/adapter-utils": "workspace:*",
"@fontsource-variable/jetbrains-mono": "5.2.8",
"@phosphor-icons/react": "2.1.10",
"clsx": "2.1.1",
"electron-log": "5.4.3",
"electron-updater": "6.8.3",
"framer-motion": "12.36.0",
"immer": "^11.1.4",
"react": "19.2.4",
"react-dom": "19.2.4",
"tailwind-merge": "3.5.0",
"zod": "4.3.6"
"zod": "4.3.6",
"zustand": "^5.0.12"
},
"devDependencies": {
"@eslint/js": "9.38.0",
"@playwright/test": "1.58.2",
"@tailwindcss/vite": "4.2.1",
"@types/node": "^25.2.3",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.0.0",
"@eslint/js": "9.38.0",
"electron": "39.8.0",
"electron-builder": "26.8.1",
"electron-vite": "5.0.0",
+35
View File
@@ -38,6 +38,9 @@ importers:
framer-motion:
specifier: 12.36.0
version: 12.36.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
immer:
specifier: ^11.1.4
version: 11.1.4
react:
specifier: 19.2.4
version: 19.2.4
@@ -50,6 +53,9 @@ importers:
zod:
specifier: 4.3.6
version: 4.3.6
zustand:
specifier: ^5.0.12
version: 5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4)
devDependencies:
'@eslint/js':
specifier: 9.38.0
@@ -1928,6 +1934,9 @@ packages:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'}
immer@11.1.4:
resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==}
import-fresh@3.3.1:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
@@ -2916,6 +2925,24 @@ packages:
zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
zustand@5.0.12:
resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=18.0.0'
immer: '>=9.0.6'
react: '>=18.0.0'
use-sync-external-store: '>=1.2.0'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
use-sync-external-store:
optional: true
snapshots:
7zip-bin@5.2.0: {}
@@ -4783,6 +4810,8 @@ snapshots:
ignore@7.0.5: {}
immer@11.1.4: {}
import-fresh@3.3.1:
dependencies:
parent-module: 1.0.1
@@ -5676,3 +5705,9 @@ snapshots:
zod: 4.3.6
zod@4.3.6: {}
zustand@5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4):
optionalDependencies:
'@types/react': 19.2.14
immer: 11.1.4
react: 19.2.4
+30
View File
@@ -0,0 +1,30 @@
type BatchCallback = () => void;
export function createEventBatcher(callback: BatchCallback, windowMs = 50) {
let timer: ReturnType<typeof setTimeout> | null = null;
function schedule() {
if (timer !== null) return; // already scheduled -- coalesce
timer = setTimeout(() => {
timer = null;
callback();
}, windowMs);
}
function flush() {
if (timer !== null) {
clearTimeout(timer);
timer = null;
callback();
}
}
function dispose() {
if (timer !== null) {
clearTimeout(timer);
timer = null;
}
}
return { schedule, flush, dispose };
}
+47
View File
@@ -0,0 +1,47 @@
import { useAppStore } from "./store";
// Navigation slice selectors
export const useSection = () => useAppStore((s) => s.section);
export const useSetSection = () => useAppStore((s) => s.setSection);
export const useNavigateTo = () => useAppStore((s) => s.navigateTo);
export const useCommandPaletteOpen = () =>
useAppStore((s) => s.commandPaletteOpen);
export const useConsoleOpen = () => useAppStore((s) => s.consoleOpen);
export const useSidebarCollapsed = () =>
useAppStore((s) => s.sidebarCollapsed);
// Company slice selectors
export const useSnapshot = () => useAppStore((s) => s.snapshot);
export const useCurrentCompanyId = () =>
useAppStore((s) => s.currentCompanyId);
export const useError = () => useAppStore((s) => s.error);
export const useInboxItems = () => useAppStore((s) => s.inboxItems);
export const useCompanyMetrics = () => useAppStore((s) => s.companyMetrics);
export const useStandupReport = () => useAppStore((s) => s.standupReport);
export const useBadges = () => useAppStore((s) => s.badges);
// Selection slice selectors
export const useSelectedTaskId = () => useAppStore((s) => s.selectedTaskId);
export const useSelectedAgentId = () =>
useAppStore((s) => s.selectedAgentId);
export const useSelectedApprovalId = () =>
useAppStore((s) => s.selectedApprovalId);
export const useSelectedRunId = () => useAppStore((s) => s.selectedRunId);
export const useSelectedGoalId = () => useAppStore((s) => s.selectedGoalId);
export const useSelectedProjectId = () =>
useAppStore((s) => s.selectedProjectId);
export const useSelectedWorkspaceId = () =>
useAppStore((s) => s.selectedWorkspaceId);
export const useSelectedConnectorId = () =>
useAppStore((s) => s.selectedConnectorId);
export const useSelectedCommunicationEntityId = () =>
useAppStore((s) => s.selectedCommunicationEntityId);
// UI slice selectors
export const useLocale = () => useAppStore((s) => s.locale);
export const useShowOnboarding = () => useAppStore((s) => s.showOnboarding);
export const useTaskViewMode = () => useAppStore((s) => s.taskViewMode);
// Social slice selectors
export const useSocialAccounts = () => useAppStore((s) => s.socialAccounts);
export const useBrowserActions = () => useAppStore((s) => s.browserActions);
@@ -0,0 +1,17 @@
import type { StateCreator } from "zustand";
import type { AppState } from "../store";
export interface CommunicationSlice {
// Placeholder for future message state -- this slice will be populated
// during component decomposition. The communication component currently
// manages its own internal state.
}
export const createCommunicationSlice: StateCreator<
AppState,
[["zustand/immer", never]],
[],
CommunicationSlice
> = () => ({
// Empty state for now
});
+88
View File
@@ -0,0 +1,88 @@
import type { StateCreator } from "zustand";
import type { AppState } from "../store";
import type {
CompanyMetrics,
InboxItem,
ProfileSnapshot,
StandupReport,
} from "@shared/types";
import type { SidebarBadges } from "@shared/contracts";
export interface CompanySlice {
snapshot: ProfileSnapshot | null;
currentCompanyId: string | null;
companyMetrics: CompanyMetrics | null;
standupReport: StandupReport | null;
inboxItems: InboxItem[];
badges: SidebarBadges | null;
error: string | null;
setSnapshot: (snapshot: ProfileSnapshot | null) => void;
setCurrentCompanyId: (id: string | null) => void;
setCompanyMetrics: (metrics: CompanyMetrics | null) => void;
setStandupReport: (report: StandupReport | null) => void;
setInboxItems: (items: InboxItem[]) => void;
setBadges: (badges: SidebarBadges | null) => void;
setError: (error: string | null) => void;
refreshSnapshot: () => Promise<void>;
}
export const createCompanySlice: StateCreator<
AppState,
[["zustand/immer", never]],
[],
CompanySlice
> = (set) => ({
snapshot: null,
currentCompanyId: null,
companyMetrics: null,
standupReport: null,
inboxItems: [],
badges: null,
error: null,
setSnapshot: (snapshot) =>
set((state) => {
state.snapshot = snapshot;
}),
setCurrentCompanyId: (id) =>
set((state) => {
state.currentCompanyId = id;
}),
setCompanyMetrics: (metrics) =>
set((state) => {
state.companyMetrics = metrics;
}),
setStandupReport: (report) =>
set((state) => {
state.standupReport = report;
}),
setInboxItems: (items) =>
set((state) => {
state.inboxItems = items;
}),
setBadges: (badges) =>
set((state) => {
state.badges = badges;
}),
setError: (error) =>
set((state) => {
state.error = error;
}),
refreshSnapshot: async () => {
try {
const result = await window.agentCompany.loadSnapshot();
if (result.ok) {
set((state) => {
state.snapshot = result.data;
});
} else {
set((state) => {
state.error = result.error.message;
});
}
} catch (err) {
set((state) => {
state.error = err instanceof Error ? err.message : String(err);
});
}
},
});
+86
View File
@@ -0,0 +1,86 @@
import type { StateCreator } from "zustand";
import type { AppState } from "../store";
import type { SectionId } from "@shared/types";
export interface NavigationSlice {
section: SectionId;
commandPaletteOpen: boolean;
commandPaletteSearch: string;
selectedPaletteIndex: number;
consoleOpen: boolean;
sidebarCollapsed: boolean;
companySwitcherOpen: boolean;
setSection: (section: SectionId) => void;
toggleCommandPalette: () => void;
setCommandPaletteSearch: (search: string) => void;
setSelectedPaletteIndex: (index: number) => void;
toggleConsole: () => void;
toggleSidebar: () => void;
toggleCompanySwitcher: () => void;
navigateTo: (section: SectionId, entityId?: string) => void;
}
export const createNavigationSlice: StateCreator<
AppState,
[["zustand/immer", never]],
[],
NavigationSlice
> = (set) => ({
section: "overview",
commandPaletteOpen: false,
commandPaletteSearch: "",
selectedPaletteIndex: 0,
consoleOpen: false,
sidebarCollapsed: false,
companySwitcherOpen: false,
setSection: (section) =>
set((state) => {
state.section = section;
}),
toggleCommandPalette: () =>
set((state) => {
state.commandPaletteOpen = !state.commandPaletteOpen;
if (!state.commandPaletteOpen) {
state.commandPaletteSearch = "";
state.selectedPaletteIndex = 0;
}
}),
setCommandPaletteSearch: (search) =>
set((state) => {
state.commandPaletteSearch = search;
state.selectedPaletteIndex = 0;
}),
setSelectedPaletteIndex: (index) =>
set((state) => {
state.selectedPaletteIndex = index;
}),
toggleConsole: () =>
set((state) => {
state.consoleOpen = !state.consoleOpen;
}),
toggleSidebar: () =>
set((state) => {
state.sidebarCollapsed = !state.sidebarCollapsed;
}),
toggleCompanySwitcher: () =>
set((state) => {
state.companySwitcherOpen = !state.companySwitcherOpen;
}),
navigateTo: (section, entityId) =>
set((state) => {
state.section = section;
if (entityId) {
if (section === "agents") state.selectedAgentId = entityId;
else if (section === "tasks") state.selectedTaskId = entityId;
else if (section === "approvals") state.selectedApprovalId = entityId;
else if (section === "runs") {
state.selectedRunId = entityId;
state.consoleOpen = true;
} else if (section === "goals") state.selectedGoalId = entityId;
else if (section === "projects") state.selectedProjectId = entityId;
else if (section === "connectors") state.selectedConnectorId = entityId;
else if (section === "communication")
state.selectedCommunicationEntityId = entityId;
}
}),
});
+89
View File
@@ -0,0 +1,89 @@
import type { StateCreator } from "zustand";
import type { AppState } from "../store";
export interface SelectionSlice {
selectedTaskId: string | null;
selectedConnectorId: string | null;
selectedApprovalId: string | null;
selectedRunId: string | null;
selectedAgentId: string | null;
selectedGoalId: string | null;
selectedProjectId: string | null;
selectedWorkspaceId: string | null;
selectedCommunicationEntityId: string | null;
setSelectedTaskId: (id: string | null) => void;
setSelectedConnectorId: (id: string | null) => void;
setSelectedApprovalId: (id: string | null) => void;
setSelectedRunId: (id: string | null) => void;
setSelectedAgentId: (id: string | null) => void;
setSelectedGoalId: (id: string | null) => void;
setSelectedProjectId: (id: string | null) => void;
setSelectedWorkspaceId: (id: string | null) => void;
setSelectedCommunicationEntityId: (id: string | null) => void;
clearCompanyScopedSelections: () => void;
}
export const createSelectionSlice: StateCreator<
AppState,
[["zustand/immer", never]],
[],
SelectionSlice
> = (set) => ({
selectedTaskId: null,
selectedConnectorId: null,
selectedApprovalId: null,
selectedRunId: null,
selectedAgentId: null,
selectedGoalId: null,
selectedProjectId: null,
selectedWorkspaceId: null,
selectedCommunicationEntityId: null,
setSelectedTaskId: (id) =>
set((state) => {
state.selectedTaskId = id;
}),
setSelectedConnectorId: (id) =>
set((state) => {
state.selectedConnectorId = id;
}),
setSelectedApprovalId: (id) =>
set((state) => {
state.selectedApprovalId = id;
}),
setSelectedRunId: (id) =>
set((state) => {
state.selectedRunId = id;
}),
setSelectedAgentId: (id) =>
set((state) => {
state.selectedAgentId = id;
}),
setSelectedGoalId: (id) =>
set((state) => {
state.selectedGoalId = id;
}),
setSelectedProjectId: (id) =>
set((state) => {
state.selectedProjectId = id;
}),
setSelectedWorkspaceId: (id) =>
set((state) => {
state.selectedWorkspaceId = id;
}),
setSelectedCommunicationEntityId: (id) =>
set((state) => {
state.selectedCommunicationEntityId = id;
}),
clearCompanyScopedSelections: () =>
set((state) => {
state.selectedTaskId = null;
state.selectedApprovalId = null;
state.selectedRunId = null;
state.selectedAgentId = null;
state.selectedGoalId = null;
state.selectedProjectId = null;
state.selectedWorkspaceId = null;
state.selectedCommunicationEntityId = null;
// selectedConnectorId is NOT cleared -- connectors are global
}),
});
+28
View File
@@ -0,0 +1,28 @@
import type { StateCreator } from "zustand";
import type { AppState } from "../store";
import type { SocialAccountRecord, BrowserActionRecord } from "@shared/types";
export interface SocialSlice {
socialAccounts: SocialAccountRecord[];
browserActions: BrowserActionRecord[];
setSocialAccounts: (accounts: SocialAccountRecord[]) => void;
setBrowserActions: (actions: BrowserActionRecord[]) => void;
}
export const createSocialSlice: StateCreator<
AppState,
[["zustand/immer", never]],
[],
SocialSlice
> = (set) => ({
socialAccounts: [],
browserActions: [],
setSocialAccounts: (accounts) =>
set((state) => {
state.socialAccounts = accounts;
}),
setBrowserActions: (actions) =>
set((state) => {
state.browserActions = actions;
}),
});
+57
View File
@@ -0,0 +1,57 @@
import type { StateCreator } from "zustand";
import type { AppState } from "../store";
import type { AppLocale } from "@shared/locale";
import { DEFAULT_LOCALE } from "@shared/locale";
export interface UiSlice {
locale: AppLocale;
showOnboarding: boolean;
initialOnboardingLocked: boolean;
confirmDeleteCompanyId: string | null;
taskViewMode: "list" | "kanban";
logRevision: number;
setLocale: (locale: AppLocale) => void;
setShowOnboarding: (show: boolean) => void;
setInitialOnboardingLocked: (locked: boolean) => void;
setConfirmDeleteCompanyId: (id: string | null) => void;
setTaskViewMode: (mode: "list" | "kanban") => void;
incrementLogRevision: () => void;
}
export const createUiSlice: StateCreator<
AppState,
[["zustand/immer", never]],
[],
UiSlice
> = (set) => ({
locale: DEFAULT_LOCALE,
showOnboarding: false,
initialOnboardingLocked: false,
confirmDeleteCompanyId: null,
taskViewMode: "list",
logRevision: 0,
setLocale: (locale) =>
set((state) => {
state.locale = locale;
}),
setShowOnboarding: (show) =>
set((state) => {
state.showOnboarding = show;
}),
setInitialOnboardingLocked: (locked) =>
set((state) => {
state.initialOnboardingLocked = locked;
}),
setConfirmDeleteCompanyId: (id) =>
set((state) => {
state.confirmDeleteCompanyId = id;
}),
setTaskViewMode: (mode) =>
set((state) => {
state.taskViewMode = mode;
}),
incrementLogRevision: () =>
set((state) => {
state.logRevision += 1;
}),
});
+42
View File
@@ -0,0 +1,42 @@
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
import { devtools } from "zustand/middleware";
import {
createNavigationSlice,
type NavigationSlice,
} from "./slices/navigation";
import { createCompanySlice, type CompanySlice } from "./slices/company";
import {
createSelectionSlice,
type SelectionSlice,
} from "./slices/selection";
import { createUiSlice, type UiSlice } from "./slices/ui";
import {
createCommunicationSlice,
type CommunicationSlice,
} from "./slices/communication";
import { createSocialSlice, type SocialSlice } from "./slices/social";
export type AppState = NavigationSlice &
CompanySlice &
SelectionSlice &
UiSlice &
CommunicationSlice &
SocialSlice;
export const useAppStore = create<AppState>()(
devtools(
immer((...a) => ({
...createNavigationSlice(...a),
...createCompanySlice(...a),
...createSelectionSlice(...a),
...createUiSlice(...a),
...createCommunicationSlice(...a),
...createSocialSlice(...a),
})),
{
name: "AgentCompany",
enabled: process.env.NODE_ENV === "development",
}
)
);
+73
View File
@@ -0,0 +1,73 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { createEventBatcher } from "@renderer/lib/ipc-batcher";
describe("createEventBatcher", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("calls callback after windowMs timeout (50ms)", () => {
const callback = vi.fn();
const batcher = createEventBatcher(callback, 50);
batcher.schedule();
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(50);
expect(callback).toHaveBeenCalledTimes(1);
});
it("coalesces multiple schedule() calls within window into single callback", () => {
const callback = vi.fn();
const batcher = createEventBatcher(callback, 50);
batcher.schedule();
batcher.schedule();
batcher.schedule();
vi.advanceTimersByTime(50);
expect(callback).toHaveBeenCalledTimes(1);
});
it("flush() immediately invokes callback and clears pending timer", () => {
const callback = vi.fn();
const batcher = createEventBatcher(callback, 50);
batcher.schedule();
batcher.flush();
expect(callback).toHaveBeenCalledTimes(1);
// Advancing time should not trigger another callback
vi.advanceTimersByTime(100);
expect(callback).toHaveBeenCalledTimes(1);
});
it("dispose() cancels pending timer without invoking callback", () => {
const callback = vi.fn();
const batcher = createEventBatcher(callback, 50);
batcher.schedule();
batcher.dispose();
vi.advanceTimersByTime(100);
expect(callback).not.toHaveBeenCalled();
});
it("schedule() after flush() starts new timer", () => {
const callback = vi.fn();
const batcher = createEventBatcher(callback, 50);
batcher.schedule();
batcher.flush();
expect(callback).toHaveBeenCalledTimes(1);
batcher.schedule();
vi.advanceTimersByTime(50);
expect(callback).toHaveBeenCalledTimes(2);
});
});
+220
View File
@@ -0,0 +1,220 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { useAppStore } from "@renderer/state/store";
import type { ProfileSnapshot } from "@shared/types";
// Mock window.agentCompany for refreshSnapshot test
const mockLoadSnapshot = vi.fn();
vi.stubGlobal("window", {
agentCompany: {
loadSnapshot: mockLoadSnapshot,
},
});
describe("useAppStore", () => {
beforeEach(() => {
// Reset store to initial state before each test
useAppStore.setState(useAppStore.getInitialState());
vi.clearAllMocks();
});
it("is created without errors and has all 6 slice properties", () => {
const state = useAppStore.getState();
// NavigationSlice
expect(state).toHaveProperty("section");
expect(state).toHaveProperty("setSection");
expect(state).toHaveProperty("commandPaletteOpen");
expect(state).toHaveProperty("toggleCommandPalette");
// CompanySlice
expect(state).toHaveProperty("snapshot");
expect(state).toHaveProperty("setSnapshot");
expect(state).toHaveProperty("refreshSnapshot");
// SelectionSlice
expect(state).toHaveProperty("selectedTaskId");
expect(state).toHaveProperty("clearCompanyScopedSelections");
// UiSlice
expect(state).toHaveProperty("locale");
expect(state).toHaveProperty("setLocale");
// CommunicationSlice - exists (even if minimal)
expect(state).toBeDefined();
// SocialSlice
expect(state).toHaveProperty("socialAccounts");
expect(state).toHaveProperty("setSocialAccounts");
});
it("NavigationSlice.setSection('tasks') updates section to 'tasks'", () => {
expect(useAppStore.getState().section).toBe("overview");
useAppStore.getState().setSection("tasks");
expect(useAppStore.getState().section).toBe("tasks");
});
it("NavigationSlice.toggleCommandPalette() toggles commandPaletteOpen and clears search on close", () => {
expect(useAppStore.getState().commandPaletteOpen).toBe(false);
// Open
useAppStore.getState().toggleCommandPalette();
expect(useAppStore.getState().commandPaletteOpen).toBe(true);
// Set some search state
useAppStore.getState().setCommandPaletteSearch("test");
useAppStore.getState().setSelectedPaletteIndex(3);
expect(useAppStore.getState().commandPaletteSearch).toBe("test");
expect(useAppStore.getState().selectedPaletteIndex).toBe(3);
// Close -- should clear search and index
useAppStore.getState().toggleCommandPalette();
expect(useAppStore.getState().commandPaletteOpen).toBe(false);
expect(useAppStore.getState().commandPaletteSearch).toBe("");
expect(useAppStore.getState().selectedPaletteIndex).toBe(0);
});
it("NavigationSlice.navigateTo('agents', 'agent-123') sets section to 'agents' and selectedAgentId to 'agent-123'", () => {
useAppStore.getState().navigateTo("agents", "agent-123");
expect(useAppStore.getState().section).toBe("agents");
expect(useAppStore.getState().selectedAgentId).toBe("agent-123");
});
it("SelectionSlice.clearCompanyScopedSelections() nulls all selection IDs", () => {
// Set some selections
useAppStore.getState().setSelectedTaskId("task-1");
useAppStore.getState().setSelectedAgentId("agent-1");
useAppStore.getState().setSelectedApprovalId("approval-1");
useAppStore.getState().setSelectedRunId("run-1");
useAppStore.getState().setSelectedGoalId("goal-1");
useAppStore.getState().setSelectedProjectId("project-1");
useAppStore.getState().setSelectedWorkspaceId("workspace-1");
useAppStore.getState().setSelectedCommunicationEntityId("comm-1");
useAppStore.getState().setSelectedConnectorId("conn-1");
useAppStore.getState().clearCompanyScopedSelections();
const state = useAppStore.getState();
expect(state.selectedTaskId).toBeNull();
expect(state.selectedAgentId).toBeNull();
expect(state.selectedApprovalId).toBeNull();
expect(state.selectedRunId).toBeNull();
expect(state.selectedGoalId).toBeNull();
expect(state.selectedProjectId).toBeNull();
expect(state.selectedWorkspaceId).toBeNull();
expect(state.selectedCommunicationEntityId).toBeNull();
// Connector is NOT cleared per spec
expect(state.selectedConnectorId).toBe("conn-1");
});
it("CompanySlice.setSnapshot(mockSnapshot) stores snapshot correctly", () => {
const mockSnapshot: ProfileSnapshot = {
currentCompanyId: "company-1",
profilePath: "/test/path",
updatesEnabled: true,
theme: "system",
locale: "en",
backend: { secretBackend: "safe_storage" },
companies: [],
connectors: [],
workspaces: [],
agents: [],
goals: [],
projects: [],
tasks: [],
approvals: [],
runs: [],
costs: [],
activity: [],
secrets: [],
comments: [],
socialAccounts: [],
meetings: [],
documents: [],
knowledgeBase: [],
sprints: [],
automationRules: [],
agentMessages: [],
workflows: [],
};
expect(useAppStore.getState().snapshot).toBeNull();
useAppStore.getState().setSnapshot(mockSnapshot);
expect(useAppStore.getState().snapshot).toEqual(mockSnapshot);
});
it("CompanySlice.refreshSnapshot() calls window.agentCompany.loadSnapshot()", async () => {
const mockSnapshot: ProfileSnapshot = {
currentCompanyId: "company-1",
profilePath: "/test/path",
updatesEnabled: true,
theme: "system",
locale: "en",
backend: { secretBackend: "safe_storage" },
companies: [],
connectors: [],
workspaces: [],
agents: [],
goals: [],
projects: [],
tasks: [],
approvals: [],
runs: [],
costs: [],
activity: [],
secrets: [],
comments: [],
socialAccounts: [],
meetings: [],
documents: [],
knowledgeBase: [],
sprints: [],
automationRules: [],
agentMessages: [],
workflows: [],
};
mockLoadSnapshot.mockResolvedValue({ ok: true, data: mockSnapshot });
await useAppStore.getState().refreshSnapshot();
expect(mockLoadSnapshot).toHaveBeenCalledTimes(1);
expect(useAppStore.getState().snapshot).toEqual(mockSnapshot);
});
it("UiSlice.setLocale('zh') updates locale to 'zh'", () => {
expect(useAppStore.getState().locale).toBe("en");
useAppStore.getState().setLocale("zh");
expect(useAppStore.getState().locale).toBe("zh");
});
it("SocialSlice.setSocialAccounts([account]) stores accounts", () => {
const account = {
id: "acc-1",
companyId: "company-1",
platform: "twitter" as const,
accountName: "@test",
displayName: "Test",
profileUrl: "https://twitter.com/test",
credentialSecretId: null,
status: "active" as const,
requireApproval: false,
metadataJson: "{}",
lastUsedAt: null,
createdAt: "2026-01-01",
updatedAt: "2026-01-01",
};
expect(useAppStore.getState().socialAccounts).toEqual([]);
useAppStore.getState().setSocialAccounts([account]);
expect(useAppStore.getState().socialAccounts).toEqual([account]);
});
});