Files
AgentCompany/tests/unit/store.test.ts
T
ZacharyZhang-NY 487f2456ff refactor(badges): derive from snapshot; delete getSidebarBadges IPC chain
Badges were fetched via a dedicated IPC that aggregated counts on the
server, while the same data lived in ProfileSnapshot on the client.
Two paths to the same answer drift whenever one side mutates between
refreshes — the visible symptom being "badge = 1 unread" with an empty
inbox after a message gets marked read in another screen.

Delete the whole server path. Sidebar badges are now derived on the
client from the snapshot via `deriveSidebarBadges(snapshot, companyId)`
in `src/shared/sidebar-badges.ts`. One source of truth; drift is
physically impossible.

Deletions:
- database/company-settings.ts::getSidebarBadges (120-line SQL aggregation)
- database-base.ts::getSidebarBadges pass-through
- ipc-core-read-handlers.ts: handler
- preload/index.ts: bridge binding
- shared/contracts/channels.ts: channel name
- shared/contracts/desktop-api/companies.ts: API signature
- shared/contracts/schemas-badges.ts::sidebarBadgesSchema Zod schema
- renderer/state/slices/company.ts: `badges` field, `setBadges`,
  `refreshBadges`
- useDataLoader: both getSidebarBadges call sites
- useCompanyController: badge reset on company switch
- useDesktopEvents: refreshBadges call on granular patches
- tests/unit/store.test.ts: refreshBadges test (deleted code)

The snapshot's agentMessages already carries board-reader read state
via boardReadReceiptMap (`snapshot.ts:39-47` has always LEFT JOIN'd
agent_message_reads with agent_id = BOARD_READER_ID). So derivation
needs no schema change — the infrastructure was in place, just unused.

fix(communication/MessageList): render orphaned-reply messages at the
top level

The thread-flattening used `filteredMessages.filter(m => !m.parentMessageId)`
to pick top-level messages. When an agent replies to a company/incident
post with a DM (the CEO prompt explicitly instructs this in
wake-reasons/en.ts:119), the DM lands in the direct channel with
parentMessageId pointing to a message that lives in another channel.
That parent is not in `filteredMessages` for the direct channel, so the
reply had no parent to nest under AND was excluded from top-level. The
channel rendered as a blank pane even though exactly one message
matched — the "clicked Dr. X ↔ Dr. Y, got empty" symptom.

Fix: a message is top-level when it either has no parent OR its parent
is outside the current filtered view. Replies with in-view parents
still nest normally.
2026-04-18 21:38:34 -04:00

509 lines
16 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { useAppStore } from "@renderer/state/store";
import type { ProfileSnapshot, TaskRecord, RunRecord, AgentRecord, ApprovalRecord } from "@shared/types";
// Mock window.agentCompany for refreshSnapshot tests. Sidebar badges
// no longer traverse IPC — they are derived from the snapshot in
// `@shared/sidebar-badges`, so there is nothing store-level to mock
// for them.
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]);
});
});
// -- Minimal snapshot fixture for patch tests --
function makeSnapshot(overrides: Partial<ProfileSnapshot> = {}): ProfileSnapshot {
return {
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: [],
...overrides,
};
}
const fakeTask = (id: string, title = "Task"): TaskRecord => ({
id,
companyId: "company-1",
projectId: null,
goalId: null,
parentId: null,
title,
description: "",
assigneeAgentId: null,
workspaceId: null,
status: "open",
priority: "medium",
activeRunId: null,
sessionJson: null,
sessionDisplayId: null,
requiresUserReview: false,
startedAt: null,
completedAt: null,
cancelledAt: null,
createdAt: "2026-01-01",
updatedAt: "2026-01-01",
} as unknown as TaskRecord);
const fakeRun = (id: string): RunRecord => ({
id,
companyId: "company-1",
taskId: "t-1",
agentId: "ag-1",
workspaceId: null,
connectorId: "claude-pro",
status: "queued",
summary: "",
errorMessage: null,
exitCode: null,
signal: null,
model: null,
sessionDisplayId: null,
unattributedCost: false,
startedAt: null,
finishedAt: null,
logPath: null,
createdAt: "2026-01-01",
updatedAt: "2026-01-01",
} as RunRecord);
const fakeAgent = (id: string, name = "Agent"): AgentRecord => ({
id,
companyId: "company-1",
name,
role: "engineer",
title: "",
department: "engineering",
status: "idle",
reportsTo: null,
connectorId: "claude-pro",
workspaceId: null,
model: null,
capabilities: "",
budgetMonthlyUsd: 100,
spentMonthlyUsd: 0,
metadataJson: "{}",
heartbeatEnabled: false,
heartbeatIntervalSec: 0,
lastHeartbeatAt: null,
createdAt: "2026-01-01",
updatedAt: "2026-01-01",
} as AgentRecord);
const fakeApproval = (id: string): ApprovalRecord => ({
id,
companyId: "company-1",
relatedTaskId: "t-1",
relatedAgentId: null,
requestedByAgentId: "ag-1",
type: "task_completion",
payloadSummary: "",
impactSummary: "",
payloadJson: null,
decisionNote: null,
state: "pending",
createdAt: "2026-01-01",
updatedAt: "2026-01-01",
} as unknown as ApprovalRecord);
describe("CompanySlice patch methods", () => {
beforeEach(() => {
useAppStore.setState(useAppStore.getInitialState());
vi.clearAllMocks();
});
// -- patchTask --
it("patchTask inserts new task at beginning when task ID is not found", () => {
const snapshot = makeSnapshot({ tasks: [fakeTask("t-existing")] });
useAppStore.getState().setSnapshot(snapshot);
const newTask = fakeTask("t-new", "New Task");
useAppStore.getState().patchTask(newTask);
const tasks = useAppStore.getState().snapshot!.tasks;
expect(tasks).toHaveLength(2);
expect(tasks[0]!.id).toBe("t-new");
expect(tasks[1]!.id).toBe("t-existing");
});
it("patchTask updates existing task in-place when task ID matches", () => {
const snapshot = makeSnapshot({ tasks: [fakeTask("t-1", "Original")] });
useAppStore.getState().setSnapshot(snapshot);
const updated = fakeTask("t-1", "Updated");
useAppStore.getState().patchTask(updated);
const tasks = useAppStore.getState().snapshot!.tasks;
expect(tasks).toHaveLength(1);
expect(tasks[0]!.title).toBe("Updated");
});
it("removeTask filters out task by ID", () => {
const snapshot = makeSnapshot({ tasks: [fakeTask("t-1"), fakeTask("t-2")] });
useAppStore.getState().setSnapshot(snapshot);
useAppStore.getState().removeTask("t-1");
const tasks = useAppStore.getState().snapshot!.tasks;
expect(tasks).toHaveLength(1);
expect(tasks[0]!.id).toBe("t-2");
});
// -- patchRun --
it("patchRun inserts new run when run ID is not found", () => {
const snapshot = makeSnapshot({ runs: [fakeRun("r-existing")] });
useAppStore.getState().setSnapshot(snapshot);
useAppStore.getState().patchRun(fakeRun("r-new"));
const runs = useAppStore.getState().snapshot!.runs;
expect(runs).toHaveLength(2);
expect(runs[0]!.id).toBe("r-new");
});
it("patchRun updates existing run in-place", () => {
const run = fakeRun("r-1");
const snapshot = makeSnapshot({ runs: [run] });
useAppStore.getState().setSnapshot(snapshot);
const updated = { ...fakeRun("r-1"), status: "running" as const };
useAppStore.getState().patchRun(updated);
const runs = useAppStore.getState().snapshot!.runs;
expect(runs).toHaveLength(1);
expect(runs[0]!.status).toBe("running");
});
it("removeRun filters out run by ID", () => {
const snapshot = makeSnapshot({ runs: [fakeRun("r-1"), fakeRun("r-2")] });
useAppStore.getState().setSnapshot(snapshot);
useAppStore.getState().removeRun("r-1");
const runs = useAppStore.getState().snapshot!.runs;
expect(runs).toHaveLength(1);
expect(runs[0]!.id).toBe("r-2");
});
// -- patchAgent --
it("patchAgent inserts new agent when agent ID is not found", () => {
const snapshot = makeSnapshot({ agents: [fakeAgent("ag-existing")] });
useAppStore.getState().setSnapshot(snapshot);
useAppStore.getState().patchAgent(fakeAgent("ag-new", "New Agent"));
const agents = useAppStore.getState().snapshot!.agents;
expect(agents).toHaveLength(2);
expect(agents[0]!.id).toBe("ag-new");
});
it("patchAgent updates existing agent in-place", () => {
const snapshot = makeSnapshot({ agents: [fakeAgent("ag-1", "Old")] });
useAppStore.getState().setSnapshot(snapshot);
useAppStore.getState().patchAgent(fakeAgent("ag-1", "New"));
const agents = useAppStore.getState().snapshot!.agents;
expect(agents).toHaveLength(1);
expect(agents[0]!.name).toBe("New");
});
it("removeAgent filters out agent by ID", () => {
const snapshot = makeSnapshot({ agents: [fakeAgent("ag-1"), fakeAgent("ag-2")] });
useAppStore.getState().setSnapshot(snapshot);
useAppStore.getState().removeAgent("ag-1");
const agents = useAppStore.getState().snapshot!.agents;
expect(agents).toHaveLength(1);
expect(agents[0]!.id).toBe("ag-2");
});
// -- patchApproval --
it("patchApproval inserts new approval when approval ID is not found", () => {
const snapshot = makeSnapshot({ approvals: [fakeApproval("ap-existing")] });
useAppStore.getState().setSnapshot(snapshot);
useAppStore.getState().patchApproval(fakeApproval("ap-new"));
const approvals = useAppStore.getState().snapshot!.approvals;
expect(approvals).toHaveLength(2);
expect(approvals[0]!.id).toBe("ap-new");
});
it("patchApproval updates existing approval in-place", () => {
const approval = fakeApproval("ap-1");
const snapshot = makeSnapshot({ approvals: [approval] });
useAppStore.getState().setSnapshot(snapshot);
const updated = { ...fakeApproval("ap-1"), state: "approved" as const };
useAppStore.getState().patchApproval(updated);
const approvals = useAppStore.getState().snapshot!.approvals;
expect(approvals).toHaveLength(1);
expect(approvals[0]!.state).toBe("approved");
});
it("removeApproval filters out approval by ID", () => {
const snapshot = makeSnapshot({ approvals: [fakeApproval("ap-1"), fakeApproval("ap-2")] });
useAppStore.getState().setSnapshot(snapshot);
useAppStore.getState().removeApproval("ap-1");
const approvals = useAppStore.getState().snapshot!.approvals;
expect(approvals).toHaveLength(1);
expect(approvals[0]!.id).toBe("ap-2");
});
// -- null-snapshot safety --
it("patch methods are no-ops when snapshot is null", () => {
expect(useAppStore.getState().snapshot).toBeNull();
// These should not crash
useAppStore.getState().patchTask(fakeTask("t-1"));
useAppStore.getState().removeTask("t-1");
useAppStore.getState().patchRun(fakeRun("r-1"));
useAppStore.getState().removeRun("r-1");
useAppStore.getState().patchAgent(fakeAgent("ag-1"));
useAppStore.getState().removeAgent("ag-1");
useAppStore.getState().patchApproval(fakeApproval("ap-1"));
useAppStore.getState().removeApproval("ap-1");
expect(useAppStore.getState().snapshot).toBeNull();
});
});